feat(xo-web/settings/config): cloud backup (#6917)

This commit is contained in:
Mathieu
2023-06-30 19:09:56 +02:00
committed by GitHub
parent c68630e2d6
commit 01302d7a60
18 changed files with 256 additions and 24 deletions

View File

@@ -10,6 +10,7 @@
- [Import/Disk] Enhance clarity for importing ISO files [Forum#7243](https://xcp-ng.org/forum/topic/7243/can-t-import-iso-through-ova-not-a-supported-filetype?_=1685710667937) (PR [#6874](https://github.com/vatesfr/xen-orchestra/pull/6874))
- [Import/Disk] Ability to import ISO from a URL (PR [#6924](https://github.com/vatesfr/xen-orchestra/pull/6924))
- [Import/export VDI] Ability to export/import disks in RAW format (PR [#6925](https://github.com/vatesfr/xen-orchestra/pull/6925))
- [XO config] Add the possibility to backup/import/download XO config from/to the XO cloud (PR [#6917](https://github.com/vatesfr/xen-orchestra/pull/6917))
### Bug fixes

View File

@@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: undefined,
// Original text: 'Config'
settingsConfigPage: undefined,
// Original text: "About"
aboutPage: 'Acerca de',

View File

@@ -140,9 +140,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: 'IPs',
// Original text: "Config"
settingsConfigPage: 'Configuration',
// Original text: "About"
aboutPage: 'À propos',

View File

@@ -122,9 +122,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: 'IP Címek',
// Original text: "Config"
settingsConfigPage: 'Beállítás',
// Original text: "About"
aboutPage: 'Információ',

View File

@@ -365,9 +365,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IPs',
// Original text: 'Config'
settingsConfigPage: 'Configurazione',
// Original text: 'About'
aboutPage: 'Informazioni',

View File

@@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IP адреса',
// Original text: 'Config'
settingsConfigPage: 'Когфигурация',
// Original text: "About"
aboutPage: 'О программе',

View File

@@ -179,9 +179,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: "IP'ler",
// Original text: "Config"
settingsConfigPage: 'Yapılandırma',
// Original text: "About"
aboutPage: 'Hakkında',

View File

@@ -9,6 +9,9 @@ const messages = {
creation: 'Creation',
description: 'Description',
deleteSourceVm: 'Delete source VM',
disable: 'Disable',
download: 'Download',
enable: 'Enable',
expiration: 'Expiration',
hostIp: 'Host IP',
keyValue: '{key}: {value}',
@@ -192,7 +195,6 @@ const messages = {
settingsLogsPage: 'Logs',
settingsCloudConfigsPage: 'Cloud configs',
settingsIpsPage: 'IPs',
settingsConfigPage: 'Config',
aboutPage: 'About',
aboutXoaPlan: 'About XO {xoaPlan}',
newMenu: 'New',
@@ -2331,6 +2333,16 @@ const messages = {
migrateVdiMessage:
'All the VDIs attached to a VM must either be on a shared SR or on the same host (local SR) for the VM to be able to start.',
// ----- XO cloud config -----
backedUpXoConfigs: 'Backed up XO Configs',
manageXoConfigCloudBackup: 'Manage XO Config Cloud Backup',
selectXoConfig: 'Select XO config',
xoConfigCloudBackup: 'XO Config Cloud Backup',
xoConfigCloudBackupTips:
'Your encrypted configuration is securely stored inside your Vates account and backed up once a day',
xoCloudConfigEnterPassphrase: 'If you want to encrypt backups, please enter a passphrase:',
xoCloudConfigRestoreEnterPassphrase: 'If the config is encrypted, please enter the passphrase:',
// ----- XOSAN -----
xosanTitle: 'XOSAN',
xosanSuggestions: 'Suggestions',

View File

@@ -10,7 +10,7 @@ import decorate from './apply-decorators'
import Icon from './icon'
import Link from './link'
import Tooltip from './tooltip'
import { addSubscriptions, connectStore, formatSize } from './utils'
import { addSubscriptions, connectStore, formatSize, ShortDate } from './utils'
import { createGetObject, createSelector } from './selectors'
import { FormattedDate } from 'react-intl'
import { isSrWritable, subscribeBackupNgJobs, subscribeProxies, subscribeRemotes, subscribeUsers } from './xo'
@@ -529,7 +529,12 @@ const xoItemToRender = {
}
return <span>{label}</span>
},
xoConfig: ({ createdAt }) => (
<span>
<Icon icon='xo-cloud-config' /> <ShortDate timestamp={createdAt} />
</span>
)
,
// XO objects.
pool: props => <Pool {...props} />,

View File

@@ -42,6 +42,7 @@ import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
import {
isSrWritable,
subscribeCloudConfigs,
subscribeCloudXoConfigBackups,
subscribeCurrentUser,
subscribeGroups,
subscribeIpPools,
@@ -1077,3 +1078,20 @@ export const SelectNetworkConfig = makeSubscriptionSelect(
}),
{ placeholder: _('selectNetworkConfigs') }
)
// ===================================================================
export const SelectXoCloudConfig = makeSubscriptionSelect(
subscriber =>
subscribeCloudXoConfigBackups(configs => {
const xoObjects = groupBy(
map(configs, config => ({ ...config, type: 'xoConfig' })),
'xoaId'
)
subscriber({
xoObjects,
xoContainers: map(xoObjects, (configs, id) => ({ ...configs, id, type: 'VM' })),
})
}),
{ placeholder: _('selectXoConfig') }
)

View File

@@ -591,6 +591,15 @@ export const subscribeXoTasks = createSubscription(async previousTasks => {
return Array.from(tasks.values()).sort(({ start: start1 }, { start: start2 }) => start1 - start2)
})
export const subscribeCloudXoConfigBackups = createSubscription(
() => fetch('./rest/v0/cloud/xo-config/backups?fields=xoaId,createdAt,id,content_href').then(resp => resp.json()),
{ polling: 6e4 }
)
export const subscribeCloudXoConfig = createSubscription(() =>
fetch('./rest/v0/cloud/xo-config').then(resp => resp.json())
)
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
@@ -3411,6 +3420,8 @@ export const subscribeTunnelState = createSubscription(() => _call('xoa.supportT
export const getApplianceInfo = () => _call('xoa.getApplianceInfo')
export const getApiApplianceInfo = () => fetch('./rest/v0/appliance').then(resp => resp.json())
// Proxy --------------------------------------------------------------------
export const getAllProxies = () => _call('proxy.getAll')

View File

@@ -1231,6 +1231,10 @@
@extend .fa;
@extend .fa-arrows-h;
}
&-xo-cloud-config {
@extend .fa;
@extend .fa-cloud-upload;
}
// XOSAN related

View File

@@ -415,7 +415,7 @@ export default class Menu extends Component {
{
to: '/settings/config',
icon: 'menu-settings-config',
label: 'settingsConfigPage',
label: 'xoConfig',
},
],
},

View File

@@ -6,8 +6,11 @@ import Dropzone from 'dropzone'
import Icon from 'icon'
import React from 'react'
import { formatSize } from 'utils'
import { getXoaPlan, SOURCES } from 'xoa-plans'
import { importConfig, exportConfig } from 'xo'
import CloudConfig from './xo-cloud-config'
// ===================================================================
export default class Config extends Component {
@@ -60,7 +63,8 @@ export default class Config extends Component {
return (
<div>
<div className='mb-1'>
{getXoaPlan() !== SOURCES && <CloudConfig />}
<div className='mb-1 mt-1'>
<h2>
<Icon icon='import' /> {_('importConfig')}
</h2>

View File

@@ -0,0 +1,23 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import React from 'react'
import { Password } from 'form'
class BackupXoConfigModal extends BaseComponent {
get value() {
return {
passphrase: this.state.passphrase,
}
}
render() {
return (
<div>
<label>{_('xoCloudConfigEnterPassphrase')}</label>
<Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />
</div>
)
}
}
export default BackupXoConfigModal

View File

@@ -0,0 +1,149 @@
import _ from 'intl'
import ActionButton from 'action-button'
import addSubscriptions from 'add-subscriptions'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import { confirm } from 'modal'
import { getApiApplianceInfo, subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
import { groupBy, sortBy } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { SelectXoCloudConfig } from 'select-objects'
import BackupXoConfigModal from './backup-xo-config-modal'
import RestoreXoConfigModal from './restore-xo-config-modal'
const CloudConfig = decorate([
addSubscriptions({
cloudXoConfig: subscribeCloudXoConfig,
cloudXoConfigBackups: subscribeCloudXoConfigBackups,
}),
provideState({
initialState: () => ({ config: undefined }),
effects: {
downloadCloudXoConfig:
() =>
({ config, isConfigDefined }) => {
if (isConfigDefined) {
window.open(config.content_href, '_blank')
}
},
restoreCloudXoConfig:
() =>
async ({ config, isConfigDefined }) => {
if (isConfigDefined) {
const { passphrase } = await confirm({
icon: 'backup',
title: _('xoConfigCloudBackup'),
body: <RestoreXoConfigModal />,
})
const resp = await fetch(`./rest/v0/cloud/xo-config/backups/${config.id}/actions/import?sync`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
passphrase,
}),
})
if (!resp.ok) {
throw new Error(resp.statusText)
}
return {
config: undefined,
}
}
},
onChangeCloudXoConfig: (_, config) => ({
config,
}),
toggleEnableCloudXoConfig:
() =>
async (state, { cloudXoConfig }) => {
let passphrase
if (!cloudXoConfig?.enabled) {
const params = await confirm({
icon: 'backup',
title: _('xoConfigCloudBackup'),
body: <BackupXoConfigModal />,
})
passphrase = params.passphrase
}
const resp = await fetch('./rest/v0/cloud/xo-config', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: !cloudXoConfig?.enabled,
passphrase,
}),
})
if (!resp.ok) {
throw new Error(resp.statusText)
}
subscribeCloudXoConfig.forceRefresh()
},
},
computed: {
applianceId: async () => {
const { id } = await getApiApplianceInfo()
return id
},
groupedConfigs: ({ applianceId, sortedConfigs }) =>
sortBy(groupBy(sortedConfigs, 'xoaId'), config => (config[0].xoaId === applianceId ? -1 : 1)),
isConfigDefined: ({ config }) => config != null,
sortedConfigs: (_, { cloudXoConfigBackups }) =>
cloudXoConfigBackups?.sort((config, nextConfig) => config.createdAt - nextConfig.createdAt),
},
}),
injectState,
({ effects, state, cloudXoConfig }) => (
<div>
<div className='mb-1'>
<h2>
<Icon icon='backup' /> {_('manageXoConfigCloudBackup')}
</h2>
<em>
<Icon icon='info' /> {_('xoConfigCloudBackupTips')}
</em>
<br />
<ActionButton
btnStyle={cloudXoConfig?.enabled ? 'warning' : 'primary'}
handler={effects.toggleEnableCloudXoConfig}
icon='backup'
>
{cloudXoConfig?.enabled ? _('disable') : _('enable')}
</ActionButton>
</div>
<div>
<h2>
<Icon icon='xo-cloud-config' /> {_('backedUpXoConfigs')}
</h2>
<SelectXoCloudConfig onChange={effects.onChangeCloudXoConfig} value={state.config} />
<div className='mt-1'>
<ActionButton
handler={effects.restoreCloudXoConfig}
btnStyle='warning'
icon='upload'
disabled={!state.isConfigDefined}
>
{_('restore')}
</ActionButton>{' '}
<ActionButton
btnStyle='primary'
icon='download'
handler={effects.downloadCloudXoConfig}
disabled={!state.isConfigDefined}
>
{_('download')}
</ActionButton>
</div>
</div>
</div>
),
])
export default CloudConfig

View File

@@ -0,0 +1,23 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import React from 'react'
import { Password } from 'form'
class RestoreXoConfigModal extends BaseComponent {
get value() {
return {
passphrase: this.state.passphrase,
}
}
render() {
return (
<div>
<label>{_('xoCloudConfigRestoreEnterPassphrase')}</label>
<Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />
</div>
)
}
}
export default RestoreXoConfigModal

View File

@@ -59,7 +59,7 @@ const HEADER = (
<Icon icon='template' /> {_('settingsCloudConfigsPage')}
</NavLink>
<NavLink to='/settings/config'>
<Icon icon='menu-settings-config' /> {_('settingsConfigPage')}
<Icon icon='menu-settings-config' /> {_('xoConfig')}
</NavLink>
</NavTabs>
</Col>