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
|
## 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
|
## 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": {
|
"dependencies": {
|
||||||
"babel-runtime": "^6.18.0",
|
"babel-runtime": "^6.18.0",
|
||||||
|
"cron": "^1.2.1",
|
||||||
|
"handlebars": "^4.0.6",
|
||||||
|
"html-pdf": "^2.1.0",
|
||||||
"lodash": "^4.17.2",
|
"lodash": "^4.17.2",
|
||||||
"promise-toolbox": "^0.8.0"
|
"promise-toolbox": "^0.8.0"
|
||||||
},
|
},
|
||||||
@ -63,7 +66,8 @@
|
|||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"lodash"
|
"lodash",
|
||||||
|
"transform-runtime"
|
||||||
],
|
],
|
||||||
"presets": [
|
"presets": [
|
||||||
[
|
[
|
||||||
|
@ -1,5 +1,58 @@
|
|||||||
import { all } from 'promise-toolbox'
|
import Handlebars from 'handlebars'
|
||||||
import { forEach, isFinite, map, sortBy } from 'lodash'
|
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 = {
|
export const configurationSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -13,7 +66,8 @@ export const configurationSchema = {
|
|||||||
},
|
},
|
||||||
periodicity: {
|
periodicity: {
|
||||||
type: 'string',
|
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) {
|
function computeMean (values) {
|
||||||
let sum = 0
|
let sum = 0
|
||||||
let tot = 0
|
let n = 0
|
||||||
forEach(values, (val) => {
|
forEach(values, val => {
|
||||||
sum += val || 0
|
if (isFinite(val)) {
|
||||||
tot += val ? 1 : 0
|
sum += val
|
||||||
})
|
n++
|
||||||
return sum / tot
|
|
||||||
}
|
|
||||||
function computeMax (values) {
|
|
||||||
let max = -Infinity
|
|
||||||
forEach(values, (val) => {
|
|
||||||
if (val && val > max) {
|
|
||||||
max = val
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return max
|
|
||||||
}
|
return sum / n
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareNumbersDesc (a, b) {
|
const computeDoubleMean = val => computeMean(val.map(computeMean))
|
||||||
if (a > b) {
|
|
||||||
return -1
|
function computeMeans (objects, options) {
|
||||||
}
|
return zipObject(
|
||||||
if (a < b) {
|
options,
|
||||||
return 1
|
map(
|
||||||
}
|
options,
|
||||||
return 0
|
opt => round(computeMean(map(objects, opt)), 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
function sortArray (values) {
|
|
||||||
let n = 3
|
function getTop (objects, options) {
|
||||||
let sort = values.sort(compareNumbersDesc)
|
return zipObject(
|
||||||
return sort.slice(0, n)
|
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 {
|
class UsageReportPlugin {
|
||||||
constructor (xo) {
|
constructor ({xo, getDataDir}) {
|
||||||
this._xo = xo
|
this._xo = xo
|
||||||
this._unsets = []
|
this._dir = getDataDir
|
||||||
|
// Defined in configure().
|
||||||
|
this._conf = null
|
||||||
}
|
}
|
||||||
|
|
||||||
configure ({emails}) {
|
configure (configuration) {
|
||||||
this.mailsReceivers = emails
|
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 ],
|
this._job.start()
|
||||||
// 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)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unload () {
|
unload () {
|
||||||
for (let i = 0; i < this._unsets; ++i) {
|
this._job.stop()
|
||||||
this._unsets[i]()
|
|
||||||
}
|
|
||||||
|
|
||||||
this._unsets.length = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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