diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffb0064be..70251f030 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Export VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3194](https://github.com/vatesfr/xen-orchestra/pull/3194))
- Search syntax support wildcard (`*`) and regular expressions [#3190](https://github.com/vatesfr/xen-orchestra/issues/3190) (PRs [#3198](https://github.com/vatesfr/xen-orchestra/pull/3198) & [#3199](https://github.com/vatesfr/xen-orchestra/pull/3199))
+- Import VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3216](https://github.com/vatesfr/xen-orchestra/pull/3216))
### Bug fixes
diff --git a/packages/xo-web/src/common/dropzone/index.js b/packages/xo-web/src/common/dropzone/index.js
index 0e18e7529..b2f8697dd 100644
--- a/packages/xo-web/src/common/dropzone/index.js
+++ b/packages/xo-web/src/common/dropzone/index.js
@@ -8,16 +8,18 @@ import styles from './index.css'
@propTypes({
onDrop: propTypes.func,
message: propTypes.node,
+ multiple: propTypes.bool,
})
export default class Dropzone extends Component {
render () {
- const { onDrop, message } = this.props
+ const { onDrop, message, multiple } = this.props
return (
{message}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 4ae3932ac..30a99f921 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -918,6 +918,9 @@ const messages = {
deleteSelectedVdis: 'Delete selected VDIs',
deleteSelectedVdi: 'Delete selected VDI',
exportVdi: 'Export VDI content',
+ importVdi: 'Import VDI content',
+ importVdiNoFile: 'No file selected',
+ selectVdiMessage: 'Drop VHD file here',
useQuotaWarning:
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
notEnoughSpaceInResourceSet:
@@ -1242,8 +1245,11 @@ const messages = {
importVmsCleanList: 'Reset',
vmImportSuccess: 'VM import success',
vmImportFailed: 'VM import failed',
+ vdiImportSuccess: 'VDI import success',
+ vdiImportFailed: 'VDI import failed',
setVmFailed: 'Error on setting the VM: {vm}',
startVmImport: 'Import starting…',
+ startVdiImport: 'VDI import starting…',
startVmExport: 'Export starting…',
startVdiExport: 'VDI export starting…',
nCpus: 'N CPUs',
diff --git a/packages/xo-web/src/common/xo/import-vdi-modal/index.js b/packages/xo-web/src/common/xo/import-vdi-modal/index.js
new file mode 100644
index 000000000..32cc0f5b0
--- /dev/null
+++ b/packages/xo-web/src/common/xo/import-vdi-modal/index.js
@@ -0,0 +1,21 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Dropzone from 'dropzone'
+import React from 'react'
+
+export default class ImportVdiModalBody extends Component {
+ get value () {
+ return this.state.file
+ }
+
+ render () {
+ const { file } = this.state
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index 86eba2d14..79606a324 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -1256,6 +1256,38 @@ export const importVm = (file, type = 'xva', data = undefined, sr) => {
)
}
+import ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first
+export const importVdi = async vdi => {
+ const file = await confirm({
+ body: ,
+ icon: 'import',
+ title: _('importVdi'),
+ })
+
+ if (file === undefined) {
+ error(_('importVdi'), _('importVdiNoFile'))
+ return
+ }
+
+ const { name } = file
+ info(_('startVdiImport'), name)
+
+ return _call('disk.importContent', { id: resolveId(vdi) }).then(
+ ({ $sendTo }) =>
+ post($sendTo, file)
+ .then(res => {
+ if (res.status !== 200) {
+ throw res.status
+ }
+ success(_('vdiImportSuccess'), name)
+ return res.json().then(body => body.result)
+ })
+ .catch(err => {
+ error(_('vdiImportFailed'), err)
+ })
+ )
+}
+
export const importVms = (vms, sr) =>
Promise.all(
map(vms, ({ file, type, data }) =>
diff --git a/packages/xo-web/src/xo-app/sr/tab-disks.js b/packages/xo-web/src/xo-app/sr/tab-disks.js
index 029e64ac5..3634920db 100644
--- a/packages/xo-web/src/xo-app/sr/tab-disks.js
+++ b/packages/xo-web/src/xo-app/sr/tab-disks.js
@@ -16,7 +16,7 @@ import { Text } from 'editable'
import { SizeInput, Toggle } from 'form'
import { Container, Row, Col } from 'grid'
import { connectStore, formatSize, noop } from 'utils'
-import { concat, isEmpty, map, some } from 'lodash'
+import { concat, groupBy, isEmpty, map, mapValues, pick, some } from 'lodash'
import {
createGetObjectsOfType,
createSelector,
@@ -31,6 +31,7 @@ import {
disconnectVbd,
editVdi,
exportVdi,
+ importVdi,
isVmRunning,
} from 'xo'
@@ -191,6 +192,12 @@ const INDIVIDUAL_ACTIONS = [
icon: 'export',
label: _('exportVdi'),
},
+ {
+ disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id],
+ handler: importVdi,
+ icon: 'import',
+ label: _('importVdi'),
+ },
{
handler: vdi => copy(vdi.uuid),
icon: 'clipboard',
@@ -276,6 +283,7 @@ class NewDisk extends Component {
@connectStore(() => ({
checkPermissions: getCheckPermissions,
+ vbds: createGetObjectsOfType('VBD'),
}))
export default class SrDisks extends Component {
_closeNewDiskForm = () => this.setState({ newDisk: false })
@@ -293,6 +301,15 @@ export default class SrDisks extends Component {
(check, id) => check(id, 'administrate')
)
+ _getIsVdiAttached = createSelector(
+ createSelector(
+ () => this.props.vbds,
+ () => map(this.props.vdis, 'id'),
+ (vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis)
+ ),
+ vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached'))
+ )
+
render () {
const vdis = this._getAllVdis()
const { newDisk } = this.state
@@ -325,6 +342,7 @@ export default class SrDisks extends Component {
({
checkPermissions: getCheckPermissions,
isAdmin,
+ allVbds: createGetObjectsOfType('VBD'),
}))
export default class TabDisks extends Component {
constructor (props) {
@@ -652,12 +665,27 @@ export default class TabDisks extends Component {
(vdis, vbds, vm) => mapValues(vdis, vdi => find(vbds, { VDI: vdi.id }))
)
+ _getIsVdiAttached = createSelector(
+ createSelector(
+ () => this.props.allVbds,
+ () => Object.keys(this.props.vdis),
+ (vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis)
+ ),
+ vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached'))
+ )
+
individualActions = [
{
handler: exportVdi,
icon: 'export',
label: _('exportVdi'),
},
+ {
+ disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id],
+ handler: importVdi,
+ icon: 'import',
+ label: _('importVdi'),
+ },
{
handler: this._migrateVdi,
icon: 'vdi-migrate',
@@ -735,6 +763,7 @@ export default class TabDisks extends Component {
actions={ACTIONS}
collection={vdis}
columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS}
+ data-isVdiAttached={this._getIsVdiAttached()}
data-srs={srs}
data-vbdsByVdi={this._getVbdsByVdi()}
data-vm={vm}