Implementation (#15)
This commit is contained in:
parent
128b169ae2
commit
31189be2c8
@ -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
|
||||
|
||||
|
BIN
packages/xo-server-usage-report/images/logo.png
Normal file
BIN
packages/xo-server-usage-report/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
packages/xo-server-usage-report/images/xo.png
Normal file
BIN
packages/xo-server-usage-report/images/xo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -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": [
|
||||
[
|
||||
|
@ -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}<br>\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}<br>\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)
|
||||
|
||||
// ===================================================================
|
||||
|
597
packages/xo-server-usage-report/templates/xoReport.html
Normal file
597
packages/xo-server-usage-report/templates/xoReport.html
Normal file
@ -0,0 +1,597 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
html, body {
|
||||
font-family: 'Legacy',sans-serif;
|
||||
}
|
||||
|
||||
.page:not(:first-child) {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
#pageHeader .logo {
|
||||
overflow:auto;
|
||||
float: left;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#pageHeader .logo img {
|
||||
width: 50px;;
|
||||
height: 50px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#pageHeader .logo label{
|
||||
font-weight: 450;
|
||||
font-size: 28px;
|
||||
color: #171717;
|
||||
margin-top:8px;
|
||||
margin-left: 5px;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
#pageHeader .date {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
#pageHeader .date span{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#pageHeader hr{
|
||||
margin-top: 7%;
|
||||
}
|
||||
|
||||
.page .global{
|
||||
border-collapse: collapse;
|
||||
margin: auto;
|
||||
border: 1px solid #95a5a6;
|
||||
}
|
||||
|
||||
.page .global tr:nth-child(1) td {
|
||||
border: 1px solid #95a5a6;
|
||||
padding: 15px;
|
||||
font-size: 35px;
|
||||
background-color: #95a5a6;
|
||||
color: #ecf0f1;
|
||||
text-shadow: 1px 0px 0px #ecf0f1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page .global tr td:not(#title) {
|
||||
margin-left:10px;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page .global tr td:nth-child(1):not(#title) {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page .global tr:nth-child(2) td {
|
||||
border-top: 1px solid #95a5a6;
|
||||
}
|
||||
|
||||
.page .global tr:nth-last-child(2) td {
|
||||
border-bottom: 1px solid #95a5a6;
|
||||
}
|
||||
|
||||
.top table{
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.top table caption {
|
||||
border: 1px solid #95a5a6;
|
||||
font-size: 18px;
|
||||
background-color: #95a5a6;
|
||||
color: #ecf0f1;
|
||||
text-shadow: 1px 0px 0px #ecf0f1;
|
||||
text-align: center;
|
||||
height: 30px;
|
||||
line-height:30px;
|
||||
}
|
||||
|
||||
.top table .tableHeader{
|
||||
background-color: #DCDDDE;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.top table th {
|
||||
background-color: #DCDDDE;
|
||||
}
|
||||
|
||||
.top table td:not(.tableHeader){
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
border:1px solid #95a5a6;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="pageHeader">
|
||||
<div class="logo">
|
||||
<!-- Only absolute path is supported -->
|
||||
<img src ={{style.imgXo}} alt= "Xo Logo"> <label>XOA</label>
|
||||
</div>
|
||||
<div class="date">
|
||||
<span>{{#if style.prevDate}} {{style.prevDate}} {{else}} 0000-00-00 {{/if}}</span> - <span>{{style.currDate}}</span>
|
||||
</div>
|
||||
<br>
|
||||
<hr color="#95a5a6" size="1px"/>
|
||||
</div>
|
||||
|
||||
<div id="pageFooter" align="center">
|
||||
<hr color="#95a5a6" size="1px"/>
|
||||
<span style="color:#7f8c8d">-{{style.page}}-</span>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<table class ="global">
|
||||
<tr>
|
||||
<td id="title" rowspan="13">VM</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number:</td>
|
||||
<td>{{global.vms.number}}</td>
|
||||
<td>
|
||||
{{#if global.vmsEvolution.number}}
|
||||
{{#compare global.vmsEvolution.number ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.number}}
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU:</td>
|
||||
<td>{{global.vms.cpu}} %</td> <!-- One condition doesn't work -->
|
||||
<td style='color:{{#compare global.vmsEvolution.cpu ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.cpu}}
|
||||
{{#compare global.vmsEvolution.cpu ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.cpu}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>RAM:</td>
|
||||
<td>{{global.vms.ram}} GiB</td>
|
||||
<td style='color:{{#compare global.vmsEvolution.ram ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.ram}}
|
||||
{{#compare global.vmsEvolution.ram ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.ram}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Disk read:</td>
|
||||
<td>{{global.vms.diskRead}} MiB</td>
|
||||
<td style='color:{{#compare global.vmsEvolution.diskRead ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.diskRead}}
|
||||
{{#compare global.vmsEvolution.diskRead ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.diskRead}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Disk write:</td>
|
||||
<td>{{global.vms.diskWrite}} MiB</td>
|
||||
<td style='color:{{#compare global.vmsEvolution.diskWrite ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.diskWrite}}
|
||||
{{#compare global.vmsEvolution.diskWrite ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.diskWrite}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Net reception:</td>
|
||||
<td>{{global.vms.netReception}} KiB</td>
|
||||
<td style='color:{{#compare global.vmsEvolution.netReception ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.netReception}}
|
||||
{{#compare global.vmsEvolution.netReception ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.netReception}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Net transmission:</td>
|
||||
<td>{{global.vms.netTransmission}} KiB</td>
|
||||
<td style='color:{{#compare global.vmsEvolution.netTransmission ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.vmsEvolution.netTransmission}}
|
||||
{{#compare global.vmsEvolution.netTransmission ">" 0}}+{{/compare}}
|
||||
{{global.vmsEvolution.netTransmission}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
</table>
|
||||
|
||||
<div class="top">
|
||||
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.cpu.length "+" 1}}' class="tableHeader">CPU</td>
|
||||
</tr>
|
||||
{{#each topVms.cpu}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} %</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.ram.length "+" 1}}' class="tableHeader">RAM</td>
|
||||
</tr>
|
||||
{{#each topVms.ram}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} GiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.diskRead.length "+" 1}}' class="tableHeader">Disk read</td>
|
||||
</tr>
|
||||
{{#each topVms.diskRead}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} MiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.diskWrite.length "+" 1}}' class="tableHeader">Disk write</td>
|
||||
</tr>
|
||||
{{#each topVms.diskWrite}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} MiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.netReception.length "+" 1}}' class="tableHeader">Net reception</td>
|
||||
</tr>
|
||||
{{#each topVms.netReception}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} KiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topVms.netTransmission.length "+" 1}}' class="tableHeader">Net transmission</td>
|
||||
</tr>
|
||||
{{#each topVms.netTransmission}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} KiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<table class ="global">
|
||||
<tr>
|
||||
<td id="title" rowspan="13">Host</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number:</td>
|
||||
<td>{{global.hosts.number}}</td>
|
||||
<td>
|
||||
{{#if global.hostsEvolution.number}}
|
||||
{{#compare global.hostsEvolution.number ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.number}}
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU:</td>
|
||||
<td>{{global.hosts.cpu}} %</td>
|
||||
<td style='color:{{#compare global.hostsEvolution.cpu ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.hostsEvolution.cpu}}
|
||||
{{#compare global.hostsEvolution.cpu ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.cpu}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>RAM:</td>
|
||||
<td>{{global.hosts.ram}} GiB</td>
|
||||
<td style='color:{{#compare global.hostsEvolution.ram ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.hostsEvolution.ram}}
|
||||
{{#compare global.hostsEvolution.ram ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.ram}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Load average:</td>
|
||||
<td>{{global.hosts.load}} </td>
|
||||
<td style='color:{{#compare global.hostsEvolution.load ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.hostsEvolution.load}}
|
||||
{{#compare global.hostsEvolution.load ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.load}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Net reception:</td>
|
||||
<td>{{global.hosts.netReception}} KiB</td>
|
||||
<td style='color:{{#compare global.hostsEvolution.netReception ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.hostsEvolution.netReception}}
|
||||
{{#compare global.hostsEvolution.netReception ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.netReception}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
<tr>
|
||||
<td>Net transmission:</td>
|
||||
<td>{{global.hosts.netTransmission}} KiB</td>
|
||||
<td style='color:{{#compare global.hostsEvolution.netTransmission ">" 0}} red {{else}} green {{/compare}}'>
|
||||
{{#if global.hostsEvolution.netTransmission}}
|
||||
{{#compare global.hostsEvolution.netTransmission ">" 0}}+{{/compare}}
|
||||
{{global.hostsEvolution.netTransmission}}%
|
||||
{{else}}
|
||||
0
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr>
|
||||
</table>
|
||||
|
||||
<div class="top">
|
||||
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan='{{math topHosts.cpu.length "+" 1}}' class="tableHeader">CPU</td>
|
||||
</tr>
|
||||
{{#each topHosts.cpu}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} %</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topHosts.ram.length "+" 1}}' class="tableHeader">RAM</td>
|
||||
</tr>
|
||||
{{#each topHosts.ram}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} GiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>3rd top usages</caption>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>UUID</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan='{{math topHosts.load.length "+" 1}}' class="tableHeader">Load average</td>
|
||||
</tr>
|
||||
{{#each topHosts.load}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} </td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topHosts.netReception.length "+" 1}}' class="tableHeader">Net reception</td>
|
||||
</tr>
|
||||
{{#each topHosts.netReception}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} KiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
<tr>
|
||||
<td rowspan='{{math topHosts.netTransmission.length "+" 1}}' class="tableHeader">Net transmission</td>
|
||||
</tr>
|
||||
{{#each topHosts.netTransmission}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.value}} KiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<div class="top">
|
||||
<table>
|
||||
<caption>3rd most allocated space </caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<th>value</th>
|
||||
</tr>
|
||||
{{#each topAllocation}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.size}} GiB</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Hosts missing patches</caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<th>Patches</th>
|
||||
</tr>
|
||||
{{#if hostsMissingPatches}}
|
||||
{{#each hostsMissingPatches}}
|
||||
<tr>
|
||||
<td>{{this.uuid}}</td>
|
||||
<td>{{this.patches}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>All hosts are updated!</td>
|
||||
<td>No patch!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Added Users</caption>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
{{#if usersEvolution.added}}
|
||||
{{#each usersEvolution.added}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No added users!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Removed Users</caption>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
{{#if usersEvolution.removed}}
|
||||
{{#each usersEvolution.removed}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No removed users!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Added Vms</caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
</tr>
|
||||
{{#if vmsRessourcesEvolution.added}}
|
||||
{{#each vmsRessourcesEvolution.added}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No added VMs!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<caption>Removed Vms</caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
</tr>
|
||||
{{#if vmsRessourcesEvolution.removed}}
|
||||
{{#each vmsRessourcesEvolution.removed}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No removed VMs!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Added Hosts</caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
</tr>
|
||||
{{#if hostsRessourcesEvolution.added}}
|
||||
{{#each hostsRessourcesEvolution.added}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No added Hosts!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
<table>
|
||||
<caption>Removed Hosts</caption>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
</tr>
|
||||
{{#if hostsRessourcesEvolution.removed}}
|
||||
{{#each hostsRessourcesEvolution.removed}}
|
||||
<tr>
|
||||
<td>{{this}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>No removed Hosts!</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user