Compare commits

...

37 Commits

Author SHA1 Message Date
Girish Ramakrishnan 26aefadfba systemd: fix crashnotifier 2015-09-07 21:40:01 -07:00
Girish Ramakrishnan 51a28842cf systemd: pass the instance name as argument 2015-09-07 21:16:22 -07:00
Girish Ramakrishnan 210c2f3cc1 Output some logs in crashnotifier 2015-09-07 21:10:00 -07:00
Girish Ramakrishnan 773c326eb7 systemd: just wait for 5 seconds for box to die 2015-09-07 20:58:14 -07:00
Girish Ramakrishnan cb2fb026c5 systemd: do not restart crashnotifier 2015-09-07 20:54:58 -07:00
Girish Ramakrishnan a4731ad054 200m is a more sane memory limit 2015-09-07 20:48:29 -07:00
Girish Ramakrishnan aa33938fb5 systemd: fix config files 2015-09-07 20:46:32 -07:00
Girish Ramakrishnan edfe8f1ad0 disable pager when collecting logs 2015-09-07 20:27:27 -07:00
Girish Ramakrishnan 41399a2593 Make crashnotifier.js executable 2015-09-07 20:15:13 -07:00
Girish Ramakrishnan 2a4c467ab8 systemd: Fix crashnotifier 2015-09-07 20:14:37 -07:00
Girish Ramakrishnan 6be6092c0e Add memory limits on services 2015-09-07 19:16:34 -07:00
Girish Ramakrishnan e76584b0da Move from supervisor to systemd
This removes logrotate as well since we use systemd logging
2015-09-07 14:31:25 -07:00
Girish Ramakrishnan b3816615db run upto 5 apptasks in parallel
fixes #482
2015-09-05 09:17:46 -07:00
Johannes Zellner 212d0bd55a Revert "Add hack for broken app backup tarballs"
This reverts commit 9723951bfc.
2015-08-31 21:44:24 -07:00
Girish Ramakrishnan 712ada940e Add hack for broken app backup tarballs 2015-08-31 18:58:38 -07:00
Johannes Zellner ba690c6346 Add missing records argument 2015-08-30 23:00:01 -07:00
Johannes Zellner e910e19f57 Fix debug tag 2015-08-30 22:54:52 -07:00
Johannes Zellner 0c2532b0b5 Give default value to config.dnsInSync 2015-08-30 22:35:44 -07:00
Johannes Zellner 9c9b17a5f0 Remove cloudron.config prior to every test run 2015-08-30 22:35:44 -07:00
Johannes Zellner 816dea91ec Assert for dns record values 2015-08-30 22:35:44 -07:00
Johannes Zellner c228f8d4d5 Merge admin dns and mail dns setup
This now also checks if the mail records are in sync
2015-08-30 22:35:43 -07:00
Johannes Zellner 05bb99fad4 give dns record changeIds as a result for addMany() 2015-08-30 22:35:43 -07:00
Johannes Zellner 51b2457b3d Setup webadmin domain on the box side 2015-08-30 22:35:43 -07:00
Girish Ramakrishnan ed71fca23e Fix css 2015-08-30 22:25:18 -07:00
Girish Ramakrishnan 20e8e72ac2 reserved blocks are used 2015-08-30 22:24:57 -07:00
Girish Ramakrishnan 13fe0eb882 Only display one donut for memory usage 2015-08-30 22:13:01 -07:00
Girish Ramakrishnan e0476c9030 Reboot is a post route 2015-08-30 21:38:54 -07:00
Girish Ramakrishnan fca82fd775 Display upto 600mb for apps 2015-08-30 17:21:44 -07:00
Johannes Zellner 37c8ba8ddd Reduce logging for aws credentials 2015-08-30 17:03:10 -07:00
Johannes Zellner f87011b5c2 Also always check for dns propagation 2015-08-30 17:00:23 -07:00
Johannes Zellner 7f149700f8 Remove wrong optimization for subdomain records 2015-08-30 16:54:33 -07:00
Johannes Zellner 78ba9070fc use config.appFqdn() to handle custom domains 2015-08-30 16:29:09 -07:00
Johannes Zellner e31e5e1f69 Reuse dnsRecordId for record status id 2015-08-30 15:58:54 -07:00
Johannes Zellner 31d9027677 Query dns status with aws statusId 2015-08-30 15:51:33 -07:00
Johannes Zellner debcd6f353 aws provides uppercase properties 2015-08-30 15:47:08 -07:00
Johannes Zellner 5cb1681922 Fixup the zonename comparison 2015-08-30 15:37:18 -07:00
Johannes Zellner 9074bccea0 Move subdomain management from appstore to box 2015-08-30 15:29:14 -07:00
37 changed files with 536 additions and 350 deletions
+1 -5
View File
@@ -4,10 +4,6 @@ docs/
webadmin/dist/ webadmin/dist/
setup/splash/website/ setup/splash/website/
# vim swam files # vim swap files
*.swp *.swp
# supervisor
supervisord.pid
supervisord.log
-1
View File
@@ -4,7 +4,6 @@ The Box
Development setup Development setup
----------------- -----------------
* sudo useradd -m yellowtent * sudo useradd -m yellowtent
** This dummy user is required for supervisor 'box' configs
** Add admin-localhost as 127.0.0.1 in /etc/hosts ** Add admin-localhost as 127.0.0.1 in /etc/hosts
** All apps will be installed as hypened-subdomains of localhost. You should add ** All apps will be installed as hypened-subdomains of localhost. You should add
hyphened-subdomains of your apps into /etc/hosts hyphened-subdomains of your apps into /etc/hosts
Regular → Executable
+20 -26
View File
@@ -2,20 +2,12 @@
'use strict'; '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'), var assert = require('assert'),
mailer = require('./src/mailer.js'), mailer = require('./src/mailer.js'),
safe = require('safetydance'), safe = require('safetydance'),
supervisor = require('supervisord-eventlistener'),
path = require('path'), path = require('path'),
util = require('util'); util = require('util');
var gLastNotifyTime = {};
var gCooldownTime = 1000 * 60 * 5; // 5 min
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh'); var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
function collectLogs(program, callback) { function collectLogs(program, callback) {
@@ -26,28 +18,30 @@ function collectLogs(program, callback) {
callback(null, logs); callback(null, logs);
} }
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) { function sendCrashNotification(processName) {
if (data.expected === '1') return console.error('Normal app %s exit', data.processname); collectLogs(processName, function (error, result) {
console.error('%s exited unexpectedly', data.processname);
collectLogs(data.processname, function (error, result) {
if (error) { if (error) {
console.error('Failed to collect logs.', error); console.error('Failed to collect logs.', error);
result = util.format('Failed to collect logs.', error); result = util.format('Failed to collect logs.', error);
} }
if (!gLastNotifyTime[data.processname] || gLastNotifyTime[data.processname] < Date.now() - gCooldownTime) { console.log('Sending crash notification email for', processName);
console.error('Send mail.'); mailer.sendCrashNotification(processName, result);
mailer.sendCrashNotification(data.processname, result);
gLastNotifyTime[data.processname] = Date.now();
} else {
console.error('Do not send mail, already sent one recently.');
}
}); });
}); }
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...');
});
+7 -13
View File
@@ -9,22 +9,22 @@
}, },
"aws-sdk": { "aws-sdk": {
"version": "2.1.46", "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", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
"dependencies": { "dependencies": {
"sax": { "sax": {
"version": "0.5.3", "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" "resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
}, },
"xml2js": { "xml2js": {
"version": "0.2.8", "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" "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
}, },
"xmlbuilder": { "xmlbuilder": {
"version": "0.4.2", "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" "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
} }
} }
@@ -156,16 +156,15 @@
"connect-lastmile": { "connect-lastmile": {
"version": "0.0.13", "version": "0.0.13",
"from": "connect-lastmile@0.0.13", "from": "connect-lastmile@0.0.13",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "2.1.3", "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", "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"dependencies": { "dependencies": {
"ms": { "ms": {
"version": "0.7.0", "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" "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
} }
} }
@@ -2268,7 +2267,7 @@
}, },
"safetydance": { "safetydance": {
"version": "0.0.19", "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" "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
}, },
"semver": { "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": { "tail-stream": {
"version": "0.2.1", "version": "0.2.1",
"from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz", "from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
-1
View File
@@ -61,7 +61,6 @@
"split": "^1.0.0", "split": "^1.0.0",
"superagent": "~0.21.0", "superagent": "~0.21.0",
"supererror": "^0.7.0", "supererror": "^0.7.0",
"supervisord-eventlistener": "^0.1.0",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz", "tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"underscore": "^1.7.0", "underscore": "^1.7.0",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
+2 -2
View File
@@ -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 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. 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. 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. 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. * 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 setup_infra.sh
This setups containers like graphite, mail and the addons containers. This setups containers like graphite, mail and the addons containers.
+4 -7
View File
@@ -13,13 +13,10 @@ readonly DATA_DIR="/home/yellowtent/data"
rm -rf "${CONFIG_DIR}" rm -rf "${CONFIG_DIR}"
sudo -u yellowtent mkdir "${CONFIG_DIR}" sudo -u yellowtent mkdir "${CONFIG_DIR}"
########## logrotate (default ubuntu runs this daily) ########## systemd
rm -rf /etc/logrotate.d/* cp -r "${container_files}/systemd/." /etc/systemd/system/
cp -r "${container_files}/logrotate/." /etc/logrotate.d/ systemctl daemon-reload
systemctl enable cloudron.target
########## supervisor
rm -rf /etc/supervisor/*
cp -r "${container_files}/supervisor/." /etc/supervisor/
########## sudoers ########## sudoers
rm /etc/sudoers.d/* rm /etc/sudoers.d/*
-6
View File
@@ -1,6 +0,0 @@
/var/log/cloudron/*log {
missingok
notifempty
size 100k
nocompress
}
-7
View File
@@ -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
+17
View File
@@ -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
+10
View File
@@ -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
+16
View File
@@ -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
View File
@@ -166,22 +166,10 @@ ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
mysql -u root -p${mysql_root_password} \ 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 -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" set_progress "80" "Starting Cloudron"
# looks like restarting supervisor completely is the only way to reload it systemctl start cloudron.target
service supervisor stop || true
echo -n "Waiting for supervisord to stop" sleep 2 # give systemd sometime to start the processes
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
set_progress "85" "Reloading nginx" set_progress "85" "Reloading nginx"
nginx -s reload nginx -s reload
+2 -10
View File
@@ -2,14 +2,6 @@
set -eu -o pipefail set -eu -o pipefail
echo "Stopping box code" echo "Stopping cloudron"
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 ""
systemctl stop cloudron.target
+29 -64
View File
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
util = require('util'), 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 // 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() }; var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
superagent subdomains.add(record, function (error, changeId) {
.post(config.apiServerOrigin() + '/api/v1/subdomains') if (error) return callback(error);
.set('Accept', 'application/json')
.query({ token: config.token() })
.send({ records: [ record ] })
.end(function (error, res) {
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 updateApp(app, { dnsRecordId: changeId }, callback);
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);
});
} }
function unregisterSubdomain(app, callback) { function unregisterSubdomain(app, location, callback) {
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId); debugApp(app, 'Unregistering subdomain: %s', location);
if (!app.dnsRecordId) return callback(null);
// do not unregister bare domain because we show a error/cloudron info page there // 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 var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId) subdomains.remove(record, function (error) {
.query({ token: config.token() }) if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
.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);
}
updateApp(app, { dnsRecordId: null }, callback); updateApp(app, { dnsRecordId: null }, callback);
}); });
} }
function removeIcon(app, callback) { function removeIcon(app, callback) {
@@ -486,21 +471,15 @@ function waitForDnsPropagation(app, callback) {
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000); setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
} }
superagent subdomains.status(app.dnsRecordId, function (error, result) {
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status') if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
.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));
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 // updates the app object and the database
@@ -539,7 +518,7 @@ function install(app, callback) {
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
unregisterSubdomain.bind(null, app), unregisterSubdomain.bind(null, app, app.location),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs // removeIcon.bind(null, app), // do not remove icon for non-appstore installs
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
@@ -684,17 +663,15 @@ function restore(app, callback) {
// note that configure is called after an infra update as well // note that configure is called after an infra update as well
function configure(app, callback) { function configure(app, callback) {
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
async.series([ async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
function (next) { function (next) {
if (!locationChanged) return next(); // oldConfig can be null during an infra update
unregisterSubdomain(app, next); if (!app.oldConfig || app.oldConfig.location === app.location) return next();
unregisterSubdomain(app, app.oldConfig.location, next);
}, },
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
unconfigureNginx.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' }), updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app), allocateOAuthProxyCredentials.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
if (!locationChanged) return next(); registerSubdomain.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app)
], next);
},
// re-setup addons since they rely on the app's fqdn (e.g oauth) // re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }), updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
@@ -726,14 +697,8 @@ function configure(app, callback) {
runApp.bind(null, app), runApp.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
if (!locationChanged) return next(); exports._waitForDnsPropagation.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app)
], next);
},
// done! // done!
function (callback) { function (callback) {
@@ -837,7 +802,7 @@ function uninstall(app, callback) {
deleteImage.bind(null, app, app.manifest), deleteImage.bind(null, app, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }), 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' }), updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
+165 -5
View File
@@ -8,13 +8,18 @@ exports = module.exports = {
getAWSCredentials: getAWSCredentials, getAWSCredentials: getAWSCredentials,
getSignedUploadUrl: getSignedUploadUrl, getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
}; };
var assert = require('assert'), var assert = require('assert'),
AWS = require('aws-sdk'), AWS = require('aws-sdk'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:aws'), debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'), superagent = require('superagent'),
util = require('util'); util = require('util');
@@ -45,8 +50,6 @@ AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
function getAWSCredentials(callback) { function getAWSCredentials(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('getAWSCredentials()');
// CaaS // CaaS
if (config.token()) { if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials'; 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.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response')); if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
debug('getAWSCredentials()', result.body.credentials);
return callback(null, { return callback(null, {
accessKeyId: result.body.credentials.AccessKeyId, accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey, secretAccessKey: result.body.credentials.SecretAccessKey,
@@ -120,3 +121,162 @@ function getSignedDownloadUrl(filename, callback) {
callback(null, { url: url, sessionToken: credentials.sessionToken }); 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
View File
@@ -25,7 +25,6 @@ var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError, AppsError = require('./apps.js').AppsError,
assert = require('assert'), assert = require('assert'),
async = require('async'), async = require('async'),
aws = require('./aws.js'),
backups = require('./backups.js'), backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError, BackupsError = require('./backups.js').BackupsError,
clientdb = require('./clientdb.js'), clientdb = require('./clientdb.js'),
@@ -40,6 +39,7 @@ var apps = require('./apps.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
SettingsError = settings.SettingsError, SettingsError = settings.SettingsError,
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
tokendb = require('./tokendb.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'), BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update'; 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... gCloudronDetails = null; // cached cloudron details like region,size...
function debugApp(app, args) { function debugApp(app, args) {
@@ -110,20 +110,17 @@ function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV !== 'test') { 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); callback(null);
} }
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
clearTimeout(gAddMailDnsRecordsTimerId); clearTimeout(gAddDnsRecordsTimerId);
gAddMailDnsRecordsTimerId = null; gAddDnsRecordsTimerId = null;
callback(null); callback(null);
} }
@@ -271,6 +268,9 @@ function getConfig(callback) {
} }
function sendHeartbeat() { 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'; 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) { 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) { function addDnsRecords() {
assert.strictEqual(typeof callback, 'function'); if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
var DKIM_SELECTOR = 'mail'; var DKIM_SELECTOR = 'mail';
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io'; 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 dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8'); 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 // remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join(''); publicKey = publicKey.split('\n').slice(1, -2).join('');
// note that dmarc requires special DNS records for external RUF and RUA // note that dmarc requires special DNS records for external RUF and RUA
var records = [ 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 // 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"' }, { subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
// t=s limits the domainkey to this domain and not it's subdomains // 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 + '"' } { 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 subdomains.addMany(records, function (error, changeIds) {
.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) {
if (error) { if (error) {
console.error('Mail DNS record addition failed', error); console.error('Admin DNS record addition failed', error);
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000); gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
return; return;
} }
debug('Added Mail DNS records successfully'); function checkIfInSync() {
config.set('mailDnsRecordIds', ids); 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();
}); });
} }
+5
View File
@@ -80,6 +80,7 @@ function initConfig() {
accessKeyId: null, // selfhosting only accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only secretAccessKey: null // selfhosting only
}; };
data.dnsInSync = false;
if (exports.CLOUDRON) { if (exports.CLOUDRON) {
data.port = 3000; data.port = 3000;
@@ -96,6 +97,7 @@ function initConfig() {
name: 'boxtest' name: 'boxtest'
}; };
data.token = 'APPSTORE_TOKEN'; data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
} else { } else {
assert(false, 'Unknown environment. This should not happen!'); assert(false, 'Unknown environment. This should not happen!');
} }
@@ -109,6 +111,9 @@ function initConfig() {
saveSync(); saveSync();
} }
// cleanup any old config file we have for tests
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
initConfig(); initConfig();
// set(obj) or set(key, value) // set(obj) or set(key, value)
+21 -5
View File
@@ -9,6 +9,7 @@ function Locker() {
this._operation = null; this._operation = null;
this._timestamp = null; this._timestamp = null;
this._watcherId = -1; this._watcherId = -1;
this._lockDepth = 0; // recursive locks
} }
util.inherits(Locker, EventEmitter); 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); if (this._operation !== null) return new Error('Already locked for ' + this._operation);
this._operation = operation; this._operation = operation;
++this._lockDepth;
this._timestamp = new Date(); this._timestamp = new Date();
var that = this; var that = this;
this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5); this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5);
@@ -35,17 +37,31 @@ Locker.prototype.lock = function (operation) {
return null; 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) { Locker.prototype.unlock = function (operation) {
assert.strictEqual(typeof operation, 'string'); 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 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._operation = null;
this._timestamp = null; this._timestamp = null;
clearInterval(this._watcherId); clearInterval(this._watcherId);
this._watcherId = -1; this._watcherId = -1;
} else {
debug('Recursive lock released : %s. Depth : %s', this._operation, this._lockDepth);
}
this.emit('unlocked', operation); this.emit('unlocked', operation);
+2 -2
View File
@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log" echo "${program_name}.log"
echo "-------------------" echo "-------------------"
tail --lines=100 /var/log/supervisor/${program_name}.log journalctl --no-pager -u ${program_name} -n 100
echo echo
echo echo
echo "dmesg" echo "dmesg"
@@ -31,7 +31,7 @@ echo
echo echo
echo "docker" echo "docker"
echo "------" echo "------"
tail --lines=100 /var/log/upstart/docker.log journalctl --no-pager -u docker -n 50
echo echo
echo echo
-6
View File
@@ -12,12 +12,6 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0 exit 0
fi 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 if [[ "${BOX_ENV}" == "cloudron" ]]; then
nginx -s reload nginx -s reload
fi fi
+1 -1
View File
@@ -97,7 +97,7 @@ function initializeExpressSync() {
// private routes // private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig); 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.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/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate); router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs); router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
+39
View File
@@ -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';
+82
View File
@@ -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
View File
@@ -17,10 +17,7 @@ var appdb = require('./appdb.js'),
var gActiveTasks = { }; var gActiveTasks = { };
var gPendingTasks = [ ]; var gPendingTasks = [ ];
// Task concurrency is 1 for two reasons: var TASK_CONCURRENCY = 5;
// 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 NOOP_CALLBACK = function (error) { console.error(error); }; var NOOP_CALLBACK = function (error) { console.error(error); };
function initialize(callback) { function initialize(callback) {
@@ -54,7 +51,8 @@ function uninitialize(callback) {
function startNextTask() { function startNextTask() {
if (gPendingTasks.length === 0) return; 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()); startAppTask(gPendingTasks.shift());
} }
@@ -63,7 +61,7 @@ function startAppTask(appId) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert(!(appId in gActiveTasks)); 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) { if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug('Reached concurrency limit, queueing task for %s', appId); debug('Reached concurrency limit, queueing task for %s', appId);
+1 -1
View File
@@ -425,7 +425,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}; };
Client.prototype.reboot = function (callback) { 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)); if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data); callback(null, data);
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
+1 -25
View File
@@ -14,39 +14,15 @@
<div class="row shadow memory-app-container"> <div class="row shadow memory-app-container">
<h2>Disk Usage</h2> <h2>Disk Usage</h2>
<br/> <br/>
<div class="col-md-4"> <div class="col-md-offset-4 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>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['docker'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['docker'].used }} GB</span>
</p>
</div>
<div class="col-md-4">
<h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4> <h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4>
<canvas id="boxDiskUsageChart" width="200" height="200"></canvas> <canvas id="boxDiskUsageChart" width="200" height="200"></canvas>
<p> <p>
<span class="text-success">Free {{ diskUsage['box'].free }} GB</span> <span class="text-success">Free {{ diskUsage['box'].free }} GB</span>
&nbsp; &nbsp;
<span class="text-warning">Reserved {{ diskUsage['box'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['box'].used }} GB</span> <span class="text-primary">Used {{ diskUsage['box'].used }} GB</span>
</p> </p>
</div> </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>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['cloudron'].reserved }} GB</span>
&nbsp;
<span class="text-primary">Used {{ diskUsage['cloudron'].used }} GB</span>
</p>
</div>
</div> </div>
<br/> <br/>
+4 -20
View File
@@ -33,8 +33,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
function renderDisk(type, free, reserved, used) { function renderDisk(type, free, reserved, used) {
$scope.diskUsage[type] = { $scope.diskUsage[type] = {
used: bytesToGigaBytes(used.datapoints[0][0]), used: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0]),
reserved: bytesToGigaBytes(reserved.datapoints[0][0]),
free: bytesToGigaBytes(free.datapoints[0][0]), free: bytesToGigaBytes(free.datapoints[0][0]),
sum: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0] + 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", color: "#2196F3",
highlight: "#82C4F8", highlight: "#82C4F8",
label: "Used" label: "Used"
}, {
value: $scope.diskUsage[type].reserved,
color: "#f0ad4e",
highlight: "#F8D9AC",
label: "Reserved"
}, { }, {
value: $scope.diskUsage[type].free, value: $scope.diskUsage[type].free,
color:"#27CE65", color:"#27CE65",
@@ -98,7 +92,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
var options = { var options = {
scaleOverride: true, scaleOverride: true,
scaleSteps: 10, scaleSteps: 10,
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 20, scaleStepWidth: $scope.activeApp === 'system' ? 200 : 60,
scaleStartValue: 0 scaleStartValue: 0
}; };
@@ -111,21 +105,11 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
Client.graphs([ Client.graphs([
'averageSeries(collectd.localhost.df-loop0.df_complex-free)', 'averageSeries(collectd.localhost.df-loop0.df_complex-free)',
'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)', 'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)',
'averageSeries(collectd.localhost.df-loop0.df_complex-used)', '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)',
], '-1min', function (error, data) { ], '-1min', function (error, data) {
if (error) return console.log(error); if (error) return console.log(error);
renderDisk('docker', data[0], data[1], data[2]); renderDisk('box', data[0], data[1], data[2]);
renderDisk('box', data[3], data[4], data[5]);
renderDisk('cloudron', data[6], data[7], data[8]);
}); });
}; };