Latest changes from Harvester master - a537c1ae38eb7030542ac371f24ae3336cd9d422

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia 2024-09-30 14:57:53 +02:00
parent 2ca86930ad
commit 118aaf16b7
No known key found for this signature in database
GPG Key ID: E6D011B7415D4393
35 changed files with 1484 additions and 93 deletions

View File

@ -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",

View File

@ -0,0 +1,123 @@
<script>
import { RadioGroup } from '@components/Form/Radio';
export default {
name: 'HarvesterFilterVMSchedule',
emits: ['change-rows'],
components: { RadioGroup },
props: {
rows: {
type: Array,
required: true,
},
},
data() {
return {
selected: '',
};
},
computed: {
scheduleOptions() {
const options = this.rows.map(r => r.sourceSchedule).filter(r => r);
return Array.from(new Set(options));
},
},
methods: {
onSelect(selected) {
this.selected = selected;
this.filterRows();
},
remove() {
this.selected = '';
this.filterRows();
},
filterRows() {
if (!this.selected) {
this.$emit('change-rows', this.rows);
return;
}
const filteredRows = this.rows.filter(row => row.sourceSchedule === this.selected);
this.$emit('change-rows', filteredRows, this.selected);
}
},
watch: {
rows: {
deep: true,
immediate: false,
handler() {
this.filterRows();
}
}
}
};
</script>
<template>
<div class="vm-schedule-filter">
<template>
<span v-if="selected" class="banner-item bg-warning">
{{ t('harvester.tableHeaders.vmSchedule') }}{{ selected ? ` = ${selected}`: '' }}<i class="icon icon-close ml-5" @click="remove" />
</span>
</template>
<v-dropdown
:triggers="scheduleOptions.length ? ['click'] : []"
placement="bottom-end"
offset="1"
>
<button ref="actionDropDown" class="btn bg-primary mr-10">
<slot name="title">
{{ t('harvester.fields.filterSchedule') }}
</slot>
</button>
<template #popper>
<div class="filter-popup">
<RadioGroup
v-model:value="selected"
class="mr-10 ml-10"
name="model"
:options="scheduleOptions"
:labels="scheduleOptions"
@input="onSelect"
/>
</div>
</template>
</v-dropdown>
</div>
</template>
<style lang="scss" scoped>
.vm-schedule-filter {
display: inline-block;
.banner-item {
display: inline-block;
font-size: 16px;
margin-right: 10px;
padding: 6px;
border-radius: 2px;
i {
cursor: pointer;
vertical-align: middle;
}
}
}
.filter-popup {
width: max-content;
}
</style>

View File

@ -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',

View File

@ -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',
};

View File

@ -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',
};

View File

@ -0,0 +1,129 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import { STATE, NAME, AGE } from '@shell/config/table-headers';
import { allSettled } from '../../utils/promise';
import { BACKUP_TYPE } from '../../config/types';
import { HCI } from '../../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
export default {
name: 'BackupList',
components: { ResourceTable },
props: {
id: {
type: String,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allSettled({ backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }) });
this.rows = hash.backups;
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
const schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.BACKUP);
return {
rows: [],
schema
};
},
computed: {
headers() {
const cols = [
STATE,
{
...NAME,
width: 400
},
{
name: 'targetVM',
labelKey: 'tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
sort: 'attachVM',
formatter: 'AttachVMWithName'
},
{
name: 'backupTarget',
labelKey: 'tableHeaders.backupTarget',
value: 'backupTarget',
sort: 'backupTarget',
align: 'left',
formatter: 'HarvesterBackupTargetValidation'
},
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
sort: 'status.readyToUse',
align: 'center',
formatter: 'Checked',
},
];
if (this.hasBackupProgresses) {
cols.push({
name: 'backupProgress',
labelKey: 'tableHeaders.progress',
value: 'backupProgress',
sort: 'backupProgress',
align: 'left',
formatter: 'HarvesterBackupProgressBar',
});
}
cols.push(AGE);
return cols;
},
hasBackupProgresses() {
return !!this.rows.find(R => R.status?.progress !== undefined);
},
filteredRows() {
let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.BACKUP);
if (this.id) {
r = r.filter(backup => backup.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id);
}
return r;
},
},
};
</script>
<template>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="false"
:rows="filteredRows"
:schema="schema"
key-field="_key"
default-sort-by="age"
>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="row?.status?.source"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
</router-link>
<span v-else>
{{ row.nameDisplay }}
</span>
</span>
</td>
</template>
</ResourceTable>
</template>

