Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-web/backup): UI mirror backup implementation #6858

Merged
merged 9 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”

- [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] 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))
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved

### 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,
Rajaa-BARHTAOUI marked this conversation as resolved.
Show resolved Hide resolved
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