From cdf83caa725973dbfbb45bce507e4d2786ee09cd Mon Sep 17 00:00:00 2001 From: Lloyd Hilaiel <lloyd@hilaiel.com> Date: Fri, 27 Jan 2012 16:44:52 -0700 Subject: [PATCH] partial implementation of super-magic instadeploy of browserid to AWS instances. next up? security groups. Then automatic git remote addition. The addition of app user and a whole pile of tweaking on the template image. --- package.json | 77 ++++++++++++++++++++++--------------------- scripts/deploy.js | 51 ++++++++++++++++++++++++++++ scripts/deploy/aws.js | 7 ++++ scripts/deploy/key.js | 57 ++++++++++++++++++++++++++++++++ scripts/deploy/vm.js | 69 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 37 deletions(-) create mode 100755 scripts/deploy.js create mode 100644 scripts/deploy/aws.js create mode 100644 scripts/deploy/key.js create mode 100644 scripts/deploy/vm.js diff --git a/package.json b/package.json index 57c45121b..a2cd5c9af 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,42 @@ { - "name": "browserid" - , "version": "0.0.1" - , "private": true - , "dependencies": { - "JSONSelect": "0.3.0" - , "bcrypt": "0.4.1" - , "compute-cluster": "0.0.6" - , "connect": "1.7.2" - , "client-sessions": "0.0.3" - , "connect-cookie-session": "0.0.2" - , "connect-logger-statsd": "0.0.1" - , "ejs": "0.4.3" - , "express": "2.5.0" - , "jwcrypto": "0.1.1" - , "mustache": "0.3.1-dev" - , "mysql" : "0.9.5" - , "node-gettext": "0.1.1" - , "node-statsd": "https://github.com/downloads/lloyd/node-statsd/3a73de.tgz" - , "nodemailer": "0.1.18" - , "optimist" : "0.2.8" - , "postprocess": "0.0.3" - , "semver": "1.0.12" - , "temp": "0.2.0" - , "uglify-js": "1.0.6" - , "uglifycss": "0.0.4" - , "urlparse": "0.0.1" - , "vows": "0.5.13" - , "winston" : "0.5.6" - } - , "scripts": { - "postinstall": "./scripts/generate_ephemeral_keys.sh", - "test": "./scripts/run_all_tests.sh", - "start": "./scripts/run_locally.js" - }, - "engines": { - "node": ">= 0.6.2" - } + "name": "browserid", + "version": "0.0.1", + "private": true, + "dependencies": { + "JSONSelect": "0.4.0", + "bcrypt": "0.4.1", + "compute-cluster": "0.0.6", + "connect": "1.7.2", + "client-sessions": "0.0.3", + "connect-cookie-session": "0.0.2", + "connect-logger-statsd": "0.0.1", + "ejs": "0.4.3", + "express": "2.5.0", + "jwcrypto": "0.1.1", + "mustache": "0.3.1-dev", + "mysql": "0.9.5", + "node-gettext": "0.1.1", + "node-statsd": "https://github.com/downloads/lloyd/node-statsd/3a73de.tgz", + "nodemailer": "0.1.18", + "optimist": "0.2.8", + "postprocess": "0.0.3", + "semver": "1.0.12", + "temp": "0.2.0", + "uglify-js": "1.0.6", + "uglifycss": "0.0.4", + "urlparse": "0.0.1", + "winston": "0.5.6" + }, + "devDependencies": { + "vows": "0.5.13", + "aws-lib": "0.0.5" + }, + "scripts": { + "postinstall": "./scripts/generate_ephemeral_keys.sh", + "test": "./scripts/run_all_tests.sh", + "start": "./scripts/run_locally.js" + }, + "engines": { + "node": ">= 0.6.2" + } } diff --git a/scripts/deploy.js b/scripts/deploy.js new file mode 100755 index 000000000..77dd41a0c --- /dev/null +++ b/scripts/deploy.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +const +aws = require('./deploy/aws.js'); +path = require('path'); +vm = require('./deploy/vm.js'), +key = require('./deploy/key.js'); + +var verbs = {}; + +function checkErr(err) { + if (err) { + process.stderr.write('fatal error: ' + err + "\n"); + process.exit(1); + } +} + +verbs['deploy'] = function(args) { + vm.startImage(function(err, r) { + checkErr(err); + vm.waitForInstance(r.instanceId, function(err, r) { + console.log(err, r); + }); + }); +}; + +verbs['list'] = function(args) { + vm.list(function(err, r) { + checkErr(err); + console.log(JSON.stringify(r, null, 2)); + }); +}; + +var error = (process.argv.length <= 2); + +if (!error) { + var verb = process.argv[2]; + if (!verbs[verb]) error = "no such command: " + verb; + else { + verbs[verb](process.argv.slice(2)); + } +} + +if (error) { + if (typeof error === 'string') process.stderr.write('fatal error: ' + error + "\n\n"); + + process.stderr.write('A command line tool to deploy BrowserID onto Amazon\'s EC2\n'); + process.stderr.write('Usage: ' + path.basename(__filename) + + ' <' + Object.keys(verbs).join('|') + "> [args]\n"); + process.exit(1); +} diff --git a/scripts/deploy/aws.js b/scripts/deploy/aws.js new file mode 100644 index 000000000..6641989e9 --- /dev/null +++ b/scripts/deploy/aws.js @@ -0,0 +1,7 @@ +const +awslib = require('aws-lib'); + +module.exports = awslib.createEC2Client(process.env['AWS_ID'], process.env['AWS_SECRET'], { + version: '2011-12-15' +}); + diff --git a/scripts/deploy/key.js b/scripts/deploy/key.js new file mode 100644 index 000000000..d93da0158 --- /dev/null +++ b/scripts/deploy/key.js @@ -0,0 +1,57 @@ +const +aws = require('./aws.js'), +path = require('path'), +fs = require('fs'), +child_process = require('child_process'), +jsel = require('JSONSelect'), +crypto = require('crypto'); + +const keyPath = process.env['PUBKEY'] || path.join(process.env['HOME'], ".ssh", "id_rsa.pub"); + +exports.read = function(cb) { + fs.readFile(keyPath, cb); +}; + +exports.fingerprint = function(cb) { + exports.read(function(err, buf) { + if (err) return cb(err); + var b = new Buffer(buf.toString().split(' ')[1], 'base64'); + var md5sum = crypto.createHash('md5'); + md5sum.update(b); + cb(null, md5sum.digest('hex')); + }); +/* + child_process.exec( + "ssh-keygen -lf " + keyPath, + function(err, r) { + if (!err) r = r.split(' ')[1]; + cb(err, r); + }); +*/ +}; + +exports.getName = function(cb) { + exports.fingerprint(function(err, fingerprint) { + if (err) return cb(err); + + var keyName = "browserid deploy key (" + fingerprint + ")"; + + // is this fingerprint known? + aws.call('DescribeKeyPairs', {}, function(result) { + var found = jsel.match(":has(.keyName:val(?)) > .keyName", [ keyName ], result); + if (found.length) return cb(null, keyName); + + // key isn't yet installed! + exports.read(function(err, key) { + aws.call('ImportKeyPair', { + KeyName: keyName, + PublicKeyMaterial: new Buffer(key).toString('base64') + }, function(result) { + if (!result) return cb('null result from ec2 on key addition'); + if (result.Errors) return cb(result.Errors.Error.Message); + cb(null, keyName); + }); + }); + }); + }); +}; diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js new file mode 100644 index 000000000..e8d43f079 --- /dev/null +++ b/scripts/deploy/vm.js @@ -0,0 +1,69 @@ +const +aws = require('./aws.js'); +jsel = require('JSONSelect'), +key = require('./key.js'); + +const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-51ac7d38'; + +function extractInstanceDeets(horribleBlob) { + var instance = {}; + ["instanceId", "imageId", "instanceState", "dnsName", "keyName", "instanceType", + "ipAddress"].forEach(function(key) { + if (horribleBlob[key]) instance[key] = horribleBlob[key]; + }); + return instance; +} + +exports.list = function(cb) { + aws.call('DescribeInstances', {}, function(result) { + var instances = []; + jsel.forEach(".instancesSet > .item", result, function(item) { + instances.push(extractInstanceDeets(item)); + }); + cb(null, instances); + }); +}; + +function returnSingleImageInfo(result, cb) { + if (!result) return cb('no results from ec2 api'); + try { return cb(result.Errors.Error.Message); } catch(e) {}; + try { + result = jsel.match(".instancesSet > .item", result)[0]; + cb(null, extractInstanceDeets(result)); + } catch(e) { + return cb("couldn't extract new instance details from ec2 response: " + e); + } +} + +exports.startImage = function(cb) { + key.getName(function(err, r) { + if (err) return cb(err); + aws.call('RunInstances', { + ImageId: BROWSERID_TEMPLATE_IMAGE_ID, + KeyName: r, + InstanceType: 't1.micro', + MinCount: 1, + MaxCount: 1 + }, function (result) { + returnSingleImageInfo(result, cb); + }); + }); +}; + +exports.waitForInstance = function(id, cb) { + aws.call('DescribeInstanceStatus', { + InstanceId: id + }, function(r) { + if (!r) return cb('no response from ec2'); + if (!r.instanceStatusSet) return cb('malformed response from ec2'); + if (Object.keys(r.instanceStatusSet).length) { + var deets = extractInstanceDeets(r.instanceStatusSet.item); + if (deets && deets.instanceState && deets.instanceState.name === 'running') { + return aws.call('DescribeInstances', { InstanceId: id }, function(result) { + returnSingleImageInfo(result, cb); + }); + } + } + setTimeout(function(){ exports.waitForInstance(id, cb); }, 1000); + }); +}; -- GitLab