View File

@ -0,0 +1,97 @@
<script>
import ResourceTable from '@shell/components/ResourceTable';
import { STATE, NAME, AGE } from '@shell/config/table-headers';
import { allSettled } from '../../utils/promise';
import { BACKUP_TYPE } from '../../config/types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../../types';
import { schema } from '../../list/harvesterhci.io.vmsnapshot';
export default {
name: 'SnapshotList',
components: { ResourceTable },
props: {
id: {
type: String,
required: true,
},
},
async fetch() {
const hash = await allSettled({ backups: this.$store.dispatch('harvester/findAll', { type: HCI.BACKUP }) });
this.rows = hash.backups;
},
data() {
return {
rows: [],
schema
};
},
computed: {
headers() {
return [
STATE,
NAME,
{
name: 'targetVM',
labelKey: 'tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
formatter: 'AttachVMWithName'
},
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
align: 'center',
formatter: 'Checked',
},
AGE
];
},
filteredRows() {
let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.SNAPSHOT);
if (this.id) {
r = r.filter(row => row.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id);
}
return r;
},
},
};
</script>
<template>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="false"
:rows="filteredRows"
:schema="schema"
key-field="_key"
default-sort-by="age"
>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="row?.status?.source"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
</router-link>
<span v-else>
{{ row.nameDisplay }}
</span>
</span>
</td>
</template>
</ResourceTable>
</template>

View File

@ -0,0 +1,125 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { BACKUP_TYPE } from '../../config/types';
import BackupList from './BackupList';
import SnapshotList from './SnapshotList';
import cronstrue from 'cronstrue';
export default {
name: 'ScheduleVmBackupDetail',
components: {
BackupList,
SnapshotList,
Tab,
Tabbed,
LabelValue,
},
props: {
value: {
type: Object,
required: true,
},
},
computed: {
isBackup() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP;
},
isSnapshot() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.SNAPSHOT;
},
cronExpression() {
let cronHint = '';
try {
cronHint = cronstrue.toString(this.value.spec.cron, { verbose: true });
} catch (e) {
cronHint = this.t('generic.invalidCron');
}
return cronHint ? `${ this.value.spec.cron } (${ cronHint })` : this.value.spec.cron;
}
}
};
</script>
<template>
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
<Tab
name="basic"
:label="t('harvester.virtualMachine.detail.tabs.basics')"
class="bordered-table"
:weight="99"
>
<div class="row">
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.detail.namespace')"
:value="value.metadata.namespace"
/>
</div>
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.detail.sourceVM')"
:value="value.spec.vmbackup.source.name"
/>
</div>
</div>
<div class="row">
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.cron')"
:value="cronExpression"
/>
</div>
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.scheduleType')"
:value="value.spec.vmbackup.type"
/>
</div>
</div>
<div class="row">
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.retain.label')"
:value="value.spec.retain"
/>
</div>
<div class="col span-6 mb-20">
<LabelValue
:name="t('harvester.schedule.maxFailure.label')"
:value="value.spec.maxFailure"
/>
</div>
</div>
</Tab>
<Tab
v-if="isBackup"
name="backups"
:label="t('harvester.schedule.tabs.backups')"
:weight="89"
class="bordered-table"
>
<BackupList :id="value.id" />
</Tab>
<Tab
v-if="isSnapshot"
name="snapshots"
:label="t('harvester.schedule.tabs.snapshots')"
:weight="79"
class="bordered-table"
>
<SnapshotList :id="value.id" />
</Tab>
</Tabbed>
</template>
<style lang="scss" scoped>
.error {
color: var(--error);
}
</style>

View File

