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',
@ -55,10 +62,10 @@ export const FLOW_TYPE = {
};
export const ADD_ONS = {
HARVESTER_SEEDER: 'harvester-seeder',
PCI_DEVICE_CONTROLLER: 'pcidevices-controller',
RANCHER_LOGGING: 'rancher-logging',
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
NVIDIA_DRIVER_TOOLKIT_CONTROLLER: 'nvidia-driver-toolkit'
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',
};

View File

@ -1,49 +1,55 @@
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',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
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',
IMAGE_NAME: 'harvesterhci.io/image-name',
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',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
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',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
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

@ -1,33 +1,39 @@
export const HCI_SETTING = {
BACKUP_TARGET: 'backup-target',
CONTAINERD_REGISTRY: 'containerd-registry',
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
UI_SOURCE: 'ui-source',
UI_PL: 'ui-pl',
HTTP_PROXY: 'http-proxy',
ADDITIONAL_CA: 'additional-ca',
OVERCOMMIT_CONFIG: 'overcommit-config',
CLUSTER_REGISTRATION_URL: 'cluster-registration-url',
DEFAULT_STORAGE_CLASS: 'default-storage-class',
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
STORAGE_NETWORK: 'storage-network',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SSL_CERTIFICATES: 'ssl-certificates',
SSL_PARAMETERS: 'ssl-parameters',
SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces',
AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths',
RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers',
BACKUP_TARGET: 'backup-target',
CONTAINERD_REGISTRY: 'containerd-registry',
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
UI_SOURCE: 'ui-source',
UI_PL: 'ui-pl',
HTTP_PROXY: 'http-proxy',
ADDITIONAL_CA: 'additional-ca',
OVERCOMMIT_CONFIG: 'overcommit-config',
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',
SSL_PARAMETERS: 'ssl-parameters',
SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces',
AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths',
RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
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' },
@ -49,14 +56,16 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.ADDITIONAL_CA]: {
kind: 'multiline', canReset: true, from: 'import'
},
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_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' },
[HCI_SETTING.RANCHER_MANAGER_SUPPORT]: { kind: 'boolean' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
[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' },
[HCI_SETTING.RANCHER_MANAGER_SUPPORT]: { kind: 'boolean' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
kind: 'json', from: 'import', canReset: true
},
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
@ -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
key: 'harvester.host.ntp.ntpSyncStatus.isUnsynced',
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,15 +60,18 @@ export default {
data() {
return {
switchToCloud: false,
hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL,
showVmMetrics: false,
showVmMetrics: false,
};
},
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: {
@ -25,9 +26,9 @@ export default {
data() {
return {
errors: [],
unhealthyVM: '',
force: false
errors: [],
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 {
}
};
await retrySave();
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>
@ -308,6 +335,37 @@ export default {
</div>
</div>
<div class="row mb-20">
<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 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" />
@ -315,13 +373,6 @@ export default {
<div v-if="showAdvanced">
<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"
@ -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)) {
this.value['type'] = 'cd-rom';
this.value['bus'] = 'sata';
} else {
this.value['type'] = 'disk';
this.value['bus'] = 'virtio';
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"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:display-side-by-side="false"
:show-annotations="false"
:show-label-title="false"
<Banner
v-if="showCpuPinningBanner"
color="warning"
>
<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>
<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"
/>
<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]);
this.matchingNodes = {
matched: 1,
total: allNodes.length,
none: false,
sample: matchNode ? matchNode.nameDisplay : selector[HOSTNAME],
};
if (matchNode) {
this.matchingNodes = {
matched: 1,
total: allNodes.length,
none: false,
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}">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -134,15 +134,16 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
vms: this.fetchClusterResources(HCI.VM),
nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT),
metricNodes: this.fetchClusterResources(METRIC.NODE),
settings: this.fetchClusterResources(HCI.SETTING),
services: this.fetchClusterResources(SERVICE),
metric: this.fetchClusterResources(METRIC.NODE),
longhornNode: this.fetchClusterResources(LONGHORN.NODES) || [],
_pods: this.$store.dispatch('harvester/findAll', { type: POD }),
vms: this.fetchClusterResources(HCI.VM),
nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT),
metricNodes: this.fetchClusterResources(METRIC.NODE),
settings: this.fetchClusterResources(HCI.SETTING),
services: this.fetchClusterResources(SERVICE),
metric: this.fetchClusterResources(METRIC.NODE),
longhornNodes: this.fetchClusterResources(LONGHORN.NODES),
longhornSettings: this.fetchClusterResources(LONGHORN.SETTINGS),
_pods: this.$store.dispatch('harvester/findAll', { type: POD }),
};
(this.accessibleResources || []).map((a) => {
@ -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 total;
}, {
used: 0,
scheduled: 0,
maximum: 0,
reserved: 0,
total: 0
});
return out;
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)">
{{ t('harvester.host.console') }}
</button>
<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) || [];
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);
}
if (!ownedBy) {
return null;
}
};
const ownedId = JSON.parse(ownedBy)[0]?.refs?.[0];
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);
@ -44,22 +44,22 @@ export default {
const projectArgs = {
type: MANAGEMENT.PROJECT,
opt: {
url: `${MANAGEMENT.PROJECT}/${escape(id)}`,
opt: {
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 });
}
};
@ -67,8 +67,8 @@ export default {
if (id !== 'local' && getters['schemaFor'](MANAGEMENT.SETTING)) { // multi-cluster
const settings = await dispatch('findAll', {
type: MANAGEMENT.SETTING,
id: SETTING.SYSTEM_NAMESPACES,
opt: { url: `${virtualBase}/${MANAGEMENT.SETTING}s/`, force: true }
id: SETTING.SYSTEM_NAMESPACES,
opt: { url: `${ virtualBase }/${ MANAGEMENT.SETTING }s/`, force: true }
});
const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES);
@ -80,17 +80,21 @@ export default {
}
}
const hash: { [key: string]: Promise<any> } = {
projects: fetchProjects(),
virtualCount: dispatch('findAll', { type: COUNT }),
const hash: { [key: string]: Promise<any>} = {
projects: fetchProjects(),
virtualCount: dispatch('findAll', { type: COUNT }),
virtualNamespaces: dispatch('findAll', { type: NAMESPACE }),
settings: dispatch('findAll', { type: HCI.SETTING }),
clusters: dispatch('management/findAll', {
settings: dispatch('findAll', { type: HCI.SETTING }),
clusters: dispatch('management/findAll', {
type: MANAGEMENT.CLUSTER,
opt: { force: true }
opt: { force: true }
}, { 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 });
}
@ -101,15 +105,15 @@ export default {
commit('updateNamespaces', {
filters: [],
all: getters.filterNamespace(),
all: getters.filterNamespace(),
getters
}, { root: true });
// Solve compatibility with Rancher v2.6.x, fell remove these codes after not support v2.6.x
const definition = {
def: false,
parseJSON: true,
inheritFrom: DEV,
def: false,
parseJSON: true,
inheritFrom: DEV,
asUserPreference: true,
};
@ -134,9 +138,9 @@ export default {
if (isMultiCluster) {
commit('managementChanged', {
ready: true,
ready: true,
isMultiCluster: true,
isRancher: true,
isRancher: true,
}, { root: true });
}
},

View File

@ -14,9 +14,9 @@ export default {
divider,
notFilterNamespaces
}: any) => {
const out: { id: string, kind: string, label: string }[] = [{
id: ALL,
kind: NAMESPACE_FILTER_KINDS.SPECIAL,
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,8 +70,8 @@ export default {
}
out.push({
id: `project://${id}`,
kind: 'project',
id: `project://${ id }`,
kind: 'project',
label: project.nameDisplay,
});
@ -90,8 +88,8 @@ export default {
}
out.push({
id: ALL_ORPHANS,
kind: 'project',
id: ALL_ORPHANS,
kind: 'project',
label: rootGetters['i18n/t']('nav.ns.orphan'),
});

View File

@ -28,12 +28,12 @@ const harvesterFactory = (): CoreStoreSpecifics => {
return steveFactory;
};
const config: CoreStoreConfig = {
namespace: PRODUCT_NAME,
namespace: PRODUCT_NAME,
isClusterStore: true
};
export default {
specifics: harvesterFactory(),
config,
init: steveStoreInit
init: steveStoreInit
};

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