diff --git a/app.coffee b/app.coffee index bcfc5eab..a8c865e2 100644 --- a/app.coffee +++ b/app.coffee @@ -1,169 +1,47 @@ -fs = require('fs') -async = require('async') -request = require('request') -posix = require('posix') express = require('express') +async = require('async') +bootstrap = require('./bootstrap') +state = require('./state') +settings = require('./settings') -{exec, spawn} = require('child_process') - -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 +tasks = [ (callback) -> - try - callback(null, require('/mnt/config.json')) - catch error - callback(error) - # bootstrapping - (config, callback) -> - request.post("#{API_ENDPOINT}/associate", { - json: - user: config.id - }, (error, response, body) -> - if error - return callback(error) - - 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 - console.log("No package.json") - callback() - (callback) -> - 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 - console.log("No Procfile found") - callback() - ] - - async.waterfall(tasks1, (error, hash) -> - console.log("Checking for new version..") - 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)) + if state.get('virgin') + handler = (error) -> + if error + setTimeout(-> bootstrap(handler), 10000) + else + state.set('virgin', false) + callback() + bootstrap(handler) else - console.log("No new version found") - setTimeout(callback, POLLING_INTERVAL) - ) - -stage2Tasks = [ - (callback) -> async.forever(updateRepo, callback) + callback() + # (callback) -> ] -if state.virgin - tasks = stage1Tasks.concat(stage2Tasks) -else - tasks = stage2Tasks - -async.series(tasks, (error, results) -> - if (error) +async.series(tasks, (error) -> + if error console.error(error) ) app = express() app.post('/blink', (req, res) -> - count = 0 + state = 0 + toggleLed = -> + state = (state + 1) % 2 + fs.writeFileSync(settings.LED_FILE, state) + + interval = setInterval(toggleLed, settings.BLINK_STEP) + setTimeout(-> + clearInterval(interval) + res.send(200) + , 5000) +) + +app.post('/update', (req, res) -> + - async.whilst( - -> return count < 25 - (callback) -> - fs.writeFile(LED_FILE, '1', (err) -> - setTimeout( -> - fs.writeFileSync(LED_FILE, '0') - BLINK_STEP) - ) - count++ - setTimeout(callback, 2*BLINK_STEP) - (err) -> - #5 seconds have passed - ) - res.send(200) ) app.listen(80) diff --git a/application.coffee b/application.coffee new file mode 100644 index 00000000..aa6460a9 --- /dev/null +++ b/application.coffee @@ -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 diff --git a/bootstrap.coffee b/bootstrap.coffee new file mode 100644 index 00000000..4724dc87 --- /dev/null +++ b/bootstrap.coffee @@ -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) -> diff --git a/settings.coffee b/settings.coffee new file mode 100644 index 00000000..ee43eea3 --- /dev/null +++ b/settings.coffee @@ -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 diff --git a/state.coffee b/state.coffee new file mode 100644 index 00000000..0d952a65 --- /dev/null +++ b/state.coffee @@ -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)