Compare commits

...
8 Commits
Author SHA1 Message Date
6700b2055e feat: add support for configuring transparent hugepages (#414) (#598)
* feat: add support for configuring transparent hugepages

Related-to: https://github.com/harvester/harvester/issues/5006



* fix: return empty object if hugepages can't be found for node

Related-to: https://github.com/harvester/harvester/issues/5006




---------




(cherry picked from commit 30de2b1a18)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Tim Serong <tserong@suse.com>
Co-authored-by: Moritz Röhrich <moritz.rohrich@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 14:15:27 +08:00
mergify[bot]andAndy Lee e486852f7a chore: bump version to v1.7.0-rc2 (#596) (#597)
(cherry picked from commit 6fedcc353c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-12 09:21:01 +08:00
mergify[bot]andYiya Chen c19341bec9 feat: support for HotPlugNICs from Kubevirt (#582) (#594)
* refactor: rename hotplug volume
* feat: add hotplug NIC
* feat: add hot unplug
* refactor: rename NIC
* feat: get latest status
* feat: disable not ready options
* feat: filter out system networks
* refactor: update wordings

---------


(cherry picked from commit f9bff21e84)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-11 11:46:43 +08:00
mergify[bot]andAndy Lee 8cbb9d6b18 chore: update yarn.lock for @rancher/shell v3.0.8-rc.8 (#591) (#592)
(cherry picked from commit 6735826e15)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 16:19:48 +08:00
mergify[bot]andAndy Lee 00f0953592 chore: bump shell version to v3.0.8-rc.8 (#588) (#590)
(cherry picked from commit 9e17e239cf)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-10 15:05:04 +08:00
mergify[bot]andAndy Lee 58507f0b2e ci: lint last commit if is empty string or all zero (#584) (#586)
(cherry picked from commit db58024351)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-06 15:53:50 +08:00
mergify[bot]andTim Serong 7785d7f469 feat: enable snapshot and clone for LHv2 (#379) (#587)
Now that Longhorn supports volume clone with the V2 data engine, we
can enable volume snapshot and clone.

Related issue: https://github.com/harvester/harvester/issues/6710


(cherry picked from commit a1cf41bda9)

Signed-off-by: Tim Serong <tserong@suse.com>
Co-authored-by: Tim Serong <tserong@suse.com>
2025-11-06 15:53:20 +08:00
mergify[bot]andAndy Lee 2c043e0a8e chore: bump version to v1.7.0-rc1 (#583) (#585)
(cherry picked from commit 81bf19419c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-04 16:12:14 +08:00
21 changed files with 1930 additions and 272 deletions
+16 -3
View File
@@ -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 }}
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "harvester-ui-extension", "name": "harvester-ui-extension",
"version": "1.7.0-dev", "version": "1.7.0-rc2",
"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",
+1
View File
@@ -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`,
}; };
+2 -1
View File
@@ -53,7 +53,8 @@ const FEATURE_FLAGS = {
'vmMachineTypeAuto', 'vmMachineTypeAuto',
'lhV2VolExpansion', 'lhV2VolExpansion',
'l2VlanTrunkMode', 'l2VlanTrunkMode',
'kubevirtMigration' 'kubevirtMigration',
'hotplugNic'
] ]
}; };
@@ -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>
@@ -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>
@@ -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'],
@@ -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>
@@ -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,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"
@@ -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;
+35 -10
View File
@@ -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
@@ -442,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:
@@ -514,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:
@@ -582,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:
@@ -613,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}
@@ -1695,7 +1720,7 @@ harvester:
available: Available available: Available
total: Total total: Total
vGPUID: vGPU ID vGPUID: vGPU ID
goSriovGPU: goSriovGPU:
prefix: Please enable the supported GPU devices in prefix: Please enable the supported GPU devices in
middle: SR-IOV GPU Devices middle: SR-IOV GPU Devices
suffix: page to manage the vGPU MIG configurations. suffix: page to manage the vGPU MIG configurations.
@@ -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 -1
View File
@@ -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-rc2",
"private": false, "private": false,
"rancher": { "rancher": {
"annotations": { "annotations": {
+1
View File
@@ -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',
+1102 -128
View File
File diff suppressed because it is too large Load Diff