1 var VM = require('/usr/vm/node_modules/VM');
2 var ZWatch = require('./zwatch');
3 var common = require('./common');
4 var async = require('/usr/node/node_modules/async');
5 var execFile = require('child_process').execFile;
6 var fs = require('fs');
7 var net = require('net');
8 var path = require('path');
9 var util = require('util');
10 var zsock = require('/usr/node/node_modules/zsock');
11 var zutil = require('/usr/node/node_modules/zutil');
12
13 var sdc_fields = [
14 'alias',
15 'billing_id',
16 'brand',
17 'cpu_cap',
18 'cpu_shares',
19 'create_timestamp',
20 'server_uuid',
21 'image_uuid',
22 'datacenter_name',
23 'do_not_inventory',
24 'dns_domain',
25 'force_metadata_socket',
26 'fs_allowed',
27 'hostname',
28 'limit_priv',
29 'last_modified',
30 'max_physical_memory',
31 'max_locked_memory',
32 'max_lwps',
33 'max_swap',
34 'nics',
35 'owner_uuid',
36 'package_name',
37 'package_version',
38 'quota',
39 'ram',
40 'resolvers',
41 'routes',
42 'state',
43 'tmpfs',
44 'uuid',
45 'vcpus',
46 'vnc_port',
47 'zfs_io_priority',
48 'zonepath',
49 'zonename'
50 ];
51
52 var MetadataAgent = module.exports = function (options) {
53 this.log = options.log;
54 this.zlog = {};
55 this.zones = {};
56 this.zoneConnections = {};
57 };
58
59 MetadataAgent.prototype.createZoneLog = function (type, zonename) {
60 var self = this;
61 self.zlog[zonename] = self.log.child({brand: type, 'zonename': zonename});
62 return (self.zlog[zonename]);
63 };
64
65 MetadataAgent.prototype.updateZone = function (zonename, callback) {
66 var self = this;
67 var log = self.log;
68
69 function shouldLoad(cb) {
70 if (!self.zones.hasOwnProperty(zonename)) {
71 // don't have a cache, load this guy
72 log.info('no cache for: ' + zonename + ', loading');
73 cb(true);
74 return;
75 }
76
77 // we do have a cached version, we'll reload only if timestamp changed.
78 fs.stat('/etc/zones/' + zonename + '.xml', function (err, stats) {
79 var old_mtime;
80
81 if (err) {
82 // fail open when we can't stat
83 log.error({err: err}, 'cannot fs.stat() ' + zonename);
84 cb(true);
85 return;
86 }
87
88 old_mtime = (new Date(self.zones[zonename].last_modified));
89 if (stats.mtime.getTime() > old_mtime.getTime()) {
90 log.info('last_modified was updated, reloading: ' + zonename);
91 cb(true);
92 return;
93 }
94
95 log.debug('using cache for: ' + zonename);
96 cb(false);
97 });
98 }
99
100 shouldLoad(function (load) {
101 if (load) {
102 VM.lookup({ zonename: zonename }, { fields: sdc_fields },
103 function (error, machines) {
104 self.zones[zonename] = machines[0];
105 callback();
106 return;
107 }
108 );
109 } else {
110 // no need to reload since there's no change, use existing data
111 callback();
112 return;
113 }
114 });
115 };
116
117 MetadataAgent.prototype.createServersOnExistingZones = function () {
118 var self = this;
119
120 VM.lookup({}, { fields: sdc_fields }, function (error, zones) {
121 async.forEach(zones, function (zone, cb) {
122 if (zone.zonename === 'global') {
123 cb();
124 return;
125 }
126
127 self.zones[zone.zonename] = zone;
128
129 if (zone.state !== 'running') {
130 self.log.debug('skipping zone ' + zone.zonename + ' which has '
131 + 'non-running state: ' + zone.state);
132 cb();
133 return;
134 }
135
136 if (error) {
137 throw error;
138 }
139
140 if (!self.zlog[zone.zonename]) {
141 // create a logger specific to this VM
142 self.createZoneLog(zone.brand, zone.zonename);
143 }
144
145 if (zone.brand === 'kvm') {
146 self.startKVMSocketServer(zone.zonename, function (err) {
147 cb();
148 });
149 } else {
150 self.startZoneSocketServer(zone.zonename, function (err) {
151 cb();
152 });
153 }
154 }, function (err) {
155 self.log.info('Created zone metadata sockets on ' + zones.length
156 + ' zones');
157 });
158 });
159 };
160
161 MetadataAgent.prototype.startDeletedZonesPurger = function () {
162 var cmd = '/usr/sbin/zoneadm';
163 var self = this;
164
165 // Every 5 minutes we check to see whether zones we've got in self.zones
166 // were deleted. If they are, we delete the record from the cache.
167 setInterval(function () {
168 execFile(cmd, ['list', '-c'], function (err, stdout, stderr) {
169 var zones = {};
170 if (err) {
171 self.log.error({err: err}, 'unable to get list of zones');
172 return;
173 }
174
175 // each output line is a zonename, so we turn this into an object
176 // that looks like:
177 //
178 // {
179 // zonename: true,
180 // zonename: true
181 // ...
182 // }
183 //
184 // so we can then loop through all the cached zonenames and remove
185 // those that don't exist on the system any longer.
186 stdout.split(/\n/).forEach(function (z) {
187 zones[z] = true;
188 });
189 Object.keys(self.zones).forEach(function (z) {
190 if (!zones.hasOwnProperty(z)) {
191 self.log.info(z + ' no longer exists, purging from cache');
192 delete self.zones[z];
193 if (self.zlog.hasOwnProperty(z)) {
194 delete self.zlog[z];
195 }
196 }
197 });
198 });
199 }, (5 * 60 * 1000));
200
201 self.log.info('Setup interval to purge deleted zones.');
202 };
203
204 MetadataAgent.prototype.start = function () {
205 var self = this;
206 var zwatch = this.zwatch = new ZWatch();
207 self.createServersOnExistingZones();
208 self.startDeletedZonesPurger();
209
210 zwatch.on('zone_transition', function (msg) {
211 if (msg.cmd === 'start') {
212 self.updateZone(msg.zonename, function (error) {
213 if (error) {
214 self.log.error({err: error}, 'Error updating attributes: '
215 + error.message);
216 return;
217 }
218 if (!self.zlog[msg.zonename]) {
219 // create a logger specific to this VM
220 self.createZoneLog(self.zones[msg.zonename].brand,
221 msg.zonename);
222 }
223 if (self.zones[msg.zonename].brand === 'kvm') {
224 self.startKVMSocketServer(msg.zonename);
225 } else {
226 self.startZoneSocketServer(msg.zonename);
227 }
228 });
229 } else if (msg.cmd === 'stop') {
230 if (self.zoneConnections[msg.zonename]) {
231 self.zoneConnections[msg.zonename].end();
232 }
233 }
234 });
235
236 zwatch.start(self.log);
237 };
238
239 MetadataAgent.prototype.stop = function () {
240 this.zwatch.stop();
241 };
242
243 MetadataAgent.prototype.startKVMSocketServer = function (zonename, callback) {
244 var self = this;
245 var vmobj = self.zones[zonename];
246 var zlog = self.zlog[zonename];
247 var sockpath = path.join(vmobj.zonepath, '/root/tmp/vm.ttyb');
248
249 zlog.info('Starting socket server');
250
251 async.waterfall([
252 function (cb) {
253 common.retryUntil(2000, 120000,
254 function (c) {
255 fs.exists(sockpath, function (exists) {
256 zlog.debug(sockpath + ' exists: ' + exists);
257 setTimeout(function () {
258 c(null, exists);
259 }, 1000);
260 });
261 }, function (error) {
262 if (error) {
263 zlog.error({err: error}, 'Timed out waiting for '
264 + 'metadata socket');
265 } else {
266 zlog.debug('returning from startKVMSocketServer w/o '
267 + 'error');
268 }
269 cb(error);
270 }
271 );
272 }
273 ], function (error) {
274 var zopts = { zone: zonename, sockpath: sockpath };
275 self.createKVMServer(zopts, function () {
276 if (callback) {
277 callback();
278 }
279 });
280 });
281 };
282
283 function rtrim(str, chars) {
284 chars = chars || '\\s';
285 str = str || '';
286 return str.replace(new RegExp('[' + chars + ']+$', 'g'), '');
287 }
288
289 MetadataAgent.prototype.createKVMServer = function (zopts, callback) {
290 var self = this;
291 var zlog = self.zlog[zopts.zone];
292 var kvmstream = new net.Stream();
293
294 self.zoneConnections[zopts.zone] = {
295 conn: new net.Stream(),
296 done: false,
297 end: function () {
298 if (this.done) {
299 return;
300 }
301 this.done = true;
302 zlog.info('Closing kvm stream for ' + zopts.zone);
303 kvmstream.end();
304 }
305 };
306
307 var buffer = '';
308 var handler = self.makeMetadataHandler(zopts.zone, kvmstream);
309
310 kvmstream.on('data', function (data) {
311 var chunk, chunks;
312 buffer += data.toString();
313 chunks = buffer.split('\n');
314 while (chunks.length > 1) {
315 chunk = chunks.shift();
316 handler(chunk);
317 }
318 buffer = chunks.pop();
319 });
320
321 kvmstream.on('error', function (e) {
322 zlog.error({err: e}, 'KVM Socket error: ' + e.message);
323 });
324
325 kvmstream.connect(zopts.sockpath);
326 callback();
327 };
328
329 MetadataAgent.prototype.startZoneSocketServer =
330 function (zonename, callback) {
331 var self = this;
332 var zlog = self.zlog[zonename];
333 var zonePath = self.zones[zonename].zonepath;
334 var localpath = '/.zonecontrol';
335 var zonecontrolpath = path.join(zonePath, 'root', localpath);
336
337 zlog.info('Starting socket server');
338
339 function ensureZonecontrolExists(cb) {
340 fs.exists(zonecontrolpath, function (exists) {
341 if (exists) {
342 cb();
343 return;
344 } else {
345 fs.mkdir(zonecontrolpath, parseInt('700', 8), function (error) {
346 cb(error);
347 });
348 }
349 });
350 }
351
352 ensureZonecontrolExists(function (err) {
353 var sockpath = path.join(localpath, 'metadata.sock');
354 var zopts = {
355 zone: zonename,
356 path: sockpath
357 };
358
359 if (err) {
360 callback({err: err}, 'unable to create ' + zonecontrolpath);
361 return;
362 }
363
364 self.createZoneSocket(zopts, function (createErr) {
365 if (createErr) {
366 // We call callback here, but don't include the error because
367 // this is running in async.forEach and we don't want to fail
368 // the others and there's nothing we can do to recover anyway.
369 if (callback) {
370 callback();
371 }
372 return;
373 }
374
375 zlog.info('Zone socket created.');
376
377 if (callback) {
378 callback();
379 }
380 });
381 });
382 };
383
384 MetadataAgent.prototype.createZoneSocket =
385 function (zopts, callback, waitSecs) {
386 var self = this;
387 var zlog = self.zlog[zopts.zone];
388 waitSecs = waitSecs || 1;
389
390 zsock.createZoneSocket(zopts, function (error, fd) {
391 if (error) {
392 // If we get errors trying to create the zone socket, wait and then
393 // keep retrying.
394 waitSecs = waitSecs * 2;
395 zlog.error(
396 { err: error },
397 'createZoneSocket error, %s seconds before next attempt',
398 waitSecs);
399 setTimeout(function () {
400 self.createZoneSocket(zopts, function () {}, waitSecs);
401 }, waitSecs * 1000);
402 return;
403 }
404
405 var server = net.createServer(function (socket) {
406 var handler = self.makeMetadataHandler(zopts.zone, socket);
407 var buffer = '';
408 socket.on('data', function (data) {
409 var chunk, chunks;
410 buffer += data.toString();
411 chunks = buffer.split('\n');
412 while (chunks.length > 1) {
413 chunk = chunks.shift();
414 handler(chunk);
415 }
416 buffer = chunks.pop();
417 });
418
419 socket.on('error', function (err) {
420 zlog.error({err: err}, 'ZSocket error: ' + err.message);
421 zlog.info('Attempting to recover; closing and recreating zone '
422 + 'socket and server.');
423 try {
424 server.close();
425 } catch (e) {
426 zlog.error({err: e}, 'Caught exception closing server: '
427 + e.message);
428 }
429
430 socket.end();
431 self.createZoneSocket(zopts);
432 });
433 });
434
435 self.zoneConnections[zopts.zone] = {
436 conn: server,
437 done: false,
438 end: function () {
439 if (this.done) {
440 return;
441 }
442 this.done = true;
443 zlog.info('Closing server');
444 server.close();
445 }
446 };
447
448 server.on('error', function (e) {
449 zlog.error({err: e}, 'Zone socket error: ' + e.message);
450 if (e.code !== 'EINTR') {
451 throw e;
452 }
453 });
454 var Pipe = process.binding('pipe_wrap').Pipe;
455 var p = new Pipe(true);
456 p.open(fd);
457 p.readable = p.writable = true;
458 server._handle = p;
459
460 server.listen();
461 });
462
463 if (callback) {
464 callback();
465 }
466 };
467
468 MetadataAgent.prototype.makeMetadataHandler = function (zone, socket) {
469 var self = this;
470 var zlog = self.zlog[zone];
471 var write = function (str) {
472 if (socket.writable) {
473 socket.write(str);
474 } else {
475 zlog.error('Socket for ' + zone + ' closed before we could write '
476 + 'anything.');
477 }
478 };
479
480 return function (data) {
481 var cmd;
482 var parts;
483 var val;
484 var vmobj;
485 var want;
486
487 parts = rtrim(data.toString()).replace(/\n$/, '')
488 .match(/^([^\s]+)\s?(.*)/);
489
490 if (!parts) {
491 write('invalid command\n');
492 return;
493 }
494
495 cmd = parts[1];
496 want = parts[2];
497
498 if (cmd === 'GET' && !want) {
499 write('invalid command\n');
500 return;
501 }
502
503 vmobj = self.zones[zone];
504
505 if (cmd === 'GET') {
506 zlog.info('Serving ' + want);
507 if (want.slice(0, 4) === 'sdc:') {
508 want = want.slice(4);
509
510 // NOTE: sdc:nics, sdc:resolvers and sdc:routes are not a
511 // committed interface, do not rely on it. At this point it
512 // should only be used by mdata-fetch, if you add a consumer
513 // that depends on it, please add a note about that here
514 // otherwise expect it will be removed on you sometime.
515 if (want === 'nics' && vmobj.hasOwnProperty('nics')) {
516 val = JSON.stringify(vmobj.nics);
517 returnit(null, val);
518 return;
519 } else if (want === 'resolvers'
520 && vmobj.hasOwnProperty('resolvers')) {
521
522 // resolvers and routes are special because we might reload
523 // metadata trying to get the new ones w/o zone reboot. To
524 // ensure these are fresh we always run updateZone which
525 // reloads the data if stale.
526 self.updateZone(zone, function () {
527 // See NOTE above about nics, same applies to resolvers.
528 // It's here solely for the use of mdata-fetch.
529 val = JSON.stringify(self.zones[zone].resolvers);
530 returnit(null, val);
531 return;
532 });
533 } else if (want === 'routes'
534 && vmobj.hasOwnProperty('routes')) {
535
536 var vmRoutes = [];
537
538 self.updateZone(zone, function () {
539
540 vmobj = self.zones[zone];
541
542 // The notes above about resolvers also to routes. It's
543 // here solely for the use of mdata-fetch, and we need
544 // to do the updateZone here so that we have latest
545 // data.
546 for (var r in vmobj.routes) {
547 var route = { linklocal: false, dst: r };
548 var nicIdx = vmobj.routes[r].match(/nics\[(\d+)\]/);
549 if (!nicIdx) {
550 // Non link-local route: we have all the
551 // information we need already
552 route.gateway = vmobj.routes[r];
553 vmRoutes.push(route);
554 continue;
555 }
556 nicIdx = Number(nicIdx[1]);
557
558 // Link-local route: we need the IP of the local nic
559 if (!vmobj.hasOwnProperty('nics')
560 || !vmobj.nics[nicIdx]
561 || !vmobj.nics[nicIdx].hasOwnProperty('ip')
562 || vmobj.nics[nicIdx].ip === 'dhcp') {
563
564 continue;
565 }
566
567 route.gateway = vmobj.nics[nicIdx].ip;
568 route.linklocal = true;
569 vmRoutes.push(route);
570 }
571
572 returnit(null, JSON.stringify(vmRoutes));
573 return;
574 });
575 } else if (want === 'operator-script') {
576 addMetadata(function (err) {
577 if (err) {
578 returnit(new Error('Unable to load metadata: '
579 + err.message));
580 return;
581 }
582
583 returnit(null,
584 vmobj.internal_metadata['operator-script']);
585 return;
586 });
587 } else {
588 addTags(function (err) {
589 if (!err) {
590 val = VM.flatten(vmobj, want);
591 }
592 returnit(err, val);
593 return;
594 });
595 }
596 } else {
597 // not sdc:, so key will come from *_mdata
598 addMetadata(function (err) {
599 var which_mdata = 'customer_metadata';
600
601 if (err) {
602 returnit(new Error('Unable to load metadata: '
603 + err.message));
604 return;
605 }
606
607 if (want.match(/_pw$/)) {
608 which_mdata = 'internal_metadata';
609 }
610
611 if (vmobj.hasOwnProperty(which_mdata)) {
612 returnit(null, vmobj[which_mdata][want]);
613 return;
614 } else {
615 returnit(new Error('Zone did not contain '
616 + which_mdata));
617 return;
618 }
619 });
620 }
621 } else if (cmd === 'KEYS') {
622 addMetadata(function (err) {
623 var ckeys = [];
624 var ikeys = [];
625
626 if (err) {
627 returnit(new Error('Unable to load metadata: '
628 + err.message));
629 return;
630 }
631
632 // *_pw$ keys come from internal_metadata, everything else comes
633 // from customer_metadata
634 ckeys = Object.keys(vmobj.customer_metadata)
635 .filter(function (k) {
636
637 return (!k.match(/_pw$/));
638 });
639 ikeys = Object.keys(vmobj.internal_metadata)
640 .filter(function (k) {
641
642 return (k.match(/_pw$/));
643 });
644
645 returnit(null, ckeys.concat(ikeys).join('\n'));
646 return;
647 });
648 } else {
649 zlog.error('Unknown command ' + cmd);
650 returnit(new Error('Unknown command ' + cmd));
651 return;
652 }
653
654 function addTags(cb) {
655 var filename;
656
657 filename = vmobj.zonepath + '/config/tags.json';
658 fs.readFile(filename, function (err, file_data) {
659
660 if (err && err.code === 'ENOENT') {
661 vmobj.tags = {};
662 cb();
663 return;
664 }
665
666 if (err) {
667 zlog.error({err: err}, 'failed to load tags.json: '
668 + err.message);
669 cb(err);
670 return;
671 }
672
673 try {
674 vmobj.tags = JSON.parse(file_data.toString());
675 cb();
676 } catch (e) {
677 zlog.error({err: e}, 'unable to tags.json for ' + zone
678 + ': ' + e.message);
679 cb(e);
680 }
681
682 return;
683 });
684 }
685
686 function addMetadata(cb) {
687 var filename;
688
689 // If we got here, our answer comes from metadata.
690 // XXX In the future, if the require overhead here ends up being
691 // larger than a stat would be, we might want to cache these and
692 // reload when mtime changes.
693
694 filename = vmobj.zonepath + '/config/metadata.json';
695
696 fs.readFile(filename, function (err, file_data) {
697 var json = {};
698 var mdata_types = [ 'customer_metadata', 'internal_metadata' ];
699
700 // start w/ both empty, if we fail partway through there will
701 // just be no metadata instead of wrong metadata.
702 vmobj.customer_metadata = {};
703 vmobj.internal_metadata = {};
704
705 if (err && err.code === 'ENOENT') {
706 cb();
707 return;
708 }
709
710 if (err) {
711 zlog.error({err: err}, 'failed to load mdata.json: '
712 + err.message);
713 cb(err);
714 return;
715 }
716
717 try {
718 json = JSON.parse(file_data.toString());
719 mdata_types.forEach(function (mdata) {
720 if (json.hasOwnProperty(mdata)) {
721 vmobj[mdata] = json[mdata];
722 }
723 });
724 cb();
725 } catch (e) {
726 zlog.error({err: e}, 'unable to load metadata.json for '
727 + zone + ': ' + e.message);
728 cb(e);
729 }
730
731 return;
732 });
733 }
734
735 function returnit(error, retval) {
736 var towrite;
737
738 if (error) {
739 zlog.error(error.message);
740 write('FAILURE\n');
741 return;
742 }
743
744 // String value
745 if (common.isString(retval)) {
746 towrite = retval.replace(/^\./mg, '..');
747 write('SUCCESS\n' + towrite + '\n.\n');
748 return;
749 } else if (!isNaN(retval)) {
750 towrite = retval.toString().replace(/^\./mg, '..');
751 write('SUCCESS\n' + towrite + '\n.\n');
752 return;
753 } else if (retval) {
754 // Non-string value
755 write('FAILURE\n');
756 return;
757 } else {
758 // Nothing to return
759 write('NOTFOUND\n');
760 return;
761 }
762 }
763 };
764 };