feat(xo-web/backup): scheduled health check (#6227)
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user