-
+
-
-
+
-
-
-
-
+
+
+
-
+
@@ -162,7 +162,7 @@ export default {
-
+
@@ -180,7 +180,6 @@ export default {
-
+import { RadioGroup } from '@components/Form/Radio';
+import { Banner } from '@components/Banner';
+import { LabeledInput } from '@components/Form/LabeledInput';
+import CruResource from '@shell/components/CruResource';
+import Tabbed from '@shell/components/Tabbed';
+import Tab from '@shell/components/Tabbed/Tab';
+import MessageLink from '@shell/components/MessageLink';
+import LabeledSelect from '@shell/components/form/LabeledSelect';
+import CreateEditView from '@shell/mixins/create-edit-view';
+import { isCronValid } from '@pkg/harvester/utils/cron';
+import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
+import { allHash } from '@shell/utils/promise';
+import { HCI } from '../types';
+import { sortBy } from '@shell/utils/sort';
+import { BACKUP_TYPE } from '../config/types';
+import { _EDIT, _CREATE } from '@shell/config/query-params';
+
+export default {
+ name: 'CreateVMSchedule',
+ components: {
+ CruResource,
+ Tabbed,
+ Tab,
+ RadioGroup,
+ LabeledInput,
+ LabeledSelect,
+ MessageLink,
+ Banner,
+ },
+
+ mixins: [CreateEditView],
+
+ async fetch() {
+ const hash = await allHash({
+ settings: this.$store.dispatch('harvester/findAll', { type: HCI.SETTING }),
+ vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
+ });
+
+ this.allVms = hash.vms;
+ this.settings = hash.settings;
+ },
+
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ },
+ mode: {
+ type: String,
+ required: true,
+ }
+ },
+
+ data() {
+ if (this.mode === _CREATE) {
+ const defaultNs = this.$store.getters['defaultNamespace'];
+ const vmNamespace = this.$route.query?.vmNamespace || defaultNs;
+ const vmName = this.$route.query?.vmName;
+
+ delete this.value.metadata.annotations;
+ delete this.value.metadata.labels;
+ // set value.metadata
+ this.$set(this.value, 'metadata', {
+ namespace: vmNamespace,
+ name: vmName ? `svmbackup-${ vmName }` : ''
+ });
+ // set value.spec
+ if (!this.value.spec) {
+ this.$set(this.value, 'spec', {
+ cron: '',
+ retain: 8,
+ maxFailure: 4,
+ });
+ // set value.spec.vmbackup
+ this.$set(this.value.spec, 'vmbackup', {
+ source: {
+ apiGroup: 'kubevirt.io',
+ kind: 'VirtualMachine',
+ name: vmName || ''
+ },
+ type: BACKUP_TYPE.BACKUP
+ });
+ }
+ }
+
+ return { settings: [] };
+ },
+
+ computed: {
+ backupTargetResource() {
+ return this.settings.find( O => O.id === 'backup-target');
+ },
+ isEmptyValue() {
+ return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
+ },
+ canUpdate() {
+ return this?.backupTargetResource?.canUpdate;
+ },
+ errorMessage() {
+ return this.backupTargetResource?.errMessage;
+ },
+ canSave() {
+ return !!this.value.spec.cron && isCronValid(this.value.spec.cron) &&
+ !!this.value.metadata.name &&
+ !!this.value.metadata.namespace &&
+ !!this.value.spec.retain &&
+ !!this.value.spec.maxFailure;
+ },
+ isBackupTargetUnAvailable() {
+ return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
+ },
+ vmOptions() {
+ const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter(vm => vm.metadata.namespace === this.value.metadata.namespace);
+ const vmObjectLists = nsVmList.map(obj => ({
+ label: obj.nameDisplay,
+ value: obj.name,
+ }));
+
+ return sortBy(vmObjectLists, 'label');
+ },
+
+ namespaces() {
+ const allNamespaces = this.$store.getters['allNamespaces'];
+ const out = sortBy(
+ allNamespaces.map((obj) => {
+ return {
+ label: obj.nameDisplay,
+ value: obj.id,
+ };
+ }),
+ 'label'
+ );
+
+ return out;
+ },
+ toBackupTargetSetting() {
+ const { cluster } = this.$router?.currentRoute?.params || {};
+
+ return {
+ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-id`,
+ params: {
+ resource: `${ HCI.SETTING }`,
+ cluster,
+ id: 'backup-target'
+ },
+ query: { mode: _EDIT }
+ };
+ },
+ scheduleTypeOptions() {
+ return [BACKUP_TYPE.BACKUP, BACKUP_TYPE.SNAPSHOT];
+ }
+ },
+
+ watch: {
+ 'value.metadata.namespace'() {
+ this.value.spec.vmbackup.source.name = '';
+ },
+ 'value.spec.vmbackup.source.name'(neu) {
+ this.value.metadata.name = `svm${ this.value.spec.vmbackup.type }-${ neu }`;
+ }
+ },
+
+ methods: {
+ onTypeChange(newType) {
+ this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
+ },
+ getBackupTargetValueIsEmpty(resource) {
+ let out = true;
+
+ if (resource?.value) {
+ try {
+ const valueJson = JSON.parse(resource?.value);
+
+ out = !valueJson.type;
+ } catch (e) {}
+ }
+
+ return out;
+ },
+
+ validateFailure(count) {
+ if (this.value.spec.retain && count > this.value.spec.retain) {
+ this.$set(this.value.spec, 'maxFailure', this.value.spec.retain);
+ }
+ },
+ },
+};
+
+
+
+ errors = e"
+ >
+
+
+
+
+
+ {{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue
index 05b4c1b3139..b42c29e4a81 100644
--- a/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue
+++ b/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue
@@ -159,6 +159,10 @@ export default {
},
methods: {
+ cancelAction() {
+ this.$router.go(-1);
+ },
+
async saveRestore(buttonCb) {
this.update();
@@ -261,7 +265,7 @@ 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 @@
+
+
+
+ {{ value }}
+
+
+ {{ value }}
+
+
+ —
+
+
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 @@
+
+
+
+
+ {{ value }}
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ 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..e5d65dce216 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,
};
},
@@ -63,19 +67,32 @@ export default {
labelKey: 'tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
+ sort: 'attachVM',
formatter: 'AttachVMWithName'
},
+ {
+ name: 'backupCreatedFrom',
+ labelKey: 'harvester.tableHeaders.vmSchedule',
+ value: 'sourceSchedule',
+ sort: 'sourceSchedule',
+ formatter: 'BackupCreatedFrom',
+ },
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
- align: 'left',
+ align: 'center',
+ sort: 'status.readyToUse',
formatter: 'Checked',
},
AGE
];
},
+ getRawRows() {
+ return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.SNAPSHOT);
+ },
+
schema() {
return schema;
},
@@ -85,9 +102,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 +132,23 @@ export default {
:type-display="typeDisplay"
:create-button-label="t('harvester.vmSnapshot.createText')"
/>
-
+
+
+
@@ -126,6 +164,6 @@ export default {
|
-
+
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"
|