'use strict'; /* global angular:false */ /* global $:false */ angular.module('Application').controller('SystemController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) { Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); }); $scope.config = Client.getConfig(); $scope.ready = false; $scope.services = []; $scope.disks = []; $scope.memory = null; $scope.errorMessage = ''; $scope.setError = function (context, error) { $scope.errorMessage = 'Error loading ' + context + ' : ' + error.message; }; // http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript function getRandomColor() { var letters = '0123456789ABCDEF'.split(''); var color = '#'; for (var i = 0; i < 6; i++ ) { color += letters[Math.floor(Math.random() * 16)]; } return color; } var colorIndex = 0; var colors = [ '#2196F3', '#3995b1', '#f0ad4e', '#ff4c4c' ]; function getNextColor() { if (colors[colorIndex+1]) return colors[colorIndex++]; return getRandomColor(); } function refresh(serviceName, callback) { callback = callback || function () {}; Client.getService(serviceName, function (error, result) { if (error) Client.error(error); var service = $scope.services.find(function (s) { return s.name === serviceName; }); if (!service) $scope.services.push(result); service.status = result.status; service.config = result.config; service.memoryUsed = result.memoryUsed; service.memoryPercent = result.memoryPercent; callback(null, service); }); } $scope.restartService = function (serviceName) { function waitForActive(serviceName) { refresh(serviceName, function (error, result) { if (result.status === 'active') return; setTimeout(function () { waitForActive(serviceName); }, 3000); }); } $scope.services.find(function (s) { return s.name === serviceName; }).status = 'starting'; Client.restartService(serviceName, function (error) { if (error) return Client.error(error); // show "busy" indicator for 3 seconds to show some ui activity setTimeout(function () { waitForActive(serviceName); }, 3000); }); }; $scope.serviceConfigure = { error: null, busy: false, service: null, // form model memoryLimit: 0, memoryTicks: [], show: function (service) { $scope.serviceConfigure.reset(); $scope.serviceConfigure.service = service; $scope.serviceConfigure.memoryLimit = service.config.memorySwap; // memory + swap // TODO improve those $scope.serviceConfigure.memoryTicks = []; // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below) // TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates $scope.serviceConfigure.memoryTicks = []; var npow2 = Math.pow(2, Math.ceil(Math.log($scope.memory.memory)/Math.log(2))); for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) { $scope.serviceConfigure.memoryTicks.push(i * 1024 * 1024); } $('#serviceConfigureModal').modal('show'); }, submit: function (memoryLimit) { $scope.serviceConfigure.busy = true; $scope.serviceConfigure.error = null; Client.configureService($scope.serviceConfigure.service.name, { memorySwap: memoryLimit, memory: memoryLimit/2 }, function (error) { $scope.serviceConfigure.busy = false; if (error) { $scope.serviceConfigure.error = error.message; return; } refresh($scope.serviceConfigure.service.name); $('#serviceConfigureModal').modal('hide'); $scope.serviceConfigure.reset(); }); }, reset: function () { $scope.serviceConfigure.busy = false; $scope.serviceConfigure.error = null; $scope.serviceConfigure.service = null; $scope.serviceConfigure.memoryLimit = 0; $scope.serviceConfigure.memoryTicks = []; $scope.serviceConfigureForm.$setPristine(); $scope.serviceConfigureForm.$setUntouched(); } }; $scope.reboot = { busy: false, show: function () { $scope.reboot.busy = false; $('#rebootModal').modal('show'); }, submit: function () { $scope.reboot.busy = true; Client.reboot(function (error) { if (error) return Client.error(error); $('#rebootModal').modal('hide'); // trigger refetch to show offline banner $timeout(function () { Client.getStatus(function () {}); }, 8000); }); } }; function updateDiskGraphs() { // https://graphite.readthedocs.io/en/latest/render_api.html#paths-and-wildcards // on scaleway, for some reason docker devices are collected as part of collectd // until we figure why just hardcode popular disk devices - https://www.mjmwired.net/kernel/Documentation/devices.txt Client.disks(function (error, result) { if (error) return $scope.setError('disk', error); // segregate locations into the correct disks based on 'filesystem' result.disks.forEach(function (disk, index) { disk.id = index; disk.contains = []; if (disk.filesystem === result.platformDataDisk) disk.contains.push({ label: 'Platform data', id: 'platformdata', usage: 0 }); if (disk.filesystem === result.boxDataDisk) disk.contains.push({ label: 'Box data', id: 'boxdata', usage: 0 }); if (disk.filesystem === result.dockerDataDisk) disk.contains.push({ label: 'Docker images', id: 'docker', usage: 0 }); if (disk.filesystem === result.mailDataDisk) disk.contains.push({ label: 'Email data', id: 'maildata', usage: 0 }); if (disk.filesystem === result.backupsDisk) disk.contains.push({ label: 'Backup data', id: 'cloudron-backup', usage: 0 }); const apps = Object.keys(result.apps).filter(function (appId) { return result.apps[appId] === disk.filesystem; }); apps.forEach(function (appId) { var app = Client.getCachedAppSync(appId); disk.contains.push({ app: app, label: app.label || app.fqdn, id: appId, usage: 0 }); }); }); $scope.disks = result.disks; // [ { filesystem, type, size, used, available, capacity, mountpoint }] // render data of each disk $scope.disks.forEach(function (disk, index) { // /dev/sda1 -> sda1 // /dev/mapper/foo -> mapper_foo (see #348) var diskName = disk.filesystem.slice(disk.filesystem.indexOf('/', 1) + 1); diskName = diskName.replace(/\//g, '_'); // use collectd instead of df data so the timeframe matches with the du data Client.graphs([ 'absolute(collectd.localhost.df-' + diskName + '.df_complex-free)', 'absolute(collectd.localhost.df-' + diskName + '.df_complex-reserved)', 'absolute(collectd.localhost.df-' + diskName + '.df_complex-used)' ], '-1min', {}, function (error, data) { if (error) return $scope.setError('disk', error); disk.size = data[2].datapoints[0][0] + data[1].datapoints[0][0] + data[0].datapoints[0][0]; disk.free = data[0].datapoints[0][0]; disk.occupied = data[2].datapoints[0][0] + data[1].datapoints[0][0]; colorIndex = 0; disk.contains.forEach(function (content) { content.color = getNextColor(); }); // get disk usage data var graphiteQueries = disk.contains.map(function (content) { return 'absolute(collectd.localhost.du-' + content.id + '.capacity-usage)'; }); Client.graphs(graphiteQueries, '-1day', { noNullPoints: true }, function (error, data) { if (error) return $scope.setError('disk', error); var usageOther = disk.occupied; data.forEach(function (d) { var content = disk.contains.find(function (content) { return d.target.indexOf(content.id) !== -1; }); if (!content) return; // didn't match any content var tmp = d.datapoints[d.datapoints.length-1][0]; content.usage = tmp; // deduct from overal disk usage to track other usageOther -= tmp; }); if (index === 0) { // the root mount point is the first disk disk.contains.push({ label: 'Everything else (Ubuntu, Swap, ...)', id: 'other', color: '#27CE65', usage: usageOther }); } }); }); }); }); } Client.onReady(function () { Client.memory(function (error, memory) { if (error) console.error(error); $scope.memory = memory; updateDiskGraphs(); Client.getServices(function (error, result) { if (error) return Client.error(error); $scope.services = result.map(function (serviceName) { return { name: serviceName }; }); // just kick off the status fetching $scope.services.forEach(function (s) { refresh(s.name); }); $scope.ready = true; }); }); }); Client.onReconnect(function () { $scope.reboot.busy = false; }); }]);