-
+
@@ -162,7 +162,7 @@ export default {
-
+
@@ -180,7 +180,6 @@ export default {
-
+import { exceptionToErrorsArray } from '@shell/utils/error';
+import { mapGetters } from 'vuex';
+import { Card } from '@components/Card';
+import { Banner } from '@components/Banner';
+import AsyncButton from '@shell/components/AsyncButton';
+import { LabeledInput } from '@components/Form/LabeledInput';
+
+export default {
+ name: 'HarvesterScheduleModal',
+
+ components: {
+ AsyncButton,
+ Card,
+ LabeledInput,
+ Banner
+ },
+
+ props: {
+ resources: {
+ type: Array,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ retain: 2,
+ autoResume: false,
+ cronExpression: '',
+ maxFlunk: 2,
+ targetVM: '',
+ errors: []
+ };
+ },
+
+ computed: {
+ ...mapGetters({ t: 'i18n/t' }),
+
+ actionResource() {
+ return this.resources[0];
+ }
+ },
+
+ methods: {
+ close() {
+ this.targetVM = '';
+ this.$emit('close');
+ },
+
+ async save(buttonCb) {
+ if (this.actionResource) {
+ try {
+ const res = await this.actionResource.doAction(
+ 'backup',
+ { name: this.backUpName },
+ {},
+ false
+ );
+
+ if (res._status === 200 || res._status === 204) {
+ this.$store.dispatch(
+ 'growl/success',
+ {
+ title: this.t('generic.notification.title.succeed'),
+ message: this.t('harvester.modal.backup.success', { backUpName: this.backUpName })
+ },
+ { root: true }
+ );
+
+ this.close();
+
+ buttonCb(true);
+ } else {
+ const error = [res?.data] || exceptionToErrorsArray(res);
+
+ this.$set(this, 'errors', error);
+ buttonCb(false);
+ }
+ } catch (err) {
+ const error = err?.data || err;
+ const message = exceptionToErrorsArray(error);
+
+ this.$set(this, 'errors', message);
+ buttonCb(false);
+ }
+ }
+ }
+ }
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue
new file mode 100644
index 00000000000..b204444a9c2
--- /dev/null
+++ b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue
@@ -0,0 +1,312 @@
+
+
+
+ errors = e"
+ >
+
+
+
+
+
+ {{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 4a992224ecc..91b4103bc81 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..936c46af598 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 @@
+
+
+
+ {{ value }}
+
+
+ {{ value }}
+
+
+ —
+
+
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index 0cca30624fa..3c9a7987647 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -52,6 +52,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 VM
name: New VM Name
@@ -136,8 +142,11 @@ harvester:
setDefaultVersion: Set default version
addTemplateVersion: Add templateVersion
backup: Take Backup
+ createSchedule: Create VM Schedule
restore: Restore
restoreNewVM: Restore New
+ resumeSchedule: Resume
+ suspendSchedule: Suspend
restoreExistingVM: Replace Existing
migrate: Migrate
abortMigration: Abort Migration
@@ -168,6 +177,12 @@ harvester:
actions: Actions
readyToUse: Ready To Use
backupTarget: Backup Target
+ cronExpression: Cron Expression
+ retain: Retain
+ scheduleType: Type
+ maxFailure: Max Failure
+ sourceVm: Source VM
+ vmSchedule: VM Schedule
targetVm: Target VM
hostIp: Host IP
vm:
@@ -211,6 +226,7 @@ harvester:
promiscuous: Promiscuous
ipv4Address: IPv4 Address
filterLabels: Filter Labels
+ filterSchedule: Filter VM Schedule
storageClass: Storage Class
dockerImage: Docker Image
pci:
@@ -517,6 +533,7 @@ harvester:
size: Size
edit: Edit
bus: Bus
+ readyToUse: Ready To Use
bootOrder: Boot Order
volume: Volume
dockerImage: Docker Image
@@ -762,6 +779,36 @@ harvester:
doc: Before you upgrade to the newer Harvester version, you must perform the required
pre-upgrade checks for your cluster. Complete only those tasks that apply to your environment.
tip: Failure to perform these checks may result in a failed upgrade or hitting known issues that require a manual workaround fix.
moreNotes: For more details about the release notes, please visit -
+ schedule:
+ label: VM Schedules
+ createTitle: Create VM Schedule
+ createButtonText: Create VM Schedule
+ scheduleType: VM 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: VM Backups
@@ -800,7 +847,6 @@ harvester:
starting: Backup initiating
progress: Backup in progress
complete: Backup completed
-
restore:
progress:
details: Volume details
@@ -1414,6 +1460,11 @@ typeLabel:
one { Template }
other { Templates }
}
+ harvesterhci.io.schedulevmbackup: |-
+ {count, plural,
+ one { VM Schedule }
+ other { VM Schedules }
+ }
harvesterhci.io.virtualmachinebackup: |-
{count, plural,
one { VM 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..29b8e30bcd4
--- /dev/null
+++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.nameDisplay }}
+
+
+ {{ row.nameDisplay }}
+
+
+ |
+
+
+
+
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"
>
+
+
+
{{ row.nameDisplay }}
diff --git a/pkg/harvester/list/harvesterhci.io.vmsnapshot.vue b/pkg/harvester/list/harvesterhci.io.vmsnapshot.vue
index ed94a79ff4d..6315f2c14dc 100644
--- a/pkg/harvester/list/harvesterhci.io.vmsnapshot.vue
+++ b/pkg/harvester/list/harvesterhci.io.vmsnapshot.vue
@@ -2,14 +2,15 @@
import Loading from '@shell/components/Loading';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
-
import { HCI } from '../types';
import { SCHEMA } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
+import FilterVMSchedule from '../components/FilterVMSchedule';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { BACKUP_TYPE } from '../config/types';
+import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
-const schema = {
+export const schema = {
id: HCI.VM_SNAPSHOT,
type: SCHEMA,
attributes: {
@@ -22,7 +23,7 @@ const schema = {
export default {
name: 'HarvesterListVMSnapshot',
components: {
- ResourceTable, Loading, Masthead
+ ResourceTable, Loading, Masthead, FilterVMSchedule
},
async fetch() {
@@ -39,6 +40,7 @@ export default {
}
this.rows = hash.rows;
+ this.snapshots = hash.rows;
},
data() {
@@ -47,7 +49,9 @@ export default {
const resource = params.resource;
return {
- rows: [],
+ rows: [],
+ snapshots: [],
+ searchSchedule: '',
resource,
};
},
@@ -65,17 +69,27 @@ export default {
align: 'left',
formatter: 'AttachVMWithName'
},
+ {
+ name: 'backupCreatedFrom',
+ labelKey: 'harvester.tableHeaders.vmSchedule',
+ value: 'sourceSchedule',
+ formatter: 'BackupCreatedFrom',
+ },
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
- align: 'left',
+ align: 'center',
formatter: 'Checked',
},
AGE
];
},
+ getRawRows() {
+ return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.SNAPSHOT);
+ },
+
schema() {
return schema;
},
@@ -85,9 +99,24 @@ export default {
},
filteredRows() {
- return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.BACKUP);
+ return this.snapshots.filter(r => r.spec?.type !== BACKUP_TYPE.BACKUP);
},
},
+
+ methods: {
+ changeRows(filteredRows, searchSchedule) {
+ this.$set(this, 'searchSchedule', searchSchedule);
+ this.$set(this, 'snapshots', filteredRows);
+ },
+
+ sortGenerationFn() {
+ let base = defaultTableSortGenerationFn(this.schema, this.$store);
+
+ base += this.searchSchedule;
+
+ return base;
+ },
+ }
};
@@ -100,17 +129,23 @@ export default {
:type-display="typeDisplay"
:create-button-label="t('harvester.vmSnapshot.createText')"
/>
-
+
+
+
@@ -126,6 +161,6 @@ export default {
|
-
+
diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js
index ea31a5a3213..f34f143483f 100644
--- a/pkg/harvester/mixins/harvester-vm/index.js
+++ b/pkg/harvester/mixins/harvester-vm/index.js
@@ -302,7 +302,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) {
@@ -336,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) || [];
@@ -403,7 +404,8 @@ export default {
this.refreshYamlEditor();
},
- getDiskRows(vm) {
+ getDiskRows(vm, volumeBackups) {
+ console.log('🚀 ~ getDiskRows ~ volumeBackups:', volumeBackups);
const namespace = vm.metadata.namespace;
const _volumes = vm.spec.template.spec.volumes || [];
const _disks = vm.spec.template.spec.domain.devices.disks || [];
@@ -424,6 +426,7 @@ export default {
storageClassName: '',
image: this.imageId,
volumeMode: 'Block',
+ volumeBackups: volumeBackups?.find(vBackup => vBackup.volumeName === 'disk-0') || null,
});
} else {
out = _disks.map( (DISK, index) => {
@@ -503,24 +506,25 @@ export default {
const volumeStatus = this.pvcs.find(P => P.id === `${ this.value.metadata.namespace }/${ volumeName }`)?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
return {
- id: randomStr(5),
+ id: randomStr(5),
bootOrder,
source,
- name: DISK.name,
+ name: DISK.name,
realName,
bus,
volumeName,
container,
accessMode,
- size: `${ formatSize }Gi`,
- volumeMode: volumeMode || this.customVolumeMode,
+ size: `${ formatSize }Gi`,
+ volumeMode: volumeMode || this.customVolumeMode,
image,
type,
storageClassName,
hotpluggable,
volumeStatus,
dataSource,
- namespace
+ namespace,
+ volumeBackups: volumeBackups?.find(vBackup => vBackup.volumeName === DISK.name) || null,
};
});
}
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..fa9a2220c28 100644
--- a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js
+++ b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js
@@ -18,9 +18,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,23 +80,22 @@ export default class HciVmBackup extends HarvesterResource {
}
restoreExistingVM(resource = this) {
- const route = this.currentRoute();
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
- params: { resource: route.params.resource },
+ params: { resource: HCI.BACKUP },
query: { restoreMode: 'existing', resourceName: resource.name }
});
}
restoreNewVM(resource = this) {
- const route = this.currentRoute();
+ // const route = this.currentRoute();
const router = this.currentRouter();
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
- params: { resource: route.params.resource },
+ params: { resource: HCI.BACKUP },
query: { restoreMode: 'new', resourceName: resource.name }
});
}
@@ -125,6 +123,10 @@ export default class HciVmBackup extends HarvesterResource {
return colorForState(state);
}
+ get sourceSchedule() {
+ return this.metadata?.annotations['harvesterhci.io/svmbackupId'];
+ }
+
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 e4cd26f8d2e..a8822992fb5 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';
@@ -156,6 +156,12 @@ export default class VirtVm extends HarvesterResource {
icon: 'icon icon-backup',
label: this.t('harvester.action.vmSnapshot')
},
+ {
+ action: 'createSchedule',
+ enabled: true,
+ icon: 'icon icon-history',
+ label: this.t('harvester.action.createSchedule')
+ },
{
action: 'restoreVM',
enabled: !!this.actions?.restore,
@@ -318,6 +324,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 6638cacc77d..5dad3b1d5b4 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/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 5744afeaab1..d6e6ea9740b 100644
--- a/shell/package.json
+++ b/shell/package.json
@@ -58,8 +58,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/shell/utils/validators/cron-schedule.js b/shell/utils/validators/cron-schedule.js
index d726f6644c3..ebea8885c5f 100644
--- a/shell/utils/validators/cron-schedule.js
+++ b/shell/utils/validators/cron-schedule.js
@@ -7,3 +7,13 @@ export function cronSchedule(schedule = '', getters, errors) {
errors.push(getters['i18n/t']('validation.invalidCron'));
}
}
+
+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 2d93df2cc7c..c7225b406cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5490,10 +5490,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"
|