feat(xo-web/metadata-backups): metadata logs implementation (#4014)

Fixes #4005
This commit is contained in:
badrAZ 2019-04-11 12:00:33 +02:00 committed by Pierre Donias
parent 88160bae1d
commit 865d2df124
3 changed files with 296 additions and 257 deletions

View File

@ -8,6 +8,7 @@
- [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129)) - [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129))
- [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126)) - [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126))
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121)) - [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
- [Metadata backup] Logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
### Bug fixes ### Bug fixes

View File

@ -38,7 +38,6 @@ export const LogStatus = ({ log, tooltip = _('logDisplayDetails') }) => {
return ( return (
<ActionButton <ActionButton
btnStyle={className} btnStyle={className}
disabled={log.status !== 'failure' && isEmpty(log.tasks)}
handler={showTasks} handler={showTasks}
handlerParam={log.id} handlerParam={log.id}
icon='preview' icon='preview'

View File

@ -1,16 +1,20 @@
import _, { FormattedDuration } from 'intl' import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button' import ActionButton from 'action-button'
import decorate from 'apply-decorators' import decorate from 'apply-decorators'
import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon' import Icon from 'icon'
import React from 'react' import React from 'react'
import Select from 'form/select' import Select from 'form/select'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { addSubscriptions, formatSize, formatSpeed } from 'utils' import { addSubscriptions, formatSize, formatSpeed } from 'utils'
import { countBy, filter, get, keyBy, map } from 'lodash' import { countBy, cloneDeep, filter, keyBy, map } from 'lodash'
import { FormattedDate } from 'react-intl' import { FormattedDate } from 'react-intl'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo' import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
import { Vm, Sr, Remote } from 'render-xo-item' import { Vm, Sr, Remote, Pool } from 'render-xo-item'
const hasTaskFailed = ({ status }) =>
status !== 'success' && status !== 'pending'
const TASK_STATUS = { const TASK_STATUS = {
failure: { failure: {
@ -44,19 +48,75 @@ const TaskStateInfos = ({ status }) => {
) )
} }
const TaskDate = ({ label, value }) => const TaskDate = ({ value }) => (
_.keyValue( <FormattedDate
_(label), value={new Date(value)}
<FormattedDate month='short'
value={new Date(value)} day='numeric'
month='short' year='numeric'
day='numeric' hour='2-digit'
year='numeric' minute='2-digit'
hour='2-digit' second='2-digit'
minute='2-digit' />
second='2-digit' )
/>
const TaskStart = ({ task }) => (
<div>{_.keyValue(_('taskStart'), <TaskDate value={task.start} />)}</div>
)
const TaskEnd = ({ task }) =>
task.end !== undefined ? (
<div>{_.keyValue(_('taskEnd'), <TaskDate value={task.end} />)}</div>
) : null
const TaskDuration = ({ task }) =>
task.end !== undefined ? (
<div>
{_.keyValue(
_('taskDuration'),
<FormattedDuration duration={task.end - task.start} />
)}
</div>
) : null
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
const UNHEALTHY_VDI_CHAIN_LINK =
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
const TaskError = ({ task }) => {
let message
if (
!hasTaskFailed(task) ||
(message = defined(() => task.result.message, () => task.result.code)) ===
undefined
) {
return null
}
if (message === UNHEALTHY_VDI_CHAIN_ERROR) {
return (
<div>
<Tooltip content={_('clickForMoreInformation')}>
<a
className='text-info'
href={UNHEALTHY_VDI_CHAIN_LINK}
rel='noopener noreferrer'
target='_blank'
>
<Icon icon='info' /> {_('unhealthyVdiChainError')}
</a>
</Tooltip>
</div>
)
}
const [label, className] =
task.status === 'skipped'
? [_('taskReason'), 'text-info']
: [_('taskError'), 'text-danger']
return (
<div>{_.keyValue(label, <span className={className}>{message}</span>)}</div>
) )
}
const Warnings = ({ warnings }) => const Warnings = ({ warnings }) =>
warnings !== undefined ? ( warnings !== undefined ? (
@ -72,9 +132,168 @@ const Warnings = ({ warnings }) =>
</div> </div>
) : null ) : null
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain' const VmTask = ({ children, restartVmJob, task }) => (
const UNHEALTHY_VDI_CHAIN_LINK = <div>
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection' <Vm id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />{' '}
{restartVmJob !== undefined && hasTaskFailed(task) && (
<ActionButton
handler={restartVmJob}
icon='run'
size='small'
tooltip={_('backupRestartVm')}
data-vm={task.data.id}
/>
)}
<Warnings warnings={task.warnings} />
{children}
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
{task.transfer !== undefined && (
<div>
{_.keyValue(
_('taskTransferredDataSize'),
formatSize(task.transfer.size)
)}
<br />
{_.keyValue(
_('taskTransferredDataSpeed'),
formatSpeed(task.transfer.size, task.transfer.duration)
)}
</div>
)}
{task.merge !== undefined && (
<div>
{_.keyValue(_('taskMergedDataSize'), formatSize(task.merge.size))}
<br />
{_.keyValue(
_('taskMergedDataSpeed'),
formatSpeed(task.merge.size, task.merge.duration)
)}
</div>
)}
{task.isFull !== undefined &&
_.keyValue(_('exportType'), task.isFull ? 'full' : 'delta')}
</div>
)
const PoolTask = ({ children, task }) => (
<div>
<Pool id={task.data.id} link newTab />{' '}
<TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
{children}
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
</div>
)
const XoTask = ({ children, task }) => (
<div>
<Icon icon='menu-xoa' /> XO <TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
{children}
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
</div>
)
const SnapshotTask = ({ task }) => (
<div>
<Icon icon='task' /> {_('snapshotVmLabel')}{' '}
<TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskError task={task} />
</div>
)
const RemoteTask = ({ children, task }) => (
<div>
<Remote id={task.data.id} link newTab />{' '}
<TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
{children}
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
</div>
)
const SrTask = ({ children, task }) => (
<div>
<Sr id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
{children}
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
</div>
)
const TransferMergeTask = ({ task }) => {
const size = get(() => task.result.size)
return (
<div>
<Icon icon='task' /> {task.message}{' '}
<TaskStateInfos status={task.status} />
<Warnings warnings={task.warnings} />
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskDuration task={task} />
<TaskError task={task} />
{size > 0 && (
<div>
{_.keyValue(_('operationSize'), formatSize(size))}
<br />
{_.keyValue(
_('operationSpeed'),
formatSpeed(size, task.end - task.start)
)}
</div>
)}
</div>
)
}
const COMPONENT_BY_TYPE = {
vm: VmTask,
remote: RemoteTask,
sr: SrTask,
pool: PoolTask,
xo: XoTask,
}
const COMPONENT_BY_MESSAGE = {
snapshot: SnapshotTask,
merge: TransferMergeTask,
transfer: TransferMergeTask,
}
const TaskLi = ({ className, task, ...props }) => {
let Component
if (
(Component = defined(
() => COMPONENT_BY_TYPE[task.data.type.toLowerCase()],
COMPONENT_BY_MESSAGE[task.message]
)) === undefined
) {
return null
}
return (
<li className={className}>
<Component task={task} {...props} />
</li>
)
}
export default decorate([ export default decorate([
addSubscriptions(({ id }) => ({ addSubscriptions(({ id }) => ({
@ -107,10 +326,39 @@ export default decorate([
}, },
}, },
computed: { computed: {
filteredTaskLogs: ( log: (_, { log }) => {
{ defaultFilter, filter: value = defaultFilter }, if (log === undefined) {
{ log = {} } return {}
) => }
if (log.tasks === undefined) {
return log
}
let newLog
log.tasks.forEach((task, key) => {
if (task.tasks === undefined || get(() => task.data.type) !== 'VM') {
return
}
const subTaskWithIsFull = task.tasks.find(
({ data = {} }) => data.isFull !== undefined
)
if (subTaskWithIsFull !== undefined) {
if (newLog === undefined) {
newLog = cloneDeep(log)
}
newLog.tasks[key].isFull = subTaskWithIsFull.data.isFull
}
})
return defined(newLog, log)
},
filteredTaskLogs: ({
defaultFilter,
filter: value = defaultFilter,
log,
}) =>
value === 'all' value === 'all'
? log.tasks ? log.tasks
: filter(log.tasks, ({ status }) => status === value), : filter(log.tasks, ({ status }) => status === value),
@ -119,8 +367,8 @@ export default decorate([
{_(label)} ({countByStatus[value] || 0}) {_(label)} ({countByStatus[value] || 0})
</span> </span>
), ),
countByStatus: (_, { log = {} }) => ({ countByStatus: ({ log }) => ({
all: get(log.tasks, 'length'), all: get(() => log.tasks.length),
...countBy(log.tasks, 'status'), ...countBy(log.tasks, 'status'),
}), }),
options: ({ countByStatus }) => [ options: ({ countByStatus }) => [
@ -169,13 +417,13 @@ export default decorate([
}, },
}), }),
injectState, injectState,
({ log = {}, remotes, state, effects }) => { ({ remotes, state, effects }) => {
const { status, result, scheduleId } = log const { scheduleId, warnings, tasks = [] } = state.log
return (status === 'failure' || status === 'skipped') && return tasks.length === 0 ? (
result !== undefined ? ( <div>
<span className={status === 'skipped' ? 'text-info' : 'text-danger'}> <Warnings warnings={warnings} />
<Icon icon='alarm' /> {result.message} <TaskError task={state.log} />
</span> </div>
) : ( ) : (
<div> <div>
<Select <Select
@ -188,238 +436,29 @@ export default decorate([
value={state.filter || state.defaultFilter} value={state.filter || state.defaultFilter}
valueKey='value' valueKey='value'
/> />
<Warnings warnings={log.warnings} /> <Warnings warnings={warnings} />
<br /> <br />
<ul className='list-group'> <ul className='list-group'>
{map(state.filteredTaskLogs, taskLog => { {map(state.filteredTaskLogs, taskLog => {
let globalIsFull
return ( return (
<li key={taskLog.data.id} className='list-group-item'> <TaskLi
<Vm id={taskLog.data.id} link newTab /> ( className='list-group-item'
{taskLog.data.id.slice(4, 8)}){' '} key={taskLog.id}
<TaskStateInfos status={taskLog.status} />{' '} restartVmJob={scheduleId && effects.restartVmJob}
{scheduleId !== undefined && task={taskLog}
taskLog.status !== 'success' && >
taskLog.status !== 'pending' && (
<ActionButton
handler={effects.restartVmJob}
icon='run'
size='small'
tooltip={_('backupRestartVm')}
data-vm={taskLog.data.id}
/>
)}
<Warnings warnings={taskLog.warnings} />
<ul> <ul>
{map(taskLog.tasks, subTaskLog => { {map(taskLog.tasks, subTaskLog => (
if ( <TaskLi key={subTaskLog.id} task={subTaskLog}>
subTaskLog.message !== 'export' && <ul>
subTaskLog.message !== 'snapshot' {map(subTaskLog.tasks, subSubTaskLog => (
) { <TaskLi task={subSubTaskLog} key={subSubTaskLog.id} />
return ))}
} </ul>
</TaskLi>
const isFull = get(subTaskLog.data, 'isFull') ))}
if (isFull !== undefined && globalIsFull === undefined) {
globalIsFull = isFull
}
return (
<li key={subTaskLog.id}>
{subTaskLog.message === 'snapshot' ? (
<span>
<Icon icon='task' /> {_('snapshotVmLabel')}
</span>
) : subTaskLog.data.type === 'remote' ? (
<span>
<Remote id={subTaskLog.data.id} link newTab /> (
{subTaskLog.data.id.slice(4, 8)})
</span>
) : (
<span>
<Sr id={subTaskLog.data.id} link newTab /> (
{subTaskLog.data.id.slice(4, 8)})
</span>
)}{' '}
<TaskStateInfos status={subTaskLog.status} />
<Warnings warnings={subTaskLog.warnings} />
<ul>
{map(subTaskLog.tasks, operationLog => {
if (
operationLog.message !== 'merge' &&
operationLog.message !== 'transfer'
) {
return
}
return (
<li key={operationLog.id}>
<span>
<Icon icon='task' /> {operationLog.message}
</span>{' '}
<TaskStateInfos status={operationLog.status} />
<Warnings warnings={operationLog.warnings} />
<br />
<TaskDate
label='taskStart'
value={operationLog.start}
/>
{operationLog.end !== undefined && (
<div>
<TaskDate
label='taskEnd'
value={operationLog.end}
/>
<br />
{_.keyValue(
_('taskDuration'),
<FormattedDuration
duration={
operationLog.end - operationLog.start
}
/>
)}
<br />
{operationLog.status === 'failure'
? _.keyValue(
_('taskError'),
<span className='text-danger'>
{operationLog.result.message}
</span>
)
: operationLog.result.size > 0 && (
<div>
{_.keyValue(
_('operationSize'),
formatSize(
operationLog.result.size
)
)}
<br />
{_.keyValue(
_('operationSpeed'),
formatSpeed(
operationLog.result.size,
operationLog.end -
operationLog.start
)
)}
</div>
)}
</div>
)}
</li>
)
})}
</ul>
<TaskDate label='taskStart' value={subTaskLog.start} />
{subTaskLog.end !== undefined && (
<div>
<TaskDate label='taskEnd' value={subTaskLog.end} />
<br />
{subTaskLog.message !== 'snapshot' &&
_.keyValue(
_('taskDuration'),
<FormattedDuration
duration={subTaskLog.end - subTaskLog.start}
/>
)}
<br />
{subTaskLog.status === 'failure' &&
subTaskLog.result !== undefined &&
_.keyValue(
_('taskError'),
<span className='text-danger'>
{subTaskLog.result.message}
</span>
)}
</div>
)}
</li>
)
})}
</ul> </ul>
<TaskDate label='taskStart' value={taskLog.start} /> </TaskLi>
<br />
{taskLog.end !== undefined && (
<div>
<TaskDate label='taskEnd' value={taskLog.end} />
<br />
{_.keyValue(
_('taskDuration'),
<FormattedDuration
duration={taskLog.end - taskLog.start}
/>
)}
<br />
{taskLog.result !== undefined ? (
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
<Tooltip content={_('clickForMoreInformation')}>
<a
className='text-info'
href={UNHEALTHY_VDI_CHAIN_LINK}
rel='noopener noreferrer'
target='_blank'
>
<Icon icon='info' /> {_('unhealthyVdiChainError')}
</a>
</Tooltip>
) : (
_.keyValue(
taskLog.status === 'skipped'
? _('taskReason')
: _('taskError'),
<span
className={
taskLog.status === 'skipped'
? 'text-info'
: 'text-danger'
}
>
{taskLog.result.message}
</span>
)
)
) : (
<div>
{taskLog.transfer !== undefined && (
<div>
{_.keyValue(
_('taskTransferredDataSize'),
formatSize(taskLog.transfer.size)
)}
<br />
{_.keyValue(
_('taskTransferredDataSpeed'),
formatSpeed(
taskLog.transfer.size,
taskLog.transfer.duration
)
)}
</div>
)}
{taskLog.merge !== undefined && (
<div>
{_.keyValue(
_('taskMergedDataSize'),
formatSize(taskLog.merge.size)
)}
<br />
{_.keyValue(
_('taskMergedDataSpeed'),
formatSpeed(
taskLog.merge.size,
taskLog.merge.duration
)
)}
</div>
)}
</div>
)}
</div>
)}
{globalIsFull !== undefined &&
_.keyValue(_('exportType'), globalIsFull ? 'full' : 'delta')}
</li>
) )
})} })}
</ul> </ul>