feat(xo-web/backup): scheduled health check (#6227)

This commit is contained in:
Mathieu
2022-05-25 15:25:07 +02:00
committed by GitHub
parent 1f9cf458ec
commit cae3555ca7
6 changed files with 123 additions and 15 deletions

View File

@@ -17,10 +17,12 @@ get.params = {
id: { type: 'string' }, id: { type: 'string' },
} }
export function create({ cron, enabled, jobId, name, timezone }) { export function create({ cron, enabled, healthCheckSr, healthCheckVmsWithTags, jobId, name, timezone }) {
return this.createSchedule({ return this.createSchedule({
cron, cron,
enabled, enabled,
healthCheckSr,
healthCheckVmsWithTags,
jobId, jobId,
name, name,
timezone, timezone,
@@ -33,13 +35,15 @@ create.description = 'Creates a new schedule'
create.params = { create.params = {
cron: { type: 'string' }, cron: { type: 'string' },
enabled: { type: 'boolean', optional: true }, enabled: { type: 'boolean', optional: true },
healthCheckSr: { type: 'string', optional: true },
healthCheckVmsWithTags: { type: 'array', items: { type: 'string' }, optional: true },
jobId: { type: 'string' }, jobId: { type: 'string' },
name: { type: 'string', optional: true }, name: { type: 'string', optional: true },
timezone: { type: 'string', optional: true }, timezone: { type: 'string', optional: true },
} }
export async function set({ cron, enabled, id, jobId, name, timezone }) { export async function set({ cron, enabled, healthCheckSr, healthCheckVmsWithTags, id, jobId, name, timezone }) {
await this.updateSchedule({ cron, enabled, id, jobId, name, timezone }) await this.updateSchedule({ cron, enabled, healthCheckSr, healthCheckVmsWithTags, id, jobId, name, timezone })
} }
set.permission = 'admin' set.permission = 'admin'
@@ -47,6 +51,8 @@ set.description = 'Modifies an existing schedule'
set.params = { set.params = {
cron: { type: 'string', optional: true }, cron: { type: 'string', optional: true },
enabled: { type: 'boolean', optional: true }, enabled: { type: 'boolean', optional: true },
healthCheckSr: { type: 'string', optional: true },
healthCheckVmsWithTags: { type: 'array', items: { type: 'string' }, optional: true },
id: { type: 'string' }, id: { type: 'string' },
jobId: { type: 'string', optional: true }, jobId: { type: 'string', optional: true },
name: { type: ['string', 'null'], optional: true }, name: { type: ['string', 'null'], optional: true },

View File

@@ -1,6 +1,7 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js' import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import keyBy from 'lodash/keyBy.js' import keyBy from 'lodash/keyBy.js'
import { createSchedule } from '@xen-orchestra/cron' import { createSchedule } from '@xen-orchestra/cron'
import { ifDef } from '@xen-orchestra/defined'
import { ignoreErrors } from 'promise-toolbox' import { ignoreErrors } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors.js' import { noSuchObject } from 'xo-common/api-errors.js'
@@ -72,11 +73,13 @@ export default class Scheduling {
}) })
} }
async createSchedule({ cron, enabled, jobId, name = '', timezone, userId }) { async createSchedule({ cron, enabled, healthCheckSr, healthCheckVmsWithTags, jobId, name = '', timezone, userId }) {
const schedule = ( const schedule = (
await this._db.add({ await this._db.add({
cron, cron,
enabled, enabled,
healthCheckSr,
healthCheckVmsWithTags: JSON.stringify(healthCheckVmsWithTags),
jobId, jobId,
name, name,
timezone, timezone,
@@ -92,11 +95,17 @@ export default class Scheduling {
if (schedule === undefined) { if (schedule === undefined) {
throw noSuchObject(id, 'schedule') throw noSuchObject(id, 'schedule')
} }
return schedule.properties return {
...schedule.properties,
healthCheckVmsWithTags: ifDef(schedule.properties.healthCheckVmsWithTags, JSON.parse),
}
} }
async getAllSchedules() { async getAllSchedules() {
return this._db.get() return (await this._db.get()).map(schedule => ({
...schedule,
healthCheckVmsWithTags: ifDef(schedule.healthCheckVmsWithTags, JSON.parse),
}))
} }
async deleteSchedule(id) { async deleteSchedule(id) {
@@ -104,9 +113,19 @@ export default class Scheduling {
await this._db.remove(id) await this._db.remove(id)
} }
async updateSchedule({ cron, enabled, id, jobId, name, timezone, userId }) { async updateSchedule({ cron, enabled, healthCheckSr, healthCheckVmsWithTags, id, jobId, name, timezone, userId }) {
const schedule = await this.getSchedule(id) const schedule = await this.getSchedule(id)
patch(schedule, { cron, enabled, jobId, name, timezone, userId }) patch(schedule, {
cron,
enabled,
// null to delete the key of the object in case we remove healthcheck
healthCheckSr: healthCheckVmsWithTags !== undefined ? healthCheckSr : null,
healthCheckVmsWithTags: JSON.stringify(healthCheckVmsWithTags) ?? null,
jobId,
name,
timezone,
userId,
})
this._start(schedule) this._start(schedule)

View File

@@ -1662,10 +1662,15 @@ const messages = {
'Are you sure you want to delete all the backups from {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}?', 'Are you sure you want to delete all the backups from {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}?',
bulkDeleteMetadataBackupsConfirmText: bulkDeleteMetadataBackupsConfirmText:
'delete {nMetadataBackups} metadata backup{nMetadataBackups, plural, one {} other {s}}', 'delete {nMetadataBackups} metadata backup{nMetadataBackups, plural, one {} other {s}}',
healthCheck: 'Health check',
healthCheckChooseSr: 'Choose SR used for VMs restoration',
healthCheckTagsInfo: 'If no tags are specified, all VMs in the backup will be tested.',
healthCheckAvailablePremiumUser: 'Only available to premium users',
remoteNotCompatibleWithSelectedProxy: remoteNotCompatibleWithSelectedProxy:
"The backup will not be run on this remote because it's not compatible with the selected proxy", "The backup will not be run on this remote because it's not compatible with the selected proxy",
remoteLoadBackupsFailure: 'Loading backups failed', remoteLoadBackupsFailure: 'Loading backups failed',
remoteLoadBackupsFailureMessage: 'Failed to load backups from {name}.', remoteLoadBackupsFailureMessage: 'Failed to load backups from {name}.',
vmsTags: 'VMs tags',
// ----- Restore files view ----- // ----- Restore files view -----
restoreFiles: 'Restore backup files', restoreFiles: 'Restore backup files',

View File

@@ -2108,8 +2108,13 @@ export const cancelJob = ({ id, name, runId }) =>
// Backup/Schedule --------------------------------------------------------- // Backup/Schedule ---------------------------------------------------------
export const createSchedule = (jobId, { cron, enabled, name = undefined, timezone = undefined }) => export const createSchedule = (
_call('schedule.create', { jobId, cron, enabled, name, timezone })::tap(subscribeSchedules.forceRefresh) jobId,
{ cron, enabled, healthCheckSr, healthCheckVmsWithTags, name = undefined, timezone = undefined }
) =>
_call('schedule.create', { jobId, cron, enabled, healthCheckSr, healthCheckVmsWithTags, name, timezone })::tap(
subscribeSchedules.forceRefresh
)
export const deleteBackupSchedule = async schedule => { export const deleteBackupSchedule = async schedule => {
await confirm({ await confirm({
@@ -2140,8 +2145,10 @@ export const deleteSchedules = schedules =>
export const disableSchedule = id => editSchedule({ id, enabled: false }) export const disableSchedule = id => editSchedule({ id, enabled: false })
export const editSchedule = ({ id, jobId, cron, enabled, name, timezone }) => export const editSchedule = ({ id, jobId, cron, enabled, healthCheckSr, healthCheckVmsWithTags, name, timezone }) =>
_call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(subscribeSchedules.forceRefresh) _call('schedule.set', { id, jobId, cron, enabled, healthCheckSr, healthCheckVmsWithTags, name, timezone })::tap(
subscribeSchedules.forceRefresh
)
export const enableSchedule = id => editSchedule({ id, enabled: true }) export const enableSchedule = id => editSchedule({ id, enabled: true })

View File

@@ -299,7 +299,9 @@ const New = decorate([
newSchedule.cron !== oldSchedule.cron || newSchedule.cron !== oldSchedule.cron ||
newSchedule.name !== oldSchedule.name || newSchedule.name !== oldSchedule.name ||
newSchedule.timezone !== oldSchedule.timezone || newSchedule.timezone !== oldSchedule.timezone ||
newSchedule.enabled !== oldSchedule.enabled newSchedule.enabled !== oldSchedule.enabled ||
newSchedule.healthCheckSr !== oldSchedule.healthCheckSr ||
newSchedule.healthCheckVmsWithTags !== oldSchedule.healthCheckVmsWithTags
) { ) {
return editSchedule({ return editSchedule({
id, id,
@@ -307,6 +309,8 @@ const New = decorate([
name: newSchedule.name, name: newSchedule.name,
timezone: newSchedule.timezone, timezone: newSchedule.timezone,
enabled: newSchedule.enabled, enabled: newSchedule.enabled,
healthCheckSr: newSchedule.healthCheckSr,
healthCheckVmsWithTags: newSchedule.healthCheckVmsWithTags,
}) })
} }
}) })
@@ -322,6 +326,8 @@ const New = decorate([
name: schedule.name, name: schedule.name,
timezone: schedule.timezone, timezone: schedule.timezone,
enabled: schedule.enabled, enabled: schedule.enabled,
healthCheckSr: schedule.healthCheckSr,
healthCheckVmsWithTags: schedule.healthCheckVmsWithTags,
}) })
settings = settings.withMutations(settings => { settings = settings.withMutations(settings => {
@@ -488,7 +494,19 @@ const New = decorate([
saveSchedule: saveSchedule:
( (
_, _,
{ copyRetention, cron, enabled = true, exportRetention, fullInterval, id, name, snapshotRetention, timezone } {
copyRetention,
cron,
enabled = true,
exportRetention,
fullInterval,
healthCheckSr,
healthCheckVmsWithTags,
id,
name,
snapshotRetention,
timezone,
}
) => ) =>
({ propSettings, schedules, settings = propSettings }) => ({ ({ propSettings, schedules, settings = propSettings }) => ({
schedules: { schedules: {
@@ -497,6 +515,8 @@ const New = decorate([
...schedules[id], ...schedules[id],
cron, cron,
enabled, enabled,
healthCheckSr,
healthCheckVmsWithTags,
id, id,
name, name,
timezone, timezone,

View File

@@ -4,13 +4,16 @@ import Icon from 'icon'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling' import Scheduler, { SchedulePreview } from 'scheduling'
import Tooltip from 'tooltip' import Tooltip, { conditionalTooltip } from 'tooltip'
import { Card, CardBlock } from 'card' import { Card, CardBlock } from 'card'
import { generateId } from 'reaclette-utils' import { generateId } from 'reaclette-utils'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
import { Number } from 'form' import { Number } from 'form'
import { FormGroup, Input } from './../utils' import { FormGroup, Input } from './../utils'
import Tags from '../../../common/tags'
import { getXoaPlan, PREMIUM } from '../../../common/xoa-plans'
import { SelectSr } from '../../../common/select-objects'
const New = decorate([ const New = decorate([
provideState({ provideState({
@@ -64,11 +67,26 @@ const New = decorate([
name: value.trim() === '' ? null : value, name: value.trim() === '' ? null : value,
}) })
}, },
setHealthCheckTags({ setSchedule }, tags) {
setSchedule({
healthCheckVmsWithTags: tags,
})
},
toggleForceFullBackup({ setSchedule }) { toggleForceFullBackup({ setSchedule }) {
setSchedule({ setSchedule({
fullInterval: this.state.forceFullBackup ? undefined : 1, fullInterval: this.state.forceFullBackup ? undefined : 1,
}) })
}, },
toggleHealthCheck({ setSchedule }, { target: { checked } }) {
setSchedule({
healthCheckVmsWithTags: checked ? [] : undefined,
})
},
setHealthCheckSr({ setSchedule }, sr) {
setSchedule({
healthCheckSr: sr.id,
})
},
}, },
}), }),
injectState, injectState,
@@ -115,6 +133,39 @@ const New = decorate([
<Number min='0' onChange={effects.setSnapshotRetention} value={schedule.snapshotRetention} required /> <Number min='0' onChange={effects.setSnapshotRetention} value={schedule.snapshotRetention} required />
</FormGroup> </FormGroup>
)} )}
<FormGroup>
<label>
<strong>{_('healthCheck')}</strong>{' '}
{conditionalTooltip(
<input
checked={schedule.healthCheckVmsWithTags !== undefined}
disabled={getXoaPlan().value < PREMIUM.value}
onChange={effects.toggleHealthCheck}
type='checkbox'
/>,
getXoaPlan().value < PREMIUM.value ? _('healthCheckAvailablePremiumUser') : undefined
)}
</label>
{schedule.healthCheckVmsWithTags !== undefined && (
<div className='mb-2'>
<strong>{_('vmsTags')}</strong>
<br />
<em>
<Icon icon='info' /> {_('healthCheckTagsInfo')}
</em>
<p className='h2'>
<Tags labels={schedule.healthCheckVmsWithTags} onChange={effects.setHealthCheckTags} />
</p>
<strong>{_('sr')}</strong>
<SelectSr
onChange={effects.setHealthCheckSr}
placeholder={_('healthCheckChooseSr')}
required
value={schedule.healthCheckSr}
/>
</div>
)}
</FormGroup>
{modes.isDelta && ( {modes.isDelta && (
<FormGroup> <FormGroup>
<label> <label>