feat(backup-reports): rewrite with Handlebars (#7543)

This commit is contained in:
b-Nollet
2024-06-05 15:47:06 +02:00
committed by GitHub
parent 99e44fe139
commit 3f4ca1b322
23 changed files with 378 additions and 298 deletions

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Ignore all handlebars files:
**/*.hbs

View File

@@ -28,5 +28,6 @@
<!--packages-start-->
- @vates/fuse-vhd patch
- xo-server-backup-reports major
<!--packages-end-->

View File

@@ -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"

View 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)
}
})

View File

@@ -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',
})
}

View 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}}}*

View File

@@ -0,0 +1 @@
[Xen Orchestra] {{{log.status}}} Metadata backup report for {{{log.jobName}}} {{{getIcon log.status}}}

View File

@@ -0,0 +1,8 @@
{{#*inline "indentedBlock"}}
{{>taskBody task=.}}
{{>reportTemporalData}}
{{>reportError task=.}}
{{>reportWarnings task =.}}
{{/inline}}
- **{{>taskTitle task=. jobName=''}}** {{getIcon status}}
{{> indentedBlock}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -0,0 +1,3 @@
{{#each task.warnings}}
- **⚠️ {{{message}}}**
{{/each}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -0,0 +1,8 @@
---
## {{{tasksByStatus.interrupted.count}}} Interrupted
{{#each tasksByStatus.interrupted.tasks}}
{{>vmText formatDate=../formatDate}}
{{>vmSubText formatDate=../formatDate}}
{{/each}}

View File

@@ -0,0 +1,8 @@
---
## {{{tasksByStatus.skipped.count}}} Skipped
{{#each tasksByStatus.skipped.tasks}}
{{>vmText formatDate=../formatDate}}
- **Reason**: {{{message}}}
{{/each}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}

View 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}}}*

View File

@@ -0,0 +1 @@
[Xen Orchestra] {{{log.status}}} Backup report for {{{jobName}}} {{{getIcon log.status}}}

View File

@@ -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==