mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-06 19:20:12 +00:00
Massive rearchitecture
This commit is contained in:
parent
331cc4d754
commit
309a1d2036
172
app.coffee
172
app.coffee
@ -1,169 +1,47 @@
|
|||||||
fs = require('fs')
|
|
||||||
async = require('async')
|
|
||||||
request = require('request')
|
|
||||||
posix = require('posix')
|
|
||||||
express = require('express')
|
express = require('express')
|
||||||
|
async = require('async')
|
||||||
|
bootstrap = require('./bootstrap')
|
||||||
|
state = require('./state')
|
||||||
|
settings = require('./settings')
|
||||||
|
|
||||||
{exec, spawn} = require('child_process')
|
tasks = [
|
||||||
|
|
||||||
STATE_FILE = '/opt/ewa-client-bootstrap/state.json'
|
|
||||||
API_ENDPOINT = 'http://paras.rulemotion.com:1337'
|
|
||||||
HAKI_PATH = '/home/haki'
|
|
||||||
POLLING_INTERVAL = 30000
|
|
||||||
|
|
||||||
LED_FILE = "/sys/class/leds/led0/brightness"
|
|
||||||
BLINK_STEP = 100
|
|
||||||
|
|
||||||
try
|
|
||||||
state = require(STATE_FILE)
|
|
||||||
catch e
|
|
||||||
console.error(e)
|
|
||||||
process.exit()
|
|
||||||
|
|
||||||
bootstrapTasks = [
|
|
||||||
# get config from extra partition
|
|
||||||
(callback) ->
|
(callback) ->
|
||||||
try
|
if state.get('virgin')
|
||||||
callback(null, require('/mnt/config.json'))
|
handler = (error) ->
|
||||||
catch error
|
|
||||||
callback(error)
|
|
||||||
# bootstrapping
|
|
||||||
(config, callback) ->
|
|
||||||
request.post("#{API_ENDPOINT}/associate", {
|
|
||||||
json:
|
|
||||||
user: config.id
|
|
||||||
}, (error, response, body) ->
|
|
||||||
if error
|
if error
|
||||||
return callback(error)
|
setTimeout(-> bootstrap(handler), 10000)
|
||||||
|
|
||||||
if typeof body isnt 'object'
|
|
||||||
callback(body)
|
|
||||||
|
|
||||||
state.virgin = false
|
|
||||||
state.uuid = body.uuid
|
|
||||||
state.gitUrl = body.gitUrl
|
|
||||||
|
|
||||||
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.appendFileSync('/etc/openvpn/client.conf', "remote #{body.vpnhost} #{body.vpnport}")
|
|
||||||
|
|
||||||
callback(null)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
hakiExec = (command, options, callback) ->
|
|
||||||
options.uid = posix.getpwnam('haki').uid
|
|
||||||
|
|
||||||
ps = spawn(process.env.SHELL, ['-c', command], options)
|
|
||||||
stdout = ''
|
|
||||||
stderr = ''
|
|
||||||
ps.stdout.on('data', (chunk) ->
|
|
||||||
stdout += chunk
|
|
||||||
)
|
|
||||||
ps.stderr.on('data', (chunk) ->
|
|
||||||
stderr += chunk
|
|
||||||
)
|
|
||||||
ps.on('exit', (error) -> callback(error, stdout, stderr))
|
|
||||||
ps.on('error', (error) -> callback(error, stdout, stderr))
|
|
||||||
|
|
||||||
stage1Tasks = [
|
|
||||||
# superuser tasks
|
|
||||||
(callback) -> async.waterfall(bootstrapTasks, callback)
|
|
||||||
(callback) -> fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 4)) ; callback()
|
|
||||||
(callback) -> exec('systemctl stop openvpn@client', callback)
|
|
||||||
(callback) -> exec('systemctl start openvpn@client', callback)
|
|
||||||
(callback) -> exec('systemctl enable openvpn@client', callback)
|
|
||||||
# haki user tasks
|
|
||||||
(callback) -> hakiExec('mkdir hakiapp', cwd: HAKI_PATH, callback)
|
|
||||||
(callback) -> hakiExec('git init', cwd: "#{HAKI_PATH}/hakiapp", callback)
|
|
||||||
(callback) -> hakiExec("git remote add origin #{state.gitUrl}", cwd: "#{HAKI_PATH}/hakiapp", callback)
|
|
||||||
# done
|
|
||||||
(callback) -> console.log('Bootstrapped') ; callback()
|
|
||||||
]
|
|
||||||
|
|
||||||
updateRepo = (callback) ->
|
|
||||||
tasks1 = [
|
|
||||||
(callback) -> hakiExec('git pull origin master', cwd: "#{HAKI_PATH}/hakiapp", callback)
|
|
||||||
(stdout, stderr, callback) -> hakiExec('git rev-parse HEAD', cwd: "#{HAKI_PATH}/hakiapp", callback)
|
|
||||||
(stdout, stderr, callback) -> callback(null, stdout.trim())
|
|
||||||
]
|
|
||||||
|
|
||||||
tasks2 = [
|
|
||||||
(callback) ->
|
|
||||||
console.log("Checking for package.json")
|
|
||||||
if fs.existsSync("#{HAKI_PATH}/hakiapp/package.json")
|
|
||||||
console.log("Found, npm installing")
|
|
||||||
ps = spawn('sudo', ['-u', 'haki', 'npm', 'install'],
|
|
||||||
cwd: "#{HAKI_PATH}/hakiapp"
|
|
||||||
stdio: [0, 1, 2]
|
|
||||||
)
|
|
||||||
ps.on('exit', callback)
|
|
||||||
ps.on('error', callback)
|
|
||||||
else
|
else
|
||||||
console.log("No package.json")
|
state.set('virgin', false)
|
||||||
callback()
|
callback()
|
||||||
(callback) ->
|
bootstrap(handler)
|
||||||
console.log("Checking for Procfile")
|
|
||||||
if fs.existsSync("#{HAKI_PATH}/hakiapp/Procfile")
|
|
||||||
console.log("Found Procfile, starting app..")
|
|
||||||
ps = spawn('foreman', ['start'],
|
|
||||||
cwd: "#{HAKI_PATH}/hakiapp"
|
|
||||||
stdio: [0, 1, 2]
|
|
||||||
uid: posix.getpwnam('haki').uid
|
|
||||||
)
|
|
||||||
ps.on('exit', callback)
|
|
||||||
ps.on('error', callback)
|
|
||||||
else
|
else
|
||||||
console.log("No Procfile found")
|
|
||||||
callback()
|
callback()
|
||||||
|
# (callback) ->
|
||||||
]
|
]
|
||||||
|
|
||||||
async.waterfall(tasks1, (error, hash) ->
|
async.series(tasks, (error) ->
|
||||||
console.log("Checking for new version..")
|
if error
|
||||||
if hash isnt state.gitHead
|
|
||||||
console.log("New version found #{state.gitHead}->#{hash}")
|
|
||||||
state.gitHead = hash
|
|
||||||
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 4))
|
|
||||||
async.series(tasks2, (callback) -> setTimeout(callback, POLLING_INTERVAL))
|
|
||||||
else
|
|
||||||
console.log("No new version found")
|
|
||||||
setTimeout(callback, POLLING_INTERVAL)
|
|
||||||
)
|
|
||||||
|
|
||||||
stage2Tasks = [
|
|
||||||
(callback) -> async.forever(updateRepo, callback)
|
|
||||||
]
|
|
||||||
|
|
||||||
if state.virgin
|
|
||||||
tasks = stage1Tasks.concat(stage2Tasks)
|
|
||||||
else
|
|
||||||
tasks = stage2Tasks
|
|
||||||
|
|
||||||
async.series(tasks, (error, results) ->
|
|
||||||
if (error)
|
|
||||||
console.error(error)
|
console.error(error)
|
||||||
)
|
)
|
||||||
|
|
||||||
app = express()
|
app = express()
|
||||||
|
|
||||||
app.post('/blink', (req, res) ->
|
app.post('/blink', (req, res) ->
|
||||||
count = 0
|
state = 0
|
||||||
|
toggleLed = ->
|
||||||
|
state = (state + 1) % 2
|
||||||
|
fs.writeFileSync(settings.LED_FILE, state)
|
||||||
|
|
||||||
async.whilst(
|
interval = setInterval(toggleLed, settings.BLINK_STEP)
|
||||||
-> return count < 25
|
|
||||||
(callback) ->
|
|
||||||
fs.writeFile(LED_FILE, '1', (err) ->
|
|
||||||
setTimeout(->
|
setTimeout(->
|
||||||
fs.writeFileSync(LED_FILE, '0')
|
clearInterval(interval)
|
||||||
BLINK_STEP)
|
|
||||||
)
|
|
||||||
count++
|
|
||||||
setTimeout(callback, 2*BLINK_STEP)
|
|
||||||
(err) ->
|
|
||||||
#5 seconds have passed
|
|
||||||
)
|
|
||||||
res.send(200)
|
res.send(200)
|
||||||
|
, 5000)
|
||||||
|
)
|
||||||
|
|
||||||
|
app.post('/update', (req, res) ->
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
app.listen(80)
|
app.listen(80)
|
||||||
|
86
application.coffee
Normal file
86
application.coffee
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{spawn} = require('child_process')
|
||||||
|
{getpwnam} = require('posix')
|
||||||
|
async = require('async')
|
||||||
|
|
||||||
|
class Application
|
||||||
|
constructor: (@repo, @path, @user) ->
|
||||||
|
@process = null
|
||||||
|
@inprogress = false
|
||||||
|
@queue = []
|
||||||
|
@options =
|
||||||
|
cwd: @path
|
||||||
|
stdio: 'inherit'
|
||||||
|
uid: getpwnam(@user).uid
|
||||||
|
env:
|
||||||
|
USER: @user
|
||||||
|
USERNAME: @user
|
||||||
|
|
||||||
|
_init: (callback) ->
|
||||||
|
tasks = [
|
||||||
|
# Create the directory for the project
|
||||||
|
(callback) =>
|
||||||
|
spawn('mkdir', ['-p', @path]).on('exit', callback).on('error', callback)
|
||||||
|
|
||||||
|
# Change the owner to the user
|
||||||
|
(callback) =>
|
||||||
|
spawn('chown', [@user, @path]).on('exit', callback).on('error', callback)
|
||||||
|
|
||||||
|
# Initalize a new empty git repo
|
||||||
|
(callback) =>
|
||||||
|
spawn('git', ['init'], @options).on('exit', callback).on('error', callback)
|
||||||
|
|
||||||
|
# Add the remote origin to the repo
|
||||||
|
(callback) =>
|
||||||
|
spawn('git', ['remote', 'add', 'origin', @repo], @options).on('exit', callback).on('error', callback)
|
||||||
|
]
|
||||||
|
async.series(tasks, callback)
|
||||||
|
|
||||||
|
_start: (callback) ->
|
||||||
|
if not @process
|
||||||
|
@process = spawn('foreman', ['start'], @options)
|
||||||
|
callback?()
|
||||||
|
|
||||||
|
_stop: (callback) ->
|
||||||
|
# Kill will return false if process has already died
|
||||||
|
handler = =>
|
||||||
|
@process = null
|
||||||
|
callback?()
|
||||||
|
|
||||||
|
spawn('pkill', ['-TERM', '-P', @process.pid], @options).on('exit', handler).on('error', handler)
|
||||||
|
|
||||||
|
_update: (callback) ->
|
||||||
|
shouldRestartApp = Boolean(@process)
|
||||||
|
tasks = [
|
||||||
|
# Stop the application if running
|
||||||
|
(callback) => shouldRestartApp and @_stop(callback) or callback()
|
||||||
|
|
||||||
|
# Pull new commits
|
||||||
|
(callback) =>
|
||||||
|
spawn('git', ['pull', 'origin', 'master'], @options).on('exit', callback).on('error', callback)
|
||||||
|
|
||||||
|
# Install npm dependencies
|
||||||
|
(callback) =>
|
||||||
|
spawn('npm', ['install'], @options).on('exit', callback).on('error', callback)
|
||||||
|
|
||||||
|
# Start the app
|
||||||
|
(callback) => shouldRestartApp and @_start(callback) or callback()
|
||||||
|
]
|
||||||
|
async.series(tasks, callback)
|
||||||
|
|
||||||
|
# These methods shouldn't be called in parallel, queue them if they conflict
|
||||||
|
['start', 'stop', 'init', 'update'].forEach((method) ->
|
||||||
|
Application::[method] = (callback) ->
|
||||||
|
if @inprogress
|
||||||
|
@queue.push([method, arguments])
|
||||||
|
else
|
||||||
|
@inprogress = true
|
||||||
|
@['_' + method](=>
|
||||||
|
@inprogress = false
|
||||||
|
if @queue.length isnt 0
|
||||||
|
[next, args] = @queue.shift()
|
||||||
|
@[next](args...)
|
||||||
|
callback?(arguments...)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = Application
|
40
bootstrap.coffee
Normal file
40
bootstrap.coffee
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
settings = require('./settings')
|
||||||
|
state = require('./state')
|
||||||
|
{exec} = require('child_process')
|
||||||
|
|
||||||
|
bootstrapTasks = [
|
||||||
|
# get config from extra partition
|
||||||
|
(callback) ->
|
||||||
|
try
|
||||||
|
callback(null, require('/mnt/config.json'))
|
||||||
|
catch error
|
||||||
|
callback(error)
|
||||||
|
# bootstrapping
|
||||||
|
(config, callback) ->
|
||||||
|
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.virgin = false
|
||||||
|
state.uuid = body.uuid
|
||||||
|
state.gitUrl = body.gitUrl
|
||||||
|
state.sync()
|
||||||
|
|
||||||
|
vpnConf = fs.readFileSync('/etc/openvpn/client.conf.template', 'utf8')
|
||||||
|
vpnConf += "remote #{body.vpnhost} #{body.vpnport}")
|
||||||
|
|
||||||
|
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) -> exec('systemctl start openvpn@client', callback)
|
||||||
|
(callback) -> exec('systemctl enable openvpn@client', callback)
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = (callback) ->
|
7
settings.coffee
Normal file
7
settings.coffee
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports =
|
||||||
|
STATE_FILE: '/opt/ewa-client-bootstrap/state.json'
|
||||||
|
API_ENDPOINT: 'http://paras.rulemotion.com:1337'
|
||||||
|
HAKI_PATH: '/home/haki'
|
||||||
|
|
||||||
|
LED_FILE: "/sys/class/leds/led0/brightness"
|
||||||
|
BLINK_STEP: 100
|
18
state.coffee
Normal file
18
state.coffee
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
exports.sync = -> sync(state)
|
Loading…
x
Reference in New Issue
Block a user