Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26aefadfba | |||
| 51a28842cf | |||
| 210c2f3cc1 | |||
| 773c326eb7 | |||
| cb2fb026c5 | |||
| a4731ad054 | |||
| aa33938fb5 | |||
| edfe8f1ad0 | |||
| 41399a2593 | |||
| 2a4c467ab8 | |||
| 6be6092c0e | |||
| e76584b0da | |||
| b3816615db | |||
| 212d0bd55a | |||
| 712ada940e | |||
| ba690c6346 | |||
| e910e19f57 | |||
| 0c2532b0b5 | |||
| 9c9b17a5f0 | |||
| 816dea91ec | |||
| c228f8d4d5 | |||
| 05bb99fad4 | |||
| 51b2457b3d | |||
| ed71fca23e | |||
| 20e8e72ac2 | |||
| 13fe0eb882 | |||
| e0476c9030 | |||
| fca82fd775 | |||
| 37c8ba8ddd | |||
| f87011b5c2 | |||
| 7f149700f8 | |||
| 78ba9070fc | |||
| e31e5e1f69 | |||
| 31d9027677 | |||
| debcd6f353 | |||
| 5cb1681922 | |||
| 9074bccea0 |
+1
-5
@@ -4,10 +4,6 @@ docs/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
|
||||
# vim swam files
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
# supervisor
|
||||
supervisord.pid
|
||||
supervisord.log
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ The Box
|
||||
Development setup
|
||||
-----------------
|
||||
* sudo useradd -m yellowtent
|
||||
** This dummy user is required for supervisor 'box' configs
|
||||
** Add admin-localhost as 127.0.0.1 in /etc/hosts
|
||||
** All apps will be installed as hypened-subdomains of localhost. You should add
|
||||
hyphened-subdomains of your apps into /etc/hosts
|
||||
|
||||
Regular → Executable
+20
-26
@@ -2,20 +2,12 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// WARNING This is a supervisor eventlistener!
|
||||
// The communication happens via stdin/stdout
|
||||
// !! No console.log() allowed
|
||||
// !! Do not set DEBUG
|
||||
|
||||
var assert = require('assert'),
|
||||
mailer = require('./src/mailer.js'),
|
||||
safe = require('safetydance'),
|
||||
supervisor = require('supervisord-eventlistener'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
|
||||
var gLastNotifyTime = {};
|
||||
var gCooldownTime = 1000 * 60 * 5; // 5 min
|
||||
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
|
||||
|
||||
function collectLogs(program, callback) {
|
||||
@@ -26,28 +18,30 @@ function collectLogs(program, callback) {
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) {
|
||||
if (data.expected === '1') return console.error('Normal app %s exit', data.processname);
|
||||
|
||||
console.error('%s exited unexpectedly', data.processname);
|
||||
|
||||
collectLogs(data.processname, function (error, result) {
|
||||
function sendCrashNotification(processName) {
|
||||
collectLogs(processName, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
result = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
if (!gLastNotifyTime[data.processname] || gLastNotifyTime[data.processname] < Date.now() - gCooldownTime) {
|
||||
console.error('Send mail.');
|
||||
mailer.sendCrashNotification(data.processname, result);
|
||||
gLastNotifyTime[data.processname] = Date.now();
|
||||
} else {
|
||||
console.error('Do not send mail, already sent one recently.');
|
||||
}
|
||||
console.log('Sending crash notification email for', processName);
|
||||
mailer.sendCrashNotification(processName, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
mailer.initialize(function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
sendCrashNotification(processName);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
mailer.initialize(function () {
|
||||
supervisor.listen(process.stdin, process.stdout);
|
||||
console.error('Crashnotifier listening...');
|
||||
});
|
||||
|
||||
Generated
+7
-13
@@ -9,22 +9,22 @@
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.1.46",
|
||||
"from": "aws-sdk@*",
|
||||
"from": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
|
||||
"dependencies": {
|
||||
"sax": {
|
||||
"version": "0.5.3",
|
||||
"from": "sax@0.5.3",
|
||||
"from": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz",
|
||||
"resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.2.8",
|
||||
"from": "xml2js@0.2.8",
|
||||
"from": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "0.4.2",
|
||||
"from": "xmlbuilder@0.4.2",
|
||||
"from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
|
||||
}
|
||||
}
|
||||
@@ -156,16 +156,15 @@
|
||||
"connect-lastmile": {
|
||||
"version": "0.0.13",
|
||||
"from": "connect-lastmile@0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.1.3",
|
||||
"from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"from": "debug@>=2.1.0 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.0",
|
||||
"from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
|
||||
"from": "ms@0.7.0",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||
}
|
||||
}
|
||||
@@ -2268,7 +2267,7 @@
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.19",
|
||||
"from": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz",
|
||||
"from": "safetydance@0.0.19",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||
},
|
||||
"semver": {
|
||||
@@ -2442,11 +2441,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"supervisord-eventlistener": {
|
||||
"version": "0.1.0",
|
||||
"from": "https://registry.npmjs.org/supervisord-eventlistener/-/supervisord-eventlistener-0.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/supervisord-eventlistener/-/supervisord-eventlistener-0.1.0.tgz"
|
||||
},
|
||||
"tail-stream": {
|
||||
"version": "0.2.1",
|
||||
"from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"split": "^1.0.0",
|
||||
"superagent": "~0.21.0",
|
||||
"supererror": "^0.7.0",
|
||||
"supervisord-eventlistener": "^0.1.0",
|
||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
"underscore": "^1.7.0",
|
||||
"valid-url": "^1.0.9",
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ and replace it with a new one for an update.
|
||||
|
||||
Because we do not package things as Docker yet, we should be careful
|
||||
about the code here. We have to expect remains of an older setup code.
|
||||
For example, older supervisor or nginx configs might be around.
|
||||
For example, older systemd or nginx configs might be around.
|
||||
|
||||
The config directory is _part_ of the container and is not a VOLUME.
|
||||
Which is to say that the files will be nuked from one update to the next.
|
||||
@@ -40,7 +40,7 @@ version (see below) or the mysql/postgresql data etc.
|
||||
|
||||
* It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf.
|
||||
|
||||
* supervisor is then started
|
||||
* box services are then started
|
||||
|
||||
setup_infra.sh
|
||||
This setups containers like graphite, mail and the addons containers.
|
||||
|
||||
+4
-7
@@ -13,13 +13,10 @@ readonly DATA_DIR="/home/yellowtent/data"
|
||||
rm -rf "${CONFIG_DIR}"
|
||||
sudo -u yellowtent mkdir "${CONFIG_DIR}"
|
||||
|
||||
########## logrotate (default ubuntu runs this daily)
|
||||
rm -rf /etc/logrotate.d/*
|
||||
cp -r "${container_files}/logrotate/." /etc/logrotate.d/
|
||||
|
||||
########## supervisor
|
||||
rm -rf /etc/supervisor/*
|
||||
cp -r "${container_files}/supervisor/." /etc/supervisor/
|
||||
########## systemd
|
||||
cp -r "${container_files}/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable cloudron.target
|
||||
|
||||
########## sudoers
|
||||
rm /etc/sudoers.d/*
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/var/log/cloudron/*log {
|
||||
missingok
|
||||
notifempty
|
||||
size 100k
|
||||
nocompress
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/var/log/supervisor/*log {
|
||||
missingok
|
||||
copytruncate
|
||||
notifempty
|
||||
size 100k
|
||||
nocompress
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
[program:apphealthtask]
|
||||
command=/usr/bin/node "/home/yellowtent/box/apphealthtask.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/apphealthtask.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=2
|
||||
user=yellowtent
|
||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
||||
@@ -1,10 +0,0 @@
|
||||
[program:box]
|
||||
command=/usr/bin/node "/home/yellowtent/box/app.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/box.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=2
|
||||
user=yellowtent
|
||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*,connect-lastmile",BOX_ENV="cloudron",NODE_ENV="production"
|
||||
@@ -1,11 +0,0 @@
|
||||
[eventlistener:crashnotifier]
|
||||
command=/usr/bin/node "/home/yellowtent/box/crashnotifier.js"
|
||||
events=PROCESS_STATE
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=false
|
||||
stderr_logfile=/var/log/supervisor/crashnotifier.log
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=2
|
||||
user=yellowtent
|
||||
environment=HOME="/home/yellowtent",USER="yellowtent",BOX_ENV="cloudron",NODE_ENV="production"
|
||||
@@ -1,10 +0,0 @@
|
||||
[program:janitor]
|
||||
command=/usr/bin/node "/home/yellowtent/box/janitor.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/janitor.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=2
|
||||
user=yellowtent
|
||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
||||
@@ -1,10 +0,0 @@
|
||||
[program:oauthproxy]
|
||||
command=/usr/bin/node "/home/yellowtent/box/oauthproxy.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/oauthproxy.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=2
|
||||
user=yellowtent
|
||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
||||
@@ -1,33 +0,0 @@
|
||||
; supervisor config file
|
||||
|
||||
; http://coffeeonthekeyboard.com/using-supervisorctl-with-linux-permissions-but-without-root-or-sudo-977/
|
||||
[inet_http_server]
|
||||
port = 127.0.0.1:9001
|
||||
|
||||
[supervisord]
|
||||
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
|
||||
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
|
||||
logfile_maxbytes = 50MB
|
||||
logfile_backups=10
|
||||
loglevel = info
|
||||
nodaemon = false
|
||||
childlogdir = /var/log/supervisor/
|
||||
|
||||
; the below section must remain in the config file for RPC
|
||||
; (supervisorctl/web interface) to work, additional interfaces may be
|
||||
; added by defining them in separate rpcinterface: sections
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=http://127.0.0.1:9001
|
||||
|
||||
; The [include] section can just contain the "files" setting. This
|
||||
; setting can list multiple files (separated by whitespace or
|
||||
; newlines). It can also contain wildcards. The filenames are
|
||||
; interpreted as relative to this file. Included files *cannot*
|
||||
; include files themselves.
|
||||
|
||||
[include]
|
||||
files = conf.d/*.conf
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Cloudron App Health Monitor
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart="/home/yellowtent/box/apphealthtask.js"
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=50M
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Cloudron Admin
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart="/home/yellowtent/box/app.js"
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=200M
|
||||
TimeoutStopSec=5s
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Cloudron Smart Cloud
|
||||
Documentation=https://cloudron.io/documentation.html
|
||||
StopWhenUnneeded=true
|
||||
Requires=apphealthtask.service box.service janitor.service oauthproxy.service
|
||||
After=apphealthtask.service box.service janitor.service oauthproxy.service
|
||||
# AllowIsolate=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,15 @@
|
||||
# http://northernlightlabs.se/systemd.status.mail.on.unit.failure
|
||||
[Unit]
|
||||
Description=Cloudron Crash Notifier for %i
|
||||
# otherwise, systemd will kill this unit immediately as nobody requires it
|
||||
StopWhenUnneeded=false
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
ExecStart="/home/yellowtent/box/crashnotifier.js" %I
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Cloudron Janitor
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart="/home/yellowtent/box/janitor.js"
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=50M
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Cloudron OAuth Proxy Service
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart="/home/yellowtent/box/oauthproxy.js"
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
KillMode=process
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=50M
|
||||
|
||||
+3
-15
@@ -166,22 +166,10 @@ ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
|
||||
|
||||
set_progress "80" "Reloading supervisor"
|
||||
# looks like restarting supervisor completely is the only way to reload it
|
||||
service supervisor stop || true
|
||||
set_progress "80" "Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
|
||||
echo -n "Waiting for supervisord to stop"
|
||||
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "Starting supervisor"
|
||||
|
||||
service supervisor start
|
||||
|
||||
sleep 2 # give supervisor sometime to start the processes
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
set_progress "85" "Reloading nginx"
|
||||
nginx -s reload
|
||||
|
||||
+2
-10
@@ -2,14 +2,6 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
echo "Stopping box code"
|
||||
|
||||
service supervisor stop || true
|
||||
|
||||
echo -n "Waiting for supervisord to stop"
|
||||
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
echo "Stopping cloudron"
|
||||
|
||||
systemctl stop cloudron.target
|
||||
|
||||
+29
-64
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
@@ -429,43 +430,27 @@ function registerSubdomain(app, callback) {
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: [ record ] })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
subdomains.add(record, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Registered subdomain status: %s', res.status);
|
||||
debugApp(app, 'Registered subdomain.');
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body)));
|
||||
|
||||
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
|
||||
});
|
||||
updateApp(app, { dnsRecordId: changeId }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomain(app, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId);
|
||||
|
||||
if (!app.dnsRecordId) return callback(null);
|
||||
function unregisterSubdomain(app, location, callback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
// do not unregister bare domain because we show a error/cloudron info page there
|
||||
if (app.location === '') return updateApp(app, { dnsRecordId: null }, callback);
|
||||
if (location === '') return callback(null);
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId)
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error making request: %s', error);
|
||||
} else if (res.status !== 204) {
|
||||
debugApp(app, 'Error unregistering subdomain:', res.status, res.body);
|
||||
}
|
||||
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
|
||||
subdomains.remove(record, function (error) {
|
||||
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
@@ -486,21 +471,15 @@ function waitForDnsPropagation(app, callback) {
|
||||
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
|
||||
}
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, res) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
subdomains.status(app.dnsRecordId, function (error, result) {
|
||||
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
|
||||
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, res.status);
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
|
||||
|
||||
if (res.status !== 200) return retry(new Error(util.format('Error getting record status: %s %j', res.status, res.body)));
|
||||
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
|
||||
if (res.body.status !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, res.body.status)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -539,7 +518,7 @@ function install(app, callback) {
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -684,17 +663,15 @@ function restore(app, callback) {
|
||||
|
||||
// note that configure is called after an infra update as well
|
||||
function configure(app, callback) {
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
unregisterSubdomain(app, next);
|
||||
// oldConfig can be null during an infra update
|
||||
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -705,14 +682,8 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
@@ -726,14 +697,8 @@ function configure(app, callback) {
|
||||
|
||||
runApp.bind(null, app),
|
||||
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app)
|
||||
], next);
|
||||
},
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -837,7 +802,7 @@ function uninstall(app, callback) {
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
+165
-5
@@ -8,13 +8,18 @@ exports = module.exports = {
|
||||
getAWSCredentials: getAWSCredentials,
|
||||
|
||||
getSignedUploadUrl: getSignedUploadUrl,
|
||||
getSignedDownloadUrl: getSignedDownloadUrl
|
||||
getSignedDownloadUrl: getSignedDownloadUrl,
|
||||
|
||||
addSubdomain: addSubdomain,
|
||||
delSubdomain: delSubdomain,
|
||||
getChangeStatus: getChangeStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:aws'),
|
||||
SubdomainError = require('./subdomainerror.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -45,8 +50,6 @@ AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
|
||||
function getAWSCredentials(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getAWSCredentials()');
|
||||
|
||||
// CaaS
|
||||
if (config.token()) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
|
||||
@@ -55,8 +58,6 @@ function getAWSCredentials(callback) {
|
||||
if (result.statusCode !== 201) return callback(new Error(result.text));
|
||||
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
|
||||
|
||||
debug('getAWSCredentials()', result.body.credentials);
|
||||
|
||||
return callback(null, {
|
||||
accessKeyId: result.body.credentials.AccessKeyId,
|
||||
secretAccessKey: result.body.credentials.SecretAccessKey,
|
||||
@@ -120,3 +121,162 @@ function getSignedDownloadUrl(filename, callback) {
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
function getZoneByName(zoneName, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getZoneByName: %s', zoneName);
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
|
||||
var zone = result.HostedZones.filter(function (zone) {
|
||||
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
debug('getZoneByName: found zone', zone);
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'UPSERT',
|
||||
ResourceRecordSet: {
|
||||
Type: type,
|
||||
Name: fqdn,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'PriorRequestNotComplete') {
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error) {
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
debug('addSubdomain: success. changeInfoId:%j', result);
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delSubdomain(zoneName, subdomain, type, value, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
|
||||
|
||||
getZoneByName(zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var resourceRecordSet = {
|
||||
Name: fqdn,
|
||||
Type: type,
|
||||
ResourceRecords: [{
|
||||
Value: value
|
||||
}],
|
||||
Weight: 0,
|
||||
SetIdentifier: fqdn,
|
||||
TTL: 1
|
||||
};
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
Changes: [{
|
||||
Action: 'DELETE',
|
||||
ResourceRecordSet: resourceRecordSet
|
||||
}]
|
||||
},
|
||||
HostedZoneId: zone.Id
|
||||
};
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('delSubdomain: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('delSubdomain: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('delSubdomain: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('delSubdomain: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
} else if (error) {
|
||||
debug('delSubdomain: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
}
|
||||
|
||||
debug('delSubdomain: success');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getChangeStatus(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (changeId === '') return callback(null, 'INSYNC');
|
||||
|
||||
getAWSCredentials(function (error, credentials) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(credentials);
|
||||
route53.getChange({ Id: changeId }, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.ChangeInfo.Status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+54
-38
@@ -25,7 +25,6 @@ var apps = require('./apps.js'),
|
||||
AppsError = require('./apps.js').AppsError,
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
aws = require('./aws.js'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
clientdb = require('./clientdb.js'),
|
||||
@@ -40,6 +39,7 @@ var apps = require('./apps.js'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
@@ -56,7 +56,7 @@ var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
|
||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
|
||||
|
||||
var gAddMailDnsRecordsTimerId = null,
|
||||
var gAddDnsRecordsTimerId = null,
|
||||
gCloudronDetails = null; // cached cloudron details like region,size...
|
||||
|
||||
function debugApp(app, args) {
|
||||
@@ -110,20 +110,17 @@ function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') {
|
||||
addMailDnsRecords();
|
||||
addDnsRecords();
|
||||
}
|
||||
|
||||
// Send heartbeat once we are up and running, this speeds up the Cloudron creation, as otherwise we are bound to the cron.js settings
|
||||
sendHeartbeat();
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gAddMailDnsRecordsTimerId);
|
||||
gAddMailDnsRecordsTimerId = null;
|
||||
clearTimeout(gAddDnsRecordsTimerId);
|
||||
gAddDnsRecordsTimerId = null;
|
||||
|
||||
callback(null);
|
||||
}
|
||||
@@ -271,6 +268,9 @@ function getConfig(callback) {
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
// Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
|
||||
if (!config.get('dnsInSync')) return;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
@@ -280,8 +280,8 @@ function sendHeartbeat() {
|
||||
});
|
||||
}
|
||||
|
||||
function sendMailDnsRecordsRequest(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function addDnsRecords() {
|
||||
if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
|
||||
|
||||
var DKIM_SELECTOR = 'mail';
|
||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||
@@ -289,13 +289,20 @@ function sendMailDnsRecordsRequest(callback) {
|
||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
|
||||
if (publicKey === null) return callback(new Error('Error reading dkim public key'));
|
||||
if (publicKey === null) {
|
||||
console.error('Error reading dkim public key. Stop DNS setup.');
|
||||
return;
|
||||
}
|
||||
|
||||
// remove header, footer and new lines
|
||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||
|
||||
// note that dmarc requires special DNS records for external RUF and RUA
|
||||
var records = [
|
||||
// naked domain
|
||||
{ subdomain: '', type: 'A', value: sysinfo.getIp() },
|
||||
// webadmin domain
|
||||
{ subdomain: 'my', type: 'A', value: sysinfo.getIp() },
|
||||
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
|
||||
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
@@ -304,38 +311,47 @@ function sendMailDnsRecordsRequest(callback) {
|
||||
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
|
||||
];
|
||||
|
||||
debug('sendMailDnsRecords request:%s', JSON.stringify(records));
|
||||
debug('addDnsRecords:', records);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
||||
.set('Accept', 'application/json')
|
||||
.query({ token: config.token() })
|
||||
.send({ records: records })
|
||||
.end(function (error, res) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendMailDnsRecords status: %s', res.status);
|
||||
|
||||
if (res.status === 409) return callback(null); // already registered
|
||||
|
||||
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
|
||||
|
||||
return callback(null, res.body.ids);
|
||||
});
|
||||
}
|
||||
|
||||
function addMailDnsRecords() {
|
||||
if (config.get('mailDnsRecordIds').length !== 0) return; // already registered
|
||||
|
||||
sendMailDnsRecordsRequest(function (error, ids) {
|
||||
subdomains.addMany(records, function (error, changeIds) {
|
||||
if (error) {
|
||||
console.error('Mail DNS record addition failed', error);
|
||||
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000);
|
||||
console.error('Admin DNS record addition failed', error);
|
||||
gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Added Mail DNS records successfully');
|
||||
config.set('mailDnsRecordIds', ids);
|
||||
function checkIfInSync() {
|
||||
debug('addDnsRecords: Check if admin DNS record is in sync.');
|
||||
|
||||
var allDone = true;
|
||||
|
||||
async.each(changeIds, function (changeId, callback) {
|
||||
subdomains.status(changeId, function (error, result) {
|
||||
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
|
||||
|
||||
if (result !== 'done') allDone = false;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// retry if needed
|
||||
if (error || !allDone) {
|
||||
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
config.set('dnsInSync', true);
|
||||
|
||||
// send heartbeat after the dns records are done
|
||||
sendHeartbeat();
|
||||
|
||||
debug('addDnsRecords: done');
|
||||
});
|
||||
}
|
||||
|
||||
checkIfInSync();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ function initConfig() {
|
||||
accessKeyId: null, // selfhosting only
|
||||
secretAccessKey: null // selfhosting only
|
||||
};
|
||||
data.dnsInSync = false;
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
@@ -96,6 +97,7 @@ function initConfig() {
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.aws.backupBucket = 'testbucket';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
}
|
||||
@@ -109,6 +111,9 @@ function initConfig() {
|
||||
saveSync();
|
||||
}
|
||||
|
||||
// cleanup any old config file we have for tests
|
||||
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
|
||||
|
||||
initConfig();
|
||||
|
||||
// set(obj) or set(key, value)
|
||||
|
||||
+21
-5
@@ -9,6 +9,7 @@ function Locker() {
|
||||
this._operation = null;
|
||||
this._timestamp = null;
|
||||
this._watcherId = -1;
|
||||
this._lockDepth = 0; // recursive locks
|
||||
}
|
||||
util.inherits(Locker, EventEmitter);
|
||||
|
||||
@@ -24,6 +25,7 @@ Locker.prototype.lock = function (operation) {
|
||||
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
|
||||
|
||||
this._operation = operation;
|
||||
++this._lockDepth;
|
||||
this._timestamp = new Date();
|
||||
var that = this;
|
||||
this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5);
|
||||
@@ -35,17 +37,31 @@ Locker.prototype.lock = function (operation) {
|
||||
return null;
|
||||
};
|
||||
|
||||
Locker.prototype.recursiveLock = function (operation) {
|
||||
if (this._operation === operation) {
|
||||
++this._lockDepth;
|
||||
debug('Re-acquired : %s Depth : %s', this._operation, this._lockDepth);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.lock(operation);
|
||||
};
|
||||
|
||||
Locker.prototype.unlock = function (operation) {
|
||||
assert.strictEqual(typeof operation, 'string');
|
||||
|
||||
if (this._operation !== operation) throw new Error('Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
|
||||
|
||||
debug('Released : %s', this._operation);
|
||||
if (--this._lockDepth === 0) {
|
||||
debug('Released : %s', this._operation);
|
||||
|
||||
this._operation = null;
|
||||
this._timestamp = null;
|
||||
clearInterval(this._watcherId);
|
||||
this._watcherId = -1;
|
||||
this._operation = null;
|
||||
this._timestamp = null;
|
||||
clearInterval(this._watcherId);
|
||||
this._watcherId = -1;
|
||||
} else {
|
||||
debug('Recursive lock released : %s. Depth : %s', this._operation, this._lockDepth);
|
||||
}
|
||||
|
||||
this.emit('unlocked', operation);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ readonly program_name=$1
|
||||
|
||||
echo "${program_name}.log"
|
||||
echo "-------------------"
|
||||
tail --lines=100 /var/log/supervisor/${program_name}.log
|
||||
journalctl --no-pager -u ${program_name} -n 100
|
||||
echo
|
||||
echo
|
||||
echo "dmesg"
|
||||
@@ -31,7 +31,7 @@ echo
|
||||
echo
|
||||
echo "docker"
|
||||
echo "------"
|
||||
tail --lines=100 /var/log/upstart/docker.log
|
||||
journalctl --no-pager -u docker -n 50
|
||||
echo
|
||||
echo
|
||||
|
||||
|
||||
@@ -12,12 +12,6 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${OSTYPE}" == "darwin"* ]]; then
|
||||
# On Mac, brew installs supervisor in /usr/local/bin
|
||||
export PATH=$PATH:/usr/local/bin
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
nginx -s reload
|
||||
fi
|
||||
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ function initializeExpressSync() {
|
||||
// private routes
|
||||
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
|
||||
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
|
||||
router.get ('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
|
||||
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
|
||||
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = SubdomainError;
|
||||
|
||||
function SubdomainError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SubdomainError, Error);
|
||||
|
||||
SubdomainError.NOT_FOUND = 'No such domain';
|
||||
SubdomainError.INTERNAL_ERROR = 'Internal error';
|
||||
SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.FAILED_TOO_OFTEN = 'Failed too often';
|
||||
SubdomainError.ALREADY_EXISTS = 'Domain already exists';
|
||||
SubdomainError.BAD_FIELD = 'Bad Field';
|
||||
SubdomainError.BAD_STATE = 'Bad State';
|
||||
SubdomainError.INVALID_ZONE_NAME = 'Invalid domain name';
|
||||
SubdomainError.INVALID_TASK = 'Invalid task';
|
||||
@@ -0,0 +1,82 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
aws = require('./aws.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:subdomains'),
|
||||
util = require('util'),
|
||||
SubdomainError = require('./subdomainerror.js');
|
||||
|
||||
module.exports = exports = {
|
||||
add: add,
|
||||
addMany: addMany,
|
||||
remove: remove,
|
||||
status: status
|
||||
};
|
||||
|
||||
function add(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
assert.strictEqual(typeof record.subdomain, 'string');
|
||||
assert.strictEqual(typeof record.type, 'string');
|
||||
assert.strictEqual(typeof record.value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: ', record);
|
||||
|
||||
aws.addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeId);
|
||||
});
|
||||
}
|
||||
|
||||
function addMany(records, callback) {
|
||||
assert(util.isArray(records));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('addMany: ', records);
|
||||
|
||||
var changeIds = [];
|
||||
|
||||
async.eachSeries(records, function (record, callback) {
|
||||
add(record, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
changeIds.push(changeId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, changeIds);
|
||||
});
|
||||
}
|
||||
|
||||
function remove(record, callback) {
|
||||
assert.strictEqual(typeof record, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('remove: ', record);
|
||||
|
||||
aws.delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
|
||||
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
debug('deleteSubdomain: successfully deleted subdomain from aws.');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function status(changeId, callback) {
|
||||
assert.strictEqual(typeof changeId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('status: ', changeId);
|
||||
|
||||
aws.getChangeStatus(changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
});
|
||||
}
|
||||
+4
-6
@@ -17,10 +17,7 @@ var appdb = require('./appdb.js'),
|
||||
var gActiveTasks = { };
|
||||
var gPendingTasks = [ ];
|
||||
|
||||
// Task concurrency is 1 for two reasons:
|
||||
// 1. The backup scripts (app and box) turn off swap after finish disregarding other backup processes
|
||||
// 2. apptask getFreePort has race with multiprocess
|
||||
var TASK_CONCURRENCY = 1;
|
||||
var TASK_CONCURRENCY = 5;
|
||||
var NOOP_CALLBACK = function (error) { console.error(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
@@ -54,7 +51,8 @@ function uninitialize(callback) {
|
||||
|
||||
function startNextTask() {
|
||||
if (gPendingTasks.length === 0) return;
|
||||
assert.strictEqual(Object.keys(gActiveTasks).length, 0); // since we allow only one task at a time
|
||||
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
startAppTask(gPendingTasks.shift());
|
||||
}
|
||||
@@ -63,7 +61,7 @@ function startAppTask(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(!(appId in gActiveTasks));
|
||||
|
||||
var lockError = locker.lock(locker.OP_APPTASK);
|
||||
var lockError = locker.recursiveLock(locker.OP_APPTASK);
|
||||
|
||||
if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug('Reached concurrency limit, queueing task for %s', appId);
|
||||
|
||||
@@ -425,7 +425,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.reboot = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/cloudron/reboot').success(function(data, status) {
|
||||
$http.post(client.apiOrigin + '/api/v1/cloudron/reboot', { }).success(function(data, status) {
|
||||
if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
|
||||
@@ -14,39 +14,15 @@
|
||||
<div class="row shadow memory-app-container">
|
||||
<h2>Disk Usage</h2>
|
||||
<br/>
|
||||
<div class="col-md-4">
|
||||
<h4>Applications <span class="badge">{{ diskUsage['docker'].sum }} GB</span></h4>
|
||||
<canvas id="dockerDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['docker'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['docker'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['docker'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-offset-4 col-md-4">
|
||||
<h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4>
|
||||
<canvas id="boxDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['box'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['box'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['box'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>System (all) <span class="badge">{{ diskUsage['cloudron'].sum }} GB</span></h4>
|
||||
<canvas id="cloudronDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['cloudron'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['cloudron'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['cloudron'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -33,8 +33,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
|
||||
function renderDisk(type, free, reserved, used) {
|
||||
$scope.diskUsage[type] = {
|
||||
used: bytesToGigaBytes(used.datapoints[0][0]),
|
||||
reserved: bytesToGigaBytes(reserved.datapoints[0][0]),
|
||||
used: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0]),
|
||||
free: bytesToGigaBytes(free.datapoints[0][0]),
|
||||
sum: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0] + free.datapoints[0][0])
|
||||
};
|
||||
@@ -44,11 +43,6 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
color: "#2196F3",
|
||||
highlight: "#82C4F8",
|
||||
label: "Used"
|
||||
}, {
|
||||
value: $scope.diskUsage[type].reserved,
|
||||
color: "#f0ad4e",
|
||||
highlight: "#F8D9AC",
|
||||
label: "Reserved"
|
||||
}, {
|
||||
value: $scope.diskUsage[type].free,
|
||||
color:"#27CE65",
|
||||
@@ -98,7 +92,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
var options = {
|
||||
scaleOverride: true,
|
||||
scaleSteps: 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 20,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 60,
|
||||
scaleStartValue: 0
|
||||
};
|
||||
|
||||
@@ -111,21 +105,11 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
Client.graphs([
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-used)',
|
||||
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-used)',
|
||||
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-used)',
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-used)'
|
||||
], '-1min', function (error, data) {
|
||||
if (error) return console.log(error);
|
||||
|
||||
renderDisk('docker', data[0], data[1], data[2]);
|
||||
renderDisk('box', data[3], data[4], data[5]);
|
||||
renderDisk('cloudron', data[6], data[7], data[8]);
|
||||
renderDisk('box', data[0], data[1], data[2]);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user