From 31189be2c8ec5fa1be2eb396f03d454a7b358608 Mon Sep 17 00:00:00 2001 From: badrAZ Date: Fri, 27 Jan 2017 18:22:01 +0100 Subject: [PATCH] Implementation (#15) --- packages/xo-server-usage-report/README.md | 3 +- .../xo-server-usage-report/images/logo.png | Bin 0 -> 10747 bytes packages/xo-server-usage-report/images/xo.png | Bin 0 -> 11025 bytes packages/xo-server-usage-report/package.json | 6 +- packages/xo-server-usage-report/src/index.js | 805 +++++++++--------- .../templates/xoReport.html | 597 +++++++++++++ 6 files changed, 1008 insertions(+), 403 deletions(-) create mode 100644 packages/xo-server-usage-report/images/logo.png create mode 100644 packages/xo-server-usage-report/images/xo.png create mode 100644 packages/xo-server-usage-report/templates/xoReport.html diff --git a/packages/xo-server-usage-report/README.md b/packages/xo-server-usage-report/README.md index e16f7df78..3398b8d78 100644 --- a/packages/xo-server-usage-report/README.md +++ b/packages/xo-server-usage-report/README.md @@ -12,7 +12,8 @@ Installation of the [npm package](https://npmjs.org/package/xo-server-usage-repo ## Usage -**TODO** +Like all other xo-server plugins, it can be configured directly via +the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html). ## Development diff --git a/packages/xo-server-usage-report/images/logo.png b/packages/xo-server-usage-report/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8f9627e1c7402773421fcaaf9d451ab16b9259 GIT binary patch literal 10747 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 literal 0 HcmV?d00001 diff --git a/packages/xo-server-usage-report/package.json b/packages/xo-server-usage-report/package.json index cce3e763f..83a1b1be2 100644 --- a/packages/xo-server-usage-report/package.json +++ b/packages/xo-server-usage-report/package.json @@ -34,6 +34,9 @@ }, "dependencies": { "babel-runtime": "^6.18.0", + "cron": "^1.2.1", + "handlebars": "^4.0.6", + "html-pdf": "^2.1.0", "lodash": "^4.17.2", "promise-toolbox": "^0.8.0" }, @@ -63,7 +66,8 @@ }, "babel": { "plugins": [ - "lodash" + "lodash", + "transform-runtime" ], "presets": [ [ diff --git a/packages/xo-server-usage-report/src/index.js b/packages/xo-server-usage-report/src/index.js index b64fffcfb..9005c0af1 100644 --- a/packages/xo-server-usage-report/src/index.js +++ b/packages/xo-server-usage-report/src/index.js @@ -1,5 +1,58 @@ -import { all } from 'promise-toolbox' -import { forEach, isFinite, map, sortBy } from 'lodash' +import Handlebars from 'handlebars' +import pdf from 'html-pdf' +import { CronJob } from 'cron' +import { + assign, + concat, + difference, + filter, + forEach, + isFinite, + map, + orderBy, + round, + values, + zipObject +} from 'lodash' +import { + fromCallback, + promisify +} from 'promise-toolbox' +import { + readFile, + writeFile +} from 'fs' + +// =================================================================== + +const pReadFile = promisify(readFile) +const pWriteFile = promisify(writeFile) + +const currDate = new Date().toISOString().slice(0, 10) + +const absolutePath = process.platform === 'linux' ? `file://${__dirname}` : `${__dirname}` +const htmlPath = `${__dirname}/../templates/xoReport.html` +const imgVates = `${absolutePath}/../images/logo.png` // Only absolute path is supported +const imgXo = `${absolutePath}/../images/xo.png` + +const compareOperators = { + '>': (l, r) => l > r +} +const mathOperators = { + '+': (l, r) => l + r +} + +const gibPower = Math.pow(2, 30) +const mibPower = Math.pow(2, 20) +const kibPower = Math.pow(2, 10) +let template = null + +;(async () => { + const html = await pReadFile(htmlPath, 'utf8') + template = Handlebars.compile(html) +})() + +// =================================================================== export const configurationSchema = { type: 'object', @@ -13,7 +66,8 @@ export const configurationSchema = { }, periodicity: { type: 'string', - description: 'enter monthly or weekly' + enum: ['monthly', 'weekly'], + description: 'If you choose weekly you will receive the report every sunday and if you choose monthly you will receive it every first day of the month.' } }, @@ -22,429 +76,378 @@ export const configurationSchema = { } // =================================================================== + +Handlebars.registerHelper('compare', function (lvalue, operator, rvalue, options) { + if (arguments.length < 3) { + throw new Error('Handlerbars Helper "compare" needs 2 parameters') + } + + if (!compareOperators[operator]) { + throw new Error(`Handlerbars Helper "compare" doesn't know the operator ${operator}`) + } + + return compareOperators[operator](lvalue, rvalue) ? options.fn(this) : options.inverse(this) +}) + +Handlebars.registerHelper('math', function (lvalue, operator, rvalue, options) { + if (arguments.length < 3) { + throw new Error('Handlerbars Helper "math" needs 2 parameters') + } + + if (!mathOperators[operator]) { + throw new Error(`Handlerbars Helper "math" doesn't know the operator ${operator}`) + } + + return mathOperators[operator](+lvalue, +rvalue) +}) + +// =================================================================== + function computeMean (values) { let sum = 0 - let tot = 0 - forEach(values, (val) => { - sum += val || 0 - tot += val ? 1 : 0 - }) - return sum / tot -} -function computeMax (values) { - let max = -Infinity - forEach(values, (val) => { - if (val && val > max) { - max = val + let n = 0 + forEach(values, val => { + if (isFinite(val)) { + sum += val + n++ } }) - return max -} -function computeMin (values) { - let min = +Infinity - forEach(values, (val) => { - if (val && val < min) { - min = val - } - }) - return min -} -function computeCpuMax (cpus) { - return sortArray(cpus.map(computeMax)) -} -function computeCpuMin (cpus) { - return computeMin(cpus.map(computeMin)) -} -function computeCpuMean (cpus) { - return computeMean(cpus.map(computeMean)) + + return sum / n } -function compareNumbersDesc (a, b) { - if (a > b) { - return -1 - } - if (a < b) { - return 1 - } - return 0 +const computeDoubleMean = val => computeMean(val.map(computeMean)) + +function computeMeans (objects, options) { + return zipObject( + options, + map( + options, + opt => round(computeMean(map(objects, opt)), 2) + ) + ) } -function sortArray (values) { - let n = 3 - let sort = values.sort(compareNumbersDesc) - return sort.slice(0, n) + +function getTop (objects, options) { + return zipObject( + options, + map( + options, + opt => map( + orderBy(objects, object => { + const value = object[opt] + + return isNaN(value) ? -Infinity : value + }, 'desc').slice(0, 3), + obj => ({ + uuid: obj.uuid, + value: round(obj[opt], 2) + }) + ) + ) + ) +} + +function conputePercentage (curr, prev, options) { + return zipObject( + options, + map( + options, + opt => prev[opt] === 0 ? 'NONE' : `${round((curr[opt] - prev[opt]) * 100 / prev[opt], 2)}` + ) + ) +} + +function getDiff (oldElements, newElements) { + return { + added: difference(oldElements, newElements), + removed: difference(newElements, oldElements) + } +} + +// =================================================================== + +function getVmsStats ({ + runningVms, + xo +}) { + return Promise.all(map(runningVms, async vm => { + const vmStats = await xo.getXapiVmStats(vm, 'days') + return { + uuid: vm.uuid.split('-')[0], + cpu: computeDoubleMean(vmStats.stats.cpus), + ram: computeMean(vmStats.stats.memoryUsed) / gibPower, + diskRead: computeDoubleMean(values(vmStats.stats.xvds.r)) / mibPower, + diskWrite: computeDoubleMean(values(vmStats.stats.xvds.w)) / mibPower, + netReception: computeDoubleMean(vmStats.stats.vifs.rx) / kibPower, + netTransmission: computeDoubleMean(vmStats.stats.vifs.tx) / kibPower + } + })) +} + +function getHostsStats ({ + runningHosts, + xo +}) { + return Promise.all(map(runningHosts, async host => { + const hostStats = await xo.getXapiHostStats(host, 'days') + return { + uuid: host.uuid.split('-')[0], + cpu: computeDoubleMean(hostStats.stats.cpus), + ram: computeMean(hostStats.stats.memoryUsed) / gibPower, + load: computeMean(hostStats.stats.load), + netReception: computeDoubleMean(hostStats.stats.pifs.rx) / kibPower, + netTransmission: computeDoubleMean(hostStats.stats.pifs.tx) / kibPower + } + })) +} + +function computeGlobalVmsStats ({ + haltedVms, + vmsStats, + xo +}) { + const allVms = concat(map(vmsStats, 'uuid'), map(haltedVms, vm => vm.uuid.split('-')[0])) + + return assign(computeMeans(vmsStats, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']), { + number: allVms.length, + allVms + }) +} + +function computeGlobalHostsStats ({ + haltedHosts, + hostsStats, + xo +}) { + const allHosts = concat(map(hostsStats, 'uuid'), map(haltedHosts, host => host.uuid.split('-')[0])) + + return assign(computeMeans(hostsStats, ['cpu', 'ram', 'load', 'netReception', 'netTransmission']), { + number: allHosts.length, + allHosts + }) +} + +function getTopVms ({ + vmsStats, + xo +}) { + return getTop(vmsStats, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']) +} + +function getTopHosts ({ + hostsStats, + xo +}) { + return getTop(hostsStats, ['cpu', 'ram', 'load', 'netReception', 'netTransmission']) +} + +function getMostAllocatedSpaces ({ + disks, + xo +}) { + return map( + orderBy(disks, ['size'], ['desc']).slice(0, 3), disk => ({ + uuid: disk.uuid.split('-')[0], + size: round(disk.size / gibPower, 2) + })) +} + +function getHostsMissingPatches ({ + hosts, + xo +}) { + return Promise.all(map(hosts, async host => { + const hostsPatches = await xo.getXapi(host).listMissingPoolPatchesOnHost(host.uuid) + if (hostsPatches.length > 0) { + return { + uuid: host.uuid, + patches: map(hostsPatches, patch => 'name') + } + } + })) +} + +function getAllUsersEmail (users) { + return map(users, 'email') +} + +async function storeStats ({ + data, + storedStatsPath +}) { + await pWriteFile(storedStatsPath, JSON.stringify(data)) +} + +async function computeEvolution ({ + storedStatsPath, + ...newStats +}) { + try { + const oldStats = JSON.parse(await pReadFile(storedStatsPath, 'utf8')) + const newStatsVms = newStats.vms + const oldStatsVms = oldStats.global.vms + const newStatsHosts = newStats.hosts + const oldStatsHosts = oldStats.global.hosts + + const prevDate = oldStats.style.currDate + + const vmsEvolution = { + number: newStatsVms.number - oldStatsVms.number, + ...conputePercentage(newStatsVms, oldStatsVms, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']) + } + + const hostsEvolution = { + number: newStatsHosts.number - oldStatsHosts.number, + ...conputePercentage(newStatsHosts, oldStatsHosts, ['cpu', 'ram', 'load', 'netReception', 'netTransmission']) + } + + const vmsRessourcesEvolution = getDiff(oldStatsVms.allVms, newStatsVms.allVms) + const hostsRessourcesEvolution = getDiff(oldStatsHosts.allHosts, newStatsHosts.allHosts) + + const usersEvolution = getDiff(oldStats.users, newStats.users) + + return { + vmsEvolution, + hostsEvolution, + prevDate, + vmsRessourcesEvolution, + hostsRessourcesEvolution, + usersEvolution + } + } catch (err) { + if (err.code !== 'ENOENT') throw err + } +} + +async function dataBuilder ({ + xo, + storedStatsPath +}) { + const xoObjects = values(xo.getObjects()) + const runningVms = filter(xoObjects, {type: 'VM', power_state: 'Running'}) + const haltedVms = filter(xoObjects, {type: 'VM', power_state: 'Halted'}) + const runningHosts = filter(xoObjects, {type: 'host', power_state: 'Running'}) + const haltedHosts = filter(xoObjects, {type: 'host', power_state: 'Halted'}) + const disks = filter(xoObjects, {type: 'SR'}) + const [users, vmsStats, hostsStats, topAllocation, hostsMissingPatches] = await Promise.all([ + xo.getAllUsers(), + getVmsStats({xo, runningVms}), + getHostsStats({xo, runningHosts}), + getMostAllocatedSpaces({xo, disks}), + getHostsMissingPatches({xo, runningHosts}) + ]) + + const [globalVmsStats, globalHostsStats, topVms, topHosts, usersEmail] = await Promise.all([ + computeGlobalVmsStats({xo, vmsStats, haltedVms}), + computeGlobalHostsStats({xo, hostsStats, haltedHosts}), + getTopVms({xo, vmsStats}), + getTopHosts({xo, hostsStats}), + getAllUsersEmail(users) + ]) + const evolution = await computeEvolution({ + storedStatsPath, + hosts: globalHostsStats, + usersEmail, + vms: globalVmsStats + }) + + const data = { + global: { + vms: globalVmsStats, + hosts: globalHostsStats, + vmsEvolution: evolution && evolution.vmsEvolution, + hostsEvolution: evolution && evolution.hostsEvolution + }, + topVms, + topHosts, + hostsMissingPatches, + usersEmail, + topAllocation, + vmsRessourcesEvolution: evolution && evolution.vmsRessourcesEvolution, + hostsRessourcesEvolution: evolution && evolution.hostsRessourcesEvolution, + usersEvolution: evolution && evolution.usersEvolution, + style: { + imgVates, + imgXo, + currDate, + prevDate: evolution && evolution.prevDate, + page: '{{page}}' + } + } + + return data } // =================================================================== class UsageReportPlugin { - constructor (xo) { + constructor ({xo, getDataDir}) { this._xo = xo - this._unsets = [] + this._dir = getDataDir + // Defined in configure(). + this._conf = null } - configure ({emails}) { - this.mailsReceivers = emails + configure (configuration) { + this._conf = configuration + + this._job = new CronJob({ + cronTime: configuration.periodicity === 'monthly' ? '00 06 1 * *' : '00 06 * * 0', + onTick: () => this._sendReport(), + start: false + }) } - load () { - const this_ = this - // TOP Max Cpu - this._unsets.push(this._xo.api.addMethod('generateCpu', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxCpu = computeCpuMax(machineStats.stats.cpus) - return { - 'max': maxCpu - } - })) - // TOP Max Load - // xo-cli generate machine=4a2dccec-83ff-4212-9e16-44fbc0527961 granularity=days - this._unsets.push(this._xo.api.addMethod('generateLoad', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxLoad = sortArray(machineStats.stats.load) - return { - 'max': maxLoad - } - })) - // TOP Max Memory - this._unsets.push(this._xo.api.addMethod('generateMemory', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxMemory = sortArray(machineStats.stats.memory) - return { - 'max': maxMemory - } - })) - // TOP Max MemoryUsed - this._unsets.push(this._xo.api.addMethod('generateMemoryUsed', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxMemoryUsed = sortArray(machineStats.stats.memoryUsed) - return { - 'max': maxMemoryUsed - } - })) -// ============================================================================= + async load () { + const dir = await this._dir() + this._storedStatsPath = `${dir}/stats.json` - // Returns { host1_Id: [ highestCpuUsage, ... , lowestCpuUsage ], - // host2_Id: [ highestCpuUsage, ... , lowestCpuUsage ] } - this._unsets.push(this._xo.api.addMethod('generateGlobalCpuReport', async ({ machines, granularity }) => { - machines = machines.split(',') - const hostMean = {} - for (let machine of machines) { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - const cpusMean = [] - forEach(machineStats.stats.cpus, (cpu) => { - cpusMean.push(computeMean(cpu)) - }) - hostMean[machine] = sortArray(cpusMean) - } - return hostMean - })) - - // Single host: get stats from its VMs. - // Returns { vm1_Id: vm1_Stats, vm2_Id: vm2_Stats, ... } - async function _getHostVmsStats (machine, granularity) { - const host = await this_._xo.getObject(machine) - const objects = await this_._xo.getObjects() - - const promises = {} - forEach(objects, (obj) => { - if (obj.type === 'VM' && obj.power_state === 'Running' && obj.$poolId === host.$poolId) { - promises[obj.id] = this_._xo.getXapiVmStats(obj, granularity) - } - }) - - return promises::all() - } - - this._unsets.push(this._xo.api.addMethod('generateHostVmsReport', async ({ machine, granularity }) => { - return _getHostVmsStats(machine, granularity) - })) - - // Multiple hosts: get stats from all of their VMs - // Returns { host1_Id: { vm1_Id: vm1_Stats, vm2_Id: vm2_Stats } - // host2_Id: { vm3_Id: vm3_Stats } } - async function _getHostsVmsStats (machines, granularity) { - machines = machines.split(',') - - const promises = {} - forEach(machines, (machine) => { - promises[machine] = _getHostVmsStats(machine, granularity) - }) - - return promises::all() - } - - this._unsets.push(this._xo.api.addMethod('generateHostsVmsReport', async ({ machines, granularity }) => { - return _getHostsVmsStats(machines, granularity) - })) - - // Returns { vm1_Id: { 'rx': vm1_RAverageUsage, 'tx': vm1_TAverageUsage } - // vm2_Id: { 'rx': vm2_RAverageUsage, 'tx': vm2_TAverageUsage } } - async function _getHostVmsNetworkUsage (machine, granularity) { - const vmsStats = await _getHostVmsStats(machine, granularity) - // Reading average usage of the network (all VIFs) for each resident VM - const hostVmsNetworkStats = {} - forEach(vmsStats, (vmStats, vmId) => { - const reception = vmStats.stats.vifs.rx - const transfer = vmStats.stats.vifs.tx - - const receptionVifsMeans = reception.map(vifStats => computeMean(vifStats)) - const transferVifsMeans = transfer.map(vifStats => computeMean(vifStats)) - - const receptionMean = computeMean(receptionVifsMeans) - const transferMean = computeMean(transferVifsMeans) - - hostVmsNetworkStats[vmId] = { 'rx': receptionMean, 'tx': transferMean } - }) - return hostVmsNetworkStats - } - - // Returns { 'rx': [ { 'id': vmA_Id, 'value': vmA_receptionMeanUsage } , ..., { 'id': vmB_Id, 'value': vmB_receptionMeanUsage } ] - // 'tx': [ { 'id': vmC_Id, 'value': vmC_receptionMeanUsage } , ..., { 'id': vmD_Id, 'value': vmD_receptionMeanUsage } ] } - // --> vmA is the most network using VM in reception - async function _getHostVmsSortedNetworkUsage (machine, granularity) { - const networkStats = await _getHostVmsNetworkUsage(machine, granularity) - forEach(networkStats, (vmNetworkStats, vmId) => { - vmNetworkStats.id = vmId - }) - - const sortedReception = sortBy(networkStats, vm => -vm.rx) - const sortedTransfer = sortBy(networkStats, vm => -vm.tx) - - const sortedArrays = { - 'rx': map(sortedReception, vm => { - return { 'id': vm.id, 'value': vm.rx } - }), - 'tx': map(sortedTransfer, vm => { - return { 'id': vm.id, 'value': vm.tx } - }) - } - - return sortedArrays - } - - // Returns [ { 'id': vm1_Id, 'rx|tx': vm1_Usage, 'id': vm2_Id, 'rx|tx': vm2_Usage } - // `number` VMs ordered from the highest to the lowest according to `criteria` - async function _getHostVmsReport (machine, granularity, criteria, number) { - if (!criteria) { - criteria = 'tx' - } - if (criteria !== 'tx' && criteria !== 'rx') { - throw new Error('`criteria` must be either `tx` or `rx`') - } - if (!number) { - number = 3 - } - number = +number - if (!isFinite(number)) { - throw new Error('`number` must be a number') - } - const networkUsage = await _getHostVmsSortedNetworkUsage(machine, granularity) - const sortedNetworkStats = networkUsage[criteria] - return sortedNetworkStats.slice(0, number) - } - - this._unsets.push(this._xo.api.addMethod('generateHostNetworkReport', async ({ machine, granularity, criteria, number }) => { - return _getHostVmsReport(machine, granularity, criteria, number) - })) - - // faire la moyenne pour chaque vm, puis la moyenne de cette moyenne pour chaque hote et faire moyenne finale - this._unsets.push(this._xo.api.addMethod('generateGlobalMemoryUsedReport', async ({ machines, granularity }) => { - // const stats = await _getHostsVmsStats(machines, granularity) - - machines = machines.split(',') - const hostMean = {} - for (let machine of machines) { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - const memoryUsedMean = [] - forEach(machineStats.stats.memoryUsed, (cpu) => { - memoryUsedMean.push(computeMean) - }) - hostMean[machine] = sortArray(memoryUsedMean) - } - return hostMean - })) - - // let maxMemoryUsed = sortArray(machineStats.stats.memoryUsed) - // Cpus - this._unsets.push(this._xo.api.addMethod('generateCpuReport', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxCpu = computeCpuMax(machineStats.stats.cpus) - let minCpu = computeCpuMin(machineStats.stats.cpus) - let meanCpu = computeCpuMean(machineStats.stats.cpus) - - return { - 'max': maxCpu, - 'min': minCpu, - 'mean': meanCpu - } - })) - - // Load - this._unsets.push(this._xo.api.addMethod('generateLoadReport', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxLoad = computeMax(machineStats.stats.load) - let minLoad = computeMin(machineStats.stats.load) - let meanLoad = computeMean(machineStats.stats.load) - - return { - 'max': maxLoad, - 'min': minLoad, - 'mean': meanLoad - } - })) - - // Memory - this._unsets.push(this._xo.api.addMethod('generateMemoryReport', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxMemory = computeMax(machineStats.stats.memory) - let minMemory = computeMin(machineStats.stats.memory) - let meanMemory = computeMean(machineStats.stats.memory) - - return { - 'max': maxMemory, - 'min': minMemory, - 'mean': meanMemory - } - })) - - // MemoryUsed - this._unsets.push(this._xo.api.addMethod('generateMemoryUsedReport', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxMemoryUsed = computeMax(machineStats.stats.memoryUsed) - let minMemoryUsed = computeMin(machineStats.stats.memoryUsed) - let meanMemoryUsed = computeMean(machineStats.stats.memoryUsed) - - return { - 'max': maxMemoryUsed, - 'min': minMemoryUsed, - 'mean': meanMemoryUsed - } - })) - - // MemoryFree - this._unsets.push(this._xo.api.addMethod('generateMemoryFreeReport', async ({ machine, granularity }) => { - const machineStats = await this_._xo.getXapiHostStats(this_._xo.getObject(machine), granularity) - let maxMemoryFree = computeMax(machineStats.stats.memoryFree) - let minMemoryFree = computeMin(machineStats.stats.memoryFree) - let meanMemoryFree = computeMean(machineStats.stats.memoryFree) - - return { - 'max': maxMemoryFree, - 'min': minMemoryFree, - 'mean': meanMemoryFree - } - })) - -// ============================================================================= - // `report` format : - // { - // title, - // categories: [ - // { - // title, --> optional - // type: 'list', - // content: { - // item1: { id: ID, value: VALUE } - // item2: { id: ID, value: VALUE } - // } - // }, - // { - // title, --> optional - // type: 'table', - // headers: [ header1, ..., headerN ] --> optional - // content: { prop1: PROP1, ..., propN: PROPN } - // } - // ] - // } - async function _generateMarkdown (report, titleDepth = 0) { - const hashOffset = new Array(+titleDepth + 1).join('#') - let mdReport = report.title ? `${hashOffset}# ${report.title}\n` : '' - forEach(report.categories, category => { - mdReport += category.title ? `\n${hashOffset}## ${category.title}\n\n` : '' - mdReport += category.beforeContent ? `${category.beforeContent}
\n\n` : '' - if (category.type === 'list') { - forEach(category.content, (obj) => { - mdReport += `- **${obj.id}** :\t${obj.value}\n` - }) - } else if (category.type === 'table') { - if (category.headers) { - mdReport += '|' - let underline = '|' - forEach(category.headers, header => { - mdReport += `${header}|` - underline += '---|' - }) - mdReport += `\n${underline}\n` - } - forEach(category.content, line => { - mdReport += '|' - forEach(line, (col) => { - mdReport += `${col}|` - }) - mdReport += '\n' - }) - } - mdReport += category.afterContent ? `\n${category.afterContent}
\n` : '' - }) - return mdReport - } - - this._unsets.push(this._xo.api.addMethod('generateMarkdownReport', async ({ machine, titleDepth }) => { - const txReport = await _getHostVmsReport(machine, 'seconds', 'tx', 3) - const rxReport = await _getHostVmsReport(machine, 'seconds', 'rx', 3) - - const report = {} - report.title = 'Network usage report' - - const category1 = {} - category1.title = 'Transfer' - category1.beforeContent = 'These are the 3 most network using VMs in transfer (upload):' - category1.type = 'list' - category1.content = txReport - - const category2 = {} - category2.title = 'Reception' - category2.beforeContent = 'These are the 3 most network using VMs in reception (download):' - category2.type = 'table' - category2.headers = ['ID', 'Usage'] - category2.content = rxReport - - report.categories = [category1, category2] - - return _generateMarkdown(report, titleDepth) - })) + this._job.start() } unload () { - for (let i = 0; i < this._unsets; ++i) { - this._unsets[i]() - } - - this._unsets.length = 0 + this._job.stop() } + + test () { + return this._sendReport() + } + + async _sendReport () { + const data = await dataBuilder({ + xo: this._xo, + storedStatsPath: this._storedStatsPath + }) + const result = template(data) + const stream = await fromCallback(cb => pdf.create(result).toStream(cb)) + + await Promise.all([ + this._xo.sendEmail({ + to: this._conf.emails, + subject: `[Xen Orchestra] Xo Report - ${currDate}`, + markdown: `Hi there, + + You have chosen to receive your xo report ${this._conf.periodicity}. + Please, find the attached report. + + best regards.`, + attachments: [{ + filename: `xoReport_${currDate}.pdf`, + content: stream + }] + }), + storeStats({ + data, + storedStatsPath: this._storedStatsPath + }) + ]) + } + } - /* if (this._xo.sendEmail) { - await this._xo.sendEmail({ - to: this._mailsReceivers, - // subject: 'Usage Reports (XenOrchestra)', - markdown - }) - } - else { - throw 'error, sendEmail does not exist' - } */ - /* if (periodicity = 'monthly') { - throw console.log('monthly') - } else {} */ -/* var data = {}, - dir = __dirname + '/home/thannos/xo-server/lab1_days.json' -fs.readdirSync(dir).forEach(function (file) { - data[file.replace(/\.json$/, '')] = require(dir + file) -}) */ - // =================================================================== -export default ({ xo }) => new UsageReportPlugin(xo) +export default opts => new UsageReportPlugin(opts) // =================================================================== diff --git a/packages/xo-server-usage-report/templates/xoReport.html b/packages/xo-server-usage-report/templates/xoReport.html new file mode 100644 index 000000000..08ed3b0d8 --- /dev/null +++ b/packages/xo-server-usage-report/templates/xoReport.html @@ -0,0 +1,597 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VM
Number:{{global.vms.number}} + {{#if global.vmsEvolution.number}} + {{#compare global.vmsEvolution.number ">" 0}}+{{/compare}} + {{global.vmsEvolution.number}} + {{else}} + 0 + {{/if}} +
CPU:{{global.vms.cpu}} % + {{#if global.vmsEvolution.cpu}} + {{#compare global.vmsEvolution.cpu ">" 0}}+{{/compare}} + {{global.vmsEvolution.cpu}}% + {{else}} + 0 + {{/if}} +
RAM:{{global.vms.ram}} GiB + {{#if global.vmsEvolution.ram}} + {{#compare global.vmsEvolution.ram ">" 0}}+{{/compare}} + {{global.vmsEvolution.ram}}% + {{else}} + 0 + {{/if}} +
Disk read:{{global.vms.diskRead}} MiB + {{#if global.vmsEvolution.diskRead}} + {{#compare global.vmsEvolution.diskRead ">" 0}}+{{/compare}} + {{global.vmsEvolution.diskRead}}% + {{else}} + 0 + {{/if}} +
Disk write:{{global.vms.diskWrite}} MiB + {{#if global.vmsEvolution.diskWrite}} + {{#compare global.vmsEvolution.diskWrite ">" 0}}+{{/compare}} + {{global.vmsEvolution.diskWrite}}% + {{else}} + 0 + {{/if}} +
Net reception:{{global.vms.netReception}} KiB + {{#if global.vmsEvolution.netReception}} + {{#compare global.vmsEvolution.netReception ">" 0}}+{{/compare}} + {{global.vmsEvolution.netReception}}% + {{else}} + 0 + {{/if}} +
Net transmission:{{global.vms.netTransmission}} KiB + {{#if global.vmsEvolution.netTransmission}} + {{#compare global.vmsEvolution.netTransmission ">" 0}}+{{/compare}} + {{global.vmsEvolution.netTransmission}}% + {{else}} + 0 + {{/if}} +
+ +
+ + + + + + + + + + + + + {{#each topVms.cpu}} + + + + + {{/each}} + + + + + {{#each topVms.ram}} + + + + + {{/each}} +
3rd top usages
UUIDValue
CPU
{{this.uuid}}{{this.value}} %
RAM
{{this.uuid}}{{this.value}} GiB
+ + + + + + + + + + + {{#each topVms.diskRead}} + + + + + {{/each}} + + + + {{#each topVms.diskWrite}} + + + + + {{/each}} + + + + {{#each topVms.netReception}} + + + + + {{/each}} + + + + {{#each topVms.netTransmission}} + + + + + {{/each}} +
3rd top usages
UUIDValue
Disk read
{{this.uuid}}{{this.value}} MiB
Disk write
{{this.uuid}}{{this.value}} MiB
Net reception
{{this.uuid}}{{this.value}} KiB
Net transmission
{{this.uuid}}{{this.value}} KiB
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Host
Number:{{global.hosts.number}} + {{#if global.hostsEvolution.number}} + {{#compare global.hostsEvolution.number ">" 0}}+{{/compare}} + {{global.hostsEvolution.number}} + {{else}} + 0 + {{/if}} +
CPU:{{global.hosts.cpu}} % + {{#if global.hostsEvolution.cpu}} + {{#compare global.hostsEvolution.cpu ">" 0}}+{{/compare}} + {{global.hostsEvolution.cpu}}% + {{else}} + 0 + {{/if}} +
RAM:{{global.hosts.ram}} GiB + {{#if global.hostsEvolution.ram}} + {{#compare global.hostsEvolution.ram ">" 0}}+{{/compare}} + {{global.hostsEvolution.ram}}% + {{else}} + 0 + {{/if}} +
Load average:{{global.hosts.load}} + {{#if global.hostsEvolution.load}} + {{#compare global.hostsEvolution.load ">" 0}}+{{/compare}} + {{global.hostsEvolution.load}}% + {{else}} + 0 + {{/if}} +
Net reception:{{global.hosts.netReception}} KiB + {{#if global.hostsEvolution.netReception}} + {{#compare global.hostsEvolution.netReception ">" 0}}+{{/compare}} + {{global.hostsEvolution.netReception}}% + {{else}} + 0 + {{/if}} +
Net transmission:{{global.hosts.netTransmission}} KiB + {{#if global.hostsEvolution.netTransmission}} + {{#compare global.hostsEvolution.netTransmission ">" 0}}+{{/compare}} + {{global.hostsEvolution.netTransmission}}% + {{else}} + 0 + {{/if}} +
+ +
+ + + + + + + + + + + + {{#each topHosts.cpu}} + + + + + {{/each}} + + + + {{#each topHosts.ram}} + + + + + {{/each}} +
3rd top usages
UUIDValue
CPU
{{this.uuid}}{{this.value}} %
RAM
{{this.uuid}}{{this.value}} GiB
+ + + + + + + + + + + {{#each topHosts.load}} + + + + + {{/each}} + + + + {{#each topHosts.netReception}} + + + + + {{/each}} + + + + {{#each topHosts.netTransmission}} + + + + + {{/each}} +
3rd top usages
UUIDValue
Load average
{{this.uuid}}{{this.value}}
Net reception
{{this.uuid}}{{this.value}} KiB
Net transmission
{{this.uuid}}{{this.value}} KiB
+
+
+ +
+
+ + + + + + + {{#each topAllocation}} + + + + + {{/each}} +
3rd most allocated space
UUIDvalue
{{this.uuid}}{{this.size}} GiB
+ + + + + + + {{#if hostsMissingPatches}} + {{#each hostsMissingPatches}} + + + + + {{/each}} + {{else}} + + + + + {{/if}} +
Hosts missing patches
UUIDPatches
{{this.uuid}}{{this.patches}}
All hosts are updated!No patch!
+ + + + + + {{#if usersEvolution.added}} + {{#each usersEvolution.added}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Added Users
Email
{{this}}
No added users!
+ + + + + + {{#if usersEvolution.removed}} + {{#each usersEvolution.removed}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Removed Users
Email
{{this}}
No removed users!
+ + + + + + {{#if vmsRessourcesEvolution.added}} + {{#each vmsRessourcesEvolution.added}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Added Vms
UUID
{{this}}
No added VMs!
+ + + + + + + {{#if vmsRessourcesEvolution.removed}} + {{#each vmsRessourcesEvolution.removed}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Removed Vms
UUID
{{this}}
No removed VMs!
+ + + + + + {{#if hostsRessourcesEvolution.added}} + {{#each hostsRessourcesEvolution.added}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Added Hosts
UUID
{{this}}
No added Hosts!
+ + + + + + {{#if hostsRessourcesEvolution.removed}} + {{#each hostsRessourcesEvolution.removed}} + + + + {{/each}} + {{else}} + + + + {{/if}} +
Removed Hosts
UUID
{{this}}
No removed Hosts!
+
+
+ +