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