diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 3025e9aa09a..33b43e79a52 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -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
diff --git a/packages/xo-web/src/common/intl/locales/es.js b/packages/xo-web/src/common/intl/locales/es.js
index b46fd1295d6..01327133683 100644
--- a/packages/xo-web/src/common/intl/locales/es.js
+++ b/packages/xo-web/src/common/intl/locales/es.js
@@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: undefined,
- // Original text: 'Config'
- settingsConfigPage: undefined,
-
// Original text: "About"
aboutPage: 'Acerca de',
diff --git a/packages/xo-web/src/common/intl/locales/fr.js b/packages/xo-web/src/common/intl/locales/fr.js
index 854b2ea1036..0e66b0a93c9 100644
--- a/packages/xo-web/src/common/intl/locales/fr.js
+++ b/packages/xo-web/src/common/intl/locales/fr.js
@@ -140,9 +140,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: 'IPs',
- // Original text: "Config"
- settingsConfigPage: 'Configuration',
-
// Original text: "About"
aboutPage: 'À propos',
diff --git a/packages/xo-web/src/common/intl/locales/hu.js b/packages/xo-web/src/common/intl/locales/hu.js
index 066a60cf808..7ccf1fdb157 100644
--- a/packages/xo-web/src/common/intl/locales/hu.js
+++ b/packages/xo-web/src/common/intl/locales/hu.js
@@ -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ó',
diff --git a/packages/xo-web/src/common/intl/locales/it.js b/packages/xo-web/src/common/intl/locales/it.js
index 43146a81454..546e915778b 100644
--- a/packages/xo-web/src/common/intl/locales/it.js
+++ b/packages/xo-web/src/common/intl/locales/it.js
@@ -365,9 +365,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IPs',
- // Original text: 'Config'
- settingsConfigPage: 'Configurazione',
-
// Original text: 'About'
aboutPage: 'Informazioni',
diff --git a/packages/xo-web/src/common/intl/locales/ru.js b/packages/xo-web/src/common/intl/locales/ru.js
index 15c4f395f24..be843663e05 100644
--- a/packages/xo-web/src/common/intl/locales/ru.js
+++ b/packages/xo-web/src/common/intl/locales/ru.js
@@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IP адреса',
- // Original text: 'Config'
- settingsConfigPage: 'Когфигурация',
-
// Original text: "About"
aboutPage: 'О программе',
diff --git a/packages/xo-web/src/common/intl/locales/tr.js b/packages/xo-web/src/common/intl/locales/tr.js
index fc9ef715a6e..b39257b925f 100644
--- a/packages/xo-web/src/common/intl/locales/tr.js
+++ b/packages/xo-web/src/common/intl/locales/tr.js
@@ -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',
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 4e982652d3f..6e7cc62a76e 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -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,15 @@ 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:',
+
// ----- XOSAN -----
xosanTitle: 'XOSAN',
xosanSuggestions: 'Suggestions',
diff --git a/packages/xo-web/src/common/render-xo-item.js b/packages/xo-web/src/common/render-xo-item.js
index f8efc44a523..0fbbc069d8f 100644
--- a/packages/xo-web/src/common/render-xo-item.js
+++ b/packages/xo-web/src/common/render-xo-item.js
@@ -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 {label}
},
-
+ xoConfig: ({ createdAt }) => (
+
+
+
+ )
+ ,
// XO objects.
pool: props => ,
diff --git a/packages/xo-web/src/common/select-objects.js b/packages/xo-web/src/common/select-objects.js
index 65938cdb49d..42b20e1d1d0 100644
--- a/packages/xo-web/src/common/select-objects.js
+++ b/packages/xo-web/src/common/select-objects.js
@@ -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') }
+)
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index 92ad20e18d0..2a3e0e6b1b1 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -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')
diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss
index 204aa730804..17a5b8a297a 100644
--- a/packages/xo-web/src/icons.scss
+++ b/packages/xo-web/src/icons.scss
@@ -1231,6 +1231,10 @@
@extend .fa;
@extend .fa-arrows-h;
}
+ &-xo-cloud-config {
+ @extend .fa;
+ @extend .fa-cloud-upload;
+ }
// XOSAN related
diff --git a/packages/xo-web/src/xo-app/menu/index.js b/packages/xo-web/src/xo-app/menu/index.js
index 52ead7aa58c..7c17728e310 100644
--- a/packages/xo-web/src/xo-app/menu/index.js
+++ b/packages/xo-web/src/xo-app/menu/index.js
@@ -415,7 +415,7 @@ export default class Menu extends Component {
{
to: '/settings/config',
icon: 'menu-settings-config',
- label: 'settingsConfigPage',
+ label: 'xoConfig',
},
],
},
diff --git a/packages/xo-web/src/xo-app/settings/config/index.js b/packages/xo-web/src/xo-app/settings/config/index.js
index aaf2fc1cfc1..8a769a79d70 100644
--- a/packages/xo-web/src/xo-app/settings/config/index.js
+++ b/packages/xo-web/src/xo-app/settings/config/index.js
@@ -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 (
-
+ {getXoaPlan() !== SOURCES &&
}
+
{_('importConfig')}
diff --git a/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js
new file mode 100644
index 00000000000..bbd72de890a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js
@@ -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 (
+
+
+
+
+ )
+ }
+}
+
+export default BackupXoConfigModal
diff --git a/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js
new file mode 100644
index 00000000000..c6afc453e5b
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js
@@ -0,0 +1,136 @@
+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'
+
+const CloudConfig = decorate([
+ addSubscriptions({
+ cloudXoConfig: subscribeCloudXoConfig,
+ cloudXoConfigBackups: subscribeCloudXoConfigBackups,
+ }),
+ provideState({
+ initialState: () => ({ config: undefined }),
+ effects: {
+ downloadCloudXoConfig:
+ () =>
+ ({ config, isConfigDefined }) => {
+ if (isConfigDefined) {
+ window.open(config.content_href, '_blank')
+ }
+ },
+ uploadCloudXoConfig:
+ () =>
+ async ({ config, isConfigDefined }) => {
+ if (isConfigDefined) {
+ const resp = await fetch(`./rest/v0/cloud/xo-config/backups/${config.id}/actions/import?sync`, {
+ method: 'POST',
+ })
+ 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:
,
+ })
+ 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 }) => (
+
+
+
+ {_('manageXoConfigCloudBackup')}
+
+
+ {_('xoConfigCloudBackupTips')}
+
+
+
+ {cloudXoConfig?.enabled ? _('disable') : _('enable')}
+
+
+
+
+ {_('backedUpXoConfigs')}
+
+
+
+
+ {_('restore')}
+ {' '}
+
+ {_('download')}
+
+
+
+
+ ),
+])
+
+export default CloudConfig
diff --git a/packages/xo-web/src/xo-app/settings/index.js b/packages/xo-web/src/xo-app/settings/index.js
index bd9ce422351..6936473dd68 100644
--- a/packages/xo-web/src/xo-app/settings/index.js
+++ b/packages/xo-web/src/xo-app/settings/index.js
@@ -59,7 +59,7 @@ const HEADER = (
{_('settingsCloudConfigsPage')}
- {_('settingsConfigPage')}
+ {_('xoConfig')}