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