Latest changes from harvester/master

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia 2024-09-19 10:37:36 +02:00
parent deeccf3db6
commit ec3d88aeb7
No known key found for this signature in database
GPG Key ID: E6D011B7415D4393
56 changed files with 1498 additions and 2197 deletions

View File

@ -3,7 +3,7 @@ import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
import Tag from '@shell/components/Tag';
export default {
name: 'DiskTags',
name: 'Tags',
components: { Tag },

View File

@ -35,13 +35,23 @@ export default {
version: '',
enableLogging: true,
readyReleaseNote: false,
isOpen: false,
isOpen: false
};
},
computed: {
...mapGetters(['currentCluster']),
latestUpgrade() {
return this.upgrade?.find(u => u.isLatestUpgrade);
},
isUpgradeInProgress() {
return this.latestUpgrade &&
!this.latestUpgrade.isUpgradeSucceeded &&
!this.latestUpgrade.isUpgradeFailed;
},
versionOptions() {
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
@ -133,7 +143,7 @@ export default {
/>
</h1>
<button
v-if="versionOptions.length"
v-if="versionOptions.length && !isUpgradeInProgress"
type="button"
class="btn bg-warning btn-sm"
@click="open"

View File

@ -34,15 +34,7 @@ export default {
},
data() {
const categorySettings = this.settings.filter((s) => {
if (this.category !== 'advanced') {
return (CATEGORY[this.category] || []).find(item => item === s.id);
} else if (this.category === 'advanced') {
const allCategory = Object.keys(CATEGORY);
return !allCategory.some(category => (CATEGORY[category] || []).find(item => item === s.id));
}
}) || [];
const categorySettings = this.filterCategorySettings();
return {
HCI_SETTING,
@ -52,7 +44,27 @@ export default {
computed: { ...mapGetters({ t: 'i18n/t' }) },
watch: {
settings: {
deep: true,
handler() {
this['categorySettings'] = this.filterCategorySettings();
}
}
},
methods: {
filterCategorySettings() {
return this.settings.filter((s) => {
if (this.category !== 'advanced') {
return (CATEGORY[this.category] || []).find(item => item === s.id);
} else if (this.category === 'advanced') {
const allCategory = Object.keys(CATEGORY);
return !allCategory.some(category => (CATEGORY[category] || []).find(item => item === s.id));
}
}) || [];
},
showActionMenu(e, setting) {
const actionElement = e.srcElement;
@ -104,7 +116,7 @@ export default {
<template>
<div>
<div v-for="(setting, i) in categorySettings" class="advanced-setting mb-20" :key="i" >
<div v-for="(setting, i) in categorySettings" :key="i">
<div class="header">
<div class="title">
<h1>
@ -112,6 +124,9 @@ export default {
<span v-if="setting.customized" class="modified">
Modified
</span>
<span v-if="setting.technicalPreview" v-clean-tooltip="t('advancedSettings.technicalPreview')" class="technical-preview">
Technical Preview
</span>
</h1>
<h2 v-clean-html="t(setting.description, {}, true)">
</h2>
@ -200,4 +215,12 @@ export default {
padding: 2px 10px;
font-size: 12px;
}
.technical-preview {
margin-left: 10px;
border: 1px solid var(--warning);
border-radius: 5px;
padding: 2px 10px;
font-size: 12px;
}
</style>

View File

@ -1,5 +1,6 @@
<script>
import { Banner } from '@components/Banner';
import { DOC_LINKS } from '../config/doc-links';
export default {
name: 'HarvesterUpgradeInfo',
@ -16,6 +17,10 @@ export default {
computed: {
releaseVersion() {
return !!this.version ? `https://github.com/harvester/harvester/releases/tag/${ this.version }` : `https://github.com/harvester/harvester/releases`;
},
upgradeLink() {
return DOC_LINKS.UPGRADE_URL;
}
},
};
@ -26,15 +31,14 @@ export default {
<Banner color="warning">
<div>
<strong>{{ t('harvester.upgradePage.upgradeInfo.warning') }}:</strong>
<p v-clean-html="t('harvester.upgradePage.upgradeInfo.doc', {}, true)" class="mb-5">
</p>
<p v-clean-html="t('harvester.upgradePage.upgradeInfo.doc', {url: upgradeLink}, true)" class="mb-5"></p>
<p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p>
<p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a :href="releaseVersion" target="_blank">{{ t('generic.moreInfo') }} </a>
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a :href="releaseVersion" target="_blank">{{ t('generic.moreInfo') }}</a>
</p>
</div>
</Banner>

View File

@ -61,7 +61,7 @@ export default {
window.open(
url,
'_blank',
'toolbars=0,width=900,height=700,left=0,top=0,noreferrer'
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
);
},

View File

@ -0,0 +1,39 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AdditionalGuestMemoryOverheadRatio',
components: { LabeledInput },
mixins: [CreateEditView],
data() {
return { ratio: this.value.value || this.value.default };
},
methods: {
update() {
this.value['value'] = this.ratio;
},
useDefault() {
this['ratio'] = this.value.default;
this.update();
},
},
};
</script>
<template>
<div class="row">
<div class="col span-12">
<LabeledInput
v-model:value="ratio"
:label="t('harvester.setting.ratio')"
@update:value="update"
/>
</div>
</div>
</template>

View File

@ -10,6 +10,7 @@ import Tip from '@shell/components/Tip';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
import { HCI } from '../../types';
import { DOC_LINKS } from '../../config/doc-links';
export default {
name: 'HarvesterEditStorageNetwork',
@ -85,6 +86,9 @@ export default {
},
computed: {
storageNetworkExampleLink() {
return DOC_LINKS.STORAGE_NETWORK_EXAMPLE;
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
@ -206,8 +210,8 @@ export default {
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label"
/>
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.setting.storageNetwork.tip')">
<t k="harvester.setting.storageNetwork.tip" :raw="true" />
<Tip class="mb-20" icon="icon icon-info">
<t k="harvester.setting.storageNetwork.tip" :raw="true" :url="storageNetworkExampleLink" />
</Tip>
<ArrayList

View File

@ -0,0 +1,12 @@
const pkgJson = require('../package.json');
import semver from 'semver';
const docVersion = `v${ semver.major(pkgJson.version) }.${ semver.minor(pkgJson.version) }`;
export const DOC_LINKS = {
CONSOLE_URL: `https://docs.harvesterhci.io/${ docVersion }/host/`,
RANCHER_INTEGRATION_URL: `https://docs.harvesterhci.io/${ docVersion }/rancher/rancher-integration`,
STORAGE_NETWORK_EXAMPLE: `https://docs.harvesterhci.io/${ docVersion }/advanced/storagenetwork#configuration-example`,
KSMTUNED_MODE: `https://docs.harvesterhci.io/${ docVersion }/host/#ksmtuned-mode`,
UPGRADE_URL: `https://docs.harvesterhci.io/${ docVersion }/upgrade/index`
};

View File

@ -23,7 +23,7 @@ export const InterfaceOption = [{
export const SOURCE_TYPE = {
NEW: 'New',
IMAGE: 'VM Image',
IMAGE: 'Virtual Machine Image',
ATTACH_VOLUME: 'Existing Volume',
CONTAINER: 'Container'
};
@ -41,7 +41,14 @@ export const ACCESS_CREDENTIALS = {
INJECT_SSH: 'sshPublicKey'
};
export const RunStrategys = ['Always', 'RerunOnFailure', 'Manual', 'Halted'];
export const runStrategies = ['Always', 'RerunOnFailure', 'Manual', 'Halted'];
export const maintenanceStrategies = [
'Migrate',
'ShutdownAndRestartAfterEnable',
'ShutdownAndRestartAfterDisable',
'Shutdown'
];
export const VOLUME_DATA_SOURCE_KIND = {
VolumeSnapshot: 'VolumeSnapshot',
@ -57,8 +64,8 @@ export const FLOW_TYPE = {
export const ADD_ONS = {
HARVESTER_SEEDER: 'harvester-seeder',
PCI_DEVICE_CONTROLLER: 'pcidevices-controller',
NVIDIA_DRIVER_TOOLKIT_CONTROLLER: 'nvidia-driver-toolkit',
RANCHER_LOGGING: 'rancher-logging',
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
NVIDIA_DRIVER_TOOLKIT_CONTROLLER: 'nvidia-driver-toolkit'
};

View File

@ -1,7 +1,6 @@
export const HCI = {
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
OWNED_BY: 'harvesterhci.io/owned-by',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
@ -18,6 +17,7 @@ export const HCI = {
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
@ -25,6 +25,7 @@ export const HCI = {
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
@ -33,6 +34,7 @@ export const HCI = {
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
@ -46,4 +48,8 @@ export const HCI = {
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
CPU_MANAGER: 'cpumanager'
};

View File

@ -16,7 +16,9 @@ export const HCI_SETTING = {
CLUSTER_REGISTRATION_URL: 'cluster-registration-url',
DEFAULT_STORAGE_CLASS: 'default-storage-class',
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
STORAGE_NETWORK: 'storage-network',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SSL_CERTIFICATES: 'ssl-certificates',
@ -28,6 +30,10 @@ export const HCI_SETTING = {
CSI_DRIVER_CONFIG: 'csi-driver-config',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio'
};
export const HCI_ALLOWED_SETTINGS = {
@ -41,6 +47,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.VLAN]: {
kind: 'custom', from: 'import', alias: 'vlan'
},
[HCI_SETTING.AUTO_ROTATE_RKE2_CERTS]: { kind: 'json', from: 'import' },
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
@ -51,6 +58,8 @@ export const HCI_ALLOWED_SETTINGS = {
},
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
@ -75,6 +84,9 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true
},
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: {},
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: { kind: 'boolean', technicalPreview: true },
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {

View File

@ -11,6 +11,14 @@ export const IMAGE_DOWNLOAD_SIZE = {
width: 120
};
export const IMAGE_VIRTUAL_SIZE = {
name: 'virtualSize',
labelKey: 'harvester.tableHeaders.virtualSize',
value: 'virtualSize',
sort: 'status.virtualSize',
width: 120
};
export const IMAGE_PROGRESS = {
name: 'Uploaded',
labelKey: 'tableHeaders.progress',

View File

@ -48,6 +48,13 @@ export default {
customName() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
},
cpuManagerStatus() {
if (this.value.isCPUManagerEnableInProgress) {
return this.t('generic.loading');
}
return this.t(`generic.${ this.value.isCPUManagerEnabled ? 'enabled' : 'disabled' }`);
},
consoleUrl() {
const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
@ -224,6 +231,9 @@ export default {
</div>
<div class="row mb-20">
<div v-if="!value.isEtcd" class="col span-6">
<LabelValue :name="t('harvester.host.detail.cpuManager')" :value="cpuManagerStatus" />
</div>
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
<a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
@ -268,7 +278,7 @@ export default {
<HarvesterStorageUsed
:row="value"
:resource-name="t('harvester.host.detail.storage')"
:show-reserved="true"
:show-allocated="true"
/>
</div>
</div>

View File

@ -4,13 +4,15 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import HarvesterDisk from '../../mixins/harvester-disk';
import { RadioGroup } from '@components/Form/Radio';
export default {
components: {
LabelValue,
BadgeState,
Banner,
Tag
Tag,
RadioGroup
},
mixins: [
@ -37,6 +39,18 @@ export default {
return {};
},
computed: {
targetDisk() {
return this.disks.find(disk => disk.name === this.value.name);
},
schedulableTooltipMessage() {
const { name, path } = this.value;
if (this.targetDisk && !this.targetDisk.allowScheduling && name && path) {
return this.t('harvester.host.disk.allowScheduling.tooltip', { name, path });
} else {
return this.schedulableCondition.message;
}
},
allowSchedulingOptions() {
return [{
label: this.t('generic.enabled'),
@ -117,6 +131,16 @@ export default {
</div>
<div class="row mt-10">
<div class="col span-12">
<div class="pull-left">
<RadioGroup
v-model:value="value.allowScheduling"
name="diskScheduling"
:label="t('harvester.host.disk.allowScheduling.label')"
:mode="mode"
:options="allowSchedulingOptions"
:row="true"
/>
</div>
<div class="pull-right">
{{ t('harvester.host.disk.conditions') }}:
<BadgeState
@ -127,9 +151,9 @@ export default {
class="mr-10 ml-10 state"
/>
<BadgeState
v-clean-tooltip="schedulableCondition.message"
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
v-clean-tooltip="schedulableTooltipMessage"
:color="schedulableCondition.status === 'True' && targetDisk?.allowScheduling ? 'bg-success' : 'bg-error' "
:icon="schedulableCondition.status === 'True' && targetDisk?.allowScheduling ? 'icon-checkmark' : 'icon-warning' "
label="Schedulable"
class="mr-10 state"
/>

View File

@ -104,7 +104,7 @@ export default {
key-field="_key"
>
<template cell:state="scope">
<template cell:state="scope" class="state-col">
<div class="state">
<HarvesterVmState class="vmstate" :row="scope.row" :all-cluster-network="allClusterNetwork" />
</div>

View File

@ -311,14 +311,12 @@ export default {
};
}
const current = this.ntpSync?.currentNtpServers || '';
if (status === 'unsynced') {
return {
status: 'unsynced',
warning: {
key: 'harvester.host.ntp.ntpSyncStatus.isUnsynced',
current
current: this.ntpSync?.currentNtpServers ? `<code>${ this.ntpSync.currentNtpServers }</code>` : '',
}
};
}

View File

@ -35,6 +35,10 @@ export default {
return this.value?.downSize;
},
virtualSize() {
return this.value?.virtualSize;
},
url() {
return this.value?.spec?.url || '-';
},
@ -100,6 +104,12 @@ export default {
</div>
</div>
<div class="row">
<div class="col span-12">
<LabelValue :name="t('harvester.image.virtualSize')" :value="virtualSize" class="mb-20" />
</div>
</div>
<div class="row">
<div class="col span-12">
<LabelValue :name="t('nameNsDescription.description.label')" :value="description" class="mb-20" />

View File

@ -11,6 +11,7 @@ import NodeScheduling from '@shell/components/form/NodeScheduling';
import PodAffinity from '@shell/components/form/PodAffinity';
import KeyValue from '@shell/components/form/KeyValue';
import Labels from '@shell/components/form/Labels';
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import VM_MIXIN from '../../mixins/harvester-vm';
@ -22,6 +23,7 @@ import Events from './VirtualMachineTabs/VirtualMachineEvents';
import Migration from './VirtualMachineTabs/VirtualMachineMigration';
import OverviewBasics from './VirtualMachineTabs/VirtualMachineBasics';
import OverviewKeypairs from './VirtualMachineTabs/VirtualMachineKeypairs';
import { formatSi } from '@shell/utils/units';
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
@ -33,6 +35,7 @@ export default {
Tabbed,
Events,
OverviewBasics,
LabelValue,
Volume,
Network,
OverviewKeypairs,
@ -57,6 +60,7 @@ export default {
data() {
return {
hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL,
showVmMetrics: false,
@ -66,6 +70,8 @@ export default {
async created() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.hasResourceQuotaSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA);
const hash = {
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }),
@ -75,6 +81,10 @@ export default {
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
};
if (this.hasResourceQuotaSchema) {
hash.resourceQuotas = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESOURCE_QUOTA });
}
await allHash(hash);
setPromiseResult(
@ -88,6 +98,22 @@ export default {
computed: {
...mapGetters(['currentCluster']),
totalSnapshotSize() {
if (this.value.snapshotSizeQuota === undefined || this.value.snapshotSizeQuota === null) {
return ' - ';
}
if (this.value.snapshotSizeQuota === 0) {
return '0';
}
return formatSi(this.value.snapshotSizeQuota, {
increment: 1024,
addSuffix: true,
suffix: 'i',
});
},
vmi() {
const inStore = this.$store.getters['currentProduct'].inStore;
@ -175,10 +201,17 @@ export default {
<Network v-model:value="networkRows" mode="view" />
</Tab>
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="3">
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="4">
<OverviewKeypairs :value="value" />
</Tab>
<Tab v-if="hasResourceQuotaSchema" name="quotas" :label="t('harvester.tab.quotas')" :weight="3">
<LabelValue
:name="t('harvester.snapshot.totalSnapshotSize')"
:value="totalSnapshotSize"
/>
</Tab>
<Tab
v-if="showVmMetrics"
name="vm-metrics"

View File

@ -8,6 +8,7 @@ import { STATE, NAME, AGE } from '@shell/config/table-headers';
import { matching } from '@shell/utils/selector';
import { NODE } from '@shell/config/types';
import { isEmpty } from '@shell/utils/object';
import { HCI } from '@pkg/harvester/config/labels-annotations';
export default {
components: {
@ -55,10 +56,13 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
const matchedNodes = this.value?.metadata?.annotations?.[HCI.MATCHED_NODES];
const selector = this.value?.spec?.nodeSelector;
if (!isEmpty(selector)) {
return matching(nodes, selector);
} else if (matchedNodes && matchedNodes.length > 0) {
return nodes.filter(node => matchedNodes.includes(node.id));
} else {
return nodes;
}

View File

@ -6,6 +6,7 @@ import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { BadgeState } from '@components/BadgeState';
import { ucFirst } from '@shell/utils/string';
export default {
components: {
@ -26,7 +27,7 @@ export default {
data() {
return {
errors: [],
unhealthyVM: '',
unhealthyVMs: [],
force: false
};
},
@ -40,13 +41,21 @@ export default {
},
methods: {
ucFirst,
onInputForce(v) {
if (v) {
this.unhealthyVMs = [];
}
},
close() {
this.$emit('close');
},
async apply(buttonCb) {
this.errors = [];
this.unhealthyVM = '';
this.unhealthyVMs = [];
try {
const res = await this.actionResource.doAction('maintenancePossible');
@ -62,8 +71,8 @@ export default {
} else if (res._status === 200 || res._status === 204) {
const res = await this.actionResource.doAction('listUnhealthyVM');
if (res.message) {
this.unhealthyVM = res;
if (res?.length) {
this.unhealthyVMs = res;
buttonCb(false);
} else {
await this.actionResource.doAction('enableMaintenanceMode', { force: 'false' });
@ -95,15 +104,20 @@ export default {
<Checkbox
v-model:value="force"
label-key="harvester.host.enableMaintenance.force"
@update:value="onInputForce"
/>
</div>
<Banner color="warning" :label="t('harvester.host.enableMaintenance.protip')" class="mb-0" />
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
<div v-if="unhealthyVM">
<Banner color="error mb-5">
<Banner color="warning" :label="t('harvester.host.enableMaintenance.protip')" />
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="ucFirst(err)" />
<Banner v-if="!force" class="mt-0" color="warning" :labelKey="'harvester.host.enableMaintenance.shutDownVMs'" />
<div v-for="(unhealthyVM, i) in unhealthyVMs" :key="i">
<Banner color="error mt-0 mb-5">
<p>
{{ unhealthyVM.message }}
{{ ucFirst(unhealthyVM.message) }}
</p>
</Banner>

View File

@ -72,8 +72,11 @@ export default {
const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => {
// do not allow to migrate to self node
return !!this.availableNodes.includes(n.id);
const isNotSelfNode = !!this.availableNodes.includes(n.id);
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
const matchingCpuManagerConfig = n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => {
let label = n?.metadata?.name;
const value = n?.metadata?.name;

View File

@ -0,0 +1,121 @@
<script>
import { mapGetters, mapState } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error';
export default {
name: 'HarvesterVMQuotaDialog',
components: {
AsyncButton,
Card,
UnitInput,
Banner
},
props: {
resources: {
type: Array,
required: true
}
},
created() {
this.totalSnapshotSize = this.modalData.snapshotSizeQuota;
},
data() {
return {
totalSnapshotSize: '',
errors: []
};
},
computed: {
...mapState('action-menu', ['modalData']),
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.totalSnapshotSize = '';
this.$emit('close');
},
async save(buttonDone) {
try {
// call delete action if user input 0Gi or empty string
if (this.totalSnapshotSize === null || this.totalSnapshotSize === '0Gi' ) {
await this.actionResource.doAction('deleteResourceQuota');
} else {
await this.actionResource.doAction('updateResourceQuota', { totalSnapshotSizeQuota: this.totalSnapshotSize });
}
this.close();
buttonDone(true);
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonDone(false);
}
},
}
};
</script>
<template>
<Card :show-highlight-border="false">
<h4
slot="title"
v-clean-html="t('harvester.modal.quota.editQuota')"
class="text-default-text"
/>
<template #body>
<Banner color="info">
{{ t('harvester.modal.quota.bannerMessage') }}
</Banner>
<UnitInput
v-model:value="totalSnapshotSize"
v-int-number
:label="t('harvester.snapshot.totalSnapshotSize')"
:disabled="false"
:mode="create"
:input-exponent="3"
:increment="1024"
:output-modifier="true"
suffix="GiB"
class="mb-20"
/>
</template>
<div slot="actions" class="actions">
<div class="buttons">
<button class="btn role-secondary mr-10" @click="close">
{{ t('generic.cancel') }}
</button>
<AsyncButton @click="save" />
</div>
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
</div>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>

View File

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

View File

@ -5,7 +5,7 @@ import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import { RadioGroup, RadioButton } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk';
import DiskTags from '../../components/DiskTags';
import Tags from '../../components/DiskTags';
import { HCI } from '../../types';
import { LONGHORN_SYSTEM } from './index';
@ -17,7 +17,7 @@ export default {
Banner,
RadioGroup,
RadioButton,
DiskTags,
Tags,
},
mixins: [
@ -44,6 +44,18 @@ export default {
return {};
},
computed: {
targetDisk() {
return this.disks.find(disk => disk.name === this.value.name);
},
schedulableTooltipMessage() {
const { name, path } = this.value;
if (this.targetDisk && !this.targetDisk.allowScheduling && name && path) {
return this.t('harvester.host.disk.allowScheduling.tooltip', { name, path });
} else {
return this.schedulableCondition.message;
}
},
allowSchedulingOptions() {
return [{
label: this.t('generic.enabled'),
@ -171,7 +183,7 @@ export default {
<div v-if="!value.isNew">
<div class="row">
<div class="col span-12">
<DiskTags
<Tags
v-model:value="value.tags"
:label="t('harvester.host.disk.tags.label')"
:add-label="t('harvester.host.disk.tags.addLabel')"
@ -181,6 +193,16 @@ export default {
</div>
<div class="row mt-10">
<div class="col span-12">
<div class="pull-left">
<RadioGroup
v-model:value="value.allowScheduling"
name="diskScheduling"
:label="t('harvester.host.disk.allowScheduling.label')"
:mode="mode"
:options="allowSchedulingOptions"
:row="true"
/>
</div>
<div class="pull-right">
{{ t('harvester.host.disk.conditions') }}:
<BadgeState
@ -191,9 +213,9 @@ export default {
class="mr-10 ml-10 state"
/>
<BadgeState
v-clean-tooltip="schedulableCondition.message"
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
v-clean-tooltip="schedulableTooltipMessage"
:color="schedulableCondition.status === 'True' && targetDisk?.allowScheduling ? 'bg-success' : 'bg-error' "
:icon="schedulableCondition.status === 'True' && targetDisk?.allowScheduling ? 'icon-checkmark' : 'icon-warning' "
label="Schedulable"
class="mr-10 state"
/>

View File

@ -5,6 +5,7 @@ import UnitInput from '@shell/components/form/UnitInput';
import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types';
import { DOC_LINKS } from '../../config/doc-links';
export const ksmtunedMode = [{
value: 'standard',
@ -86,6 +87,10 @@ export default {
showKsmt() {
return this.spec.run === 'run';
},
ksmtunedLink() {
return DOC_LINKS.KSMTUNED_MODE;
}
},
@ -135,7 +140,7 @@ export default {
<Checkbox v-model:value="enableMergeAcrossNodes" :mode="mode" class="check mb-20" type="checkbox" :label="t('harvester.host.ksmtuned.enableMergeNodes')" />
<h3>
<t k="harvester.host.ksmtuned.modeLink" :raw="true" />
<t k="harvester.host.ksmtuned.modeLink" :raw="true" :url="ksmtunedLink" />
</h3>
<RadioGroup
v-model:value="spec.mode"

View File

@ -25,10 +25,10 @@ import { _EDIT } from '@shell/config/query-params';
import { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner';
import { HCI } from '../../types';
import DiskTags from '../../components/DiskTags';
import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned';
import Tags from '../../components/DiskTags';
export const LONGHORN_SYSTEM = 'longhorn-system';
@ -46,7 +46,7 @@ export default {
ButtonDropdown,
KeyValue,
Banner,
DiskTags,
Tags,
Loading,
HarvesterSeeder,
MessageLink,
@ -478,10 +478,11 @@ export default {
const disks = this.longhornNode?.spec?.disks || {};
// update each disk tags and scheduling
this.newDisks.map((disk) => {
(disks[disk.name] || {}).tags = disk.tags;
(disks[disk.name] || {}).allowScheduling = disk.allowScheduling;
});
let count = 0;
const retrySave = async() => {
@ -505,7 +506,9 @@ export default {
}
};
if (this.longhornNode) {
await retrySave();
}
},
},
};
@ -547,7 +550,7 @@ export default {
class="row mb-20"
>
<div class="col span-12">
<DiskTags
<Tags
v-model:value="longhornNode.spec.tags"
:label="t('harvester.host.tags.label')"
:add-label="t('harvester.host.tags.addLabel')"
@ -650,7 +653,6 @@ export default {
:value="filteredLabels"
:add-label="t('labels.addLabel')"
:mode="mode"
:title="t('labels.labels.title')"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="updateHostLabels"

View File

@ -143,10 +143,19 @@ export default {
},
input(neu) {
const pattern = /^([1-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-9][0-4])$/;
if (neu === '') {
this.config.vlan = '';
if (!pattern.test(neu) && neu !== '') {
this.config.vlan = neu > 4094 ? 4094 : 1;
return;
}
const newValue = Number(neu);
if (newValue > 4094) {
this.config.vlan = 4094;
} else if (newValue < 1) {
this.config.vlan = 1;
} else {
this.config.vlan = newValue;
}
},

View File

@ -17,7 +17,7 @@ import { STORAGE_CLASS, LONGHORN } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object';
import { CSI_DRIVER } from '../../types';
import DiskTags from '../../components/DiskTags';
import Tags from '../../components/DiskTags';
const LONGHORN_DRIVER = 'driver.longhorn.io';
@ -34,7 +34,7 @@ export default {
Tab,
Tabbed,
Loading,
DiskTags,
Tags,
},
mixins: [CreateEditView],
@ -304,7 +304,7 @@ export default {
/>
</div>
<div class="col span-8 value">
<DiskTags
<Tags
v-model:value="scope.row.value.values"
:add-label="t('generic.add')"
:mode="modeOverride"

View File

@ -162,6 +162,7 @@ export default {
buttonCb(false);
}
} else {
this.value.spec.url = this.value.spec.url?.trim() || '';
this.save(buttonCb);
}
},
@ -277,6 +278,7 @@ export default {
:can-yaml="showEditAsYaml ? true : false"
:apply-hooks="applyHooks"
@finish="saveImage"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"

View File

@ -18,7 +18,6 @@ import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations
import CreateEditView from '@shell/mixins/create-edit-view';
import { AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import { HCI } from '../types';
import { RunStrategys } from '../config/harvester-map';
import VM_MIXIN from '../mixins/harvester-vm';
import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
@ -74,7 +73,6 @@ export default {
description: '',
defaultVersion: null,
isDefaultVersion: false,
RunStrategys,
};
},
@ -263,7 +261,7 @@ export default {
<Tab
name="nodeScheduling"
:label="t('workload.container.titles.nodeScheduling')"
:weight="-89"
:weight="-3"
>
<template #default="{active}">
<NodeScheduling
@ -274,7 +272,7 @@ export default {
</template>
</Tab>
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-90">
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-4">
<template #default="{active}">
<PodAffinity
:mode="mode"
@ -287,13 +285,42 @@ export default {
</template>
</Tab>
<Tab
:name="t('generic.labels')"
:label="t('harvester.tab.instanceLabel')"
:weight="-5"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@input="value.setInstanceLabels($event)"
/>
</template>
</Labels>
</Tab>
<Tab name="advanced" :label="t('harvester.tab.advanced')" :weight="-99">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="RunStrategys"
:options="runStrategies"
:mode="mode"
/>
</div>
@ -309,12 +336,15 @@ export default {
</div>
<div class="row mb-20">
<a v-if="showAdvanced" v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
<a v-else v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
<div class="col span-6">
<LabeledSelect
v-model:value="maintenanceStrategy"
label-key="harvester.virtualMachine.maintenanceStrategy.label"
:options="maintenanceStrategies"
:get-option-label="getMaintenanceStrategyOptionLabel"
:mode="mode"
/>
</div>
<div v-if="showAdvanced">
<div class="row mb-20">
<div class="col span-6">
<Reserved
:reserved-memory="reservedMemory"
@ -322,6 +352,27 @@ export default {
@updateReserved="updateReserved"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<Checkbox
v-model:value="cpuPinning"
class="check"
type="checkbox"
label-key="harvester.virtualMachine.cpuPinning.label"
:mode="mode"
/>
</div>
</div>
<div class="row mb-20">
<a v-if="showAdvanced" v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
<a v-else v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
</div>
<div v-if="showAdvanced">
<div class="row mb-20">
<div class="col span-6">
<UnitInput
v-model:value="terminationGracePeriodSeconds"
@ -386,35 +437,6 @@ export default {
:mode="mode"
/>
</Tab>
<Tab
:name="t('generic.labels')"
:label="t('harvester.tab.instanceLabel')"
:weight="-99"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
/>
</template>
</Labels>
</Tab>
</Tabbed>
</CruResource>
</template>

View File

@ -56,6 +56,7 @@ export default {
const hash = await allHash(_hash);
this.snapshots = hash.snapshots;
this.images = hash.images;
const defaultStorage = this.$store.getters[`harvester/all`](STORAGE_CLASS).find( O => O.isDefault);
@ -77,6 +78,7 @@ export default {
storage,
imageId,
snapshots: [],
images: [],
};
},
@ -108,10 +110,8 @@ export default {
},
imageOption() {
const choices = this.$store.getters['harvester/all'](HCI.IMAGE);
return sortBy(
choices
this.images
.filter(obj => obj.isReady)
.map((obj) => {
return {
@ -249,7 +249,17 @@ export default {
this.value['spec'] = spec;
},
updateImage() {
if (this.isVMImage && this.imageId) {
const imageResource = this.images?.find(image => this.imageId === image.id);
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (imageSize) {
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
}
}
this.update();
},
generateYaml() {
const out = saferDump(this.value);
@ -300,7 +310,7 @@ export default {
required
:mode="mode"
class="mb-20"
@update:value="update"
@update:value="updateImage"
/>
<LabeledSelect

View File

@ -140,15 +140,24 @@ export default {
onImageChange() {
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( I => this.value.image === I.id);
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (this.idx === 0) {
if (/iso$/i.test(imageResource?.imageSuffix)) {
if (isIsoImage) {
this.value['type'] = 'cd-rom';
this.value['bus'] = 'sata';
} else {
this.value['type'] = 'disk';
this.value['bus'] = 'virtio';
}
if (imageSize) {
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10);
}
this.value['size'] = `${ imageSizeGiB }Gi`;
}
this.update();

View File

@ -28,7 +28,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
import VM_MIXIN from '../../mixins/harvester-vm';
import { RunStrategys } from '../../config/harvester-map';
import { HCI } from '../../types';
import RestartVMDialog from '../../dialog/RestartVMDialog';
import VirtualMachineVGpuDevices from './VirtualMachineVGpuDevices/index';
@ -40,6 +39,8 @@ import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume';
import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved';
import { Banner } from '@components/Banner';
import MessageLink from '@shell/components/MessageLink';
export default {
name: 'HarvesterEditVM',
@ -68,6 +69,8 @@ export default {
UnitInput,
VirtualMachineVGpuDevices,
KeyValue,
Banner,
MessageLink
},
mixins: [CreateEditView, VM_MIXIN],
@ -96,13 +99,19 @@ export default {
isOpen: false,
hostname,
isRestartImmediately,
RunStrategys,
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
to() {
return {
name: 'harvester-c-cluster-resource',
params: { cluster: this.$store.getters['clusterId'], resource: HCI.HOST },
};
},
machineTypeOptions() {
return [{
label: 'None',
@ -173,6 +182,26 @@ export default {
hasStartAction() {
return this.value.hasAction('start');
},
enableCpuPinningCheckbox() {
if (this.mode === 'create') {
return this.nodes.some(node => node.isCPUManagerEnabled); // any one of nodes has label cpuManager=true
}
return true;
},
showCpuPinningBanner() {
if (this.mode === 'edit') {
return this.cpuPinning !== !!this.cloneVM.spec.template.spec.domain.cpu.dedicatedCpuPlacement;
}
if (this.mode === 'create') {
return this.nodes.every(node => !node.isCPUManagerEnabled); // no node enabled CPU manager
}
return false;
}
},
watch: {
@ -273,6 +302,15 @@ export default {
},
methods: {
cancelAction() {
const { fromPage = HCI.VM } = this.$route?.query; // default back to VM list page
const cancelOverride = {
name: this.doneRoute,
params: { resource: fromPage }
};
this.$router.replace(cancelOverride);
},
saveVM(buttonCb) {
clear(this.errors);
@ -437,12 +475,14 @@ export default {
id="vm"
:done-route="doneRoute"
:resource="value"
:cancelEvent="true"
:mode="mode"
:can-yaml="isSingle ? true : false"
:errors="errors"
:generate-yaml="generateYaml"
:apply-hooks="applyHooks"
@finish="saveVM"
@cancel="cancelAction"
>
<RadioGroup
v-if="isCreate"
@ -606,44 +646,54 @@ export default {
/>
</Tab>
<Tab
v-if="enabledSriovgpu"
:label="t('harvester.tab.vGpuDevices')"
name="vGpuDevices"
:weight="-6"
>
<VirtualMachineVGpuDevices
:mode="mode"
:value="spec.template.spec"
:vm="value"
/>
<Tab v-if="enabledSriovgpu" :label="t('harvester.tab.vGpuDevices')" name="vGpuDevices" :weight="-6">
<VirtualMachineVGpuDevices :mode="mode" :value="spec.template.spec" :vm="value" />
</Tab>
<Tab v-if="isEdit" :label="t('harvester.tab.accessCredentials')" name="accessCredentials" :weight="-7">
<AccessCredentials v-model:value="accessCredentials" :mode="mode" :resource="value" :is-qemu-installed="isQemuInstalled" />
</Tab>
<Tab
v-if="isEdit"
:label="t('harvester.tab.accessCredentials')"
name="accessCredentials"
:weight="-7"
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-8"
>
<AccessCredentials
v-model:value="accessCredentials"
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:resource="value"
:is-qemu-installed="isQemuInstalled"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
/>
</template>
</Labels>
</Tab>
<Tab
name="advanced"
:label="t('harvester.tab.advanced')"
:weight="-8"
:weight="-9"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy"
:options="RunStrategys"
:options="runStrategies"
:mode="mode"
/>
</div>
@ -659,24 +709,30 @@ export default {
</div>
<div class="row mb-20">
<a
v-if="showAdvanced"
v-t="'harvester.generic.showMore'"
role="button"
@click="toggleAdvanced"
/>
<a
v-else
v-t="'harvester.generic.showMore'"
role="button"
@click="toggleAdvanced"
<div class="col span-6">
<LabeledSelect
v-model:value="maintenanceStrategy"
label-key="harvester.virtualMachine.maintenanceStrategy.label"
:options="maintenanceStrategies"
:get-option-label="getMaintenanceStrategyOptionLabel"
:mode="mode"
/>
</div>
<div class="col span-6">
<Reserved
:reserved-memory="reservedMemory"
:mode="mode"
@updateReserved="updateReserved"
/>
</div>
</div>
<div
v-if="showAdvanced"
class="mb-20"
>
<div class="row mb-20">
<a v-if="showAdvanced" v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
<a v-else v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
</div>
<div v-if="showAdvanced" class="mb-20">
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
@ -698,13 +754,6 @@ export default {
</div>
<div class="row mb-20">
<div class="col span-6">
<Reserved
:reserved-memory="reservedMemory"
:mode="mode"
@updateReserved="updateReserved"
/>
</div>
<div class="col span-6">
<UnitInput
v-model:value="terminationGracePeriodSeconds"
@ -730,6 +779,16 @@ export default {
@updateDataTemplateId="updateDataTemplateId"
/>
<Checkbox
v-model:value="cpuPinning"
:disabled="!enableCpuPinningCheckbox"
class="check"
type="checkbox"
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
label-key="harvester.virtualMachine.cpuPinning.label"
:mode="mode"
/>
<Checkbox
v-model:value="installUSBTablet"
class="check mt-20"
@ -773,35 +832,21 @@ export default {
:label="t('harvester.virtualMachine.secureBoot')"
:mode="mode"
/>
</Tab>
<Tab
name="instanceLabel"
:label="t('harvester.tab.instanceLabel')"
:weight="-99"
<Banner
v-if="showCpuPinningBanner"
color="warning"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
>
<template #labels="{toggler}">
<KeyValue
key="labels"
:value="value.instanceLabels"
:protected-keys="value.systemLabels || []"
:toggle-filter="toggler"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:value-can-be-empty="true"
@update:value="value.setInstanceLabels($event)"
<MessageLink
v-if="mode === 'create'"
:to="to"
prefix-label="harvester.virtualMachine.advancedOptions.cpuManager.prefix"
middle-label="harvester.virtualMachine.advancedOptions.cpuManager.middle"
suffix-label="harvester.virtualMachine.advancedOptions.cpuManager.suffix"
/>
</template>
</Labels>
<span v-if="mode==='edit'">
{{ t('harvester.virtualMachine.cpuPinning.restartVMMessage') }}
</span>
</Banner>
</Tab>
</Tabbed>

View File

@ -314,12 +314,21 @@ export default {
} else if (selector[HOSTNAME] && Object.keys(selector).length === 1) {
const matchNode = allNodes.find(n => n.id === selector[HOSTNAME]);
if (matchNode) {
this.matchingNodes = {
matched: 1,
total: allNodes.length,
none: false,
sample: matchNode ? matchNode.nameDisplay : selector[HOSTNAME],
sample: matchNode.nameDisplay,
};
} else {
this.matchingNodes = {
matched: 0,
total: 0,
none: true,
sample: null,
};
}
} else {
const match = matching(allNodes, selector);

View File

@ -0,0 +1,44 @@
<script>
export default {
name: 'HarvesterCPUPinningFormatter',
props: {
value: {
type: String, // id
default: '',
},
rows: {
type: Array,
required: true,
},
},
computed: {
row() {
return this.rows.find(r => r.id === this.value);
},
cpuManagerStatus() {
if (this.row?.isCPUManagerEnableInProgress) {
return this.t('generic.loading');
}
return this.row?.isCPUManagerEnabled ? this.t('generic.enabled') : this.t('generic.disabled');
},
}
};
</script>
<template>
<span v-if="row?.isCPUManagerEnableInProgress" v-clean-tooltip="cpuManagerStatus">
<i class="icon icon-spinner icon-spin" />
</span>
<span v-else-if="row?.isCPUManagerEnabled" v-clean-tooltip="cpuManagerStatus">
<i class="icon icon-checkmark" />
</span>
<span
v-else
v-clean-tooltip="cpuManagerStatus"
class="text-muted"
>
&mdash;
</span>
</template>

View File

@ -23,77 +23,76 @@ export default {
default: ''
},
showReserved: {
showAllocated: {
type: Boolean,
default: false,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.longhornSettings = await this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.SETTINGS });
},
data() {
return {};
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornSettings = this.$store.getters[`${ inStore }/all`](LONGHORN.SETTINGS) || [];
return { longhornSettings };
},
computed: {
usage() {
storageStats() {
const stats = {
used: 0,
scheduled: 0,
maximum: 0,
reserved: 0,
total: 0
};
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`) || {};
const node = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`) || {};
const storageOverProvisioningPercentageSetting = this.longhornSettings.find(s => s.id === 'longhorn-system/storage-over-provisioning-percentage');
const disks = node?.spec?.disks || {};
const diskStatus = node?.status?.diskStatus || {};
return longhornNode?.used || 0;
},
stats.used += node?.spec?.allowScheduling ? node.used : 0;
reserved() {
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`);
let reserved = 0;
const disks = longhornNode?.spec?.disks || {};
Object.values(disks).map((disk) => {
if (disk.allowScheduling) {
reserved += disk.storageReserved;
}
Object.keys(disks).map((key) => {
stats.scheduled += node?.spec?.allowScheduling ? (diskStatus[key]?.storageScheduled || 0) : 0;
stats.reserved += disks[key]?.storageReserved || 0;
});
Object.values(diskStatus).map((diskStat) => {
stats.maximum += diskStat?.storageMaximum || 0;
});
return reserved;
},
stats.total = ((stats.maximum - stats.reserved) * Number(storageOverProvisioningPercentageSetting?.value ?? 0)) / 100;
total() {
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`);
let out = 0;
const diskStatus = longhornNode?.status?.diskStatus || {};
Object.values(diskStatus).map((disk) => {
if (disk?.storageMaximum) {
out += disk.storageMaximum;
}
});
return out;
return stats;
},
units() {
const exponent = exponentNeeded(this.total, 1024);
const exponent = exponentNeeded(this.storageStats.maximum, 1024);
return `${ UNITS[exponent] }iB`;
},
used() {
let out = this.formatter(this.usage || 0);
let out = this.formatter(this.storageStats.used);
if (!Number.parseFloat(out) > 0) {
out = this.formatter(this.usage || 0, { canRoundToZero: false });
out = this.formatter(this.storageStats.used, { canRoundToZero: false });
}
return out;
},
formatReserved() {
let out = this.formatter(this.reserved || 0);
formatAllocated() {
let out = this.formatter(this.storageStats.scheduled);
if (!Number.parseFloat(out) > 0) {
out = this.formatter(this.reserved || 0, { canRoundToZero: false });
out = this.formatter(this.storageStats.scheduled, { canRoundToZero: false });
}
return out;
@ -102,15 +101,15 @@ export default {
usedAmountTemplateValues() {
return {
used: this.used,
total: this.formatter(this.total || 0),
total: this.formatter(this.storageStats.maximum),
unit: this.units,
};
},
reservedAmountTemplateValues() {
allocatedAmountTemplateValues() {
return {
used: this.formatReserved,
total: this.formatter(this.total || 0),
used: this.formatAllocated,
total: this.formatter(this.storageStats.total),
unit: this.units,
};
},
@ -118,7 +117,7 @@ export default {
methods: {
formatter(value, format) {
const minExponent = exponentNeeded(this.total, 1024);
const minExponent = exponentNeeded(this.storageStats.maximum, 1024);
const formatOptions = {
addSuffix: false,
increment: 1024,
@ -137,21 +136,21 @@ export default {
<template>
<div>
<div
v-if="showReserved"
v-if="showAllocated"
>
<ConsumptionGauge
:capacity="total"
:used="reserved"
:capacity="storageStats.total"
:used="storageStats.scheduled"
:units="units"
:number-formatter="formatter"
:resource-name="resourceName"
>
<template #title="{formattedPercentage}">
<span>
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }}
{{ t('clusterIndexPage.hardwareResourceGauge.allocated') }}
</span>
<span class="precent-data">
{{ t('node.detail.glance.consumptionGauge.amount', reservedAmountTemplateValues) }}
{{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}
<span class="ml-10 percentage">
/&nbsp;{{ formattedPercentage }}
</span>
@ -160,13 +159,13 @@ export default {
</ConsumptionGauge>
</div>
<ConsumptionGauge
:capacity="total"
:used="usage"
:capacity="storageStats.maximum"
:used="storageStats.used"
:units="units"
:number-formatter="formatter"
:resource-name="showReserved ? '' : resourceName"
:resource-name="showAllocated ? '' : resourceName"
:class="{
'mt-10': showReserved,
'mt-10': showAllocated,
}"
>
<template #title="{formattedPercentage}">

View File

@ -6,14 +6,15 @@ generic:
labels: Labels
inProgress: In Progress
basic: Basic
loading: Loading...
nav:
group:
networks: Networks
backupAndSnapshot: Backup & Snapshot
backupAndSnapshot: Backup and Snapshots
Monitoring: Monitoring
Logging: Logging
'Monitoring & Logging': Monitoring & Logging
'Monitoring and Logging': Monitoring and Logging
resourceTable:
groupBy:
@ -28,7 +29,7 @@ members:
asyncButton:
restart:
action: Save & Restart
action: Save and Restart
success: Restarted
waiting: Restarting&hellip;
@ -38,9 +39,13 @@ harvester:
backup:
success: 'Backup { backUpName } has been initiated.'
addBackup: Add Backup
quota:
editVMQuota: Edit VM Quota
editQuota: Edit Quota
bannerMessage: Set to empty string or 0 to remove total snapshot size quota.
restore:
success: 'Restore { name } created successfully.'
title: Backup and restore
title: Backup and Restore
selectBackup: Select Backup
message:
backup: Please select the backup that needs to be restored.
@ -53,16 +58,16 @@ harvester:
success: 'Template { templateName } created successfully.'
failed: 'Failed generated template!'
cloneVM:
title: Clone VM
name: New VM Name
title: Clone Virtual Machine
name: New Virtual Machine Name
type: Clone volume data
action:
create: Create
clone: Clone
message:
tip: Please enter a VM name!
success: 'VM { name } cloned successfully.'
failed: 'Failed clone VM!'
tip: Please enter a virtual machine name!
success: 'Virtual machine { name } cloned successfully.'
failed: 'Failed clone virtual machine!'
exportImage:
title: Export to Image
name: Name
@ -77,21 +82,21 @@ harvester:
label: Target Node
placeholder: Choose Target Node
ejectCDROM:
title: Eject CDROM
title: Eject CD-ROM
warnTip: Eject volume will restart the virtual machine.
operationTip: 'Select the volume you want to delete:'
delete: Delete
bundle:
title: Generate Support Bundle
title: Generate a Support Bundle
url: Issue URL
description: Description
requiredDesc: Description is required!
titleDescription: Collect system-related log in Harvester, to help with troubleshooting and support.
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
hotplug:
success: 'Volume { diskName } is mounted to the VM { vm }.'
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
title: Add Volume
hotunplug:
success: 'Volume { name } is detach successfully.'
success: 'Volume { name } is detached successfully.'
snapshot:
title: Take Snapshot
name: Name
@ -109,10 +114,10 @@ harvester:
vmSnapshot:
title: Take VM Snapshot
name: Name
success: 'Take VM Snapshot { name } successfully.'
success: 'Take virtual machine Snapshot { name } successfully.'
restart:
title: Restart VM
tip: Restart the Virtual Machine now for configuration changes to take effect.
title: Restart Virtual Machine
tip: Restart the virtual machine for configuration changes to take effect.
cancel: Save
notification:
title:
@ -130,11 +135,12 @@ harvester:
deepClone: Clone
shallowClone: Clone Template
unpause: Unpause
ejectCDROM: Eject CDROM
ejectCDROM: Eject CD-ROM
editVMQuota: Edit VM Quota
launchFormTemplate: Launch instance from template
modifyTemplate: Modify template (Create new version)
setDefaultVersion: Set default version
addTemplateVersion: Add templateVersion
addTemplateVersion: Add template version
backup: Take Backup
restore: Restore
restoreNewVM: Restore New
@ -144,6 +150,8 @@ harvester:
createTemplate: Generate Template
enableMaintenance: Enable Maintenance Mode
disableMaintenance: Disable Maintenance Mode
enableCPUManager: Enable CPU Manager
disableCPUManager: Disable CPU Manager
cordon: Cordon
uncordon: Uncordon
addHotplug: Add Volume
@ -152,23 +160,25 @@ harvester:
cancelExpand: Cancel Expand
snapshot: Take Snapshot
pvcClone: Clone Volume
vmSnapshot: Take VM Snapshot
vmSnapshot: Take Virtual Machine Snapshot
shutdown: Shut Down
powerOn: Power On
reboot: Reboot
forceStop: Force Stop
tableHeaders:
size: Size
virtualSize: Virtual Size
progress: Progress
message: Message
phase: Phase
attachedVM: Attached VM
attachedVM: Attached Virtual Machine
cpuManager: CPU Manager
fingerprint: Fingerprint
value: Value
actions: Actions
readyToUse: Ready To Use
backupTarget: Backup Target
targetVm: Target VM
targetVm: Target Virtual Machine
hostIp: Host IP
vm:
ipAddress: IP Address
@ -176,10 +186,11 @@ harvester:
defaultVersion: Default Version
network:
type: Type
vlan: Vlan ID
vlan: VLAN ID
snapshotTargetVolume: Original Volume
volumeSnapshotCounts: Snapshot Counts
networkState: Network State
totalSnapshotQuota: Total Snapshot Quota
storageClass: Storage Class
restore: Restore
tab:
@ -188,8 +199,10 @@ harvester:
advanced: Advanced Options
accessCredentials: Access Credentials
pciDevices: PCI Devices
vGpuDevices: VGPU Devices
vmScheduling: VM Scheduling
vGpuDevices: vGPU Devices
vmScheduling: Virtual Machine Scheduling
quotas: Quotas
snapshots: Snapshots
instanceLabel: Instance Labels
fields:
version: Version
@ -200,24 +213,24 @@ harvester:
volume: Volume
network: Network
model: Model
macAddress: Mac Address
macAddress: MAC address
port: Port
protocol: Protocol
remove: REMOVE
remove: Remove
PhysicalNic: Physical NIC
cpu: Cpu
cpu: CPU
memory: Memory
virtualName: Virtual machine name
promiscuous: Promiscuous
ipv4Address: IPv4 Address
filterLabels: Filter Labels
storageClass: Storage Class
dockerImage: Docker Image
ipv4Address: IPv4 address
filterLabels: Filter labels
storageClass: Storage class
dockerImage: Docker image
pci:
available: Available Devices
compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this VM.'
howToUseDevice: 'Use the table below to enable PCI passthrough on each device you want to use in this virtual machine.'
deviceInTheSameHost: 'You can only select devices on the same host.'
oldFormatDevices:
help: |-
@ -228,11 +241,11 @@ harvester:
{oldFormatDevicesHTML}
</ul>
<p>
Please use the following instructions to update the VM:
Please use the following instructions to update the virtual machine:
</p>
<ol>
<li>Stop the VM, edit the VM YAML, and remove the <Code>hostDevices</Code> section, and save VM the changes to the YAML file.</li>
<li>Edit the VM, and add the already enabled PCIDevice from the list of available PCIDevices, and save and start VM.</li>
<li>Stop the virtual machine, edit the virtual machine YAML, and remove the <Code>hostDevices</Code> section, and save virtual machine the changes to the YAML file.</li>
<li>Edit the virtual machine, and add the already enabled PCI Device from the list of available PCIDevices, and save and start VM.</li>
</ol>
showCompatibility: Show device compatibility matrix
hideCompatibility: Hide device compatibility matrix
@ -241,12 +254,12 @@ harvester:
cantUnclaim: You cannot disable passthrough on a device claimed by another user.
enableGroup: Enable Group
disableGroup: Disable Group
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the VM's host."
labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host."
goSetting:
prefix: The pcidevices-controller addon is not enabled, click
prefix: The pcidevices-controller add-on is not enabled, click
middle: here
suffix: to enable it to manage your PCI devices.
noPCIPermission: Please contact system admin to enable the PCI devices first.
noPCIPermission: Please contact your system administrator to enable the PCI devices first.
enablePassthroughWarning: Please be careful not to use host-owned PCI devices (e.g., management and VLAN NICs). Incorrect device allocation may cause damage to your cluster, including node failure.
matrixHostName: Host Name
matrixDeviceClaimName: Device Claim Name
@ -299,20 +312,21 @@ harvester:
events:
label: Events
vmMetrics:
label: VM Metrics
label: Virtual Machine Metrics
version: Version
host:
console: Console
label: Hosts
inconsistentIP: "Host IP is inconsistent, current IP: { currentIP }, initial IP: { initIP }"
noConsoleUrl: 'Console URL not specified'
promote:
none: ' '
running: promoting
failed: promote failed
unknown: promote halted
promoteRestart: restarting
promoteSucceed: promote completed
running: Promoting
failed: Promote failed
unknown: Promote halted
promoteRestart: Restarting
promoteSucceed: Promote completed
tabs:
network: Network
overview: Overview
@ -322,10 +336,10 @@ harvester:
storage: Storage
labels: Labels
ksmtuned: Ksmtuned
seeder: Out-of-Band Access
seeder: Out-of-band Access
detail:
kvm:
disableMessage: Hardware-based virtualization is disabled or not supported. Hardware-based virtualization must be enabled before creating any Virtual Machines.
disableMessage: Hardware-based virtualization is disabled or not supported. Hardware-based virtualization must be enabled before creating any virtual machines.
title:
network: Network Configuration
hostIP: Host IP
@ -349,9 +363,11 @@ harvester:
serialNumber: Serial Number
model: Model
etcd: Witness Node
cpuManager: CPU Manager
enableMaintenance:
title: Enable Maintenance Mode
protip: The operation will migrate all virtual machines on this node to other nodes.
shutDownVMs: Check <b>Force</b> option to shutdown virtual machines which cannot be migrated in live mode.
force: Force
cordon:
title: Cordon
@ -360,15 +376,15 @@ harvester:
run: Run Strategy
configure: Configure
mode: Mode
modeLink: Mode <a href="https://docs.harvesterhci.io/v1.1/host/#ksmtuned-mode" target="_blank"><i class="icon icon-info" /></a>
modeLink: Mode <a href="{url}" target="_blank"><i class="icon icon-info" /></a>
thresCoef: Threshold Coefficient
enableMergeNodes: Enable Merge Across Nodes
enableMergeNodes: Enable merging across nodes
enable: Enable
disable: Disable
ksmStatus: KSM Status
modeOption:
standard: Standard
high: High-Perfomanace
high: High-performance
customized: Customized
parameters:
title: Ksmtuned Parameters
@ -400,11 +416,12 @@ harvester:
label: Storage Reserved
allowScheduling:
label: Scheduling
tooltip: Disk {name} ({path}) scheduling is disabled
evictionRequested:
label: Eviction Requested
forceFormatted:
label: Force Formatted
toolTip: Force formatted will cleanup disk data, make sure you backup all available data to prevent data loss.
toolTip: Force formatted will clean up disk data, make sure you backup all available data to prevent data loss.
yes: Yes (Ext4 File System)
description:
label: Description
@ -437,10 +454,8 @@ harvester:
tips: You can configure multiple IPv4 addresses or host addresses.
placeholder: e.g. IPv4
ntpSyncStatus:
isDisabled: 'The NTP is disabled. Please check the NTP service is active.'
isUnsynced: 'The NTP is not synced with the NTP server <code>{current}</code>'
ntpConfigSyncStatus:
isUnsynced: 'The NTP config is not synced. Current: <code>{current}</code>, Wanted: <code>{wanted}</code> '
isDisabled: 'NTP is disabled. Please check the NTP service is active.'
isUnsynced: 'NTP is not synchronized with the NTP server {current}.'
virtualMachine:
label: Virtual Machines
@ -453,7 +468,7 @@ harvester:
nameLabel: Name
host:
label: Hostname
placeholder: default to the virtual machine name.
placeholder: Default to the virtual machine name.
multiple:
label: Multiple Instance
nameNsDescription: Name prefix for each instance
@ -462,20 +477,20 @@ harvester:
nameLabel: Name Prefix
host:
label: Host Prefix Name
placeholder: default to the virtual machine name.
placeholder: Default to the virtual machine name.
useTemplate:
label: "Use VM Template:"
label: "Use the virtual machine template:"
template:
label: Template
version:
label: Version
console:
novnc: Open in Web VNC
novnc: Open in WebVNC
serial: Open in Serial Console
promptRemove:
title: 'Select the volume you want to delete:'
deleteAll: Delete All
tips: "Warn: The snapshots of vm will be deleted with VM 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
@ -487,6 +502,17 @@ harvester:
other {Start}
} the virtual machine now to take effect of the configuration changes.
runStrategy: Run Strategy
maintenanceStrategy:
label: Maintenance Strategy
options:
Migrate: Migrate
ShutdownAndRestartAfterEnable: Shutdown and Restart After Enable
ShutdownAndRestartAfterDisable: Shutdown and Restart After Disable
Shutdown: Shutdown
cpuPinning:
label: Enable CPU Pinning
tooltip: Enable CPU Pinning brings better performance and reduce latency for the virtual machine
restartVMMessage: Changing the CPU Pinning setting requires a virtual machine reboot for the change to take effect
restartNow: |-
{restart, select,
true {Restart}
@ -497,6 +523,10 @@ harvester:
enableUsb: Enable USB Tablet
advancedOptions:
tpm: Enable TPM
cpuManager:
prefix: You must enable CPU Manager for at least one node in
middle: 'host page'
suffix: to enable CPU Pinning for VM
usbTip: Provides an absolute pointer device which often helps with getting a consistent mouse cursor position in VNC.
sshTitle: Add Public SSH Key
imageTip: An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.
@ -504,7 +534,7 @@ harvester:
secureBoot: Secure Boot
volume:
dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order.
volumeTip: The VM only contains a cd-rom volume. You may want to add additional disk volumes.
volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes.
macTip: "MAC address as seen inside the guest system."
volumeUpdate: 'Set volume { name } successfully'
type: Type
@ -516,7 +546,7 @@ harvester:
dockerImage: Docker Image
addVolume: Add Volume
addExistingVolume: Add Existing Volume
addVmImage: Add VM Image
addVmImage: Add a Virtual Machine Image
addContainer: Add Container
setFirst: Set as root volume
saveVolume: Update Volume
@ -546,23 +576,23 @@ harvester:
network:
label: Network Data Template
title: "Network Data:"
tip: "The network-data configuration allows you to customize the instances networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, vlans) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
tip: "The network-data configuration allows you to customize the instances networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
scheduling:
affinity:
anyNode: 'Run VM on any available node'
schedulingRules: 'Run VM on node(s) matching scheduling rules'
specificNode: Run VM on specific node - (Live migration is not supported)
anyNode: 'Run virtual machine on any available node'
schedulingRules: 'Run virtual machine on node(s) matching scheduling rules'
specificNode: Run virtual machine on specific node - (Live migration is not supported)
networkNotSupport: Network not support schedule
accessCredentials:
resetPwd:
label: Add Basic Auth
injectSSH:
label: Add SSHKey
label: Add SSH Key
users: Select Users
addUser: Add User
tips: qemu-guest-agent must be installed to enable Access Credentials, the VM should be restarted after credentials added. Need to enter the VM to edit password or remove SSH-Key after deleting the credentials.
tips: qemu-guest-agent must be installed to enable the accessing of credentials. The virtual machine needs to be restarted after credentials added. You need to be in the virtual machine to edit your password or remove an SSH-Key after deleting the credentials.
userTips: The user to be added must already exist; otherwise, the credentials will not take effect.
duplicatedUser: User already exist.
duplicatedUser: User already exists.
invalidUser: Invalid Username.
input:
name: Name
@ -594,7 +624,7 @@ harvester:
monitor: Monitor Data
keypairs: SSH Keys
cloudConfig: Cloud Config
metrics: VM Metrics
metrics: Virtual Machine Metrics
details:
title:
vmDetails: Virtual Machine Details
@ -602,6 +632,7 @@ harvester:
services: Services
users: Logged in users
name: Name
totalSnapshotQuota: Total Snapshot Quota
namespace: Namespace
created: Created
hostname: Hostname
@ -623,7 +654,7 @@ harvester:
flavor: Flavor
tolerations: Tolerations
dedicatedResources: Dedicated Resources
down: VM not running
down: Virtual machine not running
affinityRules: Affinity Rules
sourceNode: Source Node
targetNode: Target Node
@ -636,7 +667,7 @@ harvester:
from: Generated from
down: No events in the past hour
console:
down: This Virtual Machine is down. Please start it to access its console.
down: This virtual machine is down. Please start it to access its console.
shortcutKeys: Shortcut Keys
customShortcutKeys: Custom Shortcut Keys
management: Management Shortcut Keys
@ -644,13 +675,13 @@ harvester:
start: Record
recording: Recording
stop: Stop Recording
tips: Pressing the record button will capture your keyboard inputs.
tips: Press the record button to capture your keyboard inputs.
send: Send
preferredKeys: Preferred Custom Shortcut Keys
terminationGracePeriodSeconds:
label: Termination Grace Period
affinity:
thisPodNamespace: This VM's namespace
thisPodNamespace: This virtual machine's namespace
matchExpressions:
inNamespaces: "Workloads in these namespaces"
namespaces:
@ -668,7 +699,7 @@ harvester:
kind: Kind
sourceOptions:
new: New
vmImage: VM Image
vmImage: Virtual Machine Image
image: Image
frontend: Frontend
blockdev: Block Device
@ -680,7 +711,7 @@ harvester:
lastBackupAt: Last Backup At
replicasNumber: Replicas Number
promptRemove:
tips: "Warn: The snapshots of volume will be deleted with volume."
tips: "Warn: The volume's snapshots will be deleted with this volume."
externalLink:
tips: Check volume details
rebuildingMessage: 'Rebuilding: {percentage}%'
@ -691,7 +722,8 @@ harvester:
basics: Basics
url: URL
size: Size
urlTip: 'supports the <code>raw</code> and <code>qcow2</code> image formats which are supported by <a href="https://www.qemu.org/docs/master/system/images.html#disk-image-file-formats" target="_blank">qemu</a>. Bootable ISO images can also be used and are treated like <code>raw</code> images.'
virtualSize: Virtual Size
urlTip: 'Supports the <code>raw</code> and <code>qcow2</code> image formats which are supported by <a href="https://www.qemu.org/docs/master/system/images.html#disk-image-file-formats" target="_blank">qemu</a>. Bootable ISO images can also be used and are treated like <code>raw</code> images.'
fileName: File Name
uploadFile: Upload File
source: Source
@ -716,10 +748,10 @@ harvester:
tips:
notExistImage:
title: Image {name} does not exist!
message: Please select a new Image.
message: Please select a new image.
notExistNode:
title: Node {name} does not exist!
message: Please select a new Node.
message: Please select a new node.
upgradePage:
upgradeApp: Upgrade Software
@ -736,8 +768,8 @@ harvester:
selectExisting: Select Existing Image
createRepository: Creating Upgrade Repository
succeeded: Succeeded
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Note</a>.
checkReady: I have read and understood the upgrade content related to this Harvester version.
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
pending: Pending
repoInfo:
upgradeStatus: Upgrade Status
@ -747,29 +779,29 @@ harvester:
harvesterChart: Harvester Chart
success: Success
fail: Fail
ongoing: on-going
ongoing: On-going
downloadLog: Download Log
logStatus: Log Download Status
dismissMessage: Dismiss it
upgradeInfo:
warning: WARNING
doc: Before you upgrade to the newer Harvester version, you must perform the required <a href="https://docs.harvesterhci.io/v1.0/upgrade/automatic/" target="_blank"> pre-upgrade checks </a>for your cluster. Complete only those tasks that apply to your environment.
tip: Failure to perform these checks may result in a failed upgrade or hitting known issues that require a manual workaround fix.
doc: Read the <a href="{url}" target="_blank">documentation</a> before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to.
tip: Unmet system requirements and incorrectly performed procedures may cause complete upgrade failure and other issues that require manual workarounds.
moreNotes: For more details about the release notes, please visit -
backup:
label: VM Backups
label: Virtual Machine Backups
createText: Restore Backup
title: Restore Virtual Machine
backupTargetTip: The endpoint used to access the backupstore. NFS and S3 are supported.
message:
noSetting:
prefix: You must configure the backup target in
prefix: You must configure the backup target
middle: 'setting'
suffix: before creating a new backup.
errorTip:
prefix: Backup Target value in
middle: Setting
prefix: Backup target value in
middle: setting
suffix: "is invalid, error: "
viewSetting:
prefix: Click
@ -804,21 +836,21 @@ harvester:
complete: Restore completed
network:
label: VM Networks
label: Virtual Machine Networks
tabs:
basics: Basics
layer3Network: Route
clusterNetwork:
label: Cluster Network
create: Create a New Cluster Network
create: Create a new cluster network
toolTip: Define your custom cluster scope network name
createPlaceholder: Input a new Cluster Network name
selectOrCreatePlaceholder: Select or Create a new Cluster Network
selectPlaceholder: Select a Cluster Network
createPlaceholder: Input a new cluster network name
selectOrCreatePlaceholder: Select or create a new cluster network
selectPlaceholder: Select a cluster network
layer3Network:
mode:
label: Mode
auto: Auto(DHCP)
auto: Auto (DHCP)
manual: Manual
serverIPAddr:
label: DHCP Server IP
@ -842,9 +874,9 @@ harvester:
validation:
physicalNIC: DefaultPhysicalNIC
placeholder:
accessKeyId: specify your access key id
secretAccessKey: specify your secret access key
cert: upload a self-signed SSL certificate
accessKeyId: Specify your access key ID
secretAccessKey: Specify your secret access key
cert: Upload a self-signed SSL certificate
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
defaultPhysicalNIC: Default Network Interface
percentTip: The value in parentheses represents the distribution percentage of the network interface on all hosts. If an interface less than 100% is selected, the user needs to manually specify the network interface on the host where the vlan network configuration fails.
@ -869,10 +901,11 @@ harvester:
placeholder: e.g. 172.16.0.1/32
invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all VMs before applying this setting. <br/> Users have to ensure the Cluster Network is configured and VLAN Config will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
tip: 'IP Range should be in IPV4 format. <code>Number of IPs Required = Number of Nodes * 4 + Number of Disks * 2 + Number of Images to Download/Upload </code>. See <a href="https://docs.harvesterhci.io/v1.2/advanced/storagenetwork#configuration-example" target="_blank">doc</a> for more details.'
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 4 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
vmForceDeletionPolicy:
period: Period
ratio : Ratio
autoRotateRKE2Certs:
expiringInHours: Expiring in
httpProxy:
@ -907,7 +940,7 @@ harvester:
upgrade:
selectExitImage: Please select the OS image to upgrade.
imageUrl: Please input a valid image url.
imageUrl: Please input a valid image URL.
chooseFile: Please select to upload an image.
checksum: Checksum
harvesterMonitoring:
@ -923,12 +956,12 @@ harvester:
retention: How long to retain metrics
retentionSize: Maximum size of metrics
clusterRegistrationUrl:
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher dashboard UI via the <code> Virtualization Management </code> page.
message: To completely unset the imported Harvester cluster, please also remove it on the Rancher Dashboard UI via the <code> Virtualization Management </code> page.
ntpServers:
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.
cloudTemplate:
label: Cloud Config Templates
label: Cloud Configuration Templates
templateType: Template Type
userData: User Data
networkData: Network Data
@ -941,7 +974,7 @@ harvester:
internal:
rancher:
title: Access Embedded Rancher UI
titleDescription: We only support to use the embedded Rancher dashboard for debugging and validation purpose. For Rancher's multi-cluster and multi-tenant integration, please refer to the docs <a target="_blank" href="https://docs.harvesterhci.io/v1.2/rancher/index" rel="noopener noreferrer nofollow">here</a>.
titleDescription: You can only use the embedded Rancher UI for debugging and validation purposes. For more information about how Harvester integrates with Rancher, see the <a target="_blank" href="{url}" rel="noopener noreferrer nofollow">documentation</a>.
longhorn:
title: Access Embedded Longhorn UI
titleDescription: We only support to use the embedded Longhorn UI for debugging and validation purpose.
@ -961,7 +994,7 @@ harvester:
cidr:
label: CIDR/IP Range
invalid: '"CIDR/IP Range" is invalid.'
toolTip: "We can apply multiple pools or ranges by seperating them with commas. i.e. 192.168.0.200/30,192.168.0.200/29 or 192.168.0.10-192.168.0.11"
toolTip: "We can apply multiple pools or ranges by separating them with commas. For example: 192.168.0.200/30,192.168.0.200/29 or 192.168.0.10-192.168.0.11"
add:
label: Add IP Pools
@ -977,15 +1010,15 @@ harvester:
alertmanagerConfig:
label: Alertmanager Configs
diabledMonitoringTips:
prefix: 'You must enable'
middle: 'Monitoring'
suffix: 'addon at first.'
prefix: 'Enable the'
middle: 'monitoring'
suffix: 'add-on first.'
diabledAlertingTips:
prefix: 'You must enable'
prefix: 'Enable'
middle: 'Alertmanager'
suffix: 'for configs to take effect.'
suffix: 'for configuration to take effect.'
disabledAddon:
prefix: 'Monitoring Addon is disabled now, click'
prefix: 'The monitoring add-on is disabled, click'
middle: 'here'
suffix: 'to enable it.'
@ -1003,18 +1036,19 @@ harvester:
output:
label: Output
diabledTips:
prefix: 'You must enable'
middle: 'Logging'
suffix: 'for configs to take effect.'
prefix: 'Enable'
middle: 'logging'
suffix: 'for configuration to take effect.'
snapshot:
totalSnapshotSize: Total Snapshot Size
label: Volume Snapshots
targetVolume: Original Volume
size: Size
image: Image
vmSnapshot:
label: VM Snapshots
label: Virtual Machine Snapshots
createText: Restore Snapshot
snapshot: Snapshot
@ -1036,7 +1070,7 @@ harvester:
title: Storage Classes
customize:
volumeBindingMode:
later: Bind and provision a persistent volume once a VM using the PersistentVolumeClaim is created
later: Bind and provision a persistent volume once a virtual machine using the PersistentVolumeClaim is created
parameters:
numberOfReplicas:
label: Number Of Replicas
@ -1052,11 +1086,11 @@ harvester:
label: Migratable
allowedTopologies:
title: Allowed Topologies
tooltip: Allowed Topologies helps scheduling VMs on hosts which match all of below expressions.
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
vlanConfig:
title: Network Configs
createNetworkConfig: Create Network Config
title: Network Configuration
createNetworkConfig: Create Network Configuration
action:
migrate: Migrate
titles:
@ -1097,39 +1131,39 @@ harvester:
vlanStatus:
vlanConfig:
label: Network Config
label: Network Configuration
clusterNetwork:
title: Cluster Networks/Configs
title: Cluster Network Configuration
create:
button:
label: Create Cluster Network
clusterNetwork: There are no network configs defined.
label: Create a Cluster Network
clusterNetwork: There are no network configurations defined.
mgmt: mgmt is a built-in cluster management network and does not support any additional network configurations.
notExist: 'Cluster Network "{ clusterNetwork }" does not exist'
notReady: 'Cluster Network "{ clusterNetwork }" is not ready'
addons:
descriptions:
'harvester-system/vm-import-controller': vm-import-controller is an addon to help migrate VM workloads from other source clusters to an existing Harvester cluster.
'harvester-system/pcidevices-controller': pcidevices-controller is an addon to help discover PCI devices for nodes in your cluster and allow users to prepare devices for PCI Passthrough, for use with Harvester VM and guest Clusters.
'cattle-logging-system/rancher-logging': rancher-logging is an addon to collect versatile logs, events and audits from the Harvester cluster and route them to many kinds of servers based on flows.
'harvester-system/rancher-vcluster': rancher-vcluster deploys a vcluster with rancher installed.
'cattle-monitoring-system/rancher-monitoring': rancher-monitoring is an addon to collect Harvester cluster and VM metrics, view them on the embedded dashboard, and send alert(s) to remote servers.
'vm-import-controller': vm-import-controller is an addon to help migrate VM workloads from other source clusters to an existing Harvester cluster.
'pcidevices-controller': pcidevices-controller is an addon to help discover PCI devices for nodes in your cluster and allow users to prepare devices for PCI Passthrough, for use with Harvester VM and guest Clusters.
'nvidia-driver-toolkit': 'nvidia-driver-toolkit is an addon to enable vGPU devices and assign them to Harvester VMs.'
'rancher-logging': rancher-logging is an addon to collect versatile logs, events and audits from the Harvester cluster and route them to many kinds of servers based on flows.
'rancher-monitoring': rancher-monitoring is an addon to collect Harvester cluster and VM metrics, view them on the embedded dashboard, and send alert(s) to remote servers.
'rancher-vcluster': rancher-vcluster deploys a vcluster with rancher installed.
'harvester-seeder': harvester-seeder is an addon that uses ipmi and redfish to discover hardware information and perform out-of-band operations.
'harvester-system/harvester-seeder': harvester-seeder is an addon that uses ipmi and redfish to discover hardware information and perform out-of-band operations.
'harvester-system/vm-import-controller': vm-import-controller is an add-on to help migrate virtual machine workloads from other source clusters to an existing Harvester cluster.
'harvester-system/pcidevices-controller': pcidevices-controller is an add-on to help discover PCI devices for nodes in your cluster and allow users to prepare devices for PCI Passthrough, for use with Harvester virtual machine and guest clusters.
'cattle-logging-system/rancher-logging': rancher-logging is an add-on to collect versatile logs, events, and audits from the Harvester cluster and route them to many kinds of servers based on flows.
'harvester-system/rancher-vcluster': rancher-vcluster deploys a virtual cluster (vcluster) with Rancher installed.
'cattle-monitoring-system/rancher-monitoring': rancher-monitoring is an add-on that collects Harvester cluster and virtual machine metrics and allows you to view the metrics on an embedded dashboard and send alert(s) to remote servers.
'vm-import-controller': vm-import-controller is an add-on to help migrate virtual machine workloads from other source clusters to an existing Harvester cluster.
'pcidevices-controller': pcidevices-controller is an add-on to help discover PCI devices for nodes in your cluster and allow users to prepare devices for PCI Passthrough, for use with Harvester virtual machines and guest clusters.
'nvidia-driver-toolkit': 'nvidia-driver-toolkit is an add-on to enable vGPU devices and assign them to Harvester virtual machines.'
'rancher-logging': rancher-logging is an add-on to collect versatile logs, events, and audits from the Harvester cluster and route them to many kinds of servers based on flows.
'rancher-monitoring': rancher-monitoring is an add-on to collect Harvester cluster and virtual machine metrics, view them on the embedded dashboard, and send alert(s) to remote servers.
'rancher-vcluster': rancher-vcluster deploys a virtual cluster (vcluster) with Rancher installed.
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
vmImport:
titles:
basic: Basic
pvc: Volume
rancherVcluster:
accessRancher: Access Rancher Dashboard
accessRancher: Access the Rancher Dashboard
hostname: Hostname
rancherVersion: Rancher Version
password: Bootstrap Password
@ -1190,12 +1224,12 @@ harvester:
label: Backend Servers
healthCheck:
warning:
portInUse: Warning, Backend Port {port} is in use in Health Check settings; in case of updating the port, update the Health Check settings accordingly.
portInUse: Warning, the Backend Port {port} is in use in Health Check settings. If you need to update the port, update the Health Check settings accordingly.
ipPool:
label: IP Pools
network:
label: VM Network
label: Virtual Machine Network
tabs:
range: Range
scope: Scope
@ -1231,7 +1265,7 @@ harvester:
label: Health Check Port
healthCheckSuccessThreshold:
label: Health Check Success Threshold
description: If the number of times the prober continuously detects an address successfully reaches the success threshold, then the backend server can start to forward traffic.
description: If the number of times the probe continuously detects an address successfully reaches the success threshold, then the backend server can start to forward traffic.
healthCheckFailureThreshold:
label: Health Check Failure Threshold
description: The backend server will stop forwarding traffic if the number of health check failures reaches the failure threshold.
@ -1256,22 +1290,22 @@ harvester:
sriovgpu:
label: SR-IOV GPU Devices
nodeName: Node
numVFs: Number Of Virtual Functions
numVFs: Number of Virtual Functions
vfAddresses: Virtual Functions Addresses
vGpuDevices: vGPU Devices
showMore: Show More
parentSriov: Filter By Parent SR-IOV GPU
noPermission: Please contact system admin to add Harvester addons first
noPermission: Please contact your system admiistrator to add Harvester add-ons first.
goSetting:
prefix: The nvidia-driver-toolkit addon is not enabled, click
prefix: The nvidia-driver-toolkit add-on is not enabled, click
middle: here
suffix: to enable it to manage your SR-IOV GPU devices.
vgpu:
label: vGPU Devices
noPermission: Please contact system admin to add Harvester addons first
noPermission: Please contact system administrator to add Harvester add-ons first.
goSetting:
prefix: The nvidia-driver-toolkit addon is not enabled, click
prefix: The nvidia-driver-toolkit add-on is not enabled, click
middle: here
suffix: to enable it to manage your vGPU devices.
enableGroup: Enable Group
@ -1282,7 +1316,7 @@ harvester:
available: Available Devices
compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.'
howToUseDevice: 'Use the table below to enable vGPU devices you want to use in this VM.'
howToUseDevice: 'Use the table below to enable vGPU devices you want to use in this virtual machine.'
deviceInTheSameHost: 'You can only select devices on the same host.'
harvesterVlanConfigMigrateDialog:
@ -1293,11 +1327,11 @@ harvester:
seeder:
banner:
enable:
prefix: Addon "harvester-seeder" is disabled now,
middle: click here
prefix: The "harvester-seeder" add-on is disabled.
middle: Click here
suffix: to enable it.
noAccess: Please contact system admin to enable the Out-of-Band Access first.
noAddon: Addon "harvester-seeder" is not exist, please check if it is installed.
noAccess: Please contact your system administrator to enable the Out-of-Band Access first.
noAddon: The "harvester-seeder" add-on does not exist, please check if it is installed.
noInventory: Waiting for "inventories.metal.harvesterhci.io" to be ready.
inventory:
host:
@ -1318,10 +1352,10 @@ harvester:
label: Polling Interval
affinity:
thisPodNamespace: This VM's namespace
thisPodNamespace: This virtual machine's namespace
matchExpressions:
inNamespaces: "Workloads in these namespaces"
vmAffinityTitle: VM Scheduling
vmAffinityTitle: Virtual Machine Scheduling
namespaces:
placeholder: e.g. default,system,base
label: Namespaces
@ -1330,35 +1364,40 @@ harvester:
placeholder: 'topology.kubernetes.io/zone'
advancedSettings:
technicalPreview: 'Technical Previews allow users to test and evaluate early-access functionality prior to official supported releases'
descriptions:
'harv-vlan': Default Network Interface name of the VLAN network.
'harv-backup-target': Custom backup target to store VM backups.
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme.
'harv-csi-driver-config': Configure additional information for csi drivers.
'harv-backup-target': Custom backup target to store virtual machine backups.
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
'harv-csi-driver-config': Configure additional information for CSI drivers.
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
'harv-log-level': Configure Harvester server log level. Default to info.
'harv-log-level': Configure Harvester server log level. Defaults to Info.
'harv-server-version': Harvester server version.
'harv-upgrade-checker-enabled': Specify whether to enable Harvester upgrade check or not. Default is true.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to true.
'harv-ui-source': Config how to load the UI source.
'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True.
'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the <code>upgrade-checker-enabled</code> is equal to True.
'harv-ui-source': Configure how to load the UI source.
'harv-ui-index': 'HTML index location for the UI.'
'harv-ui-plugin-index': 'JS index location for the harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for mutil-cluster management.
'harv-ui-plugin-index': 'JS index location for the Harvester plugin UI.'
'harv-cluster-registration-url': Registration URL for multi-cluster management.
'harv-http-proxy': 'HTTP proxy for Harvester to access external services.'
'harv-additional-ca': 'Custom CA root certificates for TLS validation.'
'harv-overcommit-config': 'Resource overcommit configuration.'
'harv-support-bundle-timeout': 'Support Bundle timeout config in minutes, use 0 to disable the timeout.'
'harv-support-bundle-expiration': 'Support Bundle expiration config in minutes.'
'harv-vm-force-reset-policy': Config the force-reset action when a VM is stuck on a node that is down.
'harv-support-bundle-timeout': 'Support bundle timeout configuration in minutes, use 0 to disable the timeout.'
'harv-support-bundle-expiration': 'Support bundle expiration configuration in minutes.'
'harv-support-bundle-node-collection-timeout': 'Support bundle node collection timeout configuration in minutes.'
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
'harv-storage-network': 'Longhorn storage-network setting.'
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as VM storage.
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
'harv-default-vm-termination-grace-period-seconds': Config the VM termination grace period for VM stop.
'harv-default-vm-termination-grace-period-seconds': Configure the virtual machine termination grace period for virtual machine stop.
'harv-ntp-servers': Configure NTP server. You can configure multiple IPv4 addresses or host addresses.
'harv-auto-rotate-rke2-certs': The certificate rotation mechanism relies on Rancher. Harvester will automatically update certificates generation to trigger rotation.
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
typeLabel:
kubevirt.io.virtualmachine: |-
@ -1383,8 +1422,8 @@ typeLabel:
}
harvesterhci.io.networkattachmentdefinition: |-
{count, plural,
one { VM Network }
other { VM Networks }
one { Virtual Machines Network }
other { Virtual Machines Networks }
}
harvesterhci.io.volume: |-
{count, plural,
@ -1408,13 +1447,13 @@ typeLabel:
}
harvesterhci.io.virtualmachinebackup: |-
{count, plural,
one { VM Backup }
other { VM Backups }
one { Virtual Machines Backup }
other { Virtual Machines Backups }
}
harvesterhci.io.cloudtemplate: |-
{count, plural,
one { Cloud Config Template }
other { Cloud Config Templates }
one { Cloud Configuration Template }
other { Cloud Configuration Templates }
}
harvesterhci.io.volumesnapshot: |-
{count, plural,
@ -1423,18 +1462,18 @@ typeLabel:
}
harvesterhci.io.vmsnapshot: |-
{count, plural,
one { VM Snapshot }
other { VM Snapshots }
one { Virtual Machines Snapshot }
other { Virtual Machines Snapshots }
}
network.harvesterhci.io.vlanconfig: |-
{count, plural,
one { Network Config }
other { Network Configs }
one { Network Configuration }
other { Network Configurations }
}
harvesterhci.io.monitoring.alertmanagerconfig: |-
{count, plural,
one { Alertmanager Config }
other { Alertmanager Configs }
one { Alertmanager Configuration }
other { Alertmanager Configurations }
}
harvesterhci.io.logging.clusterflow: |-
{count, plural,
@ -1514,30 +1553,3 @@ typeLabel:
one { IP Pool }
other { IP Pools }
}
harvesterManager:
manage: Manage
cluster:
label: Harvester Clusters
none: There are no Harvester Clusters
learnMore: Learn more about Harvester from the <a target="_blank" href="https://harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Web Site</a> or read the the <a target="_blank" href="https://docs.harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Docs</a>
description: Harvester is a modern Hyperconverged infrastructure (HCI) solution built for bare metal servers using enterprise-grade open source technologies including Kubernetes, Kubevirt and Longhorn.
plugins:
loadError: Error loading harvester plugin
rke:
templateError: Incorrect template format
affinity:
thisPodNamespace: This VM's namespace
matchExpressions:
inNamespaces: "Workloads in these namespaces"
vmAffinityTitle: VM Scheduling
namespaces:
placeholder: e.g. default,system,base
label: Namespaces
addLabel: Add Workload Selector
topologyKey:
placeholder: 'topology.kubernetes.io/zone'
vGpu:
title: VGPUs
label: VGPU type
placeholder: 'Please select a VGPU'

File diff suppressed because it is too large Load Diff

View File

@ -141,7 +141,8 @@ export default {
settings: this.fetchClusterResources(HCI.SETTING),
services: this.fetchClusterResources(SERVICE),
metric: this.fetchClusterResources(METRIC.NODE),
longhornNode: this.fetchClusterResources(LONGHORN.NODES) || [],
longhornNodes: this.fetchClusterResources(LONGHORN.NODES),
longhornSettings: this.fetchClusterResources(LONGHORN.SETTINGS),
_pods: this.$store.dispatch('harvester/findAll', { type: POD }),
};
@ -155,6 +156,10 @@ export default {
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS });
}
if (this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES)) {
this.hasLonghornSchema = true;
}
const res = await allHash(hash);
for ( const k in res ) {
@ -226,6 +231,7 @@ export default {
showClusterMetrics: false,
showVmMetrics: false,
enabledMonitoringAddon: false,
hasLonghornSchema: false,
};
},
@ -305,8 +311,7 @@ export default {
currentVersion() {
const inStore = this.$store.getters['currentProduct'].inStore;
const settings = this.$store.getters[`${ inStore }/all`](HCI.SETTING);
const setting = settings.find( S => S.id === 'server-version');
const setting = this.$store.getters[`${ inStore }/byId`](HCI.SETTING, 'server-version');
return setting?.value || setting?.default;
},
@ -364,53 +369,46 @@ export default {
return out;
},
storageUsage() {
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES) || [];
return longhornNodes.filter(node => node.spec?.allowScheduling).reduce((total, node) => {
return total + node.used;
}, 0);
},
storageReservedTotal() {
let out = 0;
(this.longhornNode || []).filter(node => node.spec?.allowScheduling).forEach((node) => {
storageStats() {
const storageOverProvisioningPercentageSetting = this.longhornSettings.find(s => s.id === 'longhorn-system/storage-over-provisioning-percentage');
const stats = this.longhornNodes.reduce((total, node) => {
const disks = node?.spec?.disks || {};
Object.values(disks).map((disk) => {
if (disk.allowScheduling) {
out += disk.storageReserved;
}
});
});
return out;
},
storageTotal() {
let out = 0;
(this.longhornNode || []).forEach((node) => {
const diskStatus = node?.status?.diskStatus || {};
Object.values(diskStatus).map((disk) => {
if (disk?.storageMaximum) {
out += disk.storageMaximum;
}
total.used += node?.spec?.allowScheduling ? node.used : 0;
Object.keys(disks).map((key) => {
total.scheduled += node?.spec?.allowScheduling ? (diskStatus[key]?.storageScheduled || 0) : 0;
total.reserved += disks[key]?.storageReserved || 0;
});
Object.values(diskStatus).map((diskStat) => {
total.maximum += diskStat?.storageMaximum || 0;
});
return out;
return total;
}, {
used: 0,
scheduled: 0,
maximum: 0,
reserved: 0,
total: 0
});
stats.total = ((stats.maximum - stats.reserved) * Number(storageOverProvisioningPercentageSetting?.value ?? 0)) / 100;
return stats;
},
storageUsed() {
return this.createMemoryValues(this.storageTotal, this.storageUsage);
const stats = this.storageStats;
return this.createMemoryValues(stats.maximum, stats.used);
},
storageReserved() {
return this.createMemoryValues(this.storageTotal, this.storageReservedTotal);
storageAllocated() {
const stats = this.storageStats;
return this.createMemoryValues(stats.total, stats.scheduled);
},
vmEvents() {
@ -644,7 +642,7 @@ export default {
<div
class="hardware-resource-gauges"
:class="{
live: !storageTotal,
live: !hasLonghornSchema,
}"
>
<HardwareResourceGauge
@ -658,10 +656,11 @@ export default {
:used="ramUsed"
/>
<HardwareResourceGauge
v-if="storageTotal"
v-if="hasLonghornSchema"
:name="t('harvester.dashboard.hardwareResourceGauge.storage')"
:used="storageUsed"
:reserved="storageReserved"
:reserved="storageAllocated"
:reserved-title="t('clusterIndexPage.hardwareResourceGauge.allocated')"
/>
</div>
</template>

View File

@ -8,6 +8,7 @@ import {
import { allHash } from '@shell/utils/promise';
import metricPoller from '@shell/mixins/metric-poller';
import { HCI } from '../types';
import { DOC_LINKS } from '../config/doc-links';
const schema = {
id: HCI.HOST,
@ -91,6 +92,7 @@ export default {
value: 'internalIp',
formatter: 'CopyToClipboard',
sort: ['internalIp'],
align: 'center',
},
];
@ -98,7 +100,7 @@ export default {
const metricCol = [
{
name: 'cpu',
labelKey: 'tableHeaders.cpu',
labelKey: 'node.detail.glance.consumptionGauge.cpu',
value: 'id',
formatter: 'HarvesterCPUUsed',
formatterOpts: { showUsed: true },
@ -121,12 +123,23 @@ export default {
labelKey: 'tableHeaders.storage',
value: 'id',
formatter: 'HarvesterStorageUsed',
formatterOpts: { showReserved: true },
formatterOpts: { showAllocated: true },
};
out.splice(-1, 0, storageHeader);
}
out.push({
name: 'cpuManager',
labelKey: 'harvester.tableHeaders.cpuManager',
value: 'id',
formatter: 'HarvesterCPUPinning',
formatterOpts: { rows: this.rows },
width: 150,
align: 'center',
});
if (this.hasLonghornSchema) {
out.push({
name: 'diskState',
@ -143,7 +156,7 @@ export default {
name: 'console',
label: ' ',
align: 'right',
width: 65,
width: 80,
});
return out;
@ -151,6 +164,10 @@ export default {
schema() {
return schema;
},
consoleDocLink() {
return DOC_LINKS.CONSOLE_URL;
}
},
methods: {
@ -169,7 +186,15 @@ export default {
goto(row) {
window.open(row.consoleUrl, '_blank');
},
consoleTooltip(row) {
if (!row.consoleUrl) {
return this.t('harvester.host.noConsoleUrl');
}
return '';
},
},
typeDisplay() {
@ -199,10 +224,19 @@ export default {
>
<template #cell:console="{row}">
<button type="button" class="btn btn-sm role-primary" :disabled="!row.consoleUrl" @click="goto(row)">
<div class="console-button">
<button v-clean-tooltip="consoleTooltip(row)" type="button" class="mr-5 btn btn-sm role-primary" :disabled="!row.consoleUrl" @click="goto(row)">
{{ t('harvester.host.console') }}
</button>
<a v-if="!row.consoleUrl" :href="consoleDocLink" target="_blank"><i class="icon icon-info" /></a>
</div>
</template>
</ResourceTable>
</div>
</template>
<style lang="scss" scoped>
.console-button {
display: flex;
}
</style>

View File

@ -103,7 +103,6 @@ export default {
:schema="schema"
:rows="rows"
key-field="_key"
/>
</div>
</template>

View File

@ -56,8 +56,6 @@ export default {
this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false });
}
console.log('ppppppppppp', hash);
this.rows = hash.pvcs;
},
@ -151,23 +149,23 @@ export default {
>
<template
cell:state="scope"
>
cell:state="scope"
>
<div class="state">
<HarvesterVolumeState
class="vmstate"
:row="scope.row"
/>
class="vmstate"
:row="scope.row"
/>
</div>
</template>
<template
cell:AttachedVM="scope"
>
cell:AttachedVM="scope"
>
<div>
<router-link
v-if="getVMName(scope.row)"
:to="goTo(scope.row)"
>
v-if="getVMName(scope.row)"
:to="goTo(scope.row)"
>
{{ getVMName(scope.row) }}
</router-link>
</div>

View File

@ -81,6 +81,10 @@ export default {
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
};
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA)) {
_hash.resourceQuotas = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESOURCE_QUOTA });
}
if (this.$store.getters[`${ inStore }/schemaFor`](NODE)) {
_hash.nodes = this.$store.dispatch(`${ inStore }/findAll`, { type: NODE });
this.hasNode = true;
@ -116,7 +120,7 @@ export default {
headers() {
const restoreCol = {
name: 'restoreProgress',
labelKey: 'tableHeaders.restore',
labelKey: 'harvester.tableHeaders.restore',
value: 'restoreProgress',
align: 'left',
formatter: 'HarvesterBackupProgressBar',
@ -128,7 +132,7 @@ export default {
value: 'nodeName',
sort: ['realAttachNodeName'],
formatter: 'HarvesterHost',
labelKey: 'tableHeaders.node'
labelKey: 'harvester.tableHeaders.vm.node'
};
const cols = clone(VM_HEADERS);
@ -137,7 +141,7 @@ export default {
cols.splice(-1, 0, nodeCol);
}
if (this.hasRestoredVMs) {
if (this.hasBackUpRestoreInProgress) {
cols.splice(-1, 0, restoreCol);
}
@ -150,8 +154,11 @@ export default {
return [...this.allVMs, ...matchVMIs];
},
hasRestoredVMs() {
return !!this.rows.find(r => !!r.restoreResource);
/**
* We want to show the progress bar only for Backup's restore; snapshot's restore is immediate.
*/
hasBackUpRestoreInProgress() {
return !!this.rows.find(r => r.restoreResource && !r.restoreResource.fromSnapshot && !r.restoreResource.isComplete);
}
},
@ -179,7 +186,7 @@ export default {
key-field="_key"
>
<template cell:state="scope">
<template cell:state="scope" class="state-col">
<div class="state">
<HarvesterVmState class="vmstate" :row="scope.row" :all-node-network="allNodeNetworks" :all-cluster-network="allClusterNetworks" />
</div>

View File

@ -144,6 +144,10 @@ export default {
return !!spec?.template?.spec?.domain?.firmware?.bootloader?.efi?.secureBoot;
},
isCpuPinning(spec) {
return !!spec?.template?.spec?.domain?.cpu?.dedicatedCpuPlacement;
},
getCloudInitNoCloud(spec) {
const secret = this.getSecret(spec);
let userData = secret?.decodedData?.userdata;

View File

@ -18,7 +18,7 @@ import {
import { HOSTNAME } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { uniq } from '@shell/utils/array';
import { ADD_ONS, SOURCE_TYPE, ACCESS_CREDENTIALS } from '../../config/harvester-map';
import { ADD_ONS, SOURCE_TYPE, ACCESS_CREDENTIALS, maintenanceStrategies, runStrategies } from '../../config/harvester-map';
import { HCI_SETTING } from '../../config/settings';
import { HCI } from '../../types';
import { parseVolumeClaimTemplates } from '../../utils/vm';
@ -136,6 +136,9 @@ export default {
spec: null,
osType: 'linux',
sshKey: [],
maintenanceStrategies,
maintenanceStrategy: 'Migrate',
runStrategies,
runStrategy: 'RerunOnFailure',
installAgent: true,
hasCreateVolumes: [],
@ -164,6 +167,7 @@ export default {
enabledSriovgpu: false,
immutableMode: this.realMode === _CREATE ? _CREATE : _VIEW,
terminationGracePeriodSeconds: '',
cpuPinning: false,
};
},
@ -316,6 +320,11 @@ export default {
};
}
if (!vm.metadata.labels) {
vm.metadata.labels = {};
}
const maintenanceStrategy = vm.metadata.labels?.[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY] || 'Migrate';
const runStrategy = spec.runStrategy || 'RerunOnFailure';
const machineType = value.machineType;
const cpu = spec.template.spec.domain?.cpu?.cores;
@ -352,6 +361,7 @@ export default {
const efiEnabled = this.isEfiEnabled(spec);
const tpmEnabled = this.isTpmEnabled(spec);
const secureBoot = this.isSecureBoot(spec);
const cpuPinning = this.isCpuPinning(spec);
const secretRef = this.getSecret(spec);
const accessCredentials = this.getAccessCredentials(spec);
@ -362,6 +372,7 @@ export default {
}
this['spec'] = spec;
this['maintenanceStrategy'] = maintenanceStrategy;
this['runStrategy'] = runStrategy;
this['secretRef'] = secretRef;
this['accessCredentials'] = accessCredentials;
@ -382,6 +393,7 @@ export default {
this['efiEnabled'] = efiEnabled;
this['tpmEnabled'] = tpmEnabled;
this['secureBoot'] = secureBoot;
this['cpuPinning'] = cpuPinning;
this['hasCreateVolumes'] = hasCreateVolumes;
this['networkRows'] = networkRows;
@ -401,15 +413,37 @@ export default {
let out = [];
if (_disks.length === 0) {
let bus = 'virtio';
let type = HARD_DISK;
let size = '10Gi';
const imageResource = this.images.find( I => this.imageId === I.id);
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
if (isIsoImage) {
bus = 'sata';
type = CD_ROM;
}
if (imageSize) {
let imageSizeGiB = Math.ceil(imageSize / 1024 / 1024 / 1024);
if (!isIsoImage) {
imageSizeGiB = Math.max(imageSizeGiB, 10);
}
size = `${ imageSizeGiB }Gi`;
}
out.push({
id: randomStr(5),
source: SOURCE_TYPE.IMAGE,
name: 'disk-0',
accessMode: 'ReadWriteMany',
bus: 'virtio',
bus,
volumeName: '',
size: '10Gi',
type: HARD_DISK,
size,
type,
storageClassName: '',
image: this.imageId,
volumeMode: 'Block',
@ -574,6 +608,12 @@ export default {
} else {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
}
if (this.maintenanceStrategy === 'Migrate') {
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
} else {
vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY] = this.maintenanceStrategy;
}
},
parseDiskRows(disk) {
@ -681,27 +721,24 @@ export default {
spec = this.multiVMScheduler(spec);
}
this.value.metadata['annotations'] = {
...this.value.metadata.annotations,
this.value.metadata['annotations'] = {...this.value.metadata.annotations,
[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: JSON.stringify(volumeClaimTemplates),
[HCI_ANNOTATIONS.NETWORK_IPS]: JSON.stringify(this.value.networkIps)
};
[HCI_ANNOTATIONS.NETWORK_IPS]: JSON.stringify(this.value.networkIps)};
this.value.metadata['labels'] = {
...this.value.metadata.labels,
this.value.metadata['labels'] = {...this.value.metadata.labels,
[HCI_ANNOTATIONS.CREATOR]: 'harvester',
[HCI_ANNOTATIONS.OS]: this.osType
};
[HCI_ANNOTATIONS.OS]: this.osType};
this.value['spec'] = spec;
this['spec'] = spec;
} else if (this.resource === HCI.VM_VERSION) {
this.value.spec.vm['spec'] = spec;
this.value.spec.vm.metadata['annotations'] = { ...this.value.spec.vm.metadata.annotations, [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: JSON.stringify(volumeClaimTemplates) };
this.value.spec.vm.metadata['labels'] = {
...this.value.spec.vm.metadata.labels,
[HCI_ANNOTATIONS.OS]: this.osType
this.value.spec.vm.metadata['annotations'] = {
...this.value.spec.vm.metadata.annotations,
[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: JSON.stringify(volumeClaimTemplates),
};
this.value.spec.vm.metadata['labels'] = {...this.value.spec.vm.metadata.labels,
[HCI_ANNOTATIONS.OS]: this.osType,};
this['spec'] = spec;
}
},
@ -819,6 +856,10 @@ export default {
}
},
getMaintenanceStrategyOptionLabel(opt) {
return this.t(`harvester.virtualMachine.maintenanceStrategy.options.${ opt.label || opt }`);
},
getInitUserData(config) {
const _QGA_JSON = this.getMatchQGA(config.osType);
@ -1339,6 +1380,14 @@ export default {
}
},
setCpuPinning(value) {
if (value) {
set(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement', true);
} else {
delete this.spec.template.spec.domain.cpu['dedicatedCpuPlacement'];
}
},
setTPM(tpmEnabled) {
if (tpmEnabled) {
set(this.spec.template.spec.domain.devices, 'tpm', {});
@ -1462,6 +1511,10 @@ export default {
this.setBootMethod({ efi: this.efiEnabled, secureBoot: val });
},
cpuPinning(value) {
this.setCpuPinning(value);
},
tpmEnabled(val) {
this.setTPM(val);
},

View File

@ -5,6 +5,7 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
import { get } from '@shell/utils/object';
import { NAMESPACE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { HCI } from '../../types';
const OBSCURE_NAMESPACE_PREFIX = [
'c-', // cluster namespace
@ -37,15 +38,32 @@ export default class HciNamespace extends namespace {
weight: -10,
};
const editQuotaAction = {
action: 'editNSQuota',
label: this.t('harvester.modal.quota.editQuota'),
icon: 'icon icon-storage',
enabled: !!this?.actions?.updateResourceQuota && !!this?.actions?.deleteResourceQuota,
weight: -11,
};
if (remove > -1) {
out.splice(remove, 1);
}
insertAt(out, out.length - 1, promptRemove);
insertAt(out, out.length - 5, editQuotaAction);
return out;
}
editNSQuota(resources = this) {
this.$dispatch('promptModal', {
resources,
snapshotSizeQuota: this.snapshotSizeQuota,
component: 'HarvesterQuotaDialog'
});
}
promptRemove(resources = this) {
this.$dispatch('promptModal', {
resources,
@ -54,6 +72,17 @@ export default class HciNamespace extends namespace {
});
}
get nsResourceQuota() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const allResQuotas = this.$rootGetters[`${ inStore }/all`](HCI.RESOURCE_QUOTA);
return allResQuotas.find( RQ => RQ.metadata.namespace === this.id);
}
get snapshotSizeQuota() {
return this.nsResourceQuota?.spec?.snapshotLimit?.namespaceTotalSnapshotSizeQuota;
}
get isSystem() {
const systemNamespaces = ['fleet-default'];

View File

@ -59,6 +59,22 @@ export default class HciNode extends HarvesterResource {
total: 1
};
const enableCPUManager = {
action: 'enableCPUManager',
enabled: this.hasAction('enableCPUManager') && !this.isCPUManagerEnableInProgress && !this.isCPUManagerEnabled && !this.isEtcd, // witness node doesn't have CPU manager
icon: 'icon icon-fw icon-os-management',
label: this.t('harvester.action.enableCPUManager'),
total: 1
};
const disableCPUManager = {
action: 'disableCPUManager',
enabled: this.hasAction('disableCPUManager') && !this.isCPUManagerEnableInProgress && this.isCPUManagerEnabled && !this.isEtcd,
icon: 'icon icon-fw icon-os-management',
label: this.t('harvester.action.disableCPUManager'),
total: 1
};
const shutDown = {
action: 'shutDown',
enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && !this.isStopped && !!this.inventory,
@ -88,6 +104,8 @@ export default class HciNode extends HarvesterResource {
uncordon,
enableMaintenance,
disableMaintenance,
enableCPUManager,
disableCPUManager,
shutDown,
powerOn,
reboot,
@ -134,15 +152,12 @@ export default class HciNode extends HarvesterResource {
get consoleUrl() {
const url = this.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
const validator = /^[a-z]+:\/\//;
if (!url) {
if (!url?.match(validator)) {
return false;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return `http://${ url }`;
}
return url;
}
@ -308,6 +323,14 @@ export default class HciNode extends HarvesterResource {
this.doAction('disableMaintenanceMode', {});
}
enableCPUManager() {
this.doActionGrowl('enableCPUManager', {});
}
disableCPUManager() {
this.doActionGrowl('disableCPUManager', {});
}
get isUnSchedulable() {
return (
this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_SCHEDULABLE] === 'false' ||
@ -347,6 +370,28 @@ export default class HciNode extends HarvesterResource {
);
}
get isCPUManagerEnabled() {
return this.metadata?.labels?.[HCI_ANNOTATIONS.CPU_MANAGER] === 'true';
}
get isCPUManagerEnableInProgress() {
return this.cpuManagerUpdateStatus === 'requested' || this.cpuManagerUpdateStatus === 'running';
}
get isCPUManagerEnableFailed() {
return this.cpuManagerUpdateStatus === 'failed';
}
get cpuManagerUpdateStatus() {
try {
const cpuManagerUpdate = JSON.parse(this.metadata.annotations[HCI_ANNOTATIONS.NODE_CPU_MANAGER_UPDATE_STATUS] || '{}');
return cpuManagerUpdate.status || '';
} catch {
return '';
}
}
get longhornDisks() {
const inStore = this.$rootGetters['currentProduct'].inStore;
const longhornNode = this.$rootGetters[`${ inStore }/byId`](

View File

@ -20,12 +20,10 @@ export default class HciPv extends HarvesterResource {
const storageClassName =
realMode === _CLONE ? this.spec.storageClassName : '';
this['spec'] = {
accessModes,
this['spec'] = {accessModes,
storageClassName,
volumeName: '',
resources: { requests: { storage } }
};
resources: { requests: { storage } }};
}
get availableActions() {
@ -98,7 +96,6 @@ export default class HciPv extends HarvesterResource {
}
get stateDisplay() {
const ownedBy = this?.metadata?.annotations?.[HCI_ANNOTATIONS.OWNED_BY];
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
const degradedVolume = volumeError === DEGRADED_ERROR;
const status = this?.status?.phase === 'Bound' && !volumeError && this.isLonghornVolumeReady ? 'Ready' : 'Not Ready';
@ -107,7 +104,7 @@ export default class HciPv extends HarvesterResource {
if (findBy(conditions, 'type', 'Resizing')?.status === 'True') {
return 'Resizing';
} else if (ownedBy && !volumeError) {
} else if (!!this.attachVM && !volumeError) {
return 'In-use';
} else if (degradedVolume) {
return 'Degraded';
@ -179,17 +176,19 @@ export default class HciPv extends HarvesterResource {
}
get attachVM() {
const allVMs = this.$rootGetters['harvester/all'](HCI.VM);
const ownedBy =
get(this, `metadata.annotations."${ HCI_ANNOTATIONS.OWNED_BY }"`) || '';
const allVMs = this.$rootGetters['harvester/all'](HCI.VM) || [];
if (!ownedBy) {
return null;
const findAttachVM = (vm) => {
const attachVolumes = vm.spec.template?.spec?.volumes || [];
if (vm.namespace === this.namespace && attachVolumes.length > 0) {
return attachVolumes.find(vol => vol.persistentVolumeClaim?.claimName === this.name);
}
const ownedId = JSON.parse(ownedBy)[0]?.refs?.[0];
return null;
};
return allVMs.find(D => D.id === ownedId);
return allVMs.find(findAttachVM);
}
get isAvailable() {

View File

@ -9,6 +9,10 @@ export default class HciUpgrade extends HarvesterResource {
return this?.metadata?.labels?.[HCI.LATEST_UPGRADE] === 'true';
}
get isUpgradeFailed() {
return this?.metadata?.labels?.[HCI.UPGRADE_STATE] === 'Failed';
}
get isUpgradeSucceeded() {
return this?.metadata?.labels?.[HCI.UPGRADE_STATE] === 'Succeeded';
}
@ -117,10 +121,10 @@ export default class HciUpgrade extends HarvesterResource {
get upgradeImageMessage() {
const conditions = this?.status?.conditions || [];
const imageReady = conditions.find( cond => cond.type === 'ImageReady');
const hasError = imageReady?.status === 'False';
const success = imageReady?.status === 'True';
const message = imageReady?.message || imageReady?.reason;
return hasError ? message : '';
return success ? '' : message;
}
get nodeUpgradeMessage() {

View File

@ -47,7 +47,7 @@ export default class HciVmImage extends HarvesterResource {
{
action: 'createFromImage',
enabled: canCreateVM,
icon: 'icon icon-fw icon-spinner',
icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'),
disabled: !this.isReady,
},
@ -74,7 +74,7 @@ export default class HciVmImage extends HarvesterResource {
router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM },
query: { image: this.id }
query: { image: this.id, fromPage: HCI.IMAGE }
});
}
@ -101,6 +101,10 @@ export default class HciVmImage extends HarvesterResource {
const imported = this.getStatusConditionOfType('Imported');
if (imported?.status === 'Unknown') {
if (this.spec.sourceType === 'restore') {
return 'Restoring';
}
if (this.spec.sourceType === 'download') {
return 'Downloading';
}
@ -131,7 +135,8 @@ export default class HciVmImage extends HarvesterResource {
const conditions = this?.status?.conditions || [];
const initialized = conditions.find( cond => cond.type === 'Initialized');
const imported = conditions.find( cond => cond.type === 'Imported');
const message = initialized?.message || imported?.message;
const retryLimitExceeded = conditions.find( cond => cond.type === 'RetryLimitExceeded');
const message = initialized?.message || imported?.message || retryLimitExceeded?.message;
return ucFirst(message);
}
@ -167,6 +172,21 @@ export default class HciVmImage extends HarvesterResource {
});
}
get virtualSize() {
const virtualSize = this.status?.virtualSize;
if (!virtualSize) {
return '-';
}
return formatSi(virtualSize, {
increment: 1024,
maxPrecision: 2,
suffix: 'B',
firstSuffix: 'B',
});
}
getStatusConditionOfType(type, defaultValue = []) {
const conditions = Array.isArray(get(this, 'status.conditions')) ? this.status.conditions : defaultValue;

View File

@ -133,7 +133,7 @@ export default class VirtVm extends HarvesterResource {
{
action: 'softrebootVM',
enabled: !!this.actions?.softreboot,
icon: 'icon icon-refresh',
icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot')
},
{
@ -152,9 +152,15 @@ export default class VirtVm extends HarvesterResource {
{
action: 'takeVMSnapshot',
enabled: !!this.actions?.backup,
icon: 'icon icon-backup',
icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot')
},
{
action: 'editVMQuota',
enabled: !!this.actions?.updateResourceQuota && !!this.actions.deleteResourceQuota,
icon: 'icon icon-storage',
label: this.t('harvester.action.editVMQuota')
},
{
action: 'restoreVM',
enabled: !!this.actions?.restore,
@ -331,6 +337,14 @@ export default class VirtVm extends HarvesterResource {
});
}
editVMQuota(resources = this) {
this.$dispatch('promptModal', {
resources,
snapshotSizeQuota: this.snapshotSizeQuota,
component: 'HarvesterQuotaDialog'
});
}
unplugVolume(diskName) {
const resources = this;
@ -541,6 +555,17 @@ export default class VirtVm extends HarvesterResource {
return null;
}
get nsResourceQuota() {
const inStore = this.productInStore;
const allResQuotas = this.$rootGetters[`${ inStore }/all`](HCI.RESOURCE_QUOTA);
return allResQuotas.find( RQ => RQ.namespace === this.metadata.namespace);
}
get snapshotSizeQuota() {
return this.nsResourceQuota?.spec?.snapshotLimit?.vmTotalSnapshotSizeQuota?.[this.metadata.name];
}
get vmi() {
const inStore = this.productInStore;
@ -646,6 +671,10 @@ export default class VirtVm extends HarvesterResource {
return null;
}
get isTerminating() {
return !!this?.metadata?.deletionTimestamp;
}
get otherState() {
const state = (this.vmi &&
[VMIPhase.Scheduling, VMIPhase.Scheduled].includes(
@ -685,18 +714,21 @@ export default class VirtVm extends HarvesterResource {
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE);
return allRestore.find(O => O.id === id);
const res = allRestore.find(O => O.id === id);
if (res) {
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
res.fromSnapshot = !!allBackups
.filter(b => b.spec?.type !== BACKUP_TYPE.BACKUP)
.find(s => s.id === `${ res.spec?.virtualMachineBackupNamespace }/${ res.spec?.virtualMachineBackupName }`);
}
return res;
}
get restoreProgress() {
const inStore = this.productInStore;
const allBackups = this.$rootGetters[`${ inStore }/all`](HCI.BACKUP);
const isSnapshotRestore = !!allBackups
.filter(b => b.spec?.type !== BACKUP_TYPE.BACKUP)
.find(s => s.id === `${ this.restoreResource?.spec?.virtualMachineBackupNamespace }/${ this.restoreResource?.spec?.virtualMachineBackupName }`);
if (isSnapshotRestore) {
if (this.isVMError || this.isTerminating) {
return {};
}
@ -726,7 +758,7 @@ export default class VirtVm extends HarvesterResource {
return 'Restoring';
}
if (this?.metadata?.deletionTimestamp) {
if (this.isTerminating) {
return 'Terminating';
}

View File

@ -7,6 +7,7 @@ import CommunityLinks from '@shell/components/CommunityLinks';
import { SCHEMA } from '@shell/config/types';
import HarvesterSupportBundle from '../../../../dialog/HarvesterSupportBundle';
import { HCI } from '../../../../types';
import { DOC_LINKS } from '../../../../config/doc-links';
export default {
components: {
@ -71,13 +72,18 @@ export default {
const { host, params } = this.internalPrefix;
return `https://${ host }/k8s/clusters/${ params.cluster }/api/v1/namespaces/longhorn-system/services/http:longhorn-frontend:80/proxy/#/dashboard`;
}
},
rancherIntegrationLink() {
return DOC_LINKS.RANCHER_INTEGRATION_URL;
},
},
methods: {
open() {
this.$store.commit('harvester-common/toggleBundleModal', true);
}
},
}
};
</script>
@ -89,10 +95,7 @@ export default {
<IndentedPanel>
<div class="content mt-20">
<div class="promo">
<div
v-if="showSupportBundle"
class="box mb-20 box-primary"
>
<div v-if="showSupportBundle" class="box mb-20 box-primary">
<h2>
{{ t('harvester.modal.bundle.title') }}
</h2>
@ -109,10 +112,7 @@ export default {
</button>
</div>
</div>
<div
class="box box-primary"
:class="{'mb-20': dev }"
>
<div class="box box-primary" :class="{'mb-20': dev }">
<h2>
{{ t('harvester.support.kubeconfig.title') }}
</h2>
@ -129,41 +129,24 @@ export default {
</button>
</div>
</div>
<div
v-if="dev"
class="row"
>
<div v-if="dev" class="row">
<div class="col span-6 box box-primary">
<h2>
<a
rel="nofollow noopener noreferrer"
target="_blank"
:href="rancherLink"
>{{ t('harvester.support.internal.rancher.title') }} <i class="icon icon-external-link" /></a>
<a rel="nofollow noopener noreferrer" target="_blank" :href="rancherLink">{{ t('harvester.support.internal.rancher.title') }} <i class="icon icon-external-link" /></a>
</h2>
<div>
<p class="warning">
<t
k="harvester.support.internal.rancher.titleDescription"
:raw="true"
/>
<t k="harvester.support.internal.rancher.titleDescription" :raw="true" :url="rancherIntegrationLink" />
</p>
</div>
</div>
<div class="col span-6 box box-primary">
<h2>
<a
rel="nofollow noopener noreferrer"
target="_blank"
:href="longhornLink"
>{{ t('harvester.support.internal.longhorn.title') }} <i class="icon icon-external-link" /></a>
<a rel="nofollow noopener noreferrer" target="_blank" :href="longhornLink">{{ t('harvester.support.internal.longhorn.title') }} <i class="icon icon-external-link" /></a>
</h2>
<div>
<p class="warning">
<t
k="harvester.support.internal.longhorn.titleDescription"
:raw="true"
/>
<t k="harvester.support.internal.longhorn.titleDescription" :raw="true" />
</p>
</div>
</div>
@ -173,17 +156,9 @@ export default {
<CommunityLinks :link-options="options" />
</div>
<div class="external">
<a
href="https://www.suse.com/suse-harvester/support-matrix/all-supported-versions"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ t('harvester.support.community.learnMore') }} <i class="icon icon-external-link" /></a>
<a href="https://www.suse.com/suse-harvester/support-matrix/all-supported-versions" target="_blank" rel="noopener noreferrer nofollow">{{ t('harvester.support.community.learnMore') }} <i class="icon icon-external-link" /></a>
or
<a
href="https://www.suse.com/products/harvester/"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ t('harvester.support.community.pricing') }} <i class="icon icon-external-link" /></a>
<a href="https://www.suse.com/products/harvester/" target="_blank" rel="noopener noreferrer nofollow">{{ t('harvester.support.community.pricing') }} <i class="icon icon-external-link" /></a>
</div>
</div>
</IndentedPanel>

View File

@ -18,16 +18,16 @@ export default {
const cluster = await dispatch('management/find', {
type: MANAGEMENT.CLUSTER,
id,
opt: { url: `${MANAGEMENT.CLUSTER}s/${escape(id)}` }
opt: { url: `${ MANAGEMENT.CLUSTER }s/${ escape(id) }` }
}, { root: true });
let virtualBase = `/k8s/clusters/${escape(id)}/v1/harvester`;
let virtualBase = `/k8s/clusters/${ escape(id) }/v1/harvester`;
if (id === 'local') {
virtualBase = `/v1/harvester`;
}
if (!cluster) {
if ( !cluster ) {
commit('clusterId', null, { root: true });
commit('applyConfig', { baseUrl: null });
throw new ClusterNotFoundError(id);
@ -45,21 +45,21 @@ export default {
const projectArgs = {
type: MANAGEMENT.PROJECT,
opt: {
url: `${MANAGEMENT.PROJECT}/${escape(id)}`,
url: `${ MANAGEMENT.PROJECT }/${ escape(id) }`,
watchNamespace: id
}
};
const fetchProjects = async () => {
const fetchProjects = async() => {
let limit = 30000;
const sleep = 100;
while (limit > 0 && !rootState.managementReady) {
await setTimeout(() => { }, sleep);
while ( limit > 0 && !rootState.managementReady ) {
await setTimeout(() => {}, sleep);
limit -= sleep;
}
if (rootGetters['management/schemaFor'](MANAGEMENT.PROJECT)) {
if ( rootGetters['management/schemaFor'](MANAGEMENT.PROJECT) ) {
return dispatch('management/findAll', projectArgs, { root: true });
}
};
@ -68,7 +68,7 @@ export default {
const settings = await dispatch('findAll', {
type: MANAGEMENT.SETTING,
id: SETTING.SYSTEM_NAMESPACES,
opt: { url: `${virtualBase}/${MANAGEMENT.SETTING}s/`, force: true }
opt: { url: `${ virtualBase }/${ MANAGEMENT.SETTING }s/`, force: true }
});
const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES);
@ -80,7 +80,7 @@ export default {
}
}
const hash: { [key: string]: Promise<any> } = {
const hash: { [key: string]: Promise<any>} = {
projects: fetchProjects(),
virtualCount: dispatch('findAll', { type: COUNT }),
virtualNamespaces: dispatch('findAll', { type: NAMESPACE }),
@ -91,6 +91,10 @@ export default {
}, { root: true }),
};
if (getters['schemaFor'](HCI.RESOURCE_QUOTA)) {
hash.resourceQuota = dispatch('findAll', { type: HCI.RESOURCE_QUOTA });
}
if (getters['schemaFor'](HCI.UPGRADE)) {
hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE });
}

View File

@ -14,7 +14,7 @@ export default {
divider,
notFilterNamespaces
}: any) => {
const out: { id: string, kind: string, label: string }[] = [{
const out = [{
id: ALL,
kind: NAMESPACE_FILTER_KINDS.SPECIAL,
label: rootGetters['i18n/t']('nav.ns.all'),
@ -30,11 +30,9 @@ export default {
MANAGEMENT.PROJECT
);
projects = sortBy(
filterBy(projects, 'spec.clusterName', cluster.id),
['nameDisplay'],
null
).filter((project: any) => project.nameDisplay !== 'System');
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
'nameDisplay',
]).filter((project: any) => project.nameDisplay !== 'System');
const projectsById: any = {};
const namespacesByProject: any = {};
@ -72,7 +70,7 @@ export default {
}
out.push({
id: `project://${id}`,
id: `project://${ id }`,
kind: 'project',
label: project.nameDisplay,
});

View File

@ -31,6 +31,7 @@ export const HCI = {
FLOW: 'harvesterhci.io.logging.flow',
OUTPUT: 'harvesterhci.io.logging.output',
STORAGE: 'harvesterhci.io.storage',
RESOURCE_QUOTA: 'harvesterhci.io.resourcequota',
KSTUNED: 'node.harvesterhci.io.ksmtuned',
PCI_DEVICE: 'devices.harvesterhci.io.pcidevice',
PCI_CLAIM: 'devices.harvesterhci.io.pcideviceclaim',