From 118aaf16b7066b51d0acf2f2d10d190cb25514fb Mon Sep 17 00:00:00 2001 From: Francesco Torchia Date: Mon, 30 Sep 2024 14:57:53 +0200 Subject: [PATCH] Latest changes from Harvester master - a537c1ae38eb7030542ac371f24ae3336cd9d422 Signed-off-by: Francesco Torchia --- package.json | 1 + pkg/harvester/components/FilterVMSchedule.vue | 123 ++++++++ pkg/harvester/config/harvester-cluster.js | 15 + pkg/harvester/config/labels-annotations.js | 1 + pkg/harvester/config/table-headers.js | 37 +++ .../BackupList.vue | 129 ++++++++ .../SnapshotList.vue | 97 ++++++ .../index.vue | 125 ++++++++ .../index.vue | 19 +- pkg/harvester/dialog/EnableUSBPassthrough.vue | 37 ++- .../dialog/HarvesterMigrationDialog.vue | 1 - .../edit/harvesterhci.io.schedulevmbackup.vue | 298 ++++++++++++++++++ .../harvesterhci.io.virtualmachinebackup.vue | 6 +- .../edit/harvesterhci.io.vmsnapshot.vue | 5 +- .../VirtualMachineVolume/index.vue | 8 +- .../VirtualMachineVolume/type/container.vue | 28 +- .../VirtualMachineVolume/type/existing.vue | 27 +- .../VirtualMachineVolume/type/vmImage.vue | 25 +- .../VirtualMachineVolume/type/volume.vue | 26 +- .../edit/kubevirt.io.virtualmachine/index.vue | 1 - .../formatters/BackupCreatedFrom.vue | 41 +++ .../formatters/HarvesterCronExpression.vue | 32 ++ .../formatters/HarvesterStorageUsed.vue | 2 +- pkg/harvester/l10n/en-us.yaml | 55 +++- .../list/harvesterhci.io.dashboard.vue | 2 +- .../list/harvesterhci.io.schedulevmbackup.vue | 123 ++++++++ .../harvesterhci.io.virtualmachinebackup.vue | 76 +++-- .../list/harvesterhci.io.vmsnapshot.vue | 54 +++- pkg/harvester/mixins/harvester-vm/index.js | 22 +- .../harvesterhci.io.schedulevmbackup.js | 97 ++++++ .../harvesterhci.io.virtualmachinebackup.js | 26 +- .../models/kubevirt.io.virtualmachine.js | 18 +- pkg/harvester/types.ts | 1 + pkg/harvester/utils/cron.js | 11 + yarn.lock | 8 +- 35 files changed, 1484 insertions(+), 93 deletions(-) create mode 100644 pkg/harvester/components/FilterVMSchedule.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue create mode 100644 pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue create mode 100644 pkg/harvester/formatters/BackupCreatedFrom.vue create mode 100644 pkg/harvester/formatters/HarvesterCronExpression.vue create mode 100644 pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue create mode 100644 pkg/harvester/models/harvesterhci.io.schedulevmbackup.js create mode 100644 pkg/harvester/utils/cron.js diff --git a/package.json b/package.json index fb26714e..72f6cf37 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "resolutions": { "@types/node": "~20.10.0", + "cronstrue": "2.50.0", "d3-color": "3.1.0", "ejs": "3.1.9", "follow-redirects": "1.15.2", diff --git a/pkg/harvester/components/FilterVMSchedule.vue b/pkg/harvester/components/FilterVMSchedule.vue new file mode 100644 index 00000000..a68ba95f --- /dev/null +++ b/pkg/harvester/components/FilterVMSchedule.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 6bb0f43c..80c55174 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -398,6 +398,7 @@ export function init($plugin, store) { basicType( [ + HCI.SCHEDULE_VM_BACKUP, HCI.BACKUP, HCI.SNAPSHOT, HCI.VM_SNAPSHOT, @@ -445,6 +446,20 @@ export function init($plugin, store) { exact: false }); + configureType(HCI.SCHEDULE_VM_BACKUP, { showListMasthead: false, showConfigView: false }); + virtualType({ + labelKey: 'harvester.schedule.label', + name: HCI.SCHEDULE_VM_BACKUP, + namespaced: true, + weight: 201, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.SCHEDULE_VM_BACKUP } + }, + exact: false, + ifHaveType: HCI.SCHEDULE_VM_BACKUP, + }); + configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false }); virtualType({ labelKey: 'harvester.backup.label', diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js index ba1864ff..64a27c53 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -53,4 +53,5 @@ export const HCI = { NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status', CPU_MANAGER: 'cpumanager', VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails', + SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId', }; diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index b580f4f0..05f07425 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -40,3 +40,40 @@ export const SNAPSHOT_TARGET_VOLUME = { sort: 'spec.source.persistentVolumeClaimName', formatter: 'SnapshotTargetVolume', }; + +// The column of cron expression volume on VM schedules list page +export const VM_SCHEDULE_CRON = { + name: 'CronExpression', + labelKey: 'harvester.tableHeaders.cronExpression', + value: 'spec.cron', + align: 'center', + sort: 'spec.cron', + formatter: 'HarvesterCronExpression', +}; + +// The column of retain on VM schedules list page +export const VM_SCHEDULE_RETAIN = { + name: 'Retain', + labelKey: 'harvester.tableHeaders.retain', + value: 'spec.retain', + sort: 'spec.retain', + align: 'center', +}; + +// The column of maxFailure on VM schedules list page +export const VM_SCHEDULE_MAX_FAILURE = { + name: 'MaxFailure', + labelKey: 'harvester.tableHeaders.maxFailure', + value: 'spec.maxFailure', + sort: 'spec.maxFailure', + align: 'center', +}; + +// The column of type on VM schedules list page +export const VM_SCHEDULE_TYPE = { + name: 'Type', + labelKey: 'harvester.tableHeaders.scheduleType', + value: 'spec.vmbackup.type', + sort: 'spec.vmbackup.type', + align: 'center', +}; diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue new file mode 100644 index 00000000..61eee424 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue @@ -0,0 +1,129 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue new file mode 100644 index 00000000..60df034d --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue @@ -0,0 +1,97 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue new file mode 100644 index 00000000..274bcf5e --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue index c0e5211a..14c98c2b 100644 --- a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue +++ b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue @@ -127,29 +127,29 @@ export default {
-
+
-
+
-
+
-
+
- - - +
+ +
-
+
-
+
diff --git a/pkg/harvester/edit/harvesterhci.io.vmsnapshot.vue b/pkg/harvester/edit/harvesterhci.io.vmsnapshot.vue index e8675fd5..29d3024f 100644 --- a/pkg/harvester/edit/harvesterhci.io.vmsnapshot.vue +++ b/pkg/harvester/edit/harvesterhci.io.vmsnapshot.vue @@ -157,6 +157,9 @@ export default { }, methods: { + cancelAction() { + this.$router.go(-1); + }, async saveRestore(buttonCb) { this.update(); @@ -241,7 +244,7 @@ export default {
-
+
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue index 332a2b6f..d7d0cafa 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue @@ -225,13 +225,15 @@ export default { } }, - headerFor(type) { - return { + headerFor(type, hasVolBackups = false) { + const mainHeader = { [SOURCE_TYPE.NEW]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.volume'), [SOURCE_TYPE.IMAGE]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.vmImage'), [SOURCE_TYPE.ATTACH_VOLUME]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.existingVolume'), [SOURCE_TYPE.CONTAINER]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.container'), }[type]; + + return hasVolBackups ? `${ mainHeader } and Backups` : mainHeader; }, update() { @@ -322,7 +324,7 @@ export default { - {{ headerFor(volume.source) }} + {{ headerFor(volume.source, !!volume?.volumeBackups) }}
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue index 8372665a..13757823 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue @@ -3,6 +3,7 @@ import { LabeledInput } from '@components/Form/LabeledInput'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import InputOrDisplay from '@shell/components/InputOrDisplay'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; +import { Banner } from '@components/Banner'; export default { name: 'HarvesterEditContainer', @@ -10,7 +11,7 @@ export default { emits: ['update'], components: { - LabeledInput, LabeledSelect, InputOrDisplay + LabeledInput, LabeledSelect, InputOrDisplay, Banner }, props: { @@ -70,7 +71,6 @@ export default { />
-
-
+
+
+ + + +
+
+
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue index 59b9b1ed..74e5bc60 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue @@ -1,9 +1,10 @@ + + diff --git a/pkg/harvester/formatters/HarvesterCronExpression.vue b/pkg/harvester/formatters/HarvesterCronExpression.vue new file mode 100644 index 00000000..6755314f --- /dev/null +++ b/pkg/harvester/formatters/HarvesterCronExpression.vue @@ -0,0 +1,32 @@ + + + diff --git a/pkg/harvester/formatters/HarvesterStorageUsed.vue b/pkg/harvester/formatters/HarvesterStorageUsed.vue index 395fb26f..e37f27c9 100644 --- a/pkg/harvester/formatters/HarvesterStorageUsed.vue +++ b/pkg/harvester/formatters/HarvesterStorageUsed.vue @@ -147,7 +147,7 @@ export default { > diff --git a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue new file mode 100644 index 00000000..961a1bdc --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue @@ -0,0 +1,123 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue index 52220fbd..6a11611b 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue @@ -6,34 +6,37 @@ import Masthead from '@shell/components/ResourceList/Masthead'; import ResourceTable from '@shell/components/ResourceTable'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; +import FilterVMSchedule from '../components/FilterVMSchedule'; import { HCI } from '../types'; import { allSettled } from '../utils/promise'; import { BACKUP_TYPE } from '../config/types'; +import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue'; export default { name: 'HarvesterListBackup', components: { - ResourceTable, Banner, Loading, Masthead, MessageLink + ResourceTable, Banner, Loading, Masthead, MessageLink, FilterVMSchedule }, props: { schema: { type: Object, required: true, - } + }, }, async fetch() { const inStore = this.$store.getters['currentProduct'].inStore; const hash = await allSettled({ - vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), - settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), - rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), + settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), + backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + scheduleList: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }), }); - this.rows = hash.rows; + this.backups = hash.backups; + this.rows = hash.backups; this.settings = hash.settings; - if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) { const backupTargetResource = hash.settings.find( O => O.id === 'backup-target'); const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource); @@ -50,10 +53,12 @@ export default { const resource = params.resource; return { - rows: [], - settings: [], + rows: [], + backups: [], + settings: [], resource, - to: `${ HCI.SETTING }/backup-target?mode=edit`, + to: `${ HCI.SETTING }/backup-target?mode=edit`, + searchSchedule: '' }; }, @@ -85,7 +90,25 @@ export default { } return out; - } + }, + + getRow(row) { + return row.status && row.status.source; + }, + + changeRows(filteredRows, searchSchedule) { + this[searchSchedule] = searchSchedule; + this[backups] = filteredRows; + }, + + sortGenerationFn() { + let base = defaultTableSortGenerationFn(this.schema, this.$store); + + base += this.searchSchedule; + + return base; + }, + }, computed: { @@ -101,6 +124,12 @@ export default { align: 'left', formatter: 'AttachVMWithName' }, + { + name: 'backupCreatedFrom', + labelKey: 'harvester.tableHeaders.vmSchedule', + value: 'sourceSchedule', + formatter: 'BackupCreatedFrom', + }, { name: 'backupTarget', labelKey: 'tableHeaders.backupTarget', @@ -112,7 +141,7 @@ export default { name: 'readyToUse', labelKey: 'tableHeaders.readyToUse', value: 'status.readyToUse', - align: 'left', + align: 'center', formatter: 'Checked', }, ]; @@ -124,25 +153,24 @@ export default { value: 'backupProgress', align: 'left', formatter: 'HarvesterBackupProgressBar', - width: 200, }); } - cols.push(AGE); return cols; }, hasBackupProgresses() { - return !!this.rows.find(R => R.status?.progress !== undefined); + return !!this.backups.find(r => r.status?.progress !== undefined); }, - filteredRows() { - return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT); + return this.backups.filter(r => r.spec?.type !== BACKUP_TYPE.SNAPSHOT); + }, + getRawRows() { + return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.BACKUP); }, - backupTargetResource() { - return this.settings.find( O => O.id === 'backup-target'); + return this.settings.find(O => O.id === 'backup-target'); }, isEmptyValue() { @@ -211,16 +239,22 @@ export default { :headers="headers" :groupable="true" :rows="filteredRows" + :sort-generation-fn="sortGenerationFn" :schema="schema" key-field="_key" default-sort-by="age" - > + diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index 411cb4dc..00251ac1 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -301,6 +301,7 @@ export default { } = config; const vm = this.resourceType === HCI.VM ? value : this.resourceType === HCI.BACKUP ? this.value.status?.source : value.spec.vm; + const volumeBackups = this.resourceType === HCI.BACKUP ? this.value.status?.volumeBackups : null; const spec = vm?.spec; @@ -335,7 +336,8 @@ export default { const sshKey = this.getSSHFromAnnotation(spec) || []; const imageId = this.getRootImageId(vm) || ''; - const diskRows = this.getDiskRows(vm); + const diskRows = this.getDiskRows(vm, volumeBackups); + const networkRows = this.getNetworkRows(vm, { fromTemplate, init }); const hasCreateVolumes = this.getHasCreatedVolumes(spec) || []; @@ -404,7 +406,7 @@ export default { this.refreshYamlEditor(); }, - getDiskRows(vm) { + getDiskRows(vm, volBackups) { const namespace = vm.metadata.namespace; const _volumes = vm.spec.template.spec.volumes || []; const _disks = vm.spec.template.spec.domain.devices.disks || []; @@ -422,6 +424,7 @@ export default { const isIsoImage = /iso$/i.test(imageResource?.imageSuffix); const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize); const isEncrypted = imageResource?.isEncrypted || false; + const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === 'disk-0') || null ; if (isIsoImage) { bus = 'sata'; @@ -449,7 +452,8 @@ export default { storageClassName: '', image: this.imageId, volumeMode: 'Block', - isEncrypted + isEncrypted, + volumeBackups, }); } else { out = _disks.map( (DISK, index) => { @@ -531,6 +535,7 @@ export default { const volumeStatus = pvc?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR]; const isEncrypted = pvc?.isEncrypted || false; + const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === DISK.name) || null; return { id: randomStr(5), @@ -551,7 +556,8 @@ export default { volumeStatus, dataSource, namespace, - isEncrypted + isEncrypted, + volumeBackups, }; }); } @@ -1396,6 +1402,14 @@ export default { } }, + setCpuPinning(value) { + if (value) { + set(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement', true); + } else { + this.$delete(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement'); + } + }, + setTPM(tpmEnabled) { if (tpmEnabled) { set(this.spec.template.spec.domain.devices, 'tpm', {}); diff --git a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js new file mode 100644 index 00000000..13a2f566 --- /dev/null +++ b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js @@ -0,0 +1,97 @@ +import HarvesterResource from './harvester'; +import { get } from '@shell/utils/object'; +import { findBy } from '@shell/utils/array'; +import { colorForState, stateDisplay, STATES } from '@shell/plugins/dashboard-store/resource-class'; +import { _CREATE } from '@shell/config/query-params'; +import { ucFirst, escapeHtml } from '@shell/utils/string'; + +export default class ScheduleVmBackup extends HarvesterResource { + detailPageHeaderActionOverride(realMode) { + if (realMode === _CREATE) { + return this.t('harvester.schedule.createTitle'); + } + } + + get _availableActions() { + const toFilter = ['goToClone']; + + const out = super._availableActions.filter((action) => { + if (!toFilter.includes(action.action)) { + return action; + } + }); + + return [ + { + action: 'resumeSchedule', + enabled: ucFirst(this.state) === STATES.suspended.label, + icon: 'icons icon-play', + label: this.t('harvester.action.resumeSchedule'), + }, + { + action: 'suspendSchedule', + enabled: ucFirst(this.state) === STATES.active.label, + icon: 'icons icon-pause', + label: this.t('harvester.action.suspendSchedule'), + }, + ...out + ]; + } + + async suspendSchedule() { + try { + this.spec.suspend = true; // suspend schedule + await this.save(); + } catch (err) { + this.spec.suspend = false; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + async resumeSchedule() { + try { + this.spec.suspend = false; // resume schedule + await this.save(); + } catch (err) { + this.spec.suspend = true; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + get state() { + const conditions = get(this, 'status.conditions'); + const isSuspended = findBy(conditions, 'type', 'BackupSuspend')?.status === 'True'; + + if (isSuspended) { + return STATES.suspended.label; + } + + return this.metadata.state.name; + } + + get stateDescription() { + const suspendedCondition = (this.status?.conditions || []).find(c => c.type === 'BackupSuspend'); + + return ucFirst(suspendedCondition?.message) || super.stateDescription; + } + + get stateBackground() { + return colorForState(this.stateDisplay).replace('text-', 'bg-'); + } + + get stateColor() { + return colorForState(this.state); + } + + get stateDisplay() { + return stateDisplay(this.state); + } +} diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js index 995fda03..0e36227b 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js @@ -1,6 +1,7 @@ import { get, clone } from '@shell/utils/object'; import { findBy } from '@shell/utils/array'; import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; +import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { _CREATE } from '@shell/config/query-params'; import { HCI } from '../types'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; @@ -18,9 +19,8 @@ export default class HciVmBackup extends HarvesterResource { get detailLocation() { const detailLocation = clone(this._detailLocation); - const route = this.currentRoute(); - detailLocation.params.resource = route.params.resource; + detailLocation.params.resource = HCI.BACKUP; return detailLocation; } @@ -81,24 +81,30 @@ export default class HciVmBackup extends HarvesterResource { } restoreExistingVM(resource = this) { - const route = this.currentRoute(); const router = this.currentRouter(); + const targetResource = resource.spec.type === BACKUP_TYPE.BACKUP ? HCI.BACKUP : HCI.VM_SNAPSHOT; router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, - query: { restoreMode: 'existing', resourceName: resource.name } + params: { resource: targetResource }, + query: { + restoreMode: 'existing', + resourceName: resource.name, + } }); } restoreNewVM(resource = this) { - const route = this.currentRoute(); const router = this.currentRouter(); + const targetResource = resource.spec.type === BACKUP_TYPE.BACKUP ? HCI.BACKUP : HCI.VM_SNAPSHOT; router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, - query: { restoreMode: 'new', resourceName: resource.name } + params: { resource: targetResource }, + query: { + restoreMode: 'new', + resourceName: resource.name, + } }); } @@ -125,6 +131,10 @@ export default class HciVmBackup extends HarvesterResource { return colorForState(state); } + get sourceSchedule() { + return this.metadata?.annotations[HCI_ANNOTATIONS.SVM_BACKUP_ID]; + } + get attachVM() { return this.spec.source.name; } diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index 61bd5527..9d39163e 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -1,6 +1,6 @@ import { load } from 'js-yaml'; import { omitBy, pickBy } from 'lodash'; - +import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; import { POD, NODE, PVC } from '@shell/config/types'; import { findBy } from '@shell/utils/array'; @@ -161,6 +161,12 @@ export default class VirtVm extends HarvesterResource { icon: 'icon icon-storage', label: this.t('harvester.action.editVMQuota') }, + { + action: 'createSchedule', + enabled: true, + icon: 'icon icon-history', + label: this.t('harvester.action.createSchedule') + }, { action: 'restoreVM', enabled: !!this.actions?.restore, @@ -323,6 +329,16 @@ export default class VirtVm extends HarvesterResource { ); } + createSchedule(resources = this) { + const router = this.currentRouter(); + + router.push({ + name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, + params: { resource: HCI.SCHEDULE_VM_BACKUP }, + query: { vmNamespace: this.metadata.namespace, vmName: this.metadata.name } + }); + } + backupVM(resources = this) { this.$dispatch('promptModal', { resources, diff --git a/pkg/harvester/types.ts b/pkg/harvester/types.ts index 27a16ee8..3e889a0a 100644 --- a/pkg/harvester/types.ts +++ b/pkg/harvester/types.ts @@ -11,6 +11,7 @@ export const HCI = { SETTING: 'harvesterhci.io.setting', UPGRADE: 'harvesterhci.io.upgrade', UPGRADE_LOG: 'harvesterhci.io.upgradelog', + SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup', BACKUP: 'harvesterhci.io.virtualmachinebackup', RESTORE: 'harvesterhci.io.virtualmachinerestore', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', diff --git a/pkg/harvester/utils/cron.js b/pkg/harvester/utils/cron.js new file mode 100644 index 00000000..67f883cd --- /dev/null +++ b/pkg/harvester/utils/cron.js @@ -0,0 +1,11 @@ +import cronstrue from 'cronstrue'; + +export function isCronValid(schedule = '') { + try { + const hint = cronstrue.toString(schedule); + + return !!hint; + } catch (e) { + return false; + } +} diff --git a/yarn.lock b/yarn.lock index 5c4ff4d6..874c8f78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5960,10 +5960,10 @@ cron-validator@1.2.0: resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.2.0.tgz#952d2c926b85724dfe9c0d0ca781fe956124de93" integrity sha512-fX9eq71ToAt4bJeJzFNe8OCljKNQdc2Otw4kZDfB3vyplrAyEO9Q20YgmCJ4pr+jI/QQ2yizM87Eh+b2Ty7GuQ== -cronstrue@1.95.0: - version "1.95.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.95.0.tgz#171df1fad8b0f0cb636354dd1d7842161c15478f" - integrity sha512-CdbQ17Z8Na2IdrK1SiD3zmXfE66KerQZ8/iApkGsxjmUVGJPS9M9oK4FZC3LM6ohUjjq3UeaSk+90Cf3QbXDfw== +cronstrue@1.95.0, cronstrue@2.50.0: + version "2.50.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" + integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== cross-env@6.0.3: version "6.0.3"