feat(xo-web/settings/config): cloud backup (#6917)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -137,9 +137,6 @@ export default {
|
||||
// Original text: 'IPs'
|
||||
settingsIpsPage: undefined,
|
||||
|
||||
// Original text: 'Config'
|
||||
settingsConfigPage: undefined,
|
||||
|
||||
// Original text: "About"
|
||||
aboutPage: 'Acerca de',
|
||||
|
||||
|
||||
@@ -140,9 +140,6 @@ export default {
|
||||
// Original text: "IPs"
|
||||
settingsIpsPage: 'IPs',
|
||||
|
||||
// Original text: "Config"
|
||||
settingsConfigPage: 'Configuration',
|
||||
|
||||
// Original text: "About"
|
||||
aboutPage: 'À propos',
|
||||
|
||||
|
||||
@@ -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ó',
|
||||
|
||||
|
||||
@@ -365,9 +365,6 @@ export default {
|
||||
// Original text: 'IPs'
|
||||
settingsIpsPage: 'IPs',
|
||||
|
||||
// Original text: 'Config'
|
||||
settingsConfigPage: 'Configurazione',
|
||||
|
||||
// Original text: 'About'
|
||||
aboutPage: 'Informazioni',
|
||||
|
||||
|
||||
@@ -137,9 +137,6 @@ export default {
|
||||
// Original text: 'IPs'
|
||||
settingsIpsPage: 'IP адреса',
|
||||
|
||||
// Original text: 'Config'
|
||||
settingsConfigPage: 'Когфигурация',
|
||||
|
||||
// Original text: "About"
|
||||
aboutPage: 'О программе',
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
|
||||
@@ -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') }
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1231,6 +1231,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-arrows-h;
|
||||
}
|
||||
&-xo-cloud-config {
|
||||
@extend .fa;
|
||||
@extend .fa-cloud-upload;
|
||||
}
|
||||
|
||||
// XOSAN related
|
||||
|
||||
|
||||
@@ -415,7 +415,7 @@ export default class Menu extends Component {
|
||||
{
|
||||
to: '/settings/config',
|
||||
icon: 'menu-settings-config',
|
||||
label: 'settingsConfigPage',
|
||||
label: 'xoConfig',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user