diff --git a/package.json b/package.json index 0e1a90d69f4..89baadf1633 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "clipboard-polyfill": "4.0.1", "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "d3": "7.3.0", "d3-selection": "1.4.1", diff --git a/pkg/harvester/components/FilterVMSchedule.vue b/pkg/harvester/components/FilterVMSchedule.vue new file mode 100644 index 00000000000..0cf6002a67b --- /dev/null +++ b/pkg/harvester/components/FilterVMSchedule.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/pkg/harvester/config/harvester.js b/pkg/harvester/config/harvester.js index 5fad88065b6..bcd921bf28f 100644 --- a/pkg/harvester/config/harvester.js +++ b/pkg/harvester/config/harvester.js @@ -421,6 +421,7 @@ export function init($plugin, store) { basicType( [ + HCI.SCHEDULE_VM_BACKUP, HCI.BACKUP, HCI.SNAPSHOT, HCI.VM_SNAPSHOT, @@ -468,6 +469,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 1a01034507f..64a27c5316e 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -51,5 +51,7 @@ export const HCI = { PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice', VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy', NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status', - CPU_MANAGER: 'cpumanager' + 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 b580f4f0026..05f07425827 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 00000000000..1ae77e55bf0 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue @@ -0,0 +1,130 @@ + + + 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 00000000000..9fa3111dcc0 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue @@ -0,0 +1,98 @@ + + + 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 00000000000..274bcf5eefd --- /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 f97206f2b42..affa26ceaf8 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 60f40ce50a9..207ebd00209 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 69a3b5d54dc..22be4c10ef3 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/index.vue @@ -222,13 +222,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() { @@ -291,7 +293,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 a2565eacfdf..4e8c3786b42 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue @@ -3,11 +3,12 @@ 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', components: { - LabeledInput, LabeledSelect, InputOrDisplay + LabeledInput, LabeledSelect, InputOrDisplay, Banner }, props: { @@ -67,7 +68,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 935697d6546..5da0fe34e7c 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 00000000000..6755314f185 --- /dev/null +++ b/pkg/harvester/formatters/HarvesterCronExpression.vue @@ -0,0 +1,32 @@ + + + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index b58a2569f20..5e1fe8a1b59 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -57,6 +57,12 @@ harvester: tip: Please enter a template name! success: 'Template { templateName } created successfully.' failed: 'Failed generated template!' + schedule: + title: Create Schedule + message: + tip: Please enter a schedule name! + success: 'Schedule { name } created successfully.' + failed: 'Failed create schedule!' cloneVM: title: Clone Virtual Machine name: New Virtual Machine Name @@ -144,8 +150,11 @@ harvester: setDefaultVersion: Set default version addTemplateVersion: Add template version backup: Take Backup + createSchedule: Create Schedule restore: Restore restoreNewVM: Restore New + resumeSchedule: Resume + suspendSchedule: Suspend restoreExistingVM: Replace Existing migrate: Migrate abortMigration: Abort Migration @@ -182,6 +191,12 @@ harvester: readyToUse: Ready To Use backupTarget: Backup Target targetVm: Target Virtual Machine + cronExpression: Cron Expression + retain: Retain + scheduleType: Type + maxFailure: Max Failure + sourceVm: Source VM + vmSchedule: Virtual Machine Schedule hostIp: Host IP vm: ipAddress: IP Address @@ -228,6 +243,7 @@ harvester: promiscuous: Promiscuous ipv4Address: IPv4 address filterLabels: Filter labels + filterSchedule: Filter schedule storageClass: Storage class dockerImage: Docker image pci: @@ -550,6 +566,7 @@ harvester: size: Size edit: Edit bus: Bus + readyToUse: Ready To Use bootOrder: Boot Order volume: Volume dockerImage: Docker Image @@ -808,6 +825,37 @@ harvester: doc: Read the documentation before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to. tip: Unmet system requirements and incorrectly performed procedures may cause complete upgrade failure and other issues that require manual workarounds. moreNotes: For more details about the release notes, please visit - + + schedule: + label: Virtual Machine Schedules + createTitle: Create Schedule + createButtonText: Create Schedule + scheduleType: Virtual Machine Schedule Type + cron: Cron Schedule + detail: + namespace: Namespace + sourceVM: Source Virtual Machine + tabs: + basic: Basic + backups: Backups + snapshots: Snapshots + message: + noSetting: + suffix: before creating a backup schedule + retain: + label: Retain + count: Count + tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2. + maxFailure: + label: Max Failure + count: Count + tooltip: Max number of consecutive failed backups that could be tolerated. If reach this threshold, Harvester controller will suspend the schedule job. This value should less than retain count + virtualMachine: + title: Virtual Machine Name + placeholder: Select a virtual machine + type: + snapshot: Snapshot + backup: Backup backup: label: Virtual Machine Backups @@ -846,7 +894,6 @@ harvester: starting: Backup initiating progress: Backup in progress complete: Backup completed - restore: progress: details: Volume details @@ -1492,6 +1539,11 @@ typeLabel: one { Template } other { Templates } } + harvesterhci.io.schedulevmbackup: |- + {count, plural, + one { Virtual Machine Schedule } + other { Virtual Machine Schedules } + } harvesterhci.io.virtualmachinebackup: |- {count, plural, one { Virtual Machines Backup } diff --git a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue new file mode 100644 index 00000000000..a875b317099 --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue @@ -0,0 +1,124 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue index d563a3252f0..a3d0c2fc06a 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue @@ -4,36 +4,38 @@ import Loading from '@shell/components/Loading'; import MessageLink from '@shell/components/MessageLink'; import Masthead from '@shell/components/ResourceList/Masthead'; import ResourceTable from '@shell/components/ResourceTable'; - +import FilterVMSchedule from '../components/FilterVMSchedule'; import { HCI } from '../types'; import { allSettled } from '../utils/promise'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; 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 +52,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 +89,25 @@ export default { } return out; - } + }, + + getRow(row) { + return row.status && row.status.source; + }, + + changeRows(filteredRows, searchSchedule) { + this.$set(this, 'searchSchedule', searchSchedule); + this.$set(this, 'backups', filteredRows); + }, + + sortGenerationFn() { + let base = defaultTableSortGenerationFn(this.schema, this.$store); + + base += this.searchSchedule; + + return base; + }, + }, computed: { @@ -101,6 +123,12 @@ export default { align: 'left', formatter: 'AttachVMWithName' }, + { + name: 'backupCreatedFrom', + labelKey: 'harvester.tableHeaders.vmSchedule', + value: 'sourceSchedule', + formatter: 'BackupCreatedFrom', + }, { name: 'backupTarget', labelKey: 'tableHeaders.backupTarget', @@ -112,7 +140,7 @@ export default { name: 'readyToUse', labelKey: 'tableHeaders.readyToUse', value: 'status.readyToUse', - align: 'left', + align: 'center', formatter: 'Checked', }, ]; @@ -124,25 +152,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 +238,23 @@ export default { :headers="headers" :groupable="true" :rows="filteredRows" + :sort-generation-fn="sortGenerationFn" :schema="schema" key-field="_key" default-sort-by="age" v-on="$listeners" > + diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index cf5a4b9bf39..bc49bf410a1 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -303,7 +303,7 @@ export default { } = config; const vm = this.resource === HCI.VM ? value : this.resource === HCI.BACKUP ? this.value.status?.source : value.spec.vm; - + const volumeBackups = this.resource === HCI.BACKUP ? this.value.status?.volumeBackups : null; const spec = vm?.spec; if (!spec) { @@ -337,7 +337,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) || []; @@ -406,7 +407,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 || []; @@ -424,6 +425,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'; @@ -451,7 +453,8 @@ export default { storageClassName: '', image: this.imageId, volumeMode: 'Block', - isEncrypted + isEncrypted, + volumeBackups, }); } else { out = _disks.map( (DISK, index) => { @@ -533,6 +536,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), @@ -553,7 +557,8 @@ export default { volumeStatus, dataSource, namespace, - isEncrypted + isEncrypted, + volumeBackups, }; }); } diff --git a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js new file mode 100644 index 00000000000..13a2f566100 --- /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 8857f1352e9..711a537fbbf 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js @@ -2,6 +2,7 @@ import { HCI } from '../types'; 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 HarvesterResource from './harvester'; 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 6ad7ee9861a..2ca459ec835 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -1,7 +1,7 @@ import Vue from 'vue'; 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 { HCI } from '../types'; @@ -162,6 +162,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, @@ -324,6 +330,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 27a16ee83c8..3e889a0a294 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 00000000000..67f883cd143 --- /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/pkg/rancher-components/package.json b/pkg/rancher-components/package.json index d31e73d2955..ed7767b5615 100644 --- a/pkg/rancher-components/package.json +++ b/pkg/rancher-components/package.json @@ -32,8 +32,8 @@ "@vue/test-utils": "1.2.1", "babel-eslint": "10.1.0", "core-js": "3.25.3", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "eslint": "7.32.0", "eslint-plugin-import": "2.23.4", "eslint-plugin-node": "11.1.0", diff --git a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue index c4bd59177ac..109d19bb6fd 100644 --- a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue +++ b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue @@ -144,11 +144,16 @@ export default ( if (this.type !== 'cron' || !this.value) { return; } - if (!isValidCron(this.value)) { + // refer https://github.com/GuillaumeRochat/cron-validator#readme + if (!isValidCron(this.value, { + alias: true, + allowBlankDay: true, + allowSevenAsSunday: true, + })) { return this.t('generic.invalidCron'); } try { - const hint = cronstrue.toString(this.value); + const hint = cronstrue.toString(this.value, { verbose: true }); return hint; } catch (e) { diff --git a/shell/package.json b/shell/package.json index d2b28d60de1..0aca2c6dab0 100644 --- a/shell/package.json +++ b/shell/package.json @@ -59,8 +59,8 @@ "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", "core-js": "3.21.1", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "css-loader": "6.7.3", "csv-loader": "3.0.3", diff --git a/yarn.lock b/yarn.lock index e48ede151c5..cb5e8e1333c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5486,10 +5486,15 @@ 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== +cron-validator@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e" + integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A== + +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"