Print this page
OS-2547 VM.js should protect writes to configuration files with a lockfile
Split |
Close |
Expand all |
Collapse all |
--- old/src/vm/node_modules/VM.js
+++ new/src/vm/node_modules/VM.js
1 1 /*
2 2 * CDDL HEADER START
3 3 *
4 4 * The contents of this file are subject to the terms of the
5 5 * Common Development and Distribution License, Version 1.0 only
6 6 * (the "License"). You may not use this file except in compliance
7 7 * with the License.
8 8 *
9 9 * You can obtain a copy of the license at http://smartos.org/CDDL
10 10 *
11 11 * See the License for the specific language governing permissions
12 12 * and limitations under the License.
13 13 *
14 14 * When distributing Covered Code, include this CDDL HEADER in each
15 15 * file.
16 16 *
17 17 * If applicable, add the following below this CDDL HEADER, with the
18 18 * fields enclosed by brackets "[]" replaced with your own identifying
19 19 * information: Portions Copyright [yyyy] [name of copyright owner]
20 20 *
21 21 * CDDL HEADER END
22 22 *
23 23 * Copyright (c) 2013, Joyent, Inc. All rights reserved.
24 24 *
25 25 * Experimental functions, expect these interfaces to be unstable and
26 26 * potentially go away entirely:
27 27 *
28 28 * create_snapshot(uuid, snapname, options, callback)
29 29 * delete_snapshot(uuid, snapname, options, callback)
30 30 * install(uuid, callback)
31 31 * receive(target, options, callback)
32 32 * reprovision(uuid, payload, options, callback)
33 33 * rollback_snapshot(uuid, snapname, options, callback)
34 34 * send(uuid, where, options, callback)
35 35 * getSysinfo(args, callback)
36 36 * validate(brand, action, payload, callback)
37 37 * waitForZoneState(payload, state, options, callback)
38 38 *
39 39 * Exported functions:
40 40 *
41 41 * console(uuid, callback)
42 42 * create(properties, callback)
43 43 * delete(uuid, callback)
44 44 * flatten(vmobj, key)
45 45 * info(uuid, types, callback)
46 46 * load([zonename|uuid], callback)
47 47 * lookup(match, callback)
48 48 * reboot(uuid, options={[force=true]}, callback)
49 49 * start(uuid, extra, callback)
50 50 * stop(uuid, options={[force=true]}, callback)
51 51 * sysrq(uuid, req=[nmi|screenshot], options={}, callback)
52 52 * update(uuid, properties, callback)
53 53 *
54 54 * Exported variables:
55 55 *
56 56 * logname - you can set this to a string [a-zA-Z_] to use as log name
57 57 * logger - you can set this to a node-bunyan log stream to capture the logs
58 58 * INFO_TYPES - list of supported types for the info command
59 59 * SYSRQ_TYPES - list of supported requests for sysrq
↓ open down ↓ |
59 lines elided |
↑ open up ↑ |
60 60 */
61 61
62 62 // Ensure we're using the platform's node
63 63 require('/usr/node/node_modules/platform_node_version').assert();
64 64
65 65 var assert = require('assert');
66 66 var async = require('/usr/node/node_modules/async');
67 67 var bunyan = require('/usr/node/node_modules/bunyan');
68 68 var cp = require('child_process');
69 69 var dladm = require('/usr/vm/node_modules/dladm');
70 +var lock = require('/usr/vm/node_modules/locker').lock;
70 71 var EventEmitter = require('events').EventEmitter;
71 72 var exec = cp.exec;
72 73 var execFile = cp.execFile;
73 74 var expat = require('/usr/node/node_modules/node-expat');
74 75 var fs = require('fs');
75 76 var fw = require('/usr/fw/lib/fw');
76 77 var http = require('http');
77 78 var net = require('net');
78 79 var path = require('path');
79 80 var Qmp = require('/usr/vm/node_modules/qmp').Qmp;
80 81 var spawn = cp.spawn;
81 82 var sprintf = require('/usr/node/node_modules/sprintf').sprintf;
82 83 var tty = require('tty');
83 84 var util = require('util');
84 85
85 86 var log_to_file = false;
86 87
87 88 // keep the last 512 messages just in case we end up wanting them.
88 89 var ringbuffer = new bunyan.RingBuffer({ limit: 512 });
89 90
90 91 // zfs_list_queue variables for the serialization of 'zfs list' calls
91 92 var zfs_list_in_progress = {};
92 93 var zfs_list_queue;
93 94
94 95 // global handle for the zoneevent watcher
95 96 var zoneevent;
96 97
97 98 /*
98 99 * zone states from libzonecfg/common/zonecfg_impl.h
99 100 *
100 101 * #define ZONE_STATE_STR_CONFIGURED "configured"
101 102 * #define ZONE_STATE_STR_INCOMPLETE "incomplete"
102 103 * #define ZONE_STATE_STR_INSTALLED "installed"
103 104 * #define ZONE_STATE_STR_READY "ready"
104 105 * #define ZONE_STATE_STR_MOUNTED "mounted"
105 106 * #define ZONE_STATE_STR_RUNNING "running"
106 107 * #define ZONE_STATE_STR_SHUTTING_DOWN "shutting_down"
107 108 * #define ZONE_STATE_STR_DOWN "down"
108 109 *
109 110 */
110 111
111 112 exports.FLATTENABLE_ARRAYS = [
112 113 'resolvers'
113 114 ];
114 115 exports.FLATTENABLE_ARRAY_HASH_KEYS = [
115 116 'disks',
116 117 'nics'
117 118 ];
118 119 exports.FLATTENABLE_HASH_KEYS = [
119 120 'customer_metadata',
120 121 'internal_metadata',
121 122 'routes',
122 123 'tags'
123 124 ];
124 125
125 126 var DEFAULT_MDATA_TIMEOUT = 300;
126 127 var DISABLED = 0;
127 128 var MAX_SNAPNAME_LENGTH = 64;
128 129 var MINIMUM_MAX_SWAP = 256;
129 130 var PROVISION_TIMEOUT = 300;
130 131 var STOP_TIMEOUT = 60;
131 132 var VM = this;
132 133
133 134 VM.log = null;
134 135
135 136 // can be (re)set by loader before we start.
136 137 exports.logger = null;
137 138 exports.loglevel = 'debug';
138 139
139 140 // OpenOnErrorFileStream is a bunyan stream that only creates the file when
140 141 // there's an error or higher level message or when the global log_to_file
141 142 // variable is set. For actions that modify things log_to_file is always set.
142 143 // For other actions we shouldn't log in the normal case but where we do want
143 144 // logs when something breaks. Thanks to Trent++ for most of this code.
144 145 //
145 146 // Note: if you want to rotate the logs while this is writing to a file, you
146 147 // can first move it. The watcher will notice that the log file was moved and
147 148 // reopen a new file with the original name.
148 149
149 150 function OpenOnErrorFileStream(filename) {
150 151 this.path = filename;
151 152 this.write = this.constructor.prototype.write1;
152 153 this.end = this.constructor.prototype.end1;
153 154 this.emit = this.constructor.prototype.emit1;
154 155 this.once = this.constructor.prototype.once1;
155 156
156 157 this.newStream = function () {
157 158 var self = this;
158 159 var watcher;
159 160
160 161 self.stream = fs.createWriteStream(self.path,
161 162 {flags: 'a', encoding: 'utf8'});
162 163
163 164 watcher = fs.watch(self.path, {persistent: false}, function (evt) {
164 165 if (evt != 'rename') {
165 166 return;
166 167 }
167 168 // file was renamed, we want to reopen.
168 169 if (self.stream) {
169 170 self.stream.destroySoon();
170 171 }
171 172 watcher.close();
172 173 self.stream = null;
173 174 });
174 175 };
175 176 }
176 177
177 178 OpenOnErrorFileStream.prototype.end1 = function () {
178 179 // in initial mode we're not writing anything, so nothing to flush
179 180 return;
180 181 };
181 182
182 183 OpenOnErrorFileStream.prototype.emit1 = function () {
183 184 return;
184 185 };
185 186
186 187 // Warning: never emits anything
187 188 OpenOnErrorFileStream.prototype.once1 = function () {
188 189 return;
189 190 };
190 191
191 192 // used until first ERROR or higher, then opens file and ensures future writes
192 193 // go to .write2()
193 194 OpenOnErrorFileStream.prototype.write1 = function (rec) {
194 195 var r;
195 196 var stream;
196 197
197 198 if (rec.level >= bunyan.ERROR || log_to_file) {
198 199 if (! this.stream) {
199 200 this.newStream();
200 201 }
201 202
202 203 stream = this.stream;
203 204
204 205 this.emit = function () { stream.emit.apply(stream, arguments); };
205 206 this.end = function () { stream.end.apply(stream, arguments); };
206 207 this.once = function () { stream.once.apply(stream, arguments); };
207 208 this.write = this.constructor.prototype.write2;
208 209 // dump out logs from ringbuffer too since there was an error so we can
209 210 // figure out what's going on.
210 211 for (r in ringbuffer.records) {
211 212 r = ringbuffer.records[r];
212 213 if (r != rec) {
213 214 this.write(r);
214 215 }
215 216 }
216 217
217 218 this.write(rec);
218 219 }
219 220
220 221 // This write doesn't fail (since it's going to memory or nowhere) so we
221 222 // always return true so that callers don't try to wait for 'drain' which
222 223 // we'll not emit.
223 224 return true;
224 225 };
225 226
226 227 // used when writing to file
227 228 OpenOnErrorFileStream.prototype.write2 = function (rec) {
228 229 var str;
229 230
230 231 // need to support writing '' so we know when to drain
231 232 if (typeof (rec) === 'string' && rec.length < 1) {
232 233 str = '';
233 234 } else {
234 235 str = JSON.stringify(rec, bunyan.safeCycles()) + '\n';
235 236 }
236 237
237 238 if (! this.stream) {
238 239 this.newStream();
239 240 }
240 241
241 242 return this.stream.write(str);
242 243 };
243 244
244 245 // This function should be called by any exported function from this module.
245 246 // It ensures that a logger is setup. If side_effects is true, we'll start
246 247 // writing log messages to the file right away. If not, we'll only start
247 248 // logging after we hit a message error or higher. This is intended such that
248 249 // things that are expected to change the state or modify VMs on the system:
249 250 // eg. create, start, stop, delete should have this set true. It should be
250 251 // set false when the action should not cause changes to the system:
251 252 // eg.: load, lookup, info, console, &c.
252 253 function ensureLogging(side_effects)
253 254 {
254 255 side_effects = !!side_effects; // make it boolean (undef === false)
255 256
256 257 var filename;
257 258 var logname;
258 259 var streams = [];
259 260
260 261 function start_logging() {
261 262 var params = {
262 263 name: logname,
263 264 streams: streams,
264 265 serializers: bunyan.stdSerializers
265 266 };
266 267
267 268 if (process.env.REQ_ID) {
268 269 params.req_id = process.env.REQ_ID;
269 270 } else if (process.env.req_id) {
270 271 params.req_id = process.env.req_id;
271 272 }
272 273 VM.log = bunyan.createLogger(params);
273 274 }
274 275
275 276 // This is here in case an app calls a lookup first and then a create. The
276 277 // logger will get created in no-sideeffects mode for the lookup but when
277 278 // the create is called this will force the switch to writing.
278 279 if (side_effects) {
279 280 log_to_file = true;
280 281 }
281 282
282 283 if (VM.log) {
283 284 // We're already logging, don't break things.
284 285 return;
285 286 }
286 287
287 288 if (VM.hasOwnProperty('logname')) {
288 289 logname = VM.logname.replace(/[^a-zA-Z\_]/g, '');
289 290 }
290 291 if (!logname || logname.length < 1) {
291 292 logname = 'VM';
292 293 }
293 294
294 295 if (VM.hasOwnProperty('logger') && VM.logger) {
295 296 // Use concat, in case someone's sneaky and makes more than one logger.
296 297 // We don't officially support that yet though.
297 298 streams = streams.concat(VM.logger);
298 299 }
299 300
300 301 // Add the ringbuffer which we'll dump if we switch from not writing to
301 302 // writing, and so that they'll show up in dumps.
302 303 streams.push({
303 304 level: 'trace',
304 305 type: 'raw',
305 306 stream: ringbuffer
306 307 });
307 308
308 309 try {
309 310 if (!fs.existsSync('/var/log/vm')) {
310 311 fs.mkdirSync('/var/log/vm');
311 312 }
312 313 if (!fs.existsSync('/var/log/vm/logs')) {
313 314 fs.mkdirSync('/var/log/vm/logs');
314 315 }
315 316 } catch (e) {
316 317 // We can't ever log to a file in /var/log/vm/logs if we can't create
317 318 // it, so we just log to ring buffer (above).
318 319 start_logging();
319 320 return;
320 321 }
321 322
322 323 filename = '/var/log/vm/logs/' + Date.now(0) + '-'
323 324 + sprintf('%06d', process.pid) + '-' + logname + '.log';
324 325
325 326 streams.push({
326 327 type: 'raw',
327 328 stream: new OpenOnErrorFileStream(filename),
328 329 level: VM.loglevel
329 330 });
330 331
331 332 start_logging();
332 333 }
333 334
334 335 function ltrim(str, chars)
335 336 {
336 337 chars = chars || '\\s';
337 338 str = str || '';
338 339 return str.replace(new RegExp('^[' + chars + ']+', 'g'), '');
339 340 }
340 341
341 342 function rtrim(str, chars)
342 343 {
343 344 chars = chars || '\\s';
344 345 str = str || '';
345 346 return str.replace(new RegExp('[' + chars + ']+$', 'g'), '');
346 347 }
347 348
348 349 function trim(str, chars)
349 350 {
350 351 return ltrim(rtrim(str, chars), chars);
351 352 }
352 353
353 354 function isUUID(str) {
354 355 var re = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
355 356 if (str && str.length === 36 && str.match(re)) {
356 357 return true;
357 358 } else {
358 359 return false;
359 360 }
360 361 }
361 362
362 363 function fixBoolean(str)
363 364 {
364 365 if (str === 'true') {
365 366 return true;
366 367 } else if (str === 'false') {
367 368 return false;
368 369 } else {
369 370 return str;
370 371 }
371 372 }
372 373
373 374 function fixBooleanLoose(str)
374 375 {
375 376 if (str === 'true' || str === '1' || str === 1) {
376 377 return true;
377 378 } else if (str === 'false' || str === '0' || str === 0) {
378 379 return false;
379 380 } else {
380 381 return str;
381 382 }
382 383 }
383 384
384 385 function isCIDR(str) {
385 386 if (typeof (str) !== 'string') {
386 387 return false;
387 388 }
388 389 var parts = str.split('/');
389 390 if (parts.length !== 2 || !net.isIPv4(parts[0])) {
390 391 return false;
391 392 }
392 393
393 394 var size = Number(parts[1]);
394 395 if (!size || size < 8 || size > 32) {
395 396 return false;
396 397 }
397 398
398 399 return true;
399 400 }
400 401
401 402 // IMPORTANT:
402 403 //
403 404 // Some of these properties get translated below into backward compatible
404 405 // names.
405 406 //
406 407
407 408 var UPDATABLE_NIC_PROPS = [
408 409 'primary',
409 410 'nic_tag',
410 411 'vrrp_vrid',
411 412 'vrrp_primary_ip',
412 413 'blocked_outgoing_ports',
413 414 'mac',
414 415 'gateway',
415 416 'ip',
416 417 'model',
417 418 'netmask',
418 419 'network_uuid',
419 420 'dhcp_server',
420 421 'allow_dhcp_spoofing',
421 422 'allow_ip_spoofing',
422 423 'allow_mac_spoofing',
423 424 'allow_restricted_traffic',
424 425 'allow_unfiltered_promisc',
425 426 'vlan_id'
426 427 ];
427 428
428 429 var UPDATABLE_DISK_PROPS = [
429 430 'boot',
430 431 'model'
431 432 ];
432 433
433 434 // Note: this doesn't include 'state' because of 'stopping' which is a virtual
434 435 // state and therefore lookups would be wrong (because they'd search on real
435 436 // state).
436 437 var QUICK_LOOKUP = [
437 438 'zoneid',
438 439 'zonename',
439 440 'zonepath',
440 441 'uuid',
441 442 'brand',
442 443 'ip_type'
443 444 ];
444 445
445 446 exports.DISK_MODELS = [
446 447 'virtio',
447 448 'ide',
448 449 'scsi'
449 450 ];
450 451
451 452 exports.VGA_TYPES = [
452 453 'cirrus',
453 454 'std',
454 455 'vmware',
455 456 'qxl',
456 457 'xenfb'
457 458 ];
458 459
459 460 exports.INFO_TYPES = [
460 461 'all',
461 462 'block',
462 463 'blockstats',
463 464 'chardev',
464 465 'cpus',
465 466 'kvm',
466 467 'pci',
467 468 'spice',
468 469 'status',
469 470 'version',
470 471 'vnc'
471 472 ];
472 473
473 474 exports.SYSRQ_TYPES = [
474 475 'nmi',
475 476 'screenshot'
476 477 ];
477 478
478 479 exports.COMPRESSION_TYPES = [
479 480 'on',
480 481 'off',
481 482 'lzjb',
482 483 'gzip',
483 484 'gzip-1',
484 485 'gzip-2',
485 486 'gzip-3',
486 487 'gzip-4',
487 488 'gzip-5',
488 489 'gzip-6',
489 490 'gzip-7',
490 491 'gzip-8',
491 492 'gzip-9',
492 493 'zle'
493 494 ];
494 495
495 496 exports.KVM_MEM_OVERHEAD = 1024;
496 497 exports.KVM_MIN_MEM_OVERHEAD = 256;
497 498
498 499 var XML_PROPERTIES = {
499 500 'zone': {
500 501 'name': 'zonename',
501 502 'zonepath': 'zonepath',
502 503 'autoboot': 'autoboot',
503 504 'brand': 'brand',
504 505 'limitpriv': 'limit_priv',
505 506 'fs-allowed': 'fs_allowed'
506 507 },
507 508 'zone.attr': {
508 509 'alias': 'alias',
509 510 'archive-on-delete': 'archive_on_delete',
510 511 'billing-id': 'billing_id',
511 512 'boot': 'boot',
512 513 'cpu-type': 'cpu_type',
513 514 'create-timestamp': 'create_timestamp',
514 515 'dataset-uuid': 'image_uuid',
515 516 'default-gateway': 'default_gateway',
516 517 'disk-driver': 'disk_driver',
517 518 'dns-domain': 'dns_domain',
518 519 'do-not-inventory': 'do_not_inventory',
519 520 'failed': 'failed',
520 521 'firewall-enabled': 'firewall_enabled',
521 522 'hostname': 'hostname',
522 523 'init-name': 'init_name',
523 524 'never-booted': 'never_booted',
524 525 'nic-driver': 'nic_driver',
525 526 'owner-uuid': 'owner_uuid',
526 527 'package-name': 'package_name',
527 528 'package-version': 'package_version',
528 529 'qemu-extra-opts': 'qemu_extra_opts',
529 530 'qemu-opts': 'qemu_opts',
530 531 'ram': 'ram',
531 532 'restart-init': 'restart_init',
532 533 'resolvers': 'resolvers',
533 534 'spice-opts': 'spice_opts',
534 535 'spice-password': 'spice_password',
535 536 'spice-port': 'spice_port',
536 537 'tmpfs': 'tmpfs',
537 538 'transition': 'transition',
538 539 'vcpus': 'vcpus',
539 540 'vga': 'vga',
540 541 'virtio-txtimer': 'virtio_txtimer',
541 542 'virtio-txburst': 'virtio_txburst',
542 543 'vm-version': 'v',
543 544 'vm-autoboot': 'vm_autoboot',
544 545 'vnc-password': 'vnc_password',
545 546 'vnc-port': 'vnc_port'
546 547 },
547 548 'zone.rctl.zone.cpu-shares.rctl-value': {
548 549 'limit': 'cpu_shares'
549 550 },
550 551 'zone.rctl.zone.cpu-cap.rctl-value': {
551 552 'limit': 'cpu_cap'
552 553 },
553 554 'zone.rctl.zone.zfs-io-priority.rctl-value': {
554 555 'limit': 'zfs_io_priority'
555 556 },
556 557 'zone.rctl.zone.max-lwps.rctl-value': {
557 558 'limit': 'max_lwps'
558 559 },
559 560 'zone.rctl.zone.max-physical-memory.rctl-value': {
560 561 'limit': 'max_physical_memory'
561 562 },
562 563 'zone.rctl.zone.max-locked-memory.rctl-value': {
563 564 'limit': 'max_locked_memory'
564 565 },
565 566 'zone.rctl.zone.max-swap.rctl-value': {
566 567 'limit': 'max_swap'
567 568 },
568 569 'nic': {
569 570 'ip': 'ip',
570 571 'mac-addr': 'mac',
571 572 'physical': 'interface',
572 573 'vlan-id': 'vlan_id',
573 574 'global-nic': 'nic_tag',
574 575 'dhcp_server': 'dhcp_server',
575 576 'allow_dhcp_spoofing': 'allow_dhcp_spoofing',
576 577 'allow_ip_spoofing': 'allow_ip_spoofing',
577 578 'allow_mac_spoofing': 'allow_mac_spoofing',
578 579 'allow_restricted_traffic': 'allow_restricted_traffic',
579 580 'allow_unfiltered_promisc': 'allow_unfiltered_promisc',
580 581 'allowed_ips': 'allowed_ips',
581 582 'netmask': 'netmask',
582 583 'network_uuid': 'network_uuid',
583 584 'model': 'model',
584 585 'gateway': 'gateway',
585 586 'primary': 'primary',
586 587 'vrrp_vrid': 'vrrp_vrid',
587 588 'vrrp_primary_ip': 'vrrp_primary_ip',
588 589 'blocked-outgoing-ports': 'blocked_outgoing_ports'
589 590 },
590 591 'filesystem': {
591 592 'special': 'source',
592 593 'directory': 'target',
593 594 'type': 'type',
594 595 'raw': 'raw'
595 596 },
596 597 'disk': {
597 598 'boot': 'boot',
598 599 'image-size': 'image_size',
599 600 'image-name': 'image_name',
600 601 'image-uuid': 'image_uuid',
601 602 'match': 'path',
602 603 'media': 'media',
603 604 'model': 'model',
604 605 'size': 'size'
605 606 }
606 607 };
607 608
608 609 /*
609 610 * This allows one to define a function that will be run over the values from
610 611 * the zonecfg at the point where we transform that data into a VM object.
611 612 *
612 613 */
613 614 var XML_PROPERTY_TRANSFORMS = {
614 615 'alias': unbase64,
615 616 'archive_on_delete': fixBoolean,
616 617 'autoboot': fixBoolean,
617 618 'cpu_cap': numberify,
618 619 'cpu_shares': numberify,
619 620 'disks': {
620 621 'boot': fixBoolean,
621 622 'image_size': numberify,
622 623 'size': numberify
623 624 },
624 625 'do_not_inventory': fixBoolean,
625 626 'firewall_enabled': fixBoolean,
626 627 'max_locked_memory': unmangleMem,
627 628 'max_lwps': numberify,
628 629 'max_physical_memory': unmangleMem,
629 630 'max_swap': unmangleMem,
630 631 'never_booted': fixBoolean,
631 632 'nics': {
632 633 'dhcp_server': fixBoolean,
633 634 'allow_dhcp_spoofing': fixBoolean,
634 635 'allow_ip_spoofing': fixBoolean,
635 636 'allow_mac_spoofing': fixBoolean,
636 637 'allow_restricted_traffic': fixBoolean,
637 638 'allow_unfiltered_promisc': fixBoolean,
638 639 'allowed_ips': separateCommas,
639 640 'primary': fixBooleanLoose,
640 641 'vrrp_vrid': numberify,
641 642 'vlan_id': numberify
642 643 },
643 644 'qemu_extra_opts': unbase64,
644 645 'qemu_opts': unbase64,
645 646 'ram': numberify,
646 647 'restart_init': fixBoolean,
647 648 'resolvers': separateCommas,
648 649 'spice_password': unbase64,
649 650 'spice_port': numberify,
650 651 'spice_opts': unbase64,
651 652 'tmpfs': numberify,
652 653 'v': numberify,
653 654 'vcpus': numberify,
654 655 'virtio_txburst': numberify,
655 656 'virtio_txtimer': numberify,
656 657 'vnc_password': unbase64,
657 658 'vnc_port': numberify,
658 659 'zfs_io_priority': numberify,
659 660 'zoneid': numberify
660 661 };
661 662
662 663 /*
663 664 * This defines all of the possible properties that could be in a create/update
664 665 * payload and their types. Each of the entries are required to have at least
665 666 * a 'type' property which is one of:
666 667 *
667 668 * object-array -- an array of objects
668 669 * boolean -- true or false
669 670 * flat-object -- an object that has only string properties
670 671 * integer -- integers only
671 672 * list -- Either comma separated or array list of strings
672 673 * string -- Simple string
673 674 * uuid -- A standard 00000000-0000-0000-0000-000000000000 type uuid
674 675 * zpool -- The name of an existing zpool
675 676 *
676 677 */
677 678 var PAYLOAD_PROPERTIES = {
678 679 'add_disks': {'type': 'object-array', 'check_as': 'disks'},
679 680 'add_nics': {'type': 'object-array', 'check_as': 'nics'},
680 681 'alias': {'type': 'string'},
681 682 'archive_on_delete': {'type': 'boolean'},
682 683 'autoboot': {'type': 'boolean'},
683 684 'billing_id': {'type': 'string'},
684 685 'boot': {'type': 'string'},
685 686 'brand': {'type': 'string'},
686 687 'cpu_cap': {'type': 'integer'},
687 688 'cpu_shares': {'type': 'integer'},
688 689 'cpu_type': {'type': 'string'},
689 690 'create_only': {'type': 'boolean'},
690 691 'create_timestamp': {'type': 'string'},
691 692 'customer_metadata': {'type': 'flat-object'},
692 693 'dataset_uuid': {'type': 'uuid'},
693 694 'delegate_dataset': {'type': 'boolean'},
694 695 'disks': {'type': 'object-array'},
695 696 'disks.*.block_size': {'type': 'integer'},
696 697 'disks.*.boot': {'type': 'boolean'},
697 698 'disks.*.compression': {'type': 'string'},
698 699 'disks.*.image_name': {'type': 'string'},
699 700 'disks.*.image_size': {'type': 'integer'},
700 701 'disks.*.image_uuid': {'type': 'uuid'},
701 702 'disks.*.refreservation': {'type': 'integer'},
702 703 'disks.*.size': {'type': 'integer'},
703 704 'disks.*.media': {'type': 'string'},
704 705 'disks.*.model': {'type': 'string'},
705 706 'disks.*.nocreate': {'type': 'boolean'},
706 707 'disks.*.path': {'type': 'string'},
707 708 'disks.*.zpool': {'type': 'zpool'},
708 709 'disk_driver': {'type': 'string'},
709 710 'do_not_inventory': {'type': 'boolean'},
710 711 'dns_domain': {'type': 'string'},
711 712 'filesystems': {'type': 'object-array'},
712 713 'filesystems.*.type': {'type': 'string'},
713 714 'filesystems.*.source': {'type': 'string'},
714 715 'filesystems.*.target': {'type': 'string'},
715 716 'filesystems.*.raw': {'type': 'string'},
716 717 'filesystems.*.options': {'type': 'list'},
717 718 'firewall': {'type': 'object'},
718 719 'firewall_enabled': {'type': 'boolean'},
719 720 'fs_allowed': {'type': 'list'},
720 721 'hostname': {'type': 'string'},
721 722 'image_uuid': {'type': 'uuid'},
722 723 'init_name': {'type': 'string'},
723 724 'internal_metadata': {'type': 'flat-object'},
724 725 'limit_priv': {'type': 'list'},
725 726 'max_locked_memory': {'type': 'integer'},
726 727 'max_lwps': {'type': 'integer'},
727 728 'max_physical_memory': {'type': 'integer'},
728 729 'max_swap': {'type': 'integer'},
729 730 'mdata_exec_timeout': {'type': 'integer'},
730 731 'nics': {'type': 'object-array'},
731 732 'nics.*.allow_dhcp_spoofing': {'type': 'boolean'},
732 733 'nics.*.allow_ip_spoofing': {'type': 'boolean'},
733 734 'nics.*.allow_mac_spoofing': {'type': 'boolean'},
734 735 'nics.*.allow_restricted_traffic': {'type': 'boolean'},
735 736 'nics.*.allow_unfiltered_promisc': {'type': 'boolean'},
736 737 'nics.*.allowed_ips': {'type': 'list'},
737 738 'nics.*.blocked_outgoing_ports': {'type': 'list'},
738 739 'nics.*.dhcp_server': {'type': 'boolean'},
739 740 'nics.*.gateway': {'type': 'string'},
740 741 'nics.*.interface': {'type': 'string'},
741 742 'nics.*.ip': {'type': 'string'},
742 743 'nics.*.mac': {'type': 'string'},
743 744 'nics.*.model': {'type': 'string'},
744 745 'nics.*.netmask': {'type': 'string'},
745 746 'nics.*.network_uuid': {'type': 'uuid'},
746 747 'nics.*.nic_tag': {'type': 'string'},
747 748 'nics.*.primary': {'type': 'boolean'},
748 749 'nics.*.vrrp_vrid': {'type': 'integer-8bit'},
749 750 'nics.*.vrrp_primary_ip': {'type': 'string'},
750 751 'nics.*.vlan_id': {'type': 'integer'},
751 752 'nic_driver': {'type': 'string'},
752 753 'nowait': {'type': 'boolean'},
753 754 'owner_uuid': {'type': 'string'},
754 755 'package_name': {'type': 'string'},
755 756 'package_version': {'type': 'string'},
756 757 'qemu_opts': {'type': 'string'},
757 758 'qemu_extra_opts': {'type': 'string'},
758 759 'quota': {'type': 'integer'},
759 760 'ram': {'type': 'integer'},
760 761 'remove_customer_metadata': {'type': 'list'},
761 762 'remove_disks': {'type': 'list'},
762 763 'remove_internal_metadata': {'type': 'list'},
763 764 'remove_nics': {'type': 'list'},
764 765 'remove_routes': {'type': 'list'},
765 766 'remove_tags': {'type': 'list'},
766 767 'restart_init': {'type': 'boolean'},
767 768 'resolvers': {'type': 'list'},
768 769 'routes': {'type': 'flat-object'},
769 770 'set_routes': {'type': 'flat-object'},
770 771 'set_tags': {'type': 'flat-object'},
771 772 'set_customer_metadata': {'type': 'flat-object'},
772 773 'set_internal_metadata': {'type': 'flat-object'},
773 774 'spice_opts': {'type': 'string'},
774 775 'spice_password': {'type': 'string'},
775 776 'spice_port': {'type': 'integer'},
776 777 'tags': {'type': 'flat-object'},
777 778 'tmpfs': {'type': 'integer'},
778 779 'transition': {'type': 'flat-object'},
779 780 'update_disks': {'type': 'object-array', 'check_as': 'disks'},
780 781 'update_nics': {'type': 'object-array', 'check_as': 'nics'},
781 782 'uuid': {'type': 'uuid'},
782 783 'v': {'type': 'integer'},
783 784 'vcpus': {'type': 'integer'},
784 785 'vga': {'type': 'string'},
785 786 'virtio_txburst': {'type': 'integer'},
786 787 'virtio_txtimer': {'type': 'integer'},
787 788 'vnc_password': {'type': 'string'},
788 789 'vnc_port': {'type': 'integer'},
789 790 'zfs_data_compression': {'type': 'string'},
790 791 'zfs_data_recsize': {'type': 'integer'},
791 792 'zfs_io_priority': {'type': 'integer'},
792 793 'zfs_root_compression': {'type': 'string'},
793 794 'zfs_root_recsize': {'type': 'integer'},
794 795 'zone_dataset_uuid': {'type': 'uuid'},
795 796 'zonename': {'type': 'string'},
796 797 'zfs_storage_pool_name': {'type': 'zpool'},
797 798 'zpool': {'type': 'zpool'}
798 799 };
799 800
800 801 // shared between 'joyent' and 'joyent-minimal'
801 802 var joyent_allowed = {
802 803 'add_nics': ['update'],
803 804 'alias': ['create', 'receive', 'update'],
804 805 'archive_on_delete': ['create', 'receive', 'update'],
805 806 'autoboot': ['create', 'receive', 'update'],
806 807 'billing_id': ['create', 'receive', 'update'],
807 808 'brand': ['create', 'receive'],
808 809 'cpu_cap': ['create', 'receive', 'update'],
809 810 'cpu_shares': ['create', 'receive', 'update'],
810 811 'create_only': ['receive'],
811 812 'create_timestamp': ['receive'],
812 813 'customer_metadata': ['create', 'receive'],
813 814 'dataset_uuid': ['create', 'receive'],
814 815 'delegate_dataset': ['create', 'receive'],
815 816 'do_not_inventory': ['create', 'receive', 'update'],
816 817 'dns_domain': ['create', 'receive'],
817 818 'filesystems': ['create', 'receive'],
818 819 'filesystems.*.type': ['add'],
819 820 'filesystems.*.source': ['add'],
820 821 'filesystems.*.target': ['add'],
821 822 'filesystems.*.raw': ['add'],
822 823 'filesystems.*.options': ['add'],
823 824 'firewall': ['create'],
824 825 'firewall_enabled': ['create', 'receive', 'update'],
825 826 'fs_allowed': ['create', 'receive', 'update'],
826 827 'hostname': ['create', 'receive', 'update'],
827 828 'image_uuid': ['create', 'receive'],
828 829 'init_name': ['create', 'receive', 'update'],
829 830 'internal_metadata': ['create', 'receive'],
830 831 'limit_priv': ['create', 'receive', 'update'],
831 832 'max_locked_memory': ['create', 'receive', 'update'],
832 833 'max_lwps': ['create', 'receive', 'update'],
833 834 'max_physical_memory': ['create', 'receive', 'update'],
834 835 'max_swap': ['create', 'receive', 'update'],
835 836 'mdata_exec_timeout': ['create'],
836 837 'nics': ['create', 'receive'],
837 838 'nics.*.allow_dhcp_spoofing': ['add', 'update'],
838 839 'nics.*.allow_ip_spoofing': ['add', 'update'],
839 840 'nics.*.allow_mac_spoofing': ['add', 'update'],
840 841 'nics.*.allow_restricted_traffic': ['add', 'update'],
841 842 'nics.*.allowed_ips': ['add', 'update'],
842 843 'nics.*.blocked_outgoing_ports': ['add', 'update'],
843 844 'nics.*.dhcp_server': ['add', 'update'],
844 845 'nics.*.gateway': ['add', 'update'],
845 846 'nics.*.interface': ['add', 'update'],
846 847 'nics.*.ip': ['add', 'update'],
847 848 'nics.*.mac': ['add', 'update'],
848 849 'nics.*.netmask': ['add', 'update'],
849 850 'nics.*.network_uuid': ['add', 'update'],
850 851 'nics.*.nic_tag': ['add', 'update'],
851 852 'nics.*.vrrp_vrid': ['add', 'update'],
852 853 'nics.*.vrrp_primary_ip': ['add', 'update'],
853 854 'nics.*.primary': ['add', 'update'],
854 855 'nics.*.vlan_id': ['add', 'update'],
855 856 'nowait': ['create', 'receive'],
856 857 'owner_uuid': ['create', 'receive', 'update'],
857 858 'package_name': ['create', 'receive', 'update'],
858 859 'package_version': ['create', 'receive', 'update'],
859 860 'quota': ['create', 'receive', 'update'],
860 861 'ram': ['create', 'receive', 'update'],
861 862 'remove_customer_metadata': ['update'],
862 863 'remove_internal_metadata': ['update'],
863 864 'remove_nics': ['update'],
864 865 'remove_routes': ['update'],
865 866 'remove_tags': ['update'],
866 867 'restart_init': ['create', 'receive', 'update'],
867 868 'resolvers': ['create', 'receive', 'update'],
868 869 'routes': ['create', 'receive'],
869 870 'set_customer_metadata': ['update'],
870 871 'set_internal_metadata': ['update'],
871 872 'set_routes': ['update'],
872 873 'set_tags': ['update'],
873 874 'tags': ['create', 'receive'],
874 875 'tmpfs': ['create', 'receive', 'update'],
875 876 'transition': ['receive'],
876 877 'update_nics': ['update'],
877 878 'uuid': ['create', 'receive'],
878 879 'v': ['receive'],
879 880 'zfs_data_compression': ['create', 'receive', 'update'],
880 881 'zfs_data_recsize': ['create', 'receive', 'update'],
881 882 'zfs_io_priority': ['create', 'receive', 'update'],
882 883 'zfs_root_compression': ['create', 'receive', 'update'],
883 884 'zfs_root_recsize': ['create', 'receive', 'update'],
884 885 'zfs_storage_pool_name': ['create', 'receive'],
885 886 'zonename': ['create', 'receive'],
886 887 'zpool': ['create', 'receive']
887 888 };
888 889
889 890 /*
890 891 * This defines all of the properties allowed, required and features that a
891 892 * brand has. For each of the allowed/required properties you have a list of
892 893 * actions for which this is allowed/required. For properties that are lists
893 894 * of objects, you can specify the action as 'add' or 'update' for when you're
894 895 * adding or updating one of those objects.
895 896 *
896 897 * Features can currently be one of:
897 898 *
898 899 * 'cleanup_dataset' -- (boolean) whether to remove trash before booting
899 900 * 'default_memory_overhead' -- (integer) memory above 'ram' that's added
900 901 * 'limit_priv': (list) list of priviledges for this zone (if not 'default')
901 902 * 'mdata_restart' -- (boolean) whether the brand supports restarting its
902 903 * mdata:fetch service to update properties in the zone
903 904 * 'min_memory_overhead' -- (integer) minimum delta between ram + max_physical
904 905 * 'model_required' -- (boolean) whether a .model is required on nics and disks
905 906 * 'pid_file' -- (pathname) file containing the PID for zones with one process
906 907 * 'runtime_info' -- (boolean) whether this zone supports the 'info' command
907 908 * 'serial_console' -- (boolean) whether this zone uses serial console
908 909 * 'type' -- the type of the VM (OS or KVM), all brands should include this
909 910 * 'update_mdata_exec_timeout' (boolean) whether to update mdata:exec timeout
910 911 * 'update_rctls' (boolean) whether we can update rctls 'live' for this zone
911 912 * 'use_tmpfs' -- (boolean) whether this type of zone uses tmpfs
912 913 * 'use_vm_autoboot' -- (boolean) use vm-autoboot instead of autoboot
913 914 * 'use_vmadmd' -- (boolean) use vmadmd for some actions instead of direct
914 915 * 'var_svc_provisioning' -- (boolean) whether brand uses /var/svc/provisioning
915 916 * 'wait_for_hwsetup' -- (boolean) use QMP and provision_success when hwsetup
916 917 * 'write_zone_netfiles' -- (boolean) write out files like /etc/hostname.net0
917 918 * 'zlogin_console' -- (boolean) use zlogin -C for console (vs. serial_console)
918 919 * 'zoneinit' -- (boolean) this brand's setup may be controlled by zoneinit
919 920 *
920 921 * All of the keys:
921 922 *
922 923 * allowed_properties
923 924 * required_properties
924 925 * features
925 926 *
926 927 * should be defined for each brand. Even if empty.
927 928 */
928 929 var BRAND_OPTIONS = {
929 930 'joyent': {
930 931 'allowed_properties': joyent_allowed,
931 932 'required_properties': {
932 933 'brand': ['create', 'receive'],
933 934 'image_uuid': ['create', 'receive']
934 935 }, 'features': {
935 936 'brand_install_script': '/usr/lib/brand/joyent/jinstall',
936 937 'cleanup_dataset': true,
937 938 'mdata_restart': true,
938 939 'reprovision': true,
939 940 'type': 'OS',
940 941 'update_mdata_exec_timeout': true,
941 942 'update_rctls': true,
942 943 'use_tmpfs': true,
943 944 'write_zone_netfiles': true,
944 945 'zlogin_console': true,
945 946 'zoneinit': true
946 947 }
947 948 }, 'joyent-minimal': {
948 949 'allowed_properties': joyent_allowed,
949 950 'required_properties': {
950 951 'brand': ['create', 'receive'],
951 952 'image_uuid': ['create', 'receive']
952 953 }, 'features': {
953 954 'brand_install_script': '/usr/lib/brand/joyent-minimal/jinstall',
954 955 'cleanup_dataset': true,
955 956 'mdata_restart': true,
956 957 'reprovision': true,
957 958 'type': 'OS',
958 959 'update_mdata_exec_timeout': true,
959 960 'update_rctls': true,
960 961 'use_tmpfs': true,
961 962 'var_svc_provisioning': true,
962 963 'write_zone_netfiles': true,
963 964 'zlogin_console': true
964 965 }
965 966 }, 'sngl': {
966 967 'allowed_properties': joyent_allowed,
967 968 'required_properties': {
968 969 'brand': ['create', 'receive'],
969 970 'image_uuid': ['create', 'receive']
970 971 }, 'features': {
971 972 'cleanup_dataset': true,
972 973 'mdata_restart': true,
973 974 'type': 'OS',
974 975 'update_mdata_exec_timeout': true,
975 976 'update_rctls': true,
976 977 'use_tmpfs': true,
977 978 'write_zone_netfiles': true,
978 979 'zlogin_console': true,
979 980 'zoneinit': true
980 981 }
981 982 }, 'kvm': {
982 983 'allowed_properties': {
983 984 'add_disks': ['update'],
984 985 'add_nics': ['update'],
985 986 'alias': ['create', 'receive', 'update'],
986 987 'archive_on_delete': ['create', 'receive', 'update'],
987 988 'autoboot': ['create', 'receive', 'update'],
988 989 'billing_id': ['create', 'receive', 'update'],
989 990 'boot': ['create', 'receive', 'update'],
990 991 'brand': ['create', 'receive'],
991 992 'cpu_cap': ['create', 'receive', 'update'],
992 993 'cpu_shares': ['create', 'receive', 'update'],
993 994 'cpu_type': ['create', 'receive', 'update'],
994 995 'create_only': ['receive'],
995 996 'create_timestamp': ['receive'],
996 997 'customer_metadata': ['create', 'receive'],
997 998 'disks': ['create', 'receive'],
998 999 'disks.*.block_size': ['add'],
999 1000 'disks.*.boot': ['add', 'update'],
1000 1001 'disks.*.compression': ['add', 'update'],
1001 1002 'disks.*.image_name': ['add', 'update'],
1002 1003 'disks.*.image_size': ['add'],
1003 1004 'disks.*.image_uuid': ['add'],
1004 1005 'disks.*.refreservation': ['add', 'update'],
1005 1006 'disks.*.size': ['add'],
1006 1007 'disks.*.media': ['add', 'update'],
1007 1008 'disks.*.model': ['add', 'update'],
1008 1009 'disks.*.nocreate': ['add'],
1009 1010 'disks.*.path': ['add', 'update'],
1010 1011 'disks.*.zpool': ['add'],
1011 1012 'disk_driver': ['create', 'receive', 'update'],
1012 1013 'do_not_inventory': ['create', 'receive', 'update'],
1013 1014 'firewall': ['create'],
1014 1015 'firewall_enabled': ['create', 'receive', 'update'],
1015 1016 'hostname': ['create', 'receive', 'update'],
1016 1017 'image_uuid': ['create', 'receive'],
1017 1018 'internal_metadata': ['create', 'receive'],
1018 1019 'limit_priv': ['create', 'receive', 'update'],
1019 1020 'max_locked_memory': ['create', 'receive', 'update'],
1020 1021 'max_lwps': ['create', 'receive', 'update'],
1021 1022 'max_physical_memory': ['create', 'receive', 'update'],
1022 1023 'max_swap': ['create', 'receive', 'update'],
1023 1024 'nics': ['create', 'receive'],
1024 1025 'nics.*.allow_dhcp_spoofing': ['add', 'update'],
1025 1026 'nics.*.allow_ip_spoofing': ['add', 'update'],
1026 1027 'nics.*.allow_mac_spoofing': ['add', 'update'],
1027 1028 'nics.*.allow_restricted_traffic': ['add', 'update'],
1028 1029 'nics.*.allow_unfiltered_promisc': ['add', 'update'],
1029 1030 'nics.*.allowed_ips': ['add', 'update'],
1030 1031 'nics.*.blocked_outgoing_ports': ['add', 'update'],
1031 1032 'nics.*.dhcp_server': ['add', 'update'],
1032 1033 'nics.*.gateway': ['add', 'update'],
1033 1034 'nics.*.interface': ['add', 'update'],
1034 1035 'nics.*.ip': ['add', 'update'],
1035 1036 'nics.*.mac': ['add', 'update'],
1036 1037 'nics.*.model': ['add', 'update'],
1037 1038 'nics.*.netmask': ['add', 'update'],
1038 1039 'nics.*.network_uuid': ['add', 'update'],
1039 1040 'nics.*.nic_tag': ['add', 'update'],
1040 1041 'nics.*.primary': ['add', 'update'],
1041 1042 'nics.*.vlan_id': ['add', 'update'],
1042 1043 'nic_driver': ['create', 'receive', 'update'],
1043 1044 'owner_uuid': ['create', 'receive', 'update'],
1044 1045 'package_name': ['create', 'receive', 'update'],
1045 1046 'package_version': ['create', 'receive', 'update'],
1046 1047 'qemu_opts': ['create', 'receive', 'update'],
1047 1048 'qemu_extra_opts': ['create', 'receive', 'update'],
1048 1049 'quota': ['create', 'receive', 'update'],
1049 1050 'ram': ['create', 'receive', 'update'],
1050 1051 'remove_customer_metadata': ['update'],
1051 1052 'remove_disks': ['update'],
1052 1053 'remove_internal_metadata': ['update'],
1053 1054 'remove_nics': ['update'],
1054 1055 'remove_routes': ['update'],
1055 1056 'remove_tags': ['update'],
1056 1057 'resolvers': ['create', 'receive', 'update'],
1057 1058 'set_customer_metadata': ['update'],
1058 1059 'set_internal_metadata': ['update'],
1059 1060 'set_routes': ['update'],
1060 1061 'set_tags': ['update'],
1061 1062 'spice_opts': ['create', 'receive', 'update'],
1062 1063 'spice_password': ['create', 'receive', 'update'],
1063 1064 'spice_port': ['create', 'receive', 'update'],
1064 1065 'tags': ['create', 'receive'],
1065 1066 'transition': ['receive'],
1066 1067 'update_disks': ['update'],
1067 1068 'update_nics': ['update'],
1068 1069 'uuid': ['create', 'receive'],
1069 1070 'v': ['receive'],
1070 1071 'vcpus': ['create', 'receive', 'update'],
1071 1072 'vga': ['create', 'receive', 'update'],
1072 1073 'virtio_txburst': ['create', 'receive', 'update'],
1073 1074 'virtio_txtimer': ['create', 'receive', 'update'],
1074 1075 'vnc_password': ['create', 'receive', 'update'],
1075 1076 'vnc_port': ['create', 'receive', 'update'],
1076 1077 'zfs_io_priority': ['create', 'receive', 'update'],
1077 1078 'zfs_root_compression': ['create', 'receive', 'update'],
1078 1079 'zfs_root_recsize': ['create', 'receive', 'update'],
1079 1080 'zone_dataset_uuid': ['create', 'receive'],
1080 1081 'zpool': ['create', 'receive']
1081 1082 }, 'required_properties': {
1082 1083 'brand': ['create', 'receive']
1083 1084 }, 'features': {
1084 1085 'default_memory_overhead': VM.KVM_MEM_OVERHEAD,
1085 1086 'limit_priv': ['default', '-file_link_any', '-net_access',
1086 1087 '-proc_fork', '-proc_info', '-proc_session'],
1087 1088 'min_memory_overhead': VM.KVM_MIN_MEM_OVERHEAD,
1088 1089 'model_required': true,
1089 1090 'pid_file': '/tmp/vm.pid',
1090 1091 'runtime_info': true,
1091 1092 'serial_console': true,
1092 1093 'type': 'KVM',
1093 1094 'use_vm_autoboot': true,
1094 1095 'use_vmadmd': true,
1095 1096 'var_svc_provisioning': true,
1096 1097 'wait_for_hwsetup': true
1097 1098 }
1098 1099 }
1099 1100 };
1100 1101
1101 1102 var VIRTIO_TXTIMER_DEFAULT = 200000;
1102 1103 var VIRTIO_TXBURST_DEFAULT = 128;
1103 1104
1104 1105 function getZpools(log, callback)
1105 1106 {
1106 1107 var args = ['list', '-H', '-p', '-o', 'name'];
1107 1108 var cmd = '/usr/sbin/zpool';
1108 1109 var idx;
1109 1110 var raw = [];
1110 1111 var zpools = [];
1111 1112
1112 1113 assert(log, 'no logger passed to getZpools()');
1113 1114
1114 1115 log.debug(cmd + ' ' + args.join(' '));
1115 1116 execFile(cmd, args, function (error, stdout, stderr) {
1116 1117 if (error) {
1117 1118 log.error('Unable to get list of zpools');
1118 1119 callback(error, {'stdout': stdout, 'stderr': stderr});
1119 1120 } else {
1120 1121 // strip out any empty values (last one).
1121 1122 raw = stdout.split('\n');
1122 1123 for (idx in raw) {
1123 1124 if (raw[idx].length > 0) {
1124 1125 zpools.push(raw[idx]);
1125 1126 }
1126 1127 }
1127 1128 callback(null, zpools);
1128 1129 }
1129 1130 });
1130 1131 }
1131 1132
1132 1133 /*
1133 1134 * When you need to access files inside a zoneroot, you need to be careful that
1134 1135 * there are no symlinks in the path. Since we operate from the GZ, these
1135 1136 * symlinks will be evaluated in the GZ context. Eg. a symlink in zone A with
1136 1137 * /var/run -> * /zones/<uuid of zone B>/root/var/run would mean that operating
1137 1138 * on files in zone A's /var/run would actually be touching files in zone B.
1138 1139 *
1139 1140 * To prevent that, only ever modify files inside the zoneroot from the GZ
1140 1141 * *before* first boot. After the zone is booted, it's better to use services
1141 1142 * in the zone to pull values from metadata and write out changes on next boot.
1142 1143 * It's also safe to use zlogin when the zone is running.
1143 1144 *
1144 1145 * This function is intended to be used in those cases we do write things out
1145 1146 * before the zone's first boot but the dataset might have invalid symlinks in
1146 1147 * it even then, so we still need to confirm the paths inside zoneroot before
1147 1148 * using them. It throws an exception if:
1148 1149 *
1149 1150 * - zoneroot is not an absolute path
1150 1151 * - fs.lstatSync fails
1151 1152 * - target path under zoneroot contains symlink
1152 1153 * - a component leading up to the final one is not a directory
1153 1154 * - options.type is set to 'file' and target is not a regular file
1154 1155 * - options.type is set to 'dir' and target references a non-directory
1155 1156 * - options.type is not one of 'file' or 'dir'
1156 1157 * - options.enoent_ok is false and target path doesn't exist
1157 1158 *
1158 1159 * if none of those are the case, it returns true.
1159 1160 */
1160 1161 function assertSafeZonePath(zoneroot, target, options)
1161 1162 {
1162 1163 var parts;
1163 1164 var root;
1164 1165 var stat;
1165 1166 var test;
1166 1167
1167 1168 assert((zoneroot.length > 0 && zoneroot[0] === '/'),
1168 1169 'zoneroot must be an absolute path not: [' + zoneroot + ']');
1169 1170
1170 1171 parts = trim(target, '/').split('/');
1171 1172 root = trim(zoneroot, '/');
1172 1173 test = '/' + root;
1173 1174
1174 1175 while (parts.length > 0) {
1175 1176 test = test + '/' + parts.shift();
1176 1177
1177 1178 try {
1178 1179 stat = fs.lstatSync(test);
1179 1180 } catch (e) {
1180 1181 if (e.code === 'ENOENT') {
1181 1182 if (!options.hasOwnProperty('enoent_ok')
1182 1183 || options.enoent_ok === false) {
1183 1184
1184 1185 throw e;
1185 1186 } else {
1186 1187 // enoent is ok, return true. This is mostly used when
1187 1188 // deleting files with rm -f <path>. It's ok for <path> to
1188 1189 // not exist (but not ok for any component to be a symlink)
1189 1190 // there's no point continuing though since ENOENT here
1190 1191 // means all subpaths also won't exist.
1191 1192 return true;
1192 1193 }
1193 1194 } else {
1194 1195 throw e;
1195 1196 }
1196 1197 }
1197 1198
1198 1199 if (stat.isSymbolicLink()) {
1199 1200 // it's never ok to have a symlink component
1200 1201 throw new Error(test + ' is a symlink');
1201 1202 }
1202 1203
1203 1204 // any component other than the last also needs to be a
1204 1205 // directory, last can also be a file.
1205 1206 if (parts.length === 0) {
1206 1207 // last, dir or file
1207 1208 if (!options.hasOwnProperty('type') || options.type === 'dir') {
1208 1209 if (!stat.isDirectory()) {
1209 1210 throw new Error(test + ' is not a directory');
1210 1211 }
1211 1212 } else if (options.type === 'file') {
1212 1213 if (!stat.isFile()) {
1213 1214 throw new Error(test + ' is not a file');
1214 1215 }
1215 1216 } else {
1216 1217 throw new Error('this function does not know about '
1217 1218 + options.type);
1218 1219 }
1219 1220 } else if (!stat.isDirectory()) {
1220 1221 // not last component, only dir is acceptable
1221 1222 throw new Error(test + ' is not a directory');
1222 1223 }
1223 1224 }
1224 1225 // if we didn't throw, this is valid.
1225 1226 return true;
1226 1227 }
1227 1228
1228 1229 function validateProperty(brand, prop, value, action, data, errors, log)
1229 1230 {
1230 1231 var allowed;
1231 1232 var k;
1232 1233
1233 1234 assert(log, 'no logger passed to validateProperty()');
1234 1235
1235 1236 if (!data.hasOwnProperty('zpools')) {
1236 1237 data.zpools = [];
1237 1238 }
1238 1239
1239 1240 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')) {
1240 1241 allowed = BRAND_OPTIONS[brand].allowed_properties;
1241 1242 } else {
1242 1243 allowed = {};
1243 1244 }
1244 1245
1245 1246 if (!errors.hasOwnProperty('bad_values')) {
1246 1247 errors.bad_values = [];
1247 1248 }
1248 1249 if (!errors.hasOwnProperty('bad_properties')) {
1249 1250 errors.bad_properties = [];
1250 1251 }
1251 1252
1252 1253 if (!allowed.hasOwnProperty(prop)) {
1253 1254 // thie BRAND_OPTIONS doesn't have this property at all
1254 1255 if (errors.bad_properties.indexOf(prop) === -1) {
1255 1256 errors.bad_properties.push(prop);
1256 1257 }
1257 1258 } else if (!Array.isArray(allowed[prop])
1258 1259 || allowed[prop].indexOf(action) === -1) {
1259 1260
1260 1261 // here we've ether got no actions allowed for this value,
1261 1262 // or just not this one
1262 1263 if (errors.bad_properties.indexOf(prop) === -1) {
1263 1264 errors.bad_properties.push(prop);
1264 1265 }
1265 1266 }
1266 1267
1267 1268 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)) {
1268 1269 switch (PAYLOAD_PROPERTIES[prop].type) {
1269 1270 case 'uuid':
1270 1271 if (typeof (value) === 'string' && !isUUID(value)
1271 1272 && errors.bad_values.indexOf(prop) === -1) {
1272 1273
1273 1274 errors.bad_values.push(prop);
1274 1275 }
1275 1276 break;
1276 1277 case 'boolean':
1277 1278 if (value === 1 || value === '1') {
1278 1279 log.warn('DEPRECATED: payload uses 1 instead of '
1279 1280 + 'true for ' + prop + ', use "true" instead.');
1280 1281 } else if (typeof (fixBoolean(value)) !== 'boolean'
1281 1282 && errors.bad_values.indexOf(prop) === -1) {
1282 1283
1283 1284 errors.bad_values.push(prop);
1284 1285 }
1285 1286 break;
1286 1287 case 'string':
1287 1288 if (value === undefined || value === null
1288 1289 || trim(value.toString()) === '') {
1289 1290 // if set empty/false we'll keep since this is used to unset
1290 1291 break;
1291 1292 } else if (typeof (value) !== 'string'
1292 1293 && errors.bad_values.indexOf(prop) === -1) {
1293 1294
1294 1295 errors.bad_values.push(prop);
1295 1296 }
1296 1297 break;
1297 1298 case 'integer':
1298 1299 if (value === undefined || value === null
1299 1300 || trim(value.toString()) === '') {
1300 1301 // if set empty/false we'll keep since this is used to unset
1301 1302 break;
1302 1303 } else if (((typeof (value) !== 'string'
1303 1304 && typeof (value) !== 'number')
1304 1305 || !value.toString().match(/^[0-9]+$/))
1305 1306 && errors.bad_values.indexOf(prop) === -1) {
1306 1307
1307 1308 if ((['vnc_port', 'spice_port'].indexOf(prop) !== -1)
1308 1309 && (value.toString() === '-1')) {
1309 1310
1310 1311 // these keys allow '-1' as a value, so we succeed here even
1311 1312 // though we'd otherwise fail.
1312 1313 break;
1313 1314 }
1314 1315
1315 1316 errors.bad_values.push(prop);
1316 1317 } else if (prop === 'max_swap' && value < MINIMUM_MAX_SWAP) {
1317 1318 errors.bad_values.push(prop);
1318 1319 }
1319 1320 break;
1320 1321 case 'integer-8bit':
1321 1322 if (value === undefined || value === null
1322 1323 || trim(value.toString()) === '') {
1323 1324 // if set empty/false we'll keep since this is used to unset
1324 1325 break;
1325 1326 } else if (((typeof (value) !== 'string'
1326 1327 && typeof (value) !== 'number')
1327 1328 || !value.toString().match(/^[0-9]+$/))
1328 1329 && errors.bad_values.indexOf(prop) === -1
1329 1330 ) {
1330 1331
1331 1332 errors.bad_values.push(prop);
1332 1333 break;
1333 1334 }
1334 1335 if (value < 0 || value > 255) {
1335 1336 errors.bad_values.push(prop);
1336 1337 }
1337 1338 break;
1338 1339 case 'zpool':
1339 1340 if ((typeof (value) !== 'string'
1340 1341 || data.zpools.indexOf(value) === -1)
1341 1342 && errors.bad_values.indexOf(prop) === -1) {
1342 1343
1343 1344 errors.bad_values.push(prop);
1344 1345 }
1345 1346 break;
1346 1347 case 'object':
1347 1348 if (typeof (value) !== 'object'
1348 1349 && errors.bad_values.indexOf(prop) === -1) {
1349 1350
1350 1351 errors.bad_values.push(prop);
1351 1352 }
1352 1353 break;
1353 1354 case 'flat-object':
1354 1355 if (typeof (value) !== 'object'
1355 1356 && errors.bad_values.indexOf(prop) === -1) {
1356 1357
1357 1358 errors.bad_values.push(prop);
1358 1359 }
1359 1360 for (k in value) {
1360 1361 if (typeof (value[k]) !== 'string'
1361 1362 && typeof (value[k]) !== 'number'
1362 1363 && typeof (value[k]) !== 'boolean') {
1363 1364
1364 1365 if (errors.bad_values.indexOf(prop) === -1) {
1365 1366 errors.bad_values.push(prop);
1366 1367 }
1367 1368 break;
1368 1369 }
1369 1370 }
1370 1371 break;
1371 1372 case 'list':
1372 1373 if (typeof (value) === 'string') {
1373 1374 // really any string could be valid (a one element list)
1374 1375 break;
1375 1376 } else if (Array.isArray(value)) {
1376 1377 for (k in value) {
1377 1378 if (typeof (value[k]) !== 'string'
1378 1379 && typeof (value[k]) !== 'number') {
1379 1380
1380 1381 // TODO: log something more useful here telling them
1381 1382 // the type is invalid.
1382 1383 if (errors.bad_values.indexOf(prop) === -1) {
1383 1384 errors.bad_values.push(prop);
1384 1385 }
1385 1386 break;
1386 1387 }
1387 1388 // if this is an array, it can't have commas in the
1388 1389 // values. (since we might stringify the list and
1389 1390 // we'd end up with something different.
1390 1391 if (value[k].toString().indexOf(',') !== -1
1391 1392 && errors.bad_values.indexOf(prop) === -1) {
1392 1393
1393 1394 errors.bad_values.push(prop);
1394 1395 }
1395 1396 }
1396 1397 } else {
1397 1398 // not a valid type
1398 1399 if (errors.bad_values.indexOf(prop) === -1) {
1399 1400 errors.bad_values.push(prop);
1400 1401 }
1401 1402 }
1402 1403 break;
1403 1404 case 'object-array':
1404 1405 if (!Array.isArray(value)) {
1405 1406 if (errors.bad_values.indexOf(prop) === -1) {
1406 1407 errors.bad_values.push(prop);
1407 1408 }
1408 1409 break;
1409 1410 }
1410 1411 for (k in value) {
1411 1412 if (typeof (value[k]) !== 'object') {
1412 1413 if (errors.bad_values.indexOf(prop) === -1) {
1413 1414 errors.bad_values.push(prop);
1414 1415 }
1415 1416 break;
1416 1417 }
1417 1418 }
1418 1419 break;
1419 1420 default:
1420 1421 // don't know what type of prop this is, so it's invalid
1421 1422 if (errors.bad_properties.indexOf(prop) === -1) {
1422 1423 errors.bad_properties.push(prop);
1423 1424 }
1424 1425 break;
1425 1426 }
1426 1427 }
1427 1428 }
1428 1429
1429 1430 /*
1430 1431 * image properties:
1431 1432 *
1432 1433 * size (optional, only used by zvols)
1433 1434 * type ('zvol' or 'zone-dataset')
1434 1435 * uuid
1435 1436 * zpool
1436 1437 *
1437 1438 */
1438 1439 function validateImage(image, log, callback)
1439 1440 {
1440 1441 var args;
1441 1442 var cmd = '/usr/sbin/imgadm';
1442 1443
1443 1444 args = ['get', '-P', image.zpool, image.uuid];
1444 1445
1445 1446 log.debug(cmd + ' ' + args.join(' '));
1446 1447
1447 1448 // on any error we fail closed (assume the image does not exist)
1448 1449 execFile(cmd, args, function (error, stdout, stderr) {
1449 1450 var data;
1450 1451 var e;
1451 1452
1452 1453 if (error) {
1453 1454 error.stdout = stdout;
1454 1455 error.stderr = stderr;
1455 1456 error.whatFailed = 'EEXECFILE';
1456 1457 log.error(error);
1457 1458 callback(error);
1458 1459 return;
1459 1460 }
1460 1461
1461 1462 try {
1462 1463 data = JSON.parse(stdout.toString());
1463 1464 } catch (err) {
1464 1465 data = {};
1465 1466 }
1466 1467
1467 1468 if (data.hasOwnProperty('manifest')) {
1468 1469 if (data.manifest.type !== image.type) {
1469 1470 // image is wrong type
1470 1471 e = new Error('image ' + image.uuid + ' is type '
1471 1472 + data.manifest.type + ', must be ' + image.type);
1472 1473 e.whatFailed = 'EBADTYPE';
1473 1474 log.error(e);
1474 1475 callback(e);
1475 1476 return;
1476 1477 }
1477 1478 log.info('image ' + image.uuid + ' found in imgadm');
1478 1479
1479 1480 // If image_size is missing, add it. If it's wrong, error.
1480 1481 if (data.manifest.hasOwnProperty('image_size')) {
1481 1482 if (image.hasOwnProperty('size')) {
1482 1483 if (image.size !== data.manifest.image_size) {
1483 1484 e = new Error('incorrect image_size value for image'
1484 1485 + ' ' + image.uuid + ' passed: '
1485 1486 + image.size + ' should be: '
1486 1487 + data.manifest.image_size);
1487 1488 e.whatFailed = 'EBADSIZE';
1488 1489 log.error(e);
1489 1490 callback(e);
1490 1491 return;
1491 1492 }
1492 1493 } else {
1493 1494 // image doesn't have size, manifest does, add it.
1494 1495 image.size = data.manifest.image_size;
1495 1496 }
1496 1497 }
1497 1498 // everything ok
1498 1499 callback();
1499 1500 } else {
1500 1501 e = new Error('cannot find \'manifest\' for image '
1501 1502 + image.uuid);
1502 1503 e.whatFailed = 'ENOENT';
1503 1504 log.error(e);
1504 1505 callback(e);
1505 1506 return;
1506 1507 }
1507 1508 });
1508 1509 }
1509 1510
1510 1511 // Ensure if image_uuid is passed either at top level or for disks.*.image_uuid
1511 1512 // that image_uuid exists on the system according to imgadm.
1512 1513 //
1513 1514 // NOTE: if image_size is missing from payload, but found in imgadm it is added
1514 1515 // to the payload here.
1515 1516 //
1516 1517 function validateImages(payload, errors, log, callback)
1517 1518 {
1518 1519 var check_images = [];
1519 1520 var disk_idx;
1520 1521 var pool;
1521 1522
1522 1523 if (payload.hasOwnProperty('image_uuid') && isUUID(payload.image_uuid)) {
1523 1524 if (payload.hasOwnProperty('zpool')) {
1524 1525 pool = payload.zpool;
1525 1526 } else {
1526 1527 pool = 'zones';
1527 1528 }
1528 1529
1529 1530 check_images.push({
1530 1531 'property': 'image_uuid',
1531 1532 'target': payload,
1532 1533 'type': 'zone-dataset',
1533 1534 'uuid': payload.image_uuid,
1534 1535 'zpool': pool
1535 1536 });
1536 1537 }
1537 1538
1538 1539 ['disks', 'add_disks'].forEach(function (d) {
1539 1540 if (payload.hasOwnProperty(d)) {
1540 1541 disk_idx = 0;
1541 1542 payload[d].forEach(function (disk) {
1542 1543 if (disk.hasOwnProperty('image_uuid')) {
1543 1544 if (disk.hasOwnProperty('zpool')) {
1544 1545 pool = disk.zpool;
1545 1546 } else {
1546 1547 pool = 'zones';
1547 1548 }
1548 1549 check_images.push({
1549 1550 'property_prefix': d + '.' + disk_idx,
1550 1551 'property': d + '.' + disk_idx + '.image_uuid',
1551 1552 'target': disk,
1552 1553 'type': 'zvol',
1553 1554 'uuid': disk.image_uuid,
1554 1555 'zpool': pool
1555 1556 });
1556 1557 }
1557 1558 disk_idx++;
1558 1559 });
1559 1560 }
1560 1561 });
1561 1562
1562 1563 async.forEachSeries(check_images, function (image, cb) {
1563 1564
1564 1565 var i;
1565 1566 var idx;
1566 1567
1567 1568 i = {
1568 1569 uuid: image.uuid,
1569 1570 type: image.type,
1570 1571 zpool: image.zpool
1571 1572 };
1572 1573
1573 1574 if (image.target.hasOwnProperty('image_size')) {
1574 1575 i.size = image.target.image_size;
1575 1576 }
1576 1577
1577 1578 validateImage(i, log, function (err) {
1578 1579 if (err) {
1579 1580 switch (err.whatFailed) {
1580 1581 case 'EBADSIZE':
1581 1582 // image.size is wrong (vs. manifest)
1582 1583 errors.bad_values.push(image.property_prefix
1583 1584 + '.image_size');
1584 1585 break;
1585 1586 case 'ENOENT':
1586 1587 // image.uuid not found in imgadm
1587 1588 errors.bad_values.push(image.property);
1588 1589 break;
1589 1590 case 'EBADTYPE':
1590 1591 // image.type is wrong
1591 1592 errors.bad_values.push(image.property);
1592 1593 break;
1593 1594 default:
1594 1595 // unknown error, fail closed
1595 1596 errors.bad_values.push(image.property);
1596 1597 break;
1597 1598 }
1598 1599 } else {
1599 1600 // no errors, so check if size was added
1600 1601 if (i.hasOwnProperty('size')) {
1601 1602 if (!image.target.hasOwnProperty('image_size')) {
1602 1603 image.target.image_size = i.size;
1603 1604 // Remove error that would have been added earlier
1604 1605 // when we didn't have image_size
1605 1606 idx = errors.missing_properties.indexOf(
1606 1607 image.property_prefix + '.image_size');
1607 1608 if (idx !== -1) {
1608 1609 errors.missing_properties.splice(idx, 1);
1609 1610 }
1610 1611 }
1611 1612 }
1612 1613 }
1613 1614
1614 1615 cb();
1615 1616 });
1616 1617 }, function () {
1617 1618 callback();
1618 1619 });
1619 1620 }
1620 1621
1621 1622 // This is for allowed_ips which accepts IPiv4 addresses or CIDR addresses in
1622 1623 // the form IP/MASK where MASK is 1-32.
1623 1624 function validateIPlist(list) {
1624 1625 var invalid = [];
1625 1626
1626 1627 list.forEach(function (ip) {
1627 1628 var matches;
1628 1629 if (!net.isIPv4(ip)) {
1629 1630 matches = ip.match(/^([0-9\.]+)\/([0-9]+)$/);
1630 1631 if (matches && net.isIPv4(matches[1])
1631 1632 && (Number(matches[2]) >= 1) && (Number(matches[2]) <= 32)) {
1632 1633
1633 1634 // In this case it wasn't an IPv4, but it was a valid CIDR
1634 1635 return;
1635 1636 } else {
1636 1637 invalid.push(ip);
1637 1638 }
1638 1639 }
1639 1640 });
1640 1641
1641 1642 if (invalid.length !== 0) {
1642 1643 throw new Error('invalid allowed_ips: ' + invalid.join(', '));
1643 1644 }
1644 1645
1645 1646 if (list.length > 13) {
1646 1647 throw new Error('Maximum of 13 allowed_ips per nic');
1647 1648 }
1648 1649 }
1649 1650
1650 1651 exports.validate = function (brand, action, payload, options, callback)
1651 1652 {
1652 1653 var errors = {
1653 1654 'bad_values': [],
1654 1655 'bad_properties': [],
1655 1656 'missing_properties': []
1656 1657 };
1657 1658 var log;
1658 1659 var prop;
1659 1660
1660 1661 // options is optional
1661 1662 if (arguments.length === 4) {
1662 1663 callback = arguments[3];
1663 1664 options = {};
1664 1665 }
1665 1666
1666 1667 ensureLogging(false);
1667 1668 if (options.hasOwnProperty('log')) {
1668 1669 log = options.log;
1669 1670 } else {
1670 1671 log = VM.log.child({action: 'validate'});
1671 1672 }
1672 1673
1673 1674 if (!BRAND_OPTIONS.hasOwnProperty(brand)) {
1674 1675 if (!brand) {
1675 1676 brand = 'undefined';
1676 1677 }
1677 1678 callback({'bad_brand': brand});
1678 1679 return;
1679 1680 }
1680 1681
1681 1682 // wrap the whole thing with getZpools so we have the list of pools if we
1682 1683 // need them.
1683 1684 getZpools(log, function (err, zpools) {
1684 1685 var disk_idx;
1685 1686 var idx;
1686 1687 var prefix;
1687 1688 var required;
1688 1689 var subprop;
1689 1690 var subprop_action = '';
1690 1691 var value;
1691 1692
1692 1693 if (err) {
1693 1694 /*
1694 1695 * this only happens when the zpool command fails which should be
1695 1696 * very rare, but when it does happen, we continue with an empty
1696 1697 * zpool list in case they don't need to validate zpools. If they
1697 1698 * do, every zpool will be invalid which is also what we want since
1698 1699 * nothing else that uses zpools is likely to work either.
1699 1700 *
1700 1701 */
1701 1702 zpools = [];
1702 1703 }
1703 1704
1704 1705 // loop through and weed out ones we don't allow for this action.
1705 1706 for (prop in payload) {
1706 1707 validateProperty(brand, prop, payload[prop], action,
1707 1708 {zpools: zpools}, errors, log);
1708 1709
1709 1710 // special case for complex properties where we want to check
1710 1711 // foo.*.whatever
1711 1712 if (PAYLOAD_PROPERTIES.hasOwnProperty(prop)
1712 1713 && PAYLOAD_PROPERTIES[prop].type === 'object-array'
1713 1714 && Array.isArray(payload[prop])) {
1714 1715
1715 1716 if (PAYLOAD_PROPERTIES[prop].hasOwnProperty('check_as')) {
1716 1717 prefix = PAYLOAD_PROPERTIES[prop].check_as + '.*.';
1717 1718 if (prop.match(/^add_/)) {
1718 1719 subprop_action = 'add';
1719 1720 } else if (prop.match(/^update_/)) {
1720 1721 subprop_action = 'update';
1721 1722 }
1722 1723 } else {
1723 1724 // here we've got something like 'disks' which is an add
1724 1725 prefix = prop + '.*.';
1725 1726 subprop_action = 'add';
1726 1727 }
1727 1728
1728 1729 for (idx in payload[prop]) {
1729 1730 if (typeof (payload[prop][idx]) === 'object') {
1730 1731 // subprop will be something like 'nic_tag'
1731 1732 for (subprop in payload[prop][idx]) {
1732 1733 value = payload[prop][idx][subprop];
1733 1734 validateProperty(brand, prefix + subprop, value,
1734 1735 subprop_action, {zpools: zpools}, errors, log);
1735 1736 }
1736 1737 } else if (errors.bad_values.indexOf(prop) === -1) {
1737 1738 // this is not an object so bad value in the array
1738 1739 errors.bad_values.push(prop);
1739 1740 }
1740 1741 }
1741 1742 }
1742 1743 }
1743 1744
1744 1745 // special case: if you have disks you must specify either image_uuid
1745 1746 // and image_size *or* size and block_size is only allowed when you use
1746 1747 // 'size' and image_name when you don't.
1747 1748 if (BRAND_OPTIONS[brand].hasOwnProperty('allowed_properties')
1748 1749 && BRAND_OPTIONS[brand].allowed_properties
1749 1750 .hasOwnProperty('disks')) {
1750 1751
1751 1752 function validateDiskSource(prop_prefix, disk) {
1752 1753
1753 1754 if (disk.hasOwnProperty('media') && disk.media !== 'disk') {
1754 1755 // we only care about disks here, not cdroms.
1755 1756 return;
1756 1757 }
1757 1758
1758 1759 if (disk.hasOwnProperty('image_uuid')) {
1759 1760 // with image_uuid, size is invalid and image_size is
1760 1761 // required, additionally block_size is not allowed.
1761 1762
1762 1763 if (!disk.hasOwnProperty('image_size')) {
1763 1764 errors.missing_properties.push(prop_prefix
1764 1765 + '.image_size');
1765 1766 }
1766 1767 if (disk.hasOwnProperty('size')) {
1767 1768 errors.bad_properties.push(prop_prefix + '.size');
1768 1769 }
1769 1770 if (disk.hasOwnProperty('block_size')) {
1770 1771 errors.bad_properties.push(prop_prefix
1771 1772 + '.block_size');
1772 1773 }
1773 1774 } else {
1774 1775 // without image_uuid, image_size and image_name are invalid
1775 1776 // and 'size' is required.
1776 1777
1777 1778 if (!disk.hasOwnProperty('size')) {
1778 1779 errors.missing_properties.push(prop_prefix + '.size');
1779 1780 }
1780 1781 if (disk.hasOwnProperty('image_name')) {
1781 1782 errors.bad_properties.push(prop_prefix + '.image_name');
1782 1783 }
1783 1784 if (disk.hasOwnProperty('image_size')) {
1784 1785 errors.bad_properties.push(prop_prefix + '.image_size');
1785 1786 }
1786 1787 }
1787 1788 }
1788 1789
1789 1790 if (payload.hasOwnProperty('disks')) {
1790 1791 for (disk_idx in payload.disks) {
1791 1792 validateDiskSource('disks.' + disk_idx,
1792 1793 payload.disks[disk_idx]);
1793 1794 }
1794 1795 }
1795 1796 if (payload.hasOwnProperty('add_disks')) {
1796 1797 for (disk_idx in payload.add_disks) {
1797 1798 validateDiskSource('add_disks.' + disk_idx,
1798 1799 payload.add_disks[disk_idx]);
1799 1800 }
1800 1801 }
1801 1802 }
1802 1803
1803 1804 if (BRAND_OPTIONS[brand].hasOwnProperty('required_properties')) {
1804 1805 required = BRAND_OPTIONS[brand].required_properties;
1805 1806 for (prop in required) {
1806 1807 if (required[prop].indexOf(action) !== -1
1807 1808 && !payload.hasOwnProperty(prop)) {
1808 1809
1809 1810 errors.missing_properties.push(prop);
1810 1811 }
1811 1812 }
1812 1813 }
1813 1814
1814 1815 // make sure any images in the payload are also valid
1815 1816 // NOTE: if validateImages() finds errors, it adds to 'errors' here.
1816 1817 validateImages(payload, errors, log, function () {
1817 1818
1818 1819 // we validate disks.*.refreservation here because image_size might
1819 1820 // not be populated yet until we return from validateImages()
1820 1821 ['disks', 'add_disks'].forEach(function (d) {
1821 1822 var d_idx = 0;
1822 1823 if (payload.hasOwnProperty(d)) {
1823 1824 payload[d].forEach(function (disk) {
1824 1825 if (disk.hasOwnProperty('refreservation')) {
1825 1826 if (disk.refreservation < 0) {
1826 1827 errors.bad_values.push(d + '.' + d_idx
1827 1828 + '.refreservation');
1828 1829 } else if (disk.size
1829 1830 && disk.refreservation > disk.size) {
1830 1831
1831 1832 errors.bad_values.push(d + '.' + d_idx
1832 1833 + '.refreservation');
1833 1834 } else if (disk.image_size
1834 1835 && disk.refreservation > disk.image_size) {
1835 1836
1836 1837 errors.bad_values.push(d + '.' + d_idx
1837 1838 + '.refreservation');
1838 1839 }
1839 1840 }
1840 1841 d_idx++;
1841 1842 });
1842 1843 }
1843 1844 });
1844 1845
1845 1846 if (errors.bad_properties.length > 0 || errors.bad_values.length > 0
1846 1847 || errors.missing_properties.length > 0) {
1847 1848
1848 1849 callback(errors);
1849 1850 return;
1850 1851 }
1851 1852
1852 1853 callback();
1853 1854 });
1854 1855 });
1855 1856 };
1856 1857
1857 1858 function separateCommas(str)
1858 1859 {
1859 1860 return str.split(',');
1860 1861 }
1861 1862
1862 1863 function unmangleMem(str)
1863 1864 {
1864 1865 return (Number(str) / (1024 * 1024));
1865 1866 }
1866 1867
1867 1868 function unbase64(str)
1868 1869 {
1869 1870 return new Buffer(str, 'base64').toString('ascii');
1870 1871 }
1871 1872
1872 1873 function numberify(str)
1873 1874 {
1874 1875 return Number(str);
1875 1876 }
1876 1877
1877 1878 function startElement(name, attrs, state, log) {
1878 1879 var disk = {};
1879 1880 var key;
1880 1881 var newobj;
1881 1882 var nic = {};
1882 1883 var obj;
1883 1884 var prop;
1884 1885 var stack;
1885 1886 var use;
1886 1887 var where;
1887 1888
1888 1889 assert(log, 'no logger passed to startElement()');
1889 1890
1890 1891 if (!state.hasOwnProperty('stack')) {
1891 1892 state.stack = [];
1892 1893 }
1893 1894 obj = state.obj;
1894 1895 stack = state.stack;
1895 1896
1896 1897 stack.push(name);
1897 1898 where = stack.join('.');
1898 1899
1899 1900 if (XML_PROPERTIES.hasOwnProperty(where)) {
1900 1901 for (key in XML_PROPERTIES[where]) {
1901 1902 use = XML_PROPERTIES[where][key];
1902 1903 if (attrs.hasOwnProperty(key)) {
1903 1904 obj[use] = attrs[key];
1904 1905 } else if (attrs.hasOwnProperty('name') && attrs.name === key) {
1905 1906 // attrs use the whacky {name, type, value} stuff.
1906 1907 obj[use] = attrs['value'];
1907 1908 }
1908 1909 }
1909 1910 } else if (where === 'zone.rctl') {
1910 1911 stack.push(attrs.name);
1911 1912 } else if (where === 'zone.network') {
1912 1913 // new network device
1913 1914 for (prop in attrs) {
1914 1915 if (XML_PROPERTIES.nic.hasOwnProperty(prop)) {
1915 1916 use = XML_PROPERTIES.nic[prop];
1916 1917 if (prop === 'mac-addr') {
1917 1918 // XXX SmartOS inherited the ridiculous MAC formatting from
1918 1919 // Solaris where leading zeros are removed. We should
1919 1920 // Fix that in the OS tools.
1920 1921 nic[use] = fixMac(attrs[prop]);
1921 1922 } else {
1922 1923 nic[use] = attrs[prop];
1923 1924 }
1924 1925 } else {
1925 1926 log.debug('unknown net prop: ' + prop);
1926 1927 }
1927 1928 }
1928 1929 if (!obj.hasOwnProperty('networks')) {
1929 1930 obj.networks = {};
1930 1931 }
1931 1932 obj.networks[nic.mac] = nic;
1932 1933 stack.push(nic.mac);
1933 1934 } else if (where.match(/zone\.network\...:..:..:..:..:..\.net-attr/)) {
1934 1935 if (XML_PROPERTIES.nic.hasOwnProperty(attrs.name)) {
1935 1936 use = XML_PROPERTIES.nic[attrs.name];
1936 1937 obj.networks[stack[2]][use] = attrs.value;
1937 1938 } else {
1938 1939 log.debug('unknown net prop: ' + attrs.name);
1939 1940 }
1940 1941 } else if (where === 'zone.device') {
1941 1942 // new disk device
1942 1943 for (prop in attrs) {
1943 1944 if (XML_PROPERTIES.disk.hasOwnProperty(prop)) {
1944 1945 use = XML_PROPERTIES.disk[prop];
1945 1946 disk[use] = attrs[prop];
1946 1947 } else {
1947 1948 log.debug('unknown disk prop: ' + prop);
1948 1949 }
1949 1950 }
1950 1951 if (!obj.hasOwnProperty('devices')) {
1951 1952 obj.devices = {};
1952 1953 }
1953 1954 obj.devices[disk.path] = disk;
1954 1955 stack.push(disk.path);
1955 1956 } else if (where.match(/zone\.device\.\/.*\.net-attr/)) {
1956 1957 if (XML_PROPERTIES.disk.hasOwnProperty(attrs.name)) {
1957 1958 use = XML_PROPERTIES.disk[attrs.name];
1958 1959 obj.devices[stack[2]][use] = attrs.value;
1959 1960 } else {
1960 1961 log.debug('unknown disk prop: ' + attrs.name);
1961 1962 }
1962 1963 } else if (where === 'zone.dataset') {
1963 1964 if (!obj.hasOwnProperty('datasets')) {
1964 1965 obj.datasets = [];
1965 1966 }
1966 1967 if (attrs.hasOwnProperty('name')) {
1967 1968 obj.datasets.push(attrs.name);
1968 1969 }
1969 1970 } else if (where === 'zone.filesystem') {
1970 1971 if (!obj.hasOwnProperty('filesystems')) {
1971 1972 obj.filesystems = [];
1972 1973 }
1973 1974 newobj = {};
1974 1975 for (prop in XML_PROPERTIES.filesystem) {
1975 1976 if (attrs.hasOwnProperty(prop)) {
1976 1977 newobj[XML_PROPERTIES.filesystem[prop]] = attrs[prop];
1977 1978 }
1978 1979 }
1979 1980 obj.filesystems.push(newobj);
1980 1981 } else if (where === 'zone.filesystem.fsoption') {
1981 1982 newobj = obj.filesystems.slice(-1)[0]; // the last element
1982 1983 if (!newobj.hasOwnProperty('options')) {
1983 1984 newobj.options = [];
1984 1985 }
1985 1986 newobj.options.push(attrs.name);
1986 1987 } else {
1987 1988 log.debug('unknown property: ' + where + ': '
1988 1989 + JSON.stringify(attrs));
1989 1990 }
1990 1991 }
1991 1992
1992 1993 function endElement(name, state) {
1993 1994 // trim stack back above this element
1994 1995 var stack = state.stack;
1995 1996
1996 1997 while (stack.pop() !== name) {
1997 1998 // do nothing, we just want to consume.
1998 1999 continue;
1999 2000 }
2000 2001 }
2001 2002
2002 2003 function indexSort(obj, field, pattern)
2003 2004 {
2004 2005 obj.sort(function (a, b) {
2005 2006 var avalue = 0;
2006 2007 var bvalue = 0;
2007 2008 var matches;
2008 2009
2009 2010 if (a.hasOwnProperty(field)) {
2010 2011 matches = a[field].match(pattern);
2011 2012 if (matches) {
2012 2013 avalue = Number(matches[1]);
2013 2014 }
2014 2015 }
2015 2016 if (b.hasOwnProperty(field)) {
2016 2017 matches = b[field].match(pattern);
2017 2018 if (matches) {
2018 2019 bvalue = Number(matches[1]);
2019 2020 }
2020 2021 }
2021 2022
2022 2023 return avalue - bvalue;
2023 2024 });
2024 2025 }
2025 2026
2026 2027 function applyTransforms(obj)
2027 2028 {
2028 2029 var p;
2029 2030 var pp;
2030 2031 var subobj;
2031 2032 var transforms = XML_PROPERTY_TRANSFORMS;
2032 2033
2033 2034 for (p in transforms) {
2034 2035 if (obj.hasOwnProperty(p)) {
2035 2036 if (typeof (transforms[p]) === 'object') {
2036 2037 // this is a 'complex' property like nic, and has different
2037 2038 // transforms for the sub-objects
2038 2039 for (pp in transforms[p]) {
2039 2040 for (subobj in obj[p]) {
2040 2041 if (obj[p][subobj].hasOwnProperty(pp)) {
2041 2042 obj[p][subobj][pp] =
2042 2043 transforms[p][pp](obj[p][subobj][pp]);
2043 2044 }
2044 2045 }
2045 2046 }
2046 2047 } else { // function
2047 2048 obj[p] = transforms[p](obj[p]);
2048 2049 }
2049 2050 }
2050 2051 }
2051 2052 }
2052 2053
2053 2054 // This function parses the zone XML file at /etc/zones/<zonename>.xml and adds
2054 2055 // the VM properties to a new object.
2055 2056 function getVmobj(zonename, preload_data, options, callback)
2056 2057 {
2057 2058 var filename = '/etc/zones/' + zonename + '.xml';
2058 2059 var log;
2059 2060 var parser = new expat.Parser('UTF-8');
2060 2061
2061 2062 assert(options.log, 'no logger passed to getVmobj()');
2062 2063 log = options.log;
2063 2064
2064 2065 fs.readFile(filename, function (error, data) {
2065 2066 var allowed;
2066 2067 var disk;
2067 2068 var dsinfo;
2068 2069 var fields;
2069 2070 var nic;
2070 2071 var obj = {};
2071 2072 var state = {};
2072 2073
2073 2074 if (error) {
2074 2075 callback(error);
2075 2076 return;
2076 2077 }
2077 2078
2078 2079 state.obj = obj;
2079 2080 parser.on('startElement', function (name, attrs) {
2080 2081 return startElement(name, attrs, state, log);
2081 2082 });
2082 2083 parser.on('endElement', function (name) {
2083 2084 return endElement(name, state);
2084 2085 });
2085 2086
2086 2087 if (!parser.parse(data.toString())) {
2087 2088 throw new Error('There are errors in your xml file: '
2088 2089 + parser.getError());
2089 2090 }
2090 2091
2091 2092 // now that we know which brand we are, find out what we're allowed.
2092 2093 allowed = BRAND_OPTIONS[obj.brand].allowed_properties;
2093 2094
2094 2095 // replace obj.networks with array of nics.
2095 2096 obj.nics = [];
2096 2097 for (nic in obj.networks) {
2097 2098 obj.nics.push(obj.networks[nic]);
2098 2099 }
2099 2100 delete obj.networks;
2100 2101
2101 2102 // replace obj.devices with array of disks.
2102 2103 if (allowed.hasOwnProperty('disks')) {
2103 2104 obj.disks = [];
2104 2105 for (disk in obj.devices) {
2105 2106 obj.disks.push(obj.devices[disk]);
2106 2107 }
2107 2108 }
2108 2109 delete obj.devices;
2109 2110
2110 2111 if (!BRAND_OPTIONS.hasOwnProperty(obj.brand)) {
2111 2112 throw new Error('unable to handle brand ' + obj.brand);
2112 2113 }
2113 2114
2114 2115 if (BRAND_OPTIONS[obj.brand].features.use_vm_autoboot) {
2115 2116 obj.autoboot = obj.vm_autoboot;
2116 2117 delete obj.vm_autoboot;
2117 2118 }
2118 2119
2119 2120 // apply the XML_PROPERTY_TRANSFORMs
2120 2121 applyTransforms(obj);
2121 2122
2122 2123 // probe for some fields on disks if this brand of zone supports them.
2123 2124 if (allowed.hasOwnProperty('disks')
2124 2125 && (allowed.disks.indexOf('create') !== -1)) {
2125 2126
2126 2127 for (disk in obj.disks) {
2127 2128 disk = obj.disks[disk];
2128 2129
2129 2130 if (preload_data.hasOwnProperty('dsinfo')) {
2130 2131 dsinfo = preload_data.dsinfo;
2131 2132 if (dsinfo.hasOwnProperty('mountpoints')
2132 2133 && dsinfo.mountpoints.hasOwnProperty(disk.path)) {
2133 2134
2134 2135 disk.zfs_filesystem = dsinfo.mountpoints[disk.path];
2135 2136 disk.zpool = disk.zfs_filesystem.split('/')[0];
2136 2137 } else {
2137 2138 log.trace('no mountpoint data for ' + disk.path);
2138 2139 }
2139 2140 }
2140 2141 }
2141 2142 }
2142 2143
2143 2144 if (obj.hasOwnProperty('transition')) {
2144 2145 fields = rtrim(obj.transition).split(':');
2145 2146 if (fields.length === 3) {
2146 2147 delete obj.transition;
2147 2148 obj.state = fields[0];
2148 2149 obj.transition_to = fields[1];
2149 2150 obj.transition_expire = fields[2];
2150 2151 } else {
2151 2152 log.debug('getVmobj() ignoring bad value for '
2152 2153 + 'transition "' + obj.transition + '"');
2153 2154 }
2154 2155 }
2155 2156
2156 2157 // sort the disks + nics by index
2157 2158 if (obj.hasOwnProperty('disks')) {
2158 2159 indexSort(obj.disks, 'path', /^.*-disk(\d+)$/);
2159 2160 }
2160 2161 if (obj.hasOwnProperty('nics')) {
2161 2162 indexSort(obj.nics, 'interface', /^net(\d+)$/);
2162 2163 }
2163 2164 if (obj.hasOwnProperty('filesystems')) {
2164 2165 indexSort(obj.filesystems, 'target', /^(.*)$/);
2165 2166 }
2166 2167
2167 2168 callback(null, obj);
2168 2169 });
2169 2170 }
2170 2171
2171 2172 function setQuota(dataset, quota, log, callback)
2172 2173 {
2173 2174 var newval;
2174 2175
2175 2176 assert(log, 'no logger passed to setQuota()');
2176 2177
2177 2178 if (!dataset) {
2178 2179 callback(new Error('Invalid dataset: "' + dataset + '"'));
2179 2180 return;
2180 2181 }
2181 2182
2182 2183 if (quota === 0 || quota === '0') {
2183 2184 newval = 'none';
2184 2185 } else {
2185 2186 newval = quota.toString() + 'g';
2186 2187 }
2187 2188
2188 2189 zfs(['set', 'quota=' + newval, dataset], log, function (err, fds) {
2189 2190 if (err) {
2190 2191 log.error('setQuota() cmd failed: ' + fds.stderr);
2191 2192 callback(new Error(rtrim(fds.stderr)));
2192 2193 } else {
2193 2194 callback();
2194 2195 }
2195 2196 });
2196 2197 }
2197 2198
2198 2199 function cleanDatasetObject(obj)
2199 2200 {
2200 2201 var number_fields = [
2201 2202 'avail',
2202 2203 'available',
2203 2204 'copies',
2204 2205 'creation',
2205 2206 'filesystem_limit',
2206 2207 'quota',
2207 2208 'recsize',
2208 2209 'recordsize',
2209 2210 'refer',
2210 2211 'referenced',
2211 2212 'refquota',
2212 2213 'refreserv',
2213 2214 'refreservation',
2214 2215 'reserv',
2215 2216 'reservation',
2216 2217 'snapshot_limit',
2217 2218 'usedbychildren',
2218 2219 'usedbydataset',
2219 2220 'usedbyrefreservation',
2220 2221 'usedbysnapshots',
2221 2222 'used',
2222 2223 'userrefs',
2223 2224 'utf8only',
2224 2225 'version',
2225 2226 'volblock',
2226 2227 'volblocksize',
2227 2228 'volsize',
2228 2229 'written'
2229 2230 ];
2230 2231
2231 2232 // We should always have mountpoint, dataset and type because we force them
2232 2233 // to be included in zfsList()
2233 2234 assert(obj.hasOwnProperty('mountpoint'), 'cleanDatasetObject('
2234 2235 + JSON.stringify(obj) + '): missing mountpoint');
2235 2236 assert(obj.hasOwnProperty('name'), 'cleanDatasetObject('
2236 2237 + JSON.stringify(obj) + '): missing name');
2237 2238 assert(obj.hasOwnProperty('type'), 'cleanDatasetObject('
2238 2239 + JSON.stringify(obj) + '): missing type');
2239 2240
2240 2241 // convert numeric fields to proper numbers
2241 2242 number_fields.forEach(function (field) {
2242 2243 if (obj.hasOwnProperty(field) && obj[field] !== '-') {
2243 2244 obj[field] = Number(obj[field]);
2244 2245 }
2245 2246 });
2246 2247
2247 2248 if (obj.type === 'volume') {
2248 2249 obj.mountpoint = '/dev/zvol/rdsk/' + obj.name;
2249 2250 } else if (obj.mountpoint === '-' || obj.mountpoint === 'legacy') {
2250 2251 obj.mountpoint = '/' + obj.name;
2251 2252 }
2252 2253 }
2253 2254
2254 2255 function addDatasetResult(fields, types, results, line, log)
2255 2256 {
2256 2257 var dataset;
2257 2258 var field;
2258 2259 var lfields;
2259 2260 var obj;
2260 2261 var snapparts;
2261 2262 var snapobj;
2262 2263
2263 2264 line = trim(line);
2264 2265
2265 2266 if (line.length === 0) {
2266 2267 return;
2267 2268 }
2268 2269
2269 2270 lfields = line.split(/\s+/);
2270 2271
2271 2272 if (lfields.length !== fields.length) {
2272 2273 return;
2273 2274 }
2274 2275
2275 2276 obj = {};
2276 2277
2277 2278 for (field in fields) {
2278 2279 obj[fields[field]] = lfields[field];
2279 2280 }
2280 2281
2281 2282 cleanDatasetObject(obj);
2282 2283
2283 2284 if (!results.hasOwnProperty('datasets')) {
2284 2285 results.datasets = {};
2285 2286 }
2286 2287 if (!results.hasOwnProperty('mountpoints')) {
2287 2288 results.mountpoints = {};
2288 2289 }
2289 2290 if (types.indexOf('snapshot') !== -1 && obj.type === 'snapshot') {
2290 2291 if (!results.hasOwnProperty('snapshots')) {
2291 2292 results.snapshots = {};
2292 2293 }
2293 2294
2294 2295 /*
2295 2296 * For snapshots we store the snapname and optionally creation keyed by
2296 2297 * dataset name So that we can include the list of snapshots for a
2297 2298 * dataset on a VM.
2298 2299 */
2299 2300 snapparts = obj.name.split('@');
2300 2301 assert.equal(snapparts.length, 2);
2301 2302 dataset = snapparts[0];
2302 2303 snapobj = {snapname: snapparts[1], dataset: dataset};
2303 2304 if (!results.snapshots.hasOwnProperty(dataset)) {
2304 2305 results.snapshots[dataset] = [];
2305 2306 }
2306 2307 if (obj.hasOwnProperty('creation')) {
2307 2308 snapobj.created_at = obj.creation;
2308 2309 }
2309 2310 results.snapshots[dataset].push(snapobj);
2310 2311 }
2311 2312
2312 2313 results.datasets[obj.name] = obj;
2313 2314
2314 2315 /*
2315 2316 * snapshots don't have mountpoint that we care about and we don't count
2316 2317 * 'none' as a mountpoint. If we otherwise have a mountpoint that looks like
2317 2318 * a path, we add a pointer from that to the dataset name.
2318 2319 */
2319 2320 if (obj.type !== 'snapshot' && obj.mountpoint[0] === '/') {
2320 2321 /*
2321 2322 * For zoned filesystems (delegated datasets) we don't use mountpoint as
2322 2323 * this can be changed from within the zone and is therefore not
2323 2324 * reliable. Also, when a delegated dataset is assigned but the zone's
2324 2325 * not been booted, the delegated dataset will not have the 'zoned'
2325 2326 * property. So we also check if the name ends in /data.
2326 2327 */
2327 2328 if (obj.hasOwnProperty('zoned') && obj.zoned === 'on') {
2328 2329 // don't add zoned datasets to mountpoints
2329 2330 /*jsl:pass*/
2330 2331 } else if (obj.name.split('/')[2] === 'data') {
2331 2332 // name is /data, skip
2332 2333 /*jsl:pass*/
2333 2334 } else {
2334 2335 // here we have what looks like a normal non-zoned dataset that's
2335 2336 // probably a zoneroot, add to mountpoints mapping.
2336 2337 results.mountpoints[obj.mountpoint] = obj.name;
2337 2338 }
2338 2339 }
2339 2340 }
2340 2341
2341 2342 /*
2342 2343 * Arguments:
2343 2344 *
2344 2345 * 'fields' - should be an array of fields as listed in the zfs(1m) man page.
2345 2346 * 'types' - should be one or more of: filesystem, snapshot, volume.
2346 2347 * 'log' - should be a bunyan logger object.
2347 2348 * 'callback' - will be called with (err, results)
2348 2349 *
2349 2350 * On failure: callback's err will be an Error object, ignore results.
2350 2351 * On success: callback's results is an object with one or more members of:
2351 2352 *
2352 2353 * results.datasets
2353 2354 *
2354 2355 * keyed by dataset name containing the values for the requested fields.
2355 2356 *
2356 2357 * Eg: results.datasets['zones/cores'] === { name: 'zones/cores', ... }
2357 2358 *
2358 2359 * results.mountpoints
2359 2360 *
2360 2361 * keyed by mountpoint with value being dataset name.
2361 2362 *
2362 2363 * Eg: results.mountpoints['/zones/cores'] === 'zones/cores'
2363 2364 *
2364 2365 * results.snapshots
2365 2366 *
2366 2367 * keyed by dataset with value being array of snapname and created_at.
2367 2368 *
2368 2369 * Eg: results.snapshots['/zones/cores'] === ['snap1', ...]
2369 2370 *
2370 2371 * For non-zoned filesystem datasets (these should be the zoneroot datasets),
2371 2372 * you can use mountpoint which comes from zoneadm's "cheap" info and use that
2372 2373 * to get to the dataset and from datasets[dataset] get the info.
2373 2374 *
2374 2375 * For volumes (KVM VM's disks) you can also use mountpoint as we'll set that
2375 2376 * to the block device path and that's available from the devices section of
2376 2377 * the zoneconfig.
2377 2378 *
2378 2379 * For zoned filesystems (delegated datasets) use the dataset name, as the
2379 2380 * mountpoint can be changed from within the zone.
2380 2381 *
2381 2382 */
2382 2383 function zfsList(fields, types, log, callback) {
2383 2384 var args;
2384 2385 var buffer = '';
2385 2386 var lines;
2386 2387 var cmd = '/usr/sbin/zfs';
2387 2388 var req_fields = ['mountpoint', 'name', 'type'];
2388 2389 var results = {};
2389 2390 var zfs_child;
2390 2391
2391 2392 assert(Array.isArray(types));
2392 2393 assert(Array.isArray(fields));
2393 2394 assert(log, 'no logger passed to zfsList()');
2394 2395
2395 2396 // add any missing required fields
2396 2397 req_fields.forEach(function (field) {
2397 2398 if (fields.indexOf(field) === -1) {
2398 2399 fields.push(field);
2399 2400 }
2400 2401 });
2401 2402
2402 2403 args = ['list', '-H', '-p', '-t', types.join(','), '-o', fields.join(',')];
2403 2404
2404 2405 log.debug(cmd + ' ' + args.join(' '));
2405 2406
2406 2407 zfs_child = spawn(cmd, args, {'customFds': [-1, -1, -1]});
2407 2408 log.debug('zfs running with pid ' + zfs_child.pid);
2408 2409
2409 2410 zfs_child.stdout.on('data', function (data) {
2410 2411 var line;
2411 2412
2412 2413 buffer += data.toString();
2413 2414 lines = buffer.split('\n');
2414 2415 while (lines.length > 1) {
2415 2416 line = lines.shift();
2416 2417
2417 2418 // Add this line to results
2418 2419 addDatasetResult(fields, types, results, line, log);
2419 2420 }
2420 2421 buffer = lines.pop();
2421 2422 });
2422 2423
2423 2424 // doesn't take input.
2424 2425 zfs_child.stdin.end();
2425 2426
2426 2427 zfs_child.on('exit', function (code) {
2427 2428 log.debug('zfs process ' + zfs_child.pid + ' exited with code: '
2428 2429 + code);
2429 2430 if (code === 0) {
2430 2431 callback(null, results);
2431 2432 } else {
2432 2433 callback(new Error('zfs exited prematurely with code: ' + code));
2433 2434 }
2434 2435 });
2435 2436 }
2436 2437
2437 2438 /*
2438 2439 * This queue is used to handle zfs list requests. We do this because of OS-1834
2439 2440 * in order to only run one 'zfs list' at a time. If we need to get data from
2440 2441 * 'zfs list', the parameters we want to list are pushed onto this queue. If a
2441 2442 * list is already running with the same parameters, we'll return the output
2442 2443 * from that one when it completes to all the consumers. If there's not one
2443 2444 * running, or the parameters are different, this set of parameters will be
2444 2445 * pushed onto the tail of the queue. The queue is processed serially so long
2445 2446 * as there are active requests.
2446 2447 */
2447 2448 zfs_list_queue = async.queue(function (task, callback) {
2448 2449
2449 2450 var fields = task.fields;
2450 2451 var log = task.log;
2451 2452 var started = Date.now(0);
2452 2453 var types = task.types;
2453 2454
2454 2455 zfsList(fields, types, log, function (err, data) {
2455 2456 var emitter = zfs_list_in_progress[task];
2456 2457
2457 2458 delete zfs_list_in_progress[task];
2458 2459 emitter.emit('result', err, data);
2459 2460 emitter.removeAllListeners('result');
2460 2461
2461 2462 log.debug('zfs list took ' + (Date.now(0) - started) + ' ms');
2462 2463 callback();
2463 2464 });
2464 2465
2465 2466 }, 1);
2466 2467
2467 2468 zfs_list_queue.drain = function () {
2468 2469 // We use the global log here because this queue is not tied to one action.
2469 2470 VM.log.trace('zfs_list_queue is empty');
2470 2471 };
2471 2472
2472 2473 function getZfsList(fields, types, log, callback) {
2473 2474 var sorted_fields;
2474 2475 var sorted_types;
2475 2476 var task;
2476 2477
2477 2478 sorted_fields = fields.slice().sort();
2478 2479 sorted_types = types.slice().sort();
2479 2480
2480 2481 task = {types: sorted_types, fields: sorted_fields, log: log};
2481 2482
2482 2483 try {
2483 2484 zfs_list_in_progress[task].on('result', callback);
2484 2485 } catch (e) {
2485 2486 if ((e instanceof TypeError)
2486 2487 && (!zfs_list_in_progress.hasOwnProperty(task)
2487 2488 || !zfs_list_in_progress[task].hasOwnProperty('on'))) {
2488 2489
2489 2490 zfs_list_in_progress[task] = new EventEmitter();
2490 2491 zfs_list_in_progress[task].on('result', callback);
2491 2492 zfs_list_in_progress[task].setMaxListeners(0);
2492 2493 zfs_list_queue.push(task);
2493 2494
2494 2495 // callback() will get called when 'result' is emitted.
2495 2496 } else {
2496 2497 callback(e);
2497 2498 }
2498 2499 }
2499 2500 }
2500 2501
2501 2502 function loadDatasetInfo(fields, log, callback)
2502 2503 {
2503 2504 var zfs_fields = [];
2504 2505 var zfs_types = [];
2505 2506
2506 2507 assert(log, 'no logger passed to loadDataset()');
2507 2508
2508 2509 function addField(name) {
2509 2510 if (zfs_fields.indexOf(name) === -1) {
2510 2511 zfs_fields.push(name);
2511 2512 }
2512 2513 }
2513 2514
2514 2515 function addType(name) {
2515 2516 if (zfs_types.indexOf(name) === -1) {
2516 2517 zfs_types.push(name);
2517 2518 }
2518 2519 }
2519 2520
2520 2521 if (!fields || fields.length < 1) {
2521 2522 // Default to grabbing everything we might possibly need.
2522 2523 zfs_fields = ['name', 'quota', 'volsize', 'mountpoint', 'type',
2523 2524 'compression', 'recsize', 'volblocksize', 'zoned', 'creation',
2524 2525 'refreservation'];
2525 2526 zfs_types = ['filesystem', 'snapshot', 'volume'];
2526 2527 } else {
2527 2528 if (fields.indexOf('disks') !== -1) {
2528 2529 addField('compression');
2529 2530 addField('volsize');
2530 2531 addField('volblocksize');
2531 2532 addField('refreservation');
2532 2533 addType('volume');
2533 2534 }
2534 2535 if (fields.indexOf('snapshots') !== -1) {
2535 2536 addField('creation');
2536 2537 addType('snapshot');
2537 2538 addType('filesystem');
2538 2539 addType('volume');
2539 2540 }
2540 2541 if (fields.indexOf('create_timestamp') !== -1) {
2541 2542 // We might fall back to creation on the dataset for
2542 2543 // create_timestamp if we have no create-timestamp attr.
2543 2544 addField('creation');
2544 2545 addType('filesystem');
2545 2546 }
2546 2547 if ((fields.indexOf('zfs_root_compression') !== -1)
2547 2548 || (fields.indexOf('zfs_data_compression') !== -1)) {
2548 2549
2549 2550 addField('compression');
2550 2551 addType('filesystem');
2551 2552 }
2552 2553 if ((fields.indexOf('zfs_root_recsize') !== -1)
2553 2554 || (fields.indexOf('zfs_data_recsize') !== -1)) {
2554 2555
2555 2556 addField('recsize');
2556 2557 addType('filesystem');
2557 2558 }
2558 2559 if (fields.indexOf('quota') !== -1) {
2559 2560 addField('quota');
2560 2561 addType('filesystem');
2561 2562 }
2562 2563 // both zpool and zfs_filesystem come from 'name'
2563 2564 if (fields.indexOf('zpool') !== -1
2564 2565 || fields.indexOf('zfs_filesystem') !== -1) {
2565 2566
2566 2567 addField('name');
2567 2568 addType('filesystem');
2568 2569 }
2569 2570 if (zfs_fields.length > 0) {
2570 2571 // we have some fields so we need to zfs, make sure we have name,
2571 2572 // mountpoint and type which we always need if we get anything.
2572 2573 addField('name');
2573 2574 addField('mountpoint');
2574 2575 addField('type');
2575 2576
2576 2577 if (zfs_types.indexOf('filesystem') !== -1) {
2577 2578 // to differentiate between delegated and root filesystems
2578 2579 addField('zoned');
2579 2580 }
2580 2581 } else {
2581 2582 log.debug('no need to call zfs');
2582 2583 callback(null, {
2583 2584 datasets: {},
2584 2585 mountpoints: {},
2585 2586 snapshots: {}
2586 2587 });
2587 2588 return;
2588 2589 }
2589 2590 }
2590 2591
2591 2592 /*
2592 2593 * NOTE:
2593 2594 * in the future, the plan is to have the list of types and fields
2594 2595 * be dynamic based what's actually needed to handle the request.
2595 2596 */
2596 2597
2597 2598 getZfsList(zfs_fields, zfs_types, log, callback);
2598 2599 }
2599 2600
2600 2601 function loadJsonConfig(vmobj, cfg, log, callback)
2601 2602 {
2602 2603 var filename;
2603 2604
2604 2605 assert(log, 'no logger passed to loadJsonConfig()');
2605 2606
2606 2607 if (vmobj.zonepath) {
2607 2608 filename = vmobj.zonepath + '/config/' + cfg + '.json';
2608 2609 log.trace('loadJsonConfig() loading ' + filename);
2609 2610
2610 2611 fs.readFile(filename, function (error, data) {
2611 2612 var json = {};
2612 2613
2613 2614 if (error) {
2614 2615 if (error.code === 'ENOENT') {
2615 2616 log.debug('Skipping nonexistent file ' + filename);
2616 2617 } else {
2617 2618 log.error(error,
2618 2619 'loadJsonConfig() failed to load ' + filename);
2619 2620 callback(error, {});
2620 2621 return;
2621 2622 }
2622 2623 } else {
2623 2624 try {
2624 2625 json = JSON.parse(data.toString());
2625 2626 } catch (e) {
2626 2627 json = {};
2627 2628 }
2628 2629 }
2629 2630
2630 2631 callback(null, json);
2631 2632 });
2632 2633 } else {
2633 2634 callback(null, {});
2634 2635 }
2635 2636 }
2636 2637
2637 2638 /*
2638 2639 * This preloads some data for us that comes from commands which output for
2639 2640 * *all* VMs. This allows us to just run these (expensive) commands once
2640 2641 * instead of having to run them for each VM.
2641 2642 *
2642 2643 */
2643 2644 function preloadZoneData(uuid, options, callback)
2644 2645 {
2645 2646 var data = {};
2646 2647 var log;
2647 2648
2648 2649 assert(options.log, 'no logger passed to preloadZoneData()');
2649 2650 log = options.log;
2650 2651
2651 2652 // NOTE: uuid can be null, in which case we get data for all VMs.
2652 2653
2653 2654 async.series([
2654 2655 function (cb) {
2655 2656 // We always do this (calls `zoneadm list -vc`) since we always
2656 2657 // need to know which zones exist.
2657 2658 getZoneRecords(uuid, log, function (err, records) {
2658 2659 if (!err) {
2659 2660 data.records = records;
2660 2661 }
2661 2662 cb(err);
2662 2663 });
2663 2664 }, function (cb) {
2664 2665 var fields;
2665 2666
2666 2667 if (options.hasOwnProperty('fields')) {
2667 2668 fields = options.fields;
2668 2669 } else {
2669 2670 fields = [];
2670 2671 }
2671 2672
2672 2673 loadDatasetInfo(fields, log, function (err, dsinfo) {
2673 2674 if (!err) {
2674 2675 data.dsinfo = dsinfo;
2675 2676 }
2676 2677 cb(err);
2677 2678 });
2678 2679 }, function (cb) {
2679 2680 if (options.hasOwnProperty('fields')
2680 2681 && (options.fields.indexOf('server_uuid') === -1
2681 2682 && options.fields.indexOf('datacenter_name') === -1
2682 2683 && options.fields.indexOf('headnode_id') === -1)) {
2683 2684
2684 2685 // we don't need any fields that come from sysinfo.
2685 2686 log.debug('no need to call sysinfo, no sysinfo fields in list');
2686 2687 data.sysinfo = {};
2687 2688 cb();
2688 2689 return;
2689 2690 }
2690 2691
2691 2692 VM.getSysinfo([], {log: log}, function (err, sysinfo) {
2692 2693 if (!err) {
2693 2694 data.sysinfo = sysinfo;
2694 2695 }
2695 2696 cb(err);
2696 2697 });
2697 2698 }, function (cb) {
2698 2699 var u;
2699 2700 var uuids = [];
2700 2701
2701 2702 if (options.hasOwnProperty('fields')
2702 2703 && options.fields.indexOf('pid') === -1) {
2703 2704
2704 2705 log.debug('no need to check PID files, PID not in field list');
2705 2706 cb();
2706 2707 return;
2707 2708 }
2708 2709
2709 2710 // get the PID values from running KVM VMs
2710 2711
2711 2712 for (u in data.records) {
2712 2713 uuids.push(u);
2713 2714 }
2714 2715 async.forEachSeries(uuids, function (z_uuid, zcb) {
2715 2716 var filename;
2716 2717 var z = data.records[z_uuid];
2717 2718
2718 2719 // NOTE: z.state here is equivalent to zone_state not state.
2719 2720 if (z && BRAND_OPTIONS[z.brand].hasOwnProperty('features')
2720 2721 && BRAND_OPTIONS[z.brand].features.pid_file
2721 2722 && z.state === 'running') {
2722 2723
2723 2724 // ensure pid_file is safe
2724 2725 try {
2725 2726 assertSafeZonePath(path.join(z.zonepath, '/root'),
2726 2727 BRAND_OPTIONS[z.brand].features.pid_file,
2727 2728 {type: 'file', enoent_ok: true});
2728 2729 } catch (e) {
2729 2730 // We log an error here, but not being able to get
2730 2731 // the PID for one broken machine should not impact the
2731 2732 // ability to get a list of all machines, so we just
2732 2733 // skip adding a PID and log an error here.
2733 2734 log.error(e, 'Unsafe path for ' + z.uuid + ' cannot '
2734 2735 + 'check for PID file: ' + e.message);
2735 2736 zcb();
2736 2737 return;
2737 2738 }
2738 2739
2739 2740 filename = path.join(z.zonepath, 'root',
2740 2741 BRAND_OPTIONS[z.brand].features.pid_file);
2741 2742 log.debug('checking for ' + filename);
2742 2743
2743 2744 fs.readFile(filename,
2744 2745 function (error, filedata) {
2745 2746
2746 2747 var pid;
2747 2748
2748 2749 if (!error) {
2749 2750 pid = Number(trim(filedata.toString()));
2750 2751 if (pid > 0) {
2751 2752 z.pid = pid;
2752 2753 log.debug('found PID ' + pid + ' for '
2753 2754 + z.uuid);
2754 2755 }
2755 2756 }
2756 2757 if (error && error.code === 'ENOENT') {
2757 2758 // don't return error in this case because it just
2758 2759 // didn't exist
2759 2760 log.debug('no PID file for ' + z.uuid);
2760 2761 zcb();
2761 2762 } else {
2762 2763 zcb(error);
2763 2764 }
2764 2765 });
2765 2766 } else {
2766 2767 zcb();
2767 2768 }
2768 2769 }, function (err) {
2769 2770 cb(err);
2770 2771 });
2771 2772 }
2772 2773 ], function (err, res) {
2773 2774 log.trace('leaving preloadZoneData()');
2774 2775 callback(err, data);
2775 2776 });
2776 2777 }
2777 2778
2778 2779 function getZoneRecords(uuid, log, callback)
2779 2780 {
2780 2781 var args = [];
2781 2782 var buffer = '';
2782 2783 var cmd = '/usr/sbin/zoneadm';
2783 2784 var line_count = 0;
2784 2785 var lines;
2785 2786 var results = {};
2786 2787 var zadm;
2787 2788 var zadm_stderr = '';
2788 2789
2789 2790 assert(log, 'no logger passed to getZoneRecords()');
2790 2791
2791 2792 if (uuid) {
2792 2793 // this gives us zone info if uuid is *either* a zonename or uuid
2793 2794 if (isUUID(uuid)) {
2794 2795 args.push('-z');
2795 2796 args.push(uuid);
2796 2797 args.push('-u');
2797 2798 args.push(uuid);
2798 2799 } else {
2799 2800 args.push('-z');
2800 2801 args.push(uuid);
2801 2802 }
2802 2803 }
2803 2804 args.push('list');
2804 2805 args.push('-p');
2805 2806 if (!uuid) {
2806 2807 args.push('-c');
2807 2808 }
2808 2809
2809 2810 log.debug(cmd + ' ' + args.join(' '));
2810 2811
2811 2812 zadm = spawn(cmd, args, {'customFds': [-1, -1, -1]});
2812 2813 log.debug('zoneadm running with PID ' + zadm.pid);
2813 2814
2814 2815 zadm.stderr.on('data', function (data) {
2815 2816 zadm_stderr += data.toString();
2816 2817 });
2817 2818
2818 2819 zadm.stdout.on('data', function (data) {
2819 2820 var fields;
2820 2821 var line;
2821 2822 var obj;
2822 2823
2823 2824 buffer += data.toString();
2824 2825 lines = buffer.split('\n');
2825 2826 while (lines.length > 1) {
2826 2827 line = lines.shift();
2827 2828 line_count++;
2828 2829 fields = rtrim(line).split(':');
2829 2830 if (fields.length === 8 && fields[1] !== 'global') {
2830 2831 obj = {
2831 2832 'zoneid': Number(fields[0]),
2832 2833 'zonename': fields[1],
2833 2834 'state': fields[2],
2834 2835 'zonepath': fields[3],
2835 2836 'uuid': fields[4],
2836 2837 'brand': fields[5],
2837 2838 'ip_type': fields[6]
2838 2839 };
2839 2840 log.trace('loaded: ' + JSON.stringify(obj));
2840 2841 // XXX zones in some states have no uuid. We should either fix
2841 2842 // that or use zonename for those.
2842 2843 results[obj.uuid] = obj;
2843 2844 } else if (line.replace(/ /g, '').length > 0) {
2844 2845 log.debug('getZoneRecords(' + uuid + ') ignoring: ' + line);
2845 2846 }
2846 2847 }
2847 2848 buffer = lines.pop();
2848 2849 });
2849 2850
2850 2851 // doesn't take input.
2851 2852 zadm.stdin.end();
2852 2853
2853 2854 zadm.on('close', function (code) {
2854 2855 var errmsg;
2855 2856 var new_err;
2856 2857
2857 2858 log.debug('zoneadm process ' + zadm.pid + ' exited with code: '
2858 2859 + code + ' (' + line_count + ' lines to stdout)');
2859 2860 if (code === 0) {
2860 2861 callback(null, results);
2861 2862 } else {
2862 2863 errmsg = rtrim(zadm_stderr);
2863 2864 new_err = new Error(errmsg);
2864 2865 if (errmsg.match(/No such zone configured$/)) {
2865 2866 // not existing isn't always a problem (eg. existence check)
2866 2867 new_err.code = 'ENOENT';
2867 2868 } else {
2868 2869 log.error({err: new_err, stderr: zadm_stderr},
2869 2870 'getZoneRecords() zoneadm "' + args.join(',') + '" failed');
2870 2871 }
2871 2872 callback(new_err);
2872 2873 return;
2873 2874 }
2874 2875 });
2875 2876 }
2876 2877
2877 2878 exports.flatten = function (vmobj, key)
2878 2879 {
2879 2880 var index;
2880 2881 var tokens = key.split('.');
2881 2882
2882 2883 // NOTE: VM.flatten() currently doesn't produce any logs
2883 2884
2884 2885 if (tokens.length === 3
2885 2886 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(tokens[0]) !== -1) {
2886 2887
2887 2888 if (!vmobj.hasOwnProperty(tokens[0])) {
2888 2889 return undefined;
2889 2890 }
2890 2891 if (!vmobj[tokens[0]].hasOwnProperty(tokens[1])) {
2891 2892 return undefined;
2892 2893 }
2893 2894 return vmobj[tokens[0]][tokens[1]][tokens[2]];
2894 2895 }
2895 2896
2896 2897 if (tokens.length === 2
2897 2898 && VM.FLATTENABLE_HASH_KEYS.indexOf(tokens[0]) !== -1) {
2898 2899
2899 2900 if (!vmobj.hasOwnProperty(tokens[0])) {
2900 2901 return undefined;
2901 2902 }
2902 2903 return vmobj[tokens[0]][tokens[1]];
2903 2904 }
2904 2905
2905 2906 if (tokens.length === 2
2906 2907 && VM.FLATTENABLE_ARRAYS.indexOf(tokens[0]) !== -1) {
2907 2908
2908 2909 index = Number(tokens[1]);
2909 2910
2910 2911 if (!vmobj.hasOwnProperty(tokens[0])) {
2911 2912 return undefined;
2912 2913 }
2913 2914
2914 2915 if (index === NaN || index < 0
2915 2916 || !vmobj[tokens[0]].hasOwnProperty(index)) {
2916 2917
2917 2918 return undefined;
2918 2919 }
2919 2920 return vmobj[tokens[0]][index];
2920 2921 }
2921 2922
2922 2923 return vmobj[key];
2923 2924 };
2924 2925
2925 2926 function getLastModified(vmobj, log)
2926 2927 {
2927 2928 var files = [];
2928 2929 var file;
2929 2930 var stat;
2930 2931 var timestamp = 0;
2931 2932
2932 2933 assert(log, 'no logger passed to getLastModified()');
2933 2934
2934 2935 if (vmobj.zonepath) {
2935 2936 files.push(path.join(vmobj.zonepath, '/config/metadata.json'));
2936 2937 files.push(path.join(vmobj.zonepath, '/config/routes.json'));
2937 2938 files.push(path.join(vmobj.zonepath, '/config/tags.json'));
2938 2939 } else {
2939 2940 log.debug('getLastModified() no zonepath!');
2940 2941 }
2941 2942
2942 2943 if (vmobj.hasOwnProperty('zonename')) {
2943 2944 files.push('/etc/zones/' + vmobj.zonename + '.xml');
2944 2945 } else {
2945 2946 log.debug('getLastModified() no zonename!');
2946 2947 }
2947 2948
2948 2949 for (file in files) {
2949 2950 file = files[file];
2950 2951 try {
2951 2952 stat = fs.statSync(file);
2952 2953 if (stat.isFile()) {
2953 2954 if ((timestamp === 0) || (Date.parse(stat.mtime) > timestamp)) {
2954 2955 timestamp = Date.parse(stat.mtime);
2955 2956 }
2956 2957 }
2957 2958 } catch (e) {
2958 2959 if (e.code !== 'ENOENT') {
2959 2960 log.error(e, 'Unable to get timestamp for "' + file + '":'
2960 2961 + e.message);
2961 2962 }
2962 2963 }
2963 2964 }
2964 2965
2965 2966 return ((new Date(timestamp)).toISOString());
2966 2967 }
2967 2968
2968 2969 function loadVM(uuid, data, options, callback)
2969 2970 {
2970 2971 var e;
2971 2972 var info;
2972 2973 var log;
2973 2974
2974 2975 assert(options.log, 'no logger passed to loadVM()');
2975 2976 log = options.log;
2976 2977
2977 2978 // XXX need to always have data when we get here
2978 2979 info = data.records[uuid];
2979 2980
2980 2981 if (!info) {
2981 2982 e = new Error('VM.load() empty info when getting record '
2982 2983 + 'for vm ' + uuid);
2983 2984 log.error(e);
2984 2985 callback(e);
2985 2986 return;
2986 2987 }
2987 2988
2988 2989 getVmobj(info.zonename, data, options, function (err, vmobj) {
2989 2990 if (err) {
2990 2991 callback(err);
2991 2992 return;
2992 2993 }
2993 2994
2994 2995 function wantField(field) {
2995 2996 if (options.hasOwnProperty('fields')
2996 2997 && options.fields.indexOf(field) === -1) {
2997 2998
2998 2999 return false;
2999 3000 }
3000 3001
3001 3002 return true;
3002 3003 }
3003 3004
3004 3005 // We got some bits from `zoneadm list` as <info> here, and since we
3005 3006 // already got that data, adding it to the object here is cheap. We also
3006 3007 // need some of these properties to be able to get others later, so we
3007 3008 // add them all now. If they're unwanted they'll be removed from the
3008 3009 // final object.
3009 3010 vmobj.uuid = info.uuid;
3010 3011 vmobj.zone_state = info.state;
3011 3012
3012 3013 // In the case of 'configured' zones, we might only have zonename
3013 3014 // because uuid isn't set yet. Because of that case, we set uuid
3014 3015 // to zonename if it is in UUID form.
3015 3016 if ((!vmobj.uuid || vmobj.uuid.length === 0)
3016 3017 && isUUID(vmobj.zonename)) {
3017 3018
3018 3019 vmobj.uuid = vmobj.zonename;
3019 3020 }
3020 3021
3021 3022 // These ones we never need elsewhere, so don't bother adding if we
3022 3023 // don't need to.
3023 3024 if (wantField('zoneid') && info.zoneid !== '-') {
3024 3025 vmobj.zoneid = info.zoneid;
3025 3026 }
3026 3027
3027 3028 if (wantField('pid') && info.pid) {
3028 3029 vmobj.pid = info.pid;
3029 3030 }
3030 3031
3031 3032 // find when we last modified this VM
3032 3033 if (wantField('last_modified')) {
3033 3034 vmobj.last_modified = getLastModified(vmobj, log);
3034 3035 }
3035 3036
3036 3037 // If we want resolvers, (eg. OS-2194) we always add the array here
3037 3038 // so you can tell that the resolvers are explicitly not set.
3038 3039 if (wantField('resolvers') && !vmobj.hasOwnProperty('resolvers')) {
3039 3040 vmobj.resolvers = [];
3040 3041 }
3041 3042
3042 3043 // sysinfo has server_uuid and potentially some DC info
3043 3044 if (data.hasOwnProperty('sysinfo')) {
3044 3045 if (wantField('server_uuid')
3045 3046 && data.sysinfo.hasOwnProperty('UUID')) {
3046 3047
3047 3048 vmobj.server_uuid = data.sysinfo.UUID;
3048 3049 }
3049 3050 if (wantField('datacenter_name')
3050 3051 && data.sysinfo.hasOwnProperty('Datacenter Name')) {
3051 3052
3052 3053 vmobj.datacenter_name = data.sysinfo['Datacenter Name'];
3053 3054 }
3054 3055 if (wantField('headnode_id')
3055 3056 && data.sysinfo.hasOwnProperty('Headnode ID')) {
3056 3057
3057 3058 vmobj.headnode_id = data.sysinfo['Headnode ID'];
3058 3059 }
3059 3060 }
3060 3061
3061 3062 // state could already be set here if it was overriden by a transition
3062 3063 // that's in progress. So we only change if that's not the case.
3063 3064 if (wantField('state')) {
3064 3065 if (!vmobj.hasOwnProperty('state')) {
3065 3066 if (info.state === 'installed') {
3066 3067 vmobj.state = 'stopped';
3067 3068 } else {
3068 3069 vmobj.state = info.state;
3069 3070 }
3070 3071 }
3071 3072
3072 3073 // If the zone has the 'failed' property it doesn't matter what
3073 3074 // other state it might be in, we list its state as 'failed'.
3074 3075 if (vmobj.failed) {
3075 3076 vmobj.state = 'failed';
3076 3077 }
3077 3078 }
3078 3079
3079 3080 async.series([
3080 3081 function (cb) {
3081 3082 if (!wantField('customer_metadata')
3082 3083 && !wantField('internal_metadata')) {
3083 3084
3084 3085 cb();
3085 3086 return;
3086 3087 }
3087 3088
3088 3089 loadJsonConfig(vmobj, 'metadata', log,
3089 3090 function (error, metadata) {
3090 3091 if (error) {
3091 3092 // when zone_state is 'incomplete' we could be
3092 3093 // deleting it in which case metadata may already
3093 3094 // be gone, ignore failure to load mdata when
3094 3095 // 'incomplete' because of this.
3095 3096 if (vmobj.zone_state === 'incomplete') {
3096 3097 log.debug(error, 'zone is in state incomplete '
3097 3098 + 'ignoring error: ' + error.message);
3098 3099 } else {
3099 3100 cb(error);
3100 3101 return;
3101 3102 }
3102 3103 }
3103 3104
3104 3105 if (wantField('customer_metadata')) {
3105 3106 if (metadata.hasOwnProperty('customer_metadata')) {
3106 3107 vmobj.customer_metadata
3107 3108 = metadata.customer_metadata;
3108 3109 } else {
3109 3110 vmobj.customer_metadata = {};
3110 3111 }
3111 3112 }
3112 3113
3113 3114 if (wantField('internal_metadata')) {
3114 3115 if (metadata.hasOwnProperty('internal_metadata')) {
3115 3116 vmobj.internal_metadata
3116 3117 = metadata.internal_metadata;
3117 3118 } else {
3118 3119 vmobj.internal_metadata = {};
3119 3120 }
3120 3121 }
3121 3122
3122 3123 cb();
3123 3124 });
3124 3125 }, function (cb) {
3125 3126 if (!wantField('tags')) {
3126 3127 cb();
3127 3128 return;
3128 3129 }
3129 3130
3130 3131 loadJsonConfig(vmobj, 'tags', log, function (error, tags) {
3131 3132 if (error) {
3132 3133 // when zone_state is 'incomplete' we could be deleting
3133 3134 // it in which case metadata may already be gone, ignore
3134 3135 // failure to load mdata when 'incomplete' because of
3135 3136 // this.
3136 3137 if (vmobj.zone_state === 'incomplete') {
3137 3138 log.debug(error, 'zone is in state incomplete '
3138 3139 + 'ignoring error: ' + error.message);
3139 3140 } else {
3140 3141 cb(error);
3141 3142 return;
3142 3143 }
3143 3144 }
3144 3145 vmobj.tags = tags;
3145 3146 cb();
3146 3147 });
3147 3148 }, function (cb) {
3148 3149 if (!wantField('routes')) {
3149 3150 cb();
3150 3151 return;
3151 3152 }
3152 3153
3153 3154 loadJsonConfig(vmobj, 'routes', log, function (error, routes) {
3154 3155 if (error) {
3155 3156 // same as tags above, if zone_state is 'incomplete'
3156 3157 // we could be a file that's already gone
3157 3158 if (vmobj.zone_state === 'incomplete') {
3158 3159 log.debug(error, 'zone is in state incomplete '
3159 3160 + 'ignoring error: ' + error.message);
3160 3161 } else {
3161 3162 cb(error);
3162 3163 return;
3163 3164 }
3164 3165 }
3165 3166 vmobj.routes = routes;
3166 3167 cb();
3167 3168 });
3168 3169 }, function (cb) {
3169 3170 var dsinfo;
3170 3171 var dsname;
3171 3172 var dsobj;
3172 3173 var d;
3173 3174 var delegated;
3174 3175 var disk;
3175 3176 var ds;
3176 3177 var filesys;
3177 3178 var friendly_snap;
3178 3179 var friendly_snapshots = [];
3179 3180 var matches;
3180 3181 var raw_snapshots = [];
3181 3182 var snap;
3182 3183 var snap_time;
3183 3184
3184 3185 // local alias, data.dsinfo should include all the info about
3185 3186 // this VM's zoneroot that we care about here.
3186 3187 dsinfo = data.dsinfo;
3187 3188
3188 3189 if (dsinfo.hasOwnProperty('mountpoints')
3189 3190 && dsinfo.hasOwnProperty('datasets')
3190 3191 && dsinfo.mountpoints.hasOwnProperty(vmobj.zonepath)) {
3191 3192
3192 3193 dsname = dsinfo.mountpoints[vmobj.zonepath];
3193 3194 dsobj = dsinfo.datasets[dsname];
3194 3195
3195 3196 /* dsobj.quota is in bytes, we want GiB for vmobj.quota */
3196 3197 if (wantField('quota') && dsobj.hasOwnProperty('quota')) {
3197 3198 vmobj.quota = (dsobj.quota / (1024 * 1024 * 1024));
3198 3199 log.trace('found quota "' + vmobj.quota + '" for '
3199 3200 + vmobj.uuid);
3200 3201 }
3201 3202
3202 3203 if (wantField('create_timestamp')
3203 3204 && !vmobj.hasOwnProperty('create_timestamp')
3204 3205 && dsobj.hasOwnProperty('creation')) {
3205 3206
3206 3207 log.debug('VM has no create_timestamp, using creation '
3207 3208 + 'from ' + dsobj.name);
3208 3209 vmobj.create_timestamp =
3209 3210 (new Date(dsobj.creation * 1000)).toISOString();
3210 3211 }
3211 3212
3212 3213 if (wantField('zfs_root_compression')
3213 3214 && dsobj.hasOwnProperty('compression')
3214 3215 && (dsobj.compression !== 'off')) {
3215 3216
3216 3217 vmobj.zfs_root_compression = dsobj.compression;
3217 3218 }
3218 3219
3219 3220 if (wantField('zfs_root_recsize')
3220 3221 && dsobj.hasOwnProperty('recsize')) {
3221 3222
3222 3223 vmobj.zfs_root_recsize = dsobj.recsize;
3223 3224 }
3224 3225
3225 3226 // Always add zfs_filesystem if we can because it's needed
3226 3227 // to find other properties such as delegated_dataset.
3227 3228 vmobj.zfs_filesystem = dsobj.name;
3228 3229
3229 3230 if (wantField('snapshots')
3230 3231 && dsinfo.hasOwnProperty('snapshots')
3231 3232 && dsinfo.snapshots
3232 3233 .hasOwnProperty(vmobj.zfs_filesystem)) {
3233 3234
3234 3235 raw_snapshots = raw_snapshots.concat(
3235 3236 dsinfo.snapshots[vmobj.zfs_filesystem]);
3236 3237 }
3237 3238
3238 3239 log.trace('found dataset "' + vmobj.zfs_filesystem
3239 3240 + '" for ' + vmobj.uuid);
3240 3241 } else {
3241 3242 log.trace('no dsinfo for ' + vmobj.uuid + ': '
3242 3243 + vmobj.zonepath);
3243 3244 }
3244 3245
3245 3246 // delegated datasets are keyed on the dataset name instead of
3246 3247 // mountpoint, since mountpoint can change in a zone.
3247 3248 if (vmobj.hasOwnProperty('zfs_filesystem')) {
3248 3249 delegated = vmobj.zfs_filesystem + '/data';
3249 3250 if (dsinfo.datasets.hasOwnProperty(delegated)) {
3250 3251 dsobj = dsinfo.datasets[delegated];
3251 3252
3252 3253 if (dsobj.hasOwnProperty('compression')
3253 3254 && (dsobj.compression !== 'off')) {
3254 3255
3255 3256 vmobj.zfs_data_compression = dsobj.compression;
3256 3257 }
3257 3258 if (dsobj.hasOwnProperty('recsize')) {
3258 3259 vmobj.zfs_data_recsize = dsobj.recsize;
3259 3260 }
3260 3261
3261 3262 // If there are snapshots for this dataset, add them
3262 3263 if (DISABLED) {
3263 3264 // XXX currently only support snapshot on
3264 3265 // zfs_filesystem
3265 3266 if (dsinfo.hasOwnProperty('snapshots')
3266 3267 && dsinfo.snapshots.hasOwnProperty(delegated)) {
3267 3268
3268 3269 raw_snapshots = raw_snapshots
3269 3270 .concat(dsinfo.snapshots[delegated]);
3270 3271 }
3271 3272 }
3272 3273 } else {
3273 3274 log.trace('no dsinfo for delegated dataset: '
3274 3275 + delegated);
3275 3276 }
3276 3277
3277 3278 vmobj.zpool =
3278 3279 vmobj.zfs_filesystem.split('/')[0];
3279 3280 }
3280 3281
3281 3282 if (wantField('disks') && vmobj.hasOwnProperty('disks')) {
3282 3283 for (d in vmobj.disks) {
3283 3284 d = vmobj.disks[d];
3284 3285 if (d.hasOwnProperty('path')
3285 3286 && dsinfo.mountpoints.hasOwnProperty(d.path)) {
3286 3287
3287 3288 dsname = dsinfo.mountpoints[d.path];
3288 3289 dsobj = dsinfo.datasets[dsname];
3289 3290
3290 3291 if (dsobj.hasOwnProperty('volsize')) {
3291 3292
3292 3293 /* dsobj.volsize is in bytes, we want MiB */
3293 3294 d.size = (dsobj.volsize / (1024 * 1024));
3294 3295 log.debug('found size=' + d.size + ' for '
3295 3296 + JSON.stringify(d));
3296 3297 }
3297 3298 if (dsobj.hasOwnProperty('compression')) {
3298 3299 d.compression = dsobj.compression;
3299 3300 }
3300 3301 if (dsobj.hasOwnProperty('refreservation')) {
3301 3302 /* dsobj.refreservation is in bytes, want MiB */
3302 3303 d.refreservation
3303 3304 = (dsobj.refreservation / (1024 * 1024));
3304 3305 log.debug('found refreservation='
3305 3306 + d.refreservation + ' for '
3306 3307 + JSON.stringify(d));
3307 3308 }
3308 3309 if (dsobj.hasOwnProperty('volblocksize')) {
3309 3310 d.block_size = dsobj.volblocksize;
3310 3311 }
3311 3312
3312 3313 // If there are snapshots for this dataset, add them
3313 3314 // to the list.
3314 3315 if (DISABLED) {
3315 3316 // XXX currently only support snapshots on
3316 3317 // zfs_filesystem
3317 3318 if (dsinfo.hasOwnProperty('snapshots')
3318 3319 && dsinfo.snapshots.hasOwnProperty(
3319 3320 d.zfs_filesystem)) {
3320 3321
3321 3322 raw_snapshots = raw_snapshots.concat(dsinfo
3322 3323 .snapshots[d.zfs_filesystem]);
3323 3324 }
3324 3325 }
3325 3326 } else if (d.hasOwnProperty('path')) {
3326 3327 d.missing = true;
3327 3328 } else {
3328 3329 log.warn('no dsinfo and no path for '
3329 3330 + JSON.stringify(d));
3330 3331 }
3331 3332 }
3332 3333 }
3333 3334
3334 3335 // snapshots here is the raw list of snapshots, now we need to
3335 3336 // convert it to the "friendly" list of snapshots.
3336 3337 if (wantField('snapshots')) {
3337 3338 for (snap in raw_snapshots) {
3338 3339 snap = raw_snapshots[snap];
3339 3340
3340 3341 matches = snap.snapname.match(/^vmsnap-(.*)$/);
3341 3342 if (matches && matches[1]) {
3342 3343 friendly_snap = {name: matches[1]};
3343 3344 if (snap.hasOwnProperty('created_at')) {
3344 3345 snap_time
3345 3346 = new Date(snap.created_at * 1000); // in ms
3346 3347 friendly_snap.created_at
3347 3348 = snap_time.toISOString();
3348 3349 }
3349 3350 friendly_snapshots.push(friendly_snap);
3350 3351 } else {
3351 3352 log.debug('ignoring unfriendly ' + snap.snapname);
3352 3353 continue;
3353 3354 }
3354 3355 }
3355 3356 // sort the snapshots with newest first.
3356 3357 friendly_snapshots.sort(function (a, b) {
3357 3358 if (a.created_at > b.created_at) {
3358 3359 return -1;
3359 3360 }
3360 3361 if (a.created_at < b.created_at) {
3361 3362 return 1;
3362 3363 }
3363 3364 return 0; // equal
3364 3365 });
3365 3366 vmobj.snapshots = friendly_snapshots;
3366 3367 }
3367 3368
3368 3369 if (vmobj.state === 'receiving') {
3369 3370 vmobj.missing = { 'datasets': [], 'disks': [],
3370 3371 'filesystems': [] };
3371 3372 if (!fs.existsSync(vmobj.zonepath)) {
3372 3373 vmobj.missing.datasets.push(vmobj.zonepath.substr(1));
3373 3374 }
3374 3375 for (ds in vmobj.datasets) {
3375 3376 ds = vmobj.datasets[ds];
3376 3377 vmobj.missing.datasets.push(ds);
3377 3378 }
3378 3379 for (filesys in vmobj.filesystems) {
3379 3380 filesys = vmobj.filesystems[filesys];
3380 3381 if (filesys.hasOwnProperty('source')) {
3381 3382 vmobj.missing.filesystems.push(filesys.source);
3382 3383 }
3383 3384 }
3384 3385 for (disk in vmobj.disks) {
3385 3386 disk = vmobj.disks[disk];
3386 3387 if (disk.hasOwnProperty('missing')) {
3387 3388 vmobj.missing.disks.push(disk.path);
3388 3389 }
3389 3390 }
3390 3391 }
3391 3392
3392 3393 cb();
3393 3394 }
3394 3395 ], function (error) {
3395 3396 callback(error, vmobj);
3396 3397 });
3397 3398
3398 3399 });
3399 3400 }
3400 3401
3401 3402 exports.load = function (uuid, options, callback)
3402 3403 {
3403 3404 var log;
3404 3405 var load_opts = {};
3405 3406
3406 3407 // This is a wrapper so that other internal functions here (such as lookup)
3407 3408 // can do smart things like check the quota for each VM with a separate call
3408 3409 // to zfs get.
3409 3410
3410 3411 // options is optional
3411 3412 if (arguments.length === 2) {
3412 3413 callback = arguments[1];
3413 3414 options = {};
3414 3415 }
3415 3416
3416 3417 ensureLogging(false);
3417 3418 if (options.hasOwnProperty('log')) {
3418 3419 log = options.log;
3419 3420 } else {
3420 3421 log = VM.log.child({action: 'load', vm: uuid});
3421 3422 }
3422 3423
3423 3424 load_opts.log = log;
3424 3425 if (options.hasOwnProperty('fields')) {
3425 3426 load_opts.fields = options.fields;
3426 3427 }
3427 3428
3428 3429 preloadZoneData(uuid, load_opts, function (error, data) {
3429 3430 if (error) {
3430 3431 if (options.missing_ok && error.code === 'ENOENT') {
3431 3432 // we're expecting the zone to be gone in this case (eg. delete)
3432 3433 log.debug('VM ' + uuid + ' does not exist (as expected)');
3433 3434 } else {
3434 3435 log.error(error, 'VM.load() failed to get zone record'
3435 3436 + ' for ' + uuid);
3436 3437 }
3437 3438 callback(error);
3438 3439 } else {
3439 3440 loadVM(uuid, data, load_opts, function (e, vmobj) {
3440 3441 if (e) {
3441 3442 callback(e);
3442 3443 return;
3443 3444 }
3444 3445
3445 3446 if (load_opts.hasOwnProperty('fields')) {
3446 3447 // clean out unwanted fields
3447 3448 Object.keys(vmobj).forEach(function (key) {
3448 3449 if (options.fields.indexOf(key) === -1) {
3449 3450 delete vmobj[key];
3450 3451 }
3451 3452 });
3452 3453 }
3453 3454 callback(null, vmobj);
3454 3455 });
3455 3456 }
3456 3457 });
3457 3458 };
3458 3459
3459 3460 function fixMac(str)
3460 3461 {
3461 3462 var fixed = [];
3462 3463 var octet;
3463 3464 var octets = str.split(':');
3464 3465
3465 3466 for (octet in octets) {
3466 3467 if (octets.hasOwnProperty(octet)) {
3467 3468 octet = parseInt(octets[octet], 16);
3468 3469 if (octet === 'nan') {
3469 3470 octet = 0;
3470 3471 }
3471 3472 fixed.push(sprintf('%02x', octet));
3472 3473 }
3473 3474 }
3474 3475
3475 3476 return fixed.join(':');
3476 3477 }
3477 3478
3478 3479 // zonecfg requires removing leading 0's in MACs like 01:02:03:04:05:06
3479 3480 // This function takes a MAC in normal form and puts it in the goofy form
3480 3481 // zonecfg wants.
3481 3482 function ruinMac(mac)
3482 3483 {
3483 3484 var part;
3484 3485 var parts;
3485 3486 var out = [];
3486 3487
3487 3488 parts = mac.split(':');
3488 3489
3489 3490 for (part in parts) {
3490 3491 part = ltrim(parts[part], '0');
3491 3492 if (part.length === 0) {
3492 3493 part = '0';
3493 3494 }
3494 3495 out.push(part);
3495 3496 }
3496 3497
3497 3498 return (out.join(':'));
3498 3499 }
3499 3500
3500 3501 function matcher(zone, search)
3501 3502 {
3502 3503 var fields;
3503 3504 var found;
3504 3505 var i;
3505 3506 var key;
3506 3507 var parameters_matched = 0;
3507 3508 var regex;
3508 3509 var target;
3509 3510
3510 3511 function find_match(k, targ) {
3511 3512 var value = VM.flatten(zone, k);
3512 3513
3513 3514 if (!regex && k.match(/^nics\..*\.mac$/)) {
3514 3515 // Fix for broken SmartOS MAC format
3515 3516 targ = fixMac(targ);
3516 3517 }
3517 3518
3518 3519 if (regex && (value !== undefined) && value.toString().match(targ)) {
3519 3520 found = true;
3520 3521 } else if ((value !== undefined)
3521 3522 && value.toString() === targ.toString()) {
3522 3523 found = true;
3523 3524 }
3524 3525 }
3525 3526
3526 3527 for (key in search) {
3527 3528 found = false;
3528 3529 regex = false;
3529 3530
3530 3531 target = search[key];
3531 3532 if (target[0] === '~') {
3532 3533 regex = true;
3533 3534 target = new RegExp(target.substr(1), 'i');
3534 3535 }
3535 3536
3536 3537 fields = key.split('.');
3537 3538 if (fields.length === 3 && fields[1] === '*'
3538 3539 && zone.hasOwnProperty(fields[0])
3539 3540 && VM.FLATTENABLE_ARRAY_HASH_KEYS.indexOf(fields[0]) !== -1) {
3540 3541
3541 3542 // Special case: for eg. nics.*.ip, we want to loop through all nics
3542 3543 for (i = 0; i < zone[fields[0]].length; i++) {
3543 3544 fields[1] = i;
3544 3545 find_match(fields.join('.'), target);
3545 3546 }
3546 3547 } else {
3547 3548 find_match(key, target);
3548 3549 }
3549 3550
3550 3551 if (!found) {
3551 3552 return false;
3552 3553 } else {
3553 3554 parameters_matched++;
3554 3555 }
3555 3556 }
3556 3557
3557 3558 if (parameters_matched > 0) {
3558 3559 // we would have returned false from the loop had any parameters not
3559 3560 // matched and we had at least one that did.
3560 3561 return true;
3561 3562 }
3562 3563
3563 3564 return false;
3564 3565 }
3565 3566
3566 3567 exports.lookup = function (search, options, callback)
3567 3568 {
3568 3569 var log;
3569 3570 var key;
3570 3571 var matches;
3571 3572 var need_fields = [];
3572 3573 var preload_opts = {};
3573 3574 var quick_ok = true;
3574 3575 var results = [];
3575 3576 var transform;
3576 3577
3577 3578 // options is optional
3578 3579 if (arguments.length === 2) {
3579 3580 callback = arguments[1];
3580 3581 options = {};
3581 3582 }
3582 3583
3583 3584 ensureLogging(false);
3584 3585 if (options.hasOwnProperty('log')) {
3585 3586 log = options.log;
3586 3587 } else {
3587 3588 log = VM.log.child({action: 'lookup', search: search});
3588 3589 }
3589 3590
3590 3591 // XXX the 'transform' option is not intended to be public yet and should
3591 3592 // only be used by tools willing to be rewritten if this is removed or
3592 3593 // changed.
3593 3594 if (options.hasOwnProperty('transform')) {
3594 3595 transform = options.transform;
3595 3596 }
3596 3597
3597 3598 // keep separate variable because we can have some fields we add below that
3598 3599 // we need for searching, but shouldn't be in the output.
3599 3600 if (options.hasOwnProperty('fields')) {
3600 3601 need_fields = options.fields.slice(0);
3601 3602 }
3602 3603
3603 3604 for (key in search) {
3604 3605 // To be able to search on a field, that field needs to be added to
3605 3606 // the objects, if user requested a set of fields missing the one
3606 3607 // they're searching for, add it.
3607 3608 matches = key.match(/^([^.]+)\./);
3608 3609 if (matches) {
3609 3610 if (need_fields.indexOf(matches[1]) == -1) {
3610 3611 need_fields.push(matches[1]);
3611 3612 }
3612 3613 } else {
3613 3614 if (need_fields.indexOf(key) == -1) {
3614 3615 need_fields.push(key);
3615 3616 }
3616 3617 }
3617 3618 }
3618 3619
3619 3620 // If all the keys we're searching for are in the QUICK_LOOKUP data, we
3620 3621 // don't need the full zone records to locate the VMs we're interested in.
3621 3622 for (key in need_fields) {
3622 3623 if (QUICK_LOOKUP.indexOf(key) === -1) {
3623 3624 quick_ok = false;
3624 3625 }
3625 3626 }
3626 3627
3627 3628 preload_opts.log = log;
3628 3629 if (options.hasOwnProperty('fields')) {
3629 3630 preload_opts.fields = need_fields;
3630 3631 }
3631 3632
3632 3633 // This is used when you've specified fields to remove those that might
3633 3634 // have been added as a group but are not wanted, or were added as
3634 3635 // dependencies for looking up wanted fields, or for search.
3635 3636 function filterFields(res) {
3636 3637 res.forEach(function (result) {
3637 3638 Object.keys(result).forEach(function (k) {
3638 3639 if (options.fields.indexOf(k) === -1) {
3639 3640 delete result[k];
3640 3641 }
3641 3642 });
3642 3643 });
3643 3644 }
3644 3645
3645 3646 preloadZoneData(null, preload_opts, function (err, data) {
3646 3647 var records = data.records;
3647 3648 var uuids = [];
3648 3649
3649 3650 if (err) {
3650 3651 callback(err);
3651 3652 return;
3652 3653 }
3653 3654
3654 3655 if (quick_ok) {
3655 3656 var full_results = [];
3656 3657 var load_opts = {};
3657 3658 var match;
3658 3659 var regex;
3659 3660 var source;
3660 3661 var target;
3661 3662 var u;
3662 3663 var z;
3663 3664
3664 3665 if (err) {
3665 3666 callback(err);
3666 3667 return;
3667 3668 }
3668 3669 for (z in records) {
3669 3670 z = records[z];
3670 3671 match = true;
3671 3672 for (key in search) {
3672 3673 regex = false;
3673 3674 // force field type to string so that earlier transformed
3674 3675 // number fields get back their match method and the
3675 3676 // strict not-equal operator will work on number lookups
3676 3677 source = '' + z[key];
3677 3678 target = search[key];
3678 3679 if (target[0] === '~') {
3679 3680 target = new RegExp(target.substr(1), 'i');
3680 3681 regex = true;
3681 3682 }
3682 3683 if (regex && !source.match(target)) {
3683 3684 match = false;
3684 3685 } else if (!regex && (source !== search[key])) {
3685 3686 match = false;
3686 3687 }
3687 3688 }
3688 3689 if (match && z.uuid) {
3689 3690 results.push(z.uuid);
3690 3691 }
3691 3692 }
3692 3693
3693 3694 load_opts.log = log;
3694 3695 if (options.hasOwnProperty('fields') && need_fields.length > 0) {
3695 3696 // we have a specific set of fields we want to grab
3696 3697 load_opts.fields = need_fields;
3697 3698 } else if (!options.full) {
3698 3699 // we don't need all the data so what we already got is enough
3699 3700 if (options.hasOwnProperty('fields')) {
3700 3701 filterFields(results);
3701 3702 }
3702 3703
3703 3704 callback(null,
3704 3705 results.filter(function (res) {
3705 3706 if (typeof (res) === 'object') {
3706 3707 return (Object.keys(res).length > 0);
3707 3708 } else {
3708 3709 return (true);
3709 3710 }
3710 3711 })
3711 3712 );
3712 3713 return;
3713 3714 }
3714 3715
3715 3716 function expander(uuid, cb) {
3716 3717 loadVM(uuid, data, load_opts, function (e, obj) {
3717 3718 if (e) {
3718 3719 if (e.code === 'ENOENT') {
3719 3720 // zone likely was deleted since lookup, ignore
3720 3721 cb();
3721 3722 } else {
3722 3723 cb(e);
3723 3724 }
3724 3725 } else {
3725 3726 if (transform) {
3726 3727 transform(obj);
3727 3728 }
3728 3729 full_results.push(obj);
3729 3730 cb();
3730 3731 }
3731 3732 });
3732 3733 }
3733 3734
3734 3735 async.forEachSeries(results, expander, function (e) {
3735 3736 var res_list;
3736 3737
3737 3738 if (e) {
3738 3739 log.error(e, 'VM.lookup failed to expand results: '
3739 3740 + e.message);
3740 3741 callback(e);
3741 3742 } else {
3742 3743 res_list = full_results;
3743 3744 if (options.hasOwnProperty('fields')) {
3744 3745 filterFields(res_list);
3745 3746 }
3746 3747 callback(null,
3747 3748 res_list.filter(function (res) {
3748 3749 if (typeof (res) === 'object') {
3749 3750 return (Object.keys(res).length > 0);
3750 3751 } else {
3751 3752 return (true);
3752 3753 }
3753 3754 })
3754 3755 );
3755 3756 }
3756 3757 });
3757 3758 } else {
3758 3759 // have to search the hard way (through all the data)
3759 3760 for (u in records) {
3760 3761 uuids.push(u);
3761 3762 }
3762 3763 // this is parallel!
3763 3764 async.forEach(uuids, function (uuid, cb) {
3764 3765 var vmobj = records[uuid];
3765 3766 var l_opts = {log: log};
3766 3767
3767 3768 if (options.hasOwnProperty('fields')
3768 3769 && need_fields.length > 0) {
3769 3770
3770 3771 // we have a specific set of fields we want to grab
3771 3772 l_opts.fields = need_fields;
3772 3773 }
3773 3774
3774 3775 loadVM(vmobj.uuid, data, l_opts, function (error, obj) {
3775 3776 if (error) {
3776 3777 if (error.code === 'ENOENT') {
3777 3778 // zone likely was deleted since lookup, ignore
3778 3779 cb();
3779 3780 } else {
3780 3781 cb(error);
3781 3782 }
3782 3783 } else {
3783 3784 if (transform) {
3784 3785 transform(obj);
3785 3786 }
3786 3787 if (Object.keys(search).length === 0
3787 3788 || matcher(obj, search)) {
3788 3789
3789 3790 results.push(obj);
3790 3791 }
3791 3792 cb();
3792 3793 }
3793 3794 });
3794 3795 }, function (e) {
3795 3796 var r;
3796 3797 var short_results = [];
3797 3798
3798 3799 if (e) {
3799 3800 callback(e);
3800 3801 } else {
3801 3802 if (options.full) {
3802 3803 callback(null, results);
3803 3804 } else if (options.fields && need_fields.length > 0) {
3804 3805 if (options.hasOwnProperty('fields')) {
3805 3806 filterFields(results);
3806 3807 }
3807 3808 callback(null,
3808 3809 results.filter(function (res) {
3809 3810 if (typeof (res) === 'object') {
3810 3811 return (Object.keys(res).length > 0);
3811 3812 } else {
3812 3813 return (true);
3813 3814 }
3814 3815 })
3815 3816 );
3816 3817 } else {
3817 3818 for (r in results) {
3818 3819 short_results.push(results[r].uuid);
3819 3820 }
3820 3821 callback(null, short_results);
3821 3822 }
3822 3823 }
3823 3824 });
3824 3825 }
3825 3826 });
3826 3827 };
3827 3828
3828 3829 // create a random new locally administered MAC address
3829 3830 function generateMAC()
3830 3831 {
3831 3832 var data = [(Math.floor(Math.random() * 15) + 1).toString(16) + 2];
3832 3833 for (var i = 0; i < 5; i++) {
3833 3834 var oct = (Math.floor(Math.random() * 255) + 1).toString(16);
3834 3835 if (oct.length == 1) {
3835 3836 oct = '0' + oct;
3836 3837 }
3837 3838 data.push(oct);
3838 3839 }
3839 3840
3840 3841 return data.join(':');
3841 3842 }
3842 3843
3843 3844 // return the MAC address based on a VRRP Virtual Router ID
3844 3845 function vrrpMAC(vrid) {
3845 3846 return sprintf('00:00:5e:00:01:%02x', vrid);
3846 3847 }
3847 3848
3848 3849 // Ensure we've got all the datasets necessary to create this VM
3849 3850 //
3850 3851 // IMPORTANT:
3851 3852 //
3852 3853 // On SmartOS, we assume a provisioner or some other external entity has already
3853 3854 // loaded the dataset into the system. This function just confirms that the
3854 3855 // dataset actually exists.
3855 3856 //
3856 3857 function checkDatasets(payload, log, callback)
3857 3858 {
3858 3859 var checkme = [];
3859 3860 var d;
3860 3861 var disk;
3861 3862
3862 3863 assert(log, 'no logger passed to checkDatasets()');
3863 3864
3864 3865 log.debug('Checking for required datasets.');
3865 3866
3866 3867 // build list of datasets we need to download (downloadme)
3867 3868 for (disk in payload.add_disks) {
3868 3869 if (payload.add_disks.hasOwnProperty(disk)) {
3869 3870 d = payload.add_disks[disk];
3870 3871 if (d.hasOwnProperty('image_uuid')) {
3871 3872 checkme.push(payload.zpool + '/'
3872 3873 + d.image_uuid);
3873 3874 }
3874 3875 }
3875 3876 }
3876 3877
3877 3878 function checker(dataset, cb) {
3878 3879 zfs(['list', '-o', 'name', '-H', dataset], log, function (err, fds) {
3879 3880 if (err) {
3880 3881 log.error({'err': err, 'stdout': fds.stdout,
3881 3882 'stderr': fds.stderr}, 'zfs list ' + dataset + ' '
3882 3883 + 'exited with' + ' code ' + err.code + ': ' + err.message);
3883 3884 cb(new Error('unable to find dataset: ' + dataset));
3884 3885 } else {
3885 3886 cb();
3886 3887 }
3887 3888 });
3888 3889 }
3889 3890
3890 3891 // check that we have all the volumes
3891 3892 async.forEachSeries(checkme, checker, function (err) {
3892 3893 if (err) {
3893 3894 log.error(err, 'checkDatasets() failed to find required '
3894 3895 + 'volumes');
3895 3896 callback(err);
3896 3897 } else {
3897 3898 // progress(100, 'we have all necessary datasets');
3898 3899 callback();
3899 3900 }
3900 3901 });
3901 3902 }
3902 3903
3903 3904 function lookupConflicts(macs, ips, vrids, log, callback) {
3904 3905 var conflict = false;
3905 3906 var load_fields;
3906 3907
3907 3908 load_fields = ['brand', 'state', 'nics', 'uuid', 'zonename', 'zone_state'];
3908 3909
3909 3910 assert(log, 'no logger passed to lookupConflicts()');
3910 3911
3911 3912 log.debug('checking for conflicts with '
3912 3913 + JSON.stringify(macs) + ', ' + JSON.stringify(ips) + ' and '
3913 3914 + JSON.stringify(vrids));
3914 3915
3915 3916 if (macs.length === 0 && ips.length === 0 && vrids.length === 0) {
3916 3917 log.debug('returning from conflict check (nothing to check)');
3917 3918 callback(null, conflict);
3918 3919 return;
3919 3920 }
3920 3921
3921 3922 preloadZoneData(null, {fields: load_fields, log: log},
3922 3923 function (err, data) {
3923 3924
3924 3925 var records = data.records;
3925 3926 var uuid;
3926 3927 var uuids = [];
3927 3928
3928 3929 if (err) {
3929 3930 callback(err);
3930 3931 return;
3931 3932 }
3932 3933
3933 3934 for (uuid in records) {
3934 3935 uuids.push(uuid);
3935 3936 }
3936 3937
3937 3938 // this is parallel!
3938 3939 async.forEach(uuids, function (z_uuid, cb) {
3939 3940 loadVM(z_uuid, data, {fields: load_fields, log: log},
3940 3941 function (error, obj) {
3941 3942
3942 3943 var ip;
3943 3944 var mac;
3944 3945 var vrid;
3945 3946
3946 3947 if (error) {
3947 3948 if (error.code === 'ENOENT') {
3948 3949 // zone likely was deleted since lookup, ignore it
3949 3950 cb();
3950 3951 } else {
3951 3952 cb(error);
3952 3953 }
3953 3954 return;
3954 3955 }
3955 3956
3956 3957 if (obj.state === 'failed' && obj.zone_state !== 'running') {
3957 3958 // Ignore zones that are failed unless they're 'running'
3958 3959 // which they shouldn't be because they get stopped on
3959 3960 // failure.
3960 3961 cb();
3961 3962 return;
3962 3963 }
3963 3964
3964 3965 for (ip in ips) {
3965 3966 if (ips[ip] !== 'dhcp'
3966 3967 && matcher(obj, {'nics.*.ip': ips[ip]})) {
3967 3968
3968 3969 log.error('Found conflict: ' + obj.uuid
3969 3970 + ' already has IP ' + ips[ip]);
3970 3971 conflict = true;
3971 3972 }
3972 3973 }
3973 3974 for (mac in macs) {
3974 3975 if (matcher(obj, {'nics.*.mac': macs[mac]})) {
3975 3976 log.error('Found conflict: ' + obj.uuid
3976 3977 + ' already has MAC ' + macs[mac]);
3977 3978 conflict = true;
3978 3979 }
3979 3980 }
3980 3981 for (vrid in vrids) {
3981 3982 if (matcher(obj, {'nics.*.vrrp_vrid': vrids[vrid]})) {
3982 3983 log.error('Found conflict: ' + obj.uuid
3983 3984 + ' already has VRID ' + vrids[vrid]);
3984 3985 conflict = true;
3985 3986 }
3986 3987 }
3987 3988 cb();
3988 3989 });
3989 3990 }, function (e) {
3990 3991 if (e) {
3991 3992 callback(e);
3992 3993 } else {
3993 3994 log.debug('returning from conflict check');
3994 3995 callback(null, conflict);
3995 3996 }
3996 3997 });
3997 3998 });
3998 3999 }
3999 4000
4000 4001 function lookupInvalidNicTags(nics, log, callback) {
4001 4002 var etherstubs = [];
4002 4003 var nic_tags = {};
4003 4004
4004 4005 assert(log, 'no logger passed to lookupInvalidNicTags()');
4005 4006
4006 4007 if (!nics || nics.length === 0) {
4007 4008 callback();
4008 4009 return;
4009 4010 }
4010 4011
4011 4012 async.parallel([
4012 4013 function (cb) {
4013 4014 dladm.showEtherstub(null, log, function (err, stubs) {
4014 4015 if (err) {
4015 4016 cb(err);
4016 4017 } else {
4017 4018 etherstubs = stubs;
4018 4019 cb();
4019 4020 }
4020 4021 });
4021 4022 }, function (cb) {
4022 4023 VM.getSysinfo([], {log: log}, function (err, sysinfo) {
4023 4024 if (err) {
4024 4025 cb(err);
4025 4026 } else {
4026 4027 var nic;
4027 4028 var tag;
4028 4029 for (nic in sysinfo['Network Interfaces']) {
4029 4030 nic = sysinfo['Network Interfaces'][nic];
4030 4031 for (tag in nic['NIC Names']) {
4031 4032 nic_tags[nic['NIC Names'][tag]] = 1;
4032 4033 }
4033 4034 }
4034 4035 cb();
4035 4036 }
4036 4037 });
4037 4038 }
4038 4039 ], function (err, results) {
4039 4040 if (err) {
4040 4041 callback(err);
4041 4042 return;
4042 4043 }
4043 4044
4044 4045 var nic;
4045 4046 for (nic in nics) {
4046 4047 nic = nics[nic];
4047 4048 if (!nic.hasOwnProperty('nic_tag')) {
4048 4049 continue;
4049 4050 }
4050 4051 if (!nic_tags.hasOwnProperty(nic.nic_tag)
4051 4052 && (etherstubs.indexOf(nic.nic_tag) === -1)) {
4052 4053 callback(new Error('Invalid nic tag "' + nic.nic_tag + '"'));
4053 4054 return;
4054 4055 }
4055 4056 }
4056 4057
4057 4058 callback();
4058 4059 return;
4059 4060 });
4060 4061 }
4061 4062
4062 4063 // create a new zvol for a VM
4063 4064 function createVolume(volume, log, callback)
4064 4065 {
4065 4066 var refreserv;
4066 4067 var size;
4067 4068 var snapshot;
4068 4069
4069 4070 assert(log, 'no logger passed for createVolume()');
4070 4071
4071 4072 log.debug('creating volume ' + JSON.stringify(volume));
4072 4073
4073 4074 if (volume.hasOwnProperty('image_size')) {
4074 4075 size = volume.image_size;
4075 4076 } else if (volume.hasOwnProperty('size')) {
4076 4077 size = volume.size;
4077 4078 } else {
4078 4079 callback(new Error('FATAL: createVolume(' + JSON.stringify(volume)
4079 4080 + '): ' + 'has no size or image_size'));
4080 4081 return;
4081 4082 }
4082 4083
4083 4084 if (volume.hasOwnProperty('refreservation')) {
4084 4085 refreserv = volume.refreservation;
4085 4086 } else {
4086 4087 log.debug('defaulting to refreservation = ' + size);
4087 4088 refreserv = size;
4088 4089 }
4089 4090
4090 4091 async.series([
4091 4092 function (cb) {
4092 4093 if (volume.hasOwnProperty('image_uuid')) {
4093 4094 snapshot = volume.zpool + '/' + volume.image_uuid + '@final';
4094 4095 zfs(['get', '-Ho', 'value', 'name', snapshot], log,
4095 4096 function (err, fds) {
4096 4097
4097 4098 if (err) {
4098 4099 if (fds.stderr.match('dataset does not exist')) {
4099 4100 // no @final, so we'll make a new snapshot @<uuid>
4100 4101 snapshot = volume.zpool + '/' + volume.image_uuid
4101 4102 + '@' + volume.uuid;
4102 4103
4103 4104 zfs(['snapshot', snapshot], log, function (e) {
4104 4105 cb(e);
4105 4106 });
4106 4107 } else {
4107 4108 cb(err);
4108 4109 }
4109 4110 } else {
4110 4111 // @final is here!
4111 4112 cb();
4112 4113 }
4113 4114 });
4114 4115 } else {
4115 4116 cb();
4116 4117 }
4117 4118 }, function (cb) {
4118 4119 var args;
4119 4120 var target;
4120 4121
4121 4122 target = volume.zpool + '/' + volume.uuid;
4122 4123 if (volume.hasOwnProperty('image_uuid')) {
4123 4124 // This volume is from a template/dataset/image so we create
4124 4125 // it as a clone of a the @final snapshot on the original.
4125 4126 // we already set 'snapshot' to the correct location above.
4126 4127 args = ['clone', '-F'];
4127 4128 if (volume.hasOwnProperty('compression')) {
4128 4129 args.push('-o', 'compression='
4129 4130 + volume.compression);
4130 4131 }
4131 4132 if (volume.hasOwnProperty('block_size')) {
4132 4133 args.push('-o', 'volblocksize='
4133 4134 + volume.block_size);
4134 4135 }
4135 4136 args.push('-o', 'refreservation=' + refreserv + 'M');
4136 4137 args.push(snapshot, target);
4137 4138 zfs(args, log, function (e) {
4138 4139 if (e) {
4139 4140 cb(e);
4140 4141 } else {
4141 4142 volume.path = '/dev/zvol/rdsk/' + target;
4142 4143 cb();
4143 4144 }
4144 4145 });
4145 4146 } else {
4146 4147 // This volume is not from a template/dataset/image so we create
4147 4148 // a blank new zvol for it.
4148 4149 args = ['create'];
4149 4150 if (volume.hasOwnProperty('compression')) {
4150 4151 args.push('-o', 'compression='
4151 4152 + volume.compression);
4152 4153 }
4153 4154 if (volume.hasOwnProperty('block_size')) {
4154 4155 args.push('-o', 'volblocksize='
4155 4156 + volume.block_size);
4156 4157 }
4157 4158 args.push('-o', 'refreservation=' + refreserv + 'M', '-V',
4158 4159 size + 'M', target);
4159 4160 zfs(args, log, function (err, fds) {
4160 4161 if (err) {
4161 4162 cb(err);
4162 4163 } else {
4163 4164 volume.path = '/dev/zvol/rdsk/' + target;
4164 4165 cb();
4165 4166 }
4166 4167 });
4167 4168 }
4168 4169 }
4169 4170 ], function (err, results) {
4170 4171 callback(err);
4171 4172 });
4172 4173 }
4173 4174
4174 4175 // Create all the volumes for a given VM property set
4175 4176 function createVolumes(payload, log, callback)
4176 4177 {
4177 4178 var createme = [];
4178 4179 var d;
4179 4180 var disk;
4180 4181 var disk_idx = 0;
4181 4182 var used_disk_indexes = [];
4182 4183
4183 4184 assert(log, 'no logger passed to createVolumes()');
4184 4185
4185 4186 log.debug('creating volumes: ' + JSON.stringify(payload.add_disks));
4186 4187
4187 4188 if (payload.hasOwnProperty('used_disk_indexes')) {
4188 4189 used_disk_indexes = payload.used_disk_indexes;
4189 4190 }
4190 4191
4191 4192 for (disk in payload.add_disks) {
4192 4193 if (payload.add_disks.hasOwnProperty(disk)) {
4193 4194 d = payload.add_disks[disk];
4194 4195
4195 4196 // we don't create CDROM devices or disk devices which have the
4196 4197 // nocreate: true property.
4197 4198 if (d.media !== 'cdrom' && !d.nocreate) {
4198 4199 // skip to the next unused one.
4199 4200 while (used_disk_indexes.indexOf(disk_idx) !== -1) {
4200 4201 disk_idx++;
4201 4202 }
4202 4203
4203 4204 d.index = disk_idx;
4204 4205 d.uuid = payload.uuid + '-disk' + disk_idx;
4205 4206 used_disk_indexes.push(Number(disk_idx));
4206 4207 if (!d.hasOwnProperty('zpool')) {
4207 4208 d.zpool = payload.zpool;
4208 4209 }
4209 4210 createme.push(d);
4210 4211 }
4211 4212 }
4212 4213 }
4213 4214
4214 4215 function loggedCreateVolume(volume, cb) {
4215 4216 return createVolume(volume, log, cb);
4216 4217 }
4217 4218
↓ open down ↓ |
4138 lines elided |
↑ open up ↑ |
4218 4219 // create all the volumes we found that we need.
4219 4220 async.forEachSeries(createme, loggedCreateVolume, function (err) {
4220 4221 if (err) {
4221 4222 callback(err);
4222 4223 } else {
4223 4224 callback();
4224 4225 }
4225 4226 });
4226 4227 }
4227 4228
4229 +function writeAndRename(log, name, destfile, file_data, callback)
4230 +{
4231 + var tempfile = destfile + '.new';
4232 +
4233 + log.debug('writing ' + name + ' to ' + tempfile);
4234 +
4235 + fs.writeFile(tempfile, file_data, function (err) {
4236 + if (err) {
4237 + callback(err);
4238 + return;
4239 + }
4240 +
4241 + log.debug('wrote ' + name + ' to ' + tempfile);
4242 + log.debug('renaming from ' + tempfile + ' to ' + destfile);
4243 +
4244 + fs.rename(tempfile, destfile, function (_err) {
4245 + if (_err) {
4246 + callback(_err);
4247 + return;
4248 + }
4249 +
4250 + log.debug('renamed from ' + tempfile + ' to ' + destfile);
4251 + callback();
4252 + });
4253 + });
4254 +}
4255 +
4228 4256 // writes a Zone's metadata JSON to /zones/<uuid>/config/metadata.json
4229 4257 // and /zones/<uuid>/config/tags.json.
4230 4258 function updateMetadata(vmobj, payload, log, callback)
4231 4259 {
4232 4260 var cmdata = {};
4233 4261 var imdata = {};
4234 4262 var key;
4235 4263 var mdata = {};
4236 4264 var mdata_filename;
4237 4265 var tags = {};
4238 4266 var tags_filename;
4239 4267 var zonepath;
4240 4268
4241 4269 assert(log, 'no logger passed to updateMetadata()');
4242 4270
4243 4271 if (vmobj.hasOwnProperty('zonepath')) {
4244 4272 zonepath = vmobj.zonepath;
4245 4273 } else if (vmobj.hasOwnProperty('zpool')
4246 4274 && vmobj.hasOwnProperty('zonename')) {
4247 4275
4248 4276 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename;
4249 4277 } else {
4250 4278 callback(new Error('unable to find zonepath for '
4251 4279 + JSON.stringify(vmobj)));
4252 4280 return;
4253 4281 }
4254 4282
4255 4283 // paths are under zonepath but not zoneroot
4256 4284 mdata_filename = zonepath + '/config/metadata.json';
4257 4285 tags_filename = zonepath + '/config/tags.json';
4258 4286
4259 4287 // customer_metadata
4260 4288 for (key in vmobj.customer_metadata) {
4261 4289 if (vmobj.customer_metadata.hasOwnProperty(key)) {
4262 4290 cmdata[key] = vmobj.customer_metadata[key];
4263 4291 if (payload.hasOwnProperty('remove_customer_metadata')
4264 4292 && payload.remove_customer_metadata.indexOf(key) !== -1) {
4265 4293
4266 4294 // in the remove_* list, don't load it.
4267 4295 delete cmdata[key];
4268 4296 }
4269 4297 }
4270 4298 }
4271 4299
4272 4300 for (key in payload.set_customer_metadata) {
4273 4301 if (payload.set_customer_metadata.hasOwnProperty(key)) {
4274 4302 cmdata[key] = payload.set_customer_metadata[key];
4275 4303 }
4276 4304 }
4277 4305
4278 4306 // internal_metadata
4279 4307 for (key in vmobj.internal_metadata) {
4280 4308 if (vmobj.internal_metadata.hasOwnProperty(key)) {
4281 4309 imdata[key] = vmobj.internal_metadata[key];
4282 4310 if (payload.hasOwnProperty('remove_internal_metadata')
4283 4311 && payload.remove_internal_metadata.indexOf(key) !== -1) {
4284 4312
4285 4313 // in the remove_* list, don't load it.
4286 4314 delete imdata[key];
4287 4315 }
4288 4316 }
4289 4317 }
4290 4318
4291 4319 for (key in payload.set_internal_metadata) {
4292 4320 if (payload.set_internal_metadata.hasOwnProperty(key)) {
4293 4321 imdata[key] = payload.set_internal_metadata[key];
4294 4322 }
4295 4323 }
4296 4324
4297 4325 // same thing for tags
4298 4326 for (key in vmobj.tags) {
4299 4327 if (vmobj.tags.hasOwnProperty(key)) {
4300 4328 tags[key] = vmobj.tags[key];
4301 4329 if (payload.hasOwnProperty('remove_tags')
4302 4330 && payload.remove_tags.indexOf(key) !== -1) {
4303 4331
4304 4332 // in the remove_* list, don't load it.
4305 4333 delete tags[key];
4306 4334 }
↓ open down ↓ |
69 lines elided |
↑ open up ↑ |
4307 4335 }
4308 4336 }
4309 4337
4310 4338 for (key in payload.set_tags) {
4311 4339 if (payload.set_tags.hasOwnProperty(key)) {
4312 4340 tags[key] = payload.set_tags[key];
4313 4341 }
4314 4342 }
4315 4343
4316 4344 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 - }
4345 +
4346 + async.series([
4347 + function (next) {
4348 + writeAndRename(log, 'metadata', mdata_filename,
4349 + JSON.stringify(mdata, null, 2), next);
4350 + },
4351 + function (next) {
4352 + writeAndRename(log, 'tags', tags_filename,
4353 + JSON.stringify(tags, null, 2), next);
4334 4354 }
4335 - );
4355 + ], callback);
4336 4356 }
4337 4357
4338 4358 function saveMetadata(payload, log, callback)
4339 4359 {
4340 4360 var protovm = {};
4341 4361
4342 4362 assert(log, 'no logger passed to saveMetadata()');
4343 4363
4344 4364 if (!payload.hasOwnProperty('zonepath')
4345 4365 || !payload.hasOwnProperty('zpool')
4346 4366 || !payload.hasOwnProperty('zonename')) {
4347 4367
4348 4368 callback(new Error('saveMetadata payload is missing zone '
4349 4369 + 'properties.'));
4350 4370 return;
4351 4371 }
4352 4372
4353 4373 protovm.zonepath = payload.zonepath;
4354 4374 protovm.zpool = payload.zpool;
4355 4375 protovm.zonename = payload.zonename;
4356 4376 protovm.customer_metadata = {};
4357 4377 protovm.tags = {};
4358 4378
4359 4379 if (payload.hasOwnProperty('tags')) {
4360 4380 payload.set_tags = payload.tags;
4361 4381 delete payload.tags;
4362 4382 }
4363 4383 if (payload.hasOwnProperty('customer_metadata')) {
4364 4384 payload.set_customer_metadata = payload.customer_metadata;
4365 4385 delete payload.customer_metadata;
4366 4386 }
4367 4387 if (payload.hasOwnProperty('internal_metadata')) {
4368 4388 payload.set_internal_metadata = payload.internal_metadata;
4369 4389 delete payload.internal_metadata;
4370 4390 }
4371 4391
4372 4392 updateMetadata(protovm, payload, log, callback);
4373 4393 }
4374 4394
4375 4395 // writes a zone's metadata JSON to /zones/<uuid>/config/routes.json
4376 4396 function updateRoutes(vmobj, payload, log, callback)
4377 4397 {
4378 4398 var filename;
4379 4399 var key;
4380 4400 var routes = {};
4381 4401 var zonepath;
4382 4402
4383 4403 assert(log, 'no logger passed to updateRoutes()');
4384 4404
4385 4405 if (vmobj.hasOwnProperty('zonepath')) {
4386 4406 zonepath = vmobj.zonepath;
4387 4407 } else if (vmobj.hasOwnProperty('zpool')
4388 4408 && vmobj.hasOwnProperty('zonename')) {
4389 4409
4390 4410 zonepath = '/' + vmobj.zpool + '/' + vmobj.zonename;
4391 4411 } else {
4392 4412 callback(new Error('unable to find zonepath for '
4393 4413 + JSON.stringify(vmobj)));
4394 4414 return;
4395 4415 }
4396 4416
4397 4417 // paths are under zonepath but not zoneroot
4398 4418 filename = zonepath + '/config/routes.json';
4399 4419
4400 4420 for (key in vmobj.routes) {
4401 4421 if (vmobj.routes.hasOwnProperty(key)) {
4402 4422 routes[key] = vmobj.routes[key];
4403 4423 if (payload.hasOwnProperty('remove_routes')
4404 4424 && payload.remove_routes.indexOf(key) !== -1) {
4405 4425
4406 4426 // in the remove_* list, don't load it.
4407 4427 delete routes[key];
4408 4428 }
4409 4429 }
4410 4430 }
4411 4431
4412 4432 for (key in payload.set_routes) {
4413 4433 if (payload.set_routes.hasOwnProperty(key)) {
4414 4434 routes[key] = payload.set_routes[key];
4415 4435 }
4416 4436 }
4417 4437
4418 4438 fs.writeFile(filename, JSON.stringify(routes, null, 2),
4419 4439 function (err) {
4420 4440 if (err) {
4421 4441 callback(err);
4422 4442 } else {
4423 4443 log.debug('wrote routes to ' + filename);
4424 4444 callback();
4425 4445 }
4426 4446 });
4427 4447 }
4428 4448
4429 4449 function saveRoutes(payload, log, callback)
4430 4450 {
4431 4451 var protovm = {};
4432 4452
4433 4453 assert(log, 'no logger passed to saveRoutes()');
4434 4454
4435 4455 if (!payload.hasOwnProperty('zonepath')
4436 4456 || !payload.hasOwnProperty('zpool')
4437 4457 || !payload.hasOwnProperty('zonename')) {
4438 4458
4439 4459 callback(new Error('saveRoutes payload is missing zone '
4440 4460 + 'properties.'));
4441 4461 return;
4442 4462 }
4443 4463
4444 4464 protovm.zonepath = payload.zonepath;
4445 4465 protovm.zpool = payload.zpool;
4446 4466 protovm.zonename = payload.zonename;
4447 4467
4448 4468 if (payload.hasOwnProperty('routes')) {
4449 4469 payload.set_routes = payload.routes;
4450 4470 delete payload.routes;
4451 4471 }
4452 4472
4453 4473 updateRoutes(protovm, payload, log, callback);
4454 4474 }
4455 4475
4456 4476 function createVM(payload, log, callback)
4457 4477 {
4458 4478 assert(log, 'no logger passed to createVM()');
4459 4479
4460 4480 async.series([
4461 4481 function (cb) {
4462 4482 if (!payload.create_only) {
4463 4483 // progress(2, 'checking required datasets');
4464 4484 checkDatasets(payload, log, cb);
4465 4485 } else {
4466 4486 cb();
4467 4487 }
4468 4488 }, function (cb) {
4469 4489 if (!payload.create_only) {
4470 4490 // progress(29, 'creating volumes');
4471 4491 createVolumes(payload, log, cb);
4472 4492 } else {
4473 4493 cb();
4474 4494 }
4475 4495 }, function (cb) {
4476 4496 // progress(51, 'creating zone container');
4477 4497 createZone(payload, log, cb);
4478 4498 }
4479 4499 ], function (err, results) {
4480 4500 if (err) {
4481 4501 callback(err);
4482 4502 } else {
4483 4503 callback(null, results);
4484 4504 }
4485 4505 });
4486 4506 }
4487 4507
4488 4508 function fixZoneinitMetadataSock(zoneroot, log, callback)
4489 4509 {
4490 4510 var mdata_00;
4491 4511
4492 4512 // ensure we're safe to touch these files, zone should not be running here
4493 4513 // so this just guards against malicious datasets.
4494 4514 ['/var/zoneinit/includes', '/root/zoneinit.d'].forEach(function (dir) {
4495 4515 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true});
4496 4516 });
4497 4517
4498 4518 function replaceData(filename, cb) {
4499 4519 fs.readFile(filename, 'utf8', function (error, data) {
4500 4520 if (error) {
4501 4521 log.error(error, 'failed to load 00-mdata.sh for replacement');
4502 4522 cb(error);
4503 4523 return;
4504 4524 }
4505 4525
4506 4526 data = data.replace(/\/var\/run\/smartdc\/metadata.sock/g,
4507 4527 '/.zonecontrol/metadata.sock');
4508 4528
4509 4529 log.trace('writing [' + data + '] to ' + filename);
4510 4530 fs.writeFile(filename, data, 'utf8', function (err) {
4511 4531 if (err) {
4512 4532 log.error(err, 'failed to write ' + filename);
4513 4533 }
4514 4534 cb(err);
4515 4535 });
4516 4536 });
4517 4537 }
4518 4538
4519 4539 // try /var/zoneinit/includes/00-mdata.sh first, since that's in new images
4520 4540 mdata_00 = path.join(zoneroot, '/var/zoneinit/includes/00-mdata.sh');
4521 4541 fs.exists(mdata_00, function (exists1) {
4522 4542 if (exists1) {
4523 4543 log.info('fixing socket in /var/zoneinit/includes/00-mdata.sh');
4524 4544 replaceData(mdata_00, callback);
4525 4545 } else {
4526 4546 // didn't exist, so try location it exists in older images eg. 1.6.3
4527 4547 mdata_00 = path.join(zoneroot, '/root/zoneinit.d/00-mdata.sh');
4528 4548 fs.exists(mdata_00, function (exists2) {
4529 4549 if (exists2) {
4530 4550 log.info('fixing socket in /root/zoneinit.d/00-mdata.sh');
4531 4551 replaceData(mdata_00, callback);
4532 4552 } else {
4533 4553 log.info('no 00-mdata.sh to cleanup in zoneinit');
4534 4554 callback();
4535 4555 }
4536 4556 });
4537 4557 }
4538 4558 });
4539 4559 }
4540 4560
4541 4561 function fixMdataFetchStart(zonepath, log, callback)
4542 4562 {
4543 4563 // svccfg validates zonepath
4544 4564 var mdata_fetch_start = '/lib/svc/method/mdata-fetch';
4545 4565
4546 4566 svccfg(zonepath, ['-s', 'svc:/smartdc/mdata:fetch', 'setprop', 'start/exec',
4547 4567 '=', mdata_fetch_start], log, function (error, stdio) {
4548 4568
4549 4569 if (error) {
4550 4570 log.error(error, 'failed to set mdata:fetch start method');
4551 4571 } else {
4552 4572 log.info('successfully set mdata:fetch start method');
4553 4573 }
4554 4574
4555 4575 callback(error);
4556 4576 });
4557 4577 }
4558 4578
4559 4579 function cleanupMessyDataset(zonepath, brand, log, callback)
4560 4580 {
4561 4581 var command;
4562 4582 var zoneroot = path.join(zonepath, '/root');
4563 4583
4564 4584 assert(log, 'no logger passed to cleanupMessyDataset()');
4565 4585
4566 4586 try {
4567 4587 ['/var/adm', '/var/svc/log', '/var/svc/manifest', '/root/zoneinit.d']
4568 4588 .forEach(function (dir) {
4569 4589
4570 4590 // This will ensure these are safe if they exist.
4571 4591 assertSafeZonePath(zoneroot, dir, {type: 'dir', enoent_ok: true});
4572 4592 });
4573 4593 } catch (e) {
4574 4594 log.error(e, 'Unable to cleanup dataset: ' + e.message);
4575 4595 callback(e);
4576 4596 return;
4577 4597 }
4578 4598
4579 4599 // We've verified the directories here exist, and have no symlinks in the
4580 4600 // path (or don't exist) so rm -f <dir>/<file> should be safe regardless of
4581 4601 // the type of <file>
4582 4602
4583 4603 command = 'rm -f '
4584 4604 + zoneroot + '/var/adm/utmpx '
4585 4605 + zoneroot + '/var/adm/wtmpx '
4586 4606 + zoneroot + '/var/svc/log/*.log '
4587 4607 + zoneroot + '/var/svc/mdata '
4588 4608 + zoneroot + '/var/svc/manifest/mdata.xml ';
4589 4609
4590 4610 if (! BRAND_OPTIONS[brand].features.zoneinit) {
4591 4611 // eg. joyent-minimal (don't need zoneinit)
4592 4612 command = command + zoneroot + '/root/zoneinit.xml '
4593 4613 + zoneroot + '/root/zoneinit '
4594 4614 + '&& rm -rf ' + zoneroot + '/root/zoneinit.d ';
4595 4615 }
4596 4616
4597 4617 command = command + '&& touch ' + zoneroot + '/var/adm/wtmpx';
4598 4618 log.debug(command);
4599 4619 exec(command, function (error, stdout, stderr) {
4600 4620 log.debug({err: error, stdout: stdout, stderr: stderr},
4601 4621 'returned from cleaning up dataset');
4602 4622 if (error || !BRAND_OPTIONS[brand].features.zoneinit) {
4603 4623 // either we already failed or this zone doesn't use zoneinit so
4604 4624 // we don't need to bother fixing zoneinit's scripts.
4605 4625 callback(error);
4606 4626 } else {
4607 4627 fixZoneinitMetadataSock(zoneroot, log, function (err) {
4608 4628 // See OS-2314, currently we assume all zones w/ zoneinit also
4609 4629 // have broken mdata:fetch when images are created from them.
4610 4630 // Attempt to fix that too.
4611 4631 fixMdataFetchStart(zonepath, log, callback);
4612 4632 });
4613 4633 }
4614 4634 });
4615 4635 }
4616 4636
4617 4637 // Helper for unlinking and replacing a file that you've already confirmed
4618 4638 // has no symlinks. Throws error when fs.writeFileSync does, or when
4619 4639 // fs.unlinkSync throws non ENOENT.
4620 4640 function replaceFile(zoneroot, filename, data) {
4621 4641 // first delete, in case file itself is a link
4622 4642 try {
4623 4643 fs.unlinkSync(path.join(zoneroot, filename));
4624 4644 } catch (e) {
4625 4645 if (e.code !== 'ENOENT') {
4626 4646 throw e;
4627 4647 }
4628 4648 }
4629 4649
4630 4650 fs.writeFileSync(path.join(zoneroot, filename), data);
4631 4651 }
4632 4652
4633 4653 // NOTE: we write these out initially before the zone is started, but after that
4634 4654 // rely on mdata-fetch in the zone to do the updates since we can't safely write
4635 4655 // these files in the zones.
4636 4656 function writeZoneNetfiles(payload, log, callback)
4637 4657 {
4638 4658 var hostname;
4639 4659 var n;
4640 4660 var nic;
4641 4661 var primary_found = false;
4642 4662 var zoneroot;
4643 4663
4644 4664 assert(log, 'no logger passed to writeZoneNetfiles()');
4645 4665 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload');
4646 4666
4647 4667 zoneroot = payload.zonepath + '/root';
4648 4668
4649 4669 try {
4650 4670 assertSafeZonePath(zoneroot, '/etc', {type: 'dir', enoent_ok: true});
4651 4671 } catch (e) {
4652 4672 log.error(e, 'Unable to write zone net files: ' + e.message);
4653 4673 callback(e);
4654 4674 return;
4655 4675 }
4656 4676
4657 4677 log.info('Writing network files to zone root');
4658 4678
4659 4679 try {
4660 4680 for (nic in payload.add_nics) {
4661 4681 if (payload.add_nics.hasOwnProperty(nic)) {
4662 4682 n = payload.add_nics[nic];
4663 4683
4664 4684 if (n.ip != 'dhcp') {
4665 4685 replaceFile(zoneroot, '/etc/hostname.'
4666 4686 + n.interface, n.ip + ' netmask ' + n.netmask
4667 4687 + ' up' + '\n');
4668 4688 }
4669 4689
4670 4690 if (n.hasOwnProperty('primary') && !primary_found) {
4671 4691 // only allow one primary network
4672 4692 primary_found = true;
4673 4693 if (n.hasOwnProperty('gateway')) {
4674 4694 replaceFile(zoneroot, '/etc/defaultrouter',
4675 4695 n.gateway + '\n');
4676 4696 }
4677 4697 if (n.ip == 'dhcp') {
4678 4698 replaceFile(zoneroot, '/etc/dhcp.' + n.interface, '');
4679 4699 }
4680 4700 }
4681 4701 }
4682 4702 }
4683 4703
4684 4704 // It's possible we don't have zonename or hostname set because of the
4685 4705 // ordering of adding the UUID. In any case, we'll have at least a uuid
4686 4706 // here.
4687 4707 if (payload.hasOwnProperty('hostname')) {
4688 4708 hostname = payload.hostname;
4689 4709 } else if (payload.hasOwnProperty('zonename')) {
4690 4710 hostname = payload.zonename;
4691 4711 } else {
4692 4712 hostname = payload.uuid;
4693 4713 }
4694 4714
4695 4715 replaceFile(zoneroot, '/etc/nodename', hostname + '\n');
4696 4716 } catch (e) {
4697 4717 log.error(e, 'Unable to write zone networking files: ' + e.message);
4698 4718 callback(e);
4699 4719 return;
4700 4720 }
4701 4721
4702 4722 callback();
4703 4723 }
4704 4724
4705 4725 /*
4706 4726 * NOTE: once we no longer support old datasets that need the 'zoneconfig' file,
4707 4727 * this function and calls to it can be removed.
4708 4728 *
4709 4729 * This writes out the zoneconfig file that is used by the zoneinit service in
4710 4730 * joyent branded zones' datasets.
4711 4731 *
4712 4732 */
4713 4733 function writeZoneconfig(payload, log, callback)
4714 4734 {
4715 4735 var data;
4716 4736 var hostname;
4717 4737 var n;
4718 4738 var nic;
4719 4739 var zoneroot;
4720 4740
4721 4741 assert(log, 'no logger passed to writeZoneconfig()');
4722 4742 assert(payload.hasOwnProperty('zonepath'), 'no .zonepath in payload');
4723 4743
4724 4744 zoneroot = payload.zonepath + '/root';
4725 4745
4726 4746 log.info('Writing config for zoneinit');
4727 4747
4728 4748 if (payload.hasOwnProperty('hostname')) {
4729 4749 hostname = payload.hostname;
4730 4750 } else {
4731 4751 hostname = payload.zonename;
4732 4752 }
4733 4753
4734 4754 data = 'TEMPLATE_VERSION=0.0.1\n'
4735 4755 + 'ZONENAME=' + payload.zonename + '\n'
4736 4756 + 'HOSTNAME=' + hostname + '.' + payload.dns_domain + '\n'
4737 4757 + 'TMPFS=' + payload.tmpfs + 'm\n';
4738 4758
4739 4759 if (payload.hasOwnProperty('add_nics') && payload.add_nics[0]) {
4740 4760
4741 4761 if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') {
4742 4762 data = data + 'PUBLIC_IP=' + payload.add_nics[0].ip + '\n';
4743 4763 }
4744 4764 if (payload.add_nics[1] && payload.add_nics[1].ip != 'dhcp') {
4745 4765 data = data + 'PRIVATE_IP=' + payload.add_nics[1].ip + '\n';
4746 4766 } else if (payload.add_nics[0] && payload.add_nics[0].ip != 'dhcp') {
4747 4767 // zoneinit uses private_ip for /etc/hosts, we want to
4748 4768 // make that same as public, if there's no actual private.
4749 4769 data = data + 'PRIVATE_IP=' + payload.add_nics[0].ip + '\n';
4750 4770 }
4751 4771 }
4752 4772
4753 4773 if (payload.hasOwnProperty('resolvers')) {
4754 4774 // zoneinit appends to resolv.conf rather than overwriting, so just
4755 4775 // add to the zoneconfig and let zoneinit handle it
4756 4776 data = data + 'RESOLVERS="' + payload.resolvers.join(' ') + '"\n';
4757 4777 }
4758 4778
4759 4779 for (nic in payload.add_nics) {
4760 4780 if (payload.add_nics.hasOwnProperty(nic)) {
4761 4781 n = payload.add_nics[nic];
4762 4782 data = data + n.interface.toUpperCase() + '_MAC=' + n.mac + '\n'
4763 4783 + n.interface.toUpperCase() + '_INTERFACE='
4764 4784 + n.interface.toUpperCase() + '\n';
4765 4785
4766 4786 if (n.ip != 'dhcp') {
4767 4787 data = data + n.interface.toUpperCase() + '_IP=' + n.ip + '\n'
4768 4788 + n.interface.toUpperCase() + '_NETMASK='
4769 4789 + n.netmask + '\n';
4770 4790 }
4771 4791 }
4772 4792 }
4773 4793
4774 4794 try {
4775 4795 assertSafeZonePath(zoneroot, '/var/svc/log/system-zoneinit:default.log',
4776 4796 {type: 'file', enoent_ok: true});
4777 4797 assertSafeZonePath(zoneroot, '/root/zoneconfig',
4778 4798 {type: 'file', enoent_ok: true});
4779 4799
4780 4800 replaceFile(zoneroot, '/var/svc/log/system-zoneinit:default.log', '');
4781 4801
4782 4802 log.debug('writing zoneconfig ' + JSON.stringify(data) + ' to '
4783 4803 + zoneroot);
4784 4804
4785 4805 replaceFile(zoneroot, '/root/zoneconfig', data);
4786 4806 callback();
4787 4807 } catch (e) {
4788 4808 log.error(e, 'Unable to write zoneconfig files: ' + e.message);
4789 4809 callback(e);
4790 4810 return;
4791 4811 }
4792 4812 }
4793 4813
4794 4814 function zonecfg(args, log, callback)
4795 4815 {
4796 4816 var cmd = '/usr/sbin/zonecfg';
4797 4817
4798 4818 assert(log, 'no logger passed to zonecfg()');
4799 4819
4800 4820 log.debug(cmd + ' ' + args.join(' '));
4801 4821 execFile(cmd, args, function (error, stdout, stderr) {
4802 4822 if (error) {
4803 4823 callback(error, {'stdout': stdout, 'stderr': stderr});
4804 4824 } else {
4805 4825 callback(null, {'stdout': stdout, 'stderr': stderr});
4806 4826 }
4807 4827 });
4808 4828 }
4809 4829
4810 4830 function zonecfgFile(data, args, log, callback)
4811 4831 {
4812 4832 var tmpfile = '/tmp/zonecfg.' + process.pid + '.tmp';
4813 4833
4814 4834 assert(log, 'no logger passed to zonecfgFile()');
4815 4835
4816 4836 fs.writeFile(tmpfile, data, function (err, result) {
4817 4837 if (err) {
4818 4838 // On failure we don't delete the tmpfile so we can debug it.
4819 4839 callback(err);
4820 4840 } else {
4821 4841 args.push('-f');
4822 4842 args.push(tmpfile);
4823 4843
4824 4844 zonecfg(args, log, function (e, fds) {
4825 4845 if (e) {
4826 4846 // keep temp file around for investigation
4827 4847 callback(e, fds);
4828 4848 } else {
4829 4849 fs.unlink(tmpfile, function () {
4830 4850 callback(null, fds);
4831 4851 });
4832 4852 }
4833 4853 });
4834 4854 }
4835 4855 });
4836 4856 }
4837 4857
4838 4858 function zoneadm(args, log, callback)
4839 4859 {
4840 4860 var cmd = '/usr/sbin/zoneadm';
4841 4861
4842 4862 assert(log, 'no logger passed to zoneadm()');
4843 4863
4844 4864 log.debug(cmd + ' ' + args.join(' '));
4845 4865 execFile(cmd, args, function (error, stdout, stderr) {
4846 4866 if (error) {
4847 4867 callback(error, {'stdout': stdout, 'stderr': stderr});
4848 4868 } else {
4849 4869 callback(null, {'stdout': stdout, 'stderr': stderr});
4850 4870 }
4851 4871 });
4852 4872 }
4853 4873
4854 4874 function zfs(args, log, callback)
4855 4875 {
4856 4876 var cmd = '/usr/sbin/zfs';
4857 4877
4858 4878 assert(log, 'no logger passed to zfs()');
4859 4879
4860 4880 log.debug(cmd + ' ' + args.join(' '));
4861 4881 execFile(cmd, args, function (error, stdout, stderr) {
4862 4882 if (error) {
4863 4883 callback(error, {'stdout': stdout, 'stderr': stderr});
4864 4884 } else {
4865 4885 callback(null, {'stdout': stdout, 'stderr': stderr});
4866 4886 }
4867 4887 });
4868 4888 }
4869 4889
4870 4890 exports.getSysinfo = function (args, options, callback)
4871 4891 {
4872 4892 var cmd = '/usr/bin/sysinfo';
4873 4893 var log;
4874 4894
4875 4895 // we used to allow just one argument (callback) and we also allow 2 args
4876 4896 // (args, callback) so that options is optional.
4877 4897 if (arguments.length === 1) {
4878 4898 callback = arguments[0];
4879 4899 args = [];
4880 4900 options = {};
4881 4901 }
4882 4902 if (arguments.length === 2) {
4883 4903 callback = arguments[1];
4884 4904 options = {};
4885 4905 }
4886 4906
4887 4907 ensureLogging(false);
4888 4908 if (options.hasOwnProperty('log')) {
4889 4909 log = options.log;
4890 4910 } else {
4891 4911 log = VM.log.child({action: 'getSysinfo'});
4892 4912 }
4893 4913
4894 4914 log.debug(cmd + ' ' + args.join(' '));
4895 4915 execFile(cmd, args, function (error, stdout, stderr) {
4896 4916 var sysinfo;
4897 4917
4898 4918 if (error) {
4899 4919 callback(error, {'stdout': stdout, 'stderr': stderr});
4900 4920 } else {
4901 4921 try {
4902 4922 sysinfo = JSON.parse(stdout.toString());
4903 4923 } catch (e) {
4904 4924 sysinfo = {};
4905 4925 }
4906 4926 callback(null, sysinfo);
4907 4927 }
4908 4928 });
4909 4929 };
4910 4930
4911 4931 /*
4912 4932 * This watches zone transitions and calls callback when specified
4913 4933 * state is reached. Optionally you can set a timeout which will
4914 4934 * call your callback when the timeout occurs whether the transition
4915 4935 * has happened or not.
4916 4936 *
4917 4937 * payload needs to have at least .zonename and .uuid
4918 4938 *
4919 4939 */
4920 4940 exports.waitForZoneState = function (payload, state, options, callback)
4921 4941 {
4922 4942 var log;
4923 4943 var sysevent_state;
4924 4944 var timeout;
4925 4945 var timeout_secs = PROVISION_TIMEOUT;
4926 4946 var watcher;
4927 4947
4928 4948 // options is optional
4929 4949 if (arguments.length === 3) {
4930 4950 callback = arguments[2];
4931 4951 options = {};
4932 4952 }
4933 4953
4934 4954 ensureLogging(false);
4935 4955 if (options.hasOwnProperty('log')) {
4936 4956 log = options.log;
4937 4957 } else {
4938 4958 log = VM.log.child({action: 'waitForZoneState', vm: payload.uuid});
4939 4959 }
4940 4960
4941 4961 if (options.hasOwnProperty('timeout')) {
4942 4962 timeout_secs = options.timeout;
4943 4963 }
4944 4964
4945 4965 sysevent_state = state;
4946 4966 if (state === 'installed') {
4947 4967 // Apparently the zone status 'installed' equals sysevent status
4948 4968 // 'uninitialized'
4949 4969 sysevent_state = 'uninitialized';
4950 4970 }
4951 4971
4952 4972 function done() {
4953 4973 if (timeout) {
4954 4974 clearTimeout(timeout);
4955 4975 timeout = null;
4956 4976 }
4957 4977 }
4958 4978
4959 4979 function handler(err, obj) {
4960 4980 if (err) {
4961 4981 done();
4962 4982 callback(err);
4963 4983 return;
4964 4984 }
4965 4985 log.trace('handler got: ' + JSON.stringify(obj));
4966 4986 if (obj.zonename !== payload.zonename) {
4967 4987 return;
4968 4988 }
4969 4989
4970 4990 if (obj.newstate === sysevent_state) {
4971 4991 // Load again to confirm
4972 4992 VM.lookup({'zonename': obj.zonename},
4973 4993 {fields: ['zone_state'], log: log},
4974 4994 function (error, res) {
4975 4995 var handler_retry;
4976 4996
4977 4997 if (error) {
4978 4998 watcher.cleanup();
4979 4999 done();
4980 5000 callback(error);
4981 5001 return;
4982 5002 }
4983 5003
4984 5004 if (res.length !== 1) {
4985 5005 watcher.cleanup();
4986 5006 done();
4987 5007 callback(new Error('lookup could no find VM '
4988 5008 + obj.zonename));
4989 5009 return;
4990 5010 }
4991 5011
4992 5012 if (res[0].hasOwnProperty('zone_state')
4993 5013 && res[0].zone_state === state) {
4994 5014
4995 5015 // found the state we're looking for, success!
4996 5016 log.debug('saw zone go to ' + obj.newstate + ' ('
4997 5017 + state + ') calling callback()');
4998 5018 watcher.cleanup();
4999 5019 done();
5000 5020 callback();
5001 5021 } else if (timeout) {
5002 5022 // we saw a state change to a state we don't care about
5003 5023 // so if we've not timed out try reloading again in a
5004 5024 // second.
5005 5025 if (!handler_retry) {
5006 5026 handler_retry = setTimeout(function () {
5007 5027 if (timeout) {
5008 5028 // try again if wait timeout is still set
5009 5029 handler(null, obj);
5010 5030 }
5011 5031 handler_retry = null;
5012 5032 }, 1000);
5013 5033 log.debug('zone state after lookup: '
5014 5034 + res[0].zone_state + ', still waiting');
5015 5035 } else {
5016 5036 log.debug('zone in wrong state but we already'
5017 5037 + ' have a handler running');
5018 5038 }
5019 5039 } else {
5020 5040 // no timeout set and we're not at the correct state
5021 5041 log.error('failed to reach state: ' + state);
5022 5042 callback(new Error('failed to reach state: ' + state));
5023 5043 }
5024 5044 }
5025 5045 );
5026 5046 }
5027 5047 }
5028 5048
5029 5049 watcher = watchZoneTransitions(handler, log);
5030 5050
5031 5051 timeout = setTimeout(function () {
5032 5052 var err;
5033 5053
5034 5054 done();
5035 5055 watcher.cleanup();
5036 5056 err = new Error('timed out waiting for zone to transition to ' + state);
5037 5057 err.code = 'ETIMEOUT';
5038 5058 callback(err);
5039 5059 }, timeout_secs * 1000);
5040 5060
5041 5061 // after we've started the watcher (if we checked before there'd be a race)
5042 5062 // we check whether we're already in the target state, if we are close it
5043 5063 // down and return.
5044 5064 VM.load(payload.uuid, {fields: ['zone_state'], log: log},
5045 5065 function (err, obj) {
5046 5066
5047 5067 if (err) {
5048 5068 watcher.cleanup();
5049 5069 done();
5050 5070 callback(err);
5051 5071 } else if (obj.hasOwnProperty('zone_state')
5052 5072 && obj.zone_state === state) {
5053 5073
5054 5074 watcher.cleanup();
5055 5075 done();
5056 5076 log.info('VM is in state ' + state);
5057 5077 callback(); // at correct state!
5058 5078 }
5059 5079 });
5060 5080 };
5061 5081
5062 5082 // handler() will be called with an object describing the transition for any
5063 5083 // transitions seen (after any filtering). The only filtering here is to remove
5064 5084 // duplicate events. Other filtering should be done by the caller.
5065 5085 function watchZoneTransitions(handler, log) {
5066 5086 var buffer = '';
5067 5087 var chunks;
5068 5088 var cleanup;
5069 5089 var watcher;
5070 5090 var watcher_pid;
5071 5091
5072 5092 assert(log, 'no logger passed to watchZoneTransitions()');
5073 5093
5074 5094 if (!zoneevent) {
5075 5095
5076 5096 zoneevent = new EventEmitter();
5077 5097
5078 5098 log.debug('/usr/vm/sbin/zoneevent');
5079 5099 watcher = spawn('/usr/vm/sbin/zoneevent', [],
5080 5100 {'customFds': [-1, -1, -1]});
5081 5101 log.debug('zoneevent running with pid ' + watcher.pid);
5082 5102 watcher_pid = watcher.pid;
5083 5103
5084 5104 watcher.stdout.on('data', function (data) {
5085 5105 var chunk;
5086 5106 var obj;
5087 5107 var prev_msg;
5088 5108
5089 5109 buffer += data.toString();
5090 5110 chunks = buffer.split('\n');
5091 5111 while (chunks.length > 1) {
5092 5112 chunk = chunks.shift();
5093 5113 obj = JSON.parse(chunk);
5094 5114
5095 5115 if (obj === prev_msg) {
5096 5116 // Note: sometimes sysevent emits multiple events for the
5097 5117 // same status, we only want the first one here because just
5098 5118 // because sysevent does it, doesn't make it right.
5099 5119 log.debug('duplicate zoneevent message! '
5100 5120 + JSON.stringify(obj));
5101 5121 } else if (zoneevent) {
5102 5122 zoneevent.emit('zoneevent', null, obj);
5103 5123 }
5104 5124 }
5105 5125 buffer = chunks.pop();
5106 5126 });
5107 5127
5108 5128 // doesn't take input.
5109 5129 watcher.stdin.end();
5110 5130
5111 5131 watcher.on('exit', function (code) {
5112 5132 log.warn('zoneevent watcher ' + watcher_pid + ' exited: ',
5113 5133 JSON.stringify(code));
5114 5134 // tell all the listeners of this zoneevent (if there are any) that
5115 5135 // we exited. Then null it out so next time we'll make a new one.
5116 5136 zoneevent.emit('zoneevent', new Error('zoneevent watcher exited '
5117 5137 + 'prematurely with code: ' + code));
5118 5138 zoneevent = null;
5119 5139 });
5120 5140 }
5121 5141
5122 5142 cleanup = function () {
5123 5143 var listeners;
5124 5144
5125 5145 if (zoneevent) {
5126 5146 listeners = zoneevent.listeners('zoneevent');
5127 5147
5128 5148 log.debug('cleanup called w/ listeners: '
5129 5149 + util.inspect(listeners));
5130 5150 zoneevent.removeListener('zoneevent', handler);
5131 5151 if (zoneevent.listeners('zoneevent').length === 0) {
5132 5152 log.debug('zoneevent watcher ' + watcher_pid
5133 5153 + ' cleanup called');
5134 5154 zoneevent = null;
5135 5155 if (watcher) {
5136 5156 watcher.stdout.destroy(); // so we don't send more 'data'
5137 5157 watcher.stderr.destroy();
5138 5158 watcher.removeAllListeners('exit'); // so don't fail on kill
5139 5159 watcher.kill();
5140 5160 watcher = null;
5141 5161 }
5142 5162 }
5143 5163 } else if (watcher) {
5144 5164 watcher.stdout.destroy(); // so we don't send more 'data'
5145 5165 watcher.stderr.destroy();
5146 5166 watcher.removeAllListeners('exit'); // so don't fail on our kill
5147 5167 watcher.kill();
5148 5168 watcher = null;
5149 5169 }
5150 5170 };
5151 5171
5152 5172 zoneevent.on('zoneevent', handler);
5153 5173
5154 5174 return ({'cleanup': cleanup});
5155 5175 }
5156 5176
5157 5177 function fixPayloadMemory(payload, vmobj, log)
5158 5178 {
5159 5179 var brand;
5160 5180 var max_locked;
5161 5181 var max_phys;
5162 5182 var min_overhead;
5163 5183 var ram;
5164 5184
5165 5185 assert(log, 'no logger passed to fixPayloadMemory()');
5166 5186
5167 5187 if (vmobj.hasOwnProperty('brand')) {
5168 5188 brand = vmobj.brand;
5169 5189 } else if (payload.hasOwnProperty('brand')) {
5170 5190 brand = payload.brand;
5171 5191 }
5172 5192
5173 5193 if (BRAND_OPTIONS[brand].features.default_memory_overhead
5174 5194 && payload.hasOwnProperty('ram')
5175 5195 && !payload.hasOwnProperty('max_physical_memory')) {
5176 5196
5177 5197 // For now we add overhead to the memory caps for KVM zones, this
5178 5198 // is for the qemu process itself. Since customers don't have direct
5179 5199 // access to zone memory, this exists mostly to protect against bugs.
5180 5200 payload.max_physical_memory = (payload.ram
5181 5201 + BRAND_OPTIONS[brand].features.default_memory_overhead);
5182 5202 } else if (payload.hasOwnProperty('ram')
5183 5203 && !payload.hasOwnProperty('max_physical_memory')) {
5184 5204
5185 5205 payload.max_physical_memory = payload.ram;
5186 5206 }
5187 5207
5188 5208 if (payload.hasOwnProperty('max_physical_memory')) {
5189 5209 if (!payload.hasOwnProperty('max_locked_memory')) {
5190 5210 if (vmobj.hasOwnProperty('max_locked_memory')
5191 5211 && vmobj.hasOwnProperty('max_physical_memory')) {
5192 5212
5193 5213 // we don't have a new value, so first try to keep the same
5194 5214 // delta that existed before btw. max_phys and max_locked
5195 5215 payload.max_locked_memory = payload.max_physical_memory
5196 5216 - (vmobj.max_physical_memory - vmobj.max_locked_memory);
5197 5217 } else {
5198 5218 // existing obj doesn't have max_locked, add one now
5199 5219 payload.max_locked_memory = payload.max_physical_memory;
5200 5220 }
5201 5221 }
5202 5222
5203 5223 if (!payload.hasOwnProperty('max_swap')) {
5204 5224 if (vmobj.hasOwnProperty('max_swap')
5205 5225 && vmobj.hasOwnProperty('max_physical_memory')) {
5206 5226
5207 5227 // we don't have a new value, so first try to keep the same
5208 5228 // delta that existed before btw. max_phys and max_swap
5209 5229 if (vmobj.max_swap === MINIMUM_MAX_SWAP
5210 5230 && vmobj.max_swap <= MINIMUM_MAX_SWAP
5211 5231 && payload.max_physical_memory >= MINIMUM_MAX_SWAP) {
5212 5232 // in this case we artificially inflated before to meet
5213 5233 // minimum tie back to ram.
5214 5234 payload.max_swap = payload.max_physical_memory;
5215 5235 } else {
5216 5236 payload.max_swap = payload.max_physical_memory
5217 5237 + (vmobj.max_swap - vmobj.max_physical_memory);
5218 5238 }
5219 5239 } else {
5220 5240 // existing obj doesn't have max_swap, add one now
5221 5241 payload.max_swap = payload.max_physical_memory;
5222 5242 }
5223 5243
5224 5244 // never add a max_swap less than MINIMUM_MAX_SWAP
5225 5245 if (payload.max_swap < MINIMUM_MAX_SWAP) {
5226 5246 payload.max_swap = MINIMUM_MAX_SWAP;
5227 5247 }
5228 5248 }
5229 5249 }
5230 5250
5231 5251 // if we're updating tmpfs it must be lower than our new max_physical or
5232 5252 // if we're not also changing max_physical, it must be lower than the
5233 5253 // current one.
5234 5254 if (payload.hasOwnProperty('tmpfs')) {
5235 5255 if (payload.hasOwnProperty('max_physical_memory')
5236 5256 && (Number(payload.tmpfs)
5237 5257 > Number(payload.max_physical_memory))) {
5238 5258
5239 5259 payload.tmpfs = payload.max_physical_memory;
5240 5260 } else if (Number(payload.tmpfs)
5241 5261 > Number(vmobj.max_physical_memory)) {
5242 5262
5243 5263 payload.tmpfs = vmobj.max_physical_memory;
5244 5264 }
5245 5265 }
5246 5266
5247 5267 if (payload.hasOwnProperty('max_physical_memory')
5248 5268 && BRAND_OPTIONS[brand].features.use_tmpfs
5249 5269 && !payload.hasOwnProperty('tmpfs')) {
5250 5270
5251 5271 if (vmobj.hasOwnProperty('max_physical_memory')
5252 5272 && vmobj.hasOwnProperty('tmpfs')) {
5253 5273
5254 5274 // change tmpfs to be the same ratio of ram as before
5255 5275 payload.tmpfs = ((vmobj.tmpfs / vmobj.max_physical_memory)
5256 5276 * payload.max_physical_memory);
5257 5277 payload.tmpfs = Number(payload.tmpfs).toFixed();
5258 5278 } else {
5259 5279 // tmpfs must be < max_physical_memory, if not: pretend it was
5260 5280 payload.tmpfs = payload.max_physical_memory;
5261 5281 }
5262 5282 }
5263 5283
5264 5284 // now that we've possibly adjusted target values, lower/raise values to
5265 5285 // satisify max/min.
5266 5286
5267 5287 min_overhead = BRAND_OPTIONS[brand].features.min_memory_overhead;
5268 5288 if (min_overhead) {
5269 5289 ram = payload.hasOwnProperty('ram') ? payload.ram : vmobj.ram;
5270 5290 max_phys = payload.hasOwnProperty('max_physical_memory')
5271 5291 ? payload.max_physical_memory : vmobj.max_physical_memory;
5272 5292 max_locked = payload.hasOwnProperty('max_locked_memory')
5273 5293 ? payload.max_locked_memory : vmobj.max_locked_memory;
5274 5294
5275 5295 if ((ram + min_overhead) > max_phys) {
5276 5296 payload.max_physical_memory = (ram + min_overhead);
5277 5297 }
5278 5298 if ((ram + min_overhead) > max_locked) {
5279 5299 payload.max_locked_memory = (ram + min_overhead);
5280 5300 }
5281 5301 }
5282 5302
5283 5303 if (payload.hasOwnProperty('max_locked_memory')) {
5284 5304 if (payload.hasOwnProperty('max_physical_memory')) {
5285 5305 if (payload.max_locked_memory > payload.max_physical_memory) {
5286 5306 log.warn('max_locked_memory (' + payload.max_locked_memory
5287 5307 + ') > max_physical_memory (' + payload.max_physical_memory
5288 5308 + ') clamping to ' + payload.max_physical_memory);
5289 5309 payload.max_locked_memory = payload.max_physical_memory;
5290 5310 }
5291 5311 } else if (vmobj.hasOwnProperty('max_physical_memory')) {
5292 5312 // new payload doesn't have a max_physical, so clamp to vmobj's
5293 5313 if (payload.max_locked_memory > vmobj.max_physical_memory) {
5294 5314 log.warn('max_locked_memory (' + payload.max_locked_memory
5295 5315 + ') > vm.max_physical_memory (' + vmobj.max_physical_memory
5296 5316 + ') clamping to ' + vmobj.max_physical_memory);
5297 5317 payload.max_locked_memory = vmobj.max_physical_memory;
5298 5318 }
5299 5319 }
5300 5320 }
5301 5321
5302 5322 if (payload.hasOwnProperty('max_swap')) {
5303 5323 if (payload.hasOwnProperty('max_physical_memory')) {
5304 5324 if (payload.max_swap < payload.max_physical_memory) {
5305 5325 log.warn('max_swap (' + payload.max_swap
5306 5326 + ') < max_physical_memory (' + payload.max_physical_memory
5307 5327 + ') raising to ' + payload.max_physical_memory);
5308 5328 payload.max_swap = payload.max_physical_memory;
5309 5329 }
5310 5330 } else if (vmobj.hasOwnProperty('max_physical_memory')) {
5311 5331 // new payload doesn't have a max_physical, so raise to vmobj's
5312 5332 if (payload.max_swap < vmobj.max_physical_memory) {
5313 5333 log.warn('max_swap (' + payload.max_swap
5314 5334 + ') < vm.max_physical_memory (' + vmobj.max_physical_memory
5315 5335 + ') raising to ' + vmobj.max_physical_memory);
5316 5336 payload.max_swap = vmobj.max_physical_memory;
5317 5337 }
5318 5338 }
5319 5339 }
5320 5340 }
5321 5341
5322 5342 // generate a new UUID if payload doesn't have one (also ensures that this uuid
5323 5343 // does not already belong to a zone).
5324 5344 function createZoneUUID(payload, log, callback)
5325 5345 {
5326 5346 var uuid;
5327 5347
5328 5348 assert(log, 'no logger passed to createZoneUUID()');
5329 5349
5330 5350 if (payload.hasOwnProperty('uuid')) {
5331 5351 // Ensure that the uuid is not already used.
5332 5352 getZoneRecords(null, log, function (err, records) {
5333 5353 if (err) {
5334 5354 callback(err);
5335 5355 } else {
5336 5356 if (records.hasOwnProperty(payload.uuid)) {
5337 5357 callback(new Error('vm with UUID ' + payload.uuid
5338 5358 + ' already exists.'));
5339 5359 } else {
5340 5360 callback(null, payload.uuid);
5341 5361 }
5342 5362 }
5343 5363 });
5344 5364 } else {
5345 5365 log.debug('/usr/bin/uuid -v 4');
5346 5366 execFile('/usr/bin/uuid', ['-v', '4'], function (err, stdout, stderr) {
5347 5367 if (err) {
5348 5368 callback(err);
5349 5369 return;
5350 5370 }
5351 5371
5352 5372 // chomp trailing spaces and newlines
5353 5373 uuid = stdout.toString().replace(/\s+$/g, '');
5354 5374 payload.uuid = uuid;
5355 5375 log.info('generated uuid ' + uuid + ' for new VM');
5356 5376 getZoneRecords(null, log, function (e, records) {
5357 5377 if (e) {
5358 5378 callback(e);
5359 5379 } else {
5360 5380 if (records.hasOwnProperty(payload.uuid)) {
5361 5381 callback(new Error('vm with UUID ' + payload.uuid
5362 5382 + 'already exists.'));
5363 5383 } else {
5364 5384 callback(null, payload.uuid);
5365 5385 }
5366 5386 }
5367 5387 });
5368 5388 });
5369 5389 }
5370 5390 }
5371 5391
5372 5392 function applyZoneDefaults(payload, log)
5373 5393 {
5374 5394 var allowed;
5375 5395 var disk;
5376 5396 var disks;
5377 5397 var n;
5378 5398 var nic;
5379 5399 var nics;
5380 5400 var zvol;
5381 5401
5382 5402 assert(log, 'no logger passed to applyZoneDefaults()');
5383 5403
5384 5404 log.debug('applying zone defaults');
5385 5405
5386 5406 if (!payload.hasOwnProperty('owner_uuid')) {
5387 5407 // We assume that this all-zero uuid can be treated as 'admin'
5388 5408 payload.owner_uuid = '00000000-0000-0000-0000-000000000000';
5389 5409 }
5390 5410
5391 5411 if (!payload.hasOwnProperty('autoboot')) {
5392 5412 payload.autoboot = true;
5393 5413 }
5394 5414
5395 5415 if (!payload.hasOwnProperty('brand')) {
5396 5416 payload.brand = 'joyent';
5397 5417 }
5398 5418
5399 5419 if (!payload.hasOwnProperty('zpool')) {
5400 5420 payload.zpool = 'zones';
5401 5421 }
5402 5422
5403 5423 if (!payload.hasOwnProperty('dns_domain')) {
5404 5424 payload.dns_domain = 'local';
5405 5425 }
5406 5426
5407 5427 if (!payload.hasOwnProperty('cpu_shares')) {
5408 5428 payload.cpu_shares = 100;
5409 5429 } else {
5410 5430 if (payload.cpu_shares > 65535) {
5411 5431 log.info('capping cpu_shares at 64k (was: '
5412 5432 + payload.cpu_shares + ')');
5413 5433 payload.cpu_shares = 65535; // max is 64K
5414 5434 }
5415 5435 }
5416 5436
5417 5437 if (!payload.hasOwnProperty('zfs_io_priority')) {
5418 5438 payload.zfs_io_priority = 100;
5419 5439 }
5420 5440
5421 5441 if (!payload.hasOwnProperty('max_lwps')) {
5422 5442 payload.max_lwps = 2000;
5423 5443 }
5424 5444
5425 5445 // We need to set the RAM here because we use it as the default for
5426 5446 // the max_physical_memory below. If we've set max_phys and we're not
5427 5447 // KVM, we'll use that instead of ram anyway.
5428 5448 if (!payload.hasOwnProperty('ram')) {
5429 5449 payload.ram = 256;
5430 5450 }
5431 5451
5432 5452 fixPayloadMemory(payload, {}, log);
5433 5453
5434 5454 allowed = BRAND_OPTIONS[payload.brand].allowed_properties;
5435 5455 if (allowed.hasOwnProperty('vcpus') && !payload.hasOwnProperty('vcpus')) {
5436 5456 payload.vcpus = 1;
5437 5457 }
5438 5458
5439 5459 if (BRAND_OPTIONS[payload.brand].features.use_tmpfs
5440 5460 && (!payload.hasOwnProperty('tmpfs')
5441 5461 || (Number(payload.tmpfs) > Number(payload.max_physical_memory)))) {
5442 5462
5443 5463 payload.tmpfs = payload.max_physical_memory;
5444 5464 }
5445 5465
5446 5466 if (!payload.hasOwnProperty('limit_priv')) {
5447 5467 // note: the limit privs are going to be added to the brand and
5448 5468 // shouldn't need to be set here by default when that's done.
5449 5469 if (BRAND_OPTIONS[payload.brand].features.limit_priv) {
5450 5470 payload.limit_priv
5451 5471 = BRAND_OPTIONS[payload.brand].features.limit_priv.join(',');
5452 5472 } else {
5453 5473 payload.limit_priv = 'default';
5454 5474 }
5455 5475 }
5456 5476
5457 5477 if (!payload.hasOwnProperty('quota')) {
5458 5478 payload.quota = '10'; // in GiB
5459 5479 }
5460 5480
5461 5481 if (!payload.hasOwnProperty('billing_id')) {
5462 5482 payload.billing_id = '00000000-0000-0000-0000-000000000000';
5463 5483 }
5464 5484
5465 5485 if (payload.hasOwnProperty('add_disks')) {
5466 5486 // update
5467 5487 disks = payload.add_disks;
5468 5488 } else if (payload.hasOwnProperty('disks')) {
5469 5489 disks = payload.disks;
5470 5490 } else {
5471 5491 // no disks at all
5472 5492 disks = [];
5473 5493 }
5474 5494
5475 5495 for (disk in disks) {
5476 5496 if (disks.hasOwnProperty(disk)) {
5477 5497 zvol = disks[disk];
5478 5498 if (!zvol.hasOwnProperty('model')
5479 5499 && payload.hasOwnProperty('disk_driver')) {
5480 5500
5481 5501 zvol.model = payload.disk_driver;
5482 5502 }
5483 5503 if (!zvol.hasOwnProperty('media')) {
5484 5504 zvol.media = 'disk';
5485 5505 }
5486 5506 }
5487 5507 }
5488 5508
5489 5509 if (payload.hasOwnProperty('add_nics')) {
5490 5510 // update
5491 5511 nics = payload.add_nics;
5492 5512 } else if (payload.hasOwnProperty('nics')) {
5493 5513 nics = payload.nics;
5494 5514 } else {
5495 5515 // no disks at all
5496 5516 nics = [];
5497 5517 }
5498 5518
5499 5519 for (nic in nics) {
5500 5520 if (nics.hasOwnProperty(nic)) {
5501 5521 n = nics[nic];
5502 5522 if (!n.hasOwnProperty('model')
5503 5523 && payload.hasOwnProperty('nic_driver')) {
5504 5524
5505 5525 n.model = payload.nic_driver;
5506 5526 }
5507 5527 }
5508 5528 }
5509 5529 }
5510 5530
5511 5531 function validRecordSize(candidate)
5512 5532 {
5513 5533 if (candidate < 512) {
5514 5534 // too low
5515 5535 return (false);
5516 5536 } else if (candidate > 131072) {
5517 5537 // too high
5518 5538 return (false);
5519 5539 } else if ((candidate & (candidate - 1)) !== 0) {
5520 5540 // not a power of 2
5521 5541 return (false);
5522 5542 }
5523 5543
5524 5544 return (true);
5525 5545 }
5526 5546
5527 5547 // This function gets called for both create and update to check that payload
5528 5548 // properties are reasonable. If vmobj is null, create is assumed, otherwise
5529 5549 // update is assumed.
5530 5550 function checkPayloadProperties(payload, vmobj, log, callback)
5531 5551 {
5532 5552 var array_fields = [
5533 5553 'add_nics', 'update_nics', 'remove_nics',
5534 5554 'add_disks', 'update_disks', 'remove_disks',
5535 5555 'add_filesystems', 'update_filesystems', 'remove_filesystems'
5536 5556 ];
5537 5557 var changed_nics = [];
5538 5558 var current_ips = [];
5539 5559 var current_macs = [];
5540 5560 var current_primary_ips = [];
5541 5561 var current_vrids = [];
5542 5562 var disk;
5543 5563 var dst;
5544 5564 var field;
5545 5565 var filesys;
5546 5566 var i;
5547 5567 var ips = [];
5548 5568 var is_nic = false;
5549 5569 var live_ok;
5550 5570 var mac;
5551 5571 var macs = [];
5552 5572 var m;
5553 5573 var n;
5554 5574 var nic;
5555 5575 var nics_result = {};
5556 5576 var nics_result_ordered = [];
5557 5577 var nic_fields = ['add_nics', 'update_nics'];
5558 5578 var only_vrrp_nics = true;
5559 5579 var primary_nics;
5560 5580 var prop;
5561 5581 var props;
5562 5582 var ram;
5563 5583 var route;
5564 5584 var routes_result = {};
5565 5585 var brand;
5566 5586 var vrids = [];
5567 5587 var zvol;
5568 5588
5569 5589 assert(log, 'no logger passed to checkPayloadProperties()');
5570 5590
5571 5591 if (vmobj) {
5572 5592 brand = vmobj.brand;
5573 5593 } else if (payload.hasOwnProperty('brand')) {
5574 5594 brand = payload.brand;
5575 5595 } else {
5576 5596 callback(new Error('unable to determine brand for VM'));
5577 5597 }
5578 5598
5579 5599 /* check types of fields that should be arrays */
5580 5600 for (field in array_fields) {
5581 5601 field = array_fields[field];
5582 5602 if (payload.hasOwnProperty(field) && ! Array.isArray(payload[field])) {
5583 5603 callback(new Error(field + ' must be an array.'));
5584 5604 return;
5585 5605 }
5586 5606 }
5587 5607
5588 5608 if (!vmobj) {
5589 5609 // This is a CREATE
5590 5610
5591 5611 // These should have already been enforced
5592 5612 if (payload.max_locked_memory > payload.max_physical_memory) {
5593 5613 callback(new Error('max_locked_memory must be <= '
5594 5614 + 'max_physical_memory'));
5595 5615 return;
5596 5616 }
5597 5617 if (payload.max_swap < payload.max_physical_memory) {
5598 5618 callback(new Error('max_swap must be >= max_physical_memory'));
5599 5619 return;
5600 5620 }
5601 5621
5602 5622 // We used to use zone_path instead of zonepath, so accept that too.
5603 5623 if (payload.hasOwnProperty('zone_path')
5604 5624 && !payload.hasOwnProperty('zonepath')) {
5605 5625
5606 5626 payload.zonepath = payload.zone_path;
5607 5627 delete payload.zone_path;
5608 5628 }
5609 5629 } else {
5610 5630 // This is an UPDATE
5611 5631
5612 5632 // can't update disks of a running VM
5613 5633 if (payload.hasOwnProperty('add_disks')
5614 5634 || payload.hasOwnProperty('remove_disks')) {
5615 5635
5616 5636 if ((vmobj.state !== 'stopped')
5617 5637 || (vmobj.state === 'provisioning'
5618 5638 && vmobj.zone_state !== 'installed')) {
5619 5639
5620 5640 callback(new Error('updates to disks are only allowed when '
5621 5641 + 'state is "stopped", currently: ' + vmobj.state + ' ('
5622 5642 + vmobj.zone_state + ')'));
5623 5643 return;
5624 5644 }
5625 5645 }
5626 5646
5627 5647 // For update_disks we can update refreservation and compression values
5628 5648 // while running. If there are other parameters to update though we'll
5629 5649 // reject.
5630 5650 if (payload.hasOwnProperty('update_disks')) {
5631 5651 if ((vmobj.state !== 'stopped')
5632 5652 || (vmobj.state === 'provisioning'
5633 5653 && vmobj.zone_state !== 'installed')) {
5634 5654
5635 5655 live_ok = true;
5636 5656
5637 5657 payload.update_disks.forEach(function (d) {
5638 5658 var key;
5639 5659 var keys = Object.keys(d);
5640 5660
5641 5661 while ((keys.length > 0) && live_ok) {
5642 5662 key = keys.pop();
5643 5663 if ([
5644 5664 'compression',
5645 5665 'path',
5646 5666 'refreservation'
5647 5667 ].indexOf(key) === -1) {
5648 5668
5649 5669 // this key is not allowed!
5650 5670 live_ok = false;
5651 5671 }
5652 5672 }
5653 5673 });
5654 5674
5655 5675 if (!live_ok) {
5656 5676 callback(new Error('at least one specified update to disks '
5657 5677 + 'is only allowed when state is "stopped", currently: '
5658 5678 + vmobj.state + ' (' + vmobj.zonestate + ')'));
5659 5679 return;
5660 5680 }
5661 5681 }
5662 5682 }
5663 5683
5664 5684 // if there's a min_overhead we ensure values are higher than ram.
5665 5685 if (BRAND_OPTIONS[brand].features.min_memory_overhead) {
5666 5686 if (payload.hasOwnProperty('ram')) {
5667 5687 ram = payload.ram;
5668 5688 } else {
5669 5689 ram = vmobj.ram;
5670 5690 }
5671 5691
5672 5692 // ensure none of these is < ram
5673 5693 if (payload.hasOwnProperty('max_physical_memory')
5674 5694 && payload.max_physical_memory < ram) {
5675 5695
5676 5696 callback(new Error('vm.max_physical_memory ('
5677 5697 + payload.max_physical_memory + ') cannot be lower than'
5678 5698 + ' vm.ram (' + ram + ')'));
5679 5699 return;
5680 5700 }
5681 5701 if (payload.hasOwnProperty('max_locked_memory')
5682 5702 && payload.max_locked_memory < ram) {
5683 5703
5684 5704 callback(new Error('vm.max_locked_memory ('
5685 5705 + payload.max_locked_memory + ') cannot be lower than'
5686 5706 + ' vm.ram (' + ram + ')'));
5687 5707 return;
5688 5708 }
5689 5709 // This should not be allowed anyway because max_swap will be raised
5690 5710 // to match max_physical_memory if you set it lower.
5691 5711 if (payload.hasOwnProperty('max_swap')) {
5692 5712 if (payload.max_swap < ram) {
5693 5713 callback(new Error('vm.max_swap ('
5694 5714 + payload.max_swap + ') cannot be lower than'
5695 5715 + ' vm.ram (' + ram + ')'));
5696 5716 return;
5697 5717 } else if (payload.max_swap < MINIMUM_MAX_SWAP) {
5698 5718 callback(new Error('vm.max_swap ('
5699 5719 + payload.max_swap + ') cannot be lower than '
5700 5720 + MINIMUM_MAX_SWAP + 'MiB'));
5701 5721 return;
5702 5722 }
5703 5723 }
5704 5724 }
5705 5725
5706 5726 /*
5707 5727 * keep track of current IPs/MACs so we can make sure they're not being
5708 5728 * duplicated.
5709 5729 *
5710 5730 */
5711 5731 for (nic in vmobj.nics) {
5712 5732 nic = vmobj.nics[nic];
5713 5733 if (nic.hasOwnProperty('ip') && nic.ip !== 'dhcp') {
5714 5734 current_ips.push(nic.ip);
5715 5735 }
5716 5736 if (nic.hasOwnProperty('mac')) {
5717 5737 current_macs.push(nic.mac);
5718 5738 }
5719 5739 if (nic.hasOwnProperty('vrrp_vrid')) {
5720 5740 current_vrids.push(nic.vrrp_vrid);
5721 5741 }
5722 5742 if (nic.hasOwnProperty('vrrp_primary_ip')) {
5723 5743 current_primary_ips.push(nic.vrrp_primary_ip);
5724 5744 }
5725 5745
5726 5746 if (nic.hasOwnProperty('mac') || nic.hasOwnProperty('vrrp_vrid')) {
5727 5747 mac = nic.hasOwnProperty('mac') ? nic.mac
5728 5748 : vrrpMAC(nic.vrrp_vrid);
5729 5749 if (!nics_result.hasOwnProperty(mac)) {
5730 5750 nics_result[mac] = nic;
5731 5751 nics_result_ordered.push(nic);
5732 5752 }
5733 5753 }
5734 5754 }
5735 5755
5736 5756 // Keep track of route additions / deletions, to make sure that
5737 5757 // we're not setting link-local routes against nics that don't exist
5738 5758 for (route in vmobj.routes) {
5739 5759 routes_result[route] = vmobj.routes[route];
5740 5760 }
5741 5761 }
5742 5762
5743 5763 if (payload.hasOwnProperty('add_disks')) {
5744 5764 for (disk in payload.add_disks) {
5745 5765 if (payload.add_disks.hasOwnProperty(disk)) {
5746 5766 zvol = payload.add_disks[disk];
5747 5767
5748 5768 // path is only allowed in 2 cases when adding a disk:
5749 5769 //
5750 5770 // 1) for cdrom devices
5751 5771 // 2) when nocreate is specified
5752 5772 //
5753 5773 if (zvol.hasOwnProperty('path')) {
5754 5774 if (zvol.media !== 'cdrom' && !zvol.nocreate) {
5755 5775 callback(new Error('you cannot specify a path for a '
5756 5776 + 'disk unless you set nocreate=true'));
5757 5777 return;
5758 5778 }
5759 5779 }
5760 5780
5761 5781 // NOTE: We'll have verified the .zpool argument is a valid
5762 5782 // zpool using VM.validate() if it's set.
5763 5783
5764 5784 if (zvol.hasOwnProperty('block_size')
5765 5785 && !validRecordSize(zvol.block_size)) {
5766 5786
5767 5787 callback(new Error('invalid .block_size(' + zvol.block_size
5768 5788 + '), must be 512-131072 and a power of 2.'));
5769 5789 return;
5770 5790 }
5771 5791
5772 5792 if (zvol.hasOwnProperty('block_size')
5773 5793 && zvol.hasOwnProperty('image_uuid')) {
5774 5794
5775 5795 callback(new Error('setting both .block_size and '
5776 5796 + '.image_uuid on a volume is invalid'));
5777 5797 }
5778 5798
5779 5799 if (zvol.hasOwnProperty('compression')) {
5780 5800 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) {
5781 5801 callback(new Error('invalid compression setting for '
5782 5802 + 'disk, must be one of: '
5783 5803 + VM.COMPRESSION_TYPES.join(', ')));
5784 5804 }
5785 5805 }
5786 5806
5787 5807 if (!zvol.hasOwnProperty('model')
5788 5808 || zvol.model === 'undefined') {
5789 5809
5790 5810 if (vmobj && vmobj.hasOwnProperty('disk_driver')) {
5791 5811 zvol.model = vmobj.disk_driver;
5792 5812 log.debug('set model to ' + zvol.model
5793 5813 + ' from disk_driver');
5794 5814 } else if (vmobj && vmobj.hasOwnProperty('disks')
5795 5815 && vmobj.disks.length > 0 && vmobj.disks[0].model) {
5796 5816
5797 5817 zvol.model = vmobj.disks[0].model;
5798 5818 log.debug('set model to ' + zvol.model + ' from disk0');
5799 5819 } else {
5800 5820 callback(new Error('missing .model option for '
5801 5821 + 'disk: ' + JSON.stringify(zvol)));
5802 5822 return;
5803 5823 }
5804 5824 } else if (VM.DISK_MODELS.indexOf(zvol.model) === -1) {
5805 5825 callback(new Error('"' + zvol.model + '"'
5806 5826 + ' is not a valid disk model. Valid are: '
5807 5827 + VM.DISK_MODELS.join(',')));
5808 5828 return;
5809 5829 }
5810 5830 }
5811 5831 }
5812 5832 }
5813 5833
5814 5834 if (payload.hasOwnProperty('update_disks')) {
5815 5835 for (disk in payload.update_disks) {
5816 5836 if (payload.update_disks.hasOwnProperty(disk)) {
5817 5837 zvol = payload.update_disks[disk];
5818 5838
5819 5839 if (zvol.hasOwnProperty('compression')) {
5820 5840 if (VM.COMPRESSION_TYPES.indexOf(zvol.compression) === -1) {
5821 5841 callback(new Error('invalid compression type for '
5822 5842 + 'disk, must be one of: '
5823 5843 + VM.COMPRESSION_TYPES.join(', ')));
5824 5844 }
5825 5845 }
5826 5846
5827 5847 if (zvol.hasOwnProperty('block_size')) {
5828 5848 callback(new Error('cannot change .block_size for a disk '
5829 5849 + 'after creation'));
5830 5850 return;
5831 5851 }
5832 5852 }
5833 5853 }
5834 5854 }
5835 5855
5836 5856 // If we're receiving, we might not have the filesystem yet
5837 5857 if (!payload.hasOwnProperty('transition')
5838 5858 || payload.transition.transition !== 'receiving') {
5839 5859
5840 5860 for (filesys in payload.filesystems) {
5841 5861 filesys = payload.filesystems[filesys];
5842 5862 if (!fs.existsSync(filesys.source)) {
5843 5863 callback(new Error('missing requested filesystem: '
5844 5864 + filesys.source));
5845 5865 return;
5846 5866 }
5847 5867 }
5848 5868 }
5849 5869
5850 5870 if (payload.hasOwnProperty('default_gateway')
5851 5871 && payload.default_gateway !== '') {
5852 5872
5853 5873 log.warn('DEPRECATED: default_gateway should no longer be used, '
5854 5874 + 'instead set one NIC primary and use nic.gateway.');
5855 5875 }
5856 5876
5857 5877 primary_nics = 0;
5858 5878 for (field in nic_fields) {
5859 5879 field = nic_fields[field];
5860 5880 if (payload.hasOwnProperty(field)) {
5861 5881 for (nic in payload[field]) {
5862 5882 if (payload[field].hasOwnProperty(nic)) {
5863 5883 n = payload[field][nic];
5864 5884
5865 5885 // MAC will always conflict in update, since that's the key
5866 5886 if (field === 'add_nics' && n.hasOwnProperty('mac')) {
5867 5887 if ((macs.indexOf(n.mac) !== -1)
5868 5888 || current_macs.indexOf(n.mac) !== -1) {
5869 5889
5870 5890 callback(new Error('Cannot add multiple NICs with '
5871 5891 + 'the same MAC: ' + n.mac));
5872 5892 return;
5873 5893 }
5874 5894 macs.push(n.mac);
5875 5895 }
5876 5896
5877 5897 if (field === 'add_nics' || field === 'update_nics') {
5878 5898 if (n.hasOwnProperty('primary')) {
5879 5899 if (n.primary !== true) {
5880 5900 callback(new Error('invalid value for NIC\'s '
5881 5901 + 'primary flag: ' + n.primary + ' (must be'
5882 5902 + ' true)'));
5883 5903 return;
5884 5904 }
5885 5905 primary_nics++;
5886 5906 }
5887 5907 changed_nics.push(n);
5888 5908 }
5889 5909
5890 5910 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') {
5891 5911 if (ips.indexOf(n.ip) !== -1
5892 5912 || current_ips.indexOf(n.ip) !== -1) {
5893 5913
5894 5914 callback(new Error('Cannot add multiple NICs with '
5895 5915 + 'the same IP: ' + n.ip));
5896 5916 return;
5897 5917 }
5898 5918 ips.push(n.ip);
5899 5919 }
5900 5920
5901 5921 if (n.hasOwnProperty('vrrp_vrid')) {
5902 5922 if (current_vrids.indexOf(n.vrrp_vrid) !== -1
5903 5923 || vrids.indexOf(n.vrrp_vrid) !== -1) {
5904 5924 callback(new Error('Cannot add multiple NICs with '
5905 5925 + 'the same VRID: ' + n.vrrp_vrid));
5906 5926 return;
5907 5927 }
5908 5928 vrids.push(n.vrrp_vrid);
5909 5929 }
5910 5930
5911 5931 if (field === 'add_nics'
5912 5932 && n.hasOwnProperty('vrrp_vrid')
5913 5933 && n.hasOwnProperty('mac')) {
5914 5934 callback(
5915 5935 new Error('Cannot set both mac and vrrp_vrid'));
5916 5936 return;
5917 5937 }
5918 5938
5919 5939 if (n.hasOwnProperty('vrrp_primary_ip')) {
5920 5940 current_primary_ips.push(n.vrrp_primary_ip);
5921 5941 }
5922 5942
5923 5943 if (BRAND_OPTIONS[brand].features.model_required
5924 5944 && field === 'add_nics'
5925 5945 && (!n.hasOwnProperty('model') || !n.model
5926 5946 || n.model === 'undefined' || n.model.length === 0)) {
5927 5947
5928 5948
5929 5949 if (vmobj && vmobj.hasOwnProperty('nic_driver')) {
5930 5950 n.model = vmobj.nic_driver;
5931 5951 log.debug('set model to ' + n.model
5932 5952 + ' from nic_driver');
5933 5953 } else if (vmobj && vmobj.hasOwnProperty('nics')
5934 5954 && vmobj.nics.length > 0 && vmobj.nics[0].model) {
5935 5955
5936 5956 n.model = vmobj.nics[0].model;
5937 5957 log.debug('set model to ' + n.model + ' from nic0');
5938 5958 } else {
5939 5959 callback(new Error('missing .model option for NIC: '
5940 5960 + JSON.stringify(n)));
5941 5961 return;
5942 5962 }
5943 5963 }
5944 5964
5945 5965 if (field === 'add_nics' && n.ip !== 'dhcp'
5946 5966 && (!n.hasOwnProperty('netmask')
5947 5967 || !net.isIPv4(n.netmask))) {
5948 5968
5949 5969 callback(new Error('invalid or missing .netmask option '
5950 5970 + 'for NIC: ' + JSON.stringify(n)));
5951 5971 return;
5952 5972 }
5953 5973
5954 5974 if ((field === 'add_nics' || field === 'update_nics')
5955 5975 && n.hasOwnProperty('ip') && n.ip !== 'dhcp'
5956 5976 && !net.isIPv4(n.ip)) {
5957 5977
5958 5978 callback(new Error('invalid IP for NIC: '
5959 5979 + JSON.stringify(n)));
5960 5980 return;
5961 5981 }
5962 5982
5963 5983 if (field === 'add_nics' && (!n.hasOwnProperty('nic_tag')
5964 5984 || !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/))) {
5965 5985
5966 5986 callback(new Error('invalid or missing .nic_tag option '
5967 5987 + 'for NIC: ' + JSON.stringify(n)));
5968 5988 return;
5969 5989 }
5970 5990
5971 5991 if (field === 'update_nics' && n.hasOwnProperty('model')
5972 5992 && (!n.model || n.model === 'undefined'
5973 5993 || n.model.length === 0)) {
5974 5994
5975 5995 callback(new Error('invalid .model option for NIC: '
5976 5996 + JSON.stringify(n)));
5977 5997 return;
5978 5998 }
5979 5999
5980 6000 if (field === 'update_nics' && n.hasOwnProperty('netmask')
5981 6001 && (!n.netmask || !net.isIPv4(n.netmask))) {
5982 6002
5983 6003 callback(new Error('invalid .netmask option for NIC: '
5984 6004 + JSON.stringify(n)));
5985 6005 return;
5986 6006 }
5987 6007
5988 6008 if (field === 'update_nics' && n.hasOwnProperty('nic_tag')
5989 6009 && !n.nic_tag.match(/^[a-zA-Z0-9\_]+$/)) {
5990 6010
5991 6011 callback(new Error('invalid .nic_tag option for NIC: '
5992 6012 + JSON.stringify(n)));
5993 6013 return;
5994 6014 }
5995 6015
5996 6016 if (n.hasOwnProperty('mac')
5997 6017 || n.hasOwnProperty('vrrp_vrid')) {
5998 6018 mac = n.hasOwnProperty('mac') ? n.mac
5999 6019 : vrrpMAC(n.vrrp_vrid);
6000 6020 if (nics_result.hasOwnProperty(mac)) {
6001 6021 var p;
6002 6022 for (p in n) {
6003 6023 nics_result[mac][p] = n[p];
6004 6024 }
6005 6025
6006 6026 nics_result_ordered.forEach(function (on) {
6007 6027 if (on.hasOwnProperty('mac') && on.mac == mac) {
6008 6028 for (p in n) {
6009 6029 on[p] = n[p];
6010 6030 }
6011 6031 }
6012 6032 });
6013 6033 } else {
6014 6034 nics_result[mac] = n;
6015 6035 nics_result_ordered.push(n);
6016 6036 }
6017 6037 }
6018 6038
6019 6039 if ((field === 'add_nics' || field === 'update_nics')
6020 6040 && n.hasOwnProperty('allowed_ips')) {
6021 6041 try {
6022 6042 validateIPlist(n.allowed_ips);
6023 6043 } catch (ipListErr) {
6024 6044 callback(ipListErr);
6025 6045 return;
6026 6046 }
6027 6047 }
6028 6048 }
6029 6049 }
6030 6050 }
6031 6051 }
6032 6052
6033 6053 if (payload.hasOwnProperty('remove_nics')) {
6034 6054 for (m in payload.remove_nics) {
6035 6055 m = payload.remove_nics[m];
6036 6056 n = nics_result[m];
6037 6057 if (!n) {
6038 6058 continue;
6039 6059 }
6040 6060 if (n.hasOwnProperty('ip') && n.ip != 'dhcp') {
6041 6061 i = ips.indexOf(n.ip);
6042 6062 if (i !== -1) {
6043 6063 ips.splice(i, 1);
6044 6064 }
6045 6065 i = current_ips.indexOf(n.ip);
6046 6066 if (i !== -1) {
6047 6067 current_ips.splice(i, 1);
6048 6068 }
6049 6069 }
6050 6070 delete nics_result[m];
6051 6071
6052 6072 for (i in nics_result_ordered) {
6053 6073 n = nics_result_ordered[i];
6054 6074 if (n.hasOwnProperty('mac') && n.mac == m) {
6055 6075 nics_result_ordered.splice(i, 1);
6056 6076 break;
6057 6077 }
6058 6078 }
6059 6079 }
6060 6080 }
6061 6081
6062 6082 // nics_result now has the state of the nics after the update - now check
6063 6083 // properties that depend on each other or on other nics
6064 6084 for (n in nics_result) {
6065 6085 n = nics_result[n];
6066 6086 if (n.hasOwnProperty('vrrp_vrid')) {
6067 6087 if (n.hasOwnProperty('ip')
6068 6088 && current_primary_ips.indexOf(n.ip) !== -1) {
6069 6089 callback(
6070 6090 new Error(
6071 6091 'Cannot set vrrp_primary_ip to the IP of a VRRP nic'));
6072 6092 return;
6073 6093 }
6074 6094
6075 6095 if (!n.hasOwnProperty('vrrp_primary_ip')) {
6076 6096 callback(new Error(
6077 6097 'vrrp_vrid set but not vrrp_primary_ip'));
6078 6098 return;
6079 6099 }
6080 6100 } else {
6081 6101 only_vrrp_nics = false;
6082 6102 }
6083 6103 }
6084 6104
6085 6105 if (only_vrrp_nics && Object.keys(nics_result).length !== 0) {
6086 6106 callback(new Error('VM cannot contain only VRRP nics'));
6087 6107 return;
6088 6108 }
6089 6109
6090 6110 for (i in current_primary_ips) {
6091 6111 i = current_primary_ips[i];
6092 6112 if ((current_ips.indexOf(i) === -1)
6093 6113 && (ips.indexOf(i) === -1)) {
6094 6114 callback(new Error(
6095 6115 'vrrp_primary_ip must belong to the same VM'));
6096 6116 return;
6097 6117 }
6098 6118 }
6099 6119
6100 6120 // Since we always need a primary nic, don't allow a value other than true
6101 6121 // for primary flag. Also ensure we're not trying to set primary for more
6102 6122 // than one nic.
6103 6123 if (primary_nics > 1) {
6104 6124 callback(new Error('payload specifies more than 1 primary NIC'));
6105 6125 return;
6106 6126 }
6107 6127
6108 6128 if (payload.hasOwnProperty('vga')
6109 6129 && VM.VGA_TYPES.indexOf(payload.vga) === -1) {
6110 6130
6111 6131 callback(new Error('Invalid VGA type: "' + payload.vga
6112 6132 + '", supported types are: ' + VM.VGA_TYPES.join(',')));
6113 6133 return;
6114 6134 }
6115 6135
6116 6136 function validLocalRoute(r) {
6117 6137 var nicIdx = r.match(/nics\[(\d+)\]/);
6118 6138 if (!nicIdx) {
6119 6139 is_nic = false;
6120 6140 return false;
6121 6141 }
6122 6142 is_nic = true;
6123 6143
6124 6144 if (nics_result_ordered.length === 0) {
6125 6145 return false;
6126 6146 }
6127 6147
6128 6148 nicIdx = Number(nicIdx[1]);
6129 6149 if (!nics_result_ordered[nicIdx]
6130 6150 || !nics_result_ordered[nicIdx].hasOwnProperty('ip')
6131 6151 || nics_result_ordered[nicIdx].ip === 'dhcp') {
6132 6152 return false;
6133 6153 }
6134 6154
6135 6155 return true;
6136 6156 }
6137 6157
6138 6158 props = [ 'routes', 'set_routes' ];
6139 6159 for (prop in props) {
6140 6160 prop = props[prop];
6141 6161 if (payload.hasOwnProperty(prop)) {
6142 6162 for (dst in payload[prop]) {
6143 6163 var src = payload[prop][dst];
6144 6164
6145 6165 if (!net.isIPv4(dst) && !isCIDR(dst)) {
6146 6166 callback(new Error('Invalid route destination: "' + dst
6147 6167 + '" (must be IP address or CIDR)'));
6148 6168 return;
6149 6169 }
6150 6170
6151 6171 if (!net.isIPv4(src) && !validLocalRoute(src)) {
6152 6172 callback(new Error(
6153 6173 is_nic ? 'Route gateway: "' + src
6154 6174 + '" refers to non-existent or DHCP nic'
6155 6175 : 'Invalid route gateway: "' + src
6156 6176 + '" (must be IP address or nic)'));
6157 6177 return;
6158 6178 }
6159 6179
6160 6180 routes_result[dst] = src;
6161 6181 }
6162 6182 }
6163 6183 }
6164 6184
6165 6185 if (payload.hasOwnProperty('remove_routes')) {
6166 6186 for (dst in payload.remove_routes) {
6167 6187 dst = payload.remove_routes[dst];
6168 6188 delete routes_result[dst];
6169 6189 }
6170 6190 }
6171 6191
6172 6192 // Now that we've applied all updates to routes, make sure that all
6173 6193 // link-local routes refer to a nic that still exists
6174 6194 for (dst in routes_result) {
6175 6195 if (!net.isIPv4(routes_result[dst])
6176 6196 && !validLocalRoute(routes_result[dst])) {
6177 6197 callback(new Error('Route gateway: "' + routes_result[dst]
6178 6198 + '" refers to non-existent or DHCP nic'));
6179 6199 return;
6180 6200 }
6181 6201 }
6182 6202
6183 6203 // Ensure password is not too long
6184 6204 if (payload.hasOwnProperty('vnc_password')
6185 6205 && payload.vnc_password.length > 8) {
6186 6206
6187 6207 callback(new Error('VNC password is too long, maximum length is 8 '
6188 6208 + 'characters.'));
6189 6209 return;
6190 6210 }
6191 6211
6192 6212 props = ['zfs_root_recsize', 'zfs_data_recsize'];
6193 6213 for (prop in props) {
6194 6214 prop = props[prop];
6195 6215 if (payload.hasOwnProperty(prop)) {
6196 6216 if (payload[prop] === 0 || payload[prop] === '') {
6197 6217 // this is the default, so set it back to that.
6198 6218 payload[prop] = 131072;
6199 6219 } else if (!validRecordSize(payload[prop])) {
6200 6220 callback(new Error('invalid ' + prop + ' (' + payload[prop]
6201 6221 + '), must be 512-131072 and a power of 2. '
6202 6222 + '(0 to disable)'));
6203 6223 return;
6204 6224 }
6205 6225 }
6206 6226 }
6207 6227 props = ['zfs_root_compression', 'zfs_data_compression'];
6208 6228 for (prop in props) {
6209 6229 prop = props[prop];
6210 6230
6211 6231 if (payload.hasOwnProperty(prop)) {
6212 6232 if (VM.COMPRESSION_TYPES.indexOf(payload[prop]) === -1) {
6213 6233 callback(new Error('invalid compression type for '
6214 6234 + payload[prop] + ', must be one of: '
6215 6235 + VM.COMPRESSION_TYPES.join(', ')));
6216 6236 }
6217 6237 }
6218 6238 }
6219 6239
6220 6240 // Ensure MACs and IPs are not already used on this vm
6221 6241 // NOTE: can't check other nodes yet.
6222 6242
6223 6243 async.series([
6224 6244 function (cb) {
6225 6245 lookupConflicts(macs, ips, vrids, log, function (error, conflict) {
6226 6246 if (error) {
6227 6247 cb(error);
6228 6248 } else {
6229 6249 if (conflict) {
6230 6250 cb(new Error('Conflict detected with another '
6231 6251 + 'vm, please check the MAC, IP, and VRID'));
6232 6252 } else {
6233 6253 log.debug('no conflicts');
6234 6254 cb();
6235 6255 }
6236 6256 }
6237 6257 });
6238 6258 }, function (cb) {
6239 6259 lookupInvalidNicTags(changed_nics, log, function (e) {
6240 6260 if (e) {
6241 6261 cb(e);
6242 6262 } else {
6243 6263 cb();
6244 6264 }
6245 6265 });
6246 6266 }, function (cb) {
6247 6267 // We only allow adding firewall rules on create
6248 6268 if (vmobj) {
6249 6269 log.debug('update: not validating firewall data');
6250 6270 cb();
6251 6271 return;
6252 6272 }
6253 6273
6254 6274 if (!payload.hasOwnProperty('firewall')) {
6255 6275 log.debug('no firewall data in payload: not validating');
6256 6276 cb();
6257 6277 return;
6258 6278 }
6259 6279 validateFirewall(payload, log, cb);
6260 6280 }
6261 6281 ], function (err) {
6262 6282 log.trace('leaving checkPayloadProperties()');
6263 6283 callback(err);
6264 6284 });
6265 6285 }
6266 6286
6267 6287 function createDelegatedDataset(payload, log, callback)
6268 6288 {
6269 6289 var args;
6270 6290 var ds;
6271 6291 var zcfg = '';
6272 6292
6273 6293 assert(log, 'no logger passed to createDelegatedDataset()');
6274 6294
6275 6295 if (payload.delegate_dataset) {
6276 6296 log.info('creating delegated dataset.');
6277 6297 if (!payload.hasOwnProperty('zfs_filesystem')) {
6278 6298 callback(new Error('payload missing zfs_filesystem'));
6279 6299 return;
6280 6300 }
6281 6301 ds = path.join(payload.zfs_filesystem, '/data');
6282 6302
6283 6303 args = ['create'];
6284 6304 if (payload.hasOwnProperty('zfs_data_compression')) {
6285 6305 args.push('-o', 'compression=' + payload.zfs_data_compression);
6286 6306 }
6287 6307 if (payload.hasOwnProperty('zfs_data_recsize')) {
6288 6308 args.push('-o', 'recsize=' + payload.zfs_data_recsize);
6289 6309 }
6290 6310 args.push(ds);
6291 6311
6292 6312 zfs(args, log, function (err) {
6293 6313 if (err) {
6294 6314 callback(err);
6295 6315 return;
6296 6316 }
6297 6317
6298 6318 zcfg = zcfg + 'add dataset; set name=' + ds + '; end\n';
6299 6319 zonecfg(['-u', payload.uuid, zcfg], log, function (e, fds) {
6300 6320 if (e) {
6301 6321 log.error({'err': e, stdout: fds.stdout,
6302 6322 stderr: fds.stderr}, 'unable to add delegated dataset '
6303 6323 + ds + ' to ' + payload.uuid);
6304 6324 callback(e);
6305 6325 } else {
6306 6326 log.debug({stdout: fds.stdout, stderr: fds.stderr},
6307 6327 'added delegated dataset ' + ds);
6308 6328 callback();
6309 6329 }
6310 6330 });
6311 6331 });
6312 6332 } else {
6313 6333 callback();
6314 6334 }
6315 6335 }
6316 6336
6317 6337 function buildAddRemoveList(vmobj, payload, type, key, updatable)
6318 6338 {
6319 6339 var add = [];
6320 6340 var add_key;
6321 6341 var field;
6322 6342 var newobj;
6323 6343 var oldobj;
6324 6344 var plural = type + 's';
6325 6345 var remove = [];
6326 6346 var remove_key;
6327 6347 var update_key;
6328 6348
6329 6349 // initialize some plurals
6330 6350 add_key = 'add_' + plural;
6331 6351 remove_key = 'remove_' + plural;
6332 6352 update_key = 'update_' + plural;
6333 6353
6334 6354 // There's no way to update properties on a disk or nic with zonecfg
6335 6355 // currently. Yes, really. So any disks/nics that should be updated, we
6336 6356 // remove then add with the new properties.
6337 6357 if (payload.hasOwnProperty(update_key)) {
6338 6358 for (newobj in payload[update_key]) {
6339 6359 newobj = payload[update_key][newobj];
6340 6360 for (oldobj in vmobj[plural]) {
6341 6361 oldobj = vmobj[plural][oldobj];
6342 6362
6343 6363 if (oldobj[key] === newobj[key]) {
6344 6364 // This is the one to update: remove and add.
6345 6365 remove.push(oldobj[key]);
6346 6366
6347 6367 // only some fields make sense to update.
6348 6368 for (field in updatable) {
6349 6369 field = updatable[field];
6350 6370 if (newobj.hasOwnProperty(field)) {
6351 6371 oldobj[field] = newobj[field];
6352 6372 }
6353 6373 }
6354 6374
6355 6375 add.push(oldobj);
6356 6376 }
6357 6377 }
6358 6378 }
6359 6379 }
6360 6380
6361 6381 if (payload.hasOwnProperty(remove_key)) {
6362 6382 for (newobj in payload[remove_key]) {
6363 6383 newobj = payload[remove_key][newobj];
6364 6384 remove.push(newobj);
6365 6385 }
6366 6386 }
6367 6387
6368 6388 if (payload.hasOwnProperty(add_key)) {
6369 6389 for (newobj in payload[add_key]) {
6370 6390 newobj = payload[add_key][newobj];
6371 6391 add.push(newobj);
6372 6392 }
6373 6393 }
6374 6394
6375 6395 return ({'add': add, 'remove': remove});
6376 6396 }
6377 6397
6378 6398 function buildDiskZonecfg(vmobj, payload)
6379 6399 {
6380 6400 var add = [];
6381 6401 var disk;
6382 6402 var lists;
6383 6403 var remove = [];
6384 6404 var zcfg = '';
6385 6405
6386 6406 lists = buildAddRemoveList(vmobj, payload, 'disk', 'path',
6387 6407 UPDATABLE_DISK_PROPS);
6388 6408 remove = lists.remove;
6389 6409 add = lists.add;
6390 6410
6391 6411 // remove is a list of disk paths, add a remove for each now.
6392 6412 for (disk in remove) {
6393 6413 disk = remove[disk];
6394 6414 zcfg = zcfg + 'remove -F device match=' + disk + '\n';
6395 6415 }
6396 6416
6397 6417 for (disk in add) {
6398 6418 disk = add[disk];
6399 6419
6400 6420 zcfg = zcfg + 'add device\n'
6401 6421 + 'set match=' + disk.path + '\n'
6402 6422 + 'add property (name=boot, value="'
6403 6423 + (disk.boot ? 'true' : 'false') + '")\n'
6404 6424 + 'add property (name=model, value="' + disk.model + '")\n';
6405 6425
6406 6426 if (disk.hasOwnProperty('media')) {
6407 6427 zcfg = zcfg
6408 6428 + 'add property (name=media, value="'
6409 6429 + disk.media + '")\n';
6410 6430 }
6411 6431
6412 6432 if (disk.hasOwnProperty('image_size')) {
6413 6433 zcfg = zcfg
6414 6434 + 'add property (name=image-size, value="'
6415 6435 + disk.image_size + '")\n';
6416 6436 } else if (disk.hasOwnProperty('size')) {
6417 6437 zcfg = zcfg + 'add property (name=size, value="'
6418 6438 + disk.size + '")\n';
6419 6439 }
6420 6440
6421 6441 if (disk.hasOwnProperty('image_uuid')) {
6422 6442 zcfg = zcfg
6423 6443 + 'add property (name=image-uuid, value="'
6424 6444 + disk.image_uuid + '")\n';
6425 6445 }
6426 6446
6427 6447 if (disk.hasOwnProperty('image_name')) {
6428 6448 zcfg = zcfg + 'add property (name=image-name, value="'
6429 6449 + disk.image_name + '")\n';
6430 6450 }
6431 6451
6432 6452 zcfg = zcfg + 'end\n';
6433 6453 }
6434 6454
6435 6455 return zcfg;
6436 6456 }
6437 6457
6438 6458 function buildNicZonecfg(vmobj, payload)
6439 6459 {
6440 6460 var add;
6441 6461 var lists;
6442 6462 var matches;
6443 6463 var n;
6444 6464 var new_primary;
6445 6465 var nic;
6446 6466 var nic_idx = 0;
6447 6467 var remove;
6448 6468 var updated_primary;
6449 6469 var used_nic_indexes = [];
6450 6470 var zcfg = '';
6451 6471
6452 6472 if (vmobj.hasOwnProperty('nics')) {
6453 6473 // check whether we're adding or updating to set the primary flag. If we
6454 6474 // are also find the existing NIC with the primary flag. If that's not
6455 6475 // being removed, update it to remove the primary flag.
6456 6476 if (payload.hasOwnProperty('add_nics')) {
6457 6477 for (nic in payload.add_nics) {
6458 6478 nic = payload.add_nics[nic];
6459 6479 if (nic.hasOwnProperty('primary')) {
6460 6480 new_primary = nic.mac;
6461 6481 }
6462 6482 }
6463 6483 }
6464 6484 if (payload.hasOwnProperty('update_nics')) {
6465 6485 for (nic in payload.update_nics) {
6466 6486 nic = payload.update_nics[nic];
6467 6487 if (nic.hasOwnProperty('primary')) {
6468 6488 new_primary = nic.mac;
6469 6489 }
6470 6490 }
6471 6491 }
6472 6492 if (new_primary) {
6473 6493 // find old primary
6474 6494 for (nic in vmobj.nics) {
6475 6495 nic = vmobj.nics[nic];
6476 6496 if (nic.hasOwnProperty('primary') && nic.mac !== new_primary) {
6477 6497 // we have a new primary, so un-primary the old.
6478 6498 if (payload.hasOwnProperty('remove_nics')
6479 6499 && payload.remove_nics.indexOf(nic.mac) !== -1) {
6480 6500
6481 6501 // we're removing the old primary so: done.
6482 6502 break;
6483 6503 } else if (payload.hasOwnProperty('update_nics')) {
6484 6504 updated_primary = false;
6485 6505 for (n in payload.update_nics) {
6486 6506 n = payload.update_nics[n];
6487 6507 if (n.mac === nic.mac) {
6488 6508 n.primary = false;
6489 6509 updated_primary = true;
6490 6510 }
6491 6511 }
6492 6512 if (!updated_primary) {
6493 6513 payload.update_nics.push({'mac': nic.mac,
6494 6514 'primary': false});
6495 6515 }
6496 6516 } else {
6497 6517 // just add a new update to unset the
6498 6518 payload.update_nics =
6499 6519 [ {'mac': nic.mac, 'primary': false} ];
6500 6520 }
6501 6521 }
6502 6522 }
6503 6523 }
6504 6524 }
6505 6525
6506 6526 lists = buildAddRemoveList(vmobj, payload, 'nic', 'mac',
6507 6527 UPDATABLE_NIC_PROPS);
6508 6528 remove = lists.remove;
6509 6529 add = lists.add;
6510 6530
6511 6531 // create a list of used indexes so we can find the free ones
6512 6532 if (vmobj.hasOwnProperty('nics')) {
6513 6533 for (n in vmobj.nics) {
6514 6534 if (vmobj.nics[n].hasOwnProperty('interface')) {
6515 6535 matches = vmobj.nics[n].interface.match(/^net(\d+)$/);
6516 6536 if (matches) {
6517 6537 used_nic_indexes.push(Number(matches[1]));
6518 6538 }
6519 6539 }
6520 6540 }
6521 6541 }
6522 6542
6523 6543 // assign next available interface for nics without one
6524 6544 for (nic in add) {
6525 6545 nic = add[nic];
6526 6546 if (!nic.hasOwnProperty('interface')) {
6527 6547 while (used_nic_indexes.indexOf(nic_idx) !== -1) {
6528 6548 nic_idx++;
6529 6549 }
6530 6550 nic.interface = 'net' + nic_idx;
6531 6551 used_nic_indexes.push(Number(nic_idx));
6532 6552 }
6533 6553
6534 6554 // Changing the VRID changes the MAC address too, since the VRID is
6535 6555 // encoded in the MAC. This can't be done until after
6536 6556 // buildAddRemoveList above, since mac is used as the key to figure
6537 6557 // out which nic is which
6538 6558 if (nic.hasOwnProperty('vrrp_vrid')) {
6539 6559 nic.mac = vrrpMAC(nic.vrrp_vrid);
6540 6560 }
6541 6561 }
6542 6562
6543 6563 // remove is a list of nic macs, add a remove for each now.
6544 6564 for (nic in remove) {
6545 6565 nic = remove[nic];
6546 6566 zcfg = zcfg + 'remove net mac-addr=' + ruinMac(nic) + '\n';
6547 6567 }
6548 6568
6549 6569 // properties that don't require any validation - add them if they're
6550 6570 // present:
6551 6571 var nicProperties = ['ip', 'netmask', 'network_uuid', 'model',
6552 6572 'dhcp_server', 'allow_dhcp_spoofing', 'blocked_outgoing_ports',
6553 6573 'allow_ip_spoofing', 'allow_mac_spoofing', 'allow_restricted_traffic',
6554 6574 'allow_unfiltered_promisc', 'vrrp_vrid', 'vrrp_primary_ip'];
6555 6575
6556 6576 for (nic in add) {
6557 6577 nic = add[nic];
6558 6578
6559 6579 zcfg = zcfg
6560 6580 + 'add net\n'
6561 6581 + 'set physical=' + nic.interface + '\n'
6562 6582 + 'set mac-addr=' + ruinMac(nic.mac) + '\n';
6563 6583
6564 6584 if (nic.hasOwnProperty('nic_tag')) {
6565 6585 zcfg = zcfg + 'set global-nic=' + nic.nic_tag + '\n';
6566 6586 }
6567 6587
6568 6588 if (nic.hasOwnProperty('gateway') && nic.gateway.length > 0) {
6569 6589 zcfg = zcfg + 'add property (name=gateway, value="'
6570 6590 + nic.gateway + '")\n';
6571 6591 }
6572 6592
6573 6593 if (nic.hasOwnProperty('primary') && nic.primary) {
6574 6594 zcfg = zcfg + 'add property (name=primary, value="true")\n';
6575 6595 }
6576 6596
6577 6597 if (nic.hasOwnProperty('vlan_id') && (nic.vlan_id !== '0')) {
6578 6598 zcfg = zcfg + 'set vlan-id=' + nic.vlan_id + '\n';
6579 6599 }
6580 6600
6581 6601 if (nic.hasOwnProperty('allowed_ips')) {
6582 6602 zcfg = zcfg
6583 6603 + 'add property (name=allowed_ips, value="'
6584 6604 + nic.allowed_ips.join(',') + '")\n';
6585 6605 }
6586 6606
6587 6607 for (var prop in nicProperties) {
6588 6608 prop = nicProperties[prop];
6589 6609 if (nic.hasOwnProperty(prop)) {
6590 6610 zcfg = zcfg + 'add property (name=' + prop + ', value="'
6591 6611 + nic[prop] + '")\n';
6592 6612 }
6593 6613 }
6594 6614
6595 6615 zcfg = zcfg + 'end\n';
6596 6616 }
6597 6617
6598 6618 return zcfg;
6599 6619 }
6600 6620
6601 6621 function buildFilesystemZonecfg(vmobj, payload)
6602 6622 {
6603 6623 var add = [];
6604 6624 var filesystem;
6605 6625 var lists;
6606 6626 var opt;
6607 6627 var remove = [];
6608 6628 var zcfg = '';
6609 6629
6610 6630 lists = buildAddRemoveList(vmobj, payload, 'filesystem', 'target', []);
6611 6631 remove = lists.remove;
6612 6632 add = lists.add;
6613 6633
6614 6634 // remove is a list of disk paths, add a remove for each now.
6615 6635 for (filesystem in remove) {
6616 6636 filesystem = remove[filesystem];
6617 6637 zcfg = zcfg + 'remove fs match=' + filesystem + '\n';
6618 6638 }
6619 6639
6620 6640 for (filesystem in add) {
6621 6641 filesystem = add[filesystem];
6622 6642
6623 6643 zcfg = zcfg + 'add fs\n' + 'set dir=' + filesystem.target + '\n'
6624 6644 + 'set special=' + filesystem.source + '\n' + 'set type='
6625 6645 + filesystem.type + '\n';
6626 6646 if (filesystem.hasOwnProperty('raw')) {
6627 6647 zcfg = zcfg + 'set raw=' + filesystem.raw + '\n';
6628 6648 }
6629 6649 if (filesystem.hasOwnProperty('options')) {
6630 6650 for (opt in filesystem.options) {
6631 6651 opt = filesystem.options[opt];
6632 6652 zcfg = zcfg + 'add options "' + opt + '"\n';
6633 6653 }
6634 6654 }
6635 6655 zcfg = zcfg + 'end\n';
6636 6656 }
6637 6657
6638 6658 return zcfg;
6639 6659 }
6640 6660
6641 6661 function buildZonecfgUpdate(vmobj, payload, log)
6642 6662 {
6643 6663 var brand;
6644 6664 var tmp;
6645 6665 var zcfg = '';
6646 6666
6647 6667 assert(log, 'no logger passed to buildZonecfgUpdate()');
6648 6668
6649 6669 log.debug({vmobj: vmobj, payload: payload},
6650 6670 'parameters to buildZonecfgUpdate()');
6651 6671
6652 6672 if (vmobj && vmobj.hasOwnProperty('brand')) {
6653 6673 brand = vmobj.brand;
6654 6674 } else {
6655 6675 brand = payload.brand;
6656 6676 }
6657 6677
6658 6678 // Global properties can just be set, no need to clear anything first.
6659 6679 if (payload.hasOwnProperty('cpu_shares')) {
6660 6680 zcfg = zcfg + 'set cpu-shares=' + payload.cpu_shares.toString() + '\n';
6661 6681 }
6662 6682 if (payload.hasOwnProperty('zfs_io_priority')) {
6663 6683 zcfg = zcfg + 'set zfs-io-priority='
6664 6684 + payload.zfs_io_priority.toString() + '\n';
6665 6685 }
6666 6686 if (payload.hasOwnProperty('max_lwps')) {
6667 6687 zcfg = zcfg + 'set max-lwps=' + payload.max_lwps.toString() + '\n';
6668 6688 }
6669 6689 if (payload.hasOwnProperty('limit_priv')) {
6670 6690 zcfg = zcfg + 'set limitpriv="' + payload.limit_priv + '"\n';
6671 6691 }
6672 6692
6673 6693 if (!BRAND_OPTIONS[brand].features.use_vm_autoboot
6674 6694 && payload.hasOwnProperty('autoboot')) {
6675 6695
6676 6696 // kvm autoboot is managed by the vm-autoboot attr instead
6677 6697 zcfg = zcfg + 'set autoboot=' + payload.autoboot.toString() + '\n';
6678 6698 }
6679 6699
6680 6700 // Capped Memory properties are special
6681 6701 if (payload.hasOwnProperty('max_physical_memory')
6682 6702 || payload.hasOwnProperty('max_locked_memory')
6683 6703 || payload.hasOwnProperty('max_swap')) {
6684 6704
6685 6705 // Capped memory parameters need either an add or select first.
6686 6706 if (vmobj.hasOwnProperty('max_physical_memory')
6687 6707 || vmobj.hasOwnProperty('max_locked_memory')
6688 6708 || vmobj.hasOwnProperty('max_swap')) {
6689 6709
6690 6710 // there's already a capped-memory section, use that.
6691 6711 zcfg = zcfg + 'select capped-memory; ';
6692 6712 } else {
6693 6713 zcfg = zcfg + 'add capped-memory; ';
6694 6714 }
6695 6715
6696 6716 if (payload.hasOwnProperty('max_physical_memory')) {
6697 6717 zcfg = zcfg + 'set physical='
6698 6718 + payload.max_physical_memory.toString() + 'm; ';
6699 6719 }
6700 6720 if (payload.hasOwnProperty('max_locked_memory')) {
6701 6721 zcfg = zcfg + 'set locked='
6702 6722 + payload.max_locked_memory.toString() + 'm; ';
6703 6723 }
6704 6724 if (payload.hasOwnProperty('max_swap')) {
6705 6725 zcfg = zcfg + 'set swap='
6706 6726 + payload.max_swap.toString() + 'm; ';
6707 6727 }
6708 6728
6709 6729 zcfg = zcfg + 'end\n';
6710 6730 }
6711 6731
6712 6732 // Capped CPU is special
6713 6733 if (payload.hasOwnProperty('cpu_cap')) {
6714 6734 if (vmobj.hasOwnProperty('cpu_cap')) {
6715 6735 zcfg = zcfg + 'select capped-cpu; ';
6716 6736 } else {
6717 6737 zcfg = zcfg + 'add capped-cpu; ';
6718 6738 }
6719 6739
6720 6740 zcfg = zcfg + 'set ncpus='
6721 6741 + (Number(payload.cpu_cap) * 0.01).toString() + '; end\n';
6722 6742 }
6723 6743
6724 6744 // set to empty string so property is removed when not true or when not
6725 6745 // false if that's the default for the property.
6726 6746 if (payload.hasOwnProperty('do_not_inventory')) {
6727 6747 if (payload.do_not_inventory !== true) {
6728 6748 // removing sets false as that's the default.
6729 6749 payload.do_not_inventory = '';
6730 6750 }
6731 6751 }
6732 6752
6733 6753 if (payload.hasOwnProperty('archive_on_delete')) {
6734 6754 if (payload.archive_on_delete !== true) {
6735 6755 // removing sets false as that's the default.
6736 6756 payload.archive_on_delete = '';
6737 6757 }
6738 6758 }
6739 6759
6740 6760 if (payload.hasOwnProperty('firewall_enabled')) {
6741 6761 if (payload.firewall_enabled !== true) {
6742 6762 // removing sets false as that's the default.
6743 6763 payload.firewall_enabled = '';
6744 6764 }
6745 6765 }
6746 6766
6747 6767 if (payload.hasOwnProperty('restart_init')) {
6748 6768 if (payload.restart_init === true) {
6749 6769 // removing sets true as that's the default.
6750 6770 payload.restart_init = '';
6751 6771 }
6752 6772 }
6753 6773
6754 6774 // Attributes
6755 6775 function setAttr(attr, attr_name, value) {
6756 6776 if (!value) {
6757 6777 value = payload[attr_name];
6758 6778 }
6759 6779
6760 6780 if (payload.hasOwnProperty(attr_name)) {
6761 6781 if ((typeof (value) !== 'boolean')
6762 6782 && (!value || trim(value.toString()) === '')) {
6763 6783
6764 6784 // empty values we either remove or ignore.
6765 6785 if (vmobj.hasOwnProperty(attr_name)) {
6766 6786 zcfg = zcfg + 'remove attr name=' + attr + ';';
6767 6787 // else do nothing, we don't add empty values.
6768 6788 }
6769 6789 } else {
6770 6790 if (attr_name === 'resolvers'
6771 6791 && vmobj.hasOwnProperty('resolvers')
6772 6792 && vmobj.resolvers.length === 0) {
6773 6793
6774 6794 // special case for resolvers: we always have 'resolvers'
6775 6795 // in the object, but if it's empty we don't have it in the
6776 6796 // zonecfg. Add instead of the usual update.
6777 6797 zcfg = zcfg + 'add attr; set name="' + attr + '"; '
6778 6798 + 'set type=string; ';
6779 6799 } else if (vmobj.hasOwnProperty(attr_name)) {
6780 6800 zcfg = zcfg + 'select attr name=' + attr + '; ';
6781 6801 } else {
6782 6802 zcfg = zcfg + 'add attr; set name="' + attr + '"; '
6783 6803 + 'set type=string; ';
6784 6804 }
6785 6805 zcfg = zcfg + 'set value="' + value.toString() + '"; end\n';
6786 6806 }
6787 6807 }
6788 6808 }
6789 6809 setAttr('billing-id', 'billing_id');
6790 6810 setAttr('owner-uuid', 'owner_uuid');
6791 6811 setAttr('package-name', 'package_name');
6792 6812 setAttr('package-version', 'package_version');
6793 6813 setAttr('tmpfs', 'tmpfs');
6794 6814 setAttr('hostname', 'hostname');
6795 6815 setAttr('dns-domain', 'dns_domain');
6796 6816 setAttr('default-gateway', 'default_gateway');
6797 6817 setAttr('do-not-inventory', 'do_not_inventory');
6798 6818 setAttr('archive-on-delete', 'archive_on_delete');
6799 6819 setAttr('firewall-enabled', 'firewall_enabled');
6800 6820 setAttr('restart-init', 'restart_init');
6801 6821 setAttr('init-name', 'init_name');
6802 6822 setAttr('disk-driver', 'disk_driver');
6803 6823 setAttr('nic-driver', 'nic_driver');
6804 6824
6805 6825 if (payload.hasOwnProperty('resolvers')) {
6806 6826 setAttr('resolvers', 'resolvers', payload.resolvers.join(','));
6807 6827 }
6808 6828 if (payload.hasOwnProperty('alias')) {
6809 6829 tmp = '';
6810 6830 if (payload.alias) {
6811 6831 tmp = new Buffer(payload.alias).toString('base64');
6812 6832 }
6813 6833 setAttr('alias', 'alias', tmp);
6814 6834 }
6815 6835
6816 6836 if (BRAND_OPTIONS[brand].features.use_vm_autoboot) {
6817 6837 setAttr('vm-autoboot', 'autoboot');
6818 6838 }
6819 6839
6820 6840 // XXX Used on KVM but can be passed in for 'OS' too. We only setAttr on KVM
6821 6841 if (BRAND_OPTIONS[brand].features.type === 'KVM') {
6822 6842 setAttr('ram', 'ram');
6823 6843 }
6824 6844
6825 6845 // NOTE: Thanks to normalizePayload() we'll only have these when relevant
6826 6846 setAttr('vcpus', 'vcpus');
6827 6847 setAttr('boot', 'boot');
6828 6848 setAttr('cpu-type', 'cpu_type');
6829 6849 setAttr('vga', 'vga');
6830 6850 setAttr('vnc-port', 'vnc_port');
6831 6851 setAttr('spice-port', 'spice_port');
6832 6852 setAttr('virtio-txtimer', 'virtio_txtimer');
6833 6853 setAttr('virtio-txburst', 'virtio_txburst');
6834 6854
6835 6855 // We use base64 here for these next five options:
6836 6856 //
6837 6857 // vnc_password
6838 6858 // spice_password
6839 6859 // spice_opts
6840 6860 // qemu_opts
6841 6861 // qemu_extra_opts
6842 6862 //
6843 6863 // since these can contain characters zonecfg doesn't like.
6844 6864 //
6845 6865 if (payload.hasOwnProperty('vnc_password')) {
6846 6866 if (payload.vnc_password === ''
6847 6867 && (vmobj.hasOwnProperty('vnc_password')
6848 6868 && vmobj.vnc_password !== '')) {
6849 6869
6850 6870 log.warn('Warning: VNC password was removed for VM '
6851 6871 + vmobj.uuid + ' but VM needs to be restarted for change to'
6852 6872 + 'take effect.');
6853 6873 }
6854 6874 if (payload.vnc_password.length > 0
6855 6875 && !vmobj.hasOwnProperty('vnc_password')) {
6856 6876
6857 6877 log.warn('Warning: VNC password was added to VM '
6858 6878 + vmobj.uuid + ' but VM needs to be restarted for change to'
6859 6879 + 'take effect.');
6860 6880 }
6861 6881
6862 6882 setAttr('vnc-password', 'vnc_password',
6863 6883 new Buffer(payload.vnc_password).toString('base64'));
6864 6884 }
6865 6885 if (payload.hasOwnProperty('spice_password')) {
6866 6886 if (payload.spice_password === ''
6867 6887 && (vmobj.hasOwnProperty('spice_password')
6868 6888 && vmobj.spice_password !== '')) {
6869 6889
6870 6890 log.warn('Warning: SPICE password was removed for VM '
6871 6891 + vmobj.uuid + ' but VM needs to be restarted for change to'
6872 6892 + 'take effect.');
6873 6893 }
6874 6894 if (payload.spice_password.length > 0
6875 6895 && !vmobj.hasOwnProperty('spice_password')) {
6876 6896
6877 6897 log.warn('Warning: SPICE password was added to VM '
6878 6898 + vmobj.uuid + ' but VM needs to be restarted for change to'
6879 6899 + 'take effect.');
6880 6900 }
6881 6901
6882 6902 setAttr('spice-password', 'spice_password',
6883 6903 new Buffer(payload.spice_password).toString('base64'));
6884 6904 }
6885 6905 if (payload.hasOwnProperty('spice_opts')) {
6886 6906 setAttr('spice-opts', 'spice_opts',
6887 6907 new Buffer(payload.spice_opts).toString('base64'));
6888 6908 }
6889 6909 if (payload.hasOwnProperty('qemu_opts')) {
6890 6910 setAttr('qemu-opts', 'qemu_opts',
6891 6911 new Buffer(payload.qemu_opts).toString('base64'));
6892 6912 }
6893 6913 if (payload.hasOwnProperty('qemu_extra_opts')) {
6894 6914 setAttr('qemu-extra-opts', 'qemu_extra_opts',
6895 6915 new Buffer(payload.qemu_extra_opts).toString('base64'));
6896 6916 }
6897 6917
6898 6918 // Handle disks
6899 6919 if (payload.hasOwnProperty('disks')
6900 6920 || payload.hasOwnProperty('add_disks')
6901 6921 || payload.hasOwnProperty('update_disks')
6902 6922 || payload.hasOwnProperty('remove_disks')) {
6903 6923
6904 6924 zcfg = zcfg + buildDiskZonecfg(vmobj, payload);
6905 6925 }
6906 6926
6907 6927 if (payload.hasOwnProperty('fs_allowed')) {
6908 6928 if (payload.fs_allowed === '') {
6909 6929 zcfg = zcfg + 'clear fs-allowed\n';
6910 6930 } else {
6911 6931 zcfg = zcfg + 'set fs-allowed="' + payload.fs_allowed + '"\n';
6912 6932 }
6913 6933 }
6914 6934
6915 6935 if (payload.hasOwnProperty('filesystems')
6916 6936 || payload.hasOwnProperty('add_filesystems')
6917 6937 || payload.hasOwnProperty('update_filesystems')
6918 6938 || payload.hasOwnProperty('add_filesystems')) {
6919 6939
6920 6940 zcfg = zcfg + buildFilesystemZonecfg(vmobj, payload);
6921 6941 }
6922 6942
6923 6943 zcfg = zcfg + buildNicZonecfg(vmobj, payload);
6924 6944
6925 6945 return zcfg;
6926 6946 }
6927 6947
6928 6948 // Checks that QMP is responding to query-status and if so passes the boolean
6929 6949 // value of the hwsetup parameter to the callback.
6930 6950 //
6931 6951 // vmobj must have:
6932 6952 //
6933 6953 // zonepath
6934 6954 //
6935 6955 function checkHWSetup(vmobj, log, callback)
6936 6956 {
6937 6957 var q;
6938 6958 var socket;
6939 6959
6940 6960 assert(log, 'no logger passed to checkHWSetup()');
6941 6961
6942 6962 q = new Qmp(log);
6943 6963 socket = vmobj.zonepath + '/root/tmp/vm.qmp';
6944 6964
6945 6965 q.connect(socket, function (error) {
6946 6966 if (error) {
6947 6967 log.error(error, 'q.connect(): Error: ' + error.message);
6948 6968 callback(error);
6949 6969 return;
6950 6970 }
6951 6971 q.command('query-status', null, function (e, result) {
6952 6972 if (e) {
6953 6973 log.error(e, 'q.command(query-status): Error: ' + e.message);
6954 6974 callback(e);
6955 6975 return;
6956 6976 }
6957 6977 q.disconnect();
6958 6978 callback(null, result.hwsetup ? true : false);
6959 6979 return;
6960 6980 });
6961 6981 });
6962 6982 }
6963 6983
6964 6984 // cb (if set) will be called with an Error if we can't setup the interval loop
6965 6985 // otherwise when the loop is shut down.
6966 6986 //
6967 6987 // vmobj must have:
6968 6988 //
6969 6989 // brand
6970 6990 // state
6971 6991 // uuid
6972 6992 // zonepath
6973 6993 // zoneroot
6974 6994 //
6975 6995 function markProvisionedWhenHWSetup(vmobj, options, cb)
6976 6996 {
6977 6997 var ival_handle;
6978 6998 var log;
6979 6999 var loop_interval = 3; // seconds
6980 7000 var zoneroot;
6981 7001
6982 7002 log = options.log;
6983 7003 assert(log, 'no logger passed to markProvisionedWenHWSetup()');
6984 7004 assert(vmobj.hasOwnProperty('zonepath'), 'no zonepath in vmobj');
6985 7005
6986 7006 zoneroot = path.join(vmobj.zoneroot, '/root');
6987 7007
6988 7008 if (!BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) {
6989 7009 // do nothing for zones where we don't wait for hwsetup
6990 7010 cb(new Error('brand ' + vmobj.brand + ' does not support hwsetup'));
6991 7011 return (null);
6992 7012 }
6993 7013
6994 7014 // Ensure the dataset doesn't have unsafe links as /var or /var/svc
6995 7015 // Since we're checking the 'file' provision_success, this also guarantees
6996 7016 // that if it already exists, it's not a symlink.
6997 7017 try {
6998 7018 assertSafeZonePath(zoneroot, '/var/svc/provision_success',
6999 7019 {type: 'file', enoent_ok: true});
7000 7020 } catch (e) {
7001 7021 cb(e);
7002 7022 return (null);
7003 7023 }
7004 7024
7005 7025 if (!options) {
7006 7026 options = {};
7007 7027 }
7008 7028
7009 7029 // if caller wants they can change the interval
7010 7030 if (options.hasOwnProperty('interval')) {
7011 7031 loop_interval = options.interval;
7012 7032 }
7013 7033
7014 7034 log.debug('setting hwsetup interval ' + vmobj.uuid);
7015 7035 ival_handle = setInterval(function () {
7016 7036 VM.load(vmobj.uuid, {fields: ['transition_expire', 'uuid'], log: log},
7017 7037 function (err, obj) {
7018 7038
7019 7039 var timeout_remaining;
7020 7040 var ival = ival_handle;
7021 7041
7022 7042 function done() {
7023 7043 if (ival_handle) {
7024 7044 log.debug('clearing hwsetup interval ' + vmobj.uuid);
7025 7045 clearInterval(ival);
7026 7046 ival = null;
7027 7047 } else {
7028 7048 log.debug('done but no hwsetup interval ' + vmobj.uuid);
7029 7049 }
7030 7050 }
7031 7051
7032 7052 if (err) {
7033 7053 // If the VM was deleted between calls, nothing much we can do.
7034 7054 log.error(err, 'Unable to load ' + vmobj.uuid + ' '
7035 7055 + err.message);
7036 7056 done();
7037 7057 cb(err);
7038 7058 return;
7039 7059 }
7040 7060
7041 7061 // we only do anything if we're still waiting for provisioning
7042 7062 if (vmobj.state !== 'provisioning') {
7043 7063 done();
7044 7064 cb();
7045 7065 return;
7046 7066 }
7047 7067
7048 7068 timeout_remaining =
7049 7069 (Number(obj.transition_expire) - Date.now(0)) / 1000;
7050 7070
7051 7071 if (timeout_remaining <= 0) {
7052 7072 // IMPORTANT: this may run multiple times, must be idempotent
7053 7073
7054 7074 log.warn('Marking VM ' + vmobj.uuid + ' as "failed" because'
7055 7075 + ' timeout expired and we are still "provisioning"');
7056 7076 VM.markVMFailure(vmobj, {log: log}, function (mark_err) {
7057 7077 log.warn(mark_err, 'zoneinit failed, zone is '
7058 7078 + 'being stopped for manual investigation.');
7059 7079 done();
7060 7080 cb();
7061 7081 });
7062 7082 return;
7063 7083 }
7064 7084
7065 7085 checkHWSetup(vmobj, log, function (check_err, result) {
7066 7086 if (check_err) {
7067 7087 log.debug(check_err, 'checkHWSetup Error: '
7068 7088 + check_err.message);
7069 7089 return;
7070 7090 }
7071 7091
7072 7092 if (result) {
7073 7093 log.debug('QMP says VM ' + vmobj.uuid
7074 7094 + ' completed hwsetup');
7075 7095 VM.unsetTransition(vmobj, {log: log}, function (unset_err) {
7076 7096 var provisioning;
7077 7097 var provision_success;
7078 7098
7079 7099 provisioning = path.join(vmobj.zonepath,
7080 7100 '/root/var/svc/provisioning');
7081 7101 provision_success = path.join(vmobj.zonepath,
7082 7102 '/root/var/svc/provision_success');
7083 7103
7084 7104 if (unset_err) {
7085 7105 log.error(unset_err);
7086 7106 } else {
7087 7107 log.debug('cleared transition to provisioning on'
7088 7108 + ' ' + vmobj.uuid);
7089 7109 }
7090 7110
7091 7111 fs.rename(provisioning, provision_success,
7092 7112 function (e) {
7093 7113
7094 7114 if (e) {
7095 7115 if (e.code === 'ENOENT') {
7096 7116 log.debug(e);
7097 7117 } else {
7098 7118 log.error(e);
7099 7119 }
7100 7120 }
7101 7121
7102 7122 done();
7103 7123 cb();
7104 7124 return;
7105 7125 });
7106 7126 });
7107 7127 }
7108 7128 });
7109 7129 });
7110 7130 }, loop_interval * 1000);
7111 7131
7112 7132 return (ival_handle);
7113 7133 }
7114 7134
7115 7135 function archiveVM(uuid, options, callback)
7116 7136 {
7117 7137 var archive_dirname;
7118 7138 var dirmode;
7119 7139 var log;
7120 7140 var patterns_to_archive = [];
7121 7141 var vmobj;
7122 7142
7123 7143 /*jsl:ignore*/
7124 7144 dirmode = 0755;
7125 7145 /*jsl:end*/
7126 7146
7127 7147 if (options.hasOwnProperty('log')) {
7128 7148 log = options.log;
7129 7149 } else {
7130 7150 log = VM.log;
7131 7151 }
7132 7152
7133 7153 log.debug('attempting to archive debug data for VM ' + uuid);
7134 7154
7135 7155 async.series([
7136 7156 function (cb) {
7137 7157 // ensure directory exists
7138 7158 archive_dirname = path.join('/zones/archive', uuid);
7139 7159
7140 7160 fs.mkdir(archive_dirname, dirmode, function (e) {
7141 7161 log.debug(e, 'attempted to create ' + archive_dirname);
7142 7162 cb(e);
7143 7163 return;
7144 7164 });
7145 7165 }, function (cb) {
7146 7166 VM.load(uuid, {log: log}, function (err, obj) {
7147 7167 if (err) {
7148 7168 cb(err);
7149 7169 return;
7150 7170 }
7151 7171 vmobj = obj;
7152 7172 cb();
7153 7173 });
7154 7174 }, function (cb) {
7155 7175 // write vmobj to archive
7156 7176 var filename;
7157 7177
7158 7178 filename = path.join(archive_dirname, 'vm.json');
7159 7179
7160 7180 fs.writeFile(filename, JSON.stringify(vmobj, null, 2) + '\n',
7161 7181 function (err, result) {
7162 7182
7163 7183 if (err) {
7164 7184 log.error(err, 'failed to create ' + filename + ': '
7165 7185 + err.message);
7166 7186 } else {
7167 7187 log.info('archived data to ' + filename);
7168 7188 }
7169 7189
7170 7190 cb(); // ignore error
7171 7191 });
7172 7192 }, function (cb) {
7173 7193 var cmdline = '/usr/sbin/zfs list -t all -o name | grep '
7174 7194 + vmobj.zonename + ' | xargs zfs get -pH all >'
7175 7195 + path.join(archive_dirname, 'zfs.dump');
7176 7196
7177 7197 log.debug(cmdline);
7178 7198 exec(cmdline, function (e, stdout, stderr) {
7179 7199 if (e) {
7180 7200 e.stdout = stdout;
7181 7201 e.stderr = stderr;
7182 7202 log.error({err: e}, 'failed to create '
7183 7203 + path.join(archive_dirname, 'zfs.dump'));
7184 7204 cb(e);
7185 7205 return;
7186 7206 }
7187 7207 log.info('archived data to ' + path.join(archive_dirname,
7188 7208 'zfs.dump'));
7189 7209 cb();
7190 7210 });
7191 7211 }, function (cb) {
7192 7212 patterns_to_archive.push({
7193 7213 src: path.join('/etc/zones/', vmobj.zonename + '.xml'),
7194 7214 dst: path.join(archive_dirname, 'zone.xml')
7195 7215 });
7196 7216 patterns_to_archive.push({
7197 7217 src: path.join(vmobj.zonepath, 'config'),
7198 7218 dst: archive_dirname,
7199 7219 targ: path.join(archive_dirname, 'config')
7200 7220 });
7201 7221 patterns_to_archive.push({
7202 7222 src: path.join(vmobj.zonepath, 'cores'),
7203 7223 dst: archive_dirname,
7204 7224 targ: path.join(archive_dirname, 'cores')
7205 7225 });
7206 7226
7207 7227 if (vmobj.brand === 'kvm') {
7208 7228 patterns_to_archive.push({
7209 7229 src: path.join(vmobj.zonepath, 'root/tmp/vm*.log*'),
7210 7230 dst: path.join(archive_dirname, 'vmlogs'),
7211 7231 create_dst_dir: true
7212 7232 });
7213 7233 patterns_to_archive.push({
7214 7234 src: path.join(vmobj.zonepath, 'root/startvm'),
7215 7235 dst: archive_dirname,
7216 7236 targ: path.join(archive_dirname, 'startvm')
7217 7237 });
7218 7238 } else {
7219 7239 patterns_to_archive.push({
7220 7240 src: path.join(vmobj.zonepath, 'root/var/svc/log/*'),
7221 7241 dst: path.join(archive_dirname, 'svclogs'),
7222 7242 create_dst_dir: true
7223 7243 });
7224 7244 patterns_to_archive.push({
7225 7245 src: path.join(vmobj.zonepath, 'root/var/adm/messages*'),
7226 7246 dst: path.join(archive_dirname, 'admmsgs'),
7227 7247 create_dst_dir: true
7228 7248 });
7229 7249 }
7230 7250
7231 7251 async.forEachSeries(patterns_to_archive, function (pattern, c) {
7232 7252
7233 7253 function cpPattern(p, cp_cb) {
7234 7254 var cmdline = '/usr/bin/cp -RP ' + p.src + ' ' + p.dst;
7235 7255 var targ = p.targ || p.dst;
7236 7256
7237 7257 log.debug(cmdline);
7238 7258 exec(cmdline, function (e, stdout, stderr) {
7239 7259 if (e) {
7240 7260 e.stdout = stdout;
7241 7261 e.stderr = stderr;
7242 7262 log.error({err: e}, 'failed to archive data to '
7243 7263 + targ);
7244 7264 } else {
7245 7265 log.info('archived data to ' + targ);
7246 7266 }
7247 7267 // we don't return errors here because on error copying
7248 7268 // one pattern we still want to grab the others.
7249 7269 cp_cb();
7250 7270 });
7251 7271 }
7252 7272
7253 7273 if (pattern.create_dst_dir) {
7254 7274 fs.mkdir(pattern.dst, dirmode, function (e) {
7255 7275 if (!e) {
7256 7276 log.info('created ' + pattern.dst);
7257 7277 } else {
7258 7278 log.error({err: e}, 'failed to create '
7259 7279 + pattern.dst);
7260 7280 }
7261 7281 cpPattern(pattern, c);
7262 7282 });
7263 7283 } else {
7264 7284 cpPattern(pattern, c);
7265 7285 }
7266 7286 }, function (e) {
7267 7287 log.info('finished archiving VM ' + vmobj.uuid);
7268 7288 cb(e);
7269 7289 });
7270 7290 }
7271 7291 ], function () {
7272 7292 // XXX we ignore errors as failures to archive will not block VM delete.
7273 7293 callback();
7274 7294 });
7275 7295 }
7276 7296
7277 7297 // vmobj argument should have:
7278 7298 //
7279 7299 // transition_to
7280 7300 // uuid
7281 7301 // zonename
7282 7302 //
7283 7303 exports.markVMFailure = function (vmobj, options, cb)
7284 7304 {
7285 7305 var log;
7286 7306
7287 7307 // options is optional
7288 7308 if (arguments.length === 2) {
7289 7309 cb = arguments[1];
7290 7310 options = {};
7291 7311 }
7292 7312
7293 7313 if (!vmobj || !vmobj.hasOwnProperty('uuid')
7294 7314 || !vmobj.hasOwnProperty('zonename')) {
7295 7315
7296 7316 cb(new Error('markVMFailure needs uuid + zonename'));
7297 7317 return;
7298 7318 }
7299 7319
7300 7320 ensureLogging(true);
7301 7321 if (options.hasOwnProperty('log')) {
7302 7322 log = options.log;
7303 7323 } else {
7304 7324 log = VM.log.child({action: 'markVMFailure', vm: vmobj.uuid});
7305 7325 }
7306 7326
7307 7327 function dumpDebugInfo(zonename, callback) {
7308 7328 var errors = {};
7309 7329
7310 7330 async.series([
7311 7331 function (ptree_cb) {
7312 7332 // note: if the zone is not running this returns empty but still
7313 7333 // exits 0
7314 7334 execFile('/usr/bin/ptree', ['-z', zonename],
7315 7335 function (ptree_err, ptree_stdout, ptree_stderr) {
7316 7336
7317 7337 if (ptree_err) {
7318 7338 log.error(ptree_err, 'unable to get ptree from '
7319 7339 + zonename + ': ' + ptree_stderr);
7320 7340 errors.ptree_err = ptree_err;
7321 7341 } else {
7322 7342 log.warn('processes running in ' + zonename
7323 7343 + ' at fail time:\n' + ptree_stdout);
7324 7344 }
7325 7345
7326 7346 ptree_cb(); // don't fail on error here.
7327 7347 }
7328 7348 );
7329 7349 }, function (svcs_cb) {
7330 7350 execFile('/usr/bin/svcs', ['-xv', '-z', zonename],
7331 7351 function (svcs_err, svcs_stdout, svcs_stderr) {
7332 7352
7333 7353 if (svcs_err) {
7334 7354 log.error(svcs_err, 'unable to get svcs from '
7335 7355 + zonename + ': ' + svcs_stderr);
7336 7356 errors.svcs_err = svcs_err;
7337 7357 } else {
7338 7358 log.warn('svcs -xv output for ' + zonename
7339 7359 + ' at fail time:\n' + svcs_stdout);
7340 7360 }
7341 7361
7342 7362 svcs_cb(); // don't fail on error here.
7343 7363 }
7344 7364 );
7345 7365 }, function (kstat_cb) {
7346 7366 execFile('/usr/bin/kstat', ['-n', zonename.substr(0, 30)],
7347 7367 function (kstat_err, kstat_stdout, kstat_stderr) {
7348 7368
7349 7369 if (kstat_err) {
7350 7370 log.error(kstat_err, 'unable to get kstats from '
7351 7371 + zonename + ': ' + kstat_stderr);
7352 7372 errors.kstat_err = kstat_err;
7353 7373 } else {
7354 7374 log.warn('kstat output for ' + zonename
7355 7375 + ' at fail time:\n' + kstat_stdout);
7356 7376 }
7357 7377
7358 7378 kstat_cb(); // don't fail on error here.
7359 7379 }
7360 7380 );
7361 7381 }
7362 7382 ], function () {
7363 7383 callback(errors);
7364 7384 });
7365 7385 }
7366 7386
7367 7387 dumpDebugInfo(vmobj.zonename, function (debug_err) {
7368 7388 var zcfg;
7369 7389
7370 7390 // note: we don't treat failure to dump debug info as a fatal error.
7371 7391 log.warn(debug_err, 'zone setup failed, zone is being stopped '
7372 7392 + 'for manual investigation.');
7373 7393
7374 7394 // Mark the zone as 'failed'
7375 7395 zcfg = 'remove -F attr name=failed; add attr; set name=failed; '
7376 7396 + 'set value="provisioning"; set type=string; end';
7377 7397
7378 7398 zonecfg(['-u', vmobj.uuid, zcfg], log, function (zonecfg_err, fds) {
7379 7399
7380 7400 if (zonecfg_err) {
7381 7401 log.error({err: zonecfg_err, stdout: fds.stdout,
7382 7402 stderr: fds.stderr}, 'Unable to set failure flag on '
7383 7403 + vmobj.uuid + ': ' + zonecfg_err.message);
7384 7404 } else {
7385 7405 log.debug({stdout: fds.stdout, stderr: fds.stderr},
7386 7406 'set failure flag on ' + vmobj.uuid);
7387 7407 }
7388 7408
7389 7409 // attempt to remove transition
7390 7410 VM.unsetTransition(vmobj, {log: log}, function (unset_err) {
7391 7411 if (unset_err) {
7392 7412 log.error(unset_err);
7393 7413 }
7394 7414
7395 7415 VM.stop(vmobj.uuid, {force: true, log: log},
7396 7416 function (stop_err) {
7397 7417
7398 7418 // only log errors because there's nothing to do
7399 7419
7400 7420 if (stop_err) {
7401 7421 log.error(stop_err, 'failed to stop VM '
7402 7422 + vmobj.uuid + ': ' + stop_err.message);
7403 7423 }
7404 7424
7405 7425 cb();
7406 7426 });
7407 7427 });
7408 7428 });
7409 7429 });
7410 7430 };
7411 7431
7412 7432 function svccfg(zonepath, args, log, callback)
7413 7433 {
7414 7434 var cmd = '/usr/sbin/svccfg';
7415 7435 var exec_options = {};
7416 7436 var zoneroot = path.join(zonepath, '/root');
7417 7437
7418 7438 assert(log, 'no logger passed to svccfg()');
7419 7439
7420 7440 try {
7421 7441 assertSafeZonePath(zoneroot, '/etc/svc/repository.db',
7422 7442 {type: 'file', enoent_ok: false});
7423 7443 } catch (e) {
7424 7444 log.error(e, 'Error validating /etc/svc/repository.db: ' + e.message);
7425 7445 callback(e);
7426 7446 return;
7427 7447 }
7428 7448
7429 7449 exec_options = {
7430 7450 env: {
7431 7451 'SVCCFG_CONFIGD_PATH': '/lib/svc/bin/svc.configd',
7432 7452 'SVCCFG_REPOSITORY':
7433 7453 path.join(zonepath, 'root', '/etc/svc/repository.db')
7434 7454 }
7435 7455 };
7436 7456
7437 7457 log.debug({'command': cmd + ' ' + args.join(' '),
7438 7458 'exec_options': exec_options}, 'modifying svc repo in ' + zonepath);
7439 7459 execFile(cmd, args, exec_options, function (error, stdout, stderr) {
7440 7460 if (error) {
7441 7461 callback(error, {'stdout': stdout, 'stderr': stderr});
7442 7462 } else {
7443 7463 callback(null, {'stdout': stdout, 'stderr': stderr});
7444 7464 }
7445 7465 });
7446 7466 }
7447 7467
7448 7468 // This calls cb() when /var/svc/provisioning is gone. When this calls cb()
7449 7469 // with an Error object, the provision is considered failed so this should
7450 7470 // only happen when something timed out that is unrelated to the user.
7451 7471 //
7452 7472 // This returns a function that can be called with no arguments to cancel
7453 7473 // all timers and actions pending from this function. It will also then not
7454 7474 // call the cb().
7455 7475 //
7456 7476 // IMPORTANT: this is only exported to be used by vmadmd. Do not use elsewhere!
7457 7477 //
7458 7478 // vmobj fields:
7459 7479 //
7460 7480 // state
7461 7481 // transition_expire
7462 7482 // uuid
7463 7483 // zonepath
7464 7484 //
7465 7485 exports.waitForProvisioning = function (vmobj, options, cb)
7466 7486 {
7467 7487 var dirname = path.join(vmobj.zonepath, 'root', '/var/svc');
7468 7488 var filename = path.join(dirname, 'provisioning');
7469 7489 var ival_h;
7470 7490 var log;
7471 7491 var timeout;
7472 7492 var timeout_remaining = PROVISION_TIMEOUT; // default to whole thing
7473 7493 var watcher;
7474 7494
7475 7495 // options is optional
7476 7496 if (arguments.length === 2) {
7477 7497 cb = arguments[1];
7478 7498 options = {};
7479 7499 }
7480 7500
7481 7501 ensureLogging(true);
7482 7502 if (options.hasOwnProperty('log')) {
7483 7503 log = options.log;
7484 7504 } else {
7485 7505 log = VM.log.child({action: 'waitForProvisioning', vm: vmobj.uuid});
7486 7506 }
7487 7507
7488 7508 function done() {
7489 7509 if (timeout) {
7490 7510 log.debug('clearing provision timeout for ' + vmobj.uuid);
7491 7511 clearTimeout(timeout);
7492 7512 timeout = null;
7493 7513 }
7494 7514 if (watcher) {
7495 7515 log.debug('closing /var/svc/provisioning watcher for '
7496 7516 + vmobj.uuid);
7497 7517 watcher.close();
7498 7518 watcher = null;
7499 7519 }
7500 7520 if (ival_h) {
7501 7521 log.debug('closing hwsetup check interval for ' + vmobj.uuid);
7502 7522 clearInterval(ival_h);
7503 7523 ival_h = null;
7504 7524 }
7505 7525 }
7506 7526
7507 7527 if ((vmobj.state === 'provisioning')
7508 7528 && (vmobj.hasOwnProperty('transition_expire'))) {
7509 7529
7510 7530 timeout_remaining =
7511 7531 (Number(vmobj.transition_expire) - Date.now(0)) / 1000;
7512 7532
7513 7533 // Always give it at least 1 second's chance.
7514 7534 if (timeout_remaining < 1) {
7515 7535 timeout_remaining = 1;
7516 7536 }
7517 7537 } else {
7518 7538 // don't know what to do here we're not provisioning.
7519 7539 log.warn('waitForProvisioning called when ' + vmobj.uuid
7520 7540 + ' was not provisioning');
7521 7541 cb();
7522 7542 return (null);
7523 7543 }
7524 7544
7525 7545 log.debug({
7526 7546 'transition_expire': Number(vmobj.transition_expire),
7527 7547 'now': Date.now(0)
7528 7548 }, 'waiting ' + timeout_remaining + ' sec(s) for provisioning');
7529 7549
7530 7550 log.debug('setting provision timeout for ' + vmobj.uuid);
7531 7551 timeout = setTimeout(function () {
7532 7552 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure" because we '
7533 7553 + 'hit waitForProvisioning() timeout.');
7534 7554 VM.markVMFailure(vmobj, {log: log}, function (err) {
7535 7555 var errstr = 'timed out waiting for /var/svc/provisioning to move'
7536 7556 + ' for ' + vmobj.uuid;
7537 7557 if (err) {
7538 7558 log.warn(err, 'markVMFailure(): ' + err.message);
7539 7559 }
7540 7560 log.error(errstr);
7541 7561 done();
7542 7562 cb(new Error(errstr));
7543 7563 });
7544 7564 }, (timeout_remaining * 1000));
7545 7565
7546 7566 // this starts a loop that will move provisioning -> provision_success when
7547 7567 // the hardware of the VM has been initialized the first time.
7548 7568 if (BRAND_OPTIONS[vmobj.brand].features.wait_for_hwsetup) {
7549 7569 ival_h = markProvisionedWhenHWSetup(vmobj, {log: log}, function (err) {
7550 7570 if (err) {
7551 7571 log.error(err, 'error in markProvisionedWhenHWSetup()');
7552 7572 }
7553 7573 done();
7554 7574 cb(err);
7555 7575 });
7556 7576 return (done);
7557 7577 }
7558 7578
7559 7579 watcher = fs.watch(filename, function (evt, file) {
7560 7580 // We only care about 'rename' which also fires when the file is
7561 7581 // deleted.
7562 7582 log.debug('watcher.event(' + vmobj.uuid + '): ' + evt);
7563 7583 if (evt === 'rename') {
7564 7584 fs.exists(filename, function (exists) {
7565 7585 if (exists) {
7566 7586 // somehow we still have /var/svc/provisioning!
7567 7587 log.warn('Marking VM ' + vmobj.uuid + ' as a "failure"'
7568 7588 + ' because we still have /var/svc/provisioning after '
7569 7589 + 'rename');
7570 7590 VM.markVMFailure(vmobj, {log: log}, function (err) {
7571 7591 if (err) {
7572 7592 log.warn(err, 'markVMFailure(): ' + err.message);
7573 7593 }
7574 7594 done();
7575 7595 cb(new Error('/var/svc/provisioning exists after '
7576 7596 + 'rename!'));
7577 7597 });
7578 7598 return;
7579 7599 }
7580 7600
7581 7601 // So long as /var/svc/provisioning is gone, we don't care what
7582 7602 // replaced it. Success or failure of user script doesn't
7583 7603 // matter for the state, it's provisioned now. Caller should
7584 7604 // now clear the transition.
7585 7605 done();
7586 7606 cb();
7587 7607 return;
7588 7608 });
7589 7609 }
7590 7610 });
7591 7611
7592 7612 log.debug('created watcher for ' + vmobj.uuid);
7593 7613 return (done);
7594 7614 };
7595 7615
7596 7616 // create and install a 'joyent' or 'kvm' brand zone.
7597 7617 function installZone(payload, log, callback)
7598 7618 {
7599 7619 var load_fields;
7600 7620 var receiving = false;
7601 7621 var reprovisioning = false;
7602 7622 var vmobj;
7603 7623 var zoneinit = {};
7604 7624
7605 7625 assert(log, 'no logger passed to installZone()');
7606 7626
7607 7627 log.debug('installZone()');
7608 7628
7609 7629 load_fields = [
7610 7630 'brand',
7611 7631 'firewall_enabled',
7612 7632 'missing',
7613 7633 'nics',
7614 7634 'owner_uuid',
7615 7635 'routes',
7616 7636 'state',
7617 7637 'tags',
7618 7638 'transition_to',
7619 7639 'transition_expire',
7620 7640 'uuid',
7621 7641 'zonename',
7622 7642 'zonepath'
7623 7643 ];
7624 7644
7625 7645 if (payload.reprovisioning) {
7626 7646 log.debug('installZone(): reprovisioning');
7627 7647 reprovisioning = true;
7628 7648 }
7629 7649
7630 7650 async.series([
7631 7651 function (cb) {
7632 7652
7633 7653 VM.load(payload.uuid, {fields: load_fields, log: log},
7634 7654 function (err, obj) {
7635 7655
7636 7656 if (err) {
7637 7657 cb(err);
7638 7658 return;
7639 7659 }
7640 7660 vmobj = obj;
7641 7661 cb();
7642 7662 });
7643 7663 }, function (cb) {
7644 7664 var thing;
7645 7665 var missing = false;
7646 7666 var msg;
7647 7667 var things = ['datasets', 'filesystems', 'disks'];
7648 7668
7649 7669 if (vmobj.state === 'receiving') {
7650 7670 receiving = true;
7651 7671 msg = 'zone is still missing:';
7652 7672 for (thing in things) {
7653 7673 thing = things[thing];
7654 7674 if (vmobj.missing[thing].length !== 0) {
7655 7675 msg = msg + ' ' + vmobj.missing[thing].length + ' '
7656 7676 + thing + ',';
7657 7677 missing = true;
7658 7678 }
7659 7679 }
7660 7680 msg = rtrim(msg, ',');
7661 7681
7662 7682 if (missing) {
7663 7683 cb(new Error('Unable to complete install for '
7664 7684 + vmobj.uuid + ' ' + msg));
7665 7685 return;
7666 7686 }
7667 7687 }
7668 7688 cb();
7669 7689 }, function (cb) {
7670 7690 // Install the zone.
7671 7691 // This will create the dataset and mark the zone 'installed'.
7672 7692 var args;
7673 7693
7674 7694 if (reprovisioning) {
7675 7695 // reprovisioning we do *most* of install, but not this.
7676 7696 cb();
7677 7697 return;
7678 7698 }
7679 7699
7680 7700 args = ['-z', vmobj.zonename, 'install', '-q',
7681 7701 payload.quota.toString()];
7682 7702
7683 7703 // For both OS and KVM VMs you can pass an image_uuid at the
7684 7704 // top-level. This will be your zone's root dataset. On KVM the user
7685 7705 // is never exposed to this. It's used there for something like
7686 7706 // SPICE.
7687 7707 if (payload.hasOwnProperty('image_uuid')) {
7688 7708 args.push('-t', payload.image_uuid, '-x', 'nodataset');
7689 7709 }
7690 7710
7691 7711 zoneadm(args, log, function (err, fds) {
7692 7712 if (err) {
7693 7713 log.error({err: err, stdout: fds.stdout,
7694 7714 stderr: fds.stderr}, 'zoneadm failed to install: '
7695 7715 + err.message);
7696 7716 cb(err);
7697 7717 } else {
7698 7718 log.debug({stdout: fds.stdout, stderr: fds.stderr},
7699 7719 'zoneadm installed zone');
7700 7720 cb();
7701 7721 }
7702 7722 });
7703 7723 }, function (cb) {
7704 7724 // Apply compression if set
7705 7725 var args = [];
7706 7726 if (payload.hasOwnProperty('zfs_root_compression')) {
7707 7727 args = ['set', 'compression='
7708 7728 + payload.zfs_root_compression, payload.zfs_filesystem];
7709 7729 zfs(args, log, function (err) {
7710 7730 cb(err);
7711 7731 });
7712 7732 } else {
7713 7733 cb();
7714 7734 }
7715 7735 }, function (cb) {
7716 7736 // Apply recsize if set
7717 7737 var args = [];
7718 7738 if (payload.hasOwnProperty('zfs_root_recsize')) {
7719 7739 args = ['set', 'recsize=' + payload.zfs_root_recsize,
7720 7740 payload.zfs_filesystem];
7721 7741 zfs(args, log, function (err) {
7722 7742 cb(err);
7723 7743 });
7724 7744 } else {
7725 7745 cb();
7726 7746 }
7727 7747 }, function (cb) {
7728 7748 // Some zones can have an additional 'data' dataset delegated to
7729 7749 // them for use in the zone. This will set that up. If the option
7730 7750 // is not set, the following does nothing.
7731 7751 if (!receiving && !reprovisioning) {
7732 7752 createDelegatedDataset(payload, log, function (err) {
7733 7753 if (err) {
7734 7754 cb(err);
7735 7755 } else {
7736 7756 cb();
7737 7757 }
7738 7758 });
7739 7759 } else {
7740 7760 cb();
7741 7761 }
7742 7762 }, function (cb) {
7743 7763 // Write out the zone's metadata
7744 7764 // Note: we don't do this when receiving because dataset will
7745 7765 // already contain metadata and we don't want to wipe that out.
7746 7766 if (!receiving && !reprovisioning) {
7747 7767 saveMetadata(payload, log, function (err) {
7748 7768 if (err) {
7749 7769 log.error(err, 'unable to save metadata: '
7750 7770 + err.message);
7751 7771 cb(err);
7752 7772 } else {
7753 7773 cb();
7754 7774 }
7755 7775 });
7756 7776 } else {
7757 7777 cb();
7758 7778 }
7759 7779 }, function (cb) {
7760 7780 // Write out the zone's routes
7761 7781 // Note: we don't do this when receiving because dataset will
7762 7782 // already contain routes and we don't want to wipe that out.
7763 7783 if (!receiving && !reprovisioning) {
7764 7784 saveRoutes(payload, log, function (err) {
7765 7785 if (err) {
7766 7786 log.error(err, 'unable to save routes: '
7767 7787 + err.message);
7768 7788 cb(err);
7769 7789 } else {
7770 7790 cb();
7771 7791 }
7772 7792 });
7773 7793 } else {
7774 7794 cb();
7775 7795 }
7776 7796 }, function (cb) {
7777 7797 // if we were receiving, we're done receiving now
7778 7798 if (receiving) {
7779 7799 VM.unsetTransition(vmobj, {log: log}, cb);
7780 7800 } else {
7781 7801 cb();
7782 7802 }
7783 7803 }, function (cb) {
7784 7804 // var zoneinit is in installZone() scope
7785 7805
7786 7806 // when receiving zoneinit is never run.
7787 7807 if (receiving) {
7788 7808 cb();
7789 7809 return;
7790 7810 }
7791 7811
7792 7812 getZoneinitJSON(vmobj.zonepath, log, function (zoneinit_err, data) {
7793 7813
7794 7814 if (zoneinit_err) {
7795 7815 // NOTE: not existing is not going to give us a zoneinit_err
7796 7816 log.warn(zoneinit_err, 'error in getZoneinitJSON');
7797 7817 cb(zoneinit_err);
7798 7818 return;
7799 7819 }
7800 7820
7801 7821 if (data) {
7802 7822 zoneinit = data;
7803 7823 } else {
7804 7824 zoneinit = {};
7805 7825 }
7806 7826
7807 7827 cb();
7808 7828 });
7809 7829 }, function (cb) {
7810 7830 // var_svc_provisioning is at installZone() scope
7811 7831
7812 7832 // If we're not receiving, we're provisioning a new VM and in that
7813 7833 // case we write the /var/svc/provisioning file which should exist
7814 7834 // until something in the zone decides provisioning is complete. At
7815 7835 // that point it will be moved to either:
7816 7836 //
7817 7837 // /var/svc/provision_success
7818 7838 // /var/svc/provision_failure
7819 7839 //
7820 7840 // to indicate that the provisioning setup has been completed.
7821 7841
7822 7842 if (receiving) {
7823 7843 cb();
7824 7844 return;
7825 7845 }
7826 7846
7827 7847 fs.writeFile(path.join(vmobj.zonepath, 'root',
7828 7848 '/var/svc/provisioning'), '', function (err, result) {
7829 7849
7830 7850 if (err) {
7831 7851 log.error(err, 'failed to create '
7832 7852 + '/var/svc/provisioning: ' + err.message);
7833 7853 } else {
7834 7854 log.debug('created /var/svc/provisioning in '
7835 7855 + path.join(vmobj.zonepath, 'root'));
7836 7856 }
7837 7857
7838 7858 cb(err);
7839 7859 });
7840 7860 }, function (cb) {
7841 7861 // For joyent and joyent-minimal at least, set the timeout for the
7842 7862 // svc start method to the value specified in the payload, or a
7843 7863 // default.
7844 7864
7845 7865 var timeout;
7846 7866
7847 7867 if (BRAND_OPTIONS[vmobj.brand].features.update_mdata_exec_timeout) {
7848 7868
7849 7869 if (payload.hasOwnProperty('mdata_exec_timeout')) {
7850 7870 timeout = payload.mdata_exec_timeout;
7851 7871 } else {
7852 7872 timeout = DEFAULT_MDATA_TIMEOUT;
7853 7873 }
7854 7874
7855 7875 svccfg(vmobj.zonepath, [
7856 7876 '-s', 'svc:/smartdc/mdata:execute',
7857 7877 'setprop', 'start/timeout_seconds', '=', 'count:', timeout
7858 7878 ], log, function (error, stdio) {
7859 7879
7860 7880 if (error) {
7861 7881 log.error(error, 'failed to set mdata:exec timeout');
7862 7882 cb(error);
7863 7883 return;
7864 7884 }
7865 7885
7866 7886 cb();
7867 7887 });
7868 7888 } else {
7869 7889 cb();
7870 7890 }
7871 7891
7872 7892 }, function (cb) {
7873 7893 // This writes out the 'zoneconfig' file used by zoneinit to root's
7874 7894 // home directory in the zone.
7875 7895 if (! receiving
7876 7896 && BRAND_OPTIONS[vmobj.brand].features.zoneinit
7877 7897 && (! zoneinit.hasOwnProperty('features')
7878 7898 || zoneinit.features.zoneconfig)) {
7879 7899
7880 7900 // No 'features' means old dataset. If we have old dataset or
7881 7901 // one that really wants a zoneconfig, write it out.
7882 7902
7883 7903 writeZoneconfig(payload, log, function (err) {
7884 7904 cb(err);
7885 7905 });
7886 7906 } else {
7887 7907 cb();
7888 7908 }
7889 7909 }, function (cb) {
7890 7910 if (BRAND_OPTIONS[vmobj.brand].features.write_zone_netfiles
7891 7911 && !receiving) {
7892 7912
7893 7913 writeZoneNetfiles(payload, log, function (err) {
7894 7914 cb(err);
7895 7915 });
7896 7916 } else {
7897 7917 cb();
7898 7918 }
7899 7919 }, function (cb) {
7900 7920 if (vmobj.hasOwnProperty('zonepath')
7901 7921 && BRAND_OPTIONS[vmobj.brand].features.cleanup_dataset
7902 7922 && !receiving) {
7903 7923
7904 7924 cleanupMessyDataset(vmobj.zonepath, vmobj.brand, log,
7905 7925 function (err) {
7906 7926
7907 7927 cb(err);
7908 7928 });
7909 7929 } else {
7910 7930 cb();
7911 7931 }
7912 7932 }, function (cb) {
7913 7933 // Firewall data has not changed when reprovisioning, so we don't
7914 7934 // re-run addFirewallData()
7915 7935 if (reprovisioning) {
7916 7936 cb();
7917 7937 return;
7918 7938 }
7919 7939
7920 7940 // Add firewall data if it was included
7921 7941 addFirewallData(payload, vmobj, log, cb);
7922 7942 }, function (cb) {
7923 7943
7924 7944 var cancel;
7925 7945 var calledback = false;
7926 7946 var prov_wait = true;
7927 7947 // var_svc_provisioning is at installZone() scope
7928 7948
7929 7949 // The vm is now ready to start, we'll start if autoboot is set. If
7930 7950 // not, we also don't want to wait for 'provisioning'.
7931 7951 if (!payload.autoboot) {
7932 7952 cb();
7933 7953 return;
7934 7954 }
7935 7955
7936 7956 // In these cases we never wait for provisioning -> running
7937 7957 if (payload.nowait || receiving || vmobj.state !== 'provisioning') {
7938 7958 prov_wait = false;
7939 7959 }
7940 7960
7941 7961 // most VMs support the /var/svc/provision{ing,_success,_failure}
7942 7962 // files. For those, if !nowait, we wait for the file to change
7943 7963 // from provisioning -> either provision_success, or
7944 7964 // provision_failure.
7945 7965
7946 7966 if (prov_wait) {
7947 7967 // wait for /var/svc/provisioning -> provision_success/failure
7948 7968 cancel = VM.waitForProvisioning(vmobj, {log: log},
7949 7969 function (err) {
7950 7970
7951 7971 log.debug(err, 'waited for provisioning');
7952 7972
7953 7973 if (!err) {
7954 7974 log.info('provisioning complete: '
7955 7975 + '/var/svc/provisioning is gone');
7956 7976 // this will clear the provision transition
7957 7977 VM.unsetTransition(vmobj, {log: log},
7958 7978 function (unset_err) {
7959 7979
7960 7980 if (unset_err) {
7961 7981 log.error(unset_err, 'error unsetting '
7962 7982 + 'transition: ' + unset_err.message);
7963 7983 }
7964 7984 // this and the cb in the VM.start callback might
7965 7985 // both run if we don't check this.
7966 7986 if (!calledback) {
7967 7987 calledback = true;
7968 7988 cb(unset_err);
7969 7989 }
7970 7990 });
7971 7991 } else {
7972 7992 // failed but might not be able to cb if VM.start's
7973 7993 // callback already did.
7974 7994 log.error(err, 'error waiting for provisioning: '
7975 7995 + err.message);
7976 7996 // this and the cb in the VM.start callback might
7977 7997 // both run if we don't check this.
7978 7998 if (!calledback) {
7979 7999 calledback = true;
7980 8000 cb(err);
7981 8001 }
7982 8002 }
7983 8003 });
7984 8004 }
7985 8005
7986 8006 VM.start(payload.uuid, {}, {log: log}, function (err, res) {
7987 8007 if (err) {
7988 8008 // we failed to start so we'll never see provisioning, so
7989 8009 // cancel that and return the error.
7990 8010 if (cancel) {
7991 8011 log.info('cancelling VM.waitForProvisioning');
7992 8012 cancel();
7993 8013 }
7994 8014 // this and the cb in the VM.waitForProvisioning
7995 8015 // callback might both run if we don't check this.
7996 8016 if (!calledback) {
7997 8017 calledback = true;
7998 8018 cb(err);
7999 8019 }
8000 8020 return;
8001 8021 }
8002 8022 // if we're waiting for 'provisioning' VM.waitForProvisioning's
8003 8023 // callback will call cb(). If we're not going to wait, we call
8004 8024 // it here.
8005 8025 if (!prov_wait) {
8006 8026 // this and the cb in the VM.waitForProvisioning
8007 8027 // callback might both run if we don't check this.
8008 8028 if (!calledback) {
8009 8029 calledback = true;
8010 8030 cb();
8011 8031 }
8012 8032 }
8013 8033 });
8014 8034 }], function (error) {
8015 8035 callback(error);
8016 8036 }
8017 8037 );
8018 8038 }
8019 8039
8020 8040 function getZoneinitJSON(rootpath, log, cb)
8021 8041 {
8022 8042 var filename;
8023 8043 var zoneroot;
8024 8044
8025 8045 assert(log, 'no logger passed to getZoneinitJSON()');
8026 8046
8027 8047 zoneroot = path.join('/', rootpath, 'root');
8028 8048 filename = path.join(zoneroot, '/var/zoneinit/zoneinit.json');
8029 8049
8030 8050 try {
8031 8051 assertSafeZonePath(zoneroot, '/var/zoneinit/zoneinit.json',
8032 8052 {type: 'file', enoent_ok: true});
8033 8053 } catch (e) {
8034 8054 log.error(e, 'Error validating /var/zoneinit/zoneinit.json: '
8035 8055 + e.message);
8036 8056 cb(e);
8037 8057 return;
8038 8058 }
8039 8059
8040 8060 fs.readFile(filename, function (error, data) {
8041 8061 var zoneinit;
8042 8062
8043 8063 if (error && (error.code === 'ENOENT')) {
8044 8064 // doesn't exist, leave empty
8045 8065 log.debug('zoneinit.json does not exist.');
8046 8066 cb();
8047 8067 } else if (error) {
8048 8068 // error reading: fail.
8049 8069 cb(error);
8050 8070 } else {
8051 8071 // success try to load json
8052 8072 try {
8053 8073 zoneinit = JSON.parse(data.toString());
8054 8074 log.debug({'zoneinit_json': zoneinit},
8055 8075 'parsed zoneinit.json');
8056 8076 cb(null, zoneinit);
8057 8077 } catch (e) {
8058 8078 cb(e);
8059 8079 }
8060 8080 }
8061 8081 });
8062 8082 }
8063 8083
8064 8084 function getDatasetMountpoint(dataset, log, callback)
8065 8085 {
8066 8086 var args;
8067 8087 var cmd = '/usr/sbin/zfs';
8068 8088 var mountpoint;
8069 8089
8070 8090 assert(log, 'no logger passed to getDatasetMountpoint()');
8071 8091
8072 8092 args = ['get', '-H', '-o', 'value', 'mountpoint', dataset];
8073 8093
8074 8094 log.debug(cmd + ' ' + args.join(' '));
8075 8095 execFile(cmd, args, function (error, stdout, stderr) {
8076 8096 if (error) {
8077 8097 log.error(error, 'zfs get failed with: ' + stderr);
8078 8098 callback(error);
8079 8099 } else {
8080 8100 mountpoint = stdout.replace(/\n/g, '');
8081 8101 log.debug('mountpoint: "' + mountpoint + '"');
8082 8102 callback(null, mountpoint);
8083 8103 }
8084 8104 });
8085 8105 }
8086 8106
8087 8107 // TODO: pull data out of the massive zfs list we pulled earlier
8088 8108 function checkDatasetProvisionable(payload, log, callback)
8089 8109 {
8090 8110 var dataset;
8091 8111
8092 8112 assert(log, 'no logger passed to checkDatasetProvisionable()');
8093 8113
8094 8114 if (BRAND_OPTIONS[payload.brand].features.var_svc_provisioning) {
8095 8115 // when the brand always supports /var/svc/provisioning we don't have to
8096 8116 // worry about the dataset not supporting it.
8097 8117 callback(true);
8098 8118 return;
8099 8119 }
8100 8120
8101 8121 if (!payload.hasOwnProperty('zpool')
8102 8122 || !payload.hasOwnProperty('image_uuid')) {
8103 8123
8104 8124 log.error('missing properties required to find dataset: '
8105 8125 + JSON.stringify(payload));
8106 8126 callback(false);
8107 8127 return;
8108 8128 }
8109 8129
8110 8130 dataset = payload.zpool + '/' + payload.image_uuid;
8111 8131
8112 8132 getDatasetMountpoint(dataset, log, function (dataset_err, mountpoint) {
8113 8133 if (dataset_err) {
8114 8134 log.error('unable to find mount point for ' + dataset);
8115 8135 callback(false);
8116 8136 return;
8117 8137 }
8118 8138
8119 8139 getZoneinitJSON(dataset, log, function (zoneinit_err, zoneinit) {
8120 8140 var filename_1_6_x;
8121 8141 var filename_1_8_x;
8122 8142
8123 8143 if (zoneinit_err) {
8124 8144 log.error(zoneinit_err, 'getZoneinitJSON() failed, assuming '
8125 8145 + 'not provisionable.');
8126 8146 callback(false);
8127 8147 return;
8128 8148 } else if (!zoneinit) {
8129 8149 log.debug('no data from getZoneinitJSON(), using {}');
8130 8150 zoneinit = {};
8131 8151 }
8132 8152
8133 8153 if (zoneinit.hasOwnProperty('features')) {
8134 8154 if (zoneinit.features.var_svc_provisioning) {
8135 8155 log.info('zoneinit.features.var_svc_provisioning is '
8136 8156 + 'set.');
8137 8157 callback(true);
8138 8158 return;
8139 8159 }
8140 8160 // we have features but not var_svc_provisioning === true means
8141 8161 // we can't provision. Fall through and return false.
8142 8162 } else {
8143 8163 // Didn't load zoneinit features, so check for datasets that
8144 8164 // have // 04-mdata.sh. For 1.6.x and earlier datasets this was
8145 8165 // in /root but in 1.8.0 and 1.8.1 it is in /var/zoneinit. For
8146 8166 // 1.8.2 and later we'll not get here as the zoneinit.json will
8147 8167 // exist and we'll use that.
8148 8168 filename_1_6_x = path.join(mountpoint, 'root',
8149 8169 '/root/zoneinit.d/04-mdata.sh');
8150 8170 filename_1_8_x = path.join(mountpoint, 'root',
8151 8171 '/var/zoneinit/includes/04-mdata.sh');
8152 8172
8153 8173 if (fs.existsSync(filename_1_6_x)) {
8154 8174 log.info(filename_1_6_x + ' exists');
8155 8175 callback(true);
8156 8176 return;
8157 8177 } else {
8158 8178 log.debug(filename_1_6_x + ' does not exist');
8159 8179 if (fs.existsSync(filename_1_8_x)) {
8160 8180 log.info(filename_1_8_x + ' exists');
8161 8181 callback(true);
8162 8182 return;
8163 8183 } else {
8164 8184 log.debug(filename_1_8_x + ' does not exist');
8165 8185 // this was our last chance.
8166 8186 // Fall through and return false.
8167 8187 }
8168 8188 }
8169 8189 }
8170 8190
8171 8191 callback(false);
8172 8192 return;
8173 8193 });
8174 8194 });
8175 8195 }
8176 8196
8177 8197 // create and install a 'joyent' or 'kvm' brand zone.
8178 8198 function createZone(payload, log, callback)
8179 8199 {
8180 8200 var create_time;
8181 8201 var n;
8182 8202 var now = new Date;
8183 8203 var primary_found;
8184 8204 var provision_timeout = PROVISION_TIMEOUT;
8185 8205 var t;
8186 8206 var vm_version;
8187 8207 var zcfg;
8188 8208
8189 8209 assert(log, 'no logger passed to createZone()');
8190 8210
8191 8211 log.debug('createZone()');
8192 8212
8193 8213 payload.zfs_filesystem = payload.zpool + '/' + payload.zonename;
8194 8214 payload.zonepath = '/' + payload.zfs_filesystem;
8195 8215
8196 8216 // we add create-timestamp in all cases except where we're receiving since
8197 8217 // in that case we want to preserve the original create-timestamp.
8198 8218 if (!payload.hasOwnProperty('transition')
8199 8219 || (payload.transition.transition !== 'receiving')
8200 8220 || !payload.hasOwnProperty('create_timestamp')) {
8201 8221
8202 8222 create_time = now.toISOString();
8203 8223 } else {
8204 8224 create_time = payload.create_timestamp;
8205 8225 }
8206 8226
8207 8227 // we add vm-version (property v) in all cases except where we're receiving
8208 8228 // since in that case we want to preserve the original version.
8209 8229 if (!payload.hasOwnProperty('transition')
8210 8230 || (payload.transition.transition !== 'receiving')
8211 8231 || !payload.hasOwnProperty('v')) {
8212 8232
8213 8233 vm_version = 1;
8214 8234 } else {
8215 8235 vm_version = payload.v;
8216 8236 }
8217 8237
8218 8238 // set the properties that can't be updated later here.
8219 8239 zcfg = 'create -b\n'
8220 8240 + 'set zonepath=' + payload.zonepath + '\n'
8221 8241 + 'set brand=' + payload.brand + '\n'
8222 8242 + 'set uuid=' + payload.uuid + '\n'
8223 8243 + 'set ip-type=exclusive\n'
8224 8244 + 'add attr; set name="vm-version"; set type=string; set value="'
8225 8245 + vm_version + '"; end\n'
8226 8246 + 'add attr; set name="create-timestamp"; set type=string; set value="'
8227 8247 + create_time + '"; end\n';
8228 8248
8229 8249 if (payload.hasOwnProperty('transition')) {
8230 8250 // IMPORTANT: this is for internal use only and should not be documented
8231 8251 // as an option for create's payload. Used for receive.
8232 8252 t = payload.transition;
8233 8253 zcfg = zcfg
8234 8254 + buildTransitionZonecfg(t.transition, t.target, t.timeout) + '\n';
8235 8255 } else {
8236 8256 // Assume this is really a new VM, add transition called 'provisioning'
8237 8257 // only if the machine is going to be booting.
8238 8258 if (!payload.hasOwnProperty('autoboot') || payload.autoboot) {
8239 8259 zcfg = zcfg + buildTransitionZonecfg('provisioning', 'running',
8240 8260 provision_timeout * 1000) + '\n';
8241 8261 }
8242 8262 }
8243 8263
8244 8264 // We call the property 'dataset-uuid' even though the property name is
8245 8265 // image_uuid because existing VMs in the wild will be using dataset-uuid
8246 8266 // already, and we are the point where the image becomes a dataset anyway.
8247 8267 if (payload.hasOwnProperty('image_uuid')) {
8248 8268 zcfg = zcfg + 'add attr; set name="dataset-uuid"; set type=string; '
8249 8269 + 'set value="' + payload.image_uuid + '"; end\n';
8250 8270 }
8251 8271
8252 8272 if (BRAND_OPTIONS[payload.brand].features.use_vm_autoboot) {
8253 8273 // we always set autoboot=false for VM zones, since we want vmadmd to
8254 8274 // boot them and not the zones tools. Use vm-autoboot to control VMs
8255 8275 zcfg = zcfg + 'set autoboot=false\n';
8256 8276 }
8257 8277
8258 8278 // ensure that we have a primary nic, even if one wasn't specified
8259 8279 if (payload.hasOwnProperty('add_nics') && payload.add_nics.length != 0) {
8260 8280 primary_found = false;
8261 8281
8262 8282 for (n in payload.add_nics) {
8263 8283 n = payload.add_nics[n];
8264 8284 if (n.hasOwnProperty('primary') && n.primary) {
8265 8285 primary_found = true;
8266 8286 break;
8267 8287 }
8268 8288 }
8269 8289 if (!primary_found) {
8270 8290 payload.add_nics[0].primary = true;
8271 8291 }
8272 8292 }
8273 8293
8274 8294 // Passing an empty first parameter here, tells buildZonecfgUpdate that
8275 8295 // we're talking about a new machine.
8276 8296 zcfg = zcfg + buildZonecfgUpdate({}, payload, log);
8277 8297
8278 8298 // include the zonecfg in the debug output to help track down problems.
8279 8299 log.debug(zcfg);
8280 8300
8281 8301 // send the zonecfg data we just generated as a file to zonecfg,
8282 8302 // this will create the zone.
8283 8303 zonecfgFile(zcfg, ['-z', payload.zonename], log, function (err, fds) {
8284 8304 if (err || payload.create_only) {
8285 8305 log.error({err: err, zcfg: zcfg, stdout: fds.stdout,
8286 8306 stderr: fds.stderr}, 'failed to modify zonecfg');
8287 8307 callback(err);
8288 8308 } else {
8289 8309 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8290 8310 'modified zonecfg');
8291 8311 installZone(payload, log, callback);
8292 8312 }
8293 8313 });
8294 8314 }
8295 8315
8296 8316 function normalizeNics(payload, vmobj)
8297 8317 {
8298 8318 var n;
8299 8319 var nic;
8300 8320
8301 8321 // ensure all NICs being created/added have a MAC, remove the 'index' if it
8302 8322 // is passed (that's deprecated), rename 'interface' to 'physical'.
8303 8323 if (payload.hasOwnProperty('add_nics')) {
8304 8324 for (n in payload.add_nics) {
8305 8325 if (payload.add_nics.hasOwnProperty(n)) {
8306 8326 nic = payload.add_nics[n];
8307 8327
8308 8328 if (!nic.hasOwnProperty('mac')
8309 8329 && !nic.hasOwnProperty('vrrp_vrid')) {
8310 8330 nic.mac = generateMAC();
8311 8331 }
8312 8332 delete nic.index;
8313 8333 if (nic.hasOwnProperty('interface')) {
8314 8334 nic.physical = nic.interface;
8315 8335 delete nic.interface;
8316 8336 }
8317 8337
8318 8338 // nics.*.primary only supports true value, unset false. We also
8319 8339 // handle the case here why they used the deprecated '1' value.
8320 8340 // We will have already warned them, but still support for now.
8321 8341 if (nic.hasOwnProperty('primary')) {
8322 8342 if (nic.primary || nic.primary === '1'
8323 8343 || nic.primary === 1) {
8324 8344
8325 8345 nic.primary = true;
8326 8346 } else {
8327 8347 delete nic.primary;
8328 8348 }
8329 8349 }
8330 8350 }
8331 8351 }
8332 8352 }
8333 8353 }
8334 8354
8335 8355 /*
8336 8356 * This is called for both create and update, everything here should be safe for
8337 8357 * both. The vmobj will be set if it's an update.
8338 8358 *
8339 8359 */
8340 8360 function normalizePayload(payload, vmobj, log, callback)
8341 8361 {
8342 8362 var action;
8343 8363 var allowed;
8344 8364 var brand;
8345 8365 var property;
8346 8366
8347 8367 assert(log, 'no logger passed to normalizePayload()');
8348 8368
8349 8369 // fix type of arguments that should be numbers, do this here so that fixing
8350 8370 // memory works correctly later using math.
8351 8371 for (property in payload) {
8352 8372 if (payload.hasOwnProperty(property)) {
8353 8373 if (PAYLOAD_PROPERTIES.hasOwnProperty(property)
8354 8374 && PAYLOAD_PROPERTIES[property].type === 'integer'
8355 8375 && payload[property] !== undefined) {
8356 8376 // undefined is a special case since we use that to unset props
8357 8377
8358 8378 payload[property] = Number(payload[property]);
8359 8379 if (isNaN(payload[property])) {
8360 8380 callback(new Error('Invalid value for ' + property + ': '
8361 8381 + JSON.stringify(payload[property]) + ':'
8362 8382 + typeof (payload[property])));
8363 8383 return;
8364 8384 }
8365 8385 }
8366 8386 }
8367 8387 }
8368 8388
8369 8389 if (payload.hasOwnProperty('quota') && payload.quota === undefined) {
8370 8390 // when unsetting quota we set to 0
8371 8391 payload.quota = 0;
8372 8392 }
8373 8393
8374 8394 if (vmobj) {
8375 8395 /* update */
8376 8396 fixPayloadMemory(payload, vmobj, log);
8377 8397 action = 'update';
8378 8398 } else {
8379 8399 /* this also calls fixPayloadMemory() */
8380 8400 applyZoneDefaults(payload, log);
8381 8401
8382 8402 if (payload.hasOwnProperty('create_only')
8383 8403 && payload.transition.transition === 'receiving') {
8384 8404
8385 8405 action = 'receive';
8386 8406 } else {
8387 8407 action = 'create';
8388 8408 }
8389 8409 }
8390 8410
8391 8411 // Should always have a brand after we applied defaults.
8392 8412 if (vmobj && vmobj.hasOwnProperty('brand')) {
8393 8413 brand = vmobj.brand;
8394 8414 } else if (payload.hasOwnProperty('brand')) {
8395 8415 brand = payload.brand;
8396 8416 } else {
8397 8417 callback(new Error('Unable to determine brand for payload'));
8398 8418 return;
8399 8419 }
8400 8420
8401 8421 // Historically we supported dataset_uuid for joyent+joyent-minimal and
8402 8422 // zone_dataset_uuid for kvm. Now we just support image_uuid so give a
8403 8423 // deprecation warning and translate if old version specified. This needs
8404 8424 // to happen before VM.validate because image_uuid is required for most
8405 8425 // VMs.
8406 8426 allowed = BRAND_OPTIONS[brand].allowed_properties;
8407 8427 if ((allowed.hasOwnProperty('dataset_uuid')
8408 8428 && payload.hasOwnProperty('dataset_uuid'))
8409 8429 || (allowed.hasOwnProperty('zone_dataset_uuid')
8410 8430 && payload.hasOwnProperty('zone_dataset_uuid'))) {
8411 8431
8412 8432 property = (payload.hasOwnProperty('dataset_uuid') ? 'dataset_uuid'
8413 8433 : 'zone_dataset_uuid');
8414 8434
8415 8435 if (payload.hasOwnProperty('image_uuid')) {
8416 8436 log.warn('DEPRECATED option ' + property + ' found, '
8417 8437 + 'ignoring. In the future use image_uuid only.');
8418 8438 } else {
8419 8439 log.warn('DEPRECATED option ' + property + ' found, '
8420 8440 + 'ignoring. In the future use image_uuid instead.');
8421 8441 payload.image_uuid = payload[property];
8422 8442 delete payload.dataset_uuid;
8423 8443 }
8424 8444 }
8425 8445
8426 8446 // after ZoneDefaults have been applied, we should always have zone. Now
8427 8447 // we validate the payload properties and remove any that are invalid. If
8428 8448 // there are bad values we'll just fail.
8429 8449 VM.validate(brand, action, payload, {log: log}, function (errors) {
8430 8450 var bad_prop;
8431 8451 var compound_props = ['disks', 'nics', 'filesystems'];
8432 8452 var matches;
8433 8453 var obj;
8434 8454 var prop;
8435 8455
8436 8456 if (errors) {
8437 8457 if (errors.hasOwnProperty('bad_brand')) {
8438 8458 callback(new Error('Invalid brand while validating payload: '
8439 8459 + JSON.stringify(brand)));
8440 8460 return;
8441 8461 }
8442 8462 if (errors.bad_values.length > 0) {
8443 8463 callback(new Error('Invalid value(s) for: '
8444 8464 + errors.bad_values.join(',')));
8445 8465 return;
8446 8466 }
8447 8467 if (errors.missing_properties.length > 0) {
8448 8468 callback(new Error('Missing required properties: '
8449 8469 + errors.missing_properties.join(',')));
8450 8470 return;
8451 8471 }
8452 8472 for (bad_prop in errors.bad_properties) {
8453 8473 bad_prop = errors.bad_properties[bad_prop];
8454 8474 log.warn('Warning, invalid ' + action + ' property: ['
8455 8475 + bad_prop + '] removing from payload.');
8456 8476
8457 8477 // for bad properties like nics.*.allow_unfiltered_promisc we
8458 8478 // need to remove it from add_nics, update_nics, etc.
8459 8479 for (prop in compound_props) {
8460 8480 prop = compound_props[prop];
8461 8481
8462 8482 matches = new RegExp('^' + prop
8463 8483 + '\\.\\*\\.(.*)$').exec(bad_prop);
8464 8484 if (matches) {
8465 8485 if (payload.hasOwnProperty(prop)) {
8466 8486 for (obj in payload[prop]) {
8467 8487 delete payload[prop][obj][matches[1]];
8468 8488 }
8469 8489 }
8470 8490 if (payload.hasOwnProperty('add_' + prop)) {
8471 8491 for (obj in payload['add_' + prop]) {
8472 8492 delete payload['add_' + prop][obj][matches[1]];
8473 8493 }
8474 8494 }
8475 8495 if (payload.hasOwnProperty('update_' + prop)) {
8476 8496 for (obj in payload['update_' + prop]) {
8477 8497 delete payload['update_'
8478 8498 + prop][obj][matches[1]];
8479 8499 }
8480 8500 }
8481 8501 }
8482 8502 }
8483 8503
8484 8504 delete payload[bad_prop];
8485 8505 }
8486 8506 }
8487 8507
8488 8508 // By the time we got here all the properties in the payload are allowed
8489 8509
8490 8510 // Now we make sure we've got a zonename (use uuid if not already set)
8491 8511 if (!payload.hasOwnProperty('zonename')
8492 8512 || payload.zonename === undefined) {
8493 8513
8494 8514 payload.zonename = payload.uuid;
8495 8515 }
8496 8516
8497 8517 // You use 'disks' and 'nics' when creating, but the underlying
8498 8518 // functions expect add_disks and add_nics, so we rename them now that
8499 8519 // we've confirmed we've got the correct thing for this action.
8500 8520 if (payload.hasOwnProperty('disks')) {
8501 8521 if (payload.hasOwnProperty('add_disks')) {
8502 8522 callback(new Error('Cannot specify both "disks" and '
8503 8523 + '"add_disks"'));
8504 8524 return;
8505 8525 }
8506 8526 payload.add_disks = payload.disks;
8507 8527 delete payload.disks;
8508 8528 }
8509 8529 if (payload.hasOwnProperty('nics')) {
8510 8530 if (payload.hasOwnProperty('add_nics')) {
8511 8531 callback(new Error('Cannot specify both "nics" and '
8512 8532 + '"add_nics"'));
8513 8533 return;
8514 8534 }
8515 8535 payload.add_nics = payload.nics;
8516 8536 delete payload.nics;
8517 8537 }
8518 8538 if (payload.hasOwnProperty('filesystems')) {
8519 8539 if (payload.hasOwnProperty('add_filesystems')) {
8520 8540 callback(new Error('Cannot specify both "filesystems" and '
8521 8541 + '"add_filesystems"'));
8522 8542 return;
8523 8543 }
8524 8544 payload.add_filesystems = payload.filesystems;
8525 8545 delete payload.filesystems;
8526 8546 }
8527 8547
8528 8548 // if there's a zfs_root_* and no zfs_data_*, normally the properties
8529 8549 // would fall through, we don't want that.
8530 8550 if (payload.hasOwnProperty('zfs_root_compression')
8531 8551 && !payload.hasOwnProperty('zfs_data_compression')) {
8532 8552
8533 8553 if (vmobj && vmobj.hasOwnProperty('zfs_data_compression')) {
8534 8554 // keep existing value.
8535 8555 payload.zfs_data_compression = vmobj.zfs_data_compression;
8536 8556 } else {
8537 8557 // keep default value.
8538 8558 payload.zfs_data_compression = 'off';
8539 8559 }
8540 8560 }
8541 8561 if (payload.hasOwnProperty('zfs_root_recsize')
8542 8562 && !payload.hasOwnProperty('zfs_data_recsize')) {
8543 8563
8544 8564 if (vmobj && vmobj.hasOwnProperty('zfs_data_recsize')) {
8545 8565 // keep existing value.
8546 8566 payload.zfs_data_recsize = vmobj.zfs_data_recsize;
8547 8567 } else {
8548 8568 // keep default value.
8549 8569 payload.zfs_data_recsize = 131072;
8550 8570 }
8551 8571 }
8552 8572
8553 8573 // this will ensure we've got a MAC, etc.
8554 8574 normalizeNics(payload, vmobj);
8555 8575
8556 8576 // Fix types for boolean fields in case someone put in 'false'/'true'
8557 8577 // instead of false/true
8558 8578 for (property in payload) {
8559 8579 if (payload.hasOwnProperty(property)) {
8560 8580 if (PAYLOAD_PROPERTIES.hasOwnProperty(property)
8561 8581 && PAYLOAD_PROPERTIES[property].type === 'boolean') {
8562 8582
8563 8583 payload[property] = fixBooleanLoose(payload[property]);
8564 8584 }
8565 8585 }
8566 8586 }
8567 8587
8568 8588 // We used to support zfs_storage_pool_name, but zpool is better.
8569 8589 if (payload.hasOwnProperty('zfs_storage_pool_name')) {
8570 8590 if (payload.hasOwnProperty('zpool')) {
8571 8591 log.warn('DEPRECATED option zfs_storage_pool_name found, '
8572 8592 + 'ignoring!');
8573 8593 } else {
8574 8594 log.warn('DEPRECATED option zfs_storage_pool_name found, '
8575 8595 + 'replacing with zpool!');
8576 8596 payload.zpool = payload.zfs_storage_pool_name;
8577 8597 delete payload.zfs_storage_pool_name;
8578 8598 }
8579 8599 }
8580 8600
8581 8601 // When creating a VM with SPICE you need the image_uuid, if you don't
8582 8602 // pass that, we'll remove any SPICE options.
8583 8603 if (action === 'create'
8584 8604 && !payload.hasOwnProperty('image_uuid')) {
8585 8605
8586 8606 if (payload.hasOwnProperty('spice_opts')
8587 8607 || payload.hasOwnProperty('spice_password')
8588 8608 || payload.hasOwnProperty('spice_port')) {
8589 8609
8590 8610 log.warn('Creating with SPICE options requires '
8591 8611 + 'image_uuid, REMOVING spice_*');
8592 8612 delete payload.spice_opts;
8593 8613 delete payload.spice_password;
8594 8614 delete payload.spice_port;
8595 8615 }
8596 8616 }
8597 8617
8598 8618 checkPayloadProperties(payload, vmobj, log, function (e) {
8599 8619 if (e) {
8600 8620 callback(e);
8601 8621 } else {
8602 8622 callback();
8603 8623 }
8604 8624 });
8605 8625 });
8606 8626 }
8607 8627
8608 8628 function buildTransitionZonecfg(transition, target, timeout)
8609 8629 {
8610 8630 var cmdline;
8611 8631
8612 8632 cmdline = 'add attr; set name=transition; set value="'
8613 8633 + transition + ':' + target + ':' + (Date.now(0) + timeout).toString()
8614 8634 + '"; set type=string; end';
8615 8635
8616 8636 return cmdline;
8617 8637 }
8618 8638
8619 8639 // vmobj should have:
8620 8640 //
8621 8641 // uuid
8622 8642 // transition_to (if set)
8623 8643 //
8624 8644 exports.unsetTransition = function (vmobj, options, callback)
8625 8645 {
8626 8646 var log;
8627 8647
8628 8648 // options is optional
8629 8649 if (arguments.length === 2) {
8630 8650 callback = arguments[1];
8631 8651 options = {};
8632 8652 }
8633 8653
8634 8654 ensureLogging(true);
8635 8655 if (options.hasOwnProperty('log')) {
8636 8656 log = options.log;
8637 8657 } else {
8638 8658 log = VM.log.child({action: 'unsetTransition', vm: vmobj.uuid});
8639 8659 }
8640 8660
8641 8661 zonecfg(['-u', vmobj.uuid, 'remove -F attr name=transition'], log,
8642 8662 function (err, fds) {
8643 8663
8644 8664 if (err) {
8645 8665 // log at info because this might be because already removed
8646 8666 log.info({err: err, stdout: fds.stdout, stderr: fds.stderr},
8647 8667 'unable to remove transition for zone ' + vmobj.uuid);
8648 8668 } else {
8649 8669 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8650 8670 'removed transition for zone ' + vmobj.uuid);
8651 8671 }
8652 8672
8653 8673 zonecfg(['-u', vmobj.uuid, 'info attr name=transition'], log,
8654 8674 function (info_err, info_fds) {
8655 8675
8656 8676 if (info_err) {
8657 8677 log.error({err: info_err, stdout: info_fds.stdout,
8658 8678 stderr: info_fds.stderr},
8659 8679 'failed to confirm transition removal');
8660 8680 callback(info_err);
8661 8681 return;
8662 8682 }
8663 8683
8664 8684 if (info_fds.stdout !== 'No such attr resource.\n') {
8665 8685 log.error({stdout: info_fds.stdout, stderr: info_fds.stderr},
8666 8686 'unknown error checking transition after removal');
8667 8687 callback(new Error('transition does not appear to have been '
8668 8688 + 'removed zonecfg said: ' + JSON.stringify(info_fds)));
8669 8689 return;
8670 8690 }
8671 8691
8672 8692 // removed the transition, now attempt to start if we're rebooting.
8673 8693 if (vmobj.transition_to && vmobj.transition_to === 'start') {
8674 8694 log.debug('VM ' + vmobj.uuid + ' was stopping for reboot, '
8675 8695 + 'transitioning to start.');
8676 8696 VM.start(vmobj.uuid, {}, {log: log}, function (e) {
8677 8697 if (e) {
8678 8698 log.error(e, 'failed to start when clearing '
8679 8699 + 'transition');
8680 8700 }
8681 8701 callback();
8682 8702 });
8683 8703 } else {
8684 8704 callback();
8685 8705 }
8686 8706 });
8687 8707 });
8688 8708 };
8689 8709
8690 8710 //
8691 8711 // vmobj fields used:
8692 8712 //
8693 8713 // transition
8694 8714 // uuid
8695 8715 //
8696 8716 function setTransition(vmobj, transition, target, timeout, log, callback)
8697 8717 {
8698 8718 assert(log, 'no logger passed to setTransition()');
8699 8719
8700 8720 if (!timeout) {
8701 8721 callback(new Error('setTransition() requires timeout argument.'));
8702 8722 return;
8703 8723 }
8704 8724
8705 8725 async.series([
8706 8726 function (cb) {
8707 8727 // unset an existing transition
8708 8728 if (vmobj.hasOwnProperty('transition')) {
8709 8729 VM.unsetTransition(vmobj, {log: log}, cb);
8710 8730 } else {
8711 8731 cb();
8712 8732 }
8713 8733 }, function (cb) {
8714 8734 var zcfg;
8715 8735
8716 8736 zcfg = buildTransitionZonecfg(transition, target, timeout);
8717 8737 zonecfg(['-u', vmobj.uuid, zcfg], log, function (err, fds) {
8718 8738 if (err) {
8719 8739 log.error({err: err, stdout: fds.stdout,
8720 8740 stderr: fds.stderr}, 'failed to set transition='
8721 8741 + transition + ' for VM ' + vmobj.uuid);
8722 8742 } else {
8723 8743 log.debug({stdout: fds.stdout, stderr: fds.stderr},
8724 8744 'set transition=' + transition + ' for vm '
8725 8745 + vmobj.uuid);
8726 8746 }
8727 8747
8728 8748 cb(err);
8729 8749 });
8730 8750 }
8731 8751 ], function (error) {
8732 8752 callback(error);
8733 8753 });
8734 8754 }
8735 8755
8736 8756 function receiveVM(json, log, callback)
8737 8757 {
8738 8758 var payload = {};
8739 8759
8740 8760 assert(log, 'no logger passed to receiveVM()');
8741 8761
8742 8762 try {
8743 8763 payload = JSON.parse(json);
8744 8764 } catch (e) {
8745 8765 callback(e);
8746 8766 return;
8747 8767 }
8748 8768
8749 8769 payload.create_only = true;
8750 8770
8751 8771 // adding transition here is considered to be *internal only* not for
8752 8772 // consumer use and not to be documented as a property you can use with
8753 8773 // create.
8754 8774 payload.transition =
8755 8775 {'transition': 'receiving', 'target': 'stopped', 'timeout': 86400};
8756 8776
8757 8777 // We delete tags and metadata here becasue this exists in the root
8758 8778 // dataset which we will be copying, so it would be duplicated here.
8759 8779 delete payload.customer_metadata;
8760 8780 delete payload.internal_metadata;
8761 8781 delete payload.tags;
8762 8782
8763 8783 // On receive we need to make sure that we don't create new disks so we
8764 8784 // mark them all as nocreate. We also can't set the block_size of imported
8765 8785 // volumes, so we remove that.
8766 8786 if (payload.hasOwnProperty('disks')) {
8767 8787 var disk_idx;
8768 8788
8769 8789 for (disk_idx in payload.disks) {
8770 8790 payload.disks[disk_idx].nocreate = true;
8771 8791
8772 8792 if (payload.disks[disk_idx].image_uuid) {
8773 8793 delete payload.disks[disk_idx].block_size;
8774 8794 }
8775 8795 }
8776 8796 }
8777 8797
8778 8798 VM.create(payload, {log: log}, function (err, result) {
8779 8799 if (err) {
8780 8800 callback(err);
8781 8801 }
8782 8802
8783 8803 // don't include the special transition in the payload we write out.
8784 8804 delete payload.transition;
8785 8805
8786 8806 fs.writeFile('/etc/zones/' + payload.uuid + '-receiving.json',
8787 8807 JSON.stringify(payload, null, 2), function (e) {
8788 8808
8789 8809 if (e) {
8790 8810 callback(e);
8791 8811 return;
8792 8812 }
8793 8813
8794 8814 // ready for datasets
8795 8815 callback(null, result);
8796 8816 });
8797 8817 });
8798 8818 }
8799 8819
8800 8820 function receiveStdinChunk(type, log, callback)
8801 8821 {
8802 8822 var child;
8803 8823 var chunk_name = '';
8804 8824 var chunk_size = 0;
8805 8825 var json = '';
8806 8826 var remaining = '';
8807 8827
8808 8828 assert(log, 'no logger passed to receiveStdinChunk()');
8809 8829
8810 8830 /*
8811 8831 * XXX
8812 8832 *
8813 8833 * node 0.6.x removed support for arbitrary file descriptors which
8814 8834 * means we can only handle stdin for now since we need to pass this
8815 8835 * descriptor directly to the child. 0.8.x is supposed to reintroduce
8816 8836 * this functionality. When we do, this should be changed to open
8817 8837 * the file and set fd to the descriptor, and we should be able to
8818 8838 * get rid of vmunbundle.
8819 8839 *
8820 8840 */
8821 8841
8822 8842 if (type === 'JSON') {
8823 8843 log.info('/usr/vm/sbin/vmunbundle json');
8824 8844 child = spawn('/usr/vm/sbin/vmunbundle', ['json'],
8825 8845 {customFds: [0, -1, -1]});
8826 8846 } else if (type === 'DATASET') {
8827 8847 log.info('/usr/vm/sbin/vmunbundle dataset');
8828 8848 child = spawn('/usr/vm/sbin/vmunbundle', ['dataset'],
8829 8849 {customFds: [0, -1, -1]});
8830 8850 } else {
8831 8851 callback(new Error('Unsupported chunk type ' + type));
8832 8852 }
8833 8853
8834 8854 child.stderr.on('data', function (data) {
8835 8855 var idx;
8836 8856 var line;
8837 8857 var matches;
8838 8858
8839 8859 remaining += data.toString();
8840 8860
8841 8861 idx = remaining.indexOf('\n');
8842 8862 while (idx > -1) {
8843 8863 line = trim(remaining.substring(0, idx));
8844 8864 remaining = remaining.substring(idx + 1);
8845 8865
8846 8866 log.debug('VMUNBUNDLE: ' + line);
8847 8867 matches = line.match(/Size: ([\d]+)/);
8848 8868 if (matches) {
8849 8869 chunk_size = Number(matches[1]);
8850 8870 }
8851 8871 matches = line.match(/Name: \[(.*)\]/);
8852 8872 if (matches) {
8853 8873 chunk_name = matches[1];
8854 8874 }
8855 8875
8856 8876 idx = remaining.indexOf('\n');
8857 8877 }
8858 8878 });
8859 8879
8860 8880 child.stdout.on('data', function (data) {
8861 8881 json += data.toString();
8862 8882 log.debug('json size is ' + json.length);
8863 8883 });
8864 8884
8865 8885 child.on('close', function (code) {
8866 8886 log.debug('vmunbundle process exited with code ' + code);
8867 8887 if (code === 3) {
8868 8888 log.debug('vmbundle: end of bundle.');
8869 8889 callback(null, 'EOF');
8870 8890 return;
8871 8891 } else if (code !== 0) {
8872 8892 callback(new Error('vmunbundle exited with code ' + code));
8873 8893 return;
8874 8894 }
8875 8895
8876 8896 // if it was a dataset, we've now imported it.
8877 8897 // if it was json, we've now got it in the json var.
8878 8898
8879 8899 if (type === 'DATASET') {
8880 8900 log.info('Imported dataset ' + chunk_name);
8881 8901 // delete 'sending' snapshot
8882 8902 zfs(['destroy', '-F', chunk_name + '@sending'], log,
8883 8903 function (err, fds) {
8884 8904 if (err) {
8885 8905 log.warn(err, 'Failed to destroy ' + chunk_name
8886 8906 + '@sending: ' + err.message);
8887 8907 }
8888 8908 callback();
8889 8909 }
8890 8910 );
8891 8911 } else if (type === 'JSON' && chunk_name === 'JSON'
8892 8912 && json.length <= chunk_size && json.length > 0) {
8893 8913
8894 8914 receiveVM(json, log, function (e, result) {
8895 8915 if (e) {
8896 8916 callback(e);
8897 8917 return;
8898 8918 }
8899 8919 log.info('Receive returning: ' + JSON.stringify(result));
8900 8920 callback(null, result);
8901 8921 });
8902 8922 } else {
8903 8923 log.debug('type: [' + type + ']');
8904 8924 log.debug('chunk_name: [' + chunk_name + ']');
8905 8925 log.debug('chunk_size: [' + chunk_size + ']');
8906 8926 log.debug('json.length: [' + json.length + ']');
8907 8927 log.warn('Failed to get ' + type + '!');
8908 8928 callback(new Error('Failed to get ' + type + '!'));
8909 8929 }
8910 8930 });
8911 8931 }
8912 8932
8913 8933 exports.receive = function (target, options, callback)
8914 8934 {
8915 8935 var log;
8916 8936
8917 8937 // options is optional
8918 8938 if (arguments.length === 2) {
8919 8939 callback = arguments[1];
8920 8940 options = {};
8921 8941 }
8922 8942
8923 8943 ensureLogging(true);
8924 8944
8925 8945 // We don't know anything about this VM yet, so we don't create a
8926 8946 // VM.log.child.
8927 8947 if (options.hasOwnProperty('log')) {
8928 8948 log = options.log;
8929 8949 } else {
8930 8950 log = VM.log;
8931 8951 }
8932 8952
8933 8953 log.info('Receiving VM from: ' + JSON.stringify(target));
8934 8954
8935 8955 if (target.hasOwnProperty('host') && target.hasOwnProperty('port')) {
8936 8956 // network receive not yet supported either.
8937 8957 callback(new Error('cannot receive from ' + JSON.stringify(target)));
8938 8958 return;
8939 8959 } else if (typeof (target) !== 'string' || target !== '-') {
8940 8960 callback(new Error('cannot receive from ' + JSON.stringify(target)));
8941 8961 return;
8942 8962 }
8943 8963
8944 8964 receiveStdinChunk('JSON', log, function (error, result) {
8945 8965 var eof = false;
8946 8966
8947 8967 if (error) {
8948 8968 callback(error);
8949 8969 return;
8950 8970 }
8951 8971 if (result && result === 'EOF') {
8952 8972 callback(new Error('unable to find JSON in stdin.'));
8953 8973 } else if (result && result.hasOwnProperty('uuid')) {
8954 8974 // VM started receive, now need datasets
8955 8975
8956 8976 // We have JSON, so we can log better now if we need one
8957 8977 if (!options.hasOwnProperty('log')) {
8958 8978 log = VM.log.child({action: 'receive', vm: result.uuid});
8959 8979 }
8960 8980
8961 8981 log.info('Receiving VM ' + result.uuid);
8962 8982 log.debug('now looking for datasets');
8963 8983
8964 8984 async.whilst(
8965 8985 function () { return !eof; },
8966 8986 function (cb) {
8967 8987 receiveStdinChunk('DATASET', log, function (err, res) {
8968 8988 if (err) {
8969 8989 cb(err);
8970 8990 return;
8971 8991 }
8972 8992 if (res === 'EOF') {
8973 8993 eof = true;
8974 8994 }
8975 8995 cb();
8976 8996 });
8977 8997 }, function (err) {
8978 8998 if (err) {
8979 8999 callback(err);
8980 9000 return;
8981 9001 }
8982 9002 // no error so we read all the datasets, try an install.
8983 9003 log.info('receive calling VM.install: ' + eof);
8984 9004 VM.install(result.uuid, {log: log}, function (e) {
8985 9005 if (e) {
8986 9006 log.warn(e, 'couldn\'t install VM: '
8987 9007 + e.message);
8988 9008 }
8989 9009 callback(e, result);
8990 9010 });
8991 9011 }
8992 9012 );
8993 9013 } else {
8994 9014 callback(new Error('unable to receive JSON'));
8995 9015 }
8996 9016 });
8997 9017 };
8998 9018
8999 9019 exports.reprovision = function (uuid, payload, options, callback)
9000 9020 {
9001 9021 var log;
9002 9022 var provision_timeout = PROVISION_TIMEOUT;
9003 9023 var set_transition = false;
9004 9024 var snapshot;
9005 9025 var vmobj;
9006 9026
9007 9027 // options is optional
9008 9028 if (arguments.length === 3) {
9009 9029 callback = arguments[2];
9010 9030 options = {};
9011 9031 }
9012 9032
9013 9033 ensureLogging(true);
9014 9034 if (options.hasOwnProperty('log')) {
9015 9035 log = options.log;
9016 9036 } else {
9017 9037 log = VM.log.child({action: 'reprovision', vm: uuid});
9018 9038 }
9019 9039
9020 9040 log.info('Reprovisioning VM ' + uuid + ', original payload:\n'
9021 9041 + JSON.stringify(payload, null, 2));
9022 9042
9023 9043 async.waterfall([
9024 9044 function (cb) {
9025 9045 VM.load(uuid, {
9026 9046 fields: [
9027 9047 'brand',
9028 9048 'datasets',
9029 9049 'hostname',
9030 9050 'nics',
9031 9051 'quota',
9032 9052 'state',
9033 9053 'uuid',
9034 9054 'zfs_filesystem',
9035 9055 'zone_state',
9036 9056 'zonename',
9037 9057 'zonepath',
9038 9058 'zpool'
9039 9059 ],
9040 9060 log: log
9041 9061 }, function (err, obj) {
9042 9062 if (err) {
9043 9063 cb(err);
9044 9064 return;
9045 9065 }
9046 9066 vmobj = obj;
9047 9067 log.debug('Loaded VM is: ' + JSON.stringify(vmobj, null, 2));
9048 9068 cb();
9049 9069 });
9050 9070 }, function (cb) {
9051 9071 if (BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
9052 9072 && BRAND_OPTIONS[vmobj.brand].features.reprovision
9053 9073 && BRAND_OPTIONS[vmobj.brand].features.brand_install_script) {
9054 9074
9055 9075 cb();
9056 9076 } else {
9057 9077 cb(new Error('brand "' + vmobj.brand + '" does not yet support'
9058 9078 + ' reprovision'));
9059 9079 }
9060 9080 }, function (cb) {
9061 9081 // only support image_uuid at top level (for non-KVM currently)
9062 9082 if (!payload.hasOwnProperty('image_uuid')) {
9063 9083 cb(new Error('payload is missing image_uuid'));
9064 9084 } else {
9065 9085 cb();
9066 9086 }
9067 9087 }, function (cb) {
9068 9088 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 1) {
9069 9089 cb(new Error('cannot support reprovision with multiple '
9070 9090 + 'delegated datasets'));
9071 9091 return;
9072 9092 } else if (vmobj.hasOwnProperty('datasets')
9073 9093 && vmobj.datasets.length === 1
9074 9094 && vmobj.datasets[0] !== vmobj.zfs_filesystem + '/data') {
9075 9095
9076 9096 cb(new Error('cannot support reprovision with non-standard "'
9077 9097 + vmobj.datasets[0] + '" dataset'));
9078 9098 return;
9079 9099 }
9080 9100 cb();
9081 9101 }, function (cb) {
9082 9102 // TODO: change here when we support zvols/KVM, add size
9083 9103 // & change type
9084 9104
9085 9105 validateImage({
9086 9106 type: 'zone-dataset',
9087 9107 uuid: payload.image_uuid,
9088 9108 zpool: vmobj.zpool
9089 9109 }, log, function (e) {
9090 9110 cb(e);
9091 9111 });
9092 9112 }, function (cb) {
9093 9113 // ensure we're stopped before reprovision starts
9094 9114 if (vmobj.zone_state !== 'installed') {
9095 9115 VM.stop(uuid, {log: log}, function (e) {
9096 9116 if (e) {
9097 9117 log.error(e, 'unable to stop VM ' + uuid + ': '
9098 9118 + e.message);
9099 9119 }
9100 9120 cb(e);
9101 9121 });
9102 9122 } else {
9103 9123 cb();
9104 9124 }
9105 9125 }, function (cb) {
9106 9126 // Set transition to provisioning now, we're going for it.
9107 9127 setTransition(vmobj, 'provisioning', 'running',
9108 9128 (provision_timeout * 1000), log, function (err) {
9109 9129 if (err) {
9110 9130 cb(err);
9111 9131 } else {
9112 9132 set_transition = true;
9113 9133 cb();
9114 9134 }
9115 9135 });
9116 9136 }, function (cb) {
9117 9137 // we validated any delegated dataset above, so we just need to
9118 9138 // remove the 'zoned' flag if we've got one.
9119 9139 if (!vmobj.hasOwnProperty('datasets')
9120 9140 || vmobj.datasets.length === 0) {
9121 9141
9122 9142 cb();
9123 9143 return;
9124 9144 }
9125 9145 zfs(['set', 'zoned=off', vmobj.datasets[0]], log,
9126 9146 function (err, fds) {
9127 9147
9128 9148 if (err) {
9129 9149 log.error({err: err, stdout: fds.stdout,
9130 9150 stderr: fds.stderr}, 'Unable to turn off "zoned" for '
9131 9151 + vmobj.datasets[0]);
9132 9152 }
9133 9153 cb(err);
9134 9154 });
9135 9155 }, function (cb) {
9136 9156 // if we have a delegated dataset, rename zones/<uuid>/data
9137 9157 // -> zones/<uuid>-reprovisioning-data
9138 9158 if (!vmobj.hasOwnProperty('datasets')
9139 9159 || vmobj.datasets.length === 0) {
9140 9160
9141 9161 cb();
9142 9162 return;
9143 9163 }
9144 9164 zfs(['rename', '-f', vmobj.datasets[0], vmobj.zfs_filesystem
9145 9165 + '-reprovisioning-data'], log, function (err, fds) {
9146 9166
9147 9167 if (err) {
9148 9168 log.error({err: err, stdout: fds.stdout,
9149 9169 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9150 9170 + vmobj.datasets[0]);
9151 9171 }
9152 9172 cb(err);
9153 9173 });
9154 9174 }, function (cb) {
9155 9175 // unmount <zonepath>/cores so dataset is not busy
9156 9176 zfs(['umount', vmobj.zonepath + '/cores'], log,
9157 9177 function (err, fds) {
9158 9178
9159 9179 if (err) {
9160 9180 if (trim(fds.stderr).match(/not a mountpoint$/)) {
9161 9181 log.info('ignoring failure to umount cores which '
9162 9182 + 'wasn\'t mounted');
9163 9183 cb();
9164 9184 return;
9165 9185 } else {
9166 9186 log.error({err: err, stdout: fds.stdout,
9167 9187 stderr: fds.stderr}, 'Unable to umount '
9168 9188 + vmobj.zonepath + '/cores');
9169 9189 }
9170 9190 }
9171 9191 cb(err);
9172 9192 });
9173 9193 }, function (cb) {
9174 9194 // rename <zfs_filesystem> dataset out of the way
9175 9195 zfs(['rename', '-f', vmobj.zfs_filesystem, vmobj.zfs_filesystem
9176 9196 + '-reprovisioning-root'], log, function (err, fds) {
9177 9197
9178 9198 if (err) {
9179 9199 log.error({err: err, stdout: fds.stdout,
9180 9200 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9181 9201 + vmobj.zfs_filesystem);
9182 9202 }
9183 9203 cb(err);
9184 9204 });
9185 9205 }, function (cb) {
9186 9206 var snapname = vmobj.zpool + '/' + payload.image_uuid + '@final';
9187 9207
9188 9208 // ensure we've got our snapshot
9189 9209 zfs(['get', '-Ho', 'value', 'type', snapname], log,
9190 9210 function (err, fds) {
9191 9211
9192 9212 if (!err) {
9193 9213 // snapshot already exists, use it
9194 9214 log.debug('snapshot "' + snapname + '" exists');
9195 9215 snapshot = snapname;
9196 9216 cb();
9197 9217 return;
9198 9218 }
9199 9219
9200 9220 if (fds.stderr.match(/dataset does not exist/)) {
9201 9221 // we'll use a different one. (falls throught to next func)
9202 9222 cb();
9203 9223 } else {
9204 9224 cb(err);
9205 9225 }
9206 9226 });
9207 9227 }, function (cb) {
9208 9228 var snapname;
9209 9229
9210 9230 if (snapshot) {
9211 9231 // already know which one to use, don't create one
9212 9232 cb();
9213 9233 return;
9214 9234 }
9215 9235
9216 9236 snapname = vmobj.zpool + '/' + payload.image_uuid
9217 9237 + '@' + vmobj.uuid;
9218 9238
9219 9239 // ensure we've got a snapshot
9220 9240 zfs(['get', '-Ho', 'value', 'type', snapname], log,
9221 9241 function (err, fds) {
9222 9242
9223 9243 if (!err) {
9224 9244 // snapshot already exists, use it
9225 9245 log.debug('snapshot "' + snapname + '" exists');
9226 9246 snapshot = snapname;
9227 9247 cb();
9228 9248 return;
9229 9249 }
9230 9250
9231 9251 if (fds.stderr.match(/dataset does not exist/)) {
9232 9252 zfs(['snapshot', snapname], log, function (e, snap_fds) {
9233 9253 if (e) {
9234 9254 e.stdout = snap_fds.stdout;
9235 9255 e.stderr = snap_fds.stderr;
9236 9256 log.error(e, 'Failed to create snapshot: '
9237 9257 + e.message);
9238 9258 } else {
9239 9259 log.debug('created snapshot "' + snapname + '"');
9240 9260 snapshot = snapname;
9241 9261 }
9242 9262 cb(e);
9243 9263 });
9244 9264 } else {
9245 9265 cb(err);
9246 9266 return;
9247 9267 }
9248 9268 });
9249 9269 }, function (cb) {
9250 9270 var args;
9251 9271
9252 9272 // clone the new image creating a new dataset for zoneroot
9253 9273 assert(snapshot);
9254 9274
9255 9275 args = ['clone'];
9256 9276 if (vmobj.hasOwnProperty('quota') && vmobj.quota > 0) {
9257 9277 args.push('-o');
9258 9278 args.push('quota=' + vmobj.quota + 'G');
9259 9279 }
9260 9280 args.push(snapshot);
9261 9281 args.push(vmobj.zfs_filesystem);
9262 9282
9263 9283 zfs(args, log, function (err, fds) {
9264 9284 if (err) {
9265 9285 log.error({err: err, stdout: fds.stdout,
9266 9286 stderr: fds.stderr}, 'Unable to create new clone of '
9267 9287 + payload.image_uuid);
9268 9288 }
9269 9289 cb(err);
9270 9290 });
9271 9291 }, function (cb) {
9272 9292 var cmd;
9273 9293
9274 9294 // copy zones/<uuid>-reprovisioning-root/config to
9275 9295 // zones/<uuid>/config so we keep metadata and ipf rules.
9276 9296 try {
9277 9297 fs.mkdirSync(vmobj.zonepath + '/config');
9278 9298 } catch (e) {
9279 9299 if (e.code !== 'EEXIST') {
9280 9300 e.message = 'Unable to recreate ' + vmobj.zonepath
9281 9301 + '/config: ' + e.message;
9282 9302 cb(e);
9283 9303 return;
9284 9304 }
9285 9305 }
9286 9306
9287 9307 cmd = 'cp -pPR '
9288 9308 + vmobj.zonepath + '-reprovisioning-root/config/* '
9289 9309 + vmobj.zonepath + '/config/';
9290 9310
9291 9311 log.debug(cmd);
9292 9312 exec(cmd, function (error, stdout, stderr) {
9293 9313 log.debug({'stdout': stdout, 'stderr': stderr}, 'cp results');
9294 9314 if (error) {
9295 9315 error.stdout = stdout;
9296 9316 error.stderr = stderr;
9297 9317 cb(error);
9298 9318 return;
9299 9319 } else {
9300 9320 cb();
9301 9321 }
9302 9322 });
9303 9323 }, function (cb) {
9304 9324 // destroy <zonepath>-reprovisioning-root, since it's no longer used
9305 9325 zfs(['destroy', '-r', vmobj.zfs_filesystem
9306 9326 + '-reprovisioning-root'], log, function (err, fds) {
9307 9327
9308 9328 if (err) {
9309 9329 log.error({err: err, stdout: fds.stdout,
9310 9330 stderr: fds.stderr}, 'Unable to destroy '
9311 9331 + vmobj.zfs_filesystem + '-reprovisioning-root: '
9312 9332 + err.message);
9313 9333 }
9314 9334 cb(err);
9315 9335 });
9316 9336 }, function (cb) {
9317 9337 // remount /zones/<uuid>/cores
9318 9338 zfs(['mount', vmobj.zpool + '/cores/' + uuid], log,
9319 9339 function (err, fds) {
9320 9340
9321 9341 if (err) {
9322 9342 log.error({err: err, stdout: fds.stdout,
9323 9343 stderr: fds.stderr}, 'Unable to mount ' + vmobj.zonepath
9324 9344 + '/cores: ' + err.message);
9325 9345 }
9326 9346 cb(err);
9327 9347 });
9328 9348 }, function (cb) {
9329 9349 var args = ['-r', '-R', vmobj.zonepath, '-z', vmobj.zonename];
9330 9350 var cmd = BRAND_OPTIONS[vmobj.brand].features.brand_install_script;
9331 9351
9332 9352 // We run the brand's install script here with the -r flag which
9333 9353 // tells it to do everything that's relevant to reprovision.
9334 9354
9335 9355 log.debug(cmd + ' ' + args.join(' '));
9336 9356 execFile(cmd, args, function (error, stdout, stderr) {
9337 9357 var new_err;
9338 9358
9339 9359 if (error) {
9340 9360 new_err = new Error('Error running brand install script '
9341 9361 + cmd);
9342 9362 // error's message includes stderr.
9343 9363 log.error({err: error, stdout: stdout},
9344 9364 'brand install script exited with code ' + error.code);
9345 9365 cb(new_err);
9346 9366 } else {
9347 9367 log.debug(cmd + ' stderr:\n' + stderr);
9348 9368 cb();
9349 9369 }
9350 9370 });
9351 9371 }, function (cb) {
9352 9372 // rename zones/<uuid>-reprovision-data -> zones/<uuid>/data
9353 9373 if (!vmobj.hasOwnProperty('datasets')
9354 9374 || vmobj.datasets.length === 0) {
9355 9375
9356 9376 cb();
9357 9377 return;
9358 9378 }
9359 9379 zfs(['rename', '-f', vmobj.zfs_filesystem + '-reprovisioning-data',
9360 9380 vmobj.datasets[0]], log, function (err, fds) {
9361 9381
9362 9382 if (err) {
9363 9383 log.error({err: err, stdout: fds.stdout,
9364 9384 stderr: fds.stderr}, 'Unable to (temporarily) rename '
9365 9385 + vmobj.zfs_filesystem);
9366 9386 }
9367 9387 cb(err);
9368 9388 });
9369 9389 }, function (cb) {
9370 9390 // set zoned=on for zones/<uuid>/data
9371 9391 if (!vmobj.hasOwnProperty('datasets')
9372 9392 || vmobj.datasets.length === 0) {
9373 9393
9374 9394 cb();
9375 9395 return;
9376 9396 }
9377 9397 zfs(['set', 'zoned=on', vmobj.datasets[0]], log,
9378 9398 function (err, fds) {
9379 9399
9380 9400 if (err) {
9381 9401 log.error({err: err, stdout: fds.stdout,
9382 9402 stderr: fds.stderr}, 'Unable to set "zoned" for: '
9383 9403 + vmobj.datasets[0]);
9384 9404 }
9385 9405 cb(err);
9386 9406 });
9387 9407 }, function (cb) {
9388 9408 // update zone's image_uuid field
9389 9409 var zcfg = 'select attr name=dataset-uuid; set value="'
9390 9410 + payload.image_uuid + '"; end';
9391 9411 zonecfg(['-u', uuid, zcfg], log, function (err, fds) {
9392 9412 if (err) {
9393 9413 log.error({err: err, stdout: fds.stdout,
9394 9414 stderr: fds.stderr}, 'unable to set image_uuid on VM '
9395 9415 + uuid);
9396 9416 }
9397 9417 cb(err);
9398 9418 });
9399 9419 }, function (cb) {
9400 9420 var p = {
9401 9421 autoboot: true,
9402 9422 reprovisioning: true,
9403 9423 uuid: uuid,
9404 9424 zonename: vmobj.zonename,
9405 9425 zonepath: vmobj.zonepath
9406 9426 };
9407 9427
9408 9428 // NOTE: someday we could allow mdata_exec_timeout in the original
9409 9429 // payload to reprovision and then pass it along here.
9410 9430
9411 9431 // other fields used by installZone()
9412 9432 [
9413 9433 'dns_domain',
9414 9434 'hostname',
9415 9435 'quota',
9416 9436 'resolvers',
9417 9437 'tmpfs',
9418 9438 'zfs_filesystem',
9419 9439 'zfs_root_compression',
9420 9440 'zfs_root_recsize'
9421 9441 ].forEach(function (k) {
9422 9442 if (vmobj.hasOwnProperty(k)) {
9423 9443 p[k] = vmobj[k];
9424 9444 }
9425 9445 });
9426 9446
9427 9447 // nics needs to be called add_nics here
9428 9448 if (vmobj.hasOwnProperty('nics')) {
9429 9449 p.add_nics = vmobj.nics;
9430 9450 }
9431 9451
9432 9452 installZone(p, log, function (err) {
9433 9453 log.debug(err, 'ran installZone() for reprovision');
9434 9454 cb(err);
9435 9455 });
9436 9456 }
9437 9457 ], function (err) {
9438 9458 if (err && set_transition) {
9439 9459 // remove transition now, if we failed.
9440 9460 VM.unsetTransition(vmobj, {log: log}, function () {
9441 9461 // err here is original err, we ignore failure to unset because
9442 9462 // nothing we can do about that..
9443 9463 callback(err);
9444 9464 });
9445 9465 } else {
9446 9466 callback(err);
9447 9467 }
9448 9468 });
9449 9469 };
9450 9470
9451 9471 exports.install = function (uuid, options, callback)
9452 9472 {
9453 9473 var log;
9454 9474
9455 9475 // options is optional
9456 9476 if (arguments.length === 2) {
9457 9477 callback = arguments[1];
9458 9478 options = {};
9459 9479 }
9460 9480
9461 9481 ensureLogging(true);
9462 9482 if (options.hasOwnProperty('log')) {
9463 9483 log = options.log;
9464 9484 } else {
9465 9485 log = VM.log.child({action: 'install', vm: uuid});
9466 9486 }
9467 9487
9468 9488 log.info('Installing VM ' + uuid);
9469 9489
9470 9490 fs.readFile('/etc/zones/' + uuid + '-receiving.json',
9471 9491 function (err, data) {
9472 9492 var payload;
9473 9493
9474 9494 if (err) {
9475 9495 callback(err);
9476 9496 return;
9477 9497 }
9478 9498
9479 9499 try {
9480 9500 payload = JSON.parse(data.toString());
9481 9501 } catch (e) {
9482 9502 callback(e);
9483 9503 return;
9484 9504 }
9485 9505
9486 9506 // installZone takes a payload
9487 9507 installZone(payload, log, callback);
9488 9508 }
9489 9509 );
9490 9510
9491 9511 };
9492 9512
9493 9513 function getAllDatasets(vmobj)
9494 9514 {
9495 9515 var datasets = [];
9496 9516 var disk;
9497 9517
9498 9518 if (vmobj.hasOwnProperty('zfs_filesystem')) {
9499 9519 datasets.push(vmobj.zfs_filesystem);
9500 9520 }
9501 9521
9502 9522 for (disk in vmobj.disks) {
9503 9523 disk = vmobj.disks[disk];
9504 9524 if (disk.hasOwnProperty('zfs_filesystem')) {
9505 9525 datasets.push(disk.zfs_filesystem);
9506 9526 }
9507 9527 }
9508 9528
9509 9529 return datasets;
9510 9530 }
9511 9531
9512 9532 //
9513 9533 // Headers are 512 bytes and look like:
9514 9534 //
9515 9535 // MAGIC-VMBUNDLE\0
9516 9536 // <VERSION>\0 -- ASCII #s
9517 9537 // <CHECKSUM>\0 -- ASCII (not yet used)
9518 9538 // <OBJ-NAME>\0 -- max length: 256
9519 9539 // <OBJ-SIZE>\0 -- ASCII # of bytes
9520 9540 // <PADDED-SIZE>\0 -- ASCII # of bytes, must be multiple of 512
9521 9541 // ...\0
9522 9542 //
9523 9543 function chunkHeader(name, size, padding)
9524 9544 {
9525 9545 var header = new Buffer(512);
9526 9546 var pos = 0;
9527 9547
9528 9548 header.fill(0);
9529 9549 pos += addString(header, 'MAGIC-VMBUNDLE', pos);
9530 9550 pos += addString(header, sprintf('%d', 1), pos);
9531 9551 pos += addString(header, 'CHECKSUM', pos);
9532 9552 pos += addString(header, name, pos);
9533 9553 pos += addString(header, sprintf('%d', size), pos);
9534 9554 pos += addString(header, sprintf('%d', size + padding), pos);
9535 9555
9536 9556 return (header);
9537 9557 }
9538 9558
9539 9559 // add the string to buffer at pos, returning pos of new end of the buffer.
9540 9560 function addString(buf, str, pos)
9541 9561 {
9542 9562 var len = str.length;
9543 9563 buf.write(str, pos);
9544 9564 return (len + 1);
9545 9565 }
9546 9566
9547 9567 function sendJSON(target, json, log, cb)
9548 9568 {
9549 9569 var header;
9550 9570 var pad;
9551 9571 var padding = 0;
9552 9572
9553 9573 assert(log, 'no logger passed for sendJSON()');
9554 9574
9555 9575 if (target === 'stdout') {
9556 9576 if ((json.length % 512) != 0) {
9557 9577 padding = 512 - (json.length % 512);
9558 9578 }
9559 9579 header = chunkHeader('JSON', json.length, padding);
9560 9580 process.stdout.write(header);
9561 9581 process.stdout.write(json, 'ascii');
9562 9582 if (padding > 0) {
9563 9583 pad = new Buffer(padding);
9564 9584 pad.fill(0);
9565 9585 process.stdout.write(pad);
9566 9586 }
9567 9587 cb();
9568 9588 } else {
9569 9589 log.error('Don\'t know how to send JSON to '
9570 9590 + JSON.stringify(target));
9571 9591 cb(new Error('Don\'t know how to send JSON to '
9572 9592 + JSON.stringify(target)));
9573 9593 }
9574 9594 }
9575 9595
9576 9596 function sendDataset(target, dataset, log, callback)
9577 9597 {
9578 9598 var header;
9579 9599
9580 9600 assert(log, 'no logger passed for sendDataset()');
9581 9601
9582 9602 if (target === 'stdout') {
9583 9603
9584 9604 async.series([
9585 9605 function (cb) {
9586 9606 // delete any existing 'sending' snapshot
9587 9607 zfs(['destroy', '-F', dataset + '@sending'], log,
9588 9608 function (err, fds) {
9589 9609 // We don't expect this to succeed, since that means
9590 9610 // something left an @sending around. Warn if succeeds.
9591 9611 if (!err) {
9592 9612 log.warn('Destroyed pre-existing ' + dataset
9593 9613 + '@sending');
9594 9614 }
9595 9615 cb();
9596 9616 }
9597 9617 );
9598 9618 }, function (cb) {
9599 9619 zfs(['snapshot', dataset + '@sending'], log,
9600 9620 function (err, fds) {
9601 9621
9602 9622 cb(err);
9603 9623 });
9604 9624 }, function (cb) {
9605 9625 header = chunkHeader(dataset, 0, 0);
9606 9626 process.stdout.write(header);
9607 9627 cb();
9608 9628 }, function (cb) {
9609 9629 var child;
9610 9630
9611 9631 child = spawn('/usr/sbin/zfs',
9612 9632 ['send', '-p', dataset + '@sending'],
9613 9633 {customFds: [-1, 1, -1]});
9614 9634 child.stderr.on('data', function (data) {
9615 9635 var idx;
9616 9636 var lines = trim(data.toString()).split('\n');
9617 9637
9618 9638 for (idx in lines) {
9619 9639 log.debug('zfs send: ' + trim(lines[idx]));
9620 9640 }
9621 9641 });
9622 9642 child.on('close', function (code) {
9623 9643 log.debug('zfs send process exited with code '
9624 9644 + code);
9625 9645 cb();
9626 9646 });
9627 9647 }, function (cb) {
9628 9648 zfs(['destroy', '-F', dataset + '@sending'], log,
9629 9649 function (err, fds) {
9630 9650 if (err) {
9631 9651 log.warn(err, 'Unable to destroy ' + dataset
9632 9652 + '@sending: ' + err.message);
9633 9653 }
9634 9654 cb(err);
9635 9655 }
9636 9656 );
9637 9657 }
9638 9658 ], function (err) {
9639 9659 if (err) {
9640 9660 log.error(err, 'Failed to send dataset: ' + err.message);
9641 9661 } else {
9642 9662 log.info('Successfully sent dataset');
9643 9663 }
9644 9664 callback(err);
9645 9665 });
9646 9666 } else {
9647 9667 log.error('Don\'t know how to send datasets to '
9648 9668 + JSON.stringify(target));
9649 9669 callback(new Error('Don\'t know how to send datasets to '
9650 9670 + JSON.stringify(target)));
9651 9671 }
9652 9672 }
9653 9673
9654 9674 exports.send = function (uuid, target, options, callback)
9655 9675 {
9656 9676 var datasets;
9657 9677 var log;
9658 9678 var vmobj;
9659 9679
9660 9680 // options is optional
9661 9681 if (arguments.length === 3) {
9662 9682 callback = arguments[2];
9663 9683 options = {};
9664 9684 }
9665 9685
9666 9686 ensureLogging(true);
9667 9687 if (options.hasOwnProperty('log')) {
9668 9688 log = options.log;
9669 9689 } else {
9670 9690 log = VM.log.child({action: 'send', vm: uuid});
9671 9691 }
9672 9692
9673 9693 target = 'stdout';
9674 9694
9675 9695 log.info('Sending VM ' + uuid + ' to: ' + JSON.stringify(target));
9676 9696 async.series([
9677 9697 function (cb) {
9678 9698 // make sure we *can* send first, to avoid wasting cycles
9679 9699 if (target === 'stdout' && tty.isatty(1)) {
9680 9700 log.error('Cannot send VM to a TTY.');
9681 9701 cb(new Error('Cannot send VM to a TTY.'));
9682 9702 } else {
9683 9703 cb();
9684 9704 }
9685 9705 }, function (cb) {
9686 9706 // NOTE: for this load we always load all fields, because we need
9687 9707 // to send them all to the target machine.
9688 9708 VM.load(uuid, {log: log}, function (err, obj) {
9689 9709 if (err) {
9690 9710 cb(err);
9691 9711 } else {
9692 9712 vmobj = obj;
9693 9713 cb();
9694 9714 }
9695 9715 });
9696 9716 }, function (cb) {
9697 9717 datasets = getAllDatasets(vmobj);
9698 9718 if (datasets.length < 1) {
9699 9719 log.error('Cannot send VM with no datasets.');
9700 9720 cb(new Error('VM has no datasets.'));
9701 9721 } else {
9702 9722 cb();
9703 9723 }
9704 9724 }, function (cb) {
9705 9725 if (vmobj.state !== 'stopped') {
9706 9726 // In this case we need to stop it and make sure it stopped.
9707 9727 VM.stop(uuid, {log: log}, function (e) {
9708 9728 if (e) {
9709 9729 log.error(e, 'unable to stop VM ' + uuid + ': '
9710 9730 + e.message);
9711 9731 cb(e);
9712 9732 return;
9713 9733 }
9714 9734 VM.load(uuid, {fields: ['zone_state', 'uuid'], log: log},
9715 9735 function (error, obj) {
9716 9736
9717 9737 if (error) {
9718 9738 log.error(error, 'unable to reload VM ' + uuid
9719 9739 + ': ' + error.message);
9720 9740 return;
9721 9741 }
9722 9742 if (obj.zone_state !== 'installed') {
9723 9743 log.error('after stop attempt, state is '
9724 9744 + obj.zone_state + ' != installed');
9725 9745 cb(new Error('state after stopping is '
9726 9746 + obj.zone_state + ' != installed'));
9727 9747 return;
9728 9748 }
9729 9749 cb();
9730 9750 });
9731 9751 });
9732 9752 } else {
9733 9753 // already stopped, good to go!
9734 9754 cb();
9735 9755 }
9736 9756 }, function (cb) {
9737 9757 // Clean up trash left from broken datasets (see OS-388)
9738 9758 try {
9739 9759 fs.unlinkSync(vmobj.zonepath + '/SUNWattached.xml');
9740 9760 } catch (err) {
9741 9761 // DO NOTHING, this file shouldn't have existed anyway.
9742 9762 }
9743 9763 try {
9744 9764 fs.unlinkSync(vmobj.zonepath + '/SUNWdetached.xml');
9745 9765 } catch (err) {
9746 9766 // DO NOTHING, this file shouldn't have existed anyway.
9747 9767 }
9748 9768 cb();
9749 9769 }, function (cb) {
9750 9770 // send JSON
9751 9771 var json = JSON.stringify(vmobj, null, 2) + '\n';
9752 9772 sendJSON(target, json, log, cb);
9753 9773 }, function (cb) {
9754 9774 // send datasets
9755 9775 async.forEachSeries(datasets, function (ds, c) {
9756 9776 sendDataset(target, ds, log, c);
9757 9777 }, function (e) {
9758 9778 if (e) {
9759 9779 log.error('Failed to send datasets');
9760 9780 }
9761 9781 cb(e);
9762 9782 });
9763 9783 }
9764 9784 ], function (err) {
9765 9785 callback(err);
9766 9786 });
9767 9787 };
9768 9788
9769 9789 exports.create = function (payload, options, callback)
9770 9790 {
9771 9791 var log;
9772 9792
9773 9793 // options is optional
9774 9794 if (arguments.length === 2) {
9775 9795 callback = arguments[1];
9776 9796 options = {};
9777 9797 }
9778 9798
9779 9799 ensureLogging(true);
9780 9800
9781 9801 if (options.hasOwnProperty('log')) {
9782 9802 log = options.log;
9783 9803 } else {
9784 9804 // default to VM.log until we have a uuid, then we'll switch.
9785 9805 log = VM.log;
9786 9806 }
9787 9807
9788 9808 log.info('Creating VM, original payload:\n'
9789 9809 + JSON.stringify(payload, null, 2));
9790 9810
9791 9811 async.waterfall([
9792 9812 function (cb) {
9793 9813 // We get a UUID first so that we can attach as many log messages
9794 9814 // as possible to this uuid. Since we don't have a UUID here, we
9795 9815 // send VM.log as the logger. We'll switch to a log.child as soon
9796 9816 // as we have uuid.
9797 9817 createZoneUUID(payload, log, function (e, uuid) {
9798 9818 // either payload will have .uuid or we'll return error here.
9799 9819 cb(e);
9800 9820 });
9801 9821 }, function (cb) {
9802 9822 // If we got here, payload now has .uuid and we can start logging
9803 9823 // messages with that uuid if we didn't already have a logger.
9804 9824 if (!options.hasOwnProperty('log')) {
9805 9825 log = VM.log.child({action: 'create', vm: payload.uuid});
9806 9826 }
9807 9827 cb();
9808 9828 }, function (cb) {
9809 9829 normalizePayload(payload, null, log, function (err) {
9810 9830 if (err) {
9811 9831 log.error(err, 'Failed to validate payload: '
9812 9832 + err.message);
9813 9833 } else {
9814 9834 log.debug('normalized payload:\n'
9815 9835 + JSON.stringify(payload, null, 2));
9816 9836 }
9817 9837 cb(err);
9818 9838 });
9819 9839 }, function (cb) {
9820 9840 checkDatasetProvisionable(payload, log, function (provisionable) {
9821 9841 if (!provisionable) {
9822 9842 log.error('checkDatasetProvisionable() says dataset is '
9823 9843 + 'unprovisionable');
9824 9844 cb(new Error('provisioning dataset ' + payload.image_uuid
9825 9845 + ' with brand ' + payload.brand
9826 9846 + ' is not supported'));
9827 9847 return;
9828 9848 }
9829 9849 cb();
9830 9850 });
9831 9851 }, function (cb) {
9832 9852 if (BRAND_OPTIONS[payload.brand].features.type === 'KVM') {
9833 9853 createVM(payload, log, function (error, result) {
9834 9854 if (error) {
9835 9855 cb(error);
9836 9856 } else {
9837 9857 cb(null, {'uuid': payload.uuid,
9838 9858 'zonename': payload.zonename});
9839 9859 }
9840 9860 });
9841 9861 } else {
9842 9862 createZone(payload, log, function (error, result) {
9843 9863 if (error) {
9844 9864 cb(error);
9845 9865 } else {
9846 9866 cb(null, {'uuid': payload.uuid,
9847 9867 'zonename': payload.zonename});
9848 9868 }
9849 9869 });
9850 9870 }
9851 9871 }
9852 9872 ], function (err, obj) {
9853 9873 callback(err, obj);
9854 9874 });
9855 9875 };
9856 9876
9857 9877 // delete a zvol
9858 9878 function deleteVolume(volume, log, callback)
9859 9879 {
9860 9880 var args;
9861 9881 var origin;
9862 9882
9863 9883 assert(log, 'no logger passed to deleteVolume()');
9864 9884
9865 9885 if (volume.missing) {
9866 9886 // this volume doesn't actually exist, so skip trying to delete.
9867 9887 log.info('volume ' + volume.path + ' doesn\'t exist, skipping '
9868 9888 + 'deletion');
9869 9889 callback();
9870 9890 return;
9871 9891 }
9872 9892
9873 9893 async.series([
9874 9894 function (cb) {
9875 9895 args = ['get', '-Ho', 'value', 'origin', volume.zfs_filesystem];
9876 9896 zfs(args, log, function (err, fds) {
9877 9897 if (err && fds.stderr.match('dataset does not exist')) {
9878 9898 log.info('volume ' + volume.path + ' doesn\'t exist, '
9879 9899 + 'skipping deletion');
9880 9900 cb();
9881 9901 } else {
9882 9902 origin = trim(fds.stdout);
9883 9903 log.info('found origin "' + origin + '"');
9884 9904 cb(err);
9885 9905 }
9886 9906 });
9887 9907 }, function (cb) {
9888 9908 // use recursive delete to handle possible snapshots on volume
9889 9909 args = ['destroy', '-rF', volume.zfs_filesystem];
9890 9910 zfs(args, log, function (err, fds) {
9891 9911 // err will be non-null if something broke
9892 9912 cb(err);
9893 9913 });
9894 9914 }, function (cb) {
9895 9915 // we never delete an @final snapshot, that's the one from recv
9896 9916 // that imgadm left around for us on purpose.
9897 9917 if (!origin || origin.length < 1 || origin == '-'
9898 9918 || origin.match('@final')) {
9899 9919
9900 9920 cb();
9901 9921 return;
9902 9922 }
9903 9923 args = ['destroy', '-rF', origin];
9904 9924 zfs(args, log, function (err, fds) {
9905 9925 // err will be non-null if something broke
9906 9926 cb(err);
9907 9927 });
9908 9928 }
9909 9929 ], function (err) {
9910 9930 callback(err);
9911 9931 });
9912 9932 }
9913 9933
9914 9934 function deleteZone(uuid, log, callback)
9915 9935 {
9916 9936 var load_fields;
9917 9937 var vmobj;
9918 9938
9919 9939 assert(log, 'no logger passed to deleteZone()');
9920 9940
9921 9941 load_fields = [
9922 9942 'archive_on_delete',
9923 9943 'disks',
9924 9944 'uuid',
9925 9945 'zonename'
9926 9946 ];
9927 9947
9928 9948 async.series([
9929 9949 function (cb) {
9930 9950 VM.load(uuid, {fields: load_fields, log: log}, function (err, obj) {
9931 9951 if (err) {
9932 9952 cb(err);
9933 9953 return;
9934 9954 }
9935 9955 vmobj = obj;
9936 9956 cb();
9937 9957 });
9938 9958 }, function (cb) {
9939 9959 log.debug('archive_on_delete is set to '
9940 9960 + !!vmobj.archive_on_delete);
9941 9961 if (!vmobj.archive_on_delete) {
9942 9962 cb();
9943 9963 return;
9944 9964 }
9945 9965 archiveVM(vmobj.uuid, log, function () {
9946 9966 cb();
9947 9967 });
9948 9968 // TODO: replace these next two with VM.stop(..{force: true} ?
9949 9969 }, function (cb) {
9950 9970 log.debug('setting autoboot=false');
9951 9971 zonecfg(['-u', uuid, 'set autoboot=false'], log, function (e, fds) {
9952 9972 if (e) {
9953 9973 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9954 9974 'Error setting autoboot=false');
9955 9975 } else {
9956 9976 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9957 9977 'set autoboot=false');
9958 9978 }
9959 9979 cb();
9960 9980 });
9961 9981 }, function (cb) {
9962 9982 log.debug('halting zone');
9963 9983 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) {
9964 9984 if (e) {
9965 9985 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9966 9986 'Error halting zone');
9967 9987 } else {
9968 9988 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9969 9989 'halted zone');
9970 9990 }
9971 9991 cb();
9972 9992 });
9973 9993 }, function (cb) {
9974 9994 log.debug('uninstalling zone');
9975 9995 zoneadm(['-u', uuid, 'uninstall', '-F'], log, function (e, fds) {
9976 9996 if (e) {
9977 9997 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
9978 9998 'Error uninstalling zone: ' + e.message);
9979 9999 } else {
9980 10000 log.debug({stdout: fds.stdout, stderr: fds.stderr},
9981 10001 'uninstalled zone');
9982 10002 }
9983 10003 cb();
9984 10004 });
9985 10005 }, function (cb) {
9986 10006 function loggedDeleteVolume(volume, callbk) {
9987 10007 return deleteVolume(volume, log, callbk);
9988 10008 }
9989 10009
9990 10010 if (vmobj && vmobj.hasOwnProperty('disks')) {
9991 10011 async.forEachSeries(vmobj.disks, loggedDeleteVolume,
9992 10012 function (err) {
9993 10013 if (err) {
9994 10014 log.error(err, 'Unknown error deleting volumes: '
9995 10015 + err.message);
9996 10016 cb(err);
9997 10017 } else {
9998 10018 log.info('successfully deleted volumes');
9999 10019 cb();
10000 10020 }
10001 10021 }
10002 10022 );
10003 10023 } else {
10004 10024 log.debug('skipping volume destruction for diskless '
10005 10025 + vmobj.uuid);
10006 10026 cb();
10007 10027 }
10008 10028 }, function (cb) {
10009 10029 if (vmobj.zonename) {
10010 10030 log.debug('deleting zone');
10011 10031 // XXX for some reason -u <uuid> doesn't work with delete
10012 10032 zonecfg(['-z', vmobj.zonename, 'delete', '-F'], log,
10013 10033 function (e, fds) {
10014 10034
10015 10035 if (e) {
10016 10036 log.warn({err: e, stdout: fds.stdout,
10017 10037 stderr: fds.stderr}, 'Error deleting VM');
10018 10038 } else {
10019 10039 log.debug({stdout: fds.stdout, stderr: fds.stderr},
10020 10040 'deleted VM ' + uuid);
10021 10041 }
10022 10042 cb();
10023 10043 });
10024 10044 } else {
10025 10045 cb();
10026 10046 }
10027 10047 }, function (cb) {
10028 10048 VM.load(uuid, {fields: ['uuid'], log: log, missing_ok: true},
10029 10049 function (err, obj) {
10030 10050
10031 10051 var gone = /^zoneadm:.*: No such zone configured/;
10032 10052 if (err && err.message.match(gone)) {
10033 10053 // the zone is gone, that's good.
10034 10054 log.debug('confirmed VM is gone.');
10035 10055 cb();
10036 10056 } else if (err) {
10037 10057 // there was a non-expected error.
10038 10058 cb(err);
10039 10059 } else {
10040 10060 // the VM still exists!
10041 10061 err = new Error('VM still exists after delete.');
10042 10062 err.code = 'EEXIST';
10043 10063 cb(err);
10044 10064 }
10045 10065 });
10046 10066 }, function (cb) {
10047 10067 // delete the incoming payload if it exists
10048 10068 fs.unlink('/etc/zones/' + vmobj.uuid + '-receiving.json',
10049 10069 function (e) {
10050 10070 // we can't do anyhing if this fails other than log
10051 10071 if (e && e.code !== 'ENOENT') {
10052 10072 log.warn(e, 'Failed to delete ' + vmobj.uuid
10053 10073 + '-receiving.json (' + e.code + '): ' + e.message);
10054 10074 }
10055 10075 cb();
10056 10076 }
10057 10077 );
10058 10078 }
10059 10079 ], function (error) {
10060 10080 callback(error);
10061 10081 });
10062 10082 }
10063 10083
10064 10084 exports.delete = function (uuid, options, callback)
10065 10085 {
10066 10086 var attemptDelete;
10067 10087 var last_try = 16;
10068 10088 var log;
10069 10089 var next_try = 1;
10070 10090 var tries = 0;
10071 10091
10072 10092 // options is optional
10073 10093 if (arguments.length === 2) {
10074 10094 callback = arguments[1];
10075 10095 options = {};
10076 10096 }
10077 10097
10078 10098 ensureLogging(true);
10079 10099
10080 10100 if (options.hasOwnProperty('log')) {
10081 10101 log = options.log;
10082 10102 } else {
10083 10103 log = VM.log.child({action: 'delete', vm: uuid});
10084 10104 }
10085 10105
10086 10106 log.info('Deleting VM ' + uuid);
10087 10107
10088 10108 attemptDelete = function (cb) {
10089 10109 next_try = (next_try * 2);
10090 10110 deleteZone(uuid, log, function (err) {
10091 10111 tries++;
10092 10112 if (err && err.code === 'EEXIST') {
10093 10113 // zone still existed, try again if we've not tried too much.
10094 10114 if (next_try <= last_try) {
10095 10115 log.info('VM.delete(' + tries + '): still there, '
10096 10116 + 'will try again in: ' + next_try + ' secs');
10097 10117 setTimeout(function () {
10098 10118 // try again
10099 10119 attemptDelete(cb);
10100 10120 }, next_try * 1000);
10101 10121 } else {
10102 10122 log.warn('VM.delete(' + tries + '): still there after'
10103 10123 + ' ' + next_try + ' seconds, giving up.');
10104 10124 cb(new Error('delete failed after ' + tries + ' attempts. '
10105 10125 + '(check the log for details)'));
10106 10126 return;
10107 10127 }
10108 10128 } else if (err) {
10109 10129 // error but not one we can retry from.
10110 10130 log.error(err, 'VM.delete: FATAL: ' + err.message);
10111 10131 cb(err);
10112 10132 } else {
10113 10133 // success!
10114 10134 log.debug('VM.delete: SUCCESS');
10115 10135 cb();
10116 10136 }
10117 10137 });
10118 10138 };
10119 10139
10120 10140 attemptDelete(function (err) {
10121 10141 if (err) {
10122 10142 log.error(err);
10123 10143 }
10124 10144 callback(err);
10125 10145 });
10126 10146 };
10127 10147
10128 10148 // This function needs vmobj to have:
10129 10149 //
10130 10150 // brand
10131 10151 // never_booted
10132 10152 // uuid
10133 10153 // zonename
10134 10154 //
10135 10155 function startZone(vmobj, log, callback)
10136 10156 {
10137 10157 var set_autoboot = 'set autoboot=true';
10138 10158 var uuid = vmobj.uuid;
10139 10159
10140 10160 assert(log, 'no logger passed to startZone()');
10141 10161
10142 10162 log.debug('startZone starting ' + uuid);
10143 10163
10144 10164 //
10145 10165 // We set autoboot (or vm-autoboot) here because we've just intentionally
10146 10166 // started this vm, so we want it to come up if the host is rebooted.
10147 10167 //
10148 10168 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
10149 10169 set_autoboot = 'select attr name=vm-autoboot; set value=true; end';
10150 10170 }
10151 10171
10152 10172 async.series([
10153 10173 function (cb) {
10154 10174 // do the booting
10155 10175 zoneadm(['-u', uuid, 'boot', '-X'], log, function (err, boot_fds) {
10156 10176 if (err) {
10157 10177 log.error({err: err, stdout: boot_fds.stdout,
10158 10178 stderr: boot_fds.stderr}, 'zoneadm failed to boot '
10159 10179 + 'VM');
10160 10180 } else {
10161 10181 log.debug({stdout: boot_fds.stdout,
10162 10182 stderr: boot_fds.stderr}, 'zoneadm booted VM');
10163 10183 }
10164 10184 cb(err);
10165 10185 });
10166 10186 }, function (cb) {
10167 10187 // ensure it booted
10168 10188 VM.waitForZoneState(vmobj, 'running', {timeout: 30, log: log},
10169 10189 function (err, result) {
10170 10190
10171 10191 if (err) {
10172 10192 if (err.code === 'ETIMEOUT') {
10173 10193 log.info(err, 'timeout waiting for zone to go to '
10174 10194 + '"running"');
10175 10195 } else {
10176 10196 log.error(err, 'unknown error waiting for zone to go'
10177 10197 + ' "running"');
10178 10198 }
10179 10199 } else {
10180 10200 // zone got to running
10181 10201 log.info('VM seems to have switched to "running"');
10182 10202 }
10183 10203 cb(err);
10184 10204 });
10185 10205 }, function (cb) {
10186 10206 zonecfg(['-u', uuid, set_autoboot], log,
10187 10207 function (err, autoboot_fds) {
10188 10208
10189 10209 if (err) {
10190 10210 // The vm is running at this point, erroring out here would
10191 10211 // do no good, so we just log it.
10192 10212 log.error({err: err, stdout: autoboot_fds.stdout,
10193 10213 stderr: autoboot_fds.stderr}, 'startZone(): Failed to '
10194 10214 + set_autoboot + ' for ' + uuid);
10195 10215 } else {
10196 10216 log.debug({stdout: autoboot_fds.stdout,
10197 10217 stderr: autoboot_fds.stderr}, 'set autoboot');
10198 10218 }
10199 10219 cb(err);
10200 10220 });
10201 10221 }, function (cb) {
10202 10222 if (!vmobj.never_booted) {
10203 10223 cb();
10204 10224 return;
10205 10225 }
10206 10226 zonecfg(['-u', uuid, 'remove attr name=never-booted' ], log,
10207 10227 function (err, neverbooted_fds) {
10208 10228 // Ignore errors here, because we're started.
10209 10229 if (err) {
10210 10230 log.warn({err: err, stdout: neverbooted_fds.stdout,
10211 10231 stderr: neverbooted_fds.stderr}, 'failed to remove '
10212 10232 + 'never-booted flag');
10213 10233 } else {
10214 10234 log.debug({stdout: neverbooted_fds.stdout,
10215 10235 stderr: neverbooted_fds.stderr}, 'removed '
10216 10236 + 'never-booted flag');
10217 10237 }
10218 10238 cb();
10219 10239 }
10220 10240 );
10221 10241 }
10222 10242 ], function (err) {
10223 10243 if (!err) {
10224 10244 log.info('Started ' + uuid);
10225 10245 }
10226 10246 callback(err);
10227 10247 });
10228 10248 }
10229 10249
10230 10250 // build the qemu cmdline and start up a VM
10231 10251 //
10232 10252 // vmobj needs any of the following that are defined:
10233 10253 //
10234 10254 // boot
10235 10255 // brand
10236 10256 // cpu_type
10237 10257 // default_gateway
10238 10258 // disks
10239 10259 // hostname
10240 10260 // internal_metadata
10241 10261 // never_booted
10242 10262 // nics
10243 10263 // qemu_extra_opts
10244 10264 // qemu_opts
10245 10265 // ram
10246 10266 // resolvers
10247 10267 // spice_opts
10248 10268 // spice_password
10249 10269 // spice_port
10250 10270 // state
10251 10271 // uuid
10252 10272 // vcpus
10253 10273 // vga
10254 10274 // virtio_txtimer
10255 10275 // virtio_txburst
10256 10276 // vnc_password
10257 10277 // zone_state
10258 10278 // zonename
10259 10279 // zonepath
10260 10280 //
10261 10281 function startVM(vmobj, extra, log, callback)
10262 10282 {
10263 10283 var check_path;
10264 10284 var cmdargs = [];
10265 10285 var d;
10266 10286 var defaultgw = '';
10267 10287 var disk;
10268 10288 var diskargs = '';
10269 10289 var disk_idx = 0;
10270 10290 var found;
10271 10291 var hostname = vmobj.uuid;
10272 10292 var mdata;
10273 10293 var nic;
10274 10294 var nic_idx = 0;
10275 10295 var primary_found = false;
10276 10296 var qemu_opts = '';
10277 10297 var r;
10278 10298 var script;
10279 10299 var spiceargs;
10280 10300 var uuid = vmobj.uuid;
10281 10301 var virtio_txburst;
10282 10302 var virtio_txtimer;
10283 10303 var vnic_opts;
10284 10304 var zoneroot;
10285 10305
10286 10306 assert(log, 'no logger passed to startVM');
10287 10307 assert(vmobj.hasOwnProperty('zonepath'), 'missing zonepath');
10288 10308
10289 10309 log.debug('startVM(' + uuid + ')');
10290 10310
10291 10311 if (!vmobj.hasOwnProperty('state')) {
10292 10312 callback(new Error('Cannot start VM ' + uuid + ' which has no state'));
10293 10313 return;
10294 10314 }
10295 10315
10296 10316 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning')
10297 10317 || (vmobj.state === 'provisioning'
10298 10318 && vmobj.zone_state !== 'installed')) {
10299 10319
10300 10320 callback(new Error('Cannot start VM from state: ' + vmobj.state
10301 10321 + ', must be "stopped"'));
10302 10322 return;
10303 10323 }
10304 10324
10305 10325 zoneroot = path.join(vmobj.zonepath, '/root');
10306 10326
10307 10327 // We're going to write to /startvm and /tmp/vm.metadata, we don't care if
10308 10328 // they already exist, but we don't want them to be symlinks.
10309 10329 try {
10310 10330 assertSafeZonePath(zoneroot, '/startvm',
10311 10331 {type: 'file', enoent_ok: true});
10312 10332 assertSafeZonePath(zoneroot, '/tmp/vm.metadata',
10313 10333 {type: 'file', enoent_ok: true});
10314 10334 } catch (e) {
10315 10335 log.error(e, 'Error validating files for startVM(): '
10316 10336 + e.message);
10317 10337 callback(e);
10318 10338 return;
10319 10339 }
10320 10340
10321 10341 // XXX TODO: validate vmobj data is ok to start
10322 10342
10323 10343 cmdargs.push('-m', vmobj.ram);
10324 10344 cmdargs.push('-name', vmobj.uuid);
10325 10345 cmdargs.push('-uuid', vmobj.uuid);
10326 10346
10327 10347 if (vmobj.hasOwnProperty('cpu_type')) {
10328 10348 cmdargs.push('-cpu', vmobj.cpu_type);
10329 10349 } else {
10330 10350 cmdargs.push('-cpu', 'qemu64');
10331 10351 }
10332 10352
10333 10353 if (vmobj.vcpus > 1) {
10334 10354 cmdargs.push('-smp', vmobj.vcpus);
10335 10355 }
10336 10356
10337 10357 for (disk in vmobj.disks) {
10338 10358 if (vmobj.disks.hasOwnProperty(disk)) {
10339 10359 disk = vmobj.disks[disk];
10340 10360 if (!disk.media) {
10341 10361 disk.media = 'disk';
10342 10362 }
10343 10363 diskargs = 'file=' + disk.path + ',if=' + disk.model
10344 10364 + ',index=' + disk_idx + ',media=' + disk.media;
10345 10365 if (disk.boot) {
10346 10366 diskargs = diskargs + ',boot=on';
10347 10367 }
10348 10368 cmdargs.push('-drive', diskargs);
10349 10369 disk_idx++;
10350 10370 }
10351 10371 }
10352 10372
10353 10373 // extra payload can include additional disks that we want to include only
10354 10374 // on this one boot. It can also contain a boot parameter to control boot
10355 10375 // device. See qemu http://qemu.weilnetz.de/qemu-doc.html for info on
10356 10376 // -boot options.
10357 10377 if (extra.hasOwnProperty('disks')) {
10358 10378 for (disk in extra.disks) {
10359 10379 if (extra.disks.hasOwnProperty(disk)) {
10360 10380 disk = extra.disks[disk];
10361 10381
10362 10382 // ensure this is either a disk that gets mounted in or a
10363 10383 // file that's been dropped in to the zonepath
10364 10384 found = false;
10365 10385 for (d in vmobj.disks) {
10366 10386 if (!found && vmobj.disks.hasOwnProperty(d)) {
10367 10387 d = vmobj.disks[d];
10368 10388 if (d.path === disk.path) {
10369 10389 found = true;
10370 10390 }
10371 10391 }
10372 10392 }
10373 10393 check_path = path.join(vmobj.zonepath, 'root', disk.path);
10374 10394 if (!found && fs.existsSync(check_path)) {
10375 10395 found = true;
10376 10396 }
10377 10397 if (!found) {
10378 10398 callback(new Error('Cannot find disk: ' + disk.path));
10379 10399 return;
10380 10400 }
10381 10401
10382 10402 if (!disk.media) {
10383 10403 disk.media = 'disk';
10384 10404 }
10385 10405 diskargs = 'file=' + disk.path + ',if=' + disk.model
10386 10406 + ',index=' + disk_idx + ',media=' + disk.media;
10387 10407 if (disk.boot) {
10388 10408 diskargs = diskargs + ',boot=on';
10389 10409 }
10390 10410 cmdargs.push('-drive', diskargs);
10391 10411 disk_idx++;
10392 10412 }
10393 10413 }
10394 10414 }
10395 10415
10396 10416 // helpful values:
10397 10417 // order=nc (network boot, then fallback to disk)
10398 10418 // once=d (boot on disk once and the fallback to default)
10399 10419 // order=c,once=d (boot on CDROM this time, but not subsequent boots)
10400 10420 if (extra.hasOwnProperty('boot')) {
10401 10421 cmdargs.push('-boot', extra.boot);
10402 10422 } else if (vmobj.hasOwnProperty('boot')) {
10403 10423 cmdargs.push('-boot', vmobj.boot);
10404 10424 } else {
10405 10425 // order=cd means try harddisk first (c) and cdrom if that fails (d)
10406 10426 cmdargs.push('-boot', 'order=cd');
10407 10427 }
10408 10428
10409 10429 if (vmobj.hasOwnProperty('hostname')) {
10410 10430 hostname = vmobj.hostname;
10411 10431 }
10412 10432
10413 10433 if (vmobj.hasOwnProperty('default_gateway')) {
10414 10434 defaultgw = vmobj['default_gateway'];
10415 10435 }
10416 10436
10417 10437 /*
10418 10438 * These tunables are set for all virtio vnics on this VM.
10419 10439 */
10420 10440 virtio_txtimer = VIRTIO_TXTIMER_DEFAULT;
10421 10441 virtio_txburst = VIRTIO_TXBURST_DEFAULT;
10422 10442 if (vmobj.hasOwnProperty('virtio_txtimer')) {
10423 10443 virtio_txtimer = vmobj.virtio_txtimer;
10424 10444 }
10425 10445 if (vmobj.hasOwnProperty('virtio_txburst')) {
10426 10446 virtio_txburst = vmobj.virtio_txburst;
10427 10447 }
10428 10448
10429 10449 for (nic in vmobj.nics) {
10430 10450 if (vmobj.nics.hasOwnProperty(nic)) {
10431 10451 nic = vmobj.nics[nic];
10432 10452
10433 10453 // for virtio devices, we want to be able to set the txtimer and
10434 10454 // txburst so we use a '-device' instead of a '-net' line.
10435 10455 if (nic.model === 'virtio') {
10436 10456 cmdargs.push('-device',
10437 10457 'virtio-net-pci,mac=' + nic.mac
10438 10458 + ',tx=timer,x-txtimer=' + virtio_txtimer
10439 10459 + ',x-txburst=' + virtio_txburst
10440 10460 + ',vlan=' + nic_idx);
10441 10461 } else {
10442 10462 cmdargs.push('-net',
10443 10463 'nic,macaddr=' + nic.mac
10444 10464 + ',vlan=' + nic_idx
10445 10465 + ',name=' + nic.interface
10446 10466 + ',model=' + nic.model);
10447 10467 }
10448 10468 vnic_opts = 'vnic,name=' + nic.interface
10449 10469 + ',vlan=' + nic_idx
10450 10470 + ',ifname=' + nic.interface;
10451 10471
10452 10472 if (nic.ip != 'dhcp') {
10453 10473 vnic_opts = vnic_opts
10454 10474 + ',ip=' + nic.ip
10455 10475 + ',netmask=' + nic.netmask;
10456 10476 }
10457 10477
10458 10478 // The primary network provides the resolvers, default gateway
10459 10479 // and hostname to prevent vm from trying to use settings
10460 10480 // from more than one nic
10461 10481 if (!primary_found) {
10462 10482 if (nic.hasOwnProperty('primary') && nic.primary) {
10463 10483 if (nic.hasOwnProperty('gateway') && nic.ip != 'dhcp') {
10464 10484 vnic_opts += ',gateway_ip=' + nic.gateway;
10465 10485 }
10466 10486 primary_found = true;
10467 10487 } else if (defaultgw && nic.hasOwnProperty('gateway')
10468 10488 && nic.gateway == defaultgw) {
10469 10489
10470 10490 /*
10471 10491 * XXX this exists here for backward compatibilty. New VMs
10472 10492 * and old VMs that are upgraded should not use
10473 10493 * default_gateway. When we've implemented autoupgrade
10474 10494 * this block (and all reference to default_gateway)
10475 10495 * can be removed.
10476 10496 */
10477 10497
10478 10498 if (nic.ip != 'dhcp') {
10479 10499 vnic_opts += ',gateway_ip=' + nic.gateway;
10480 10500 }
10481 10501 primary_found = true;
10482 10502 }
10483 10503
10484 10504 if (primary_found && nic.ip != 'dhcp') {
10485 10505 if (hostname) {
10486 10506 vnic_opts += ',hostname=' + hostname;
10487 10507 }
10488 10508 if (vmobj.hasOwnProperty('resolvers')) {
10489 10509 for (r in vmobj.resolvers) {
10490 10510 vnic_opts += ',dns_ip' + r + '='
10491 10511 + vmobj.resolvers[r];
10492 10512 }
10493 10513 }
10494 10514 }
10495 10515 }
10496 10516
10497 10517 cmdargs.push('-net', vnic_opts);
10498 10518 nic_idx++;
10499 10519 }
10500 10520 }
10501 10521
10502 10522 cmdargs.push('-smbios', 'type=1,manufacturer=Joyent,'
10503 10523 + 'product=SmartDC HVM,version=6.2012Q1,'
10504 10524 + 'serial=' + vmobj.uuid + ',uuid=' + vmobj.uuid + ','
10505 10525 + 'sku=001,family=Virtual Machine');
10506 10526
10507 10527 cmdargs.push('-pidfile', '/tmp/vm.pid');
10508 10528
10509 10529 if (vmobj.hasOwnProperty('vga')) {
10510 10530 cmdargs.push('-vga', vmobj.vga);
10511 10531 } else {
10512 10532 cmdargs.push('-vga', 'std');
10513 10533 }
10514 10534
10515 10535 cmdargs.push('-chardev',
10516 10536 'socket,id=qmp,path=/tmp/vm.qmp,server,nowait');
10517 10537 cmdargs.push('-qmp', 'chardev:qmp');
10518 10538
10519 10539 // serial0 is for serial console
10520 10540 cmdargs.push('-chardev',
10521 10541 'socket,id=serial0,path=/tmp/vm.console,server,nowait');
10522 10542 cmdargs.push('-serial', 'chardev:serial0');
10523 10543
10524 10544 // serial1 is used for metadata API
10525 10545 cmdargs.push('-chardev',
10526 10546 'socket,id=serial1,path=/tmp/vm.ttyb,server,nowait');
10527 10547 cmdargs.push('-serial', 'chardev:serial1');
10528 10548
10529 10549 if (!vmobj.qemu_opts) {
10530 10550 if (vmobj.hasOwnProperty('vnc_password')
10531 10551 && vmobj.vnc_password.length > 0) {
10532 10552
10533 10553 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc,password');
10534 10554 } else {
10535 10555 cmdargs.push('-vnc', 'unix:/tmp/vm.vnc');
10536 10556 }
10537 10557 if (vmobj.hasOwnProperty('spice_port')
10538 10558 && vmobj.spice_port !== -1) {
10539 10559
10540 10560 spiceargs = 'sock=/tmp/vm.spice';
10541 10561 if (!vmobj.hasOwnProperty('spice_password')
10542 10562 || vmobj.spice_password.length <= 0) {
10543 10563
10544 10564 spiceargs = spiceargs + ',disable-ticketing';
10545 10565
10546 10566 // Otherwise, spice password is set via qmp, so we don't
10547 10567 // need to do anything here
10548 10568 }
10549 10569 if (vmobj.hasOwnProperty('spice_opts')
10550 10570 && vmobj.spice_opts.length > 0) {
10551 10571
10552 10572 spiceargs = spiceargs + ',' + vmobj.spice_opts;
10553 10573 }
10554 10574 cmdargs.push('-spice', spiceargs);
10555 10575 }
10556 10576 cmdargs.push('-parallel', 'none');
10557 10577 cmdargs.push('-usb');
10558 10578 cmdargs.push('-usbdevice', 'tablet');
10559 10579 cmdargs.push('-k', 'en-us');
10560 10580 } else {
10561 10581 qemu_opts = vmobj.qemu_opts.toString();
10562 10582 }
10563 10583
10564 10584 if (vmobj.qemu_extra_opts) {
10565 10585 qemu_opts = qemu_opts + ' ' + vmobj.qemu_extra_opts;
10566 10586 }
10567 10587
10568 10588 // This actually creates the qemu process
10569 10589 script = '#!/usr/bin/bash\n\n'
10570 10590 + 'exec >/tmp/vm.startvm.log 2>&1\n\n'
10571 10591 + 'set -o xtrace\n\n'
10572 10592 + 'if [[ -x /startvm.zone ]]; then\n'
10573 10593 + ' exec /smartdc/bin/qemu-exec /startvm.zone "'
10574 10594 + cmdargs.join('" "')
10575 10595 + '" ' + qemu_opts + '\n'
10576 10596 + 'else\n'
10577 10597 + ' exec /smartdc/bin/qemu-exec /smartdc/bin/qemu-system-x86_64 "'
10578 10598 + cmdargs.join('" "')
10579 10599 + '" ' + qemu_opts + '\n'
10580 10600 + 'fi\n\n'
10581 10601 + 'exit 1\n';
10582 10602
10583 10603 try {
10584 10604 fs.writeFileSync(vmobj.zonepath + '/root/startvm', script);
10585 10605 fs.chmodSync(vmobj.zonepath + '/root/startvm', '0755');
10586 10606 } catch (e) {
10587 10607 log.warn(e, 'Unable to create /startvm script in ' + vmobj.uuid);
10588 10608 callback(new Error('cannot create /startvm'));
10589 10609 return;
10590 10610 }
10591 10611
10592 10612 mdata = {
10593 10613 'internal_metadata':
10594 10614 vmobj.internal_metadata ? vmobj.internal_metadata : {}
10595 10615 };
10596 10616 fs.writeFile(path.join(vmobj.zonepath, '/root/tmp/vm.metadata'),
10597 10617 JSON.stringify(mdata, null, 2) + '\n',
10598 10618 function (err) {
10599 10619 if (err) {
10600 10620 log.debug(err, 'FAILED TO write metadata to '
10601 10621 + '/tmp/vm.metadata: ' + err.message);
10602 10622 callback(err);
10603 10623 } else {
10604 10624 log.debug('wrote metadata to /tmp/vm.metadata');
10605 10625 startZone(vmobj, log, callback);
10606 10626 }
10607 10627 }
10608 10628 );
10609 10629 }
10610 10630
10611 10631 // according to usr/src/common/zfs/zfs_namecheck.c allowed characters are:
10612 10632 //
10613 10633 // alphanumeric characters plus the following: [-_.:%]
10614 10634 //
10615 10635 function validSnapshotName(snapname, log)
10616 10636 {
10617 10637 assert(log, 'no logger passed to validSnapshotName()');
10618 10638
10619 10639 if (snapname.length < 1 || snapname.length > MAX_SNAPNAME_LENGTH) {
10620 10640 log.error('Invalid snapname length: ' + snapname.length
10621 10641 + ' valid range: [1-' + MAX_SNAPNAME_LENGTH + ']');
10622 10642 return (false);
10623 10643 }
10624 10644
10625 10645 if (snapname.match(/[^a-zA-Z0-9\-\_\.\:\%]/)) {
10626 10646 log.error('Invalid snapshot name: contains invalid characters.');
10627 10647 return (false);
10628 10648 }
10629 10649
10630 10650 return (true);
10631 10651 }
10632 10652
10633 10653 function performSnapshotRollback(snapshots, log, callback)
10634 10654 {
10635 10655 assert(log, 'no logger passed to performSnapshotRollback()');
10636 10656
10637 10657 // NOTE: we assume machine is stopped and snapshots are already validated
10638 10658
10639 10659 function rollback(snapname, cb) {
10640 10660 var args;
10641 10661
10642 10662 args = ['rollback', '-r', snapname];
10643 10663 zfs(args, log, function (zfs_err, fds) {
10644 10664 if (zfs_err) {
10645 10665 log.error({'err': zfs_err, 'stdout': fds.stdout,
10646 10666 'stderr': fds.stdout}, 'zfs rollback of ' + snapname
10647 10667 + ' failed.');
10648 10668 cb(zfs_err);
10649 10669 return;
10650 10670 }
10651 10671 log.info('rolled back snapshot ' + snapname);
10652 10672 log.debug('zfs destroy stdout: ' + fds.stdout);
10653 10673 log.debug('zfs destroy stderr: ' + fds.stderr);
10654 10674 cb();
10655 10675 });
10656 10676 }
10657 10677
10658 10678 async.forEachSeries(snapshots, rollback, function (err) {
10659 10679 if (err) {
10660 10680 log.error(err, 'Unable to rollback some datasets.');
10661 10681 }
10662 10682 callback(err);
10663 10683 });
10664 10684 }
10665 10685
10666 10686 function updateZonecfgTimestamp(vmobj, callback)
10667 10687 {
10668 10688 var file;
10669 10689 var now;
10670 10690
10671 10691 assert(vmobj.zonename, 'updateZonecfgTimestamp() vmobj must have '
10672 10692 + '.zonename');
10673 10693
10674 10694 file = path.join('/etc/zones/', vmobj.zonename + '.xml');
10675 10695 now = new Date();
10676 10696
10677 10697 fs.utimes(file, now, now, callback);
10678 10698 }
10679 10699
10680 10700 exports.rollback_snapshot = function (uuid, snapname, options, callback)
10681 10701 {
10682 10702 var load_fields;
10683 10703 var log;
10684 10704
10685 10705 // options is optional
10686 10706 if (arguments.length === 3) {
10687 10707 callback = arguments[2];
10688 10708 options = {};
10689 10709 }
10690 10710
10691 10711 ensureLogging(true);
10692 10712 if (options.hasOwnProperty('log')) {
10693 10713 log = options.log;
10694 10714 } else {
10695 10715 log = VM.log.child({action: 'rollback_snapshot', vm: uuid});
10696 10716 }
10697 10717
10698 10718 if (!validSnapshotName(snapname, log)) {
10699 10719 callback(new Error('Invalid snapshot name'));
10700 10720 return;
10701 10721 }
10702 10722
10703 10723 load_fields = [
10704 10724 'brand',
10705 10725 'snapshots',
10706 10726 'zfs_filesystem',
10707 10727 'state',
10708 10728 'uuid'
10709 10729 ];
10710 10730
10711 10731 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10712 10732 var found;
10713 10733 var snap;
10714 10734 var snapshot_list = [];
10715 10735
10716 10736 if (err) {
10717 10737 callback(err);
10718 10738 return;
10719 10739 }
10720 10740
10721 10741 if (vmobj.brand === 'kvm') {
10722 10742 callback(new Error('snapshots for KVM VMs currently unsupported'));
10723 10743 return;
10724 10744 }
10725 10745
10726 10746 found = false;
10727 10747 if (vmobj.hasOwnProperty('snapshots')) {
10728 10748 for (snap in vmobj.snapshots) {
10729 10749 if (vmobj.snapshots[snap].name === snapname) {
10730 10750 found = true;
10731 10751 break;
10732 10752 }
10733 10753 }
10734 10754 }
10735 10755 if (!found) {
10736 10756 callback(new Error('No snapshot named "' + snapname + '" for '
10737 10757 + uuid));
10738 10758 return;
10739 10759 }
10740 10760
10741 10761 snapshot_list = [vmobj.zfs_filesystem + '@vmsnap-' + snapname];
10742 10762
10743 10763 if (vmobj.state !== 'stopped') {
10744 10764 VM.stop(vmobj.uuid, {'force': true, log: log}, function (stop_err) {
10745 10765 if (stop_err) {
10746 10766 log.error(stop_err, 'failed to stop VM ' + vmobj.uuid
10747 10767 + ': ' + stop_err.message);
10748 10768 callback(stop_err);
10749 10769 return;
10750 10770 }
10751 10771 performSnapshotRollback(snapshot_list, log,
10752 10772 function (rollback_err) {
10753 10773
10754 10774 if (rollback_err) {
10755 10775 log.error(rollback_err, 'failed to '
10756 10776 + 'performSnapshotRollback');
10757 10777 callback(rollback_err);
10758 10778 return;
10759 10779 }
10760 10780 if (options.do_not_start) {
10761 10781 callback();
10762 10782 } else {
10763 10783 VM.start(vmobj.uuid, {}, {log: log}, callback);
10764 10784 }
10765 10785 return;
10766 10786 });
10767 10787 });
10768 10788 } else {
10769 10789 performSnapshotRollback(snapshot_list, log,
10770 10790 function (rollback_err) {
10771 10791
10772 10792 if (rollback_err) {
10773 10793 log.error(rollback_err, 'failed to '
10774 10794 + 'performSnapshotRollback');
10775 10795 callback(rollback_err);
10776 10796 return;
10777 10797 }
10778 10798 if (options.do_not_start) {
10779 10799 callback();
10780 10800 } else {
10781 10801 VM.start(vmobj.uuid, {}, {log: log}, callback);
10782 10802 }
10783 10803 return;
10784 10804 });
10785 10805 }
10786 10806 });
10787 10807 };
10788 10808
10789 10809 exports.delete_snapshot = function (uuid, snapname, options, callback)
10790 10810 {
10791 10811 var load_fields;
10792 10812 var log;
10793 10813
10794 10814 // options is optional
10795 10815 if (arguments.length === 3) {
10796 10816 callback = arguments[2];
10797 10817 options = {};
10798 10818 }
10799 10819
10800 10820 ensureLogging(true);
10801 10821 if (options.hasOwnProperty('log')) {
10802 10822 log = options.log;
10803 10823 } else {
10804 10824 log = VM.log.child({action: 'delete_snapshot', vm: uuid});
10805 10825 }
10806 10826
10807 10827 if (!validSnapshotName(snapname, log)) {
10808 10828 callback(new Error('Invalid snapshot name'));
10809 10829 return;
10810 10830 }
10811 10831
10812 10832 load_fields = [
10813 10833 'brand',
10814 10834 'snapshots',
10815 10835 'zfs_filesystem',
10816 10836 'zonepath',
10817 10837 'zonename'
10818 10838 ];
10819 10839
10820 10840 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10821 10841 var found;
10822 10842 var mountpath;
10823 10843 var mountpoint;
10824 10844 var snap;
10825 10845 var zoneroot;
10826 10846
10827 10847 if (err) {
10828 10848 callback(err);
10829 10849 return;
10830 10850 }
10831 10851
10832 10852 if (vmobj.brand === 'kvm') {
10833 10853 callback(new Error('snapshots for KVM VMs currently unsupported'));
10834 10854 return;
10835 10855 }
10836 10856
10837 10857 found = false;
10838 10858 if (vmobj.hasOwnProperty('snapshots')) {
10839 10859 for (snap in vmobj.snapshots) {
10840 10860 if (vmobj.snapshots[snap].name === snapname) {
10841 10861 found = true;
10842 10862 break;
10843 10863 }
10844 10864 }
10845 10865 }
10846 10866 if (!found) {
10847 10867 callback(new Error('No snapshot named "' + snapname + '" for '
10848 10868 + uuid));
10849 10869 return;
10850 10870 }
10851 10871
10852 10872 zoneroot = vmobj.zonepath + '/root';
10853 10873 mountpath = '/checkpoints/' + snapname;
10854 10874 mountpoint = zoneroot + '/' + mountpath;
10855 10875
10856 10876 async.waterfall([
10857 10877 function (cb) {
10858 10878 // Ensure it's safe for us to be doing something in this dir
10859 10879 try {
10860 10880 assertSafeZonePath(zoneroot, mountpath,
10861 10881 {type: 'dir', enoent_ok: true});
10862 10882 } catch (e) {
10863 10883 log.error(e, 'Unsafe mountpoint for checkpoints: '
10864 10884 + e.message);
10865 10885 cb(e);
10866 10886 return;
10867 10887 }
10868 10888 cb();
10869 10889 }, function (cb) {
10870 10890 // umount snapshot
10871 10891 var argv;
10872 10892 var cmd = '/usr/sbin/umount';
10873 10893
10874 10894 argv = [mountpoint];
10875 10895
10876 10896 execFile(cmd, argv, function (e, stdout, stderr) {
10877 10897 if (e) {
10878 10898 log.error({err: e}, 'There was an error while '
10879 10899 + 'unmounting the snapshot: ' + e.message);
10880 10900 // we treat an error here as fatal only if the error
10881 10901 // was something other than 'not mounted'
10882 10902 if (!stderr.match(/ not mounted/)) {
10883 10903 cb(e);
10884 10904 return;
10885 10905 }
10886 10906 } else {
10887 10907 log.trace('umounted ' + mountpoint);
10888 10908 }
10889 10909 cb();
10890 10910 });
10891 10911 }, function (cb) {
10892 10912 // remove the mountpoint directory
10893 10913 fs.rmdir(mountpoint, function (e) {
10894 10914 if (e) {
10895 10915 log.error(e);
10896 10916 } else {
10897 10917 log.trace('removed directory ' + mountpoint);
10898 10918 }
10899 10919 cb(); // XXX not fatal because might also not exist
10900 10920 });
10901 10921 }, function (cb) {
10902 10922 var args;
10903 10923
10904 10924 args = ['destroy', vmobj.zfs_filesystem + '@vmsnap-'
10905 10925 + snapname];
10906 10926
10907 10927 zfs(args, log, function (e, fds) {
10908 10928 if (e) {
10909 10929 log.error({'err': e, 'stdout': fds.stdout,
10910 10930 'stderr': fds.stdout}, 'zfs destroy failed.');
10911 10931 cb(e);
10912 10932 return;
10913 10933 }
10914 10934 log.debug({err: e, stdout: fds.stdout, stderr: fds.stderr},
10915 10935 'zfs destroy ' + vmobj.zfs_filesystem + '@vmsnap-'
10916 10936 + snapname);
10917 10937 cb();
10918 10938 });
10919 10939 }, function (cb) {
10920 10940 updateZonecfgTimestamp(vmobj, function (e) {
10921 10941 if (e) {
10922 10942 log.warn(e, 'failed to update timestamp after deleting '
10923 10943 + 'snapshot');
10924 10944 }
10925 10945 // don't pass err because there's no recovery possible
10926 10946 // (the snapshot's gone)
10927 10947 cb();
10928 10948 });
10929 10949 }
10930 10950 ], function (error) {
10931 10951 callback(error);
10932 10952 });
10933 10953 });
10934 10954 };
10935 10955
10936 10956 exports.create_snapshot = function (uuid, snapname, options, callback)
10937 10957 {
10938 10958 var load_fields;
10939 10959 var log;
10940 10960
10941 10961 // options is optional
10942 10962 if (arguments.length === 3) {
10943 10963 callback = arguments[2];
10944 10964 options = {};
10945 10965 }
10946 10966
10947 10967 ensureLogging(true);
10948 10968
10949 10969 if (options.hasOwnProperty('log')) {
10950 10970 log = options.log;
10951 10971 } else {
10952 10972 log = VM.log.child({action: 'create_snapshot', vm: uuid});
10953 10973 }
10954 10974
10955 10975 if (!validSnapshotName(snapname, log)) {
10956 10976 callback(new Error('Invalid snapshot name'));
10957 10977 return;
10958 10978 }
10959 10979
10960 10980 load_fields = [
10961 10981 'brand',
10962 10982 'datasets',
10963 10983 'zone_state',
10964 10984 'snapshots',
10965 10985 'zfs_filesystem',
10966 10986 'zonepath',
10967 10987 'zonename'
10968 10988 ];
10969 10989
10970 10990 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
10971 10991 var full_snapname;
10972 10992 var mountpath;
10973 10993 var mountpoint;
10974 10994 var mount_snapshot = true;
10975 10995 var snap;
10976 10996 var snapshot_list = [];
10977 10997 var zoneroot;
10978 10998
10979 10999 if (err) {
10980 11000 callback(err);
10981 11001 return;
10982 11002 }
10983 11003
10984 11004 if (vmobj.brand === 'kvm') {
10985 11005 callback(new Error('snapshots for KVM VMs currently unsupported'));
10986 11006 return;
10987 11007 }
10988 11008
10989 11009 if (vmobj.hasOwnProperty('datasets') && vmobj.datasets.length > 0) {
10990 11010 callback(new Error('Cannot currently snapshot zones that have '
10991 11011 + 'datasets'));
10992 11012 return;
10993 11013 }
10994 11014
10995 11015 if (!vmobj.hasOwnProperty('zfs_filesystem')) {
10996 11016 callback(new Error('vmobj missing zfs_filesystem, cannot create '
10997 11017 + 'snapshot'));
10998 11018 return;
10999 11019 }
11000 11020
11001 11021 full_snapname = vmobj.zfs_filesystem + '@vmsnap-' + snapname;
11002 11022
11003 11023 // Check that name not already used
11004 11024 if (vmobj.hasOwnProperty('snapshots')) {
11005 11025 for (snap in vmobj.snapshots) {
11006 11026 snap = vmobj.snapshots[snap];
11007 11027
11008 11028 if (snap.name === full_snapname) {
11009 11029 callback(new Error('snapshot with name "' + snapname
11010 11030 + '" already exists.'));
11011 11031 return;
11012 11032 } else {
11013 11033 log.debug('SKIPPING ' + snap.name);
11014 11034 }
11015 11035 }
11016 11036 }
11017 11037
11018 11038 snapshot_list.push(full_snapname);
11019 11039
11020 11040 // assert snapshot_list.length > 0
11021 11041
11022 11042 log.info('Taking snapshot "' + snapname + '" of ' + uuid);
11023 11043
11024 11044 zoneroot = vmobj.zonepath + '/root';
11025 11045 mountpath = '/checkpoints/' + snapname;
11026 11046 mountpoint = zoneroot + '/' + mountpath;
11027 11047
11028 11048 async.waterfall([
11029 11049 function (cb) {
11030 11050 // take the snapshot
11031 11051 var args;
11032 11052 args = ['snapshot'].concat(snapshot_list);
11033 11053
11034 11054 zfs(args, log, function (zfs_err, fds) {
11035 11055 if (zfs_err) {
11036 11056 log.error({err: zfs_err, stdout: fds.stdout,
11037 11057 stderr: fds.stdout}, 'zfs snapshot failed.');
11038 11058 } else {
11039 11059 log.debug({err: zfs_err, stdout: fds.stdout,
11040 11060 stderr: fds.stderr}, 'zfs ' + args.join(' '));
11041 11061 }
11042 11062 cb(zfs_err);
11043 11063 });
11044 11064 }, function (cb) {
11045 11065
11046 11066 if (vmobj.zone_state !== 'running') {
11047 11067 log.info('Not mounting snapshot as zone is in state '
11048 11068 + vmobj.zone_state + ', must be: running');
11049 11069 mount_snapshot = false;
11050 11070 cb();
11051 11071 return;
11052 11072 }
11053 11073
11054 11074 // Ensure it's safe for us to be doing something in this dir
11055 11075 try {
11056 11076 assertSafeZonePath(zoneroot, mountpath,
11057 11077 {type: 'dir', enoent_ok: true});
11058 11078 } catch (e) {
11059 11079 log.error(e, 'Unsafe mountpoint for checkpoints: '
11060 11080 + e.message);
11061 11081 cb(e);
11062 11082 return;
11063 11083 }
11064 11084 cb();
11065 11085 }, function (cb) {
11066 11086 // Make the mountpoint directory and parent
11067 11087 var newmode;
11068 11088
11069 11089 if (mount_snapshot === false) {
11070 11090 cb();
11071 11091 return;
11072 11092 }
11073 11093
11074 11094 /*jsl:ignore*/
11075 11095 newmode = 0755;
11076 11096 /*jsl:end*/
11077 11097
11078 11098 function doMkdir(dir, callbk) {
11079 11099 fs.mkdir(dir, newmode, function (e) {
11080 11100 if (e && e.code !== 'EEXIST') {
11081 11101 log.error({err: e}, 'unable to create mountpoint '
11082 11102 + 'for checkpoints: ' + e.message);
11083 11103 callbk(e);
11084 11104 return;
11085 11105 }
11086 11106 callbk();
11087 11107 });
11088 11108 }
11089 11109
11090 11110 doMkdir(path.dirname(mountpoint), function (parent_e) {
11091 11111 if (parent_e) {
11092 11112 cb(parent_e);
11093 11113 return;
11094 11114 }
11095 11115 doMkdir(mountpoint, function (dir_e) {
11096 11116 if (dir_e) {
11097 11117 cb(dir_e);
11098 11118 return;
11099 11119 }
11100 11120
11101 11121 log.debug('created ' + mountpoint);
11102 11122 cb();
11103 11123 });
11104 11124 });
11105 11125 }, function (cb) {
11106 11126 var argv;
11107 11127 var cmd = '/usr/sbin/mount';
11108 11128 var snapdir;
11109 11129
11110 11130 if (mount_snapshot === false) {
11111 11131 cb();
11112 11132 return;
11113 11133 }
11114 11134
11115 11135 snapdir = vmobj.zonepath + '/.zfs/snapshot/vmsnap-' + snapname
11116 11136 + '/root';
11117 11137 argv = [ '-F', 'lofs', '-o', 'ro,setuid,nodevices', snapdir,
11118 11138 mountpoint];
11119 11139
11120 11140 execFile(cmd, argv, function (e, stdout, stderr) {
11121 11141 if (e) {
11122 11142 log.error({err: e}, 'unable to mount snapshot: '
11123 11143 + e.message);
11124 11144 }
11125 11145 // not fatal becase snapshot was already created.
11126 11146 cb();
11127 11147 });
11128 11148 }, function (cb) {
11129 11149 // update timestamp so last_modified gets bumped
11130 11150 updateZonecfgTimestamp(vmobj, function (e) {
11131 11151 if (e) {
11132 11152 log.warn(e,
11133 11153 'failed to update timestamp after snapshot');
11134 11154 }
11135 11155 // ignore error since there's no recovery
11136 11156 // (snapshot was created)
11137 11157 cb();
11138 11158 });
11139 11159 }
11140 11160 ], function (error) {
11141 11161 callback(error);
11142 11162 });
11143 11163 });
11144 11164 };
11145 11165
11146 11166 exports.start = function (uuid, extra, options, callback)
11147 11167 {
11148 11168 var load_fields;
11149 11169 var log;
11150 11170
11151 11171 load_fields = [
11152 11172 'brand',
11153 11173 'nics',
11154 11174 'state',
11155 11175 'uuid',
11156 11176 'zone_state',
11157 11177 'zonename',
11158 11178 'zonepath'
11159 11179 ];
11160 11180
11161 11181 // options is optional
11162 11182 if (arguments.length === 3) {
11163 11183 callback = arguments[2];
11164 11184 options = {};
11165 11185 }
11166 11186
11167 11187 assert(callback, 'undefined callback!');
11168 11188
11169 11189 ensureLogging(true);
11170 11190 if (options.hasOwnProperty('log')) {
11171 11191 log = options.log;
11172 11192 } else {
11173 11193 log = VM.log.child({action: 'start', vm: uuid});
11174 11194 }
11175 11195
11176 11196 log.info('Starting VM ' + uuid);
11177 11197
11178 11198 VM.load(uuid, {log: log, fields: load_fields}, function (err, vmobj) {
11179 11199 if (err) {
11180 11200 callback(err);
11181 11201 } else {
11182 11202
11183 11203 if (vmobj.state === 'running') {
11184 11204 err = new Error('VM ' + vmobj.uuid + ' is already '
11185 11205 + '\'running\'');
11186 11206 err.code = 'EALREADYRUNNING';
11187 11207 callback(err);
11188 11208 return;
11189 11209 }
11190 11210
11191 11211 if ((vmobj.state !== 'stopped' && vmobj.state !== 'provisioning')
11192 11212 || (vmobj.state === 'provisioning'
11193 11213 && vmobj.zone_state !== 'installed')) {
11194 11214
11195 11215 err = new Error('Cannot to start vm from state "' + vmobj.state
11196 11216 + '", must be "stopped".');
11197 11217 log.error(err);
11198 11218 callback(err);
11199 11219 return;
11200 11220 }
11201 11221
11202 11222 lookupInvalidNicTags(vmobj.nics, log, function (e) {
11203 11223 var kvm_load_fields = [
11204 11224 'boot',
11205 11225 'brand',
11206 11226 'cpu_type',
11207 11227 'default_gateway',
11208 11228 'disks',
11209 11229 'hostname',
11210 11230 'internal_metadata',
11211 11231 'never_booted',
11212 11232 'nics',
11213 11233 'qemu_extra_opts',
11214 11234 'qemu_opts',
11215 11235 'ram',
11216 11236 'resolvers',
11217 11237 'spice_opts',
11218 11238 'spice_password',
11219 11239 'spice_port',
11220 11240 'state',
11221 11241 'uuid',
11222 11242 'vcpus',
11223 11243 'vga',
11224 11244 'virtio_txtimer',
11225 11245 'virtio_txburst',
11226 11246 'vnc_password',
11227 11247 'zone_state',
11228 11248 'zonename',
11229 11249 'zonepath'
11230 11250 ];
11231 11251
11232 11252 if (e) {
11233 11253 callback(e);
11234 11254 return;
11235 11255 }
11236 11256
11237 11257 if (BRAND_OPTIONS[vmobj.brand].features.type === 'KVM') {
11238 11258 // when we boot KVM we need a lot more fields, so load again
11239 11259 // in that case to get the fields we need.
11240 11260 VM.load(uuid, {log: log, fields: kvm_load_fields},
11241 11261 function (error, obj) {
11242 11262
11243 11263 if (error) {
11244 11264 callback(error);
11245 11265 return;
11246 11266 }
11247 11267 startVM(obj, extra, log, callback);
11248 11268 });
11249 11269 } else if (BRAND_OPTIONS[vmobj.brand].features.type === 'OS') {
11250 11270 startZone(vmobj, log, callback);
11251 11271 } else {
11252 11272 err = new Error('no idea how to start a vm with brand: '
11253 11273 + vmobj.brand);
11254 11274 log.error(err);
11255 11275 callback(err);
11256 11276 }
11257 11277 });
11258 11278 }
11259 11279 });
11260 11280 };
11261 11281
11262 11282 function setRctl(zonename, rctl, value, log, callback)
11263 11283 {
11264 11284 var args;
11265 11285
11266 11286 assert(log, 'no logger passed to setRctl()');
11267 11287
11268 11288 args = ['-n', rctl, '-v', value.toString(), '-r', '-i', 'zone', zonename];
11269 11289 log.debug('/usr/bin/prctl ' + args.join(' '));
11270 11290 execFile('/usr/bin/prctl', args, function (error, stdout, stderr) {
11271 11291 if (error) {
11272 11292 log.error(error, 'setRctl() failed with: ' + stderr);
11273 11293 callback(error);
11274 11294 } else {
11275 11295 callback();
11276 11296 }
11277 11297 });
11278 11298 }
11279 11299
11280 11300 function resizeTmp(zonename, newsize, log, callback)
11281 11301 {
11282 11302 var args;
11283 11303
11284 11304 // NOTE: this used to update /etc/vfstab in the zone as well, but was
11285 11305 // changed with OS-920. Now vfstab is updated by mdata-fetch in the
11286 11306 // zone instead, so that will happen next boot. We still do the mount
11287 11307 // so the property update happens on the running zone.
11288 11308
11289 11309 assert(log, 'no logger passed to resizeTmp()');
11290 11310
11291 11311 args = [zonename, '/usr/sbin/mount', '-F', 'tmpfs', '-o', 'remount,size='
11292 11312 + newsize + 'm', '/tmp'];
11293 11313 log.debug('/usr/sbin/zlogin ' + args.join(' '));
11294 11314 execFile('/usr/sbin/zlogin', args, function (err, mnt_stdout, mnt_stderr) {
11295 11315 if (err) {
11296 11316 log.error({'err': err, 'stdout': mnt_stdout,
11297 11317 'stderr': mnt_stderr}, 'zlogin for ' + zonename
11298 11318 + ' exited with code ' + err.code + ' -- ' + err.message);
11299 11319 // error here is not fatal as this should be fixed on reboot
11300 11320 }
11301 11321
11302 11322 callback();
11303 11323 });
11304 11324 }
11305 11325
11306 11326 function resizeDisks(disks, updates, log, callback)
11307 11327 {
11308 11328 var d;
11309 11329 var disk;
11310 11330 var resized = 0;
11311 11331 var vols = [];
11312 11332
11313 11333 assert(log, 'no logger passed to resizeDisks()');
11314 11334
11315 11335 for (disk in updates) {
11316 11336 disk = updates[disk];
11317 11337 for (d in disks) {
11318 11338 d = disks[d];
11319 11339 if (d.path === disk.path && disk.hasOwnProperty('size')) {
11320 11340 vols.push({'disk': d, 'new_size': disk.size});
11321 11341 }
11322 11342 }
11323 11343 }
11324 11344
11325 11345 function resize(vol, cb) {
11326 11346 var args;
11327 11347 var dsk = vol.disk;
11328 11348 var size = vol.new_size;
11329 11349
11330 11350 if (dsk.hasOwnProperty('zfs_filesystem')) {
11331 11351 if (dsk.size > size) {
11332 11352 cb(new Error('cannot resize ' + dsk.zfs_filesystem
11333 11353 + ' new size must be greater than current size. ('
11334 11354 + dsk.size + ' > ' + dsk.size + ')'));
11335 11355 } else if (dsk.size === size) {
11336 11356 // no point resizing if the old+new are the same
11337 11357 cb();
11338 11358 } else {
11339 11359 args = ['set', 'volsize=' + size + 'M', dsk.zfs_filesystem];
11340 11360 zfs(args, log, function (err, fds) {
11341 11361 resized++;
11342 11362 cb(err);
11343 11363 });
11344 11364 }
11345 11365 } else {
11346 11366 cb(new Error('could not find zfs_filesystem in '
11347 11367 + JSON.stringify(dsk)));
11348 11368 }
11349 11369 }
11350 11370
11351 11371 async.forEachSeries(vols, resize, function (err) {
11352 11372 if (err) {
11353 11373 log.error(err, 'Unable to resize disks');
11354 11374 callback(err);
11355 11375 } else {
11356 11376 callback(null, resized);
11357 11377 }
11358 11378 });
11359 11379 }
11360 11380
11361 11381 function updateVnicAllowedIPs(uuid, nic, log, callback)
11362 11382 {
11363 11383 var ips = [];
11364 11384
11365 11385 assert(log, 'no logger passed to updateVnicAllowedIPs()');
11366 11386
11367 11387 if (!uuid || !nic.interface) {
11368 11388 callback();
11369 11389 return;
11370 11390 }
11371 11391
11372 11392 if (nic.hasOwnProperty('allow_ip_spoofing') && nic.allow_ip_spoofing) {
11373 11393 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback);
11374 11394 return;
11375 11395 }
11376 11396
11377 11397 if (nic.hasOwnProperty('ip')) {
11378 11398 ips.push(nic.ip);
11379 11399 }
11380 11400
11381 11401 if (nic.hasOwnProperty('vrrp_primary_ip')) {
11382 11402 ips.push(nic.vrrp_primary_ip);
11383 11403 }
11384 11404
11385 11405 if (nic.hasOwnProperty('allowed_ips')) {
11386 11406 ips = ips.concat(nic.allowed_ips);
11387 11407 }
11388 11408
11389 11409 if (!ips.length === 0) {
11390 11410 dladm.resetLinkProp(uuid, nic.interface, 'allowed-ips', log, callback);
11391 11411 } else {
11392 11412 dladm.setLinkProp(uuid, nic.interface, 'allowed-ips', ips, log,
11393 11413 callback);
11394 11414 }
11395 11415 }
11396 11416
11397 11417 function updateVnicProperties(uuid, vmobj, payload, log, callback)
11398 11418 {
11399 11419 assert(log, 'no logger passed to updateVnicProperties()');
11400 11420
11401 11421 if (vmobj.state != 'running') {
11402 11422 log.debug('VM not running: not updating vnic properties');
11403 11423 callback(null);
11404 11424 return;
11405 11425 }
11406 11426
11407 11427 if (!payload.hasOwnProperty('update_nics')) {
11408 11428 log.debug(
11409 11429 'No update_nics property: not updating vnic properties');
11410 11430 callback(null);
11411 11431 return;
11412 11432 }
11413 11433
11414 11434 async.forEach(payload.update_nics, function (nic, cb) {
11415 11435 var opt;
11416 11436 var needsUpdate = false;
11417 11437 var needsIPupdate = false;
11418 11438 var spoof_opts = {
11419 11439 'allow_ip_spoofing': 'ip-nospoof',
11420 11440 'allow_mac_spoofing': 'mac-nospoof',
11421 11441 'allow_dhcp_spoofing': 'dhcp-nospoof',
11422 11442 'allow_restricted_traffic': 'restricted'
11423 11443 };
11424 11444 var vm_nic;
11425 11445
11426 11446 // First, determine if we've changed any of the spoofing opts in this
11427 11447 // update:
11428 11448 for (opt in spoof_opts) {
11429 11449 if (nic.hasOwnProperty(opt)) {
11430 11450 needsUpdate = true;
11431 11451 break;
11432 11452 }
11433 11453 }
11434 11454
11435 11455 if (nic.hasOwnProperty('vrrp_primary_ip')
11436 11456 || nic.hasOwnProperty('allowed_ips')
11437 11457 || nic.hasOwnProperty('allow_ip_spoofing')) {
11438 11458 needsIPupdate = true;
11439 11459 }
11440 11460
11441 11461 for (vm_nic in vmobj.nics) {
11442 11462 vm_nic = vmobj.nics[vm_nic];
11443 11463 if (vm_nic.mac == nic.mac) {
11444 11464 break;
11445 11465 }
11446 11466 }
11447 11467
11448 11468 if (!vm_nic) {
11449 11469 cb(new Error('Unknown NIC: ' + nic.mac));
11450 11470 return;
11451 11471 }
11452 11472
11453 11473 if (!needsUpdate) {
11454 11474 log.debug('No spoofing / allowed IP opts updated for nic "'
11455 11475 + nic.mac + '": not updating');
11456 11476 if (needsIPupdate) {
11457 11477 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11458 11478 } else {
11459 11479 cb(null);
11460 11480 }
11461 11481 return;
11462 11482 }
11463 11483
11464 11484 // Using the updated nic object, figure out what spoofing opts to set
11465 11485 for (opt in spoof_opts) {
11466 11486 if (vm_nic.hasOwnProperty(opt) && fixBoolean(vm_nic[opt])) {
11467 11487 delete spoof_opts[opt];
11468 11488 }
11469 11489 }
11470 11490
11471 11491 if (vm_nic.hasOwnProperty('dhcp_server')
11472 11492 && fixBoolean(vm_nic.dhcp_server)) {
11473 11493 delete spoof_opts.allow_dhcp_spoofing;
11474 11494 delete spoof_opts.allow_ip_spoofing;
11475 11495 }
11476 11496
11477 11497 if (Object.keys(spoof_opts).length === 0) {
11478 11498 dladm.resetLinkProp(uuid, vm_nic.interface, 'protection', log,
11479 11499 function (err) {
11480 11500 if (err) {
11481 11501 cb(err);
11482 11502 return;
11483 11503 }
11484 11504 if (needsIPupdate) {
11485 11505 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11486 11506 return;
11487 11507 }
11488 11508 cb();
11489 11509 return;
11490 11510 });
11491 11511 } else {
11492 11512 dladm.setLinkProp(uuid, vm_nic.interface, 'protection',
11493 11513 Object.keys(spoof_opts).map(function (k) {
11494 11514 return spoof_opts[k];
11495 11515 }), log,
11496 11516 function (err) {
11497 11517 if (err) {
11498 11518 cb(err);
11499 11519 return;
11500 11520 }
11501 11521 if (needsIPupdate) {
11502 11522 updateVnicAllowedIPs(uuid, vm_nic, log, cb);
11503 11523 return;
11504 11524 }
11505 11525 cb();
11506 11526 return;
11507 11527 });
11508 11528 }
11509 11529 }, function (err) {
11510 11530 if (err) {
11511 11531 callback(err);
11512 11532 } else {
11513 11533 callback(null);
11514 11534 }
11515 11535 });
11516 11536 }
11517 11537
11518 11538 // Run a fw.js function that requires all VM records
11519 11539 function firewallVMrun(opts, fn, log, callback)
11520 11540 {
11521 11541 assert(log, 'no logger passed to firewallVMrun()');
11522 11542 VM.lookup({}, {fields: fw.VM_FIELDS, log: log}, function (err, records) {
11523 11543 if (err) {
11524 11544 callback(err);
11525 11545 return;
11526 11546 }
11527 11547
11528 11548 opts.vms = records;
11529 11549 if (fn.name == 'validatePayload') {
11530 11550 opts.logName = 'VM-create';
11531 11551 } else {
11532 11552 opts.logName = 'VM-' + (fn.name || '');
11533 11553 }
11534 11554
11535 11555 if (opts.provisioning) {
11536 11556 opts.vms.push(opts.provisioning);
11537 11557 delete opts.provisioning;
11538 11558 }
11539 11559
11540 11560 fn(opts, callback);
11541 11561 return;
11542 11562 });
11543 11563 }
11544 11564
11545 11565 function validateFirewall(payload, log, callback)
11546 11566 {
11547 11567 assert(log, 'no logger passed to validateFirewall()');
11548 11568
11549 11569 log.debug(toValidate, 'Validating firewall payload');
11550 11570 var toValidate = payload.firewall;
11551 11571 toValidate.provisioning = {
11552 11572 'state': 'provisioning'
11553 11573 };
11554 11574
11555 11575 fw.VM_FIELDS.forEach(function (field) {
11556 11576 if (payload.hasOwnProperty(field)) {
11557 11577 toValidate.provisioning[field] = payload[field];
11558 11578 }
11559 11579 });
11560 11580
11561 11581 if (payload.hasOwnProperty('add_nics')) {
11562 11582 toValidate.provisioning.nics = payload.add_nics;
11563 11583 }
11564 11584
11565 11585 // We're not actually writing data to zonepath when validating, and we
11566 11586 // don't actually have a zonepath created yet, so add a key so that the
11567 11587 // payload passes validation
11568 11588 if (!payload.hasOwnProperty('zonepath')) {
11569 11589 toValidate.provisioning.zonepath = true;
11570 11590 }
11571 11591
11572 11592 log.debug({
11573 11593 firewall: toValidate.firewall,
11574 11594 provisioning: toValidate.provisioning,
11575 11595 payload: payload
11576 11596 }, 'Validating firewall payload');
11577 11597
11578 11598 firewallVMrun(toValidate, fw.validatePayload, log,
11579 11599 function (err, res) {
11580 11600 if (err) {
11581 11601 log.error(err, 'Error validating firewall payload');
11582 11602 err.message = 'Invalid firewall payload: ' + err.message;
11583 11603 }
11584 11604
11585 11605 callback(err, res);
11586 11606 return;
11587 11607 });
11588 11608 }
11589 11609
11590 11610 function addFirewallData(payload, vmobj, log, callback)
11591 11611 {
11592 11612 var firewallOpts = payload.firewall;
11593 11613
11594 11614 assert(log, 'no logger passed to addFirewallData()');
11595 11615
11596 11616 if (!payload.hasOwnProperty('firewall')) {
11597 11617 firewallOpts = {};
11598 11618 }
11599 11619 firewallOpts.localVMs = [vmobj];
11600 11620
11601 11621 log.debug(firewallOpts, 'Adding firewall data');
11602 11622 firewallVMrun(firewallOpts, fw.add, log, function (err, res) {
11603 11623 if (err) {
11604 11624 log.error(err, 'Error adding firewall data');
11605 11625 }
11606 11626
11607 11627 callback(err, res);
11608 11628 return;
11609 11629 });
11610 11630 }
11611 11631
11612 11632 function updateFirewallData(payload, vmobj, log, callback)
11613 11633 {
11614 11634 var enablePrefix = 'En';
11615 11635 var enableFn = fw.enable;
11616 11636 var firewallOpts = payload.firewall;
11617 11637
11618 11638 assert(log, 'no logger passed to updateFirewallData()');
11619 11639
11620 11640 if (!payload.hasOwnProperty('firewall')) {
11621 11641 firewallOpts = {};
11622 11642 }
11623 11643 firewallOpts.localVMs = [vmobj];
11624 11644
11625 11645 log.debug(firewallOpts, 'Updating firewall data');
11626 11646 firewallVMrun(firewallOpts, fw.update, log, function (err, res) {
11627 11647 if (err) {
11628 11648 log.error(err, 'Error updating firewall data');
11629 11649 }
11630 11650
11631 11651 if (!payload.hasOwnProperty('firewall_enabled')) {
11632 11652 callback(err, res);
11633 11653 return;
11634 11654 }
11635 11655
11636 11656 if (!payload.firewall_enabled) {
11637 11657 enableFn = fw.disable;
11638 11658 enablePrefix = 'Dis';
11639 11659 }
11640 11660
11641 11661 log.debug('%sabling firewall for VM %s', enablePrefix, vmobj.uuid);
11642 11662 firewallVMrun({ vm: vmobj }, enableFn, log, function (err2, res2) {
11643 11663 if (err2) {
11644 11664 log.error(err, 'Error %sabling firewall',
11645 11665 enablePrefix.toLowerCase());
11646 11666 }
11647 11667
11648 11668 callback(err2, res2);
11649 11669 return;
11650 11670 });
11651 11671 });
11652 11672 }
11653 11673
11654 11674 function restartMetadataService(vmobj, payload, log, callback) {
11655 11675 var args;
11656 11676
11657 11677 assert(log, 'no logger passed to restartMetadataService()');
11658 11678
11659 11679 if (!BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
11660 11680 || !BRAND_OPTIONS[vmobj.brand].hasOwnProperty('features')
11661 11681 || !BRAND_OPTIONS[vmobj.brand].features.mdata_restart) {
11662 11682 log.debug('restarting mdata:fetch service not supported for brand '
11663 11683 + vmobj.brand);
11664 11684 callback();
11665 11685 return;
11666 11686 }
11667 11687
11668 11688 if (vmobj.state !== 'running' || !payload.hasOwnProperty('resolvers')
11669 11689 && !payload.hasOwnProperty('routes')
11670 11690 && !payload.hasOwnProperty('set_routes')
11671 11691 && !payload.hasOwnProperty('remove_routes')) {
11672 11692 callback();
11673 11693 return;
11674 11694 }
11675 11695
11676 11696 log.debug('restarting metadata service for: ' + vmobj.uuid);
11677 11697
11678 11698 args = [vmobj.zonename, '/usr/sbin/svcadm', 'restart',
11679 11699 'svc:/smartdc/mdata:fetch'];
11680 11700 log.debug('/usr/sbin/zlogin ' + args.join(' '));
11681 11701 execFile('/usr/sbin/zlogin', args, function (err, svc_stdout, svc_stderr) {
11682 11702 if (err) {
11683 11703 log.error({'err': err, 'stdout': svc_stdout,
11684 11704 'stderr': svc_stderr}, 'zlogin for ' + vmobj.zonename
11685 11705 + ' exited with code' + err.code + err.message);
11686 11706 // error here is not fatal as this should be fixed on reboot
11687 11707 }
11688 11708
11689 11709 callback();
11690 11710 });
11691 11711 }
11692 11712
11693 11713 function applyUpdates(oldobj, newobj, payload, log, callback)
11694 11714 {
11695 11715 var changed_datasets = false;
11696 11716
11697 11717 assert(log, 'no logger passed to applyUpdates()');
11698 11718
11699 11719 // Note: oldobj is the VM *before* the update, newobj *after*
11700 11720 log.debug('applying updates to ' + oldobj.uuid);
11701 11721
11702 11722 async.series([
11703 11723 function (cb) {
11704 11724 if (payload.hasOwnProperty('update_disks')
11705 11725 && oldobj.hasOwnProperty('disks')) {
11706 11726
11707 11727 resizeDisks(oldobj.disks, payload.update_disks, log,
11708 11728 function (err, resized) {
11709 11729 // If any were resized, mark that we changed something
11710 11730 if (!err && resized > 0) {
11711 11731 changed_datasets = true;
11712 11732 }
11713 11733 cb(err);
11714 11734 }
11715 11735 );
11716 11736 } else {
11717 11737 cb();
11718 11738 }
11719 11739 }, function (cb) {
11720 11740 if (payload.hasOwnProperty('quota')
11721 11741 && (Number(payload.quota) !== Number(oldobj.quota))) {
11722 11742
11723 11743 setQuota(newobj.zfs_filesystem, payload.quota, log,
11724 11744 function (err) {
11725 11745
11726 11746 if (!err) {
11727 11747 changed_datasets = true;
11728 11748 }
11729 11749 cb(err);
11730 11750 });
11731 11751 } else {
11732 11752 cb();
11733 11753 }
11734 11754 }, function (cb) {
11735 11755 // NOTE: we've already validated the value
11736 11756 if (payload.hasOwnProperty('zfs_root_recsize')
11737 11757 && (payload.zfs_root_recsize !== oldobj.zfs_root_recsize)) {
11738 11758
11739 11759 zfs(['set', 'recsize=' + payload.zfs_root_recsize,
11740 11760 newobj.zfs_filesystem], log, function (err, fds) {
11741 11761
11742 11762 if (err) {
11743 11763 log.error(err, 'failed to apply zfs_root_recsize: '
11744 11764 + fds.stderr);
11745 11765 cb(new Error(rtrim(fds.stderr)));
11746 11766 } else {
11747 11767 cb();
11748 11768 }
11749 11769 });
11750 11770 } else {
11751 11771 cb();
11752 11772 }
11753 11773 }, function (cb) {
11754 11774 // NOTE: we've already validated the value.
11755 11775 if (payload.hasOwnProperty('zfs_data_recsize')
11756 11776 && oldobj.hasOwnProperty('zfs_data_recsize')
11757 11777 && newobj.hasOwnProperty('datasets')
11758 11778 && (newobj.datasets.indexOf(newobj.zfs_filesystem
11759 11779 + '/data') !== -1)) {
11760 11780
11761 11781 zfs(['set', 'recsize=' + payload.zfs_data_recsize,
11762 11782 newobj.zfs_filesystem + '/data'], log, function (err, fds) {
11763 11783
11764 11784 if (err) {
11765 11785 log.error(err, 'failed to apply zfs_data_recsize: '
11766 11786 + fds.stderr);
11767 11787 cb(new Error(rtrim(fds.stderr)));
11768 11788 } else {
11769 11789 cb();
11770 11790 }
11771 11791 });
11772 11792 } else {
11773 11793 cb();
11774 11794 }
11775 11795 }, function (cb) {
11776 11796 // NOTE: we've already validated the value
11777 11797 if (payload.hasOwnProperty('zfs_root_compression')
11778 11798 && (payload.zfs_root_compression !==
11779 11799 oldobj.zfs_root_compression)) {
11780 11800
11781 11801 zfs(['set', 'compression=' + payload.zfs_root_compression,
11782 11802 newobj.zfs_filesystem], log, function (err, fds) {
11783 11803
11784 11804 if (err) {
11785 11805 log.error(err, 'failed to apply '
11786 11806 + 'zfs_root_compression: ' + fds.stderr);
11787 11807 cb(new Error(rtrim(fds.stderr)));
11788 11808 } else {
11789 11809 cb();
11790 11810 }
11791 11811 });
11792 11812 } else {
11793 11813 cb();
11794 11814 }
11795 11815 }, function (cb) {
11796 11816 // NOTE: we've already validated the value
11797 11817 if (payload.hasOwnProperty('zfs_data_compression')
11798 11818 && newobj.hasOwnProperty('datasets')
11799 11819 && (newobj.datasets.indexOf(newobj.zfs_filesystem
11800 11820 + '/data') !== -1)) {
11801 11821
11802 11822 zfs(['set', 'compression=' + payload.zfs_data_compression,
11803 11823 newobj.zfs_filesystem + '/data'], log, function (err, fds) {
11804 11824
11805 11825 if (err) {
11806 11826 log.error(err, 'failed to apply '
11807 11827 + 'zfs_data_compression: ' + fds.stderr);
11808 11828 cb(new Error(rtrim(fds.stderr)));
11809 11829 } else {
11810 11830 cb();
11811 11831 }
11812 11832 });
11813 11833 } else {
11814 11834 cb();
11815 11835 }
11816 11836 }, function (cb) {
11817 11837 var d;
11818 11838 var disk;
11819 11839 var zfs_updates = [];
11820 11840
11821 11841 if (payload.hasOwnProperty('update_disks')) {
11822 11842 // loop through the disks we updated and perform any updates.
11823 11843 for (disk in payload.update_disks) {
11824 11844 disk = payload.update_disks[disk];
11825 11845
11826 11846 if (!disk) {
11827 11847 continue;
11828 11848 }
11829 11849
11830 11850 for (d in oldobj.disks) {
11831 11851 d = oldobj.disks[d];
11832 11852 if (d.path === disk.path
11833 11853 && d.hasOwnProperty('zfs_filesystem')) {
11834 11854
11835 11855 if (disk.hasOwnProperty('compression')) {
11836 11856 zfs_updates.push({
11837 11857 zfs_filesystem: d.zfs_filesystem,
11838 11858 property: 'compression',
11839 11859 value: disk.compression
11840 11860 });
11841 11861 }
11842 11862
11843 11863 if (disk.hasOwnProperty('refreservation')) {
11844 11864 zfs_updates.push({
11845 11865 zfs_filesystem: d.zfs_filesystem,
11846 11866 property: 'refreservation',
11847 11867 value: disk.refreservation + 'M'
11848 11868 });
11849 11869 }
11850 11870 }
11851 11871 }
11852 11872 }
11853 11873 if (zfs_updates.length > 0) {
11854 11874 log.debug('applying ' + zfs_updates.length
11855 11875 + ' zfs updates');
11856 11876 async.each(zfs_updates, function (props, f_cb) {
11857 11877 zfs(['set', props.property + '=' + props.value,
11858 11878 props.zfs_filesystem], log, function (err, fds) {
11859 11879
11860 11880 if (err) {
11861 11881 log.error(err, 'failed to set ' + props.property
11862 11882 + '=' + props.value + ' for '
11863 11883 + props.zfs_filesystem);
11864 11884 }
11865 11885 f_cb(err);
11866 11886 });
11867 11887 }, function (err) {
11868 11888 log.debug({err: err}, 'end of zfs updates');
11869 11889 cb(err);
11870 11890 });
11871 11891 } else {
11872 11892 log.debug('no zfs updates to apply');
11873 11893 cb();
11874 11894 }
11875 11895 } else {
11876 11896 cb();
11877 11897 }
11878 11898 }, function (cb) {
11879 11899 var factor;
11880 11900 var keys = [];
11881 11901 var rctl;
11882 11902 var rctls = {
11883 11903 'cpu_shares': ['zone.cpu-shares'],
11884 11904 'zfs_io_priority': ['zone.zfs-io-priority'],
11885 11905 'max_lwps': ['zone.max-lwps'],
11886 11906 'max_physical_memory': ['zone.max-physical-memory',
11887 11907 (1024 * 1024)],
11888 11908 'max_locked_memory': ['zone.max-locked-memory', (1024 * 1024)],
11889 11909 'max_swap': ['zone.max-swap', (1024 * 1024)],
11890 11910 'cpu_cap': ['zone.cpu-cap']
11891 11911 };
11892 11912
11893 11913 if (!BRAND_OPTIONS[oldobj.brand].features.update_rctls) {
11894 11914 cb();
11895 11915 return;
11896 11916 }
11897 11917
11898 11918 for (rctl in rctls) {
11899 11919 keys.push(rctl);
11900 11920 }
11901 11921
11902 11922 async.forEachSeries(keys, function (prop, c) {
11903 11923 rctl = rctls[prop][0];
11904 11924 if (rctls[prop][1]) {
11905 11925 factor = rctls[prop][1];
11906 11926 } else {
11907 11927 factor = 1;
11908 11928 }
11909 11929
11910 11930 if (payload.hasOwnProperty(prop)) {
11911 11931 setRctl(newobj.zonename, rctl,
11912 11932 Number(payload[prop]) * factor, log,
11913 11933 function (err) {
11914 11934 if (err) {
11915 11935 log.warn(err, 'failed to set rctl: ' + prop);
11916 11936 }
11917 11937 c();
11918 11938 }
11919 11939 );
11920 11940 } else {
11921 11941 c();
11922 11942 }
11923 11943 }, function (err) {
11924 11944 cb(err);
11925 11945 });
11926 11946 }, function (cb) {
11927 11947 if ((payload.hasOwnProperty('vnc_password')
11928 11948 && (oldobj.vnc_password !== newobj.vnc_password))
11929 11949 || (payload.hasOwnProperty('vnc_port')
11930 11950 && (oldobj.vnc_port !== newobj.vnc_port))) {
11931 11951
11932 11952 // tell vmadmd to refresh_password and port (will restart
11933 11953 // listener)
11934 11954 postVmadmd(newobj.uuid, 'reload_display', {}, log,
11935 11955 function (e) {
11936 11956
11937 11957 if (e) {
11938 11958 cb(new Error('Unable to tell vmadmd to reload VNC: '
11939 11959 + e.message));
11940 11960 } else {
11941 11961 cb();
11942 11962 }
11943 11963 });
11944 11964 } else if ((payload.hasOwnProperty('spice_password')
11945 11965 && (oldobj.spice_password !== newobj.spice_password))
11946 11966 || (payload.hasOwnProperty('spice_port')
11947 11967 && (oldobj.spice_port !== newobj.spice_port))) {
11948 11968
11949 11969 // tell vmadmd to refresh_password and port (will restart
11950 11970 // listener)
11951 11971 postVmadmd(newobj.uuid, 'reload_display', {}, log,
11952 11972 function (e) {
11953 11973
11954 11974 if (e) {
11955 11975 cb(new Error('Unable to tell vmadmd to reload SPICE: '
11956 11976 + e.message));
11957 11977 } else {
11958 11978 cb();
11959 11979 }
11960 11980 });
11961 11981 } else {
11962 11982 cb();
11963 11983 }
11964 11984 }, function (cb) {
11965 11985 // we do this last, since we need the memory in the zone updated
11966 11986 // first if we're growing this.
11967 11987 if (payload.hasOwnProperty('tmpfs')) {
11968 11988 resizeTmp(newobj.zonename, payload.tmpfs, log, cb);
11969 11989 } else {
11970 11990 cb();
11971 11991 }
11972 11992 }, function (cb) {
11973 11993 var now = new Date();
11974 11994
11975 11995 // If we changed any dataset properties, we touch the zone's xml
11976 11996 // file so that last_modified is correct.
11977 11997 if (changed_datasets && newobj.hasOwnProperty('zonename')) {
11978 11998 fs.utimes('/etc/zones/' + newobj.zonename + '.xml', now, now,
11979 11999 function (err) {
11980 12000 if (err) {
11981 12001 log.warn(err, 'Unable to "touch" xml file for "'
11982 12002 + newobj.zonename + '": ' + err.message);
11983 12003 } else {
11984 12004 log.debug('Touched ' + newobj.zonename
11985 12005 + '.xml after datasets were modified.');
11986 12006 }
11987 12007 // We don't error out if we just couldn't touch because
11988 12008 // the actual updates above already did happen.
11989 12009 cb();
11990 12010 }
11991 12011 );
11992 12012 } else {
11993 12013 cb();
11994 12014 }
11995 12015 }
11996 12016
11997 12017 ], function (err, res) {
↓ open down ↓ |
7652 lines elided |
↑ open up ↑ |
11998 12018 log.debug('done applying updates to ' + oldobj.uuid);
11999 12019 callback(err);
12000 12020 });
12001 12021 }
12002 12022
12003 12023 exports.update = function (uuid, payload, options, callback)
12004 12024 {
12005 12025 var log;
12006 12026 var new_vmobj;
12007 12027 var vmobj;
12028 + var unlock;
12029 + var lockpath;
12008 12030
12009 12031 // options parameter is optional
12010 12032 if (arguments.length === 3) {
12011 12033 callback = arguments[2];
12012 12034 options = {};
12013 12035 }
12014 12036
12015 12037 ensureLogging(true);
12016 12038 if (options.hasOwnProperty('log')) {
12017 12039 log = options.log;
12018 12040 } else {
12019 12041 log = VM.log.child({action: 'update', vm: uuid});
12020 12042 }
12021 12043
12022 12044 log.info('Updating VM ' + uuid + ' with initial payload:\n'
12023 12045 + JSON.stringify(payload, null, 2));
12024 12046
12025 12047 async.series([
12026 12048 function (cb) {
12049 + lockpath = '/var/run/vm.' + uuid + '.config.lockfile';
12050 + log.debug('acquiring lock on ' + lockpath);
12051 + lock(lockpath, function (err, _unlock) {
12052 + log.debug('acquired lock on ' + lockpath);
12053 + if (err) {
12054 + cb(err);
12055 + return;
12056 + }
12057 + unlock = _unlock;
12058 + cb();
12059 + });
12060 + },
12061 + function (cb) {
12027 12062 // for update we currently always load the whole vmobj since the
12028 12063 // update functions may need to look at bits from the existing VM.
12029 12064 VM.load(uuid, {log: log}, function (err, obj) {
12030 12065 if (err) {
12031 12066 cb(err);
12032 12067 return;
12033 12068 }
12034 12069 vmobj = obj;
12035 12070 cb();
12036 12071 });
12037 12072 }, function (cb) {
12038 12073 normalizePayload(payload, vmobj, log, function (e) {
12039 12074 log.debug('Used payload:\n'
12040 12075 + JSON.stringify(payload, null, 2));
12041 12076 cb(e);
12042 12077 });
12043 12078 }, function (cb) {
12044 12079 var deletables = [];
12045 12080 var to_remove = [];
12046 12081 var n;
12047 12082
12048 12083 // destroy remove_disks before we add in case we're recreating with
12049 12084 // an existing name.
12050 12085
12051 12086 if (payload.hasOwnProperty('remove_disks')) {
12052 12087 to_remove = payload.remove_disks;
12053 12088 for (n in vmobj.disks) {
12054 12089 if (to_remove.indexOf(vmobj.disks[n].path) !== -1) {
12055 12090 deletables.push(vmobj.disks[n]);
12056 12091 }
12057 12092 }
12058 12093 } else {
12059 12094 // no disks to remove so all done.
12060 12095 cb();
12061 12096 return;
12062 12097 }
12063 12098
12064 12099 function loggedDeleteVolume(volume, callbk) {
12065 12100 return deleteVolume(volume, log, callbk);
12066 12101 }
12067 12102
12068 12103 async.forEachSeries(deletables, loggedDeleteVolume,
12069 12104 function (err) {
12070 12105 if (err) {
12071 12106 log.error(err, 'Unknown error deleting volumes: '
12072 12107 + err.message);
12073 12108 cb(err);
12074 12109 } else {
12075 12110 log.info('successfully deleted volumes');
12076 12111 cb();
12077 12112 }
12078 12113 }
12079 12114 );
12080 12115 }, function (cb) {
12081 12116 var disks = [];
12082 12117 var matches;
12083 12118 var n;
12084 12119 var p;
12085 12120 var used_disk_indexes = [];
12086 12121
12087 12122 // create any new volumes we need.
12088 12123 if (payload.hasOwnProperty('add_disks')) {
12089 12124 disks = payload.add_disks;
12090 12125 }
12091 12126
12092 12127 // create a list of used indexes so we can find the free ones to
12093 12128 // use in createVolume()
12094 12129 if (vmobj.hasOwnProperty('disks')) {
12095 12130 for (n in vmobj.disks) {
12096 12131 matches = vmobj.disks[n].path.match(/^.*-disk(\d+)$/);
12097 12132 if (matches) {
12098 12133 used_disk_indexes.push(Number(matches[1]));
12099 12134 }
12100 12135 }
12101 12136 }
12102 12137
12103 12138 // add the bits of payload createVolumes() needs.
12104 12139 p = {'add_disks': disks};
12105 12140 p.uuid = uuid;
12106 12141 if (vmobj.hasOwnProperty('zpool')) {
12107 12142 p.zpool = vmobj.zpool;
12108 12143 }
12109 12144 p.used_disk_indexes = used_disk_indexes;
12110 12145 createVolumes(p, log, function (e) {
12111 12146 cb(e);
12112 12147 });
12113 12148 }, function (cb) {
12114 12149 updateMetadata(vmobj, payload, log, function (e) {
12115 12150 cb(e);
12116 12151 });
12117 12152 }, function (cb) {
12118 12153 updateRoutes(vmobj, payload, log, function (e) {
12119 12154 cb(e);
12120 12155 });
12121 12156 }, function (cb) {
12122 12157 var zcfg;
12123 12158 // generate a payload and send as a file to zonecfg to update
12124 12159 // the zone.
12125 12160 zcfg = buildZonecfgUpdate(vmobj, payload, log);
12126 12161 zonecfgFile(zcfg, ['-u', uuid], log, function (e, fds) {
12127 12162 if (e) {
12128 12163 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr},
12129 12164 'unable to update zonecfg');
12130 12165 } else {
12131 12166 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12132 12167 'updated zonecfg');
12133 12168 }
12134 12169 cb(e);
12135 12170 });
12136 12171 }, function (cb) {
12137 12172 restartMetadataService(vmobj, payload, log, function (e) {
12138 12173 cb(e);
12139 12174 });
12140 12175 }, function (cb) {
12141 12176 updateVnicProperties(uuid, vmobj, payload, log, function (e) {
12142 12177 cb(e);
12143 12178 });
12144 12179 }, function (cb) {
12145 12180 // Update the firewall data
12146 12181 updateFirewallData(payload, vmobj, log, cb);
12147 12182 }, function (cb) {
12148 12183 // Do another full reload (all fields) so we can compare in
12149 12184 // applyUpdates() and decide what's changed that we need to apply.
12150 12185 VM.load(uuid, {log: log}, function (e, newobj) {
12151 12186 if (e) {
12152 12187 cb(e);
12153 12188 } else {
↓ open down ↓ |
117 lines elided |
↑ open up ↑ |
12154 12189 new_vmobj = newobj;
12155 12190 cb();
12156 12191 }
12157 12192 });
12158 12193 }, function (cb) {
12159 12194 applyUpdates(vmobj, new_vmobj, payload, log, function () {
12160 12195 cb();
12161 12196 });
12162 12197 }
12163 12198 ], function (e) {
12164 - callback(e);
12199 + // If we were able to hold the lockfile, and thus have an unlock
12200 + // callback, we must call it before returning, whether or not
12201 + // there was an error.
12202 + if (unlock) {
12203 + log.debug('releasing lock on ' + lockpath);
12204 + unlock(function (unlock_err) {
12205 + if (unlock_err) {
12206 + log.error(err, 'unlock error! (path ' + lockpath + ')');
12207 + } else {
12208 + log.debug('released lock on ' + lockpath);
12209 + }
12210 + callback(e);
12211 + });
12212 + } else {
12213 + callback(e);
12214 + }
12165 12215 });
12166 12216 };
12167 12217
12168 12218 function kill(uuid, log, callback)
12169 12219 {
12170 12220 var load_fields;
12171 12221 var unset_autoboot = 'set autoboot=false';
12172 12222
12173 12223 assert(log, 'no logger passed to kill()');
12174 12224
12175 12225 log.info('Killing VM ' + uuid);
12176 12226
12177 12227 load_fields = [
12178 12228 'brand',
12179 12229 'state',
12180 12230 'transition_to',
12181 12231 'uuid'
12182 12232 ];
12183 12233
12184 12234 /* We load here to ensure this vm exists. */
12185 12235 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12186 12236 if (err) {
12187 12237 callback(err);
12188 12238 return;
12189 12239 }
12190 12240
12191 12241 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12192 12242 unset_autoboot =
12193 12243 'select attr name=vm-autoboot; set value=false; end';
12194 12244 }
12195 12245
12196 12246 zoneadm(['-u', uuid, 'halt', '-X'], log, function (e, fds) {
12197 12247 var msg = trim(fds.stderr);
12198 12248
12199 12249 if (msg.match(/zone is already halted$/)) {
12200 12250 // remove transition marker, since vm is not running now.
12201 12251 VM.unsetTransition(vmobj, {log: log}, function () {
12202 12252 var new_err;
12203 12253
12204 12254 new_err = new Error('VM ' + vmobj.uuid + ' is already '
12205 12255 + 'not \'running\' (currently: ' + vmobj.state + ')');
12206 12256 new_err.code = 'ENOTRUNNING';
12207 12257 callback(new_err);
12208 12258 });
12209 12259 } else if (e) {
12210 12260 log.error({err: e, stdout: fds.stdout, stderr: fds.stderr},
12211 12261 'failed to halt VM ' + uuid);
12212 12262 callback(err, msg);
12213 12263 } else {
12214 12264 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12215 12265 'zoneadm halted VM ' + uuid);
12216 12266 zonecfg(['-u', uuid, unset_autoboot], log,
12217 12267 function (error, unset_fds) {
12218 12268
12219 12269 if (error) {
12220 12270 // The vm is dead at this point, erroring out here would
12221 12271 // do no good, so we just log it.
12222 12272 log.error({err: error, stdout: unset_fds.stdout,
12223 12273 stderr: unset_fds.stderr}, 'killVM(): Failed to '
12224 12274 + unset_autoboot);
12225 12275 } else {
12226 12276 log.debug({stdout: unset_fds.stdout,
12227 12277 stderr: unset_fds.stderr}, 'unset autoboot flag');
12228 12278 }
12229 12279 if (vmobj.state === 'stopping') {
12230 12280 // remove transition marker
12231 12281 VM.unsetTransition(vmobj, {log: log}, function () {
12232 12282 callback(null, msg);
12233 12283 });
12234 12284 } else {
12235 12285 callback(null, msg);
12236 12286 }
12237 12287 });
12238 12288 }
12239 12289 });
12240 12290 });
12241 12291 }
12242 12292
12243 12293 function postVmadmd(uuid, action, args, log, callback)
12244 12294 {
12245 12295 var arg;
12246 12296 var url_path = '/vm/' + uuid + '?action=' + action;
12247 12297 var req;
12248 12298
12249 12299 assert(log, 'no logger passed to postVmadmd()');
12250 12300
12251 12301 if (args) {
12252 12302 for (arg in args) {
12253 12303 if (args.hasOwnProperty(arg)) {
12254 12304 url_path = url_path + '&' + arg + '=' + args[arg];
12255 12305 }
12256 12306 }
12257 12307 }
12258 12308
12259 12309 log.debug('HTTP POST ' + url_path);
12260 12310 req = http.request(
12261 12311 { method: 'POST', host: '127.0.0.1', port: '8080', path: url_path },
12262 12312 function (res) {
12263 12313
12264 12314 log.debug('HTTP STATUS: ' + res.statusCode);
12265 12315 log.debug('HTTP HEADERS: ' + JSON.stringify(res.headers));
12266 12316 res.setEncoding('utf8');
12267 12317 res.on('data', function (chunk) {
12268 12318 log.debug('HTTP BODY: ' + chunk);
12269 12319 });
12270 12320 res.on('end', function () {
12271 12321 log.debug('HTTP conversation has completed.');
12272 12322 callback();
12273 12323 });
12274 12324 }
12275 12325 );
12276 12326 req.on('error', function (e) {
12277 12327 log.error(e, 'HTTP error: ' + e.message);
12278 12328 callback(e);
12279 12329 });
12280 12330 req.end();
12281 12331 }
12282 12332
12283 12333 // options parameter is *REQUIRED* for VM.stop()
12284 12334 exports.stop = function (uuid, options, callback)
12285 12335 {
12286 12336 var load_fields;
12287 12337 var log;
12288 12338 var unset_autoboot = 'set autoboot=false';
12289 12339 var vmobj;
12290 12340
12291 12341 load_fields = [
12292 12342 'brand',
12293 12343 'state',
12294 12344 'uuid',
12295 12345 'zonename'
12296 12346 ];
12297 12347
12298 12348 if (!options) {
12299 12349 options = {};
12300 12350 }
12301 12351
12302 12352 if (options.hasOwnProperty('force') && options.force) {
12303 12353 ensureLogging(true);
12304 12354 if (options.hasOwnProperty('log')) {
12305 12355 log = options.log;
12306 12356 } else {
12307 12357 log = VM.log.child({action: 'stop-F', vm: uuid});
12308 12358 }
12309 12359 kill(uuid, log, callback);
12310 12360 return;
12311 12361 } else {
12312 12362 ensureLogging(true);
12313 12363 if (options.hasOwnProperty('log')) {
12314 12364 log = options.log;
12315 12365 } else {
12316 12366 log = VM.log.child({action: 'stop', vm: uuid});
12317 12367 }
12318 12368 }
12319 12369
12320 12370 log.info('Stopping VM ' + uuid);
12321 12371
12322 12372 if (!options.timeout) {
12323 12373 options.timeout = 180;
12324 12374 }
12325 12375 if (!options.transition_to) {
12326 12376 options.transition_to = 'stopped';
12327 12377 }
12328 12378
12329 12379 async.series([
12330 12380 function (cb) {
12331 12381 /* We load here to ensure this vm exists. */
12332 12382 VM.load(uuid, {log: log, fields: load_fields}, function (err, obj) {
12333 12383 var new_err;
12334 12384
12335 12385 if (err) {
12336 12386 log.error(err);
12337 12387 cb(err);
12338 12388 return;
12339 12389 } else {
12340 12390 vmobj = obj;
12341 12391 if (vmobj.state !== 'running') {
12342 12392 new_err = new Error('VM ' + vmobj.uuid + ' is already '
12343 12393 + 'not \'running\' (currently: ' + vmobj.state
12344 12394 + ')');
12345 12395 new_err.code = 'ENOTRUNNING';
12346 12396 cb(new_err);
12347 12397 } else {
12348 12398 cb();
12349 12399 }
12350 12400 }
12351 12401 });
12352 12402 }, function (cb) {
12353 12403 // When stopping a VM that uses vm_autoboot, we assume we also do
12354 12404 // the stop through vmadmd.
12355 12405 if (BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12356 12406 async.series([
12357 12407 function (callbk) {
12358 12408 setTransition(vmobj, 'stopping', options.transition_to,
12359 12409 (options.timeout * 1000), log, function (err) {
12360 12410
12361 12411 callbk(err);
12362 12412 });
12363 12413 }, function (callbk) {
12364 12414 postVmadmd(vmobj.uuid, 'stop',
12365 12415 {'timeout': options.timeout}, log, function (err) {
12366 12416
12367 12417 if (err) {
12368 12418 log.error(err);
12369 12419 err.message = 'Unable to post "stop" to vmadmd:'
12370 12420 + ' ' + err.message;
12371 12421 }
12372 12422 callbk(err);
12373 12423 });
12374 12424 }, function (callbk) {
12375 12425
12376 12426 // different version for VMs
12377 12427 unset_autoboot = 'select attr name=vm-autoboot; '
12378 12428 + 'set value=false; end';
12379 12429
12380 12430 zonecfg(['-u', uuid, unset_autoboot], log,
12381 12431 function (err, fds) {
12382 12432 if (err) {
12383 12433 // The vm is dead at this point, failing
12384 12434 // here would do no good, so we just log it.
12385 12435 log.error({err: err, stdout: fds.stdout,
12386 12436 stderr: fds.stderr}, 'stop(): Failed to'
12387 12437 + ' ' + unset_autoboot + ' for ' + uuid
12388 12438 + ': ' + err.message);
12389 12439 } else {
12390 12440 log.info({stdout: fds.stdout,
12391 12441 stderr: fds.stderr}, 'Stopped ' + uuid);
12392 12442 }
12393 12443 callbk();
12394 12444 }
12395 12445 );
12396 12446 }
12397 12447 ], function (err) {
12398 12448 cb(err);
12399 12449 });
12400 12450 } else { // no vm_autoboot / vmadmd stop
12401 12451 cb();
12402 12452 }
12403 12453 }, function (cb) {
12404 12454 var args;
12405 12455
12406 12456 // joyent brand specific stuff
12407 12457 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0',
12408 12458 '-i', '5'];
12409 12459
12410 12460 // not using vm_autoboot means using the 'normal' boot process
12411 12461 if (!BRAND_OPTIONS[vmobj.brand].features.use_vm_autoboot) {
12412 12462 async.series([
12413 12463 function (callbk) {
12414 12464 log.debug('/usr/sbin/zlogin ' + args.join(' '));
12415 12465 execFile('/usr/sbin/zlogin', args,
12416 12466 function (err, stdout, stderr) {
12417 12467
12418 12468 if (err) {
12419 12469 log.error({'err': err, 'stdout': stdout,
12420 12470 'stderr': stderr}, 'zlogin for '
12421 12471 + vmobj.zonename + ' exited with code'
12422 12472 + err.code + ': ' + err.message);
12423 12473 callbk(err);
12424 12474 } else {
12425 12475 log.debug('zlogin claims to have worked, '
12426 12476 + 'stdout:\n' + stdout + '\nstderr:\n'
12427 12477 + stderr);
12428 12478 callbk();
12429 12479 }
12430 12480 });
12431 12481 }, function (callbk) {
12432 12482 zonecfg(['-u', uuid, unset_autoboot], log,
12433 12483 function (err, fds) {
12434 12484 if (err) {
12435 12485 // The vm is dead at this point, failing
12436 12486 // do no good, so we just log it.
12437 12487 log.warn({err: err, stdout: fds.stdout,
12438 12488 stderr: fds.stderr}, 'Failed to '
12439 12489 + unset_autoboot + ' for ' + uuid);
12440 12490 } else {
12441 12491 log.info({stdout: fds.stdout,
12442 12492 stderr: fds.stderr}, 'Stopped ' + uuid);
12443 12493 }
12444 12494 callbk();
12445 12495 }
12446 12496 );
12447 12497 }
12448 12498 ], function (err) {
12449 12499 cb(err);
12450 12500 });
12451 12501 } else { // using vmautoboot so won't shutdown from in the zone
12452 12502 cb();
12453 12503 }
12454 12504 }, function (cb) {
12455 12505 // Verify it's shut down
12456 12506 VM.waitForZoneState(vmobj, 'installed', {log: log},
12457 12507 function (err, result) {
12458 12508
12459 12509 if (err) {
12460 12510 if (err.code === 'ETIMEOUT') {
12461 12511 log.info(err, 'timeout waiting for zone to go to '
12462 12512 + '"installed"');
12463 12513 } else {
12464 12514 log.error(err, 'unknown error waiting for zone to go'
12465 12515 + ' "installed"');
12466 12516 }
12467 12517 cb(err);
12468 12518 } else {
12469 12519 // zone got to stopped
12470 12520 log.info('VM seems to have switched to "installed"');
12471 12521 cb();
12472 12522 }
12473 12523 });
12474 12524 }
12475 12525 ], function (err) {
12476 12526 callback(err);
12477 12527 });
12478 12528 };
12479 12529
12480 12530 // sends several query-* commands to QMP to get details for a VM
12481 12531 exports.info = function (uuid, types, options, callback)
12482 12532 {
12483 12533 var load_fields;
12484 12534 var log;
12485 12535
12486 12536 // options is optional
12487 12537 if (arguments.length === 3) {
12488 12538 callback = arguments[2];
12489 12539 options = {};
12490 12540 }
12491 12541
12492 12542 ensureLogging(false);
12493 12543 if (options.hasOwnProperty('log')) {
12494 12544 log = options.log;
12495 12545 } else {
12496 12546 log = VM.log.child({action: 'info', vm: uuid});
12497 12547 }
12498 12548
12499 12549 load_fields = [
12500 12550 'brand',
12501 12551 'state',
12502 12552 'uuid'
12503 12553 ];
12504 12554
12505 12555 // load to ensure we're a VM
12506 12556 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12507 12557 var type;
12508 12558
12509 12559 if (err) {
12510 12560 callback(err);
12511 12561 return;
12512 12562 }
12513 12563
12514 12564 if (!BRAND_OPTIONS[vmobj.brand].features.runtime_info) {
12515 12565 // XXX if support is added to other brands, update this message.
12516 12566 callback(new Error('the info command is only supported for KVM '
12517 12567 + 'VMs'));
12518 12568 return;
12519 12569 }
12520 12570
12521 12571 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') {
12522 12572 callback(new Error('Unable to get info for vm from state "'
12523 12573 + vmobj.state + '", must be "running" or "stopping".'));
12524 12574 return;
12525 12575 }
12526 12576
12527 12577 if (!types) {
12528 12578 types = ['all'];
12529 12579 }
12530 12580
12531 12581 for (type in types) {
12532 12582 type = types[type];
12533 12583 if (VM.INFO_TYPES.indexOf(type) === -1) {
12534 12584 callback(new Error('unknown info type: ' + type));
12535 12585 return;
12536 12586 }
12537 12587 }
12538 12588
12539 12589 http.get({ host: '127.0.0.1', port: 8080, path: '/vm/' + uuid + '/info'
12540 12590 + '?types=' + types.join(',') }, function (res) {
12541 12591
12542 12592 var data = '';
12543 12593
12544 12594 if (res.statusCode !== 200) {
12545 12595 callback(new Error('Unable to get info from vmadmd, query '
12546 12596 + 'returned ' + res.statusCode + '.'));
12547 12597 } else {
12548 12598 res.on('data', function (d) {
12549 12599 data = data + d.toString();
12550 12600 });
12551 12601 res.on('end', function (d) {
12552 12602 callback(null, JSON.parse(data));
12553 12603 });
12554 12604 }
12555 12605 }
12556 12606 ).on('error', function (e) {
12557 12607 log.error(e);
12558 12608 callback(e);
12559 12609 });
12560 12610 });
12561 12611 };
12562 12612
12563 12613 function reset(uuid, log, callback)
12564 12614 {
12565 12615 var load_fields;
12566 12616
12567 12617 assert(log, 'no logger passed to reset()');
12568 12618
12569 12619 log.info('Resetting VM ' + uuid);
12570 12620
12571 12621 load_fields = [
12572 12622 'brand',
12573 12623 'state',
12574 12624 'uuid'
12575 12625 ];
12576 12626
12577 12627 /* We load here to ensure this vm exists. */
12578 12628 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12579 12629 if (err) {
12580 12630 callback(err);
12581 12631 return;
12582 12632 }
12583 12633
12584 12634 if (vmobj.state !== 'running') {
12585 12635 callback(new Error('Cannot reset vm from state "'
12586 12636 + vmobj.state + '", must be "running".'));
12587 12637 return;
12588 12638 }
12589 12639
12590 12640 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) {
12591 12641 postVmadmd(vmobj.uuid, 'reset', {}, log, function (e) {
12592 12642 if (e) {
12593 12643 callback(new Error('Unable to post "reset" to '
12594 12644 + 'vmadmd: ' + e.message));
12595 12645 } else {
12596 12646 callback();
12597 12647 }
12598 12648 });
12599 12649 } else {
12600 12650 zoneadm(['-u', vmobj.uuid, 'reboot', '-X'], log, function (e, fds) {
12601 12651 if (e) {
12602 12652 log.warn({err: e, stdout: fds.stdout, stderr: fds.stderr},
12603 12653 'zoneadm failed to reboot VM ' + vmobj.uuid);
12604 12654 callback(new Error(rtrim(fds.stderr)));
12605 12655 } else {
12606 12656 log.debug({stdout: fds.stdout, stderr: fds.stderr},
12607 12657 'zoneadm rebooted VM ' + vmobj.uuid);
12608 12658 callback();
12609 12659 }
12610 12660 });
12611 12661 }
12612 12662 });
12613 12663 }
12614 12664
12615 12665 // options is *REQUIRED* for VM.reboot()
12616 12666 exports.reboot = function (uuid, options, callback)
12617 12667 {
12618 12668 var cleanup;
12619 12669 var log;
12620 12670 var reboot_async = false;
12621 12671 var reboot_complete = false;
12622 12672 var vmobj;
12623 12673
12624 12674 if (options.hasOwnProperty('log')) {
12625 12675 log = options.log;
12626 12676 }
12627 12677
12628 12678 if (options.hasOwnProperty('force') && options.force) {
12629 12679 ensureLogging(true);
12630 12680 if (!log) {
12631 12681 log = VM.log.child({action: 'reboot-F', vm: uuid});
12632 12682 }
12633 12683 reset(uuid, log, callback);
12634 12684 return;
12635 12685 } else {
12636 12686 ensureLogging(true);
12637 12687 log = VM.log.child({action: 'reboot', vm: uuid});
12638 12688 }
12639 12689
12640 12690 log.info('Rebooting VM ' + uuid);
12641 12691
12642 12692 if (!options) {
12643 12693 options = {};
12644 12694 }
12645 12695
12646 12696 async.series([
12647 12697 function (cb) {
12648 12698 var load_fields = [
12649 12699 'brand',
12650 12700 'nics',
12651 12701 'state',
12652 12702 'zonename'
12653 12703 ];
12654 12704
12655 12705 VM.load(uuid, {fields: load_fields, log: log},
12656 12706 function (err, obj) {
12657 12707
12658 12708 if (err) {
12659 12709 cb(err);
12660 12710 return;
12661 12711 }
12662 12712
12663 12713 if (obj.state !== 'running') {
12664 12714 cb(new Error('Cannot reboot vm from state "' + obj.state
12665 12715 + '", must be "running"'));
12666 12716 return;
12667 12717 }
12668 12718
12669 12719 vmobj = obj;
12670 12720 cb();
12671 12721 });
12672 12722 }, function (cb) {
12673 12723 // If nic tags have disappeared out from under us, don't allow a
12674 12724 // reboot that will put us into a bad state
12675 12725 lookupInvalidNicTags(vmobj.nics, log, function (e) {
12676 12726 if (e) {
12677 12727 cb(new Error('Cannot reboot vm: ' + e.message));
12678 12728 return;
12679 12729 }
12680 12730
12681 12731 cb();
12682 12732 });
12683 12733
12684 12734 }, function (cb) {
12685 12735 var watcherobj;
12686 12736
12687 12737 if (!reboot_async) {
12688 12738 watcherobj = watchZoneTransitions(function (err, ze) {
12689 12739 if (!err && ze.zonename !== vmobj.zonename) {
12690 12740 // not something we need to handle
12691 12741 return;
12692 12742 }
12693 12743
12694 12744 if (err) {
12695 12745 // XXX what should we do here?
12696 12746 log.error(err);
12697 12747 return;
12698 12748 }
12699 12749
12700 12750 log.debug(ze); // TODO move to trace
12701 12751
12702 12752 if (ze.newstate === 'running'
12703 12753 && ze.oldstate !== 'running') {
12704 12754
12705 12755 if (watcherobj) {
12706 12756 // cleanup our watcher since we found what we're
12707 12757 // looking for.
12708 12758 cleanup();
12709 12759 }
12710 12760
12711 12761 reboot_complete = true;
12712 12762 }
12713 12763 }, log);
12714 12764 cleanup = watcherobj.cleanup;
12715 12765 }
12716 12766
12717 12767 cb();
12718 12768 }, function (cb) {
12719 12769 var args;
12720 12770
12721 12771 if (BRAND_OPTIONS[vmobj.brand].features.use_vmadmd) {
12722 12772 // here we stop the machine and set a transition so vmadmd will
12723 12773 // start the machine once the stop finished.
12724 12774 options.transition_to = 'start';
12725 12775 options.log = log;
12726 12776 VM.stop(uuid, options, function (err) {
12727 12777 if (err) {
12728 12778 cb(err);
12729 12779 } else {
12730 12780 cb();
12731 12781 }
12732 12782 });
12733 12783 } else {
12734 12784 // joyent branded zones
12735 12785 args = [vmobj.zonename, '/usr/sbin/shutdown', '-y', '-g', '0',
12736 12786 '-i', '6'];
12737 12787 log.debug('/usr/sbin/zlogin ' + args.join(' '));
12738 12788 execFile('/usr/sbin/zlogin', args,
12739 12789 function (err, stdout, stderr) {
12740 12790 if (err) {
12741 12791 log.error({'err': err, 'stdout': stdout,
12742 12792 'stderr': stderr}, 'zlogin for ' + vmobj.zonename
12743 12793 + ' exited with code' + err.code + ': '
12744 12794 + err.message);
12745 12795 cb(err);
12746 12796 } else {
12747 12797 cb();
12748 12798 }
12749 12799 });
12750 12800 }
12751 12801 }, function (cb) {
12752 12802 var ival;
12753 12803 var ticks = 0;
12754 12804
12755 12805 if (reboot_async) {
12756 12806 cb();
12757 12807 return;
12758 12808 } else {
12759 12809 ticks = 180 * 10; // (180 * 10) 100ms ticks = 3m
12760 12810 ival = setInterval(function () {
12761 12811 if (reboot_complete) {
12762 12812 log.debug('reboot marked complete, cleaning up');
12763 12813 clearInterval(ival);
12764 12814 cleanup();
12765 12815 cb();
12766 12816 return;
12767 12817 }
12768 12818 ticks--;
12769 12819 if (ticks <= 0) {
12770 12820 // timed out
12771 12821 log.debug('reboot timed out, cleaning up');
12772 12822 clearInterval(ival);
12773 12823 cleanup();
12774 12824 cb(new Error('timed out waiting for zone to reboot'));
12775 12825 return;
12776 12826 }
12777 12827 }, 100);
12778 12828 }
12779 12829 }
12780 12830 ], function (err) {
12781 12831 callback(err);
12782 12832 });
12783 12833 };
12784 12834
12785 12835 // options is *REQUIRED* for VM.sysrq
12786 12836 exports.sysrq = function (uuid, req, options, callback)
12787 12837 {
12788 12838 var load_fields;
12789 12839 var log;
12790 12840
12791 12841 ensureLogging(true);
12792 12842
12793 12843 if (options.hasOwnProperty('log')) {
12794 12844 log = options.log;
12795 12845 } else {
12796 12846 log = VM.log.child({action: 'sysrq-' + req, vm: uuid});
12797 12847 }
12798 12848
12799 12849 log.info('Sending sysrq "' + req + '" to ' + uuid);
12800 12850
12801 12851 load_fields = [
12802 12852 'brand',
12803 12853 'state',
12804 12854 'uuid'
12805 12855 ];
12806 12856
12807 12857 /* We load here to ensure this vm exists. */
12808 12858 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12809 12859 if (err) {
12810 12860 callback(err);
12811 12861 return;
12812 12862 }
12813 12863
12814 12864 if (vmobj.state !== 'running' && vmobj.state !== 'stopping') {
12815 12865 callback(new Error('Unable to send request to vm from state "'
12816 12866 + vmobj.state + '", must be "running" or "stopping".'));
12817 12867 return;
12818 12868 }
12819 12869
12820 12870 if (BRAND_OPTIONS[vmobj.brand].features.type !== 'KVM') {
12821 12871 callback(new Error('The sysrq command is only supported for KVM.'));
12822 12872 return;
12823 12873 }
12824 12874
12825 12875 if (VM.SYSRQ_TYPES.indexOf(req) === -1) {
12826 12876 callback(new Error('Invalid sysrq "' + req + '" valid values: '
12827 12877 + '"' + VM.SYSRQ_TYPES.join('","') + '".'));
12828 12878 return;
12829 12879 }
12830 12880
12831 12881 postVmadmd(vmobj.uuid, 'sysrq', {'request': req}, log, function (e) {
12832 12882 if (e) {
12833 12883 callback(new Error('Unable to post "sysrq" to vmadmd: '
12834 12884 + e.message));
12835 12885 } else {
12836 12886 callback();
12837 12887 }
12838 12888 });
12839 12889 });
12840 12890 };
12841 12891
12842 12892 exports.console = function (uuid, options, callback)
12843 12893 {
12844 12894 var load_fields;
12845 12895 var log;
12846 12896
12847 12897 // options is optional
12848 12898 if (arguments.length === 2) {
12849 12899 callback = arguments[1];
12850 12900 options = {};
12851 12901 }
12852 12902
12853 12903 ensureLogging(false);
12854 12904 if (options.hasOwnProperty('log')) {
12855 12905 log = options.log;
12856 12906 } else {
12857 12907 log = VM.log.child({action: 'console', vm: uuid});
12858 12908 }
12859 12909
12860 12910 load_fields = [
12861 12911 'brand',
12862 12912 'state',
12863 12913 'zonename',
12864 12914 'zonepath'
12865 12915 ];
12866 12916
12867 12917 VM.load(uuid, {fields: load_fields, log: log}, function (err, vmobj) {
12868 12918 var args;
12869 12919 var child;
12870 12920 var cmd;
12871 12921 var stty;
12872 12922
12873 12923 if (err) {
12874 12924 callback(err);
12875 12925 return;
12876 12926 }
12877 12927 if (vmobj.state !== 'running') {
12878 12928 callback(new Error('cannot connect to console when state is '
12879 12929 + '"' + vmobj.state + '" must be "running".'));
12880 12930 return;
12881 12931 }
12882 12932
12883 12933 if (BRAND_OPTIONS[vmobj.brand].features.zlogin_console) {
12884 12934 cmd = '/usr/sbin/zlogin';
12885 12935 args = ['-C', '-e', '\\035', vmobj.zonename];
12886 12936
12887 12937 log.debug(cmd + ' ' + args.join(' '));
12888 12938 child = spawn(cmd, args, {customFds: [0, 1, 2]});
12889 12939 child.on('close', function (code) {
12890 12940 log.debug('zlogin process exited with code ' + code);
12891 12941 callback();
12892 12942 });
12893 12943 } else if (BRAND_OPTIONS[vmobj.brand].features.serial_console) {
12894 12944 async.series([
12895 12945 function (cb) {
12896 12946 cmd = '/usr/bin/stty';
12897 12947 args = ['-g'];
12898 12948 stty = '';
12899 12949
12900 12950 log.debug(cmd + ' ' + args.join(' '));
12901 12951 child = spawn(cmd, args, {customFds: [0, -1, -1]});
12902 12952 child.stdout.on('data', function (data) {
12903 12953 // log.debug('data: ' + data.toString());
12904 12954 stty = data.toString();
12905 12955 });
12906 12956 child.on('close', function (code) {
12907 12957 log.debug('stty process exited with code ' + code);
12908 12958 cb();
12909 12959 });
12910 12960 }, function (cb) {
12911 12961 cmd = '/usr/bin/socat';
12912 12962 args = ['unix-client:' + vmobj.zonepath
12913 12963 + '/root/tmp/vm.console', '-,raw,echo=0,escape=0x1d'];
12914 12964
12915 12965 log.debug(cmd + ' ' + args.join(' '));
12916 12966 child = spawn(cmd, args, {customFds: [0, 1, 2]});
12917 12967 child.on('close', function (code) {
12918 12968 log.debug('zlogin process exited with code ' + code);
12919 12969 cb();
12920 12970 });
12921 12971 }, function (cb) {
12922 12972 cmd = '/usr/bin/stty';
12923 12973 args = [stty];
12924 12974
12925 12975 log.debug(cmd + ' ' + args.join(' '));
12926 12976 child = spawn(cmd, args, {customFds: [0, -1, -1]});
12927 12977 child.on('close', function (code) {
12928 12978 log.debug('stty process exited with code ' + code);
12929 12979 cb();
12930 12980 });
12931 12981 }
12932 12982 ], function (e, results) {
12933 12983 callback(e);
12934 12984 });
12935 12985 } else {
12936 12986 callback(new Error('Cannot get console for brand: ' + vmobj.brand));
12937 12987 }
12938 12988 });
12939 12989 };
↓ open down ↓ |
765 lines elided |
↑ open up ↑ |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX