From de342a9209808219cbb8513243c4336d31ba1d33 Mon Sep 17 00:00:00 2001 From: Petros Aggelatos Date: Sat, 14 Dec 2013 05:18:20 +0000 Subject: [PATCH] Revamp/rewrite of supervisor as a docker application --- .gitignore | 1 + Dockerfile | 12 ++ app.coffee | 135 ------------------- bootstrap.coffee | 52 ------- data/state.json | 1 - package.json | 34 +++-- src/api.coffee | 22 +++ src/app.coffee | 42 ++++++ application.coffee => src/application.coffee | 0 src/bootstrap.coffee | 57 ++++++++ src/db.coffee | 28 ++++ src/openvpn.conf.tmpl | 16 +++ settings.coffee => src/settings.coffee | 1 - supervisor.js => src/supervisor.js | 2 - util.coffee => src/util.coffee | 0 src/utils.coffee | 18 +++ state.coffee | 16 --- 17 files changed, 217 insertions(+), 220 deletions(-) create mode 100644 Dockerfile delete mode 100644 app.coffee delete mode 100644 bootstrap.coffee delete mode 100644 data/state.json create mode 100644 src/api.coffee create mode 100644 src/app.coffee rename application.coffee => src/application.coffee (100%) create mode 100644 src/bootstrap.coffee create mode 100644 src/db.coffee create mode 100644 src/openvpn.conf.tmpl rename settings.coffee => src/settings.coffee (70%) rename supervisor.js => src/supervisor.js (67%) rename util.coffee => src/util.coffee (100%) create mode 100644 src/utils.coffee delete mode 100644 state.coffee diff --git a/.gitignore b/.gitignore index c76b2888..3aeab6a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ *.swp +data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d8603079 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# To build the actual image change the source +# image to resin/rpi-raspbian:jessie +FROM tianon/debian:jessie + +RUN apt-get update +RUN apt-get install -y -q nodejs npm openvpn + +ADD . /supervisor + +WORKDIR /supervisor + +CMD ["npm", "start"] diff --git a/app.coffee b/app.coffee deleted file mode 100644 index 41ba900f..00000000 --- a/app.coffee +++ /dev/null @@ -1,135 +0,0 @@ -fs = require('fs') -express = require('express') -async = require('async') -bootstrap = require('./bootstrap') -state = require('./state') -settings = require('./settings') -request = require('request') -Application = require('./application') - -console.log('Supervisor started..') - -hakiApp = null - -tasks = [ - (callback) -> - if state.get('virgin') - console.log('Device is virgin. Bootstrapping') - handler = (error) -> - if error - console.log('Bootstrapping failed with error', error) - console.log('Trying again in 10s') - setTimeout((-> bootstrap(handler)), 10000) - else - console.log('Bootstrapping successful') - state.set('virgin', false) - callback() - bootstrap(handler) - else - console.log("Device isn't a virgin") - callback() - (callback) -> - fs.writeFile('/sys/class/leds/led0/trigger', 'none', callback) - (callback) -> - hakiApp = new Application(state.get('gitUrl'), '/home/haki/hakiapp', 'haki') - - hakiApp.on 'pre-init', -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Initialising' - ) - - hakiApp.on 'post-init', -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Idle' - ) - - hakiApp.on 'pre-update', -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Updating' - ) - - hakiApp.on 'post-update', (hash) -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Idle' - commit: state.get('gitHash') - ) - - hakiApp.on 'start', -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Running' - ) - - hakiApp.on 'stop', -> - request( - uri: "#{settings.API_ENDPOINT}/ewa/device?$filter=uuid eq '#{state.get('uuid')}'" - method: 'PATCH' - json: - status: 'Idle' - ) - - if not state.get('appInitialised') - console.log('Initialising app..') - hakiApp.init((error) -> - if error then return callback(error) - state.set('appInitialised', true) - callback() - ) - else - console.log('App already initialised') - callback() - (callback) -> - console.log('Fetching new code..') - hakiApp.update(callback) - (callback) -> - console.log('Starting the app..') - hakiApp.start(callback) -] - -async.series(tasks, (error) -> - if error - console.error(error) - else - console.log('Everything is fine :)') -) - -app = express() - -app.post('/blink', (req, res) -> - ledState = 0 - toggleLed = -> - ledState = (ledState + 1) % 2 - fs.writeFileSync(settings.LED_FILE, ledState) - - interval = setInterval(toggleLed, settings.BLINK_STEP) - setTimeout(-> - clearInterval(interval) - fs.writeFileSync(settings.LED_FILE, 0) - res.send(200) - , 5000) -) - -app.post('/update', (req, res) -> - hakiApp.update((error) -> - if error - res.send(500) - else - res.send(204) - ) -) - -app.listen(80) diff --git a/bootstrap.coffee b/bootstrap.coffee deleted file mode 100644 index af1a897d..00000000 --- a/bootstrap.coffee +++ /dev/null @@ -1,52 +0,0 @@ -settings = require('./settings') -state = require('./state') -{exec} = require('child_process') -async = require('async') -request = require('request') -fs = require('fs') - -bootstrapTasks = [ - # get config from extra partition - (callback) -> - console.log('Reading the user conf file') - try - callback(null, require('/mnt/config.json')) - catch error - callback(error) - # bootstrapping - (config, callback) -> - console.log('Got user', config.username) - console.log('Posting to the API') - request.post("#{settings.API_ENDPOINT}/associate", { - json: - user: config.id - }, (error, response, body) -> - if error or response.statusCode is 404 - return callback('Error associating with user') - - state.set('virgin', false) - state.set('uuid', body.uuid) - state.set('gitUrl', body.gitUrl) - - vpnConf = fs.readFileSync('/etc/openvpn/client.conf.template', 'utf8') - vpnConf += "remote #{body.vpnhost} #{body.vpnport}\n" - - console.log('Configuring VPN') - fs.writeFileSync('/etc/openvpn/ca.crt', body.ca) - fs.writeFileSync('/etc/openvpn/client.crt', body.cert) - fs.writeFileSync('/etc/openvpn/client.key', body.key) - fs.writeFileSync('/etc/openvpn/client.conf', vpnConf) - - callback(null) - ) - (callback) -> - console.log('Starting VPN client..') - exec('systemctl start openvpn@client', callback) - - (stdout, stderr, callback) -> - console.log('Enabling VPN client..') - exec('systemctl enable openvpn@client', callback) -] - -module.exports = (callback) -> - async.waterfall(bootstrapTasks, callback) diff --git a/data/state.json b/data/state.json deleted file mode 100644 index 0b629fcf..00000000 --- a/data/state.json +++ /dev/null @@ -1 +0,0 @@ -{"virgin":true} diff --git a/package.json b/package.json index 861720fc..6fe45cc3 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,23 @@ { - "name": "resin-supervisor", - "version": "0.0.1", - "dependencies": { - "coffee-script": "~1.6.3", - "async": "~0.2.9", - "request": "~2.22.0", - "posix": "~1.0.2", - "express": "~3.2.6", - "lodash": "~1.3.1" - }, - "engines": [ - "node >= 0.10.x" - ] + "name": "resin-supervisor", + "version": "0.0.1", + "scripts": { + "start": "node src/supervisor.js" + }, + "dependencies": { + "coffee-script": "~1.6.3", + "async": "~0.2.9", + "request": "~2.22.0", + "posix": "~1.0.2", + "express": "~3.2.6", + "lodash": "~1.3.1", + "csr-gen": "~0.2.1", + "dockerode": "~0.2.3", + "sqlite3": "~2.1.19", + "knex": "~0.5.1", + "bluebird": "~0.11.5-0" + }, + "engines": [ + "node >= 0.10.x" + ] } diff --git a/src/api.coffee b/src/api.coffee new file mode 100644 index 00000000..f14e12f7 --- /dev/null +++ b/src/api.coffee @@ -0,0 +1,22 @@ +fs = require 'fs' +express = require 'express' +dockerode = require 'dockerode' + +api = express() + +LED_FILE = '/sys/class/leds/led0/brightness' +blink = (ms = 200, callback) -> + fs.writeFileSync(LED_FILE, 1) + setTimeout(-> fs.writeFile(LED_FILE, 0, callback), ms) + +api.post('/blink', (req, res) -> + interval = setInterval(blink, 400) + setTimeout(-> clearInterval(interval), 5000) + res.send(200) +) + +api.post('/update', (req, res) -> + console.log('TODO: Update the application') +) + +module.exports = api diff --git a/src/app.coffee b/src/app.coffee new file mode 100644 index 00000000..e1f37a61 --- /dev/null +++ b/src/app.coffee @@ -0,0 +1,42 @@ +knex = require './db' +Promise = require 'bluebird' +fs = Promise.promisifyAll(require('fs')) +os = require 'os' +api = require './api' +utils = require './utils' +crypto = require 'crypto' +{spawn} = require 'child_process' +bootstrap = require './bootstrap' + +console.log('Supervisor started..') + +newUuid = utils.getDeviceUuid() +oldUuid = knex('config').select('value').where(key: 'uuid') + +bootstrap = Promise.all([newUuid, oldUuid]).then(([newUuid, [oldUuid]]) -> + if newUuid is oldUuid + return true + + console.log('New device detected. Bootstrapping..') + return bootstrap(newUuid) +) + +bootstrap.then(-> + console.log('Starting OpenVPN..') + openvpn = spawn('openvpn', ['client.conf'], cwd: '/supervisor/data') + + # Prefix and log all OpenVPN output + openvpn.stdout.on('data', (data) -> + prefix = 'OPENVPN: ' + console.log((prefix + data).trim().replace(/\n/gm, '\n#{prefix}')) + ) + + # Prefix and log all OpenVPN output + openvpn.stderr.on('data', (data) -> + prefix = 'OPENVPN: ' + console.log((prefix + data).trim().replace(/\n/gm, '\n#{prefix}')) + ) + + console.log('Start API server') + api.listen(80) +) diff --git a/application.coffee b/src/application.coffee similarity index 100% rename from application.coffee rename to src/application.coffee diff --git a/src/bootstrap.coffee b/src/bootstrap.coffee new file mode 100644 index 00000000..545ce2ce --- /dev/null +++ b/src/bootstrap.coffee @@ -0,0 +1,57 @@ +Promise = require('bluebird') +_ = require('lodash') +fs = Promise.promisifyAll(require('fs')) +url = require('url') +knex = require './db' +crypto = require('crypto') +csrgen = Promise.promisify(require('csr-gen')) +request = Promise.promisify(require('request')) + +module.exports = (uuid) -> + # Load config file + config = fs.readFileAsync('/boot/config.json', 'utf8').then(JSON.parse) + + # Generate SSL certificate + keys = csrgen(uuid, + company: 'Rulemotion Ltd' + csrName: 'client.csr' + keyName: 'client.key' + outputDir: '/supervisor/data' + email: 'vpn@resin.io' + read: true + country: '' + city: '' + state: '' + division: '' + ) + + Promise.all([config, keys]).then(([config, keys]) -> + console.log('UUID:', uuid) + console.log('User ID:', config.userId) + console.log('User:', config.username) + console.log('API key:', config.apiKey) + console.log('CSR :', keys.csr) + console.log('Posting to the API..') + config.csr = keys.csr + config.uuid = uuid + return request( + method: 'POST' + url: url.resolve(process.env.API_ENDPOINT, 'associate') + json: config + ) + ).spread((response, body) -> + if response.statusCode >= 400 + throw body + + console.log('Configuring VPN..') + vpnConf = fs.readFileAsync('/supervisor/src/openvpn.conf.tmpl', 'utf8') + .then((tmpl) -> + fs.writeFileAsync('/supervisor/data/client.conf', _.template(tmpl)(body)) + ) + + Promise.all([ + fs.writeFileAsync('/supervisor/data/ca.crt', body.ca) + fs.writeFileAsync('/supervisor/data/client.crt', body.cert) + vpnConf + ]) + ).then(knex('config').select('value').where(key: 'uuid') diff --git a/src/db.coffee b/src/db.coffee new file mode 100644 index 00000000..d87f0b25 --- /dev/null +++ b/src/db.coffee @@ -0,0 +1,28 @@ +Knex = require('knex') + +knex = Knex.initialize( + client: 'sqlite3' + connection: + filename: '/supervisor/data/database.sqlite' +) + +knex.schema.hasTable('config').then((exists) -> + if not exists + knex.schema.createTable('config', (t) -> + t.increments('id').primary() + t.string('key') + t.string('value') + ) +) + +knex.schema.hasTable('app').then((exists) -> + if not exists + knex.schema.createTable('app', (t) -> + t.increments('id').primary() + t.string('name') + t.string('imageId') + t.string('status') + ) +) + +module.exports = knex diff --git a/src/openvpn.conf.tmpl b/src/openvpn.conf.tmpl new file mode 100644 index 00000000..2c7b19a1 --- /dev/null +++ b/src/openvpn.conf.tmpl @@ -0,0 +1,16 @@ +client +remote <%= vpnhost %> <%= vpnport %> +resolv-retry infinite + +ca ca.crt +cert client.crt +key client.key + +comp-lzo +dev tun +proto tcp +nobind + +persist-key +persist-tun +verb 3 diff --git a/settings.coffee b/src/settings.coffee similarity index 70% rename from settings.coffee rename to src/settings.coffee index 2436f617..ab2ae69a 100644 --- a/settings.coffee +++ b/src/settings.coffee @@ -1,5 +1,4 @@ module.exports = - STATE_FILE: '/opt/ewa-client-bootstrap/data/state.json' API_ENDPOINT: 'http://haki.io' HAKI_PATH: '/home/haki' diff --git a/supervisor.js b/src/supervisor.js similarity index 67% rename from supervisor.js rename to src/supervisor.js index 70de436b..ac273f72 100755 --- a/supervisor.js +++ b/src/supervisor.js @@ -1,4 +1,2 @@ -#!/usr/bin/env node - require('coffee-script'); require('./app'); diff --git a/util.coffee b/src/util.coffee similarity index 100% rename from util.coffee rename to src/util.coffee diff --git a/src/utils.coffee b/src/utils.coffee new file mode 100644 index 00000000..aa389d51 --- /dev/null +++ b/src/utils.coffee @@ -0,0 +1,18 @@ +Promise = require('bluebird') +fs = Promise.promisifyAll(require('fs')) +os = require('os') +crypto = require('crypto') + +# Parses the output of /proc/cpuinfo to find the "Serial : 710abf21" line +# or the hostname if there isn't a serial number (when run in dev mode) +# The uuid is the SHA1 hash of that value. +exports.getDeviceUuid = -> + fs.readFileAsync('/proc/cpuinfo', 'utf8').then((cpuinfo) -> + serial = cpuinfo + .split('\n') + .filter((line) -> line.indexOf('Serial') isnt -1)[0] + ?.split(':')[1] + .trim() or os.hostname() + + return crypto.createHash('sha1').update(serial, 'utf8').digest('hex') + ) diff --git a/state.coffee b/state.coffee deleted file mode 100644 index 72aed7f8..00000000 --- a/state.coffee +++ /dev/null @@ -1,16 +0,0 @@ -settings = require('./settings') -fs = require('fs') - -sync = (data) -> - fs.writeFileSync(settings.STATE_FILE, JSON.stringify(data)) - -if not fs.existsSync(settings.STATE_FILE) - sync({}) - -state = require(settings.STATE_FILE) - -exports.get = (key) -> state[key] - -exports.set = (key, value) -> - state[key] = value - sync(state)