mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-18 21:27:54 +00:00
Revamp/rewrite of supervisor as a docker application
This commit is contained in:
parent
5a071b35c9
commit
de342a9209
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/node_modules/
|
||||
*.swp
|
||||
data
|
||||
|
12
Dockerfile
Normal file
12
Dockerfile
Normal 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"]
|
135
app.coffee
135
app.coffee
@ -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)
|
@ -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)
|
@ -1 +0,0 @@
|
||||
{"virgin":true}
|
34
package.json
34
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"
|
||||
]
|
||||
}
|
||||
|
22
src/api.coffee
Normal file
22
src/api.coffee
Normal 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
42
src/app.coffee
Normal 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
57
src/bootstrap.coffee
Normal 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
28
src/db.coffee
Normal 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
16
src/openvpn.conf.tmpl
Normal 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
|
@ -1,5 +1,4 @@
|
||||
module.exports =
|
||||
STATE_FILE: '/opt/ewa-client-bootstrap/data/state.json'
|
||||
API_ENDPOINT: 'http://haki.io'
|
||||
HAKI_PATH: '/home/haki'
|
||||
|
@ -1,4 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('coffee-script');
|
||||
require('./app');
|
18
src/utils.coffee
Normal file
18
src/utils.coffee
Normal 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')
|
||||
)
|
16
state.coffee
16
state.coffee
@ -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)
|
Loading…
Reference in New Issue
Block a user