mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-11 06:29:51 -05:00
feat(backup-reports): rewrite with Handlebars (#7543)
This commit is contained in:
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ignore all handlebars files:
|
||||
**/*.hbs
|
||||
@@ -28,5 +28,6 @@
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/fuse-vhd patch
|
||||
- xo-server-backup-reports major
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.13.1",
|
||||
"moment-timezone": "^0.5.13"
|
||||
|
||||
79
packages/xo-server-backup-reports/src/helpers.js
Normal file
79
packages/xo-server-backup-reports/src/helpers.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Handlebars from 'handlebars'
|
||||
import humanFormat from 'human-format'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
const ICON_FAILURE = '🚨'
|
||||
const ICON_INTERRUPTED = '⚠️'
|
||||
const ICON_SKIPPED = '⏩'
|
||||
const ICON_SUCCESS = '✔'
|
||||
|
||||
const STATUS_ICON = {
|
||||
failure: ICON_FAILURE,
|
||||
interrupted: ICON_INTERRUPTED,
|
||||
skipped: ICON_SKIPPED,
|
||||
success: ICON_SUCCESS,
|
||||
}
|
||||
|
||||
const TITLE_BY_STATUS = {
|
||||
failure: n => `## ${n} Failure${n === 1 ? '' : 's'}`,
|
||||
interrupted: n => `## ${n} Interrupted`,
|
||||
skipped: n => `## ${n} Skipped`,
|
||||
success: n => `## ${n} Success${n === 1 ? '' : 'es'}`,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
|
||||
switch (operator) {
|
||||
case '===':
|
||||
return v1 === v2 ? options.fn(this) : options.inverse(this)
|
||||
case '!==':
|
||||
return v1 !== v2 ? options.fn(this) : options.inverse(this)
|
||||
case '<':
|
||||
return v1 < v2 ? options.fn(this) : options.inverse(this)
|
||||
case '<=':
|
||||
return v1 <= v2 ? options.fn(this) : options.inverse(this)
|
||||
case '>':
|
||||
return v1 > v2 ? options.fn(this) : options.inverse(this)
|
||||
case '>=':
|
||||
return v1 >= v2 ? options.fn(this) : options.inverse(this)
|
||||
case '&&':
|
||||
return v1 && v2 ? options.fn(this) : options.inverse(this)
|
||||
case '||':
|
||||
return v1 || v2 ? options.fn(this) : options.inverse(this)
|
||||
default:
|
||||
return options.inverse(this)
|
||||
}
|
||||
})
|
||||
|
||||
Handlebars.registerHelper('formatDuration', milliseconds => moment.duration(milliseconds).humanize())
|
||||
|
||||
const formatSize = bytes =>
|
||||
humanFormat(bytes, {
|
||||
scale: 'binary',
|
||||
unit: 'B',
|
||||
})
|
||||
Handlebars.registerHelper('formatSize', formatSize)
|
||||
|
||||
const formatSpeed = (bytes, milliseconds) =>
|
||||
milliseconds > 0
|
||||
? humanFormat((bytes * 1e3) / milliseconds, {
|
||||
scale: 'binary',
|
||||
unit: 'B/s',
|
||||
})
|
||||
: 'N/A'
|
||||
Handlebars.registerHelper('formatSpeed', (bytes, start, end) => formatSpeed(bytes, end - start))
|
||||
|
||||
Handlebars.registerHelper('subtract', (a, b) => a - b)
|
||||
|
||||
Handlebars.registerHelper('executeFunction', (fct, arg) => fct(arg))
|
||||
|
||||
// this could be a partial but it would be less clear
|
||||
Handlebars.registerHelper('getIcon', status => STATUS_ICON[status])
|
||||
|
||||
// this could be a partial but it would be less clear
|
||||
Handlebars.registerHelper('titleByStatus', function (status) {
|
||||
if (this && status in TITLE_BY_STATUS) {
|
||||
return TITLE_BY_STATUS[status](this.length)
|
||||
}
|
||||
})
|
||||
@@ -1,9 +1,12 @@
|
||||
import humanFormat from 'human-format'
|
||||
import Handlebars from 'handlebars'
|
||||
import moment from 'moment-timezone'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { forEach, groupBy } from 'lodash'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { extname, join, parse } from 'node:path'
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import pkg from '../package'
|
||||
import './helpers'
|
||||
|
||||
const logger = createLogger('xo:xo-server-backup-reports')
|
||||
|
||||
@@ -50,140 +53,56 @@ export const testSchema = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const INDENT = ' '
|
||||
const UNKNOWN_ITEM = 'Unknown'
|
||||
|
||||
const ICON_FAILURE = '🚨'
|
||||
const ICON_INTERRUPTED = '⚠️'
|
||||
const ICON_SKIPPED = '⏩'
|
||||
const ICON_SUCCESS = '✔'
|
||||
const ICON_WARNING = '⚠️'
|
||||
|
||||
const STATUS_ICON = {
|
||||
failure: ICON_FAILURE,
|
||||
interrupted: ICON_INTERRUPTED,
|
||||
skipped: ICON_SKIPPED,
|
||||
success: ICON_SUCCESS,
|
||||
const handlebarsPartialFiles = readdirSync(join(__dirname, '../templates/partials/')).filter(
|
||||
filename => extname(filename) === '.hbs'
|
||||
)
|
||||
for (const fileName of handlebarsPartialFiles) {
|
||||
const partial = readFileSync(join(__dirname, `../templates/partials/${fileName}`)).toString()
|
||||
Handlebars.registerPartial(parse(fileName).name, partial)
|
||||
}
|
||||
|
||||
const compiledMetadataSubject = Handlebars.compile(
|
||||
readFileSync(join(__dirname, '../templates/metadataSubject.hbs')).toString().replace(/\n$/, '')
|
||||
)
|
||||
const compiledMetadataTemplate = Handlebars.compile(
|
||||
readFileSync(join(__dirname, '../templates/metadata.hbs')).toString().replace(/\n$/, '')
|
||||
)
|
||||
const compiledVmSubject = Handlebars.compile(
|
||||
readFileSync(join(__dirname, '../templates/vmSubject.hbs')).toString().replace(/\n$/, '')
|
||||
)
|
||||
const compiledVmTemplate = Handlebars.compile(
|
||||
readFileSync(join(__dirname, '../templates/vm.hbs')).toString().replace(/\n$/, '')
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const UNKNOWN_ITEM = 'Unknown'
|
||||
|
||||
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
|
||||
const createDateFormatter = timezone =>
|
||||
timezone !== undefined
|
||||
? timestamp => moment(timestamp).tz(timezone).format(DATE_FORMAT)
|
||||
: timestamp => moment(timestamp).format(DATE_FORMAT)
|
||||
|
||||
const formatDuration = milliseconds => moment.duration(milliseconds).humanize()
|
||||
|
||||
const formatSize = bytes =>
|
||||
humanFormat(bytes, {
|
||||
scale: 'binary',
|
||||
unit: 'B',
|
||||
})
|
||||
|
||||
const formatSpeed = (bytes, milliseconds) =>
|
||||
milliseconds > 0
|
||||
? humanFormat((bytes * 1e3) / milliseconds, {
|
||||
scale: 'binary',
|
||||
unit: 'B/s',
|
||||
})
|
||||
: 'N/A'
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
const UNHEALTHY_VDI_CHAIN_MESSAGE =
|
||||
'[(unhealthy VDI chain) Job canceled to protect the VDI chain](https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection)'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const STATUS = ['failure', 'interrupted', 'skipped', 'success']
|
||||
const TITLE_BY_STATUS = {
|
||||
failure: n => `## ${n} Failure${n === 1 ? '' : 's'}`,
|
||||
interrupted: n => `## ${n} Interrupted`,
|
||||
skipped: n => `## ${n} Skipped`,
|
||||
success: n => `## ${n} Success${n === 1 ? '' : 'es'}`,
|
||||
}
|
||||
|
||||
const getTemporalDataMarkdown = (end, start, formatDate) => {
|
||||
const markdown = [`- **Start time**: ${formatDate(start)}`]
|
||||
if (end !== undefined) {
|
||||
markdown.push(`- **End time**: ${formatDate(end)}`)
|
||||
const duration = end - start
|
||||
if (duration >= 1) {
|
||||
markdown.push(`- **Duration**: ${formatDuration(duration)}`)
|
||||
}
|
||||
}
|
||||
return markdown
|
||||
}
|
||||
|
||||
const getWarningsMarkdown = (warnings = []) => warnings.map(({ message }) => `- **${ICON_WARNING} ${message}**`)
|
||||
|
||||
const getErrorMarkdown = task => {
|
||||
let message
|
||||
if (task.status === 'success' || (message = task.result?.message ?? task.result?.code) === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const label = task.status === 'skipped' ? 'Reason' : 'Error'
|
||||
return `- **${label}**: ${message}`
|
||||
}
|
||||
|
||||
const MARKDOWN_BY_TYPE = {
|
||||
pool(task, { formatDate }) {
|
||||
const { id, pool = {}, poolMaster = {} } = task.data
|
||||
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
|
||||
|
||||
return {
|
||||
body: [
|
||||
pool.uuid !== undefined ? `- **UUID**: ${pool.uuid}` : `- **ID**: ${id}`,
|
||||
...getTemporalDataMarkdown(task.end, task.start, formatDate),
|
||||
getErrorMarkdown(task),
|
||||
],
|
||||
title: `[pool] ${name}`,
|
||||
}
|
||||
},
|
||||
xo(task, { formatDate, jobName }) {
|
||||
return {
|
||||
body: [...getTemporalDataMarkdown(task.end, task.start, formatDate), getErrorMarkdown(task)],
|
||||
title: `[XO] ${jobName}`,
|
||||
}
|
||||
},
|
||||
async remote(task, { formatDate, xo }) {
|
||||
const id = task.data.id
|
||||
const name = await xo.getRemote(id).then(
|
||||
const getAdditionnalData = async (task, props) => {
|
||||
if (task.data?.type === 'remote') {
|
||||
const name = await props.xo.getRemote(task.data.id).then(
|
||||
({ name }) => name,
|
||||
error => {
|
||||
logger.warn(error)
|
||||
return UNKNOWN_ITEM
|
||||
}
|
||||
)
|
||||
return {
|
||||
body: [`- **ID**: ${id}`, ...getTemporalDataMarkdown(task.end, task.start, formatDate), getErrorMarkdown(task)],
|
||||
title: `[remote] ${name}`,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getMarkdown = (task, props) => MARKDOWN_BY_TYPE[task.data?.type]?.(task, props)
|
||||
|
||||
const toMarkdown = parts => {
|
||||
const lines = []
|
||||
let indentLevel = -1
|
||||
|
||||
const helper = part => {
|
||||
if (typeof part === 'string') {
|
||||
lines.push(`${INDENT.repeat(indentLevel)}${part}`)
|
||||
} else if (Array.isArray(part)) {
|
||||
++indentLevel
|
||||
part.forEach(helper)
|
||||
--indentLevel
|
||||
}
|
||||
return { name }
|
||||
}
|
||||
helper(parts)
|
||||
|
||||
return lines.join('\n')
|
||||
return {}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class BackupReportsXoPlugin {
|
||||
@@ -251,7 +170,7 @@ class BackupReportsXoPlugin {
|
||||
])
|
||||
|
||||
if (job.type === 'backup' || job.type === 'mirrorBackup') {
|
||||
return this._ngVmHandler(log, job, schedule, force)
|
||||
return this._vmHandler(log, job, schedule, force)
|
||||
} else if (job.type === 'metadataBackup') {
|
||||
return this._metadataHandler(log, job, schedule, force)
|
||||
}
|
||||
@@ -265,108 +184,47 @@ class BackupReportsXoPlugin {
|
||||
const formatDate = createDateFormatter(schedule?.timezone)
|
||||
|
||||
const tasksByStatus = groupBy(log.tasks, 'status')
|
||||
const n = log.tasks?.length ?? 0
|
||||
const nSuccesses = tasksByStatus.success?.length ?? 0
|
||||
|
||||
if (!force && log.data.reportWhen === 'failure') {
|
||||
delete tasksByStatus.success
|
||||
}
|
||||
|
||||
// header
|
||||
const markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Job name**: ${jobName}`,
|
||||
`- **Run ID**: ${log.id}`,
|
||||
...getTemporalDataMarkdown(log.end, log.start, formatDate),
|
||||
n !== 0 && `- **Successes**: ${nSuccesses} / ${n}`,
|
||||
...getWarningsMarkdown(log.warnings),
|
||||
getErrorMarkdown(log),
|
||||
]
|
||||
|
||||
// body
|
||||
for (const status of STATUS) {
|
||||
const tasks = tasksByStatus[status]
|
||||
if (tasks === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
// tasks header
|
||||
markdown.push('---', '', TITLE_BY_STATUS[status](tasks.length))
|
||||
|
||||
// tasks body
|
||||
for (const task of tasks) {
|
||||
const taskMarkdown = await getMarkdown(task, {
|
||||
formatDate,
|
||||
jobName: log.jobName,
|
||||
xo,
|
||||
})
|
||||
if (taskMarkdown === undefined) {
|
||||
continue
|
||||
for (const taskBatch of Object.values(tasksByStatus)) {
|
||||
for (const task of taskBatch) {
|
||||
task.additionnalData = await getAdditionnalData(task, { xo })
|
||||
for (const subTask of task.tasks) {
|
||||
subTask.additionnalData = await getAdditionnalData(subTask, { xo })
|
||||
}
|
||||
|
||||
const { title, body } = taskMarkdown
|
||||
const subMarkdown = [...body, ...getWarningsMarkdown(task.warnings)]
|
||||
|
||||
for (const subTask of task.tasks ?? []) {
|
||||
const taskMarkdown = await getMarkdown(subTask, { formatDate, xo })
|
||||
if (taskMarkdown === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const icon = STATUS_ICON[subTask.status]
|
||||
const { title, body } = taskMarkdown
|
||||
subMarkdown.push([`- **${title}** ${icon}`, [...body, ...getWarningsMarkdown(subTask.warnings)]])
|
||||
}
|
||||
markdown.push('', '', `### ${title}`, ...subMarkdown)
|
||||
}
|
||||
}
|
||||
|
||||
// footer
|
||||
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
|
||||
const context = {
|
||||
jobName,
|
||||
log,
|
||||
pkg,
|
||||
tasksByStatus,
|
||||
formatDate,
|
||||
}
|
||||
|
||||
return this._sendReport({
|
||||
subject: `[Xen Orchestra] ${log.status} − Metadata backup report for ${log.jobName} ${STATUS_ICON[log.status]}`,
|
||||
markdown: toMarkdown(markdown),
|
||||
subject: compiledMetadataSubject(context),
|
||||
markdown: compiledMetadataTemplate(context),
|
||||
success: log.status === 'success',
|
||||
})
|
||||
}
|
||||
|
||||
async _ngVmHandler(log, { name: jobName, settings }, schedule, force) {
|
||||
async _vmHandler(log, { name: jobName, settings }, schedule, force) {
|
||||
const xo = this._xo
|
||||
|
||||
const mailReceivers = get(() => settings[''].reportRecipients)
|
||||
const { reportWhen, mode } = log.data || {}
|
||||
const { reportWhen } = log.data || {}
|
||||
|
||||
const formatDate = createDateFormatter(schedule?.timezone)
|
||||
|
||||
if (log.tasks === undefined) {
|
||||
const markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Run ID**: ${log.id}`,
|
||||
`- **mode**: ${mode}`,
|
||||
...getTemporalDataMarkdown(log.end, log.start, formatDate),
|
||||
getErrorMarkdown(log),
|
||||
...getWarningsMarkdown(log.warnings),
|
||||
'---',
|
||||
'',
|
||||
`*${pkg.name} v${pkg.version}*`,
|
||||
]
|
||||
return this._sendReport({
|
||||
subject: `[Xen Orchestra] ${log.status} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
mailReceivers,
|
||||
markdown: toMarkdown(markdown),
|
||||
success: false,
|
||||
})
|
||||
}
|
||||
|
||||
const failedTasksText = []
|
||||
const skippedVmsText = []
|
||||
const successfulVmsText = []
|
||||
const interruptedVmsText = []
|
||||
const failedTasks = []
|
||||
const skippedVms = []
|
||||
const successfulVms = []
|
||||
const interruptedVms = []
|
||||
|
||||
let globalMergeSize = 0
|
||||
let globalTransferSize = 0
|
||||
@@ -375,34 +233,24 @@ class BackupReportsXoPlugin {
|
||||
let nSuccesses = 0
|
||||
let nInterrupted = 0
|
||||
|
||||
for (const taskLog of log.tasks) {
|
||||
for (const taskLog of log.tasks ?? []) {
|
||||
const { type, id } = taskLog.data ?? {}
|
||||
if (taskLog.message === 'get SR record' || taskLog.message === 'get remote adapter') {
|
||||
++nFailures
|
||||
failedTasksText.push(
|
||||
// It will ensure that it will never be in a nested list
|
||||
''
|
||||
)
|
||||
|
||||
try {
|
||||
if (type === 'SR') {
|
||||
const { name_label: name, uuid } = xo.getObject(id)
|
||||
failedTasksText.push(`### ${name}`, '', `- **UUID**: ${uuid}`)
|
||||
failedTasks.push({ taskLog, name, uuid })
|
||||
} else {
|
||||
const { name } = await xo.getRemote(id)
|
||||
failedTasksText.push(`### ${name}`, '', `- **UUID**: ${id}`)
|
||||
failedTasks.push({ taskLog, name, uuid: id })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(error)
|
||||
failedTasksText.push(`### ${UNKNOWN_ITEM}`, '', `- **UUID**: ${id}`)
|
||||
failedTasks.push({ taskLog, name: UNKNOWN_ITEM, uuid: id })
|
||||
}
|
||||
|
||||
failedTasksText.push(
|
||||
`- **Type**: ${type}`,
|
||||
...getTemporalDataMarkdown(taskLog.end, taskLog.start, formatDate),
|
||||
...getWarningsMarkdown(taskLog.warnings),
|
||||
`- **Error**: ${taskLog.result.message}`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -419,34 +267,21 @@ class BackupReportsXoPlugin {
|
||||
try {
|
||||
vm = xo.getObject(id)
|
||||
} catch (e) {}
|
||||
const text = [
|
||||
// It will ensure that it will never be in a nested list
|
||||
'',
|
||||
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
|
||||
'',
|
||||
`- **UUID**: ${vm !== undefined ? vm.uuid : id}`,
|
||||
...getTemporalDataMarkdown(taskLog.end, taskLog.start, formatDate),
|
||||
...getWarningsMarkdown(taskLog.warnings),
|
||||
]
|
||||
|
||||
const failedSubTasks = []
|
||||
const snapshotText = []
|
||||
const srsText = []
|
||||
const remotesText = []
|
||||
const failedSubTasks = [] // not used at the moment
|
||||
const snapshotSubtasks = []
|
||||
const srsSubTasks = []
|
||||
const remotesSubTasks = []
|
||||
|
||||
for (const subTaskLog of taskLog.tasks ?? []) {
|
||||
if (subTaskLog.message !== 'export' && subTaskLog.message !== 'snapshot') {
|
||||
continue
|
||||
}
|
||||
|
||||
const icon = STATUS_ICON[subTaskLog.status]
|
||||
const type = subTaskLog.data?.type
|
||||
const errorMarkdown = getErrorMarkdown(subTaskLog)
|
||||
|
||||
if (subTaskLog.message === 'snapshot') {
|
||||
snapshotText.push(`- **Snapshot** ${icon}`, [
|
||||
...getTemporalDataMarkdown(subTaskLog.end, subTaskLog.start, formatDate),
|
||||
])
|
||||
snapshotSubtasks.push({ subTaskLog })
|
||||
} else if (type === 'remote') {
|
||||
const id = subTaskLog.data.id
|
||||
const remote = await xo.getRemote(id).catch(error => {
|
||||
@@ -454,11 +289,7 @@ class BackupReportsXoPlugin {
|
||||
})
|
||||
const title = remote !== undefined ? remote.name : `Remote Not found`
|
||||
|
||||
remotesText.push(`- **${title}** (${id}) ${icon}`, [
|
||||
...getTemporalDataMarkdown(subTaskLog.end, subTaskLog.start, formatDate),
|
||||
...getWarningsMarkdown(subTaskLog.warnings),
|
||||
errorMarkdown,
|
||||
])
|
||||
remotesSubTasks.push({ subTaskLog, title, id })
|
||||
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(remote !== undefined ? remote.name : id)
|
||||
@@ -470,11 +301,7 @@ class BackupReportsXoPlugin {
|
||||
sr = xo.getObject(id)
|
||||
} catch (e) {}
|
||||
const [srName, srUuid] = sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, id]
|
||||
srsText.push(`- **${srName}** (${srUuid}) ${icon}`, [
|
||||
...getTemporalDataMarkdown(subTaskLog.end, subTaskLog.start, formatDate),
|
||||
...getWarningsMarkdown(subTaskLog.warnings),
|
||||
errorMarkdown,
|
||||
])
|
||||
srsSubTasks.push({ subTaskLog, title: srName, id: srUuid })
|
||||
if (subTaskLog.status === 'failure') {
|
||||
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
|
||||
}
|
||||
@@ -501,97 +328,65 @@ class BackupReportsXoPlugin {
|
||||
return
|
||||
}
|
||||
|
||||
const operationText = [
|
||||
`- **${operationLog.message}** ${STATUS_ICON[operationLog.status]}`,
|
||||
[
|
||||
...getTemporalDataMarkdown(operationLog.end, operationLog.start, formatDate),
|
||||
size > 0 && `- **Size**: ${formatSize(size)}`,
|
||||
size > 0 && `- **Speed**: ${formatSpeed(size, operationLog.end - operationLog.start)}`,
|
||||
...getWarningsMarkdown(operationLog.warnings),
|
||||
getErrorMarkdown(operationLog),
|
||||
],
|
||||
]
|
||||
if (type === 'remote') {
|
||||
remotesText.push(operationText)
|
||||
remotesSubTasks.push({ operationLog })
|
||||
} else if (type === 'SR') {
|
||||
srsText.push(operationText)
|
||||
srsSubTasks.push({ operationLog })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const subText = [
|
||||
...snapshotText,
|
||||
srsText.length !== 0 && `- **SRs**`,
|
||||
srsText,
|
||||
remotesText.length !== 0 && `- **Remotes**`,
|
||||
remotesText,
|
||||
]
|
||||
if (taskLog.result !== undefined) {
|
||||
if (taskLog.status === 'skipped') {
|
||||
++nSkipped
|
||||
skippedVmsText.push(
|
||||
...text,
|
||||
`- **Reason**: ${
|
||||
skippedVms.push({
|
||||
taskLog,
|
||||
vm,
|
||||
message:
|
||||
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR
|
||||
? UNHEALTHY_VDI_CHAIN_MESSAGE
|
||||
: taskLog.result.message
|
||||
}`
|
||||
)
|
||||
: taskLog.result.message,
|
||||
})
|
||||
} else {
|
||||
++nFailures
|
||||
failedTasksText.push(...text, `- **Error**: ${taskLog.result.message}`)
|
||||
failedTasks.push({ taskLog, vm })
|
||||
}
|
||||
} else {
|
||||
if (taskLog.status === 'failure') {
|
||||
++nFailures
|
||||
failedTasksText.push(...text, ...subText)
|
||||
failedTasks.push({ taskLog, vm, snapshotSubtasks, srsSubTasks, remotesSubTasks })
|
||||
} else if (taskLog.status === 'interrupted') {
|
||||
++nInterrupted
|
||||
interruptedVmsText.push(...text, ...subText)
|
||||
interruptedVms.push({ taskLog, vm, snapshotSubtasks, srsSubTasks, remotesSubTasks })
|
||||
} else {
|
||||
++nSuccesses
|
||||
successfulVmsText.push(...text, ...subText)
|
||||
successfulVms.push({ taskLog, vm, snapshotSubtasks, srsSubTasks, remotesSubTasks })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nVmTasks = nSuccesses + nFailures + nSkipped + nInterrupted
|
||||
|
||||
const markdown = [
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Run ID**: ${log.id}`,
|
||||
`- **mode**: ${mode}`,
|
||||
...getTemporalDataMarkdown(log.end, log.start, formatDate),
|
||||
`- **Successes**: ${nSuccesses} / ${nVmTasks}`,
|
||||
globalTransferSize !== 0 && `- **Transfer size**: ${formatSize(globalTransferSize)}`,
|
||||
globalMergeSize !== 0 && `- **Merge size**: ${formatSize(globalMergeSize)}`,
|
||||
...getWarningsMarkdown(log.warnings),
|
||||
'',
|
||||
]
|
||||
|
||||
if (nFailures !== 0) {
|
||||
markdown.push('---', '', `## ${nFailures} Failure${nFailures === 1 ? '' : 's'}`, '', ...failedTasksText)
|
||||
const context = {
|
||||
jobName,
|
||||
log,
|
||||
pkg,
|
||||
tasksByStatus: {
|
||||
failure: { tasks: failedTasks, count: nFailures },
|
||||
skipped: { tasks: skippedVms, count: nSkipped },
|
||||
interrupted: { tasks: interruptedVms, count: nInterrupted },
|
||||
success: { tasks: force || reportWhen !== 'failure' ? successfulVms : [], count: nSuccesses },
|
||||
vmTasks: { count: nVmTasks },
|
||||
},
|
||||
formatDate,
|
||||
globalMergeSize,
|
||||
globalTransferSize,
|
||||
}
|
||||
|
||||
if (nSkipped !== 0) {
|
||||
markdown.push('---', '', `## ${nSkipped} Skipped`, '', ...skippedVmsText)
|
||||
}
|
||||
|
||||
if (nInterrupted !== 0) {
|
||||
markdown.push('---', '', `## ${nInterrupted} Interrupted`, '', ...interruptedVmsText)
|
||||
}
|
||||
|
||||
if (nSuccesses !== 0 && (force || reportWhen !== 'failure')) {
|
||||
markdown.push('---', '', `## ${nSuccesses} Success${nSuccesses === 1 ? '' : 'es'}`, '', ...successfulVmsText)
|
||||
}
|
||||
|
||||
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
|
||||
return this._sendReport({
|
||||
mailReceivers,
|
||||
markdown: toMarkdown(markdown),
|
||||
subject: `[Xen Orchestra] ${log.status} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
markdown: compiledVmTemplate(context),
|
||||
subject: compiledVmSubject(context),
|
||||
success: log.status === 'success',
|
||||
})
|
||||
}
|
||||
|
||||
31
packages/xo-server-backup-reports/templates/metadata.hbs
Normal file
31
packages/xo-server-backup-reports/templates/metadata.hbs
Normal file
@@ -0,0 +1,31 @@
|
||||
## Global status: {{{log.status}}}
|
||||
|
||||
- **Job ID**: {{{log.jobId}}}
|
||||
- **Job name**: {{{jobName}}}
|
||||
- **Run ID**: {{{log.id}}}
|
||||
{{>reportTemporalData end=log.end start=log.start}}
|
||||
{{#if log.tasks.length}}
|
||||
- **Successes**: {{#if tasksByStatus.success.length ~}} {{{tasksByStatus.success.length}}} {{~else~}} 0 {{~/if}} / {{{log.tasks.length}}}
|
||||
{{/if}}
|
||||
{{>reportError task=log}}
|
||||
{{>reportWarnings task=log}}
|
||||
{{#each tasksByStatus}}
|
||||
---
|
||||
|
||||
{{titleByStatus @key}}
|
||||
{{#each this}}
|
||||
|
||||
|
||||
### {{>taskTitle task=. jobName=../../log.jobName}}
|
||||
{{>taskBody task=.}}
|
||||
{{>reportTemporalData formatDate=../../formatDate}}
|
||||
{{>reportError task=.}}
|
||||
{{>reportWarnings task=this}}
|
||||
{{#each this.tasks}}
|
||||
{{>metadataSubTask formatDate=../../../formatDate}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
---
|
||||
|
||||
*{{{pkg.name}}} v{{{pkg.version}}}*
|
||||
@@ -0,0 +1 @@
|
||||
[Xen Orchestra] {{{log.status}}} − Metadata backup report for {{{log.jobName}}} {{{getIcon log.status}}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{{#*inline "indentedBlock"}}
|
||||
{{>taskBody task=.}}
|
||||
{{>reportTemporalData}}
|
||||
{{>reportError task=.}}
|
||||
{{>reportWarnings task =.}}
|
||||
{{/inline}}
|
||||
- **{{>taskTitle task=. jobName=''}}** {{getIcon status}}
|
||||
{{> indentedBlock}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{#ifCond task.status '!==' 'success'}}
|
||||
{{#if task.result.message}}
|
||||
- **{{#ifCond task.status '===' 'skipped'~}} Reason {{~^~}} Error {{~/ifCond}}**: {{{task.result.message}}}
|
||||
{{else if task.result.code}}
|
||||
- **{{#ifCond task.status '===' 'skipped'~}} Reason {{~^~}} Error {{~/ifCond}}**: {{{task.result.code}}}
|
||||
{{/if}}
|
||||
{{/ifCond}}
|
||||
@@ -0,0 +1,7 @@
|
||||
- **Start time**: {{executeFunction formatDate start}}
|
||||
{{#if end}}
|
||||
- **End time**: {{executeFunction formatDate end}}
|
||||
{{#ifCond (subtract end start) '>=' 1}}
|
||||
- **Duration**: {{formatDuration (subtract end start)}}
|
||||
{{/ifCond}}
|
||||
{{/if}}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{#each task.warnings}}
|
||||
- **⚠️ {{{message}}}**
|
||||
{{/each}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{#ifCond task.data.type '===' 'remote'}}
|
||||
- **ID**: {{{task.data.id}}}
|
||||
{{/ifCond}}
|
||||
{{#ifCond task.data.type '===' 'pool'}}
|
||||
{{#if task.data.pool.uuid}}
|
||||
- **UUID**: {{{task.data.pool.uuid}}}
|
||||
{{else}}
|
||||
- **ID**: {{{task.data.id}}}
|
||||
{{/if}}
|
||||
{{/ifCond}}
|
||||
@@ -0,0 +1,9 @@
|
||||
{{#ifCond task.data.type '===' 'xo'}}
|
||||
[XO] {{{jobName}}}
|
||||
{{~/ifCond}}
|
||||
{{#ifCond task.data.type '===' 'remote'}}
|
||||
[remote] {{{task.additionnalData.name}}}
|
||||
{{~/ifCond}}
|
||||
{{#ifCond task.data.type '===' 'pool'}}
|
||||
[pool] {{#if task.data.pool.name_label ~}} {{{task.data.pool.name_label}}} {{~else if task.data.poolMaster.name_label ~}} {{{task.data.poolMaster.name_label}}} {{~else~}} Unknown {{~/if}}
|
||||
{{~/ifCond}}
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
|
||||
## {{{tasksByStatus.failure.count}}} {{#ifCond tasksByStatus.failure.count '>' 1~}} Failures {{~^~}} Failure {{~/ifCond}}
|
||||
|
||||
{{#each tasksByStatus.failure.tasks}}
|
||||
{{#if uuid}}
|
||||
|
||||
### {{{name}}}
|
||||
|
||||
- **UUID**: {{{uuid}}}
|
||||
- **Type**: {{{taskLog.data.type}}}
|
||||
{{>reportTemporalData end=taskLog.end start=taskLog.start}}
|
||||
{{>reportWarnings task=taskLog}}
|
||||
- **Error**: {{{taskLog.result.message}}}
|
||||
{{else}}
|
||||
{{>vmText formatDate=../formatDate}}
|
||||
{{#if taskLog.result}}
|
||||
- **Error**: {{{taskLog.result.message}}}
|
||||
{{else}}
|
||||
{{>vmSubText formatDate=../formatDate}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
|
||||
## {{{tasksByStatus.interrupted.count}}} Interrupted
|
||||
|
||||
{{#each tasksByStatus.interrupted.tasks}}
|
||||
{{>vmText formatDate=../formatDate}}
|
||||
{{>vmSubText formatDate=../formatDate}}
|
||||
{{/each}}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
|
||||
## {{{tasksByStatus.skipped.count}}} Skipped
|
||||
|
||||
{{#each tasksByStatus.skipped.tasks}}
|
||||
{{>vmText formatDate=../formatDate}}
|
||||
- **Reason**: {{{message}}}
|
||||
{{/each}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{#if subTaskLog}}
|
||||
- **{{{title}}}** ({{{id}}}) {{getIcon subTaskLog.status}}
|
||||
{{>reportTemporalData end=subTaskLog.end start=subTaskLog.start}}
|
||||
{{>reportWarnings task=subTaskLog}}
|
||||
{{>reportError task=subTaskLog}}
|
||||
{{else}}
|
||||
- **{{{operationLog.message}}}** {{getIcon operationLog.status}}
|
||||
{{>reportTemporalData end=operationLog.end start=operationLog.start}}
|
||||
{{#if operationLog.result.size}}
|
||||
- **Size**: {{formatSize operationLog.result.size}}
|
||||
- **Speed**: {{formatSpeed operationLog.result.size operationLog.start operationLog.end}}
|
||||
{{/if}}
|
||||
{{>reportWarnings task=operationLog}}
|
||||
{{>reportError task=operationLog}}
|
||||
{{/if}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{#each snapshotSubtasks}}
|
||||
- **Snapshot** {{getIcon subTaskLog.status}}
|
||||
{{>reportTemporalData end=subTaskLog.end start=subTaskLog.start formatDate=../formatDate}}
|
||||
{{/each}}
|
||||
{{#if srsSubTasks}}
|
||||
- **SRs**
|
||||
{{#each srsSubTasks}}
|
||||
{{>vmSubTask formatDate=../formatDate}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if remotesSubTasks}}
|
||||
- **Remotes**
|
||||
{{#each remotesSubTasks}}
|
||||
{{>vmSubTask formatDate=../formatDate}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
|
||||
## {{{tasksByStatus.success.count}}} {{#ifCond tasksByStatus.success.count '>' 1~}} Successes {{~^~}} Success {{~/ifCond}}
|
||||
|
||||
{{#each tasksByStatus.success.tasks}}
|
||||
{{>vmText formatDate=../formatDate}}
|
||||
{{>vmSubText formatDate=../formatDate}}
|
||||
{{/each}}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
{{#if vm}}
|
||||
### {{{vm.name_label}}}
|
||||
|
||||
- **UUID**: {{{vm.uuid}}}
|
||||
{{else}}
|
||||
### VM not found
|
||||
|
||||
- **UUID**: {{{taskLog.data.id}}}
|
||||
{{/if}}
|
||||
{{>reportTemporalData end=taskLog.end start=taskLog.start}}
|
||||
{{>reportWarnings task=taskLog}}
|
||||
35
packages/xo-server-backup-reports/templates/vm.hbs
Normal file
35
packages/xo-server-backup-reports/templates/vm.hbs
Normal file
@@ -0,0 +1,35 @@
|
||||
## Global status: {{{log.status}}}
|
||||
|
||||
- **Job ID**: {{{log.jobId}}}
|
||||
- **Run ID**: {{{log.id}}}
|
||||
- **Mode**: {{{log.data.mode}}}
|
||||
{{>reportTemporalData end=log.end start=log.start}}
|
||||
{{#if log.tasks}}
|
||||
- **Successes**: {{{tasksByStatus.success.count}}} / {{{tasksByStatus.vmTasks.count}}}
|
||||
{{#if globalTransferSize}}
|
||||
- **Transfer size**: {{formatSize globalTransferSize}}
|
||||
{{/if}}
|
||||
{{#if globalMergeSize}}
|
||||
- **Merge size**: {{formatSize globalMergeSize}}
|
||||
{{/if}}
|
||||
{{>reportWarnings task=log}}
|
||||
|
||||
{{#if tasksByStatus.failure.tasks}}
|
||||
{{>vmFailure}}
|
||||
{{/if}}
|
||||
{{#if tasksByStatus.skipped.tasks}}
|
||||
{{>vmSkipped}}
|
||||
{{/if}}
|
||||
{{#if tasksByStatus.interrupted.tasks}}
|
||||
{{>vmInterrupted}}
|
||||
{{/if}}
|
||||
{{#if tasksByStatus.success.tasks}}
|
||||
{{>vmSuccess}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{>reportError task=log}}
|
||||
{{>reportWarnings task=log}}
|
||||
{{/if}}
|
||||
---
|
||||
|
||||
*{{{pkg.name}}} v{{{pkg.version}}}*
|
||||
@@ -0,0 +1 @@
|
||||
[Xen Orchestra] {{{log.status}}} − Backup report for {{{jobName}}} {{{getIcon log.status}}}
|
||||
@@ -12147,7 +12147,7 @@ handle-thing@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
||||
|
||||
handlebars@^4.0.6, handlebars@^4.7.6:
|
||||
handlebars@^4.0.6, handlebars@^4.7.6, handlebars@^4.7.8:
|
||||
version "4.7.8"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9"
|
||||
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
|
||||
|
||||
Reference in New Issue
Block a user