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