implement DNS features to update zerigo automagically

This commit is contained in:
Lloyd Hilaiel 2012-06-01 14:01:47 +03:00
Родитель 9c4e6d5239
Коммит d13057af05
3 изменённых файлов: 167 добавлений и 72 удалений

155
awsbox.js
Просмотреть файл

@ -8,6 +8,7 @@ path = require('path');
vm = require('./lib/vm.js'), vm = require('./lib/vm.js'),
key = require('./lib/key.js'), key = require('./lib/key.js'),
ssh = require('./lib/ssh.js'), ssh = require('./lib/ssh.js'),
dns = require('./lib/dns.js'),
git = require('./lib/git.js'), git = require('./lib/git.js'),
optimist = require('optimist'), optimist = require('optimist'),
urlparse = require('urlparse'), urlparse = require('urlparse'),
@ -17,17 +18,20 @@ var verbs = {};
function checkErr(err) { function checkErr(err) {
if (err) { if (err) {
process.stderr.write('fatal error: ' + err + "\n"); process.stderr.write('ERRORE FATALE: ' + err + "\n");
process.exit(1); 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("Yay! You have your very own deployment. Here's the basics:\n");
console.log(" 1. deploy your code: git push " + name + " <mybranch>:master"); console.log(" 1. deploy your code: git push " + name + " HEAD:master");
console.log(" 2. visit your server on the web: http://" + deets.ipAddress); console.log(" 2. visit your server on the web: " + url);
console.log(" 3. ssh in with sudo: ssh ec2-user@" + deets.ipAddress); console.log(" 3. ssh in with sudo: ssh ec2-user@" + host);
console.log(" 4. ssh as the deployment user: ssh app@" + deets.ipAddress); 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)); 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: "); process.stdout.write("trying to remove git remote: ");
git.removeRemote(name, deets.ipAddress, function(err) { git.removeRemote(name, deets.ipAddress, function(err) {
console.log(err ? "failed: " + err : "done"); 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) { verbs['create'] = function(args) {
var parser = optimist(args) var parser = optimist(args)
.usage('awsbox create: Create a VM') .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('n', 'a short nickname for the VM.')
.describe('u', 'publically visible URL for the instance') .describe('u', 'publically visible URL for the instance')
.check(function(argv) { .check(function(argv) {
@ -112,68 +143,88 @@ verbs['create'] = function(args) {
try { try {
var awsboxJson = JSON.parse(fs.readFileSync("./.awsbox.json")); var awsboxJson = JSON.parse(fs.readFileSync("./.awsbox.json"));
} catch(e) { } catch(e) {
console.log("Fatal error! Can't read awsbox.json: " + e); checkErr("Can't read awsbox.json: " + e);
process.exit(1);
} }
console.log("attempting to set up VM \"" + name + "\""); console.log("attempting to set up VM \"" + name + "\"");
vm.startImage({ var dnsKey;
type: opts.t var dnsHost;
}, function(err, r) { 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); 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); checkErr(err);
console.log(" ... Instance ready, setting human readable name in aws"); console.log(" ... VM launched, waiting for startup (should take about 20s)");
vm.setName(r.instanceId, longName, function(err) {
vm.waitForInstance(r.instanceId, function(err, deets) {
checkErr(err); checkErr(err);
console.log(" ... name set, waiting for ssh access and configuring");
var config = { public_url: (opts.u || "http://" + deets.ipAddress) };
if (argv.x) { if (dnsHost) console.log(" ... Adding DNS Record for " + dnsHost);
console.log(" ... adding addition configuration values");
var x = JSON.parse(fs.readFileSync(argv.x));
Object.keys(x).forEach(function(key) {
config[key] = x[key];
});
}
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) { console.log(" ... Instance ready, setting human readable name in aws");
checkErr(err); vm.setName(r.instanceId, longName, function(err) {
console.log(" ... victory! server is accessible and configured"); checkErr(err);
git.addRemote(name, deets.ipAddress, function(err, r) { console.log(" ... name set, waiting for ssh access and configuring");
if (err && /already exists/.test(err)) { var config = { public_url: (opts.u || "http://" + deets.ipAddress) };
console.log("OOPS! you already have a git remote named '" + name + "'!");
console.log("to create a new one: git remote add <name> " + if (opts.x) {
"app@" + deets.ipAddress + ":git"); console.log(" ... adding additional configuration values");
} else { var x = JSON.parse(fs.readFileSync(opts.x));
checkErr(err); 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(" ... public url will be:", config.public_url);
console.log(" ... finally, installing custom packages: " + awsboxJson.packages.join(', '));
console.log(""); ssh.copyUpConfig(deets.ipAddress, config, function(err, r) {
}
ssh.installPackages(deets.ipAddress, awsboxJson.packages, function(err, r) {
checkErr(err); checkErr(err);
var postcreate = (awsboxJson.hooks && awsboxJson.hooks.postcreate) || null; console.log(" ... victory! server is accessible and configured");
ssh.runScript(deets.ipAddress, postcreate, function(err, r) { git.addRemote(name, deets.ipAddress, function(err, r) {
checkErr(err); if (err && /already exists/.test(err)) {
console.log("OOPS! you already have a git remote named '" + name + "'!");
if (opts.p && opts.s) { console.log("to create a new one: git remote add <name> " +
console.log(" ... copying up SSL cert"); "app@" + deets.ipAddress + ":git");
ssh.copySSL(deets.ipAddress, opts.p, opts.s, function(err) {
checkErr(err);
printInstructions(name, deets);
});
} else { } 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);
}
});
});
}); });
}); });
}); });

Просмотреть файл

@ -9,9 +9,7 @@ if (!process.env[envVar]) {
+ envVar; + envVar;
} }
const api_key = process.env[envVar]; function doRequest(api_key, method, path, body, cb) {
function doRequest(method, path, body, cb) {
var req = http.request({ var req = http.request({
auth: 'lloyd@hilaiel.com:' + api_key, auth: 'lloyd@hilaiel.com:' + api_key,
host: 'ns.zerigo.com', host: 'ns.zerigo.com',
@ -23,15 +21,15 @@ function doRequest(method, path, body, cb) {
'Content-Length': body ? body.length : 0 'Content-Length': body ? body.length : 0
} }
}, function(r) { }, function(r) {
if ((r.statusCode / 100).toFixed(0) != 2 && var buf = "";
r.statusCode != 404) {
return cb("non 200 status: " + r.statusCode);
}
buf = "";
r.on('data', function(chunk) { r.on('data', function(chunk) {
buf += chunk; buf += chunk;
}); });
r.on('end', function() { 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); xml2js.parseString(buf, cb);
}); });
}); });
@ -39,31 +37,69 @@ function doRequest(method, path, body, cb) {
req.end(); req.end();
}; };
exports.updateRecord = function (hostname, zone, ip, cb) { exports.updateRecord = function (key, hostname, ip, cb) {
doRequest('GET', '/api/1.1/zones.xml', null, function(err, r) { 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); if (err) return cb(err);
var m = jsel.match('object:has(:root > .domain:val(?)) > .id .#', var m = jsel.match('object:has(:root > .domain:val(?)) > .id .#',
[ zone ], r); [ hostname ], r);
if (m.length != 1) return cb("couldn't extract domain id from zerigo"); 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 path = '/api/1.1/hosts.xml?zone_id=' + m[0];
var body = '<host><data>' + ip + '</data><host-type>A</host-type>'; var body = '<host><data>' + ip + '</data><host-type>A</host-type>';
body += '<hostname>' + hostname + '</hostname>' body += '<hostname>' + host + '</hostname>'
body += '</host>'; body += '</host>';
doRequest('POST', path, body, function(err, r) { doRequest(key, 'POST', path, body, function(err, r) {
cb(err); cb(err);
}); });
}); });
}; };
exports.deleteRecord = function (hostname, cb) { exports.findByIP = function (key, ip, cb) {
doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { 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); if (err) return cb(err);
var m = jsel.match('.host .id > .#', r); var m = jsel.match('.host .id > .#', r);
if (!m.length) return cb("no such DNS record"); if (!m.length) return cb("no such DNS record");
function deleteOne() { function deleteOne() {
if (!m.length) return cb(null); if (!m.length) return cb(null);
var one = m.shift(); 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); if (err) return cb(err);
deleteOne(); deleteOne();
}); });
@ -72,8 +108,9 @@ exports.deleteRecord = function (hostname, cb) {
}); });
}; };
exports.inUse = function (hostname, cb) { exports.inUse = function (key, hostname, cb) {
doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) { 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); if (err) return cb(err);
var m = jsel.match('.host', r); var m = jsel.match('.host', r);
// we shouldn't have multiple! oops! let's return the first one // we shouldn't have multiple! oops! let's return the first one

Просмотреть файл

@ -93,12 +93,19 @@ function dateBasedVersion() {
+ pad(d.getUTCMinutes()); + pad(d.getUTCMinutes());
} }
exports.createAMI = function(name, cb) { exports.find = function(name, cb) {
exports.list(function(err, r) { exports.list(function(err, r) {
if (err) return cb('failed to list vms: ' + err); if (err) return cb('failed to list vms: ' + err);
deets = findInstance(r, name); deets = findInstance(r, name);
if (!deets) return cb('no such vm'); 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', { aws.call('CreateImage', {
InstanceId: deets.instanceId, InstanceId: deets.instanceId,