Revamp/rewrite of supervisor as a docker application

This commit is contained in:
Petros Aggelatos 2013-12-14 05:18:20 +00:00 committed by Pablo Carranza Vélez
parent 5a071b35c9
commit de342a9209
17 changed files with 217 additions and 220 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/ /node_modules/
*.swp *.swp
data

12
Dockerfile Normal file
View File

@ -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"]

View File

@ -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)

View File

@ -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)

View File

@ -1 +0,0 @@
{"virgin":true}

View File

@ -1,15 +1,23 @@
{ {
"name": "resin-supervisor", "name": "resin-supervisor",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "scripts": {
"coffee-script": "~1.6.3", "start": "node src/supervisor.js"
"async": "~0.2.9", },
"request": "~2.22.0", "dependencies": {
"posix": "~1.0.2", "coffee-script": "~1.6.3",
"express": "~3.2.6", "async": "~0.2.9",
"lodash": "~1.3.1" "request": "~2.22.0",
}, "posix": "~1.0.2",
"engines": [ "express": "~3.2.6",
"node >= 0.10.x" "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"
]
} }

22
src/api.coffee Normal file
View File

@ -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

42
src/app.coffee Normal file
View File

@ -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)
)

57
src/bootstrap.coffee Normal file
View File

@ -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')

28
src/db.coffee Normal file
View File

@ -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

16
src/openvpn.conf.tmpl Normal file
View File

@ -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

View File

@ -1,5 +1,4 @@
module.exports = module.exports =
STATE_FILE: '/opt/ewa-client-bootstrap/data/state.json'
API_ENDPOINT: 'http://haki.io' API_ENDPOINT: 'http://haki.io'
HAKI_PATH: '/home/haki' HAKI_PATH: '/home/haki'

View File

@ -1,4 +1,2 @@
#!/usr/bin/env node
require('coffee-script'); require('coffee-script');
require('./app'); require('./app');

18
src/utils.coffee Normal file
View File

@ -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')
)

View File

@ -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)