'use strict'; /* global angular */ /* global $ */ /* global async */ /* global Chart */ 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.memory = null; $scope.activeTab = 0; // 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(); } $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); }); } }; $scope.disks = { busy: true, errorMessage: '', disks: [], setError: function (error) { $scope.disks.errorMessage = 'Error loading disk : ' + error.message; }, update: function () { $scope.disks.busy = true; // 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.disks.setError(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.disks = result.disks; // [ { filesystem, type, size, used, available, capacity, mountpoint }] // render data of each disk async.eachSeries(result.disks, function (disk, iteratorCallback) { // /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)', // reserved for root (default: 5%) tune2fs -l/m 'absolute(collectd.localhost.df-' + diskName + '.df_complex-used)' ], '-1min', {}, function (error, data) { if (error) return iteratorCallback(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]; 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 iteratorCallback(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; }); disk.contains.sort(function (x, y) { return y.usage - x.usage; }); // sort by usage if ($scope.disks.disks[0] === disk) { // the root mount point is the first disk. keep this 'contains' in the end disk.contains.push({ label: 'Everything else (Ubuntu, Swap, etc)', id: 'other', color: '#27CE65', usage: usageOther }); } else { disk.contains.push({ label: 'Used', id: 'other', color: '#27CE65', usage: usageOther }); } iteratorCallback(); }); }); }, function iteratorDone(error) { if (error) $scope.disks.setError(error); $scope.disks.busy = false; }); }); } }; $scope.graphs = { error: {}, period: 6, periodLabel: '6 hours', memoryChart: null, diskChart: null, setPeriod: function (hours, label) { $scope.graphs.period = hours; $scope.graphs.periodLabel = label; $scope.graphs.show(); }, show: function () { var apps = Client.getInstalledApps(); // both in minutes var timePeriod = $scope.graphs.period * 60; var timeBucketSize = $scope.graphs.period > 24 ? (6*60) : 5; function fillGraph(canvasId, data, additionalData, label, chartPropertyName, max, valueDivider) { // translate the data from bytes to MB var datapoints = data.datapoints.map(function (d) { return parseInt((d[0] / valueDivider).toFixed(2)); }); var labels = datapoints.map(function (d, index) { var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSize)) * 60 *1000)); if ($scope.graphs.period > 24) { return dateTime.toLocaleDateString(); } else { return dateTime.toLocaleTimeString(); } }); var datasets = [{ label: label, backgroundColor: '#82C4F844', borderColor: '#2196F3', borderWidth: 1, radius: 0, data: datapoints }]; if (Array.isArray(additionalData)) { additionalData.forEach(function (data, index) { datasets.push({ label: apps[index].fqdn, fill: false, borderColor: getRandomColor(), borderWidth: 1, radius: 0, data: data.datapoints.map(function (d) { return parseInt((d[0] / valueDivider).toFixed(2)); }) }); }); } var data = { labels: labels, datasets: datasets }; var options = { maintainAspectRatio: true, aspectRatio: 2.5, legend: { display: false }, tooltips: { intersect: false }, scales: { xAxes: [{ ticks: { autoSkipPadding: 20, } }], yAxes: [{ ticks: { min: 0, max: max, beginAtZero: true } }] } }; var ctx = $(canvasId).get(0).getContext('2d'); if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy(); $scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: data, options: options }); } var cpuQuery = 'summarize(sum(collectd.localhost.aggregation-cpu-average.cpu-system, collectd.localhost.aggregation-cpu-average.cpu-user), "' + timeBucketSize + 'min", "avg")'; var systemMemoryQuery = 'summarize(sum(collectd.localhost.memory.memory-used, collectd.localhost.swap.swap-used), "' + timeBucketSize + 'min", "avg")'; var appQueries = []; apps.forEach(function (app) { appQueries.push('summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "' + timeBucketSize + 'min", "avg")') }); Client.graphs([ cpuQuery, systemMemoryQuery ].concat(appQueries), '-' + timePeriod + 'min', {}, function (error, result) { if (error) return console.error(error); fillGraph('#graphsCPUChart', result[0], null, 'CPU Usage', 'cpuChart', 100, 1); fillGraph('#graphsSystemMemoryChart', result[1], result.slice(2), 'Memory', 'memoryChart', Number.parseInt(($scope.memory.memory + $scope.memory.swap) / 1024 / 1024), 1024 * 1024); }); } }; Client.onReady(function () { Client.memory(function (error, memory) { if (error) console.error(error); $scope.memory = memory; $scope.disks.update(); }); }); Client.onReconnect(function () { $scope.reboot.busy = false; }); }]);