diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js
index b6e0d944..36b85f62 100644
--- a/pkg/harvester/config/feature-flags.js
+++ b/pkg/harvester/config/feature-flags.js
@@ -65,6 +65,7 @@ const FEATURE_FLAGS = {
'vGPUAsPCIDevice',
'instanceManagerResourcesSetting',
'rwxNetworkSetting',
+ 'createPVCWithDataVolume'
],
};
diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js
index 98c28c3c..9f2f039a 100644
--- a/pkg/harvester/config/labels-annotations.js
+++ b/pkg/harvester/config/labels-annotations.js
@@ -80,4 +80,5 @@ export const HCI = {
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
MAC_ADDRESS: 'harvesterhci.io/mac-address',
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
+ CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
};
diff --git a/pkg/harvester/config/types.js b/pkg/harvester/config/types.js
index 472ec034..71fef7a9 100644
--- a/pkg/harvester/config/types.js
+++ b/pkg/harvester/config/types.js
@@ -41,3 +41,8 @@ export const VMIMPORT_SOURCE_KINDS = {
OPENSTACK: 'OpenstackSource',
OVA: 'OvaSource',
};
+
+export const CDI_POPULATOR_KIND = {
+ VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
+ VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
+};
diff --git a/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue b/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
new file mode 100644
index 00000000..c0c14f9c
--- /dev/null
+++ b/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+ {{ t('harvester.modal.dataMigration.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.volume.vue b/pkg/harvester/edit/harvesterhci.io.volume.vue
index 5bdb5e47..285cdfab 100644
--- a/pkg/harvester/edit/harvesterhci.io.volume.vue
+++ b/pkg/harvester/edit/harvesterhci.io.volume.vue
@@ -9,8 +9,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Conditions from '@shell/components/form/Conditions';
import { Banner } from '@components/Banner';
+import { Checkbox } from '@components/Form/Checkbox';
+import jsyaml from 'js-yaml';
+import { exceptionToErrorsArray } from '@shell/utils/error';
import { allHash } from '@shell/utils/promise';
-import { get } from '@shell/utils/object';
+import { clone, get } from '@shell/utils/object';
import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { saferDump } from '@shell/utils/create-yaml';
@@ -33,6 +36,7 @@ export default {
components: {
Banner,
+ Checkbox,
Tab,
UnitInput,
CruResource,
@@ -90,14 +94,34 @@ export default {
source,
storage,
imageId,
- snapshots: [],
- images: [],
+ showAdvanced: false,
+ createWithDataVolume: false,
+ snapshots: [],
+ images: [],
GIBIBYTE
};
},
created() {
this.registerBeforeHook(this.willSave, 'willSave');
+
+ if (this.mode === _CREATE) {
+ const originalSaveYaml = this.value.saveYaml?.bind(this.value);
+
+ this.value.saveYaml = async(yaml) => {
+ if (this.createWithDataVolume && this.isBlank) {
+ const parsed = jsyaml.load(yaml);
+ const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' };
+ const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
+
+ await dataVolume.save();
+
+ return dataVolume;
+ }
+
+ return originalSaveYaml(yaml);
+ };
+ }
},
computed: {
@@ -135,6 +159,10 @@ export default {
return Object.values(VOLUME_MODE);
},
+ accessModeOptions() {
+ return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
+ },
+
imageOption() {
return sortBy(
this.images
@@ -275,6 +303,10 @@ export default {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
+ isCreatePVCWithDataVolumeFeatureEnabled() {
+ return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
+ },
+
isResizeDisabled() {
return (
!this.isLHV2VolExpansionFeatureEnabled &&
@@ -341,6 +373,58 @@ export default {
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
},
+ buildDataVolumeObj() {
+ const storage = {
+ storageClassName: this.value.spec.storageClassName,
+ resources: { requests: { storage: this.storage } },
+ };
+
+ if (this.showAdvanced && this.value.spec.accessModes?.length > 0) {
+ storage.accessModes = this.value.spec.accessModes;
+ }
+
+ if (this.showAdvanced && this.value.spec.volumeMode) {
+ storage.volumeMode = this.value.spec.volumeMode;
+ }
+
+ return {
+ type: 'cdi.kubevirt.io.datavolume',
+ apiVersion: 'cdi.kubevirt.io/v1beta1',
+ kind: 'DataVolume',
+ metadata: {
+ name: this.value.metadata.name,
+ namespace: this.value.metadata.namespace,
+ annotations: this.value.metadata.annotations || {},
+ labels: this.value.metadata.labels || {},
+ },
+ spec: {
+ source: { blank: {} },
+ storage,
+ }
+ };
+ },
+
+ async save(buttonDone) {
+ if (this.isCreate && this.isBlank && this.createWithDataVolume) {
+ try {
+ this.update();
+ const dvObj = this.buildDataVolumeObj();
+ const dataVolume = await this.$store.dispatch('harvester/create', dvObj);
+
+ await dataVolume.save();
+ buttonDone(true);
+ this.done();
+ } catch (err) {
+ const error = err?.data || err;
+
+ this['errors'] = exceptionToErrorsArray(error);
+ buttonDone(false);
+ }
+ } else {
+ await CreateEditView.methods.save.call(this, buttonDone);
+ }
+ },
+
willSave() {
this.update();
},
@@ -383,9 +467,17 @@ export default {
this.update();
},
generateYaml() {
- const out = saferDump(this.value);
+ this.update();
- return out;
+ if (this.isCreate && this.isBlank && this.createWithDataVolume) {
+ return saferDump(this.buildDataVolumeObj());
+ }
+
+ const plain = clone(this.value);
+
+ delete plain.saveYaml;
+
+ return saferDump(plain);
},
}
};
@@ -458,18 +550,6 @@ export default {
@update:value="update"
/>
-
-
{{ t('harvester.volume.longhorn.disableResize') }}
+
+
+
+
+
+ {{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }}
+
+
+
+
+
!pvc?.isGoldenImageVolume);
+ return this.rows.filter((pvc) => {
+ if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) {
+ return false;
+ }
+
+ return true;
+ });
},
headers() {
return [
diff --git a/pkg/harvester/models/harvester/persistentvolumeclaim.js b/pkg/harvester/models/harvester/persistentvolumeclaim.js
index 44f751f4..2e0ee11b 100644
--- a/pkg/harvester/models/harvester/persistentvolumeclaim.js
+++ b/pkg/harvester/models/harvester/persistentvolumeclaim.js
@@ -9,6 +9,7 @@ import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { HCI, VOLUME_SNAPSHOT } from '../../types';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
+import { CDI_POPULATOR_KIND } from '../../config/types';
import { LVM_DRIVER } from './storage.k8s.io.storageclass';
const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed'];
@@ -44,7 +45,7 @@ export default class HciPv extends HarvesterResource {
const exportImageAction = {
action: 'exportImage',
enabled: this.hasAction('export') && !this.isEncrypted,
- icon: 'icon icon-copy',
+ icon: 'icon icon-external-link',
label: this.t('harvester.action.exportImage')
};
const takeSnapshotAction = {
@@ -77,10 +78,23 @@ export default class HciPv extends HarvesterResource {
icon: 'icon icon-backup',
label: this.t('harvester.action.cancelExpand')
},
+ {
+ action: 'dataMigration',
+ enabled: this.hasAction('dataMigration') && this.createPVCWithDataVolumeFeatureEnabled,
+ icon: 'icon icon-copy',
+ label: this.t('harvester.action.dataMigration')
+ },
...out
];
}
+ dataMigration(resources = this) {
+ this.$dispatch('promptModal', {
+ resources,
+ component: 'HarvesterDataMigrationDialog'
+ });
+ }
+
exportImage(resources = this) {
this.$dispatch('promptModal', {
resources,
@@ -339,10 +353,20 @@ export default class HciPv extends HarvesterResource {
return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true';
}
+ get isCDIPopulatorVolume() {
+ const kind = this?.metadata?.annotations?.[HCI_ANNOTATIONS.CDI_POPULATOR_KIND];
+
+ return kind === CDI_POPULATOR_KIND.VOLUME_IMPORT_SOURCE || kind === CDI_POPULATOR_KIND.VOLUME_CLONE_SOURCE;
+ }
+
get thirdPartyStorageFeatureEnabled() {
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
}
+ get createPVCWithDataVolumeFeatureEnabled() {
+ return this.$rootGetters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume');
+ }
+
get resourceExternalLink() {
const host = window.location.host;
const { params } = this.currentRoute();