@ -127,29 +127,29 @@ export default {
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
<div class="row">
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
</div>
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
</div>
</div>
<div class="row">
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
</div>
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
</div>
</div>
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
<div class="mb-20">
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
</div>
<div class="row">
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
<template #value>
<div>
@ -162,7 +162,7 @@ export default {
</template>
</LabelValue>
</div>
<div class="col span-6">
<div class="col span-6 mb-20">
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
<template #value>
<div>
@ -180,7 +180,6 @@ export default {
</div>
</div>
</Tab>
<Tab
name="volume"
:label="t('harvester.tab.volume')"

View File

@ -1,13 +1,15 @@
<script>
import { HCI } from '../types';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';
import { HCI } from '../types';
export default {
name: 'HarvesterEnableUSBPassthrough',
emits: ['close'],
components: {
AsyncButton,
Card,
@ -83,25 +85,28 @@ export default {
<template>
<Card :show-highlight-border="false">
<h4
slot="title"
v-clean-html="t('promptRemove.title')"
class="text-default-text"
/>
<template #body>
<t k="harvester.usb.enablePassthroughWarning" :raw="true" />
<template #title>
<h4
v-clean-html="t('promptRemove.title')"
class="text-default-text"
/>
</template>
<div slot="actions" class="actions">
<div class="buttons">
<button class="btn role-secondary mr-10" @click="close">
{{ t('generic.cancel') }}
</button>
<template #body>
{{ t('harvester.usb.enablePassthroughWarning') }}
</template>
<AsyncButton mode="enable" @click="save" />
<template #actions>
<div class="actions">
<div class="buttons">
<button class="btn role-secondary mr-10" @click="close">
{{ t('generic.cancel') }}
</button>
<AsyncButton mode="enable" @click="save" />
</div>
</div>
</div>
</template>
</Card>
</template>

View File

@ -171,7 +171,6 @@ export default {
@click="apply"
/>
</div>
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
</template>
</Card>

View File

@ -0,0 +1,298 @@
<script>
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;
this.value['metadata'] = {
namespace: vmNamespace,
name: vmName ? `svmbackup-${ vmName }` : ''
};
if (!this.value.spec) {
this.value['spec'] = {
cron: '',
retain: 8,
maxFailure: 4,
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.value.spec['maxFailure'] = this.value.spec.retain;
}
},
},
};
</script>
<template>
<CruResource
:done-route="doneRoute"
:resource="value"
:mode="mode"
:errors="errors"
:apply-hooks="applyHooks"
:validationPassed="canSave"
@finish="save"
@error="e=>errors = e"
>
<div class="banner">
<Banner
v-if="isBackupTargetUnAvailable"
color="error"
>
<MessageLink
v-if="isEmptyValue"
:to="toBackupTargetSetting"
:target="_blank"
prefix-label="harvester.backup.message.noSetting.prefix"
middle-label="harvester.backup.message.noSetting.middle"
suffix-label="harvester.schedule.message.noSetting.suffix"
/>
<MessageLink
v-else
:to="toBackupTargetSetting"
prefix-label="harvester.backup.message.errorTip.prefix"
middle-label="harvester.backup.message.errorTip.middle"
>
<template v-slot:suffix>
{{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
</template>
</MessageLink>
</Banner>
<div class="mb-30">
<RadioGroup
v-model:value="value.spec.vmbackup.type"
name="model"
:options="scheduleTypeOptions"
:labels="[t('harvester.schedule.type.backup'), t('harvester.schedule.type.snapshot')]"
:disabled="isEdit || isView"
:mode="mode"
row
@input="onTypeChange"
/>
</div>
<div class="row mb-30">
<div class="col span-6">
<LabeledSelect
v-model:value="value.metadata.namespace"
:label="t('nameNsDescription.namespace.label')"
:options="namespaces"
required
:disabled="isBackupTargetUnAvailable || isEdit || isView"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.vmbackup.source.name"
:label="t('harvester.schedule.virtualMachine.title')"
:placeholder="t('harvester.schedule.virtualMachine.placeholder')"
:options="vmOptions"
required
:disabled="isBackupTargetUnAvailable || isEdit || isView"
/>
</div>
</div>
</div>
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
<Tab name="basics" :label="t('harvester.network.tabs.basics')" :weight="99" class="bordered-table">
<LabeledInput
v-model:value="value.spec.cron"
class="mb-30"
type="cron"
required
:mode="mode"
:label="t('harvester.schedule.cron')"
placeholder="0 * * * *"
:disabled="isBackupTargetUnAvailable || isView"
/>
<LabeledInput
v-model:value.number="value.spec.retain"
v-int-number
class="mb-30"
:min="2"
:max="250"
type="number"
:label="t('harvester.schedule.retain.label')"
required
:tooltip="t('harvester.schedule.retain.tooltip')"
:disabled="isBackupTargetUnAvailable || isView"
/>
<LabeledInput
v-model:value.number="value.spec.maxFailure"
v-int-number
class="mb-30"
:min="2"
type="number"
:label="t('harvester.schedule.maxFailure.label')"
required
:tooltip="t('harvester.schedule.maxFailure.tooltip')"
:disabled="isBackupTargetUnAvailable || isView"
@input="validateFailure"
/>
</Tab>
</Tabbed>
</CruResource>
</template>

View File

@ -159,6 +159,10 @@ export default {
},
methods: {
cancelAction() {
this.$router.go(-1);
},
async saveRestore(buttonCb) {
this.update();
@ -261,7 +265,7 @@ export default {
/>
</div>
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
</div>
</template>

View File

@ -157,6 +157,9 @@ export default {
},
methods: {
cancelAction() {
this.$router.go(-1);
},
async saveRestore(buttonCb) {
this.update();
@ -241,7 +244,7 @@ export default {
<LabeledSelect v-if="!restoreNewVm" v-model:value="deletionPolicy" :label="t('harvester.backup.restore.deletePreviousVolumes')" :options="deletionPolicyOption" />
</div>
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
</div>
</template>

View File

@ -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 {
</span>
<span v-else>
{{ headerFor(volume.source) }}
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
</span>
</h3>
<div>

View File

@ -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 {
/>
</InputOrDisplay>
</div>
<div
data-testid="input-hec-type"
class="col span-6"
@ -111,7 +111,6 @@ export default {
/>
</InputOrDisplay>
</div>
<div
data-testid="input-hec-bus"
class="col span-6"
@ -131,5 +130,28 @@ export default {
</InputOrDisplay>
</div>
</div>
<div class="row mb-20">
<div
v-if="value.volumeBackups"
class="col span-6"
>
<InputOrDisplay
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="value.volumeBackups.readyToUse"
:mode="mode"
>
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="value.volumeBackups.readyToUse"
/>
</InputOrDisplay>
</div>
</div>
<Banner
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
color="error"
class="mb-20"
:label="value.volumeBackups.error.message"
/>
</div>
</template>

View File

@ -1,9 +1,10 @@
<script>
import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabelValue from '@shell/components/LabelValue';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { Banner } from '@components/Banner';
import { sortBy } from '@shell/utils/sort';
import { PVC } from '@shell/config/types';
import { _CREATE } from '@shell/config/query-params';
@ -17,7 +18,7 @@ export default {
emits: ['update'],
components: {
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
},
props: {
@ -275,7 +276,6 @@ export default {
/>
</InputOrDisplay>
</div>
<div
data-testid="input-hee-bus"
class="col span-6"
@ -296,6 +296,27 @@ export default {
/>
</InputOrDisplay>
</div>
<div
v-if="value.volumeBackups"
class="col span-6"
>
<InputOrDisplay
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="value.volumeBackups.readyToUse"
:mode="mode"
>
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="value.volumeBackups.readyToUse"
/>
</InputOrDisplay>
</div>
</div>
<Banner
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
color="error"
class="mb-20"
:label="value.volumeBackups.error.message"
/>
</div>
</template>

View File

@ -4,6 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { Banner } from '@components/Banner';
import { PVC } from '@shell/config/types';
import { formatSi, parseSi } from '@shell/utils/units';
import { HCI } from '../../../../types';
@ -18,7 +19,7 @@ export default {
emits: ['update'],
components: {
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
},
props: {
@ -101,6 +102,12 @@ export default {
return image ? image.label : '-';
},
readyToUse() {
const val = String(this.value.volumeBackups?.readyToUse || false);
return ucFirst(val);
},
pvcsResource() {
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
@ -301,7 +308,7 @@ export default {
</div>
<div
v-if="isView"
class="col span-6"
class="col span-3"
>
<LabelValue
:name="t('harvester.virtualMachine.volume.encryption')"
@ -309,5 +316,19 @@ export default {
/>
</div>
</div>
<div class="row mb-20">
<div v-if="value.volumeBackups && isView" class="col span-3">
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="readyToUse"
/>
</div>
</div>
<Banner
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
color="error"
class="mb-20"
:label="value.volumeBackups.error.message"
/>
</div>
</template>

View File

@ -4,7 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { formatSi, parseSi } from '@shell/utils/units';
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
@ -18,7 +18,7 @@ export default {
emits: ['update'],
components: {
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, Banner
},
props: {
@ -68,9 +68,16 @@ export default {
return ucFirst(String(this.value.isEncrypted));
},
readyToUse() {
const val = String(this.value.volumeBackups?.readyToUse || false);
return ucFirst(val);
},
isView() {
return this.mode === _VIEW;
},
pvcsResource() {
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
@ -221,7 +228,6 @@ export default {
</InputOrDisplay>
</div>
</div>
<div class="row mb-20">
<div
data-testid="input-hev-bus"
@ -248,5 +254,19 @@ export default {
/>
</div>
</div>
<div class="row mb-20">
<div v-if="value.volumeBackups && isView" class="col span-3">
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="readyToUse"
/>
</div>
</div>
<Banner
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
color="error"
class="mb-20"
:label="value.volumeBackups.error.message"
/>
</div>
</template>

View File

@ -1,7 +1,6 @@
<script>
import { isEqual } from 'lodash';
import { mapGetters } from 'vuex';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import { Checkbox } from '@components/Form/Checkbox';

View File

@ -0,0 +1,41 @@
<script>
import { HCI } from '../types';
export default {
props: {
value: {
type: String,
default: ''
},
},
data() {
const scheduleList = this.$store.getters['harvester/all'](HCI.SCHEDULE_VM_BACKUP) || [];
return { scheduleList };
},
computed: {
vmSchedule() {
if (!this.value) {
return '';
} else {
return this.scheduleList.find(s => s.id === this.value);
}
},
to() {
return this.vmSchedule?.detailLocation;
},
}
};
</script>
<template>
<router-link v-if="to" :to="to">
{{ value }}
</router-link>
<span v-else-if="value">
{{ value }}
</span>
<span v-else class="text-muted">
&mdash;
</span>
</template>

View File

@ -0,0 +1,32 @@
<script>
import cronstrue from 'cronstrue';
export default {
props: {
value: {
type: String,
default: ''
},
},
computed: {
cronTooltipHint() {
let cronHint = '';
try {
cronHint = cronstrue.toString(this.value, { verbose: true });
} catch (e) {
cronHint = this.t('generic.invalidCron');
}
return cronHint || this.value.spec.cron;
}
}
};
</script>
<template>
<span v-clean-tooltip="cronTooltipHint">
{{ value }}
</span>
</template>

View File

@ -147,7 +147,7 @@ export default {
>
<template #title="{formattedPercentage}">
<span>
{{ t('harvester.formatters.hardwareResourceGauge.allocated') }}
{{ t('harvester.dashboard.hardwareResourceGauge.allocated') }}
</span>
<span class="precent-data">
{{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}

View File

@ -84,6 +84,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
@ -171,8 +177,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
@ -223,6 +232,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
@ -269,6 +284,7 @@ harvester:
promiscuous: Promiscuous
ipv4Address: IPv4 address
filterLabels: Filter labels
filterSchedule: Filter schedule
storageClass: Storage class
dockerImage: Docker image
pci:
@ -356,6 +372,7 @@ harvester:
cpu: CPU
memory: Memory
storage: Storage
allocated: Allocated
sections:
events:
label: Events
@ -590,6 +607,7 @@ harvester:
size: Size
edit: Edit
bus: Bus
readyToUse: Ready To Use
bootOrder: Boot Order
volume: Volume
dockerImage: Docker Image
@ -848,6 +866,37 @@ harvester:
doc: Read the <a href="{url}" target="_blank">documentation</a> 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
@ -886,7 +935,6 @@ harvester:
starting: Backup initiating
progress: Backup in progress
complete: Backup completed
restore:
progress:
details: Volume details
@ -1532,6 +1580,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 }

View File

@ -660,7 +660,7 @@ export default {
:name="t('harvester.dashboard.hardwareResourceGauge.storage')"
:used="storageUsed"
:reserved="storageAllocated"
:reserved-title="t('harvester.formatters.hardwareResourceGauge.allocated')"
:reserved-title="t('harvester.dashboard.hardwareResourceGauge.allocated')"
/>
</div>
</template>

View File

@ -0,0 +1,123 @@
<script>
import Loading from '@shell/components/Loading';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { HCI } from '../types';
import { allSettled } from '../utils/promise';
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
import { VM_SCHEDULE_CRON, VM_SCHEDULE_RETAIN, VM_SCHEDULE_TYPE, VM_SCHEDULE_MAX_FAILURE } from '../config/table-headers';
import { BACKUP_TYPE } from '../config/types';
export default {
name: 'HarvesterListSchedule',
components: {
ResourceTable, Loading, Masthead,
},
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 }),
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }),
});
this.rows = hash.rows;
},
data() {
const params = { ...this.$route.params };
const resource = params.resource;
return {
rows: [],
settings: [],
resource,
to: `${ HCI.SETTING }/backup-target?mode=edit`,
};
},
computed: {
headers() {
const cols = [
STATE,
NAME,
NAMESPACE,
{
name: 'sourceVm',
labelKey: 'harvester.tableHeaders.sourceVm',
value: 'spec.vmbackup.source.name',
sort: 'sourceVm',
align: 'center',
formatter: 'AttachVMWithName'
},
VM_SCHEDULE_TYPE,
VM_SCHEDULE_CRON,
VM_SCHEDULE_RETAIN,
VM_SCHEDULE_MAX_FAILURE,
AGE,
];
return cols;
},
filteredRows() {
return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT);
},
typeDisplay() {
return this.t('harvester.schedule.label');
}
},
methods: {
getRow(row) {
return row.spec?.vmbackup?.source?.name;
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="schema"
:resource="resource"
:type-display="typeDisplay"
:parentNameOverride="'Virtual Machine schedule'"
:create-button-label="t('harvester.schedule.createButtonText')"
/>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="true"
:rows="filteredRows"
:schema="schema"
key-field="_key"
default-sort-by="age"
>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="getRow(row)"
:to="row.detailLocation"
>
{{ row.nameDisplay }}
</router-link>
<span v-else>
{{ row.nameDisplay }}
</span>
</span>
</td>
</template>
</ResourceTable>
</div>
</template>

View File

@ -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"
>
<template #more-header-middle>
<FilterVMSchedule
:rows="getRawRows"
@change-rows="changeRows"
/>
</template>
<template #col:name="{row}">
<td>
<span>
<router-link
v-if="row.status && row.status.source"
v-if="getRow(row)"
:to="row.detailLocation"
>
{{ row.nameDisplay }}

View File

@ -2,14 +2,15 @@
import Loading from '@shell/components/Loading';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
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 { HCI } from '../types';
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['searchSchedule'] = searchSchedule;
this['snapshots'] = filteredRows;
},
sortGenerationFn() {
let base = defaultTableSortGenerationFn(this.schema, this.$store);
base += this.searchSchedule;
return base;
},
}
};
</script>
@ -100,17 +132,23 @@ export default {
:type-display="typeDisplay"
:create-button-label="t('harvester.vmSnapshot.createText')"
/>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="true"
:rows="filteredRows"
:schema="schema"
:sort-generation-fn="sortGenerationFn"
key-field="_key"
default-sort-by="age"
>
<template #more-header-middle>
<FilterVMSchedule
:rows="getRawRows"
@change-rows="changeRows"
/>
</template>
<template #col:name="{row}">
<td>
<span>
@ -126,6 +164,6 @@ export default {
</span>
</td>
</template>
</resourcetable>
</ResourceTable>
</div>
</template>

View File

@ -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', {});

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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',

View File

@ -0,0 +1,11 @@
import cronstrue from 'cronstrue';
export function isCronValid(schedule = '') {
try {
const hint = cronstrue.toString(schedule);
return !!hint;
} catch (e) {
return false;
}
}

View File

@ -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"