From d13057af05eab32d343de1a39180685970f1a418 Mon Sep 17 00:00:00 2001 From: Lloyd Hilaiel Date: Fri, 1 Jun 2012 14:01:47 +0300 Subject: [PATCH] implement DNS features to update zerigo automagically --- awsbox.js | 155 +++++++++++++++++++++++++++++++++++------------------ lib/dns.js | 75 +++++++++++++++++++------- lib/vm.js | 9 +++- 3 files changed, 167 insertions(+), 72 deletions(-) diff --git a/awsbox.js b/awsbox.js index 78fe5af..5dfff35 100755 --- a/awsbox.js +++ b/awsbox.js @@ -8,6 +8,7 @@ path = require('path'); vm = require('./lib/vm.js'), key = require('./lib/key.js'), ssh = require('./lib/ssh.js'), +dns = require('./lib/dns.js'), git = require('./lib/git.js'), optimist = require('optimist'), urlparse = require('urlparse'), @@ -17,17 +18,20 @@ var verbs = {}; function checkErr(err) { if (err) { - process.stderr.write('fatal error: ' + err + "\n"); + process.stderr.write('ERRORE FATALE: ' + err + "\n"); process.exit(1); } } -function printInstructions(name, deets) { +function printInstructions(name, host, url, deets) { + if (!url) url = 'http://' + deets.ipAddress; + if (!host) host = deets.ipAddress; + console.log(""); console.log("Yay! You have your very own deployment. Here's the basics:\n"); - console.log(" 1. deploy your code: git push " + name + " :master"); - console.log(" 2. visit your server on the web: http://" + deets.ipAddress); - console.log(" 3. ssh in with sudo: ssh ec2-user@" + deets.ipAddress); - console.log(" 4. ssh as the deployment user: ssh app@" + deets.ipAddress); + console.log(" 1. deploy your code: git push " + name + " HEAD:master"); + console.log(" 2. visit your server on the web: " + url); + console.log(" 3. ssh in with sudo: ssh ec2-user@" + host); + console.log(" 4. ssh as the deployment user: ssh app@" + host); console.log("\n Here are your server's details:", JSON.stringify(deets, null, 4)); } @@ -52,6 +56,26 @@ verbs['destroy'] = function(args) { process.stdout.write("trying to remove git remote: "); git.removeRemote(name, deets.ipAddress, function(err) { console.log(err ? "failed: " + err : "done"); + + if (process.env['ZERIGO_DNS_KEY']) { + process.stdout.write("trying to remove DNS: "); + var dnsKey = process.env['ZERIGO_DNS_KEY']; + dns.findByIP(dnsKey, deets.ipAddress, function(err, fqdns) { + checkErr(err); + if (!fqdns.length) return console.log("no dns entries found"); + console.log(fqdns.join(', ')); + function removeNext() { + if (!fqdns.length) return; + var fqdn = fqdns.shift(); + process.stdout.write("deleting " + fqdn + ": "); + dns.deleteRecord(dnsKey, fqdn, function(err) { + console.log(err ? "failed: " + err : "done"); + removeNext(); + }); + } + removeNext(); + }); + } }); } }); @@ -65,9 +89,16 @@ verbs['test'] = function() { }); } +verbs['findByIP'] = function(args) { + dns.findByIP(process.env['ZERIGO_DNS_KEY'], args[0], function(err, fqdns) { + console.log(err, fqdns); + }); +}; + verbs['create'] = function(args) { var parser = optimist(args) .usage('awsbox create: Create a VM') + .describe('d', 'setup DNS via zerigo (requires ZERIGO_DNS_KEY in env)') .describe('n', 'a short nickname for the VM.') .describe('u', 'publically visible URL for the instance') .check(function(argv) { @@ -112,68 +143,88 @@ verbs['create'] = function(args) { try { var awsboxJson = JSON.parse(fs.readFileSync("./.awsbox.json")); } catch(e) { - console.log("Fatal error! Can't read awsbox.json: " + e); - process.exit(1); + checkErr("Can't read awsbox.json: " + e); } console.log("attempting to set up VM \"" + name + "\""); - vm.startImage({ - type: opts.t - }, function(err, r) { + var dnsKey; + var dnsHost; + if (opts.d) { + if (!opts.u) checkErr('-d is meaningless without -u (to set DNS I need a hostname)'); + if (!process.env['ZERIGO_DNS_KEY']) checkErr('-d requires ZERIGO_DNS_KEY env var'); + dnsKey = process.env['ZERIGO_DNS_KEY']; + dnsHost = urlparse(opts.u).host; + console.log(" ... Checking for DNS availability of " + dnsHost); + } + + dns.inUse(dnsKey, dnsHost, function(err, res) { checkErr(err); - console.log(" ... VM launched, waiting for startup (should take about 20s)"); + if (res) checkErr('that domain is in use, pointing at ' + res.data); - vm.waitForInstance(r.instanceId, function(err, deets) { + vm.startImage({ + type: opts.t + }, function(err, r) { checkErr(err); - console.log(" ... Instance ready, setting human readable name in aws"); - vm.setName(r.instanceId, longName, function(err) { + console.log(" ... VM launched, waiting for startup (should take about 20s)"); + + vm.waitForInstance(r.instanceId, function(err, deets) { checkErr(err); - console.log(" ... name set, waiting for ssh access and configuring"); - var config = { public_url: (opts.u || "http://" + deets.ipAddress) }; - if (argv.x) { - console.log(" ... adding addition configuration values"); - var x = JSON.parse(fs.readFileSync(argv.x)); - Object.keys(x).forEach(function(key) { - config[key] = x[key]; - }); - } + if (dnsHost) console.log(" ... Adding DNS Record for " + dnsHost); - console.log(" ... public url will be:", config.public_url); + dns.updateRecord(dnsKey, dnsHost, deets.ipAddress, function(err) { + checkErr(err ? 'updating DNS: ' + err : err); - ssh.copyUpConfig(deets.ipAddress, config, function(err, r) { - checkErr(err); - console.log(" ... victory! server is accessible and configured"); - git.addRemote(name, deets.ipAddress, function(err, r) { - if (err && /already exists/.test(err)) { - console.log("OOPS! you already have a git remote named '" + name + "'!"); - console.log("to create a new one: git remote add " + - "app@" + deets.ipAddress + ":git"); - } else { - checkErr(err); + console.log(" ... Instance ready, setting human readable name in aws"); + vm.setName(r.instanceId, longName, function(err) { + checkErr(err); + console.log(" ... name set, waiting for ssh access and configuring"); + var config = { public_url: (opts.u || "http://" + deets.ipAddress) }; + + if (opts.x) { + console.log(" ... adding additional configuration values"); + var x = JSON.parse(fs.readFileSync(opts.x)); + Object.keys(x).forEach(function(key) { + config[key] = x[key]; + }); } - console.log(" ... and your git remote is all set up"); - if (awsboxJson.packages) { - console.log(" ... finally, installing custom packages: " + awsboxJson.packages.join(', ')); - console.log(""); - } - ssh.installPackages(deets.ipAddress, awsboxJson.packages, function(err, r) { + console.log(" ... public url will be:", config.public_url); + + ssh.copyUpConfig(deets.ipAddress, config, function(err, r) { checkErr(err); - var postcreate = (awsboxJson.hooks && awsboxJson.hooks.postcreate) || null; - ssh.runScript(deets.ipAddress, postcreate, function(err, r) { - checkErr(err); - - if (opts.p && opts.s) { - console.log(" ... copying up SSL cert"); - ssh.copySSL(deets.ipAddress, opts.p, opts.s, function(err) { - checkErr(err); - printInstructions(name, deets); - }); + console.log(" ... victory! server is accessible and configured"); + git.addRemote(name, deets.ipAddress, function(err, r) { + if (err && /already exists/.test(err)) { + console.log("OOPS! you already have a git remote named '" + name + "'!"); + console.log("to create a new one: git remote add " + + "app@" + deets.ipAddress + ":git"); } else { - printInstructions(name, deets); + checkErr(err); } + console.log(" ... and your git remote is all set up"); + + if (awsboxJson.packages) { + console.log(" ... finally, installing custom packages: " + awsboxJson.packages.join(', ')); + } + ssh.installPackages(deets.ipAddress, awsboxJson.packages, function(err, r) { + checkErr(err); + var postcreate = (awsboxJson.hooks && awsboxJson.hooks.postcreate) || null; + ssh.runScript(deets.ipAddress, postcreate, function(err, r) { + checkErr(err); + + if (opts.p && opts.s) { + console.log(" ... copying up SSL cert"); + ssh.copySSL(deets.ipAddress, opts.p, opts.s, function(err) { + checkErr(err); + printInstructions(name, dnsHost, opts.u, deets); + }); + } else { + printInstructions(name, dnsHost, opts.u, deets); + } + }); + }); }); }); }); diff --git a/lib/dns.js b/lib/dns.js index 99a2f58..ad64a61 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -9,9 +9,7 @@ if (!process.env[envVar]) { + envVar; } -const api_key = process.env[envVar]; - -function doRequest(method, path, body, cb) { +function doRequest(api_key, method, path, body, cb) { var req = http.request({ auth: 'lloyd@hilaiel.com:' + api_key, host: 'ns.zerigo.com', @@ -23,15 +21,15 @@ function doRequest(method, path, body, cb) { 'Content-Length': body ? body.length : 0 } }, function(r) { - if ((r.statusCode / 100).toFixed(0) != 2 && - r.statusCode != 404) { - return cb("non 200 status: " + r.statusCode); - } - buf = ""; + var buf = ""; r.on('data', function(chunk) { buf += chunk; }); r.on('end', function() { + if ((r.statusCode / 100).toFixed(0) != 2 && + r.statusCode != 404) { + return cb("non 200 status: " + r.statusCode + "(" + body + ")"); + } xml2js.parseString(buf, cb); }); }); @@ -39,31 +37,69 @@ function doRequest(method, path, body, cb) { req.end(); }; -exports.updateRecord = function (hostname, zone, ip, cb) { - doRequest('GET', '/api/1.1/zones.xml', null, function(err, r) { +exports.updateRecord = function (key, hostname, ip, cb) { + if (!key || !hostname) return process.nextTick(function() { cb(null) }); + + doRequest(key, 'GET', '/api/1.1/zones.xml', null, function(err, r) { + var host = ""; if (err) return cb(err); var m = jsel.match('object:has(:root > .domain:val(?)) > .id .#', - [ zone ], r); - if (m.length != 1) return cb("couldn't extract domain id from zerigo"); + [ hostname ], r); + if (m.length != 1) { + host = hostname.split('.')[0]; + hostname = hostname.split('.').slice(1).join('.'); + var m = jsel.match('object:has(:root > .domain:val(?)) > .id .#', + [ hostname ], r); + if (m.length != 1) return cb("couldn't extract domain id from zerigo"); + } + var path = '/api/1.1/hosts.xml?zone_id=' + m[0]; var body = '' + ip + 'A'; - body += '' + hostname + '' + body += '' + host + '' body += ''; - doRequest('POST', path, body, function(err, r) { + doRequest(key, 'POST', path, body, function(err, r) { cb(err); }); }); }; -exports.deleteRecord = function (hostname, cb) { - doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { +exports.findByIP = function (key, ip, cb) { + if (!key) return cb(null, null); + + doRequest(key, 'GET', '/api/1.1/zones.xml', null, function(err, r) { + if (err) return cb(err); + var zones = jsel.match('.id .#', r); + + if (!zones.length) return cb(null, []); + + var done = 0; + var found = []; + var failed; + + zones.forEach(function(zone) { + doRequest(key, 'GET', '/api/1.1/zones/' + zone + '/hosts.xml?per_page=1000', null, function(err, r) { + if (failed) return; + if (err) { + failed = err; + return cb(err); + } + var m = jsel.match('object:has(:root > .data:val(?)) > .fqdn', [ ip ], r); + found = found.concat(m); + if (++done === zones.length) cb(null, found); + }); + }); + }); +}; + +exports.deleteRecord = function (key, hostname, cb) { + doRequest(key, 'GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { if (err) return cb(err); var m = jsel.match('.host .id > .#', r); if (!m.length) return cb("no such DNS record"); function deleteOne() { if (!m.length) return cb(null); var one = m.shift(); - doRequest('DELETE', '/api/1.1/hosts/' + one + '.xml', null, function(err) { + doRequest(key, 'DELETE', '/api/1.1/hosts/' + one + '.xml', null, function(err) { if (err) return cb(err); deleteOne(); }); @@ -72,8 +108,9 @@ exports.deleteRecord = function (hostname, cb) { }); }; -exports.inUse = function (hostname, cb) { - doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { +exports.inUse = function (key, hostname, cb) { + if (!key || !hostname) return process.nextTick(function() { cb(null) }); + doRequest(key, 'GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { if (err) return cb(err); var m = jsel.match('.host', r); // we shouldn't have multiple! oops! let's return the first one diff --git a/lib/vm.js b/lib/vm.js index bb9c209..db75822 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -93,12 +93,19 @@ function dateBasedVersion() { + pad(d.getUTCMinutes()); } -exports.createAMI = function(name, cb) { +exports.find = function(name, cb) { exports.list(function(err, r) { if (err) return cb('failed to list vms: ' + err); deets = findInstance(r, name); if (!deets) return cb('no such vm'); + cb(null, deets); + }); +}; + +exports.createAMI = function(name, cb) { + exports.find(name, function(err, deets) { + if (err) return cb(err); aws.call('CreateImage', { InstanceId: deets.instanceId,