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'; import Tag from '@shell/components/Tag';
export default { export default {
name: 'DiskTags', name: 'Tags',
components: { Tag }, components: { Tag },

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ export default {
window.open( window.open(
url, url,
'_blank', '_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 { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types'; import { NODE } from '@shell/config/types';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC_LINKS } from '../../config/doc-links';
export default { export default {
name: 'HarvesterEditStorageNetwork', name: 'HarvesterEditStorageNetwork',
@ -85,6 +86,9 @@ export default {
}, },
computed: { computed: {
storageNetworkExampleLink() {
return DOC_LINKS.STORAGE_NETWORK_EXAMPLE;
},
clusterNetworkOptions() { clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
@ -206,8 +210,8 @@ export default {
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')" :placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label" label-key="harvester.setting.storageNetwork.range.label"
/> />
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.setting.storageNetwork.tip')"> <Tip class="mb-20" icon="icon icon-info">
<t k="harvester.setting.storageNetwork.tip" :raw="true" /> <t k="harvester.setting.storageNetwork.tip" :raw="true" :url="storageNetworkExampleLink" />
</Tip> </Tip>
<ArrayList <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 = { export const SOURCE_TYPE = {
NEW: 'New', NEW: 'New',
IMAGE: 'VM Image', IMAGE: 'Virtual Machine Image',
ATTACH_VOLUME: 'Existing Volume', ATTACH_VOLUME: 'Existing Volume',
CONTAINER: 'Container' CONTAINER: 'Container'
}; };
@ -41,7 +41,14 @@ export const ACCESS_CREDENTIALS = {
INJECT_SSH: 'sshPublicKey' 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 = { export const VOLUME_DATA_SOURCE_KIND = {
VolumeSnapshot: 'VolumeSnapshot', VolumeSnapshot: 'VolumeSnapshot',
@ -55,10 +62,10 @@ export const FLOW_TYPE = {
}; };
export const ADD_ONS = { export const ADD_ONS = {
HARVESTER_SEEDER: 'harvester-seeder', HARVESTER_SEEDER: 'harvester-seeder',
PCI_DEVICE_CONTROLLER: 'pcidevices-controller', PCI_DEVICE_CONTROLLER: 'pcidevices-controller',
RANCHER_LOGGING: 'rancher-logging', NVIDIA_DRIVER_TOOLKIT_CONTROLLER: 'nvidia-driver-toolkit',
RANCHER_MONITORING: 'rancher-monitoring', RANCHER_LOGGING: 'rancher-logging',
VM_IMPORT_CONTROLLER: 'vm-import-controller', RANCHER_MONITORING: 'rancher-monitoring',
NVIDIA_DRIVER_TOOLKIT_CONTROLLER: 'nvidia-driver-toolkit' VM_IMPORT_CONTROLLER: 'vm-import-controller',
}; };

View File

@ -1,49 +1,55 @@
export const HCI = { export const HCI = {
CLOUD_INIT: 'harvesterhci.io/cloud-init-template', CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip', CURRENT_IP: 'rke2.io/internal-ip',
OWNED_BY: 'harvesterhci.io/owned-by', IMAGE_ID: 'harvesterhci.io/imageId',
IMAGE_ID: 'harvesterhci.io/imageId', SSH_NAMES: 'harvesterhci.io/sshNames',
SSH_NAMES: 'harvesterhci.io/sshNames', NETWORK_IPS: 'network.harvesterhci.io/ips',
NETWORK_IPS: 'network.harvesterhci.io/ips', TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName', CREATOR: 'harvesterhci.io/creator',
CREATOR: 'harvesterhci.io/creator', OS: 'harvesterhci.io/os',
OS: 'harvesterhci.io/os', NETWORK_TYPE: 'network.harvesterhci.io/type',
NETWORK_TYPE: 'network.harvesterhci.io/type', VM_NAME: 'harvesterhci.io/vmName',
VM_NAME: 'harvesterhci.io/vmName', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory', MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status', HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name', HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url', RESTORE_NAME: 'restore.harvesterhci.io/name',
RESTORE_NAME: 'restore.harvesterhci.io/name', NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master', NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane', NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status', PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState', MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates', VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name', IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address', INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable', NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route', NETWORK_ROUTE: 'network.harvesterhci.io/route',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image', MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade', OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
UPGRADE_STATE: 'harvesterhci.io/upgradeState', LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
REAY_MESSAGE: 'harvesterhci.io/read-message', UPGRADE_STATE: 'harvesterhci.io/upgradeState',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names', REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users', DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
IMAGE_SUFFIX: 'harvesterhci.io/image-type', DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
OS_TYPE: 'harvesterhci.io/os-type', IMAGE_SUFFIX: 'harvesterhci.io/image-type',
HOST_REQUEST: 'management.cattle.io/pod-requests', OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_CLASS: 'harvesterhci.io/storageClassName', STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io', HOST_REQUEST: 'management.cattle.io/pod-requests',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental', STORAGE_CLASS: 'harvesterhci.io/storageClassName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error', STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm', ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx', VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
NODE_MANUFACTURER: 'manufacturer', KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
NODE_MODEL: 'model', KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_SERIAL_NUMBER: 'serialNumber', NODE_MANUFACTURER: 'manufacturer',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota', NODE_MODEL: 'model',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service', NODE_SERIAL_NUMBER: 'serialNumber',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device', 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 = { export const HCI_SETTING = {
BACKUP_TARGET: 'backup-target', BACKUP_TARGET: 'backup-target',
CONTAINERD_REGISTRY: 'containerd-registry', CONTAINERD_REGISTRY: 'containerd-registry',
LOG_LEVEL: 'log-level', LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version', SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index', UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index', UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled', UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url', UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan', VLAN: 'vlan',
UI_SOURCE: 'ui-source', UI_SOURCE: 'ui-source',
UI_PL: 'ui-pl', UI_PL: 'ui-pl',
HTTP_PROXY: 'http-proxy', HTTP_PROXY: 'http-proxy',
ADDITIONAL_CA: 'additional-ca', ADDITIONAL_CA: 'additional-ca',
OVERCOMMIT_CONFIG: 'overcommit-config', OVERCOMMIT_CONFIG: 'overcommit-config',
CLUSTER_REGISTRATION_URL: 'cluster-registration-url', CLUSTER_REGISTRATION_URL: 'cluster-registration-url',
DEFAULT_STORAGE_CLASS: 'default-storage-class', DEFAULT_STORAGE_CLASS: 'default-storage-class',
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout', SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image', SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
STORAGE_NETWORK: 'storage-network', SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy', SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
SSL_CERTIFICATES: 'ssl-certificates', STORAGE_NETWORK: 'storage-network',
SSL_PARAMETERS: 'ssl-parameters', VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces', SSL_CERTIFICATES: 'ssl-certificates',
AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths', SSL_PARAMETERS: 'ssl-parameters',
RELEASE_DOWNLOAD_URL: 'release-download-url', SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions', AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths',
CSI_DRIVER_CONFIG: 'csi-driver-config', RELEASE_DOWNLOAD_URL: 'release-download-url',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds', CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
NTP_SERVERS: 'ntp-servers', 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 = { export const HCI_ALLOWED_SETTINGS = {
@ -41,6 +47,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.VLAN]: { [HCI_SETTING.VLAN]: {
kind: 'custom', from: 'import', alias: '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.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SERVER_VERSION]: { readOnly: true }, [HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' }, [HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
@ -49,14 +56,16 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.ADDITIONAL_CA]: { [HCI_SETTING.ADDITIONAL_CA]: {
kind: 'multiline', canReset: true, from: 'import' kind: 'multiline', canReset: true, from: 'import'
}, },
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' }, [HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {}, [HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' }, [HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' }, [HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: {},
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' }, [HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.RANCHER_MANAGER_SUPPORT]: { kind: 'boolean' }, [HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' }, [HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: { [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 kind: 'json', from: 'import', canReset: true
}, },
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { 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]: { [HCI_SETTING.NTP_SERVERS]: {
kind: 'json', from: 'import', canReset: true 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 = { export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {

View File

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

View File

@ -48,6 +48,13 @@ export default {
customName() { customName() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME]; 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() { consoleUrl() {
const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL]; const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
@ -224,6 +231,9 @@ export default {
</div> </div>
<div class="row mb-20"> <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"> <div class="col span-6">
<LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value"> <LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
<a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a> <a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
@ -268,7 +278,7 @@ export default {
<HarvesterStorageUsed <HarvesterStorageUsed
:row="value" :row="value"
:resource-name="t('harvester.host.detail.storage')" :resource-name="t('harvester.host.detail.storage')"
:show-reserved="true" :show-allocated="true"
/> />
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -35,6 +35,10 @@ export default {
return this.value?.downSize; return this.value?.downSize;
}, },
virtualSize() {
return this.value?.virtualSize;
},
url() { url() {
return this.value?.spec?.url || '-'; return this.value?.spec?.url || '-';
}, },
@ -100,6 +104,12 @@ export default {
</div> </div>
</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="row">
<div class="col span-12"> <div class="col span-12">
<LabelValue :name="t('nameNsDescription.description.label')" :value="description" class="mb-20" /> <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 PodAffinity from '@shell/components/form/PodAffinity';
import KeyValue from '@shell/components/form/KeyValue'; import KeyValue from '@shell/components/form/KeyValue';
import Labels from '@shell/components/form/Labels'; import Labels from '@shell/components/form/Labels';
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types'; import { HCI } from '../../types';
import VM_MIXIN from '../../mixins/harvester-vm'; import VM_MIXIN from '../../mixins/harvester-vm';
@ -22,6 +23,7 @@ import Events from './VirtualMachineTabs/VirtualMachineEvents';
import Migration from './VirtualMachineTabs/VirtualMachineMigration'; import Migration from './VirtualMachineTabs/VirtualMachineMigration';
import OverviewBasics from './VirtualMachineTabs/VirtualMachineBasics'; import OverviewBasics from './VirtualMachineTabs/VirtualMachineBasics';
import OverviewKeypairs from './VirtualMachineTabs/VirtualMachineKeypairs'; 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'; 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, Tabbed,
Events, Events,
OverviewBasics, OverviewBasics,
LabelValue,
Volume, Volume,
Network, Network,
OverviewKeypairs, OverviewKeypairs,
@ -57,15 +60,18 @@ export default {
data() { data() {
return { return {
switchToCloud: false, hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL, VM_METRICS_DETAIL_URL,
showVmMetrics: false, showVmMetrics: false,
}; };
}, },
async created() { async created() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
this.hasResourceQuotaSchema = !!this.$store.getters[`${ inStore }/schemaFor`](HCI.RESOURCE_QUOTA);
const hash = { const hash = {
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }), pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }), services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }),
@ -75,6 +81,10 @@ export default {
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }), 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); await allHash(hash);
setPromiseResult( setPromiseResult(
@ -88,6 +98,22 @@ export default {
computed: { computed: {
...mapGetters(['currentCluster']), ...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() { vmi() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
@ -175,10 +201,17 @@ export default {
<Network v-model:value="networkRows" mode="view" /> <Network v-model:value="networkRows" mode="view" />
</Tab> </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" /> <OverviewKeypairs :value="value" />
</Tab> </Tab>
<Tab v-if="hasResourceQuotaSchema" name="quotas" :label="t('harvester.tab.quotas')" :weight="3">
<LabelValue
:name="t('harvester.snapshot.totalSnapshotSize')"
:value="totalSnapshotSize"
/>
</Tab>
<Tab <Tab
v-if="showVmMetrics" v-if="showVmMetrics"
name="vm-metrics" 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 { matching } from '@shell/utils/selector';
import { NODE } from '@shell/config/types'; import { NODE } from '@shell/config/types';
import { isEmpty } from '@shell/utils/object'; import { isEmpty } from '@shell/utils/object';
import { HCI } from '@pkg/harvester/config/labels-annotations';
export default { export default {
components: { components: {
@ -55,10 +56,13 @@ export default {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const nodes = this.$store.getters[`${ inStore }/all`](NODE); const nodes = this.$store.getters[`${ inStore }/all`](NODE);
const matchedNodes = this.value?.metadata?.annotations?.[HCI.MATCHED_NODES];
const selector = this.value?.spec?.nodeSelector; const selector = this.value?.spec?.nodeSelector;
if (!isEmpty(selector)) { if (!isEmpty(selector)) {
return matching(nodes, selector); return matching(nodes, selector);
} else if (matchedNodes && matchedNodes.length > 0) {
return nodes.filter(node => matchedNodes.includes(node.id));
} else { } else {
return nodes; return nodes;
} }

View File

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

View File

@ -72,8 +72,11 @@ export default {
const nodes = this.$store.getters['harvester/all'](NODE); const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => { return nodes.filter((n) => {
// do not allow to migrate to self node const isNotSelfNode = !!this.availableNodes.includes(n.id);
return !!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) => { }).map((n) => {
let label = n?.metadata?.name; let label = n?.metadata?.name;
const value = 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> </div>
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" /> <Banner v-for="(err, i) in errors" :key="i"/>
</div> </div>
</Card> </Card>
</template> </template>

View File

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

View File

@ -5,6 +5,7 @@ import UnitInput from '@shell/components/form/UnitInput';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { Checkbox } from '@components/Form/Checkbox'; import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { DOC_LINKS } from '../../config/doc-links';
export const ksmtunedMode = [{ export const ksmtunedMode = [{
value: 'standard', value: 'standard',
@ -86,6 +87,10 @@ export default {
showKsmt() { showKsmt() {
return this.spec.run === 'run'; 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')" /> <Checkbox v-model:value="enableMergeAcrossNodes" :mode="mode" class="check mb-20" type="checkbox" :label="t('harvester.host.ksmtuned.enableMergeNodes')" />
<h3> <h3>
<t k="harvester.host.ksmtuned.modeLink" :raw="true" /> <t k="harvester.host.ksmtuned.modeLink" :raw="true" :url="ksmtunedLink" />
</h3> </h3>
<RadioGroup <RadioGroup
v-model:value="spec.mode" 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 { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { HCI } from '../../types'; import { HCI } from '../../types';
import DiskTags from '../../components/DiskTags';
import HarvesterDisk from './HarvesterDisk'; import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder'; import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned'; import HarvesterKsmtuned from './HarvesterKsmtuned';
import Tags from '../../components/DiskTags';
export const LONGHORN_SYSTEM = 'longhorn-system'; export const LONGHORN_SYSTEM = 'longhorn-system';
@ -46,7 +46,7 @@ export default {
ButtonDropdown, ButtonDropdown,
KeyValue, KeyValue,
Banner, Banner,
DiskTags, Tags,
Loading, Loading,
HarvesterSeeder, HarvesterSeeder,
MessageLink, MessageLink,
@ -478,10 +478,11 @@ export default {
const disks = this.longhornNode?.spec?.disks || {}; const disks = this.longhornNode?.spec?.disks || {};
// update each disk tags and scheduling
this.newDisks.map((disk) => { this.newDisks.map((disk) => {
(disks[disk.name] || {}).tags = disk.tags; (disks[disk.name] || {}).tags = disk.tags;
(disks[disk.name] || {}).allowScheduling = disk.allowScheduling;
}); });
let count = 0; let count = 0;
const retrySave = async() => { 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" class="row mb-20"
> >
<div class="col span-12"> <div class="col span-12">
<DiskTags <Tags
v-model:value="longhornNode.spec.tags" v-model:value="longhornNode.spec.tags"
:label="t('harvester.host.tags.label')" :label="t('harvester.host.tags.label')"
:add-label="t('harvester.host.tags.addLabel')" :add-label="t('harvester.host.tags.addLabel')"
@ -650,7 +653,6 @@ export default {
:value="filteredLabels" :value="filteredLabels"
:add-label="t('labels.addLabel')" :add-label="t('labels.addLabel')"
:mode="mode" :mode="mode"
:title="t('labels.labels.title')"
:read-allowed="false" :read-allowed="false"
:value-can-be-empty="true" :value-can-be-empty="true"
@update:value="updateHostLabels" @update:value="updateHostLabels"

View File

@ -143,10 +143,19 @@ export default {
}, },
input(neu) { 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 !== '') { return;
this.config.vlan = neu > 4094 ? 4094 : 1; }
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 { allHash } from '@shell/utils/promise';
import { clone } from '@shell/utils/object'; import { clone } from '@shell/utils/object';
import { CSI_DRIVER } from '../../types'; import { CSI_DRIVER } from '../../types';
import DiskTags from '../../components/DiskTags'; import Tags from '../../components/DiskTags';
const LONGHORN_DRIVER = 'driver.longhorn.io'; const LONGHORN_DRIVER = 'driver.longhorn.io';
@ -34,7 +34,7 @@ export default {
Tab, Tab,
Tabbed, Tabbed,
Loading, Loading,
DiskTags, Tags,
}, },
mixins: [CreateEditView], mixins: [CreateEditView],
@ -304,7 +304,7 @@ export default {
/> />
</div> </div>
<div class="col span-8 value"> <div class="col span-8 value">
<DiskTags <Tags
v-model:value="scope.row.value.values" v-model:value="scope.row.value.values"
:add-label="t('generic.add')" :add-label="t('generic.add')"
:mode="modeOverride" :mode="modeOverride"

View File

@ -162,6 +162,7 @@ export default {
buttonCb(false); buttonCb(false);
} }
} else { } else {
this.value.spec.url = this.value.spec.url?.trim() || '';
this.save(buttonCb); this.save(buttonCb);
} }
}, },
@ -277,6 +278,7 @@ export default {
:can-yaml="showEditAsYaml ? true : false" :can-yaml="showEditAsYaml ? true : false"
:apply-hooks="applyHooks" :apply-hooks="applyHooks"
@finish="saveImage" @finish="saveImage"
@error="e=>errors=e"
> >
<NameNsDescription <NameNsDescription
ref="nd" 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 CreateEditView from '@shell/mixins/create-edit-view';
import { AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook'; import { AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import { HCI } from '../types'; import { HCI } from '../types';
import { RunStrategys } from '../config/harvester-map';
import VM_MIXIN from '../mixins/harvester-vm'; import VM_MIXIN from '../mixins/harvester-vm';
import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved'; import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume'; import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
@ -74,7 +73,6 @@ export default {
description: '', description: '',
defaultVersion: null, defaultVersion: null,
isDefaultVersion: false, isDefaultVersion: false,
RunStrategys,
}; };
}, },
@ -263,7 +261,7 @@ export default {
<Tab <Tab
name="nodeScheduling" name="nodeScheduling"
:label="t('workload.container.titles.nodeScheduling')" :label="t('workload.container.titles.nodeScheduling')"
:weight="-89" :weight="-3"
> >
<template #default="{active}"> <template #default="{active}">
<NodeScheduling <NodeScheduling
@ -274,7 +272,7 @@ export default {
</template> </template>
</Tab> </Tab>
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-90"> <Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="-4">
<template #default="{active}"> <template #default="{active}">
<PodAffinity <PodAffinity
:mode="mode" :mode="mode"
@ -287,13 +285,42 @@ export default {
</template> </template>
</Tab> </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"> <Tab name="advanced" :label="t('harvester.tab.advanced')" :weight="-99">
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6"> <div class="col span-6">
<LabeledSelect <LabeledSelect
v-model:value="runStrategy" v-model:value="runStrategy"
label-key="harvester.virtualMachine.runStrategy" label-key="harvester.virtualMachine.runStrategy"
:options="RunStrategys" :options="runStrategies"
:mode="mode" :mode="mode"
/> />
</div> </div>
@ -308,6 +335,37 @@ export default {
</div> </div>
</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"> <div class="row mb-20">
<a v-if="showAdvanced" v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" /> <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" /> <a v-else v-t="'harvester.generic.showMore'" role="button" @click="toggleAdvanced" />
@ -315,13 +373,6 @@ export default {
<div v-if="showAdvanced"> <div v-if="showAdvanced">
<div class="row mb-20"> <div class="row mb-20">
<div class="col span-6">
<Reserved
:reserved-memory="reservedMemory"
:mode="mode"
@updateReserved="updateReserved"
/>
</div>
<div class="col span-6"> <div class="col span-6">
<UnitInput <UnitInput
v-model:value="terminationGracePeriodSeconds" v-model:value="terminationGracePeriodSeconds"
@ -386,35 +437,6 @@ export default {
:mode="mode" :mode="mode"
/> />
</Tab> </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> </Tabbed>
</CruResource> </CruResource>
</template> </template>

View File

@ -56,6 +56,7 @@ export default {
const hash = await allHash(_hash); const hash = await allHash(_hash);
this.snapshots = hash.snapshots; this.snapshots = hash.snapshots;
this.images = hash.images;
const defaultStorage = this.$store.getters[`harvester/all`](STORAGE_CLASS).find( O => O.isDefault); const defaultStorage = this.$store.getters[`harvester/all`](STORAGE_CLASS).find( O => O.isDefault);
@ -77,6 +78,7 @@ export default {
storage, storage,
imageId, imageId,
snapshots: [], snapshots: [],
images: [],
}; };
}, },
@ -108,10 +110,8 @@ export default {
}, },
imageOption() { imageOption() {
const choices = this.$store.getters['harvester/all'](HCI.IMAGE);
return sortBy( return sortBy(
choices this.images
.filter(obj => obj.isReady) .filter(obj => obj.isReady)
.map((obj) => { .map((obj) => {
return { return {
@ -249,7 +249,17 @@ export default {
this.value['spec'] = spec; 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() { generateYaml() {
const out = saferDump(this.value); const out = saferDump(this.value);
@ -300,7 +310,7 @@ export default {
required required
:mode="mode" :mode="mode"
class="mb-20" class="mb-20"
@update:value="update" @update:value="updateImage"
/> />
<LabeledSelect <LabeledSelect

View File

@ -140,15 +140,24 @@ export default {
onImageChange() { onImageChange() {
const imageResource = this.$store.getters['harvester/all'](HCI.IMAGE)?.find( I => this.value.image === I.id); 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 (isIsoImage) {
if (/iso$/i.test(imageResource?.imageSuffix)) { this.value['type'] = 'cd-rom';
this.value['type'] = 'cd-rom'; this.value['bus'] = 'sata';
this.value['bus'] = 'sata'; } else {
} else { this.value['type'] = 'disk';
this.value['type'] = 'disk'; this.value['bus'] = 'virtio';
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(); this.update();

View File

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

View File

@ -314,12 +314,21 @@ export default {
} else if (selector[HOSTNAME] && Object.keys(selector).length === 1) { } else if (selector[HOSTNAME] && Object.keys(selector).length === 1) {
const matchNode = allNodes.find(n => n.id === selector[HOSTNAME]); const matchNode = allNodes.find(n => n.id === selector[HOSTNAME]);
this.matchingNodes = { if (matchNode) {
matched: 1, this.matchingNodes = {
total: allNodes.length, matched: 1,
none: false, total: allNodes.length,
sample: matchNode ? matchNode.nameDisplay : selector[HOSTNAME], none: false,
}; sample: matchNode.nameDisplay,
};
} else {
this.matchingNodes = {
matched: 0,
total: 0,
none: true,
sample: null,
};
}
} else { } else {
const match = matching(allNodes, selector); 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: '' default: ''
}, },
showReserved: { showAllocated: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
this.longhornSettings = await this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.SETTINGS });
},
data() { data() {
return {}; const inStore = this.$store.getters['currentProduct'].inStore;
const longhornSettings = this.$store.getters[`${ inStore }/all`](LONGHORN.SETTINGS) || [];
return { longhornSettings };
}, },
computed: { computed: {
usage() { storageStats() {
const stats = {
used: 0,
scheduled: 0,
maximum: 0,
reserved: 0,
total: 0
};
const inStore = this.$store.getters['currentProduct'].inStore; 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() { Object.keys(disks).map((key) => {
const inStore = this.$store.getters['currentProduct'].inStore; stats.scheduled += node?.spec?.allowScheduling ? (diskStatus[key]?.storageScheduled || 0) : 0;
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.row.id }`); stats.reserved += disks[key]?.storageReserved || 0;
let reserved = 0; });
Object.values(diskStatus).map((diskStat) => {
const disks = longhornNode?.spec?.disks || {}; stats.maximum += diskStat?.storageMaximum || 0;
Object.values(disks).map((disk) => {
if (disk.allowScheduling) {
reserved += disk.storageReserved;
}
}); });
return reserved; stats.total = ((stats.maximum - stats.reserved) * Number(storageOverProvisioningPercentageSetting?.value ?? 0)) / 100;
},
total() { return stats;
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;
}, },
units() { units() {
const exponent = exponentNeeded(this.total, 1024); const exponent = exponentNeeded(this.storageStats.maximum, 1024);
return `${ UNITS[exponent] }iB`; return `${ UNITS[exponent] }iB`;
}, },
used() { used() {
let out = this.formatter(this.usage || 0); let out = this.formatter(this.storageStats.used);
if (!Number.parseFloat(out) > 0) { if (!Number.parseFloat(out) > 0) {
out = this.formatter(this.usage || 0, { canRoundToZero: false }); out = this.formatter(this.storageStats.used, { canRoundToZero: false });
} }
return out; return out;
}, },
formatReserved() { formatAllocated() {
let out = this.formatter(this.reserved || 0); let out = this.formatter(this.storageStats.scheduled);
if (!Number.parseFloat(out) > 0) { if (!Number.parseFloat(out) > 0) {
out = this.formatter(this.reserved || 0, { canRoundToZero: false }); out = this.formatter(this.storageStats.scheduled, { canRoundToZero: false });
} }
return out; return out;
@ -102,15 +101,15 @@ export default {
usedAmountTemplateValues() { usedAmountTemplateValues() {
return { return {
used: this.used, used: this.used,
total: this.formatter(this.total || 0), total: this.formatter(this.storageStats.maximum),
unit: this.units, unit: this.units,
}; };
}, },
reservedAmountTemplateValues() { allocatedAmountTemplateValues() {
return { return {
used: this.formatReserved, used: this.formatAllocated,
total: this.formatter(this.total || 0), total: this.formatter(this.storageStats.total),
unit: this.units, unit: this.units,
}; };
}, },
@ -118,7 +117,7 @@ export default {
methods: { methods: {
formatter(value, format) { formatter(value, format) {
const minExponent = exponentNeeded(this.total, 1024); const minExponent = exponentNeeded(this.storageStats.maximum, 1024);
const formatOptions = { const formatOptions = {
addSuffix: false, addSuffix: false,
increment: 1024, increment: 1024,
@ -137,21 +136,21 @@ export default {
<template> <template>
<div> <div>
<div <div
v-if="showReserved" v-if="showAllocated"
> >
<ConsumptionGauge <ConsumptionGauge
:capacity="total" :capacity="storageStats.total"
:used="reserved" :used="storageStats.scheduled"
:units="units" :units="units"
:number-formatter="formatter" :number-formatter="formatter"
:resource-name="resourceName" :resource-name="resourceName"
> >
<template #title="{formattedPercentage}"> <template #title="{formattedPercentage}">
<span> <span>
{{ t('clusterIndexPage.hardwareResourceGauge.reserved') }} {{ t('clusterIndexPage.hardwareResourceGauge.allocated') }}
</span> </span>
<span class="precent-data"> <span class="precent-data">
{{ t('node.detail.glance.consumptionGauge.amount', reservedAmountTemplateValues) }} {{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}
<span class="ml-10 percentage"> <span class="ml-10 percentage">
/&nbsp;{{ formattedPercentage }} /&nbsp;{{ formattedPercentage }}
</span> </span>
@ -160,13 +159,13 @@ export default {
</ConsumptionGauge> </ConsumptionGauge>
</div> </div>
<ConsumptionGauge <ConsumptionGauge
:capacity="total" :capacity="storageStats.maximum"
:used="usage" :used="storageStats.used"
:units="units" :units="units"
:number-formatter="formatter" :number-formatter="formatter"
:resource-name="showReserved ? '' : resourceName" :resource-name="showAllocated ? '' : resourceName"
:class="{ :class="{
'mt-10': showReserved, 'mt-10': showAllocated,
}" }"
> >
<template #title="{formattedPercentage}"> <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 inStore = this.$store.getters['currentProduct'].inStore;
const hash = { const hash = {
vms: this.fetchClusterResources(HCI.VM), vms: this.fetchClusterResources(HCI.VM),
nodes: this.fetchClusterResources(NODE), nodes: this.fetchClusterResources(NODE),
events: this.fetchClusterResources(EVENT), events: this.fetchClusterResources(EVENT),
metricNodes: this.fetchClusterResources(METRIC.NODE), metricNodes: this.fetchClusterResources(METRIC.NODE),
settings: this.fetchClusterResources(HCI.SETTING), settings: this.fetchClusterResources(HCI.SETTING),
services: this.fetchClusterResources(SERVICE), services: this.fetchClusterResources(SERVICE),
metric: this.fetchClusterResources(METRIC.NODE), metric: this.fetchClusterResources(METRIC.NODE),
longhornNode: this.fetchClusterResources(LONGHORN.NODES) || [], longhornNodes: this.fetchClusterResources(LONGHORN.NODES),
_pods: this.$store.dispatch('harvester/findAll', { type: POD }), longhornSettings: this.fetchClusterResources(LONGHORN.SETTINGS),
_pods: this.$store.dispatch('harvester/findAll', { type: POD }),
}; };
(this.accessibleResources || []).map((a) => { (this.accessibleResources || []).map((a) => {
@ -155,6 +156,10 @@ export default {
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }); 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); const res = await allHash(hash);
for ( const k in res ) { for ( const k in res ) {
@ -226,6 +231,7 @@ export default {
showClusterMetrics: false, showClusterMetrics: false,
showVmMetrics: false, showVmMetrics: false,
enabledMonitoringAddon: false, enabledMonitoringAddon: false,
hasLonghornSchema: false,
}; };
}, },
@ -305,8 +311,7 @@ export default {
currentVersion() { currentVersion() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
const settings = this.$store.getters[`${ inStore }/all`](HCI.SETTING); const setting = this.$store.getters[`${ inStore }/byId`](HCI.SETTING, 'server-version');
const setting = settings.find( S => S.id === 'server-version');
return setting?.value || setting?.default; return setting?.value || setting?.default;
}, },
@ -364,53 +369,46 @@ export default {
return out; return out;
}, },
storageUsage() { storageStats() {
const inStore = this.$store.getters['currentProduct'].inStore; const storageOverProvisioningPercentageSetting = this.longhornSettings.find(s => s.id === 'longhorn-system/storage-over-provisioning-percentage');
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES) || []; const stats = this.longhornNodes.reduce((total, node) => {
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) => {
const disks = node?.spec?.disks || {}; 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 || {}; const diskStatus = node?.status?.diskStatus || {};
Object.values(diskStatus).map((disk) => { total.used += node?.spec?.allowScheduling ? node.used : 0;
if (disk?.storageMaximum) {
out += disk.storageMaximum; 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() { storageUsed() {
return this.createMemoryValues(this.storageTotal, this.storageUsage); const stats = this.storageStats;
return this.createMemoryValues(stats.maximum, stats.used);
}, },
storageReserved() { storageAllocated() {
return this.createMemoryValues(this.storageTotal, this.storageReservedTotal); const stats = this.storageStats;
return this.createMemoryValues(stats.total, stats.scheduled);
}, },
vmEvents() { vmEvents() {
@ -644,7 +642,7 @@ export default {
<div <div
class="hardware-resource-gauges" class="hardware-resource-gauges"
:class="{ :class="{
live: !storageTotal, live: !hasLonghornSchema,
}" }"
> >
<HardwareResourceGauge <HardwareResourceGauge
@ -658,10 +656,11 @@ export default {
:used="ramUsed" :used="ramUsed"
/> />
<HardwareResourceGauge <HardwareResourceGauge
v-if="storageTotal" v-if="hasLonghornSchema"
:name="t('harvester.dashboard.hardwareResourceGauge.storage')" :name="t('harvester.dashboard.hardwareResourceGauge.storage')"
:used="storageUsed" :used="storageUsed"
:reserved="storageReserved" :reserved="storageAllocated"
:reserved-title="t('clusterIndexPage.hardwareResourceGauge.allocated')"
/> />
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -81,6 +81,10 @@ export default {
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), 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)) { if (this.$store.getters[`${ inStore }/schemaFor`](NODE)) {
_hash.nodes = this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }); _hash.nodes = this.$store.dispatch(`${ inStore }/findAll`, { type: NODE });
this.hasNode = true; this.hasNode = true;
@ -116,7 +120,7 @@ export default {
headers() { headers() {
const restoreCol = { const restoreCol = {
name: 'restoreProgress', name: 'restoreProgress',
labelKey: 'tableHeaders.restore', labelKey: 'harvester.tableHeaders.restore',
value: 'restoreProgress', value: 'restoreProgress',
align: 'left', align: 'left',
formatter: 'HarvesterBackupProgressBar', formatter: 'HarvesterBackupProgressBar',
@ -128,7 +132,7 @@ export default {
value: 'nodeName', value: 'nodeName',
sort: ['realAttachNodeName'], sort: ['realAttachNodeName'],
formatter: 'HarvesterHost', formatter: 'HarvesterHost',
labelKey: 'tableHeaders.node' labelKey: 'harvester.tableHeaders.vm.node'
}; };
const cols = clone(VM_HEADERS); const cols = clone(VM_HEADERS);
@ -137,7 +141,7 @@ export default {
cols.splice(-1, 0, nodeCol); cols.splice(-1, 0, nodeCol);
} }
if (this.hasRestoredVMs) { if (this.hasBackUpRestoreInProgress) {
cols.splice(-1, 0, restoreCol); cols.splice(-1, 0, restoreCol);
} }
@ -150,8 +154,11 @@ export default {
return [...this.allVMs, ...matchVMIs]; 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" key-field="_key"
> >
<template cell:state="scope"> <template cell:state="scope" class="state-col">
<div class="state"> <div class="state">
<HarvesterVmState class="vmstate" :row="scope.row" :all-node-network="allNodeNetworks" :all-cluster-network="allClusterNetworks" /> <HarvesterVmState class="vmstate" :row="scope.row" :all-node-network="allNodeNetworks" :all-cluster-network="allClusterNetworks" />
</div> </div>

View File

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

View File

@ -18,7 +18,7 @@ import {
import { HOSTNAME } from '@shell/config/labels-annotations'; import { HOSTNAME } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { uniq } from '@shell/utils/array'; 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_SETTING } from '../../config/settings';
import { HCI } from '../../types'; import { HCI } from '../../types';
import { parseVolumeClaimTemplates } from '../../utils/vm'; import { parseVolumeClaimTemplates } from '../../utils/vm';
@ -136,6 +136,9 @@ export default {
spec: null, spec: null,
osType: 'linux', osType: 'linux',
sshKey: [], sshKey: [],
maintenanceStrategies,
maintenanceStrategy: 'Migrate',
runStrategies,
runStrategy: 'RerunOnFailure', runStrategy: 'RerunOnFailure',
installAgent: true, installAgent: true,
hasCreateVolumes: [], hasCreateVolumes: [],
@ -164,6 +167,7 @@ export default {
enabledSriovgpu: false, enabledSriovgpu: false,
immutableMode: this.realMode === _CREATE ? _CREATE : _VIEW, immutableMode: this.realMode === _CREATE ? _CREATE : _VIEW,
terminationGracePeriodSeconds: '', 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 runStrategy = spec.runStrategy || 'RerunOnFailure';
const machineType = value.machineType; const machineType = value.machineType;
const cpu = spec.template.spec.domain?.cpu?.cores; const cpu = spec.template.spec.domain?.cpu?.cores;
@ -352,6 +361,7 @@ export default {
const efiEnabled = this.isEfiEnabled(spec); const efiEnabled = this.isEfiEnabled(spec);
const tpmEnabled = this.isTpmEnabled(spec); const tpmEnabled = this.isTpmEnabled(spec);
const secureBoot = this.isSecureBoot(spec); const secureBoot = this.isSecureBoot(spec);
const cpuPinning = this.isCpuPinning(spec);
const secretRef = this.getSecret(spec); const secretRef = this.getSecret(spec);
const accessCredentials = this.getAccessCredentials(spec); const accessCredentials = this.getAccessCredentials(spec);
@ -362,6 +372,7 @@ export default {
} }
this['spec'] = spec; this['spec'] = spec;
this['maintenanceStrategy'] = maintenanceStrategy;
this['runStrategy'] = runStrategy; this['runStrategy'] = runStrategy;
this['secretRef'] = secretRef; this['secretRef'] = secretRef;
this['accessCredentials'] = accessCredentials; this['accessCredentials'] = accessCredentials;
@ -382,6 +393,7 @@ export default {
this['efiEnabled'] = efiEnabled; this['efiEnabled'] = efiEnabled;
this['tpmEnabled'] = tpmEnabled; this['tpmEnabled'] = tpmEnabled;
this['secureBoot'] = secureBoot; this['secureBoot'] = secureBoot;
this['cpuPinning'] = cpuPinning;
this['hasCreateVolumes'] = hasCreateVolumes; this['hasCreateVolumes'] = hasCreateVolumes;
this['networkRows'] = networkRows; this['networkRows'] = networkRows;
@ -401,15 +413,37 @@ export default {
let out = []; let out = [];
if (_disks.length === 0) { 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({ out.push({
id: randomStr(5), id: randomStr(5),
source: SOURCE_TYPE.IMAGE, source: SOURCE_TYPE.IMAGE,
name: 'disk-0', name: 'disk-0',
accessMode: 'ReadWriteMany', accessMode: 'ReadWriteMany',
bus: 'virtio', bus,
volumeName: '', volumeName: '',
size: '10Gi', size,
type: HARD_DISK, type,
storageClassName: '', storageClassName: '',
image: this.imageId, image: this.imageId,
volumeMode: 'Block', volumeMode: 'Block',
@ -574,6 +608,12 @@ export default {
} else { } else {
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory; 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) { parseDiskRows(disk) {
@ -681,27 +721,24 @@ export default {
spec = this.multiVMScheduler(spec); 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.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.CREATOR]: 'harvester',
[HCI_ANNOTATIONS.OS]: this.osType [HCI_ANNOTATIONS.OS]: this.osType};
};
this.value['spec'] = spec; this.value['spec'] = spec;
this['spec'] = spec; this['spec'] = spec;
} else if (this.resource === HCI.VM_VERSION) { } else if (this.resource === HCI.VM_VERSION) {
this.value.spec.vm['spec'] = spec; 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['annotations'] = {
this.value.spec.vm.metadata['labels'] = { ...this.value.spec.vm.metadata.annotations,
...this.value.spec.vm.metadata.labels, [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: JSON.stringify(volumeClaimTemplates),
[HCI_ANNOTATIONS.OS]: this.osType
}; };
this.value.spec.vm.metadata['labels'] = {...this.value.spec.vm.metadata.labels,
[HCI_ANNOTATIONS.OS]: this.osType,};
this['spec'] = spec; this['spec'] = spec;
} }
}, },
@ -819,6 +856,10 @@ export default {
} }
}, },
getMaintenanceStrategyOptionLabel(opt) {
return this.t(`harvester.virtualMachine.maintenanceStrategy.options.${ opt.label || opt }`);
},
getInitUserData(config) { getInitUserData(config) {
const _QGA_JSON = this.getMatchQGA(config.osType); 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) { setTPM(tpmEnabled) {
if (tpmEnabled) { if (tpmEnabled) {
set(this.spec.template.spec.domain.devices, 'tpm', {}); set(this.spec.template.spec.domain.devices, 'tpm', {});
@ -1462,6 +1511,10 @@ export default {
this.setBootMethod({ efi: this.efiEnabled, secureBoot: val }); this.setBootMethod({ efi: this.efiEnabled, secureBoot: val });
}, },
cpuPinning(value) {
this.setCpuPinning(value);
},
tpmEnabled(val) { tpmEnabled(val) {
this.setTPM(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 { get } from '@shell/utils/object';
import { NAMESPACE } from '@shell/config/types'; import { NAMESPACE } from '@shell/config/types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { HCI } from '../../types';
const OBSCURE_NAMESPACE_PREFIX = [ const OBSCURE_NAMESPACE_PREFIX = [
'c-', // cluster namespace 'c-', // cluster namespace
@ -37,15 +38,32 @@ export default class HciNamespace extends namespace {
weight: -10, 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) { if (remove > -1) {
out.splice(remove, 1); out.splice(remove, 1);
} }
insertAt(out, out.length - 1, promptRemove); insertAt(out, out.length - 1, promptRemove);
insertAt(out, out.length - 5, editQuotaAction);
return out; return out;
} }
editNSQuota(resources = this) {
this.$dispatch('promptModal', {
resources,
snapshotSizeQuota: this.snapshotSizeQuota,
component: 'HarvesterQuotaDialog'
});
}
promptRemove(resources = this) { promptRemove(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, 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() { get isSystem() {
const systemNamespaces = ['fleet-default']; const systemNamespaces = ['fleet-default'];

View File

@ -59,6 +59,22 @@ export default class HciNode extends HarvesterResource {
total: 1 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 = { const shutDown = {
action: 'shutDown', action: 'shutDown',
enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && !this.isStopped && !!this.inventory, enabled: this.hasAction('powerActionPossible') && this.hasAction('powerAction') && !this.isStopped && !!this.inventory,
@ -88,6 +104,8 @@ export default class HciNode extends HarvesterResource {
uncordon, uncordon,
enableMaintenance, enableMaintenance,
disableMaintenance, disableMaintenance,
enableCPUManager,
disableCPUManager,
shutDown, shutDown,
powerOn, powerOn,
reboot, reboot,
@ -134,15 +152,12 @@ export default class HciNode extends HarvesterResource {
get consoleUrl() { get consoleUrl() {
const url = this.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL]; const url = this.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
const validator = /^[a-z]+:\/\//;
if (!url) { if (!url?.match(validator)) {
return false; return false;
} }
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return `http://${ url }`;
}
return url; return url;
} }
@ -308,6 +323,14 @@ export default class HciNode extends HarvesterResource {
this.doAction('disableMaintenanceMode', {}); this.doAction('disableMaintenanceMode', {});
} }
enableCPUManager() {
this.doActionGrowl('enableCPUManager', {});
}
disableCPUManager() {
this.doActionGrowl('disableCPUManager', {});
}
get isUnSchedulable() { get isUnSchedulable() {
return ( return (
this.metadata?.labels?.[HCI_ANNOTATIONS.NODE_SCHEDULABLE] === 'false' || 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() { get longhornDisks() {
const inStore = this.$rootGetters['currentProduct'].inStore; const inStore = this.$rootGetters['currentProduct'].inStore;
const longhornNode = this.$rootGetters[`${ inStore }/byId`]( const longhornNode = this.$rootGetters[`${ inStore }/byId`](

View File

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

View File

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

View File

@ -47,7 +47,7 @@ export default class HciVmImage extends HarvesterResource {
{ {
action: 'createFromImage', action: 'createFromImage',
enabled: canCreateVM, enabled: canCreateVM,
icon: 'icon icon-fw icon-spinner', icon: 'icon icon-circle-plus',
label: this.t('harvester.action.createVM'), label: this.t('harvester.action.createVM'),
disabled: !this.isReady, disabled: !this.isReady,
}, },
@ -74,7 +74,7 @@ export default class HciVmImage extends HarvesterResource {
router.push({ router.push({
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
params: { resource: HCI.VM }, 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'); const imported = this.getStatusConditionOfType('Imported');
if (imported?.status === 'Unknown') { if (imported?.status === 'Unknown') {
if (this.spec.sourceType === 'restore') {
return 'Restoring';
}
if (this.spec.sourceType === 'download') { if (this.spec.sourceType === 'download') {
return 'Downloading'; return 'Downloading';
} }
@ -131,7 +135,8 @@ export default class HciVmImage extends HarvesterResource {
const conditions = this?.status?.conditions || []; const conditions = this?.status?.conditions || [];
const initialized = conditions.find( cond => cond.type === 'Initialized'); const initialized = conditions.find( cond => cond.type === 'Initialized');
const imported = conditions.find( cond => cond.type === 'Imported'); 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); 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 = []) { getStatusConditionOfType(type, defaultValue = []) {
const conditions = Array.isArray(get(this, 'status.conditions')) ? this.status.conditions : 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', action: 'softrebootVM',
enabled: !!this.actions?.softreboot, enabled: !!this.actions?.softreboot,
icon: 'icon icon-refresh', icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot') label: this.t('harvester.action.softreboot')
}, },
{ {
@ -152,9 +152,15 @@ export default class VirtVm extends HarvesterResource {
{ {
action: 'takeVMSnapshot', action: 'takeVMSnapshot',
enabled: !!this.actions?.backup, enabled: !!this.actions?.backup,
icon: 'icon icon-backup', icon: 'icon icon-snapshot',
label: this.t('harvester.action.vmSnapshot') 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', action: 'restoreVM',
enabled: !!this.actions?.restore, 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) { unplugVolume(diskName) {
const resources = this; const resources = this;
@ -541,6 +555,17 @@ export default class VirtVm extends HarvesterResource {
return null; 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() { get vmi() {
const inStore = this.productInStore; const inStore = this.productInStore;
@ -646,6 +671,10 @@ export default class VirtVm extends HarvesterResource {
return null; return null;
} }
get isTerminating() {
return !!this?.metadata?.deletionTimestamp;
}
get otherState() { get otherState() {
const state = (this.vmi && const state = (this.vmi &&
[VMIPhase.Scheduling, VMIPhase.Scheduled].includes( [VMIPhase.Scheduling, VMIPhase.Scheduled].includes(
@ -685,18 +714,21 @@ export default class VirtVm extends HarvesterResource {
const allRestore = this.$rootGetters[`${ inStore }/all`](HCI.RESTORE); 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() { get restoreProgress() {
const inStore = this.productInStore; if (this.isVMError || this.isTerminating) {
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) {
return {}; return {};
} }
@ -726,7 +758,7 @@ export default class VirtVm extends HarvesterResource {
return 'Restoring'; return 'Restoring';
} }
if (this?.metadata?.deletionTimestamp) { if (this.isTerminating) {
return 'Terminating'; return 'Terminating';
} }

View File

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

View File

@ -18,16 +18,16 @@ export default {
const cluster = await dispatch('management/find', { const cluster = await dispatch('management/find', {
type: MANAGEMENT.CLUSTER, type: MANAGEMENT.CLUSTER,
id, id,
opt: { url: `${MANAGEMENT.CLUSTER}s/${escape(id)}` } opt: { url: `${ MANAGEMENT.CLUSTER }s/${ escape(id) }` }
}, { root: true }); }, { root: true });
let virtualBase = `/k8s/clusters/${escape(id)}/v1/harvester`; let virtualBase = `/k8s/clusters/${ escape(id) }/v1/harvester`;
if (id === 'local') { if (id === 'local') {
virtualBase = `/v1/harvester`; virtualBase = `/v1/harvester`;
} }
if (!cluster) { if ( !cluster ) {
commit('clusterId', null, { root: true }); commit('clusterId', null, { root: true });
commit('applyConfig', { baseUrl: null }); commit('applyConfig', { baseUrl: null });
throw new ClusterNotFoundError(id); throw new ClusterNotFoundError(id);
@ -44,22 +44,22 @@ export default {
const projectArgs = { const projectArgs = {
type: MANAGEMENT.PROJECT, type: MANAGEMENT.PROJECT,
opt: { opt: {
url: `${MANAGEMENT.PROJECT}/${escape(id)}`, url: `${ MANAGEMENT.PROJECT }/${ escape(id) }`,
watchNamespace: id watchNamespace: id
} }
}; };
const fetchProjects = async () => { const fetchProjects = async() => {
let limit = 30000; let limit = 30000;
const sleep = 100; const sleep = 100;
while (limit > 0 && !rootState.managementReady) { while ( limit > 0 && !rootState.managementReady ) {
await setTimeout(() => { }, sleep); await setTimeout(() => {}, sleep);
limit -= sleep; limit -= sleep;
} }
if (rootGetters['management/schemaFor'](MANAGEMENT.PROJECT)) { if ( rootGetters['management/schemaFor'](MANAGEMENT.PROJECT) ) {
return dispatch('management/findAll', projectArgs, { root: true }); return dispatch('management/findAll', projectArgs, { root: true });
} }
}; };
@ -67,8 +67,8 @@ export default {
if (id !== 'local' && getters['schemaFor'](MANAGEMENT.SETTING)) { // multi-cluster if (id !== 'local' && getters['schemaFor'](MANAGEMENT.SETTING)) { // multi-cluster
const settings = await dispatch('findAll', { const settings = await dispatch('findAll', {
type: MANAGEMENT.SETTING, type: MANAGEMENT.SETTING,
id: SETTING.SYSTEM_NAMESPACES, id: SETTING.SYSTEM_NAMESPACES,
opt: { url: `${virtualBase}/${MANAGEMENT.SETTING}s/`, force: true } opt: { url: `${ virtualBase }/${ MANAGEMENT.SETTING }s/`, force: true }
}); });
const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES); const systemNamespaces = settings?.find((x: any) => x.id === SETTING.SYSTEM_NAMESPACES);
@ -80,17 +80,21 @@ export default {
} }
} }
const hash: { [key: string]: Promise<any> } = { const hash: { [key: string]: Promise<any>} = {
projects: fetchProjects(), projects: fetchProjects(),
virtualCount: dispatch('findAll', { type: COUNT }), virtualCount: dispatch('findAll', { type: COUNT }),
virtualNamespaces: dispatch('findAll', { type: NAMESPACE }), virtualNamespaces: dispatch('findAll', { type: NAMESPACE }),
settings: dispatch('findAll', { type: HCI.SETTING }), settings: dispatch('findAll', { type: HCI.SETTING }),
clusters: dispatch('management/findAll', { clusters: dispatch('management/findAll', {
type: MANAGEMENT.CLUSTER, type: MANAGEMENT.CLUSTER,
opt: { force: true } opt: { force: true }
}, { root: true }), }, { root: true }),
}; };
if (getters['schemaFor'](HCI.RESOURCE_QUOTA)) {
hash.resourceQuota = dispatch('findAll', { type: HCI.RESOURCE_QUOTA });
}
if (getters['schemaFor'](HCI.UPGRADE)) { if (getters['schemaFor'](HCI.UPGRADE)) {
hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE }); hash.upgrades = dispatch('findAll', { type: HCI.UPGRADE });
} }
@ -101,15 +105,15 @@ export default {
commit('updateNamespaces', { commit('updateNamespaces', {
filters: [], filters: [],
all: getters.filterNamespace(), all: getters.filterNamespace(),
getters getters
}, { root: true }); }, { root: true });
// Solve compatibility with Rancher v2.6.x, fell remove these codes after not support v2.6.x // Solve compatibility with Rancher v2.6.x, fell remove these codes after not support v2.6.x
const definition = { const definition = {
def: false, def: false,
parseJSON: true, parseJSON: true,
inheritFrom: DEV, inheritFrom: DEV,
asUserPreference: true, asUserPreference: true,
}; };
@ -134,9 +138,9 @@ export default {
if (isMultiCluster) { if (isMultiCluster) {
commit('managementChanged', { commit('managementChanged', {
ready: true, ready: true,
isMultiCluster: true, isMultiCluster: true,
isRancher: true, isRancher: true,
}, { root: true }); }, { root: true });
} }
}, },

View File

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

View File

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

View File

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