mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c85f510e | ||
|
|
f391f018de | ||
|
|
4b2e92ea15 | ||
|
|
2f956d5946 | ||
|
|
5f8d556ea2 | ||
|
|
396ab48f1c | ||
|
|
6b8c079018 | ||
|
|
7f638e86c8 | ||
|
|
0c4955a766 | ||
|
|
3f4ff30275 | ||
|
|
8e0332a364 | ||
|
|
6700b2055e | ||
|
|
e486852f7a | ||
|
|
c19341bec9 | ||
|
|
8cbb9d6b18 | ||
|
|
00f0953592 | ||
|
|
58507f0b2e | ||
|
|
7785d7f469 | ||
|
|
2c043e0a8e | ||
|
|
6f90cae482 | ||
|
|
b4980a51e7 | ||
|
|
756ed383ac | ||
|
|
7e0a9dcd80 | ||
|
|
5fae6c3087 | ||
|
|
a994d9861e |
@@ -41,8 +41,14 @@ jobs:
|
|||||||
FROM="$GITHUB_BASE_SHA"
|
FROM="$GITHUB_BASE_SHA"
|
||||||
TO="$GITHUB_HEAD_SHA"
|
TO="$GITHUB_HEAD_SHA"
|
||||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||||
FROM="$GITHUB_BEFORE"
|
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
||||||
TO="$GITHUB_AFTER"
|
# first push to HEAD
|
||||||
|
FROM=""
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
else
|
||||||
|
FROM="$GITHUB_BEFORE"
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "No valid commit range found, skipping commitlint."
|
echo "No valid commit range found, skipping commitlint."
|
||||||
exit 0
|
exit 0
|
||||||
@@ -51,7 +57,14 @@ jobs:
|
|||||||
echo "FROM=$FROM"
|
echo "FROM=$FROM"
|
||||||
echo "TO=$TO"
|
echo "TO=$TO"
|
||||||
|
|
||||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
if [ -z "$FROM" ]; then
|
||||||
|
echo "Linting last commit $TO"
|
||||||
|
npx commitlint --last --verbose
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Linting commits from $FROM to $TO"
|
||||||
|
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.7.0-dev",
|
"version": "1.7.0-rc4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
"@babel/plugin-transform-class-static-block": "7.28.3",
|
||||||
"@rancher/shell": "3.0.5-rc.8",
|
"@rancher/shell": "3.0.8-rc.8",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.21",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.2",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.14.0",
|
"qs": "6.14.0",
|
||||||
"roarr": "7.21.1",
|
"roarr": "7.21.2",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { MEBIBYTE } from '../../utils/unit';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KubevirtMigration',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
UnitInput,
|
||||||
|
RadioGroup,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
MEBIBYTE,
|
||||||
|
migration,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to parse JSON:', e.message);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue() {
|
||||||
|
if (this.value) {
|
||||||
|
this.value.value = JSON.stringify(this.migration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
const defaultMigration = this.parseJSON(this.value.default) || {};
|
||||||
|
|
||||||
|
this.migration = defaultMigration;
|
||||||
|
this.updateValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
this.updateValue();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="migration-field">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelMigrationsPerCluster"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.bandwidthPerMigration"
|
||||||
|
min="0"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="MEBIBYTE"
|
||||||
|
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.completionTimeoutPerGiB"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.progressTimeout"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
||||||
|
:key="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="mb-5"
|
||||||
|
:for="field"
|
||||||
|
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
||||||
|
<RadioGroup
|
||||||
|
:id="field"
|
||||||
|
v-model:value="migration[field]"
|
||||||
|
:options="[
|
||||||
|
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
||||||
|
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.migration-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,16 +18,20 @@ export default {
|
|||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
try {
|
if (this.customSupportBundleFeatureEnabled) {
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
try {
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
this.defaultNamespaces = response.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.defaultNamespaces = [];
|
this.defaultNamespaces = [];
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -42,24 +46,38 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
|
|
||||||
allNamespaces() {
|
allNamespaces() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
filteredNamespaces() {
|
filteredNamespaces() {
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return this.allNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
|
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||||
|
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return mappedNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
return [controlOption, ...this.filteredNamespaces];
|
return [controlOption, ...mappedNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export const DOC = {
|
|||||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||||
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
||||||
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
||||||
|
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ const FEATURE_FLAGS = {
|
|||||||
'v1.7.0': [
|
'v1.7.0': [
|
||||||
'vmMachineTypeAuto',
|
'vmMachineTypeAuto',
|
||||||
'lhV2VolExpansion',
|
'lhV2VolExpansion',
|
||||||
'l2VlanTrunkMode'
|
'l2VlanTrunkMode',
|
||||||
|
'kubevirtMigration',
|
||||||
|
'hotplugNic'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ export function init($plugin, store) {
|
|||||||
HCI.PCI_DEVICE,
|
HCI.PCI_DEVICE,
|
||||||
HCI.SR_IOVGPU_DEVICE,
|
HCI.SR_IOVGPU_DEVICE,
|
||||||
HCI.VGPU_DEVICE,
|
HCI.VGPU_DEVICE,
|
||||||
|
HCI.MIG_CONFIGURATION,
|
||||||
HCI.USB_DEVICE,
|
HCI.USB_DEVICE,
|
||||||
HCI.ADD_ONS,
|
HCI.ADD_ONS,
|
||||||
HCI.SECRET,
|
HCI.SECRET,
|
||||||
@@ -849,6 +850,26 @@ export function init($plugin, store) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.migconfiguration.label',
|
||||||
|
group: 'advanced',
|
||||||
|
weight: 12,
|
||||||
|
name: HCI.MIG_CONFIGURATION,
|
||||||
|
namespaced: false,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.MIG_CONFIGURATION }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.MIG_CONFIGURATION,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.MIG_CONFIGURATION, {
|
||||||
|
isCreatable: false,
|
||||||
|
hiddenNamespaceGroupButton: true,
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.usb.label',
|
labelKey: 'harvester.usb.label',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const HCI_SETTING = {
|
|||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
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'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -115,6 +116,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'profileName',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
||||||
|
value: 'name',
|
||||||
|
width: 75,
|
||||||
|
sort: 'name',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vGPUID',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
||||||
|
value: 'vGPUID',
|
||||||
|
width: 75,
|
||||||
|
sort: 'vGPUID',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'available',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
||||||
|
value: 'available',
|
||||||
|
width: 75,
|
||||||
|
sort: 'available',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requested',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
||||||
|
value: 'requested',
|
||||||
|
width: 75,
|
||||||
|
sort: 'requested',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
||||||
|
value: 'total',
|
||||||
|
width: 75,
|
||||||
|
sort: 'total',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
rows() {
|
||||||
|
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
||||||
|
const {
|
||||||
|
id, name, total, available
|
||||||
|
} = profile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
total,
|
||||||
|
available,
|
||||||
|
vGPUID: profile.vGPUID?.join(', ') || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
out = out.map((row) => {
|
||||||
|
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
||||||
|
|
||||||
|
return { ...row, requested };
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
:value="value"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Profile Status"
|
||||||
|
:label="t('harvester.migconfiguration.profileStatus')"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:headers="headers"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="condition"
|
||||||
|
default-sort-by="condition"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHugepages',
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
docsTransparentHugepagesLink() {
|
||||||
|
return DOC.TRANSPARENT_HUGEPAGES;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||||
|
|
||||||
|
this.hugepages = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
}) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { hugepages: {} };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="hugepages.status">
|
||||||
|
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.anon')"
|
||||||
|
:value="hugepages.status.meminfo.anonHugePages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.size')"
|
||||||
|
:value="hugepages.status.meminfo.hugepageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.total')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesTotal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.free')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesFree"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.rsvd')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesRsvd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.surp')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesSurp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>
|
||||||
|
<t
|
||||||
|
k="harvester.host.hugepages.transparent.title"
|
||||||
|
:raw="true"
|
||||||
|
:url="docsTransparentHugepagesLink"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.enabled')"
|
||||||
|
:value="hugepages.spec.transparent.enabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||||
|
:value="hugepages.spec.transparent.shmemEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.defrag')"
|
||||||
|
:value="hugepages.spec.transparent.defrag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
|
|||||||
import Disk from './HarvesterHostDisk';
|
import Disk from './HarvesterHostDisk';
|
||||||
import VlanStatus from './VlanStatus';
|
import VlanStatus from './VlanStatus';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||||
|
import HarvesterHugepages from './HarvesterHugepages.vue';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
|
||||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
@@ -46,6 +47,7 @@ export default {
|
|||||||
VlanStatus,
|
VlanStatus,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
|
HarvesterHugepages,
|
||||||
Loading,
|
Loading,
|
||||||
SortableTable,
|
SortableTable,
|
||||||
HarvesterSeeder,
|
HarvesterSeeder,
|
||||||
@@ -209,6 +211,12 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasHugepagesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||||
|
},
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||||
},
|
},
|
||||||
@@ -468,6 +476,16 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="hasHugepagesSchema"
|
||||||
|
name="hugepages"
|
||||||
|
:weight="0"
|
||||||
|
:show-header="false"
|
||||||
|
:label="t('harvester.host.tabs.hugepages')"
|
||||||
|
>
|
||||||
|
<HarvesterHugepages :node="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="seederEnabled"
|
v-if="seederEnabled"
|
||||||
name="seeder"
|
name="seeder"
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(neu);
|
const diskRows = this.getDiskRows(neu);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@@ -265,6 +266,7 @@ export default {
|
|||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
mode="view"
|
mode="view"
|
||||||
|
:vm="value"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
return this.modalData.title || 'dialog.promptRemove.title';
|
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||||
},
|
},
|
||||||
|
|
||||||
formattedType() {
|
formattedType() {
|
||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
warningMessage() {
|
warningMessage() {
|
||||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||||
|
|
||||||
const isPlural = this.type.endsWith('s');
|
const isPlural = this.type.endsWith('s');
|
||||||
const thisOrThese = isPlural ? 'these' : 'this';
|
const thisOrThese = isPlural ? 'these' : 'this';
|
||||||
@@ -145,6 +145,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
|
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AddHotplugNic',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
this.allVMNetworks = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
interfaceName: '',
|
||||||
|
networkName: '',
|
||||||
|
macAddress: '',
|
||||||
|
allVMNetworks: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources?.[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
return this.interfaceName !== '' && this.networkName !== '';
|
||||||
|
},
|
||||||
|
|
||||||
|
vmNetworksOption() {
|
||||||
|
return this.allVMNetworks
|
||||||
|
.filter((network) => {
|
||||||
|
const labels = network.metadata?.labels || {};
|
||||||
|
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
||||||
|
|
||||||
|
const isValidType = [
|
||||||
|
NETWORK_TYPE.L2VLAN,
|
||||||
|
NETWORK_TYPE.UNTAGGED,
|
||||||
|
NETWORK_TYPE.L2TRUNK_VLAN,
|
||||||
|
].includes(type);
|
||||||
|
|
||||||
|
return isValidType && !network.isSystem;
|
||||||
|
})
|
||||||
|
.map((network) => {
|
||||||
|
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
||||||
|
|
||||||
|
return ({
|
||||||
|
label,
|
||||||
|
value: network.id || '',
|
||||||
|
disabled: network.isNotReady,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.interfaceName = '';
|
||||||
|
this.networkName = '';
|
||||||
|
this.macAddress = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
interfaceName: this.interfaceName,
|
||||||
|
networkName: this.networkName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.macAddress) {
|
||||||
|
payload.macAddress = this.macAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('addNic', payload);
|
||||||
|
|
||||||
|
if ([200, 204].includes(res?._status)) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.hotplugNic.success', {
|
||||||
|
interfaceName: this.interfaceName,
|
||||||
|
vm: this.actionResource.nameDisplay
|
||||||
|
})
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
this.errors = exceptionToErrorsArray(res);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card
|
||||||
|
ref="modal"
|
||||||
|
name="modal"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="interfaceName"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="networkName"
|
||||||
|
class="mt-20"
|
||||||
|
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
||||||
|
:options="vmNetworksOption"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="macAddress"
|
||||||
|
class="mt-20"
|
||||||
|
label-key="harvester.modal.hotplugNic.macAddress"
|
||||||
|
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
:label="err"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+4
-4
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HotplugModal',
|
name: 'HotplugVolumeModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !pvc.attachVM;
|
return true;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
@@ -90,7 +90,7 @@ export default {
|
|||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.modal.hotplug.title')"
|
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
+38
-12
@@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplugModal',
|
name: 'HarvesterHotUnplug',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -35,8 +34,25 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
diskName() {
|
|
||||||
return this.modalData.diskName;
|
name() {
|
||||||
|
return this.modalData.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
isVolume() {
|
||||||
|
return this.modalData.type === 'volume';
|
||||||
|
},
|
||||||
|
|
||||||
|
titleKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
||||||
|
},
|
||||||
|
|
||||||
|
actionLabelKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
||||||
|
},
|
||||||
|
|
||||||
|
successMessageKey() {
|
||||||
|
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -47,14 +63,20 @@ export default {
|
|||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
try {
|
try {
|
||||||
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
let res;
|
||||||
|
|
||||||
|
if (this.isVolume) {
|
||||||
|
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
||||||
|
} else {
|
||||||
|
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
||||||
|
}
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'growl/success',
|
'growl/success',
|
||||||
{
|
{
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
message: this.t(this.successMessageKey, { name: this.name })
|
||||||
},
|
},
|
||||||
{ root: true }
|
{ root: true }
|
||||||
);
|
);
|
||||||
@@ -64,14 +86,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
this['errors'] = error;
|
this.errors = error;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.data || err;
|
const error = err?.data || err;
|
||||||
const message = exceptionToErrorsArray(error);
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
this['errors'] = message;
|
this.errors = message;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +109,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
v-clean-html="t(titleKey, { name })"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
@@ -111,9 +133,9 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:action-label="t(actionLabelKey)"
|
||||||
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:waiting-label="t(actionLabelKey)"
|
||||||
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
:success-label="t(actionLabelKey)"
|
||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,4 +154,8 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.card-title) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditMIGConfiguration',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
LabelValue
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { profileSpec } = this.value.spec;
|
||||||
|
|
||||||
|
return { profileSpec: profileSpec || [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === 'view';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateBeforeSave() {
|
||||||
|
// MIGConfiguration CRD don't have any namespace field,
|
||||||
|
// so we need to remove the namespace field before saving
|
||||||
|
delete this.value.metadata.namespace;
|
||||||
|
// enable the MIGConfiguration when saving
|
||||||
|
this.value.spec.enabled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
labelTitle(profile) {
|
||||||
|
return `${ profile.name } (available : ${ this.available(profile) })`;
|
||||||
|
},
|
||||||
|
|
||||||
|
available(profile) {
|
||||||
|
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
|
||||||
|
|
||||||
|
return count || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRequested(neu, profile) {
|
||||||
|
if (neu === null || neu === '') return;
|
||||||
|
const newValue = Number(neu);
|
||||||
|
const availableCount = this.available(profile);
|
||||||
|
|
||||||
|
if (newValue < 0) {
|
||||||
|
profile.requested = 0;
|
||||||
|
} else if ( newValue > availableCount ) {
|
||||||
|
profile.requested = availableCount;
|
||||||
|
} else {
|
||||||
|
profile.requested = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
finish-button-mode="enable"
|
||||||
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="profileSpec"
|
||||||
|
:label="t('harvester.migconfiguration.profileSpec')"
|
||||||
|
:weight="1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(profile, index) in profileSpec"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:value="labelTitle(profile)"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="profile.requested"
|
||||||
|
:min="0"
|
||||||
|
:disabled="isView"
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:label="`${t('harvester.migconfiguration.requested')}`"
|
||||||
|
@update:value="updateRequested($event, profile)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
|
||||||
|
export const hugepagesTHPEnabledMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Madvise',
|
||||||
|
value: 'madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const hugepagesTHPShmemEnabledMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Within Size',
|
||||||
|
value: 'within_size',
|
||||||
|
}, {
|
||||||
|
label: 'Advise',
|
||||||
|
value: 'advise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never',
|
||||||
|
}, {
|
||||||
|
label: 'Deny',
|
||||||
|
value: 'deny',
|
||||||
|
}, {
|
||||||
|
label: 'Force',
|
||||||
|
value: 'force',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const hugepagesTHPDefragMode = [{
|
||||||
|
label: 'Always',
|
||||||
|
value: 'always',
|
||||||
|
}, {
|
||||||
|
label: 'Defer',
|
||||||
|
value: 'defer',
|
||||||
|
}, {
|
||||||
|
label: 'Defer+Madvise',
|
||||||
|
value: 'defer+madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Madvise',
|
||||||
|
value: 'madvise',
|
||||||
|
}, {
|
||||||
|
label: 'Never',
|
||||||
|
value: 'never'
|
||||||
|
}];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHugepages',
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
docsTransparentHugepagesLink() {
|
||||||
|
return DOC.TRANSPARENT_HUGEPAGES;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveHugepages() {
|
||||||
|
this.hugepages['spec'] = this.spec;
|
||||||
|
|
||||||
|
await this.hugepages.save().catch((reason) => {
|
||||||
|
if (reason?.type === 'error') {
|
||||||
|
this.$store.dispatch('growl/error', {
|
||||||
|
title: this.t('harvester.notification.title.error'),
|
||||||
|
message: reason?.message,
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
return Promise.reject(new Error('saveHugepages error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||||
|
|
||||||
|
this.hugepages = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
});
|
||||||
|
this.spec = this.hugepages.spec;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hugepages: {},
|
||||||
|
spec: { transparent: {} },
|
||||||
|
hugepagesTHPEnabledMode,
|
||||||
|
hugepagesTHPShmemEnabledMode,
|
||||||
|
hugepagesTHPDefragMode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>
|
||||||
|
<t
|
||||||
|
k="harvester.host.hugepages.transparent.title"
|
||||||
|
:raw="true"
|
||||||
|
:url="docsTransparentHugepagesLink"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.enabled"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.enabled')"
|
||||||
|
:options="hugepagesTHPEnabledMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.shmemEnabled"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||||
|
:options="hugepagesTHPShmemEnabledMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="spec.transparent.defrag"
|
||||||
|
:label="t('harvester.host.hugepages.transparent.defrag')"
|
||||||
|
:options="hugepagesTHPDefragMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -28,6 +28,7 @@ import { HCI } from '../../types';
|
|||||||
import HarvesterDisk from './HarvesterDisk';
|
import HarvesterDisk from './HarvesterDisk';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||||
|
import HarvesterHugepages from './HarvesterHugepages';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
@@ -50,6 +51,7 @@ export default {
|
|||||||
ArrayListGrouped,
|
ArrayListGrouped,
|
||||||
HarvesterDisk,
|
HarvesterDisk,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
|
HarvesterHugepages,
|
||||||
ButtonDropdown,
|
ButtonDropdown,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -225,6 +227,12 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasHugepagesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||||
|
},
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@@ -647,6 +655,17 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayListGrouped>
|
</ArrayListGrouped>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasHugepagesSchema"
|
||||||
|
name="Hugepages"
|
||||||
|
:weight="70"
|
||||||
|
:label="t('harvester.host.tabs.hugepages')"
|
||||||
|
>
|
||||||
|
<HarvesterHugepages
|
||||||
|
:node="value"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="hasKsmtunedSchema"
|
v-if="hasKsmtunedSchema"
|
||||||
name="Ksmtuned"
|
name="Ksmtuned"
|
||||||
|
|||||||
@@ -296,4 +296,9 @@ export default {
|
|||||||
:deep() .edit-help code {
|
:deep() .edit-help code {
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.banner__content.closable) {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export default {
|
|||||||
<div class="key">
|
<div class="key">
|
||||||
<input
|
<input
|
||||||
v-model="scope.row.value"
|
v-model="scope.row.value"
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
@@ -14,6 +13,13 @@ export default {
|
|||||||
components: { InfoBox, Base },
|
components: { InfoBox, Base },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
vm: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
@@ -32,10 +38,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.fetchHotunplugData();
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: this.addKeyId(clone(this.value)),
|
nameIdx: 1,
|
||||||
nameIdx: 1
|
rows: this.addKeyId(clone(this.value)),
|
||||||
|
hotunpluggableNics: new Set(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,15 +75,57 @@ export default {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canCheckHotunplug() {
|
||||||
|
return !!this.vm?.actions?.findHotunpluggableNics;
|
||||||
|
},
|
||||||
|
|
||||||
|
vmState() {
|
||||||
|
return this.vm?.stateDisplay;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.rows = neu;
|
this.rows = this.mergeHotplugData(clone(neu));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vmState(newState, oldState) {
|
||||||
|
if (newState !== oldState) {
|
||||||
|
this.fetchHotunplugData();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchHotunplugData() {
|
||||||
|
if (!this.canCheckHotunplug) {
|
||||||
|
this.rows = this.mergeHotplugData(clone(this.value));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await this.vm.doAction('findHotunpluggableNics');
|
||||||
|
|
||||||
|
this.hotunpluggableNics = new Set(resp?.interfaces || []);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to fetch hot-unpluggable NICs:', e);
|
||||||
|
this.hotunpluggableNics = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rows = this.mergeHotplugData(clone(this.value));
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeHotplugData(networks) {
|
||||||
|
return (networks || []).map((network) => ({
|
||||||
|
...network,
|
||||||
|
isHotunpluggable: this.hotunpluggableNics.has(network.name),
|
||||||
|
rowKeyId: network.rowKeyId || randomStr(10)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
add(type) {
|
add(type) {
|
||||||
const name = this.generateName();
|
const name = this.generateName();
|
||||||
|
|
||||||
@@ -118,7 +171,11 @@ export default {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.$emit('update:value', this.rows);
|
this.$emit('update:value', this.rows);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
unplugNIC(network) {
|
||||||
|
this.vm.unplugNIC(network.name);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -129,17 +186,28 @@ export default {
|
|||||||
v-for="(row, i) in rows"
|
v-for="(row, i) in rows"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<button
|
<div class="box-title mb-10">
|
||||||
v-if="!isView"
|
<h3>
|
||||||
type="button"
|
{{ t('harvester.virtualMachine.network.title') }}
|
||||||
class="role-link remove-vol"
|
</h3>
|
||||||
@click="remove(row)"
|
<button
|
||||||
>
|
v-if="!isView"
|
||||||
<i class="icon icon-x" />
|
type="button"
|
||||||
</button>
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="remove(row)"
|
||||||
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
:disabled="!canCheckHotunplug"
|
||||||
|
@click="unplugNIC(row)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<Base
|
<Base
|
||||||
v-model:value="rows[i]"
|
v-model:value="rows[i]"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
@@ -162,16 +230,13 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.infoBox{
|
.box-title{
|
||||||
position: relative;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.remove-vol {
|
h3 {
|
||||||
position: absolute;
|
margin-bottom: 0;
|
||||||
top: 10px;
|
}
|
||||||
right: 16px;
|
|
||||||
padding:0px;
|
|
||||||
max-height: 28px;
|
|
||||||
min-height: 28px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -303,55 +303,57 @@ export default {
|
|||||||
v-for="(volume, i) in rows"
|
v-for="(volume, i) in rows"
|
||||||
:key="volume.id"
|
:key="volume.id"
|
||||||
>
|
>
|
||||||
<InfoBox class="box">
|
<InfoBox>
|
||||||
<button
|
<div class="box-title mb-10">
|
||||||
v-if="!isView"
|
<h3>
|
||||||
type="button"
|
<span
|
||||||
class="role-link btn btn-sm remove"
|
v-if="volume.to && isVirtualType"
|
||||||
@click="removeVolume(volume)"
|
class="title"
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="volume.hotpluggable && isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn remove"
|
|
||||||
@click="unplugVolume(volume)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
|
||||||
</button>
|
|
||||||
<h3>
|
|
||||||
<span
|
|
||||||
v-if="volume.to && isVirtualType"
|
|
||||||
class="title"
|
|
||||||
>
|
|
||||||
<router-link :to="volume.to">
|
|
||||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<BadgeStateFormatter
|
|
||||||
v-if="volume.pvc"
|
|
||||||
class="ml-10 state"
|
|
||||||
:arbitrary="true"
|
|
||||||
:row="volume.pvc"
|
|
||||||
:value="volume.pvc.state"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
|
||||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
|
||||||
class="ml-5 resource-external"
|
|
||||||
rel="nofollow noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
:href="volume.pvc.resourceExternalLink.url"
|
|
||||||
>
|
>
|
||||||
<i class="icon icon-external-link" />
|
<router-link :to="volume.to">
|
||||||
</a>
|
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||||
</span>
|
</router-link>
|
||||||
|
|
||||||
<span v-else>
|
<BadgeStateFormatter
|
||||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
v-if="volume.pvc"
|
||||||
</span>
|
class="ml-10 state"
|
||||||
</h3>
|
:arbitrary="true"
|
||||||
|
:row="volume.pvc"
|
||||||
|
:value="volume.pvc.state"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||||
|
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||||
|
class="ml-5 resource-external"
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
:href="volume.pvc.resourceExternalLink.url"
|
||||||
|
>
|
||||||
|
<i class="icon icon-external-link" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>
|
||||||
|
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
v-if="!isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="removeVolume(volume)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="volume.hotpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
@click="unplugVolume(volume)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<component
|
<component
|
||||||
:is="componentFor(volume.source)"
|
:is="componentFor(volume.source)"
|
||||||
@@ -495,25 +497,24 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box {
|
.box-title {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.state {
|
.state {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bootOrder {
|
.bootOrder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+6
-8
@@ -99,7 +99,6 @@ export default {
|
|||||||
this.allPVCs
|
this.allPVCs
|
||||||
.filter( (pvc) => {
|
.filter( (pvc) => {
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
let isBeingUsed = false;
|
|
||||||
|
|
||||||
this.rows.forEach( (O) => {
|
this.rows.forEach( (O) => {
|
||||||
if (O.volumeName === pvc.metadata.name) {
|
if (O.volumeName === pvc.metadata.name) {
|
||||||
@@ -111,17 +110,16 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// already used as image volume
|
||||||
|
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (pvc.isGoldenImageVolume) {
|
if (pvc.isGoldenImageVolume) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
return isAvailable && pvc.isAvailable;
|
||||||
isBeingUsed = false;
|
|
||||||
} else if (pvc.attachVM) {
|
|
||||||
isBeingUsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -490,6 +490,7 @@ export default {
|
|||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
if (!this.value.spec.template.spec.hostname) {
|
if (!this.value.spec.template.spec.hostname) {
|
||||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
|
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export default {
|
|||||||
return this.mode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
disableAdd() {
|
||||||
|
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
|
||||||
|
},
|
||||||
|
|
||||||
showAdd() {
|
showAdd() {
|
||||||
return !this.isView;
|
return !this.isView;
|
||||||
},
|
},
|
||||||
@@ -141,6 +145,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:disabled="disableAdd"
|
||||||
class="btn role-tertiary add"
|
class="btn role-tertiary add"
|
||||||
@click="add()"
|
@click="add()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -66,17 +66,26 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const out = (this.filteredNamespaces || []).map((namespace) => {
|
const namespaces = this.filteredNamespaces;
|
||||||
return {
|
const selected = this.rows.map((r) => r?.namespace);
|
||||||
label: namespace.metadata.name,
|
|
||||||
value: namespace.id,
|
if (this.isStandaloneHarvester) {
|
||||||
};
|
return [
|
||||||
});
|
{ label: this.t('generic.all'), value: '*' },
|
||||||
|
...namespaces.map((ns) => ({
|
||||||
|
label: ns.metadata.name,
|
||||||
|
value: ns.id,
|
||||||
|
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
label: this.t('generic.all'),
|
label: this.t('generic.all'),
|
||||||
value: '*',
|
value: '*',
|
||||||
}, ...out];
|
},
|
||||||
|
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
|
||||||
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
guestClusterOptions() {
|
guestClusterOptions() {
|
||||||
|
|||||||
@@ -154,15 +154,19 @@ harvester:
|
|||||||
nodeTimeout:
|
nodeTimeout:
|
||||||
label: Node Collection Timeout
|
label: Node Collection Timeout
|
||||||
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||||
hotplug:
|
hotplugVolume:
|
||||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||||
title: Add Volume
|
title: Add Volume
|
||||||
|
hotplugNic:
|
||||||
|
success: 'The settings have been saved, but the network interface {interfaceName} will be attached only after the virtual machine is migrated.'
|
||||||
|
title: Add Network Interface
|
||||||
|
vmNetwork: Virtual Machine Network
|
||||||
|
macAddress: MAC Address
|
||||||
|
macAddressTooltip: If left blank, the MAC address will be automatically generated.
|
||||||
cpuMemoryHotplug:
|
cpuMemoryHotplug:
|
||||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||||
title: Edit CPU and Memory
|
title: Edit CPU and Memory
|
||||||
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||||
hotunplug:
|
|
||||||
success: 'Volume { name } is detached successfully.'
|
|
||||||
snapshot:
|
snapshot:
|
||||||
title: Take Snapshot
|
title: Take Snapshot
|
||||||
name: Name
|
name: Name
|
||||||
@@ -226,7 +230,8 @@ harvester:
|
|||||||
disableCPUManager: Disable CPU Manager
|
disableCPUManager: Disable CPU Manager
|
||||||
cordon: Cordon
|
cordon: Cordon
|
||||||
uncordon: Uncordon
|
uncordon: Uncordon
|
||||||
addHotplug: Add Volume
|
addHotplugVolume: Add Volume
|
||||||
|
addHotplugNic: Hotplug Network Interface
|
||||||
exportImage: Export Image
|
exportImage: Export Image
|
||||||
viewlogs: View Logs
|
viewlogs: View Logs
|
||||||
cancelExpand: Cancel Expand
|
cancelExpand: Cancel Expand
|
||||||
@@ -272,6 +277,7 @@ harvester:
|
|||||||
cronExpression: Cron Expression
|
cronExpression: Cron Expression
|
||||||
retain: Retain
|
retain: Retain
|
||||||
scheduleType: Type
|
scheduleType: Type
|
||||||
|
configuredProfiles: Configured Profiles
|
||||||
maxFailure: Max Failure
|
maxFailure: Max Failure
|
||||||
sourceVm: Source Virtual Machine
|
sourceVm: Source Virtual Machine
|
||||||
vmSchedule: Virtual Machine Schedule
|
vmSchedule: Virtual Machine Schedule
|
||||||
@@ -441,6 +447,7 @@ harvester:
|
|||||||
storage: Storage
|
storage: Storage
|
||||||
labels: Labels
|
labels: Labels
|
||||||
ksmtuned: Ksmtuned
|
ksmtuned: Ksmtuned
|
||||||
|
hugepages: Hugepages
|
||||||
seeder: Out-of-band Access
|
seeder: Out-of-band Access
|
||||||
detail:
|
detail:
|
||||||
kvm:
|
kvm:
|
||||||
@@ -513,6 +520,20 @@ harvester:
|
|||||||
fullScans: Full Scans
|
fullScans: Full Scans
|
||||||
stableNodeChains: Stable Node Chains
|
stableNodeChains: Stable Node Chains
|
||||||
stableNodeDups: Stable Node Dups
|
stableNodeDups: Stable Node Dups
|
||||||
|
hugepages:
|
||||||
|
meminfo: Meminfo
|
||||||
|
transparent:
|
||||||
|
title: Transparent Hugepages <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
|
||||||
|
enabled: Enabled
|
||||||
|
shmemEnabled: Shared Memory Enabled
|
||||||
|
defrag: Defragmentation
|
||||||
|
status:
|
||||||
|
anon: Anonymous Hugepages (bytes)
|
||||||
|
size: Default Hugepage Size (bytes)
|
||||||
|
total: Total Hugepages
|
||||||
|
free: Free Hugepages
|
||||||
|
rsvd: Reserved Hugepages
|
||||||
|
surp: Surplus Hugepages
|
||||||
disk:
|
disk:
|
||||||
add: Add Disk
|
add: Add Disk
|
||||||
path:
|
path:
|
||||||
@@ -581,6 +602,16 @@ harvester:
|
|||||||
title: Enable CPU and memory hotplug
|
title: Enable CPU and memory hotplug
|
||||||
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||||
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||||
|
hotUnplug:
|
||||||
|
actionLabel: Detach
|
||||||
|
detachVolume:
|
||||||
|
title: 'Are you sure that you want to detach volume {name}?'
|
||||||
|
actionLabel: Detach Volume
|
||||||
|
success: 'Volume { name } is detached successfully.'
|
||||||
|
detachNIC:
|
||||||
|
title: 'Are you sure that you want to detach network interface {name}?'
|
||||||
|
actionLabel: Detach Network Interface
|
||||||
|
success: 'The settings have been saved, but the network interface {name} will be detached only after the virtual machine is migrated.'
|
||||||
instance:
|
instance:
|
||||||
singleInstance:
|
singleInstance:
|
||||||
multipleInstance:
|
multipleInstance:
|
||||||
@@ -612,11 +643,6 @@ harvester:
|
|||||||
title: 'Select the volume you want to delete:'
|
title: 'Select the volume you want to delete:'
|
||||||
deleteAll: Delete All
|
deleteAll: Delete All
|
||||||
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
|
tips: "Warn: The snapshots of the virtual machine will be deleted with virtual machine and the snapshots of volume will be deleted with volume."
|
||||||
unplug:
|
|
||||||
title: 'Are you sure that you want to detach volume {name} ?'
|
|
||||||
actionLabel: Detach
|
|
||||||
detachVolume:
|
|
||||||
Detach Volume
|
|
||||||
restartTip: |-
|
restartTip: |-
|
||||||
{restart, select,
|
{restart, select,
|
||||||
true {Restart}
|
true {Restart}
|
||||||
@@ -1063,6 +1089,8 @@ harvester:
|
|||||||
placeholder: e.g. 172.16.0.0/16
|
placeholder: e.g. 172.16.0.0/16
|
||||||
excludeIPs:
|
excludeIPs:
|
||||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
|
placeholder: single IP or 192.168.0.100..192.168.0.200
|
||||||
|
|
||||||
acl:
|
acl:
|
||||||
label: Access Control List
|
label: Access Control List
|
||||||
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
|
||||||
@@ -1178,7 +1206,7 @@ harvester:
|
|||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude IPs
|
label: Exclude IPs
|
||||||
placeholder: e.g. 172.16.0.1
|
placeholder: CIDR format, e.g. 172.16.0.10/32
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
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.'
|
||||||
@@ -1268,6 +1296,21 @@ harvester:
|
|||||||
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.
|
||||||
|
kubevirtMigration:
|
||||||
|
parseError: "Failed to parse configuration: {error}"
|
||||||
|
parallelMigrationsPerCluster: "Parallel Migrations Per Cluster"
|
||||||
|
parallelOutboundMigrationsPerNode: "Parallel Outbound Migrations Per Node"
|
||||||
|
bandwidthPerMigration: "Bandwidth Per Migration"
|
||||||
|
bandwidthPerMigrationTooltip: '0 is KubeVirt default, representing unlimited bandwidth'
|
||||||
|
completionTimeoutPerGiB: "Completion Timeout Per GiB"
|
||||||
|
progressTimeout: "Progress Timeout"
|
||||||
|
allowAutoConverge: "Allow Auto Converge"
|
||||||
|
allowPostCopy: "Allow Post Copy"
|
||||||
|
unsafeMigrationOverride: "Unsafe Migration Override"
|
||||||
|
allowWorkloadDisruption: "Allow Workload Disruption"
|
||||||
|
disableTLS: "Disable TLS"
|
||||||
|
matchSELinuxLevelOnMigration: "Match SELinux Level On Migration"
|
||||||
|
|
||||||
cloudTemplate:
|
cloudTemplate:
|
||||||
label: Cloud Configuration Templates
|
label: Cloud Configuration Templates
|
||||||
templateType: Template Type
|
templateType: Template Type
|
||||||
@@ -1664,6 +1707,26 @@ harvester:
|
|||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your SR-IOV GPU devices.
|
suffix: to enable it to manage your SR-IOV GPU devices.
|
||||||
|
|
||||||
|
migconfiguration:
|
||||||
|
label: vGPU MIG Configurations
|
||||||
|
infoBanner: To configure the MIG configuration, please disable it first and re-enable after editing the configuration.
|
||||||
|
profileSpec: Profile Specs
|
||||||
|
profileStatus: Profile Status
|
||||||
|
tableHeaders:
|
||||||
|
profileName: Profile Name
|
||||||
|
total: Total
|
||||||
|
vGPUID: vGPU ID
|
||||||
|
available: Available
|
||||||
|
requested: Requested
|
||||||
|
requested: Requested
|
||||||
|
available: Available
|
||||||
|
total: Total
|
||||||
|
vGPUID: vGPU ID
|
||||||
|
goSriovGPU:
|
||||||
|
prefix: Please enable the supported GPU devices in
|
||||||
|
middle: SR-IOV GPU Devices
|
||||||
|
suffix: page to manage the vGPU MIG configurations.
|
||||||
|
|
||||||
vgpu:
|
vgpu:
|
||||||
label: vGPU Devices
|
label: vGPU Devices
|
||||||
noPermission: Please contact system administrator to add Harvester add-ons first.
|
noPermission: Please contact system administrator to add Harvester add-ons first.
|
||||||
@@ -1782,6 +1845,7 @@ advancedSettings:
|
|||||||
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||||
'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.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@@ -1937,6 +2001,12 @@ typeLabel:
|
|||||||
other { vGPU Devices }
|
other { vGPU Devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
devices.harvesterhci.io.migconfiguration: |-
|
||||||
|
{count, plural,
|
||||||
|
one { vGPU MIG Configuration }
|
||||||
|
other { vGPU MIG Configurations }
|
||||||
|
}
|
||||||
|
|
||||||
harvesterhci.io.secret: |-
|
harvesterhci.io.secret: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Secret }
|
one { Secret }
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<script>
|
||||||
|
import { STATE, NAME } from '@shell/config/table-headers';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListMIGConfigurations',
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
Loading,
|
||||||
|
ResourceTable,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.MIG_CONFIGURATION);
|
||||||
|
this.hasAddonSchema = this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
||||||
|
|
||||||
|
if (this.hasSchema) {
|
||||||
|
try {
|
||||||
|
const hash = await allHash({
|
||||||
|
migconfigs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.MIG_CONFIGURATION }),
|
||||||
|
vGpuDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VGPU_DEVICE }),
|
||||||
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS })
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hasPCIAddon = hash.addons.find((addon) => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
|
||||||
|
this.hasSriovgpuAddon = hash.addons.find((addon) => addon.name === ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER)?.spec?.enabled === true;
|
||||||
|
this.hasSRIOVGPUSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.SR_IOVGPU_DEVICE);
|
||||||
|
|
||||||
|
if (this.hasSRIOVGPUSchema) {
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SR_IOVGPU_DEVICE });
|
||||||
|
}
|
||||||
|
this.rows = hash.migconfigs;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
schema: null,
|
||||||
|
hasAddonSchema: false,
|
||||||
|
hasPCIAddon: false,
|
||||||
|
hasSriovgpuAddon: false,
|
||||||
|
hasSRIOVGPUSchema: false,
|
||||||
|
toVGpuAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
||||||
|
toPciAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.PCI_DEVICE_CONTROLLER }?mode=edit`,
|
||||||
|
SRIOVGPUPage: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.NVIDIA_DRIVER_TOOLKIT_CONTROLLER }?mode=edit`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasSchema() {
|
||||||
|
return !!this.schema;
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsData() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const rows = this.$store.getters[`${ inStore }/all`](HCI.MIG_CONFIGURATION) || [];
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
sriovGPUPage() {
|
||||||
|
return {
|
||||||
|
name: 'harvester-c-cluster-resource',
|
||||||
|
params: { cluster: this.$store.getters['clusterId'], resource: HCI.SR_IOVGPU_DEVICE },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
showEnableSRIOVGPUMessage() {
|
||||||
|
return this.rowsData.length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
headers() {
|
||||||
|
const cols = [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
label: 'Address',
|
||||||
|
value: 'spec.gpuAddress',
|
||||||
|
sort: ['spec.gpuAddress']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Configured Profile',
|
||||||
|
label: 'Configured Count',
|
||||||
|
labelKey: 'harvester.tableHeaders.configuredProfiles',
|
||||||
|
value: 'configuredProfiles',
|
||||||
|
sort: ['configuredProfiles'],
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
labelKey: 'tableHeaders.status',
|
||||||
|
sort: ['status.status'],
|
||||||
|
value: 'status.status',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return cols;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else-if="!hasAddonSchema">
|
||||||
|
<Banner color="warning">
|
||||||
|
{{ t('harvester.vgpu.noPermission') }}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasSriovgpuAddon || !hasPCIAddon">
|
||||||
|
<Banner
|
||||||
|
v-if="!hasSriovgpuAddon"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toVGpuAddon"
|
||||||
|
prefix-label="harvester.vgpu.goSetting.prefix"
|
||||||
|
middle-label="harvester.vgpu.goSetting.middle"
|
||||||
|
suffix-label="harvester.vgpu.goSetting.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="!hasPCIAddon"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toPciAddon"
|
||||||
|
prefix-label="harvester.pci.goSetting.prefix"
|
||||||
|
middle-label="harvester.pci.goSetting.middle"
|
||||||
|
suffix-label="harvester.pci.goSetting.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasSchema">
|
||||||
|
<Banner
|
||||||
|
v-if="showEnableSRIOVGPUMessage"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="sriovGPUPage"
|
||||||
|
prefix-label="harvester.migconfiguration.goSriovGPU.prefix"
|
||||||
|
middle-label="harvester.migconfiguration.goSriovGPU.middle"
|
||||||
|
suffix-label="harvester.migconfiguration.goSriovGPU.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="!showEnableSRIOVGPUMessage"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.migconfiguration.infoBanner')"
|
||||||
|
/>
|
||||||
|
<ResourceTable
|
||||||
|
v-bind="$attrs"
|
||||||
|
:groupable="false"
|
||||||
|
:namespaced="false"
|
||||||
|
:headers="headers"
|
||||||
|
:schema="schema"
|
||||||
|
:rows="rowsData"
|
||||||
|
key-field="_key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -273,7 +273,7 @@ export default {
|
|||||||
|
|
||||||
needNewSecret() {
|
needNewSecret() {
|
||||||
// When creating a template it is always necessary to create a new secret.
|
// When creating a template it is always necessary to create a new secret.
|
||||||
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
|
return this.showYaml ? false : this.resourceType === HCI.VM_VERSION || this.isCreate;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultTerminationSetting() {
|
defaultTerminationSetting() {
|
||||||
@@ -708,17 +708,7 @@ export default {
|
|||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
const prefixName = this.value.metadata?.name || '';
|
const dataVolumeName = this.parseDataVolumeName(R);
|
||||||
|
|
||||||
let dataVolumeName = '';
|
|
||||||
|
|
||||||
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
|
||||||
dataVolumeName = R.volumeName;
|
|
||||||
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
|
||||||
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
|
||||||
} else {
|
|
||||||
dataVolumeName = R.realName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _disk = this.parseDisk(R, index);
|
const _disk = this.parseDisk(R, index);
|
||||||
const _volume = this.parseVolume(R, dataVolumeName);
|
const _volume = this.parseVolume(R, dataVolumeName);
|
||||||
@@ -1013,6 +1003,25 @@ export default {
|
|||||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parseDataVolumeName(R) {
|
||||||
|
const prefixName = this.value.metadata?.name || '';
|
||||||
|
let dataVolumeName = '';
|
||||||
|
|
||||||
|
if (!R.dataVolumeName) {
|
||||||
|
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
|
||||||
|
dataVolumeName = R.volumeName;
|
||||||
|
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
|
||||||
|
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
|
||||||
|
} else {
|
||||||
|
dataVolumeName = R.realName;
|
||||||
|
}
|
||||||
|
|
||||||
|
R.dataVolumeName = dataVolumeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.dataVolumeName;
|
||||||
|
},
|
||||||
|
|
||||||
parseDisk(R, index) {
|
parseDisk(R, index) {
|
||||||
const out = { name: R.name };
|
const out = { name: R.name };
|
||||||
|
|
||||||
@@ -1638,7 +1647,7 @@ export default {
|
|||||||
|
|
||||||
secretRef: {
|
secretRef: {
|
||||||
handler(secret) {
|
handler(secret) {
|
||||||
if (secret && this.resourceType !== HCI.BACKUP) {
|
if (secret && this.resourceType !== HCI.BACKUP && this.resourceType !== HCI.VM) {
|
||||||
this.secretName = secret?.metadata.name;
|
this.secretName = secret?.metadata.name;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing vGPU MIGConfiguration resource.
|
||||||
|
* @extends HarvesterResource
|
||||||
|
*/
|
||||||
|
export default class MIGCONFIGURATION extends HarvesterResource {
|
||||||
|
get _availableActions() {
|
||||||
|
let out = super._availableActions;
|
||||||
|
|
||||||
|
out = out.map((action) => {
|
||||||
|
if (action.action === 'showConfiguration') {
|
||||||
|
return { ...action, enabled: !this.spec.enabled };
|
||||||
|
} else if (action.action === 'goToEditYaml') {
|
||||||
|
return { ...action, enabled: !this.spec.enabled };
|
||||||
|
} else if (action.action === 'goToEdit') {
|
||||||
|
// need to wait for status to be disabled or empty value, then allow user to editConfig
|
||||||
|
return { ...action, enabled: !this.spec.enabled && ['disabled', ''].includes(this.configStatus) };
|
||||||
|
} else {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
out.push(
|
||||||
|
{
|
||||||
|
action: 'enableConfig',
|
||||||
|
enabled: !this.isEnabled,
|
||||||
|
icon: 'icon icon-fw icon-dot',
|
||||||
|
label: 'Enable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'disableConfig',
|
||||||
|
enabled: this.isEnabled,
|
||||||
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
|
label: 'Disable',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canYaml() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get disableResourceDetailDrawer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDelete() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get configStatus() {
|
||||||
|
return this.status.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
get actualState() {
|
||||||
|
return this.isEnabled ? 'Enabled' : 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateDisplay() {
|
||||||
|
return this.actualState;
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateColor() {
|
||||||
|
const state = this.actualState;
|
||||||
|
|
||||||
|
return colorForState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEnabled() {
|
||||||
|
return this.spec.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get configuredProfiles() {
|
||||||
|
const configuredProfiles = this.spec?.profileSpec?.filter((p) => p.requested > 0) || [];
|
||||||
|
|
||||||
|
if (configuredProfiles.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuredProfiles
|
||||||
|
.map((profile) => `${ profile.name } * ${ profile.requested }`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableConfig() {
|
||||||
|
try {
|
||||||
|
this.spec.enabled = true;
|
||||||
|
await this.save();
|
||||||
|
} catch (err) {
|
||||||
|
this.$dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableConfig() {
|
||||||
|
const { enabled: currentEnabled } = this.spec;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.spec.enabled = false;
|
||||||
|
await this.save();
|
||||||
|
} catch (err) {
|
||||||
|
this.spec.enabled = currentEnabled;
|
||||||
|
this.$dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,15 +35,10 @@ export default class HciPv extends HarvesterResource {
|
|||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super._availableActions;
|
let out = super._availableActions;
|
||||||
|
|
||||||
// Longhorn V2 provisioner do not support volume clone feature yet
|
const clone = out.find((action) => action.action === 'goToClone');
|
||||||
if (this.isLonghornV2) {
|
|
||||||
out = out.filter((action) => action.action !== 'goToClone');
|
|
||||||
} else {
|
|
||||||
const clone = out.find((action) => action.action === 'goToClone');
|
|
||||||
|
|
||||||
if (clone) {
|
if (clone) {
|
||||||
clone.action = 'goToCloneVolume';
|
clone.action = 'goToCloneVolume';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportImageAction = {
|
const exportImageAction = {
|
||||||
@@ -65,10 +60,6 @@ export default class HciPv extends HarvesterResource {
|
|||||||
takeSnapshotAction,
|
takeSnapshotAction,
|
||||||
...out
|
...out
|
||||||
];
|
];
|
||||||
// TODO: remove this block if Longhorn V2 engine supports restore volume snapshot
|
|
||||||
if (this.isLonghornV2) {
|
|
||||||
out = out.filter((action) => action.action !== takeSnapshotAction.action);
|
|
||||||
}
|
|
||||||
} else { // v1.4 / v1.3
|
} else { // v1.4 / v1.3
|
||||||
if (!this.isLonghorn || !this.isLonghornV2) {
|
if (!this.isLonghorn || !this.isLonghornV2) {
|
||||||
out = [
|
out = [
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ export default class NetworkAttachmentDef extends SteveModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isSystem() {
|
||||||
|
const systemNamespaces = this.$rootGetters['systemNamespaces'];
|
||||||
|
|
||||||
|
if (systemNamespaces.includes(this.metadata?.namespace)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
get isIpamStatic() {
|
get isIpamStatic() {
|
||||||
return this.parseConfig.ipam?.type === 'static';
|
return this.parseConfig.ipam?.type === 'static';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,17 +87,11 @@ const IgnoreMessages = ['pod has unbound immediate PersistentVolumeClaims'];
|
|||||||
|
|
||||||
export default class VirtVm extends HarvesterResource {
|
export default class VirtVm extends HarvesterResource {
|
||||||
get availableActions() {
|
get availableActions() {
|
||||||
let out = super._availableActions;
|
const out = super._availableActions;
|
||||||
|
const clone = out.find((action) => action.action === 'goToClone');
|
||||||
|
|
||||||
// VM attached with Longhorn V2 volume doesn't support clone feature
|
if (clone) {
|
||||||
if (this.longhornV2Volumes.length > 0) {
|
clone.action = 'goToCloneVM';
|
||||||
out = out.filter((action) => action.action !== 'goToClone');
|
|
||||||
} else {
|
|
||||||
const clone = out.find((action) => action.action === 'goToClone');
|
|
||||||
|
|
||||||
if (clone) {
|
|
||||||
clone.action = 'goToCloneVM';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -159,7 +153,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'takeVMSnapshot',
|
action: 'takeVMSnapshot',
|
||||||
enabled: (!!this.actions?.snapshot || !!this.action?.backup) && !this.longhornV2Volumes.length,
|
enabled: (!!this.actions?.snapshot || !!this.action?.backup),
|
||||||
icon: 'icon icon-snapshot',
|
icon: 'icon icon-snapshot',
|
||||||
label: this.t('harvester.action.vmSnapshot')
|
label: this.t('harvester.action.vmSnapshot')
|
||||||
},
|
},
|
||||||
@@ -206,10 +200,16 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
label: this.t('harvester.action.abortMigration')
|
label: this.t('harvester.action.abortMigration')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'addHotplug',
|
action: 'addHotplugVolume',
|
||||||
enabled: !!this.actions?.addVolume,
|
enabled: !!this.actions?.addVolume,
|
||||||
icon: 'icon icon-plus',
|
icon: 'icon icon-plus',
|
||||||
label: this.t('harvester.action.addHotplug')
|
label: this.t('harvester.action.addHotplugVolume')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'addHotplugNic',
|
||||||
|
enabled: this.hotplugNicFeatureEnabled && !!this.actions?.addNic,
|
||||||
|
icon: 'icon icon-plus',
|
||||||
|
label: this.t('harvester.action.addHotplugNic')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'createTemplate',
|
action: 'createTemplate',
|
||||||
@@ -395,8 +395,20 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
|
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
diskName,
|
name: diskName,
|
||||||
component: 'HarvesterUnplugVolume'
|
type: 'volume',
|
||||||
|
component: 'HarvesterHotUnplug',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unplugNIC(networkName) {
|
||||||
|
const resources = this;
|
||||||
|
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
name: networkName,
|
||||||
|
type: 'network',
|
||||||
|
component: 'HarvesterHotUnplug',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,10 +516,17 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addHotplug(resources = this) {
|
addHotplugVolume(resources = this) {
|
||||||
this.$dispatch('promptModal', {
|
this.$dispatch('promptModal', {
|
||||||
resources,
|
resources,
|
||||||
component: 'HarvesterAddHotplugModal'
|
component: 'HarvesterAddHotplugVolumeModal'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addHotplugNic(resources = this) {
|
||||||
|
this.$dispatch('promptModal', {
|
||||||
|
resources,
|
||||||
|
component: 'HarvesterAddHotplugNic'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1240,6 +1259,10 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypeAuto');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hotplugNicFeatureEnabled() {
|
||||||
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('hotplugNic');
|
||||||
|
}
|
||||||
|
|
||||||
get isBackupTargetUnavailable() {
|
get isBackupTargetUnavailable() {
|
||||||
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || [];
|
||||||
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.7.0-dev",
|
"version": "1.7.0-rc4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester'
|
|||||||
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
|
||||||
const IMAGE_METHOD = {
|
const IMAGE_METHOD = {
|
||||||
NEW: 'new',
|
NEW: 'new',
|
||||||
@@ -32,6 +33,7 @@ export default {
|
|||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
|
||||||
|
|
||||||
const value = await this.$store.dispatch('harvester/create', {
|
const value = await this.$store.dispatch('harvester/create', {
|
||||||
type: HCI.UPGRADE,
|
type: HCI.UPGRADE,
|
||||||
@@ -63,6 +65,7 @@ export default {
|
|||||||
sourceType: UPLOAD,
|
sourceType: UPLOAD,
|
||||||
uploadController: null,
|
uploadController: null,
|
||||||
uploadResult: null,
|
uploadResult: null,
|
||||||
|
storageClassValue: null,
|
||||||
imageValue: null,
|
imageValue: null,
|
||||||
enableLogging: true,
|
enableLogging: true,
|
||||||
IMAGE_METHOD,
|
IMAGE_METHOD,
|
||||||
@@ -79,7 +82,6 @@ export default {
|
|||||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
skipSingleReplicaDetachedVolFeatureEnabled() {
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
||||||
},
|
},
|
||||||
|
|
||||||
allOSImages() {
|
allOSImages() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
||||||
},
|
},
|
||||||
@@ -116,7 +118,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fileName() {
|
fileName() {
|
||||||
return this.file?.name || '';
|
return this.preprocessImageName(this.file?.name || '');
|
||||||
},
|
},
|
||||||
|
|
||||||
canEnableLogging() {
|
canEnableLogging() {
|
||||||
@@ -181,6 +183,38 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async createImageStorageClass(imageName = '') {
|
||||||
|
// delete related SC if existed
|
||||||
|
await this.deleteImageStorageClass(imageName);
|
||||||
|
|
||||||
|
const storageClassPayload = {
|
||||||
|
apiVersion: 'storage.k8s.io/v1',
|
||||||
|
type: STORAGE_CLASS,
|
||||||
|
metadata: { name: imageName },
|
||||||
|
volumeBindingMode: 'Immediate',
|
||||||
|
reclaimPolicy: 'Delete',
|
||||||
|
allowVolumeExpansion: true, // must be boolean type
|
||||||
|
provisioner: 'driver.longhorn.io',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.storageClassValue = await this.$store.dispatch('harvester/create', storageClassPayload);
|
||||||
|
|
||||||
|
if (this.storageClassValue && this.storageClassValue.save) {
|
||||||
|
await this.storageClassValue.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteImageStorageClass(imageName = '') {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storageClasses = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
|
const targetSC = storageClasses.find((sc) => sc.id === imageName);
|
||||||
|
|
||||||
|
if (targetSC && targetSC.remove) {
|
||||||
|
await targetSC.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async initImageValue() {
|
async initImageValue() {
|
||||||
this.imageValue = await this.$store.dispatch('harvester/create', {
|
this.imageValue = await this.$store.dispatch('harvester/create', {
|
||||||
type: HCI.IMAGE,
|
type: HCI.IMAGE,
|
||||||
@@ -191,6 +225,7 @@ export default {
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
backend: 'cdi',
|
||||||
sourceType: UPLOAD,
|
sourceType: UPLOAD,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
checksum: this.imageValue?.spec?.checksum || '',
|
checksum: this.imageValue?.spec?.checksum || '',
|
||||||
@@ -203,8 +238,9 @@ export default {
|
|||||||
|
|
||||||
this.file = {};
|
this.file = {};
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
const imageDisplayName = this.imageValue?.spec?.displayName || '';
|
||||||
|
|
||||||
if (!this.imageValue.spec.displayName && this.createNewImage) {
|
if (!imageDisplayName && this.createNewImage) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
@@ -212,24 +248,31 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Save the image first if creating a new one
|
||||||
if (this.imageSource === IMAGE_METHOD.NEW) {
|
if (this.imageSource === IMAGE_METHOD.NEW) {
|
||||||
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
||||||
|
|
||||||
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
|
||||||
this.value.spec.image = this.uploadImageId;
|
this.value.spec.image = this.uploadImageId;
|
||||||
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
|
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
|
||||||
this.imageValue.spec.sourceType = DOWNLOAD;
|
// check if URL is provided
|
||||||
if (!this.imageValue.spec.url) {
|
if (!this.imageValue.spec.url) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create related image storage class first
|
||||||
|
await this.createImageStorageClass(imageDisplayName);
|
||||||
|
this.imageValue.spec.sourceType = DOWNLOAD;
|
||||||
|
this.imageValue.spec.targetStorageClassName = imageDisplayName;
|
||||||
|
|
||||||
res = await this.imageValue.save();
|
res = await this.imageValue.save();
|
||||||
|
|
||||||
this.value.spec.image = res.id;
|
this.value.spec.image = res.id;
|
||||||
}
|
}
|
||||||
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
|
} else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
|
||||||
if (!this.imageId) {
|
if (!this.imageId) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
@@ -239,7 +282,7 @@ export default {
|
|||||||
|
|
||||||
this.value.spec.image = this.imageId;
|
this.value.spec.image = this.imageId;
|
||||||
}
|
}
|
||||||
|
// enable logging or skip single replica detection if checked
|
||||||
if (this.canEnableLogging) {
|
if (this.canEnableLogging) {
|
||||||
this.value.spec.logEnabled = this.enableLogging;
|
this.value.spec.logEnabled = this.enableLogging;
|
||||||
}
|
}
|
||||||
@@ -252,11 +295,13 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
// if anything failed, delete the created image storage class
|
||||||
|
await this.deleteImageStorageClass(imageDisplayName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async uploadFile(file) {
|
async uploadFile(file) {
|
||||||
const fileName = file.name;
|
const fileName = this.preprocessImageName(file.name);
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
||||||
@@ -280,6 +325,10 @@ export default {
|
|||||||
this.imageValue.spec.url = '';
|
this.imageValue.spec.url = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// before uploading image, we need to create related image storage class first
|
||||||
|
await this.createImageStorageClass(fileName);
|
||||||
|
this.imageValue.spec.targetStorageClassName = fileName;
|
||||||
|
|
||||||
const res = await this.imageValue.save();
|
const res = await this.imageValue.save();
|
||||||
|
|
||||||
this.uploadImageId = res.id;
|
this.uploadImageId = res.id;
|
||||||
@@ -296,20 +345,35 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.errors = exceptionToErrorsArray(e);
|
this.errors = exceptionToErrorsArray(e);
|
||||||
}
|
}
|
||||||
|
// if upload failed, delete the created image storage class
|
||||||
|
await this.deleteImageStorageClass(fileName);
|
||||||
this.file = {};
|
this.file = {};
|
||||||
this.uploadImageId = '';
|
this.uploadImageId = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// replace _ to - to meet storage class name requirement
|
||||||
|
preprocessImageName(name) {
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.toLowerCase().replace(/[_]/g, '-');
|
||||||
|
},
|
||||||
|
|
||||||
handleImageDelete(imageId) {
|
handleImageDelete(imageId) {
|
||||||
const image = this.allOSImages.find((I) => I.id === imageId);
|
const image = this.allOSImages.find((I) => I.id === imageId);
|
||||||
|
const imageDisplayName = image?.spec?.displayName || '';
|
||||||
|
|
||||||
if (image) {
|
if (image && imageDisplayName) {
|
||||||
this.$store.dispatch('harvester/promptModal', {
|
this.$store.dispatch('harvester/promptModal', {
|
||||||
resources: [image],
|
resources: [image],
|
||||||
component: 'ConfirmRelatedToRemoveDialog',
|
component: 'ConfirmRelatedToRemoveDialog',
|
||||||
needConfirmation: false,
|
needConfirmation: false,
|
||||||
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
|
||||||
|
extraActionAfterRemove: async() => {
|
||||||
|
await this.deleteImageStorageClass(imageDisplayName);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.deleteImageId = '';
|
this.deleteImageId = '';
|
||||||
}
|
}
|
||||||
@@ -419,13 +483,13 @@ export default {
|
|||||||
v-if="showUploadSuccessBanner"
|
v-if="showUploadSuccessBanner"
|
||||||
color="success"
|
color="success"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
:label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="showUploadingWarningBanner"
|
v-if="showUploadingWarningBanner"
|
||||||
color="warning"
|
color="warning"
|
||||||
class="mt-0 mb-30"
|
class="mt-0 mb-30"
|
||||||
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
:label="t('harvester.image.warning.osUpgrade.uploading', { name: fileName })"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const HCI = {
|
|||||||
STORAGE: 'harvesterhci.io.storage',
|
STORAGE: 'harvesterhci.io.storage',
|
||||||
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
|
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
|
||||||
KSTUNED: 'node.harvesterhci.io.ksmtuned',
|
KSTUNED: 'node.harvesterhci.io.ksmtuned',
|
||||||
|
HUGEPAGES: 'node.harvesterhci.io.hugepage',
|
||||||
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
|
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
|
||||||
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
|
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',
|
||||||
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
|
SR_IOV: 'devices.harvesterhci.io.sriovnetworkdevice',
|
||||||
@@ -44,6 +45,7 @@ export const HCI = {
|
|||||||
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
|
SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice',
|
||||||
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
|
USB_DEVICE: 'devices.harvesterhci.io.usbdevice',
|
||||||
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
|
USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim',
|
||||||
|
MIG_CONFIGURATION: 'devices.harvesterhci.io.migconfiguration',
|
||||||
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
|
VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig',
|
||||||
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
|
VLAN_STATUS: 'network.harvesterhci.io.vlanstatus',
|
||||||
ADD_ONS: 'harvesterhci.io.addon',
|
ADD_ONS: 'harvesterhci.io.addon',
|
||||||
@@ -53,7 +55,7 @@ export const HCI = {
|
|||||||
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
LB: 'loadbalancer.harvesterhci.io.loadbalancer',
|
||||||
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
IP_POOL: 'loadbalancer.harvesterhci.io.ippool',
|
||||||
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
HARVESTER_CONFIG: 'rke-machine-config.cattle.io.harvesterconfig',
|
||||||
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup'
|
LVM_VOLUME_GROUP: 'harvesterhci.io.lvmvolumegroup',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
export const VOLUME_SNAPSHOT = 'snapshot.storage.k8s.io.volumesnapshot';
|
||||||
|
|||||||
@@ -2,30 +2,78 @@ import { HCI } from '@pkg/harvester/config/labels-annotations';
|
|||||||
|
|
||||||
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
|
export const VM_IMAGE_FILE_FORMAT = ['qcow', 'qcow2', 'raw', 'img', 'iso'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the filename from a URL, handling query parameters and fragments
|
||||||
|
* @param {string} url - The URL to parse
|
||||||
|
* @returns {string} - The filename without query params or fragments
|
||||||
|
*/
|
||||||
|
function getFilenameFromUrl(url) {
|
||||||
|
try {
|
||||||
|
// Try to parse as a full URL
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
// Get pathname and extract the last segment
|
||||||
|
const pathname = urlObj.pathname;
|
||||||
|
|
||||||
|
return pathname.split('/').filter(Boolean).pop() || '';
|
||||||
|
} catch (e) {
|
||||||
|
// If URL parsing fails, treat as a relative path
|
||||||
|
// Remove query params and fragments manually
|
||||||
|
const cleanUrl = url.split('?')[0].split('#')[0];
|
||||||
|
|
||||||
|
return cleanUrl.split('/').pop() || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates image URL format
|
||||||
|
* @param {string} url - The image URL to validate
|
||||||
|
* @param {object} getters - Vuex getters
|
||||||
|
* @param {array} errors - Array to collect validation errors
|
||||||
|
* @param {any} validatorArgs - Additional validator arguments
|
||||||
|
* @param {string} type - Type of validation ('file' or other)
|
||||||
|
* @returns {array} - Array of validation errors
|
||||||
|
*/
|
||||||
export function imageUrl(url, getters, errors, validatorArgs, type) {
|
export function imageUrl(url, getters, errors, validatorArgs, type) {
|
||||||
|
const tipString =
|
||||||
|
type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
|
||||||
const t = getters['i18n/t'];
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
if (!url || url === '') {
|
if (!url || url === '') {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const suffixName = url.split('/').pop();
|
// Extract filename, handling query parameters and fragments
|
||||||
const fileSuffix = suffixName.split('.').pop().toLowerCase();
|
const filename = getFilenameFromUrl(url);
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
errors.push(t(tipString));
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file extension
|
||||||
|
const fileSuffix = filename.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
|
if (!VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
|
||||||
const tipString = type === 'file' ? 'harvester.validation.image.ruleFileTip' : 'harvester.validation.image.ruleTip';
|
|
||||||
|
|
||||||
errors.push(t(tipString));
|
errors.push(t(tipString));
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileRequired(annotations = {}, getters, errors, validatorArgs, type) {
|
export function fileRequired(
|
||||||
|
annotations = {},
|
||||||
|
getters,
|
||||||
|
errors,
|
||||||
|
validatorArgs,
|
||||||
|
type
|
||||||
|
) {
|
||||||
const t = getters['i18n/t'];
|
const t = getters['i18n/t'];
|
||||||
|
|
||||||
if (!annotations[HCI.IMAGE_NAME]) {
|
if (!annotations[HCI.IMAGE_NAME]) {
|
||||||
errors.push(t('validation.required', { key: t('harvester.image.fileName') }));
|
errors.push(
|
||||||
|
t('validation.required', { key: t('harvester.image.fileName') })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
|||||||
Reference in New Issue
Block a user