mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-03-22 05:01:45 +00:00
Compare commits
11 Commits
main
...
v1.8.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ea9be174 | ||
|
|
f8a479cfcf | ||
|
|
6d5e584113 | ||
|
|
a9eb04195e | ||
|
|
0441c1f6e3 | ||
|
|
dde58269d5 | ||
|
|
b8111f0ad7 | ||
|
|
6d627f82e9 | ||
|
|
cfc7a76fe7 | ||
|
|
71d3067354 | ||
|
|
9ecc372009 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.0-dev",
|
"version": "1.8.0-rc2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
104
pkg/harvester/components/settings/instance-manager-resources.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterInstanceManagerResources',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
UnitInput,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.value) return;
|
||||||
|
|
||||||
|
const cpu = { ...this.resources?.cpu };
|
||||||
|
|
||||||
|
if (cpu.v1 !== null) cpu.v1 = String(cpu.v1);
|
||||||
|
if (cpu.v2 !== null) cpu.v2 = String(cpu.v2);
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
this.resources = this.parseJSON(this.value.default) || {};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v1"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v1')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v2"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v2')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
371
pkg/harvester/components/settings/rwx-network.vue
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { NETWORK_TYPE } from '../../config/types';
|
||||||
|
|
||||||
|
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||||
|
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
||||||
|
const NETWORK = 'network';
|
||||||
|
|
||||||
|
const DEFAULT_DEDICATED_NETWORK = {
|
||||||
|
vlan: '',
|
||||||
|
clusterNetwork: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RwxNetworkSetting',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let enabled = false; // enabled / disabled options
|
||||||
|
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
||||||
|
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
let networkType = L2VLAN;
|
||||||
|
let exclude = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
||||||
|
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
||||||
|
|
||||||
|
if (parsedValue && typeof parsedValue === 'object') {
|
||||||
|
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
||||||
|
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
||||||
|
dedicatedNetwork = {
|
||||||
|
vlan: parsedNetwork.vlan || '',
|
||||||
|
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
||||||
|
range: parsedNetwork.range || '',
|
||||||
|
};
|
||||||
|
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
||||||
|
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
enabled = false;
|
||||||
|
shareStorageNetwork = false;
|
||||||
|
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
shareStorageNetwork,
|
||||||
|
dedicatedNetwork,
|
||||||
|
networkType,
|
||||||
|
exclude,
|
||||||
|
defaultAddValue: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showDedicatedNetworkConfig() {
|
||||||
|
return this.enabled && !this.shareStorageNetwork;
|
||||||
|
},
|
||||||
|
|
||||||
|
showVlan() {
|
||||||
|
return this.networkType === L2VLAN;
|
||||||
|
},
|
||||||
|
|
||||||
|
networkTypes() {
|
||||||
|
return [L2VLAN, UNTAGGED];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
||||||
|
|
||||||
|
return clusterNetworksOptions.map((network) => {
|
||||||
|
const disabled = !network.isReadyForStorageNetwork;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
||||||
|
value: network.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onUpdateEnabled() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateNetworkType() {
|
||||||
|
if (this.shareStorageNetwork) {
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateDedicatedType(neu) {
|
||||||
|
this.dedicatedNetwork.clusterNetwork = '';
|
||||||
|
|
||||||
|
if (neu === L2VLAN) {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
} else {
|
||||||
|
delete this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(neu) {
|
||||||
|
if (neu === '') {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = Number(neu);
|
||||||
|
|
||||||
|
if (newValue > 4094) {
|
||||||
|
this.dedicatedNetwork.vlan = 4094;
|
||||||
|
} else if (newValue < 1) {
|
||||||
|
this.dedicatedNetwork.vlan = 1;
|
||||||
|
} else {
|
||||||
|
this.dedicatedNetwork.vlan = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.enabled = false;
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const value = { [SHARE_STORAGE_NETWORK]: false };
|
||||||
|
|
||||||
|
if (this.enabled && this.shareStorageNetwork) {
|
||||||
|
value[SHARE_STORAGE_NETWORK] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showDedicatedNetworkConfig) {
|
||||||
|
value[NETWORK] = {};
|
||||||
|
|
||||||
|
if (this.showVlan) {
|
||||||
|
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
||||||
|
value[NETWORK].range = this.dedicatedNetwork.range;
|
||||||
|
|
||||||
|
const excludeList = this.exclude.filter((ip) => ip);
|
||||||
|
|
||||||
|
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
||||||
|
value[NETWORK].exclude = excludeList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
if (!this.showDedicatedNetworkConfig) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exclude) {
|
||||||
|
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
||||||
|
return cidr && !isValidCIDR(cidr);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInvalidCIDR) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="mode">
|
||||||
|
<Banner color="warning">
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.warning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-enable"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="onUpdateEnabled"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
v-if="enabled"
|
||||||
|
v-model:value="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-type"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
||||||
|
@update:value="onUpdateNetworkType"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<template v-if="showDedicatedNetworkConfig">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="networkType"
|
||||||
|
class="mb-20"
|
||||||
|
:options="networkTypes"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.fields.type')"
|
||||||
|
required
|
||||||
|
@update:value="onUpdateDedicatedType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="showVlan"
|
||||||
|
v-model:value.number="dedicatedNetwork.vlan"
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="dedicatedNetwork.clusterNetwork"
|
||||||
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="dedicatedNetwork.range"
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.storageNetwork.range.label"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
class="mt-20"
|
||||||
|
@update:value="update"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box mb-10">
|
||||||
|
<div class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -61,7 +61,10 @@ const FEATURE_FLAGS = {
|
|||||||
'v1.8.0': [
|
'v1.8.0': [
|
||||||
'hotplugCdRom',
|
'hotplugCdRom',
|
||||||
'supportBundleFileNameSetting',
|
'supportBundleFileNameSetting',
|
||||||
'clusterRegistrationTLSVerify'
|
'clusterRegistrationTLSVerify',
|
||||||
|
'vGPUAsPCIDevice',
|
||||||
|
'instanceManagerResourcesSetting',
|
||||||
|
'rwxNetworkSetting',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const HCI = {
|
|||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
|
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const HCI_SETTING = {
|
|||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
|
RWX_NETWORK: 'rwx-network',
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
@ -39,7 +40,8 @@ export const HCI_SETTING = {
|
|||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration'
|
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||||
|
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -80,6 +82,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||||
kind: 'custom', from: 'import', canReset: true
|
kind: 'custom', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.RWX_NETWORK]: {
|
||||||
|
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
||||||
|
},
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@ -122,6 +127,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
|
},
|
||||||
|
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||||
|
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
239
pkg/harvester/dialog/HarvesterStorageMigrationDialog.vue
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { PVC } from '@shell/config/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterStorageMigrationDialog',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sourceVolume: '',
|
||||||
|
targetVolume: '',
|
||||||
|
errors: [],
|
||||||
|
allPVCs: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceVolumeOptions() {
|
||||||
|
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
|
||||||
|
|
||||||
|
return sortBy(
|
||||||
|
volumes
|
||||||
|
.map((v) => v.persistentVolumeClaim?.claimName)
|
||||||
|
.filter((name) => !!name)
|
||||||
|
.map((name) => ({
|
||||||
|
label: name,
|
||||||
|
value: name
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
namespacePVCs() {
|
||||||
|
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
|
||||||
|
},
|
||||||
|
|
||||||
|
vmUsedVolumeNames() {
|
||||||
|
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
|
||||||
|
const names = new Set();
|
||||||
|
|
||||||
|
allVMs.forEach((vm) => {
|
||||||
|
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
|
||||||
|
const volumes = vm.spec?.template?.spec?.volumes || [];
|
||||||
|
|
||||||
|
volumes.forEach((v) => {
|
||||||
|
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
names.add(`${ vm.metadata.namespace }/${ name }`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect volume names from volumeClaimTemplates annotation
|
||||||
|
const templates = parseVolumeClaimTemplates(vm);
|
||||||
|
|
||||||
|
templates.forEach((t) => {
|
||||||
|
if (t.metadata?.name) {
|
||||||
|
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return names;
|
||||||
|
},
|
||||||
|
|
||||||
|
targetVolumeOptions() {
|
||||||
|
return sortBy(
|
||||||
|
this.namespacePVCs
|
||||||
|
.filter((pvc) => {
|
||||||
|
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
|
||||||
|
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((pvc) => ({
|
||||||
|
label: pvc.metadata.name,
|
||||||
|
value: pvc.metadata.name
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableSave() {
|
||||||
|
return !this.sourceVolume || !this.targetVolume;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.sourceVolume = '';
|
||||||
|
this.targetVolume = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonDone) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.sourceVolume) {
|
||||||
|
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.targetVolume) {
|
||||||
|
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
|
||||||
|
|
||||||
|
this['errors'] = [this.t('validation.required', { key: name })];
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.actionResource.doAction('storageMigration', {
|
||||||
|
sourceVolume: this.sourceVolume,
|
||||||
|
targetVolume: this.targetVolume
|
||||||
|
}, {}, false);
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
|
||||||
|
this['errors'] = exceptionToErrorsArray(error);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.storageMigration.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="sourceVolume"
|
||||||
|
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
|
||||||
|
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
|
||||||
|
:options="sourceVolumeOptions"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="targetVolume"
|
||||||
|
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
|
||||||
|
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
|
||||||
|
:options="targetVolumeOptions"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
#actions
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -31,6 +31,7 @@ export default {
|
|||||||
const _hash = {
|
const _hash = {
|
||||||
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
pciclaims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.PCI_CLAIM }),
|
||||||
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
sriovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOV }),
|
||||||
|
srigpuovs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await allHash(_hash);
|
await allHash(_hash);
|
||||||
@ -106,19 +107,32 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
parentSriovOptions() {
|
allSriovs() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const allSriovs = this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
|
||||||
|
|
||||||
return allSriovs.map((sriov) => {
|
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOV) || [];
|
||||||
return sriov.id;
|
},
|
||||||
});
|
allSriovGPUs() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || [];
|
||||||
|
},
|
||||||
|
parentSriovOptions() {
|
||||||
|
return this.allSriovs.map((sriov) => sriov.id);
|
||||||
|
},
|
||||||
|
parentSriovGPUOptions() {
|
||||||
|
return this.allSriovGPUs.map((sriovgpu) => sriovgpu.id);
|
||||||
},
|
},
|
||||||
parentSriovLabel() {
|
parentSriovLabel() {
|
||||||
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
return HCI_ANNOTATIONS.PARENT_SRIOV;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
parentSriovGPULabel() {
|
||||||
|
return HCI_ANNOTATIONS.PARENT_SRIOV_GPU;
|
||||||
|
},
|
||||||
|
vGPUAsPCIDeviceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
enableGroup(rows = []) {
|
enableGroup(rows = []) {
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
@ -206,6 +220,15 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
@change-rows="changeRows"
|
@change-rows="changeRows"
|
||||||
/>
|
/>
|
||||||
|
<FilterBySriov
|
||||||
|
v-if="vGPUAsPCIDeviceEnabled"
|
||||||
|
ref="filterByParentSRIOVGPU"
|
||||||
|
:parent-sriov-options="parentSriovGPUOptions"
|
||||||
|
:parent-sriov-label="parentSriovGPULabel"
|
||||||
|
:label="t('harvester.sriov.parentSriovGPU')"
|
||||||
|
:rows="rows"
|
||||||
|
@change-rows="changeRows"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ResourceTable>
|
</ResourceTable>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { set } from '@shell/utils/object';
|
|||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VirtualMachinePCIDevices',
|
name: 'VirtualMachinePCIDevices',
|
||||||
@ -15,7 +16,8 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
DeviceList,
|
DeviceList,
|
||||||
CompatibilityMatrix,
|
CompatibilityMatrix,
|
||||||
Banner
|
Banner,
|
||||||
|
MessageLink
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
@ -138,6 +140,13 @@ export default {
|
|||||||
return inUse;
|
return inUse;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toVGpuDevicesPage() {
|
||||||
|
return {
|
||||||
|
name: 'harvester-c-cluster-resource',
|
||||||
|
params: { cluster: this.$store.getters['clusterId'], resource: HCI.VGPU_DEVICE },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
devicesByNode() {
|
devicesByNode() {
|
||||||
return this.enabledDevices?.reduce((acc, device) => {
|
return this.enabledDevices?.reduce((acc, device) => {
|
||||||
const nodeName = device.status?.nodeName;
|
const nodeName = device.status?.nodeName;
|
||||||
@ -232,7 +241,12 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<Banner color="info">
|
<Banner color="info">
|
||||||
<t k="harvester.pci.howToUseDevice" />
|
<MessageLink
|
||||||
|
:to="toVGpuDevicesPage"
|
||||||
|
prefix-label="harvester.pci.howToUseDevice.prefix"
|
||||||
|
middle-label="harvester.pci.howToUseDevice.middle"
|
||||||
|
suffix-label="harvester.pci.howToUseDevice.suffix"
|
||||||
|
/>
|
||||||
</Banner>
|
</Banner>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="selectedDevices.length > 0"
|
v-if="selectedDevices.length > 0"
|
||||||
|
|||||||
@ -211,6 +211,10 @@ export default {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vGPUAsPCIDeviceEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
},
|
||||||
usbPassthroughEnabled() {
|
usbPassthroughEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('usbPassthrough');
|
||||||
},
|
},
|
||||||
@ -740,7 +744,7 @@ export default {
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="enabledSriovgpu"
|
v-if="enabledSriovgpu && !vGPUAsPCIDeviceEnabled"
|
||||||
:label="t('harvester.tab.vGpuDevices')"
|
:label="t('harvester.tab.vGpuDevices')"
|
||||||
name="vGpuDevices"
|
name="vGpuDevices"
|
||||||
:weight="-6"
|
:weight="-6"
|
||||||
|
|||||||
@ -123,6 +123,15 @@ harvester:
|
|||||||
namespace: Namespace
|
namespace: Namespace
|
||||||
message:
|
message:
|
||||||
success: 'Image { name } created successfully.'
|
success: 'Image { name } created successfully.'
|
||||||
|
storageMigration:
|
||||||
|
title: Storage Migration
|
||||||
|
fields:
|
||||||
|
sourceVolume:
|
||||||
|
label: Source Volume
|
||||||
|
placeholder: Select a source volume
|
||||||
|
targetVolume:
|
||||||
|
label: Target Volume
|
||||||
|
placeholder: Select a target volume
|
||||||
migration:
|
migration:
|
||||||
failedMessage: Latest migration failed!
|
failedMessage: Latest migration failed!
|
||||||
title: Migration
|
title: Migration
|
||||||
@ -232,6 +241,8 @@ harvester:
|
|||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
|
storageMigration: Storage Migration
|
||||||
|
cancelStorageMigration: Cancel Storage Migration
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
disableMaintenance: Disable Maintenance Mode
|
disableMaintenance: Disable Maintenance Mode
|
||||||
@ -355,7 +366,10 @@ harvester:
|
|||||||
available: Available Devices
|
available: Available Devices
|
||||||
compatibleNodes: Compatible Nodes
|
compatibleNodes: Compatible Nodes
|
||||||
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
||||||
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
|
howToUseDevice:
|
||||||
|
prefix: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.<br>For vGPU devices, please enable them on the'
|
||||||
|
middle: vGPU Devices
|
||||||
|
suffix: page first.
|
||||||
deviceInTheSameHost: 'You can only select devices on the same host.'
|
deviceInTheSameHost: 'You can only select devices on the same host.'
|
||||||
oldFormatDevices:
|
oldFormatDevices:
|
||||||
help: |-
|
help: |-
|
||||||
@ -425,7 +439,7 @@ harvester:
|
|||||||
volume:
|
volume:
|
||||||
upperType: Volume name
|
upperType: Volume name
|
||||||
lowerType: volume name
|
lowerType: volume name
|
||||||
needImageOrExisting: 'At least an image volume or an existing root-disk volume is required!'
|
needAtLeastOneBootable: 'At least one bootable volume is required!'
|
||||||
image:
|
image:
|
||||||
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
@ -1266,6 +1280,11 @@ harvester:
|
|||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
|
rwxNetwork:
|
||||||
|
warning: 'WARNING: <br/> Any change to rwx-network requires longhorn RWX volumes detached before applying this setting.<br/>Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
|
shareStorageNetwork: Share Storage Network
|
||||||
|
dedicatedRwxNetwork: Dedicated RWX Network
|
||||||
|
shareStorageNetworkWarning: The rwx-network is governed by storage-network, and changes here won't take effect until share-storage-network is set to false.
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
vmMigrationTimeout: VM Migration Timeout
|
vmMigrationTimeout: VM Migration Timeout
|
||||||
@ -1354,13 +1373,17 @@ harvester:
|
|||||||
url: URL
|
url: URL
|
||||||
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
insecureSkipTLSVerify: Insecure Skip TLS Verify
|
||||||
tip:
|
tip:
|
||||||
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure SKip TLS Verify", you must provide custom CA certificates using the
|
prefix: Harvester secures cluster registration via TLS by default. If opt out "Insecure Skip TLS Verify", you must provide custom CA certificates using the
|
||||||
middle: 'additional-ca'
|
middle: 'additional-ca'
|
||||||
suffix: setting.
|
suffix: setting.
|
||||||
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
|
||||||
ntpServers:
|
ntpServers:
|
||||||
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
isNotIPV4: The address you entered is not IPv4 or host. Please enter a valid IPv4 address or a host address.
|
||||||
isDuplicate: There are duplicate NTP server configurations.
|
isDuplicate: There are duplicate NTP server configurations.
|
||||||
|
instanceManagerResources:
|
||||||
|
parseError: "Failed to parse configuration: {error}"
|
||||||
|
v1: "V1 Data Engine"
|
||||||
|
v2: "V2 Data Engine"
|
||||||
kubevirtMigration:
|
kubevirtMigration:
|
||||||
parseError: "Failed to parse configuration: {error}"
|
parseError: "Failed to parse configuration: {error}"
|
||||||
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||||
@ -1832,7 +1855,8 @@ harvester:
|
|||||||
numVFs: Number Of Virtual Functions
|
numVFs: Number Of Virtual Functions
|
||||||
vfAddresses: Virtual Functions Addresses
|
vfAddresses: Virtual Functions Addresses
|
||||||
showMore: Show More
|
showMore: Show More
|
||||||
parentSriov: Filter By Parent SR-IOV
|
parentSriov: Filter By Parent SR-IOV Netork Device
|
||||||
|
parentSriovGPU: Filter By Parent SR-IOV GPU Device
|
||||||
|
|
||||||
sriovgpu:
|
sriovgpu:
|
||||||
label: SR-IOV GPU Devices
|
label: SR-IOV GPU Devices
|
||||||
@ -1979,6 +2003,7 @@ advancedSettings:
|
|||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
|
'harv-rwx-network': 'Configure RWX network behavior for shared or dedicated storage network usage.'
|
||||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
@ -1994,6 +2019,7 @@ advancedSettings:
|
|||||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
'harv-kubevirt-migration': 'Configure cluster-wide KubeVirt live migration parameters.'
|
||||||
|
'harv-instance-manager-resources': 'Configure resource percentage reservations for Longhorn instance manager V1 and V2. Valid instance manager CPU range between 0 - 40.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
|
|||||||
@ -707,18 +707,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
needVolume(R) {
|
needVolumeRelatedInfo(R) {
|
||||||
if (R.image === EMPTY_IMAGE) {
|
// return [needVolume, needVolumeClaimTemplate]
|
||||||
return false;
|
if (R.source === SOURCE_TYPE.CONTAINER) {
|
||||||
|
return [true, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (R.source === SOURCE_TYPE.IMAGE && R.image === EMPTY_IMAGE) {
|
||||||
|
return [false, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true, true];
|
||||||
},
|
},
|
||||||
|
|
||||||
parseDiskRows(disk) {
|
parseDiskRows(disk) {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
const diskNameLabels = [];
|
|
||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
@ -726,14 +730,18 @@ export default {
|
|||||||
|
|
||||||
disks.push(_disk);
|
disks.push(_disk);
|
||||||
|
|
||||||
if (this.needVolume(R)) {
|
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const prefixName = this.value.metadata?.name || '';
|
||||||
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
|
||||||
|
const [needVolume, needVolumeClaimTemplate] = this.needVolumeRelatedInfo(R);
|
||||||
|
|
||||||
|
if (needVolume) {
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
|
||||||
|
|
||||||
volumes.push(_volume);
|
volumes.push(_volume);
|
||||||
diskNameLabels.push(dataVolumeName);
|
}
|
||||||
|
if (needVolumeClaimTemplate) {
|
||||||
|
const _dataVolumeTemplate = this.parseVolumeClaimTemplate(R, dataVolumeName);
|
||||||
|
|
||||||
volumeClaimTemplates.push(_dataVolumeTemplate);
|
volumeClaimTemplates.push(_dataVolumeTemplate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import SteveModel from '@shell/plugins/steve/steve-class';
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
const STATUS_DISPLAY = {
|
const STATUS_DISPLAY = {
|
||||||
enabled: {
|
enabled: {
|
||||||
@ -32,7 +33,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.isEnabling,
|
enabled: !this.isEnabling && !this.isvGPUDevice,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable Passthrough',
|
label: 'Enable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -41,7 +42,7 @@ export default class PCIDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.isEnabling && this.claimedByMe,
|
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable Passthrough',
|
label: 'Disable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -52,6 +53,14 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isvGPUDevice() {
|
||||||
|
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!this.metadata?.labels?.[HCI_ANNOTATIONS.PARENT_SRIOV_GPU];
|
||||||
|
}
|
||||||
|
|
||||||
get canYaml() {
|
get canYaml() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -176,6 +185,10 @@ export default class PCIDevice extends SteveModel {
|
|||||||
return this.status?.description;
|
return this.status?.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get vGPUAsPCIDeviceFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('vGPUAsPCIDevice');
|
||||||
|
}
|
||||||
|
|
||||||
showDetachWarning() {
|
showDetachWarning() {
|
||||||
this.$dispatch('growl/warning', {
|
this.$dispatch('growl/warning', {
|
||||||
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
|
title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'),
|
||||||
|
|||||||
@ -199,6 +199,18 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
icon: 'icon icon-close',
|
icon: 'icon icon-close',
|
||||||
label: this.t('harvester.action.abortMigration')
|
label: this.t('harvester.action.abortMigration')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'storageMigration',
|
||||||
|
enabled: !!this.actions?.storageMigration,
|
||||||
|
icon: 'icon icon-copy',
|
||||||
|
label: this.t('harvester.action.storageMigration')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'cancelStorageMigration',
|
||||||
|
enabled: !!this.actions?.cancelStorageMigration,
|
||||||
|
icon: 'icon icon-close',
|
||||||
|
label: this.t('harvester.action.cancelStorageMigration')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'addHotplugVolume',
|
action: 'addHotplugVolume',
|
||||||
enabled: !!this.actions?.addVolume,
|
enabled: !!this.actions?.addVolume,
|
||||||
@ -368,6 +380,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageMigration(resources = this) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
component: 'HarvesterStorageMigrationDialog'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
backupVM(resources = this) {
|
backupVM(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -520,6 +539,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
this.doActionGrowl('abortMigration', {});
|
this.doActionGrowl('abortMigration', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelStorageMigration() {
|
||||||
|
this.doActionGrowl('cancelStorageMigration', {});
|
||||||
|
}
|
||||||
|
|
||||||
createTemplate(resources = this) {
|
createTemplate(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
@ -770,11 +793,11 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isPending() {
|
get isPending() {
|
||||||
if (this &&
|
if ((this &&
|
||||||
!this.isVMExpectedRunning &&
|
!this.isVMExpectedRunning &&
|
||||||
this.isVMCreated &&
|
this.isVMCreated &&
|
||||||
this.vmi?.status?.phase === VMIPhase.Pending
|
this.vmi?.status?.phase === VMIPhase.Pending
|
||||||
) {
|
) || (this.metadata?.annotations?.[HCI_ANNOTATIONS.CLONE_BACKEND_STORAGE_STATUS] === 'cloning')) {
|
||||||
return { status: VMIPhase.Pending };
|
return { status: VMIPhase.Pending };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.8.0-dev",
|
"version": "1.8.0-rc2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
||||||
});
|
});
|
||||||
|
|
||||||
let requiredVolume = false;
|
let hasBootableVolume = false;
|
||||||
|
|
||||||
_volumes.forEach((V, idx) => {
|
_volumes.forEach((V, idx) => {
|
||||||
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
||||||
@ -77,7 +77,7 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
const prefix = V.name || idx + 1;
|
const prefix = V.name || idx + 1;
|
||||||
|
|
||||||
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
||||||
requiredVolume = true;
|
hasBootableVolume = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
||||||
@ -137,10 +137,10 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* At least one volume must be create. (Verify only when create.)
|
* At least one bootable volume must be provided. (Verify only when create.)
|
||||||
*/
|
*/
|
||||||
if ((!requiredVolume || _volumes.length === 0) && !value.links) {
|
if (!hasBootableVolume && !value.links) {
|
||||||
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needImageOrExisting'));
|
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user