Skip to content

Commit

Permalink
feat(xo-web/backup): UI mirror backup implementation (#6858)
Browse files Browse the repository at this point in the history
See #6854
  • Loading branch information
MathieuRA authored May 31, 2023
1 parent 83c5c97 commit ee0adae
Show file tree
Hide file tree
Showing 14 changed files with 649 additions and 72 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Tasks] New type of tasks created by XO ("XO Tasks" section) (PRs [#6861](https://github.com/vatesfr/xen-orchestra/pull/6861) [#6869](https://github.com/vatesfr/xen-orchestra/pull/6869))
- [Backup/Health check] Add basic XO task for manual health check
- [Backup] Implementation of mirror backup (Entreprise plan) (PRs [#6858](https://github.com/vatesfr/xen-orchestra/pull/6858), [#6854](https://github.com/vatesfr/xen-orchestra/pull/6854))

### Bug fixes

Expand Down
7 changes: 7 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ const messages = {
xoConfig: 'XO config',
backupVms: 'Backup VMs',
backupMetadata: 'Backup metadata',
mirrorBackup: 'Mirror backup',
mirrorBackupVms: 'Mirror backup VMs',
jobsOverviewPage: 'Overview',
jobsNewPage: 'New',
jobsSchedulingPage: 'Scheduling',
Expand Down Expand Up @@ -471,6 +473,7 @@ const messages = {
missingBackupName: "A name is required to create the backup's job!",
missingVms: 'Missing VMs!',
missingBackupMode: 'You need to choose a backup mode!',
missingRemote: 'Missing remote!',
missingRemotes: 'Missing remotes!',
missingSrs: 'Missing SRs!',
missingPools: 'Missing pools!',
Expand Down Expand Up @@ -587,8 +590,12 @@ const messages = {
confirmDeleteBackupJobsTitle: 'Delete backup job{nJobs, plural, one {} other {s}}',
confirmDeleteBackupJobsBody:
'Are you sure you want to delete {nJobs, number} backup job{nJobs, plural, one {} other {s}}?',
mirrorFullBackup: 'Mirror full backup',
mirrorIncrementalBackup: 'Mirror incremental backup',
runBackupJob: 'Run backup job once',
speedLimit: 'Speed limit (in MiB/s)',
sourceRemote: 'Source remote',
targetRemotes: 'Target remotes',

// ------ Remote -----
remoteName: 'Name',
Expand Down
22 changes: 20 additions & 2 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2375,8 +2375,8 @@ export const createBackupNgJob = props => _call('backupNg.createJob', props)::ta

export const getSuggestedExcludedTags = () => _call('backupNg.getSuggestedExcludedTags')

export const deleteBackupJobs = async ({ backupIds = [], metadataBackupIds = [] }) => {
const nJobs = backupIds.length + metadataBackupIds.length
export const deleteBackupJobs = async ({ backupIds = [], metadataBackupIds = [], mirrorBackupIds = [] }) => {
const nJobs = backupIds.length + metadataBackupIds.length + mirrorBackupIds.length
if (nJobs === 0) {
return
}
Expand Down Expand Up @@ -2405,6 +2405,13 @@ export const deleteBackupJobs = async ({ backupIds = [], metadataBackupIds = []
)
)
}
if (mirrorBackupIds.length !== 0) {
promises.push(
Promise.all(mirrorBackupIds.map(id => _call('mirrorBackup.deleteJob', { id: resolveId(id) })))::tap(
subscribeMirrorBackupJobs.forceRefresh
)
)
}

return Promise.all(promises)::tap(subscribeSchedules.forceRefresh)
}
Expand Down Expand Up @@ -2490,6 +2497,17 @@ export const deleteMetadataBackups = async (backups = []) => {
}
}

// Mirror backup ---------------------------------------------------------

export const subscribeMirrorBackupJobs = createSubscription(() => _call('mirrorBackup.getAllJobs'))

export const createMirrorBackupJob = props =>
_call('mirrorBackup.createJob', props)::tap(subscribeMirrorBackupJobs.forceRefresh)

export const runMirrorBackupJob = props => _call('mirrorBackup.runJob', props)

export const editMirrorBackupJob = props => _call('mirrorBackup.editJob', props)

// Plugins -----------------------------------------------------------

export const loadPlugin = async id =>
Expand Down
4 changes: 4 additions & 0 deletions packages/xo-web/src/icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@
@extend .fa;
@extend .fa-check;
}
&-mirror-backup {
@extend .fa;
@extend .fa-files-o;
}
&-restore {
@extend .fa;
@extend .fa-upload;
Expand Down
9 changes: 7 additions & 2 deletions packages/xo-web/src/xo-app/backup/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ import Icon from 'icon'
import React from 'react'
import { injectState, provideState } from 'reaclette'
import { find, groupBy, keyBy } from 'lodash'
import { subscribeBackupNgJobs, subscribeMetadataBackupJobs, subscribeSchedules } from 'xo'
import { subscribeBackupNgJobs, subscribeMetadataBackupJobs, subscribeMirrorBackupJobs, subscribeSchedules } from 'xo'

import Metadata from './new/metadata'
import New from './new'
import NewMirrorBackup from './new/mirror'

export default decorate([
addSubscriptions({
jobs: subscribeBackupNgJobs,
metadataJobs: subscribeMetadataBackupJobs,
mirrorBackupJobs: subscribeMirrorBackupJobs,
schedulesByJob: cb =>
subscribeSchedules(schedules => {
cb(groupBy(schedules, 'jobId'))
}),
}),
provideState({
computed: {
job: (_, { jobs, metadataJobs, routeParams: { id } }) => defined(find(jobs, { id }), find(metadataJobs, { id })),
job: (_, { jobs, metadataJobs, mirrorBackupJobs, routeParams: { id } }) =>
defined(find(jobs, { id }), find(metadataJobs, { id }), find(mirrorBackupJobs, { id })),
schedules: (_, { schedulesByJob, routeParams: { id } }) => schedulesByJob && keyBy(schedulesByJob[id], 'id'),
loading: (_, props) =>
props.jobs === undefined || props.metadataJobs === undefined || props.schedulesByJob === undefined,
Expand All @@ -38,6 +41,8 @@ export default decorate([
</span>
) : job.type === 'backup' ? (
<New job={job} schedules={schedules} />
) : job.type === 'mirrorBackup' ? (
<NewMirrorBackup job={job} schedules={schedules} />
) : (
<Metadata job={job} schedules={schedules} />
),
Expand Down
6 changes: 5 additions & 1 deletion packages/xo-web/src/xo-app/backup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { subscribeBackupNgJobs, subscribeSchedules } from 'xo'
import Edit from './edit'
import FileRestore from './file-restore'
import Health from './health'
import NewVmBackup, { NewMetadataBackup } from './new'
import NewVmBackup, { NewMetadataBackup, NewMirrorBackup } from './new'
import Overview from './overview'
import Restore, { RestoreMetadata } from './restore'

Expand Down Expand Up @@ -81,6 +81,9 @@ const ChooseBackupType = () => (
<ButtonLink to='backup/new/vms'>
<Icon icon='backup' /> {_('backupVms')}
</ButtonLink>{' '}
<ButtonLink to='backup/new/mirror'>
<Icon icon='mirror-backup' /> {_('mirrorBackupVms')}
</ButtonLink>{' '}
<ButtonLink to='backup/new/metadata'>
<Icon icon='database' /> {_('backupMetadata')}
</ButtonLink>
Expand All @@ -95,6 +98,7 @@ export default routes('overview', {
':id/edit': Edit,
new: ChooseBackupType,
'new/vms': NewVmBackup,
'new/mirror': NewMirrorBackup,
'new/metadata': NewMetadataBackup,
overview: Overview,
restore: Restore,
Expand Down
4 changes: 3 additions & 1 deletion packages/xo-web/src/xo-app/backup/new/_schedules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ const Schedules = decorate([
...newSetting
} = await form({
defaultValue: setDefaultRetentions({ cron, name, timezone, ...setting }, state.retentions),
render: props => <NewSchedule retentions={state.retentions} {...props} />,
render: formProps => (
<NewSchedule retentions={state.retentions} withHealthCheck={props.withHealthCheck} {...formProps} />
),
header: (
<span>
<Icon icon='schedule' /> {_('schedule')}
Expand Down
24 changes: 23 additions & 1 deletion packages/xo-web/src/xo-app/backup/new/_schedules/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { FormGroup, Input } from '../../utils'

import { areRetentionsMissing } from '.'

import ScheduleHealthCheck from '../healthCheck/ScheduleHealthCheck'

export default decorate([
provideState({
effects: {
Expand Down Expand Up @@ -42,6 +44,18 @@ export default decorate([
[name]: value,
})
},
toggleHealthCheck:
({ setSchedule }, { target: { checked } }) =>
state =>
setSchedule({
healthCheckVmsWithTags: checked ? [] : undefined,
healthCheckSr: checked ? state.healthCheckSr : undefined,
}),
setHealthCheckSr: ({ setSchedule }, sr) => setSchedule({ healthCheckSr: sr.id }),
setHealthCheckTags: ({ setSchedule }, tags) =>
setSchedule({
healthCheckVmsWithTags: tags,
}),
},
computed: {
idInputName: generateId,
Expand All @@ -50,7 +64,7 @@ export default decorate([
},
}),
injectState,
({ effects, state, retentions, value: schedule }) => (
({ effects, state, retentions, value: schedule, withHealthCheck = false }) => (
<div>
{state.missingRetentions && (
<div className='text-danger text-md-center'>
Expand All @@ -71,6 +85,14 @@ export default decorate([
<Number data-name={valuePath} min='0' onChange={effects.setRetention} required value={schedule[valuePath]} />
</FormGroup>
))}
{withHealthCheck && (
<ScheduleHealthCheck
setHealthCheckSr={effects.setHealthCheckSr}
setHealthCheckTags={effects.setHealthCheckTags}
schedule={schedule}
toggleHealthCheck={effects.toggleHealthCheck}
/>
)}
<Scheduler onChange={effects.setCronTimezone} cronPattern={schedule.cron} timezone={schedule.timezone} />
<SchedulePreview cronPattern={schedule.cron} timezone={schedule.timezone} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import _ from 'intl'
import Icon from 'icon'
import React from 'react'
import Tags from 'tags'
import { conditionalTooltip } from 'tooltip'
import { getXoaPlan, ENTERPRISE } from 'xoa-plans'
import { SelectSr } from 'select-objects'

import { FormGroup } from '../../utils'

const ScheduleHealthCheck = ({ schedule, toggleHealthCheck, setHealthCheckTags, setHealthCheckSr }) => (
<FormGroup>
<label>
<strong>
<a
className='text-info'
rel='noreferrer'
href='https://xen-orchestra.com/docs/backups.html#backup-health-check'
target='_blank'
>
<Icon icon='info' />
</a>{' '}
{_('healthCheck')}
</strong>{' '}
{conditionalTooltip(
<input
type='checkbox'
checked={schedule.healthCheckVmsWithTags !== undefined}
disabled={getXoaPlan().value < ENTERPRISE.value}
onChange={toggleHealthCheck}
name='healthCheck'
/>,
getXoaPlan().value < ENTERPRISE.value ? _('healthCheckAvailableEnterpriseUser') : 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={setHealthCheckTags} />
</p>
<strong>{_('healthCheckChooseSr')}</strong>
<SelectSr
onChange={setHealthCheckSr}
placeholder={_('healthCheckChooseSr')}
required
value={schedule.healthCheckSr}
/>
</div>
)}
</FormGroup>
)

export default ScheduleHealthCheck
7 changes: 4 additions & 3 deletions packages/xo-web/src/xo-app/backup/new/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import getSettingsWithNonDefaultValue from '../_getSettingsWithNonDefaultValue'
import { canDeltaBackup, constructPattern, destructPattern, FormFeedback, FormGroup, Input, Li, Ul } from './../utils'

export NewMetadataBackup from './metadata'
export NewMirrorBackup from './mirror'

// ===================================================================

Expand All @@ -60,7 +61,7 @@ const DEFAULT_SCHEDULE = {
}
const RETENTION_LIMIT = 50

const ReportRecipients = decorate([
export const ReportRecipients = decorate([
provideState({
initialState: () => ({
recipient: '',
Expand Down Expand Up @@ -127,7 +128,7 @@ const ReportRecipients = decorate([

const SR_BACKEND_FAILURE_LINK = 'https://xen-orchestra.com/docs/backup_troubleshooting.html#sr-backend-failure-44'

const BACKUP_NG_DOC_LINK = 'https://xen-orchestra.com/docs/backup.html'
export const BACKUP_NG_DOC_LINK = 'https://xen-orchestra.com/docs/backup.html'

const ThinProvisionedTip = ({ label }) => (
<Tooltip content={_(label)}>
Expand Down Expand Up @@ -198,7 +199,7 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection, suggestedExc
}
}

const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
export const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
<ActionButton
handler={handler}
handlerParam={handlerParam}
Expand Down
Loading

0 comments on commit ee0adae

Please sign in to comment.