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

View File

@@ -1,6 +1,7 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import keyBy from 'lodash/keyBy.js'
import { createSchedule } from '@xen-orchestra/cron'
import { ifDef } from '@xen-orchestra/defined'
import { ignoreErrors } from 'promise-toolbox'
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 = (
await this._db.add({
cron,
enabled,
healthCheckSr,
healthCheckVmsWithTags: JSON.stringify(healthCheckVmsWithTags),
jobId,
name,
timezone,
@@ -92,11 +95,17 @@ export default class Scheduling {
if (schedule === undefined) {
throw noSuchObject(id, 'schedule')
}
return schedule.properties
return {
...schedule.properties,
healthCheckVmsWithTags: ifDef(schedule.properties.healthCheckVmsWithTags, JSON.parse),
}
}
async getAllSchedules() {
return this._db.get()
return (await this._db.get()).map(schedule => ({
...schedule,
healthCheckVmsWithTags: ifDef(schedule.healthCheckVmsWithTags, JSON.parse),
}))
}
async deleteSchedule(id) {
@@ -104,9 +113,19 @@ export default class Scheduling {
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)
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)

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}}?',
bulkDeleteMetadataBackupsConfirmText:
'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:
"The backup will not be run on this remote because it's not compatible with the selected proxy",
remoteLoadBackupsFailure: 'Loading backups failed',
remoteLoadBackupsFailureMessage: 'Failed to load backups from {name}.',
vmsTags: 'VMs tags',
// ----- Restore files view -----
restoreFiles: 'Restore backup files',

View File

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

View File

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

View File

@@ -4,13 +4,16 @@ import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import Tooltip from 'tooltip'
import Tooltip, { conditionalTooltip } from 'tooltip'
import { Card, CardBlock } from 'card'
import { generateId } from 'reaclette-utils'
import { injectState, provideState } from 'reaclette'
import { Number } from 'form'
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([
provideState({
@@ -64,11 +67,26 @@ const New = decorate([
name: value.trim() === '' ? null : value,
})
},
setHealthCheckTags({ setSchedule }, tags) {
setSchedule({
healthCheckVmsWithTags: tags,
})
},
toggleForceFullBackup({ setSchedule }) {
setSchedule({
fullInterval: this.state.forceFullBackup ? undefined : 1,
})
},
toggleHealthCheck({ setSchedule }, { target: { checked } }) {
setSchedule({
healthCheckVmsWithTags: checked ? [] : undefined,
})
},
setHealthCheckSr({ setSchedule }, sr) {
setSchedule({
healthCheckSr: sr.id,
})
},
},
}),
injectState,
@@ -115,6 +133,39 @@ const New = decorate([
<Number min='0' onChange={effects.setSnapshotRetention} value={schedule.snapshotRetention} required />
</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 && (
<FormGroup>
<label>