Add pkg/harvester components + shell portings - 1

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia
2024-10-23 17:00:46 +02:00
parent f8408469f7
commit 4f2688f6ab
265 changed files with 42316 additions and 82 deletions
@@ -0,0 +1,331 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
const COMPLETE = 'complete';
const PROMOTE_RESTART = 'promoteRestart';
const PROMOTE_SUCCEED = 'promoteSucceed';
export default {
name: 'BasicNode',
components: {
Banner,
LabelValue,
HarvesterCPUUsed,
HarvesterMemoryUsed,
HarvesterStorageUsed,
},
props: {
value: {
type: Object,
required: true,
},
metrics: {
type: Object,
required: false,
default: () => {
return null;
}
},
mode: {
type: String,
required: false,
default: 'view'
},
},
computed: {
customName() {
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
},
consoleUrl() {
const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
let value = consoleUrl;
if (!consoleUrl) {
return '';
}
if (!consoleUrl.startsWith('http://') && !consoleUrl.startsWith('https://')) {
value = `http://${ consoleUrl }`;
}
return {
display: consoleUrl,
value
};
},
cpuTotal() {
let out = 0;
if (this.metrics) {
out = this.metrics.cpuCapacity;
}
return out;
},
cpuUsage() {
let out = 0;
if (this.metrics) {
out = this.metrics.cpuUsage;
}
return out;
},
memoryTotal() {
let out = 0;
if (this.metrics) {
out = this.metrics.memoryCapacity;
}
return out;
},
memoryUsage() {
let out = 0;
if (this.metrics) {
out = this.metrics.memoryUsage;
}
return out;
},
cpuUnits() {
return 'C';
},
memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }iB`;
},
nodeType() {
if (this.value.isEtcd) {
return this.t('harvester.host.detail.etcd');
}
if (this.value.isMaster) {
return this.t('harvester.host.detail.management');
}
return this.t('harvester.host.detail.compute');
},
lastUpdateTime() {
return this.value.status?.conditions?.[0]?.lastHeartbeatTime;
},
nodeRoleState() {
if (!this.value.isEtcd) {
const promoteStatus = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.PROMOTE_STATUS];
if (promoteStatus === COMPLETE) {
const isExistRoleStatus = this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== undefined || this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE] !== undefined;
return this.t(`harvester.host.promote.${ isExistRoleStatus ? PROMOTE_SUCCEED : PROMOTE_RESTART }`);
}
}
return null;
},
hasMetricNodeSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](METRIC.NODE);
},
hasLonghornSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES);
},
},
methods: {
memoryFormatter(value) {
const exponent = exponentNeeded(this.memoryTotal, 1024);
const formatOptions = {
addSuffix: false,
increment: 1024,
minExponent: exponent
};
return formatSi(value, formatOptions);
},
}
};
</script>
<template>
<div class="host-detail">
<Banner
v-if="value.isKVMDisable"
color="error"
label-key="harvester.host.detail.kvm.disableMessage"
/>
<h3>{{ t('harvester.host.tabs.overview') }}</h3>
<div class="row mb-20">
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.customName')" :value="customName" />
</div>
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.hostIP')" :value="value.internalIp" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.os')" :value="value.status.nodeInfo.osImage" />
</div>
<div class="col span-6">
<div class="role">
<LabelValue :name="t('harvester.host.detail.role')">
<template #value>
{{ nodeType }}
<span
v-if="nodeRoleState"
class="text-warning ml-20"
>
{{ nodeRoleState }}
</span>
</template>
</LabelValue>
</div>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.create')" :value="value.metadata.creationTimestamp" />
</div>
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.update')" :value="lastUpdateTime" />
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
<a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
</LabelValue>
</div>
</div>
<div v-if="hasMetricNodeSchema">
<hr class="divider" />
<h3>{{ t('harvester.host.tabs.monitor') }}</h3>
<div class="row mb-20">
<div
class="col"
:class="{
'span-4': hasLonghornSchema,
'span-6': !hasLonghornSchema,
}"
>
<HarvesterCPUUsed
:row="value"
:resource-name="t('node.detail.glance.consumptionGauge.cpu')"
:show-used="true"
/>
</div>
<div
class="col"
:class="{
'span-4': hasLonghornSchema,
'span-6': !hasLonghornSchema,
}"
>
<HarvesterMemoryUsed
:row="value"
:resource-name="t('node.detail.glance.consumptionGauge.memory')"
:show-used="true"
/>
</div>
<div
v-if="hasLonghornSchema"
class="col span-4"
>
<HarvesterStorageUsed
:row="value"
:resource-name="t('harvester.host.detail.storage')"
:show-reserved="true"
/>
</div>
</div>
</div>
<hr class="section-divider" />
<h3>{{ t('harvester.host.detail.more') }}</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.detail.uuid')" :value="value.status.nodeInfo.systemUUID" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.detail.kernel')" :value="value.status.nodeInfo.kernelVersion" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.detail.containerRuntime')" :value="value.status.nodeInfo.containerRuntimeVersion" />
</div>
</div>
<div
v-if="value.manufacturer || value.serialNumber || value.model"
class="row mb-20"
>
<div
v-if="value.manufacturer"
class="col span-4"
>
<LabelValue
:name="t('harvester.host.detail.manufacturer')"
:value="value.manufacturer"
/>
</div>
<div
v-if="value.serialNumber"
class="col span-4"
>
<LabelValue
:name="t('harvester.host.detail.serialNumber')"
:value="value.serialNumber"
/>
</div>
<div
v-if="value.model"
class="col span-4"
>
<LabelValue
:name="t('harvester.host.detail.model')"
:value="value.model"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.role {
display: flex;
}
</style>
@@ -0,0 +1,214 @@
<script>
import Tag from '@shell/components/Tag';
import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import HarvesterDisk from '../../mixins/harvester-disk';
export default {
components: {
LabelValue,
BadgeState,
Banner,
Tag
},
mixins: [
HarvesterDisk,
],
props: {
value: {
type: Object,
default: () => {
return {};
},
},
disks: {
type: Array,
default: () => [],
},
mode: {
type: String,
default: 'edit',
},
},
data() {
return {};
},
computed: {
allowSchedulingOptions() {
return [{
label: this.t('generic.enabled'),
value: true,
}, {
label: this.t('generic.disabled'),
value: false,
}];
},
evictionRequestedOptions() {
return [{
label: this.t('generic.yes'),
value: true,
}, {
label: this.t('generic.no'),
value: false,
}];
},
provisionPhase() {
return this.value?.blockDevice?.provisionPhase || {};
},
mountedMessage() {
const state = this.value?.blockDevice?.metadata?.state || {};
if (state?.error) {
return state?.message;
} else {
return '';
}
},
},
methods: {
update() {
this.$emit('input', this.value);
},
canEditPath(value) {
if (this.mountedMessage) {
return true;
}
if (value.isNew && !!value.originPath) {
return true;
}
return false;
},
},
};
</script>
<template>
<div class="disk" @input="update">
<Banner
v-if="mountedMessage"
color="error"
:label="mountedMessage"
/>
<div v-if="!value.isNew">
<div class="row">
<div class="col span-12">
<LabelValue
v-if="value.tags.length"
:name="t('harvester.host.disk.tags.label')"
>
<template #value>
<div class="mt-5">
<Tag v-for="(prop, key) in value.tags" :key="key + prop" class="mr-5">
{{ prop }}
</Tag>
</div>
</template>
</LabelValue>
</div>
</div>
<div class="row mt-10">
<div class="col span-12">
<div class="pull-right">
{{ t('harvester.host.disk.conditions') }}:
<BadgeState
v-clean-tooltip="readyCondition.message"
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
label="Ready"
class="mr-10 ml-10 state"
/>
<BadgeState
v-clean-tooltip="schedulableCondition.message"
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
label="Schedulable"
class="mr-10 state"
/>
<BadgeState
v-if="provisionPhase.label"
:color="provisionPhase.color"
:icon="provisionPhase.icon"
:label="provisionPhase.label"
class="mr-10 state"
/>
</div>
</div>
</div>
<div v-if="!value.isNew" class="row mt-30">
<div class="col span-4">
<LabelValue
:name="t('harvester.host.disk.storageAvailable.label')"
:value="value.storageAvailable"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.disk.storageScheduled.label')"
:value="value.storageScheduled"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.disk.storageMaximum.label')"
:value="value.storageMaximum"
/>
</div>
</div>
<hr class="mt-10" />
</div>
<div class="row mt-10">
<div class="col span-4">
<LabelValue
:name="t('generic.name')"
:value="value.displayName"
/>
</div>
<div class="col span-4">
<LabelValue
:name="t('harvester.host.disk.path.label')"
:value="value.path"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.close {
top: 10px;
right: 10px;
padding:0;
position: absolute;
}
.disk {
position: relative;
.secret-name {
height: $input-height;
}
&:not(:last-of-type) {
padding-bottom: 10px;
margin-bottom: 30px;
}
}
.flex {
display: flex;
justify-content: space-between;
}
.badge-state {
padding: 2px 5px;
}
</style>
@@ -0,0 +1,171 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { HCI } from '../../types';
import { ksmtunedMode, ksmtunedRunOption } from '../../edit/harvesterhci.io.host/HarvesterKsmtuned.vue';
export default {
name: 'HarvesterKsmtuned',
components: { LabelValue },
props: {
mode: {
type: String,
required: true
},
node: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.KSTUNED });
this.ksmtuned = hash.find((node) => {
return node.id === this.node.id;
});
},
data() {
return { ksmtuned: {} };
},
computed: {
modeText() {
const mode = this.ksmtuned.spec.mode;
return ksmtunedMode.find(M => M.value === mode).label;
},
thresCoef() {
return `${ this.ksmtuned.spec.thresCoef } %`;
},
runText() {
const run = this.ksmtuned.spec.run;
return ksmtunedRunOption.find(M => M.value === run).label;
},
showRunInformation() {
return this.ksmtuned.spec.run === 'run';
},
mergeNodesText() {
return this.ksmtuned.spec?.mergeAcrossNodes ? this.t('harvester.host.ksmtuned.enable') : this.t('harvester.host.ksmtuned.disable');
},
ksmdPhase() {
return this.ksmtuned?.status?.ksmdPhase;
},
ksmdPhaseTextColor() {
return this.ksmdPhase === 'Running' ? 'text-success' : 'text-warning';
}
},
};
</script>
<template>
<div>
<template v-if="ksmtuned.status">
<h2>
{{ t('harvester.host.ksmtuned.configure') }}
</h2>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.run')" :value="runText" />
</div>
</div>
<div v-if="showRunInformation" class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.thresCoef')" :value="thresCoef" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.mode')" :value="modeText" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.enableMergeNodes')" :value="mergeNodesText" />
</div>
</div>
<div v-if="showRunInformation">
<hr class="divider" />
<h3>{{ t('harvester.host.ksmtuned.parameters.title') }}</h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.parameters.boost')" :value="ksmtuned.spec.ksmtunedParameters.boost" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.parameters.decay')" :value="ksmtuned.spec.ksmtunedParameters.decay" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.parameters.sleepMsec')" :value="ksmtuned.spec.ksmtunedParameters.sleepMsec" />
</div>
</div>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.parameters.minPages')" :value="ksmtuned.spec.ksmtunedParameters.minPages" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.parameters.maxPages')" :value="ksmtuned.spec.ksmtunedParameters.maxPages" />
</div>
</div>
</div>
<div>
<hr class="divider" />
<h3><t k="harvester.host.ksmtuned.statistics.title" :raw="true" /></h3>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.ksmStatus')">
<template #value>
<span :class="ksmdPhaseTextColor">{{ ksmdPhase }}</span>
</template>
</LabelValue>
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.sharing')" :value="ksmtuned.status.sharing" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.shared')" :value="ksmtuned.status.shared" />
</div>
</div>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.unshared')" :value="ksmtuned.status.unshared" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.volatile')" :value="ksmtuned.status.volatile" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.fullScans')" :value="ksmtuned.status.fullScans" />
</div>
</div>
<div class="row mb-20">
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeDups')" :value="ksmtuned.status.stableNodeDups" />
</div>
<div class="col span-4">
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeChains')" :value="ksmtuned.status.stableNodeChains" />
</div>
</div>
</div>
</template>
</div>
</template>
@@ -0,0 +1,131 @@
<script>
import { RadioGroup } from '@components/Form/Radio';
import LabelValue from '@shell/components/LabelValue';
import { Banner } from '@components/Banner';
export default {
name: 'HarvesterSeeder',
components: {
RadioGroup,
LabelValue,
Banner,
},
props: {
mode: {
type: String,
required: true
},
node: {
type: Object,
required: true,
},
inventory: {
type: Object,
required: true,
},
},
data() {
const enableInventory = !!this.inventory?.id;
return {
enableInventory,
value: this.inventory,
};
},
computed: {
selectedSecret() {
const namespace = this.value.spec?.baseboardSpec?.connection?.authSecretRef?.namespace;
const name = this.value?.spec?.baseboardSpec?.connection?.authSecretRef?.name;
if (namespace && name) {
return `${ namespace }/${ name }`;
} else {
return 'N/A';
}
},
},
};
</script>
<template>
<div>
<div v-if="inventory.warningMessages.length > 0">
<Banner
v-for="msg in inventory.warningMessages"
:key="msg.text"
color="error"
:label="msg.text"
/>
</div>
<div v-if="enableInventory">
<div class="row mb-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.seeder.inventory.host.label')"
:value="value.spec.baseboardSpec.connection.host"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.seeder.inventory.port.label')"
:value="value.spec.baseboardSpec.connection.port"
/>
</div>
</div>
<div class="row">
<div class="col span-6">
<LabelValue
:name="t('harvester.seeder.inventory.insecureTLS.label')"
:value="value.spec.baseboardSpec.connection.insecureTLS ? t('generic.yes') : t('generic.no')"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.seeder.inventory.secret.label')"
:value="selectedSecret"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<LabelValue
:name="t('harvester.seeder.inventory.event.label')"
:value="value.spec.events.enabled ? t('generic.enabled') : t('generic.disabled')"
/>
</div>
<div
v-if="value.spec.events.enabled"
class="col span-6"
>
<LabelValue
:name="t('harvester.seeder.inventory.pollingInterval.label')"
:value="value.spec.events.pollingInterval"
/>
</div>
</div>
</div>
<div
v-else
class="row"
>
<div class="col span-6">
<RadioGroup
v-model="enableInventory"
:options="[
{ label: t('generic.enabled'), value: true },
{ label: t('generic.disabled'), value: false }
]"
:mode="mode"
name="enableInventory"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,131 @@
<script>
import { STATE, AGE, NAME } from '@shell/config/table-headers';
import SortableTable from '@shell/components/SortableTable';
import Loading from '@shell/components/Loading';
import HarvesterVmState from '../../formatters/HarvesterVmState';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import { HOSTNAME } from '@shell/config/labels-annotations';
export default {
name: 'InstanceNode',
components: {
SortableTable,
Loading,
HarvesterVmState,
},
props: {
node: {
type: Object,
required: true,
},
},
async fetch() {
const hash = await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
});
const instanceMap = {};
(hash.vmis || []).forEach((vmi) => {
const vmiUID = vmi?.metadata?.ownerReferences?.[0]?.uid;
if (vmiUID) {
instanceMap[vmiUID] = vmi;
}
});
this.allClusterNetwork = hash.allClusterNetwork;
this.rows = hash.vms.filter((row) => {
return instanceMap[row.metadata?.uid]?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME];
});
},
data() {
return {
rows: [],
allClusterNetwork: []
};
},
computed: {
headers() {
return [
STATE,
NAME,
{
name: 'vmCPU',
labelKey: 'tableHeaders.cpu',
search: false,
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
width: 120
},
{
name: 'vmRAM',
labelKey: 'glance.memory',
search: false,
sort: ['memorySort'],
value: 'spec.template.spec.domain.resources.limits.memory',
width: 120
},
{
name: 'ip',
label: 'IP Address',
labelKey: 'harvester.tableHeaders.vm.ipAddress',
value: 'id',
formatter: 'HarvesterIpAddress'
},
{
...AGE,
sort: 'metadata.creationTimestamp:desc',
}
];
},
},
methods: {}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else id="host-instances" class="row">
<div class="col span-12">
<SortableTable
v-bind="$attrs"
:headers="headers"
default-sort-by="age"
:rows="rows"
key-field="_key"
v-on="$listeners"
>
<template slot="cell:state" slot-scope="scope" class="state-col">
<div class="state">
<HarvesterVmState class="vmstate" :row="scope.row" :all-cluster-network="allClusterNetwork" />
</div>
</template>
</Sortabletable>
</div>
</div>
</template>
<style lang="scss" scoped>
#host-instances {
::v-deep thead th {
vertical-align: middle;
}
::v-deep .state {
display: flex;
.vmstate {
margin-right: 6px;
}
}
}
</style>
@@ -0,0 +1,71 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import { _CREATE } from '@shell/config/query-params';
export default {
name: 'LinkStatus',
components: { LabelValue },
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
default: _CREATE,
},
},
};
</script>
<template>
<div>
<div class="row mt-10">
<div class="col span-6">
<LabelValue
:name="t('generic.name')"
:value="value.name"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('tableHeaders.state')"
:value="value.state"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabelValue
:name="t('tableHeaders.type')"
:value="value.type"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.fields.macAddress')"
:value="value.mac"
/>
</div>
</div>
<div class="row mt-10">
<div
v-if="value.promiscuous === 'true'"
class="col span-6"
>
<LabelValue
:name="t('harvester.fields.promiscuous')"
:value="value.promiscuous"
>
<template #value>
{{ value.promiscuous === 'true' ? t('generic.yes') : t('generic.no') }}
</template>
</LabelValue>
</div>
</div>
</div>
</template>
@@ -0,0 +1,104 @@
<script>
import LabelValue from '@shell/components/LabelValue';
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import { BadgeState } from '@components/BadgeState';
import { _CREATE } from '@shell/config/query-params';
import { findBy } from '@shell/utils/array';
import LinkStatus from './LinkStatus';
import { HCI } from '../../../types';
export default {
name: 'HarvesterHostNetwork',
components: {
LabelValue,
LinkStatus,
ArrayListGrouped,
BadgeState,
},
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
default: _CREATE,
},
},
computed: {
conditions() {
return this.value?.status?.conditions || [];
},
readyCondition() {
return findBy(this.conditions, 'type', 'ready') || {};
},
linkStatus() {
const linkMonitorId = this.value?.status?.linkMonitor;
const nodeName = this.value?.status?.node;
const inStore = this.$store.getters['currentProduct'].inStore;
const linkMonitors = this.$store.getters[`${ inStore }/all`](HCI.LINK_MONITOR);
const linkMonitor = (linkMonitors.filter(l => l.id === linkMonitorId) || [])[0] || {};
return linkMonitor?.status?.linkStatus?.[nodeName] || [];
},
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<div class="pull-right">
{{ t('resourceTabs.conditions.tab') }}:
<BadgeState
v-clean-tooltip="readyCondition.message"
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
:label="t('tableHeaders.ready')"
class="mr-10 ml-10 state"
/>
</div>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabelValue
:name="t('harvester.network.clusterNetwork.label')"
:value="value.status.clusterNetwork"
/>
</div>
<div class="col span-6">
<LabelValue
:name="t('harvester.vlanStatus.vlanConfig.label')"
:value="value.status.vlanConfig"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<ArrayListGrouped
v-model="linkStatus"
:mode="mode"
:can-remove="false"
>
<template #default="props">
<LinkStatus
:value="props.row.value"
:mode="mode"
/>
</template>
</ArrayListGrouped>
</div>
</div>
</div>
</template>
@@ -0,0 +1,488 @@
<script>
import { mapGetters } from 'vuex';
import Tag from '@shell/components/Tag';
import Tabbed from '@shell/components/Tabbed';
import Tab from '@shell/components/Tabbed/Tab';
import InfoBox from '@shell/components/InfoBox';
import LabelValue from '@shell/components/LabelValue';
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue';
import metricPoller from '@shell/mixins/metric-poller';
import {
METRIC, NODE, LONGHORN, POD, EVENT
} from '@shell/config/types';
import { HCI } from '../../types';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { allHash } from '@shell/utils/promise';
import { formatSi } from '@shell/utils/units';
import { findBy } from '@shell/utils/array';
import { clone } from '@shell/utils/object';
import { escapeHtml } from '@shell/utils/string';
import Basic from './HarvesterHostBasic';
import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system';
export default {
name: 'DetailHost',
components: {
Tabbed,
Tab,
Tag,
Basic,
Instance,
ArrayListGrouped,
Disk,
InfoBox,
VlanStatus,
LabelValue,
HarvesterKsmtuned,
Loading,
SortableTable,
HarvesterSeeder,
Banner,
},
mixins: [metricPoller],
props: {
value: {
type: Object,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
nodes: this.$store.dispatch('harvester/findAll', { type: NODE }),
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
};
if (this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS)) {
hash.hostNetworks = this.$store.dispatch('harvester/findAll', { type: HCI.VLAN_STATUS });
}
if (this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE)) {
hash.blockDevices = this.$store.dispatch('harvester/findAll', { type: HCI.BLOCK_DEVICE });
}
if (this.$store.getters['harvester/schemaFor'](LONGHORN.NODES)) {
hash.longhornNodes = this.$store.dispatch('harvester/findAll', { type: LONGHORN.NODES });
}
if (this.$store.getters['harvester/schemaFor'](HCI.LINK_MONITOR)) {
hash.linkMonitors = this.$store.dispatch('harvester/findAll', { type: HCI.LINK_MONITOR });
}
if (this.$store.getters['harvester/schemaFor'](HCI.ADD_ONS)) {
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS });
}
if (this.$store.getters['harvester/schemaFor'](HCI.INVENTORY)) {
hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
}
const res = await allHash(hash);
const hostNetworkResource = (res.hostNetworks || []).find( O => this.value.id === O.attachNodeName);
this.loadMetrics();
if (hostNetworkResource) {
this.hostNetworkResource = hostNetworkResource;
}
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
const provisionedBlockDevices = blockDevices.filter((d) => {
const provisioned = d?.spec?.fileSystem?.provisioned;
const isCurrentNode = d?.spec?.nodeName === this.value.id;
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
return provisioned && isCurrentNode && !isLonghornMounted;
})
.map((d) => {
return {
isNew: true,
name: d?.metadata?.name,
originPath: d?.spec?.fileSystem?.mountPoint,
path: d?.spec?.fileSystem?.mountPoint,
blockDevice: d,
displayName: d?.displayName,
forceFormatted: d?.spec?.fileSystem?.forceFormatted || false,
};
});
const disks = [...this.longhornDisks, ...provisionedBlockDevices];
this.disks = disks;
this.newDisks = clone(disks);
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
const seederEnabled = seeder ? seeder?.spec?.enabled : false;
if (seederEnabled) {
const inStore = this.$store.getters['currentProduct'].inStore;
const inventories = this.$store.getters[`${ inStore }/all`](HCI.INVENTORY) || [];
const inventory = inventories.find(inv => inv.id === `harvester-system/${ this.value.id }`);
if (inventory) {
this.inventory = inventory;
} else {
this.inventory = await this.$store.dispatch(`${ inStore }/create`, {
type: HCI.INVENTORY,
metadata: {
name: this.value.id,
namespace: 'harvester-system'
},
});
this.inventory.applyDefaults();
}
}
},
data() {
return {
metrics: null,
mode: 'view',
hostNetworkResource: null,
newDisks: [],
disks: [],
allEvents: [],
didLoadEvents: false,
inventory: {},
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
longhornDisks() {
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.value.id }`);
const diskStatus = longhornNode?.status?.diskStatus || {};
const diskSpec = longhornNode?.spec?.disks || {};
const formatOptions = {
increment: 1024,
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: 'iB',
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `longhorn-system/${ key }`);
return {
...diskStatus[key],
...diskSpec?.[key],
name: key,
isNew: false,
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
blockDevice,
displayName: blockDevice?.displayName || key,
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
tags: diskSpec?.[key]?.tags || [],
};
});
return longhornDisks;
},
hasKsmtunedSchema() {
const inStore = this.$store.getters['currentProduct'].inStore;
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
},
hasBlockDevicesSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
},
hasHostNetworksSchema() {
return !!this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS);
},
vlanStatuses() {
const inStore = this.$store.getters['currentProduct'].inStore;
const nodeId = this.value.id;
const vlanStatuses = this.$store.getters[`${ inStore }/all`](HCI.VLAN_STATUS);
return vlanStatuses.filter(s => s?.status?.node === nodeId) || [];
},
longhornNode() {
const inStore = this.$store.getters['currentProduct'].inStore;
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
return longhornNodes.find(node => node.id === `${ LONGHORN_SYSTEM }/${ this.value.id }`);
},
events() {
return this.allEvents.filter((event) => {
return event.involvedObject?.uid === this.value?.metadata?.uid &&
event.reason !== 'SeederUpdated';
}).map((event) => {
return {
reason: (`${ event.reason || this.t('generic.unknown') }${ event.count > 1 ? ` (${ event.count })` : '' }`).trim(),
message: event.message || this.t('generic.unknown'),
date: event.lastTimestamp || event.firstTimestamp || event.metadata.creationTimestamp,
eventType: event.eventType
};
});
},
eventHeaders() {
return [
{
name: 'reason',
label: this.t('tableHeaders.reason'),
value: 'reason',
sort: 'reason',
},
{
name: 'message',
label: this.t('tableHeaders.message'),
value: 'message',
sort: 'message',
},
{
name: 'date',
label: this.t('tableHeaders.updated'),
value: 'date',
sort: 'date:desc',
formatter: 'LiveDate',
formatterOpts: { addSuffix: true },
width: 125
},
];
},
seederEnabled() {
const inStore = this.$store.getters['currentProduct'].inStore;
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
return seeder ? seeder?.spec?.enabled : false;
},
ntpSync() {
const jsonString = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.NODE_NTP_SYNC_STATUS];
let out = null;
if (!jsonString) {
return out;
}
try {
out = JSON.parse(jsonString);
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
err,
}, { root: true });
}
return out;
},
ntpSyncedStatus() {
const status = this.ntpSync?.ntpSyncStatus;
if (status === 'disabled') {
return {
status: 'disabled',
warning: { key: 'harvester.host.ntp.ntpSyncStatus.isDisabled' }
};
}
const current = this.ntpSync?.currentNtpServers || '';
if (status === 'unsynced') {
return {
status: 'unsynced',
warning: {
key: 'harvester.host.ntp.ntpSyncStatus.isUnsynced',
current
}
};
}
return {};
},
},
methods: {
async loadMetrics() {
const schema = this.$store.getters['harvester/schemaFor'](METRIC.NODE);
if (schema) {
this.metrics = await this.$store.dispatch('harvester/find', {
type: METRIC.NODE,
id: this.value.id,
opt: { force: true, watch: false }
});
this.$forceUpdate();
}
},
// Ensures we only fetch events and show the table when the events tab has been activated
tabChange(neu) {
if (!this.didLoadEvents && neu?.selectedName === 'events') {
this.$store.dispatch(`harvester/findAll`, { type: EVENT }).then((events) => {
this.allEvents = events;
this.didLoadEvents = true;
});
}
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Banner
v-if="ntpSyncedStatus.status === 'disabled'"
color="warning"
>
<span v-clean-html="t(ntpSyncedStatus.warning.key)"></span>
</Banner>
<Banner
v-if="ntpSyncedStatus.status === 'unsynced'"
color="warning"
>
<span v-clean-html="t(ntpSyncedStatus.warning.key, { current: ntpSyncedStatus.warning.current }, true)"></span>
</Banner>
<Tabbed
v-bind="$attrs"
class="mt-15"
:side-tabs="true"
@changed="tabChange"
>
<Tab name="basics" :label="t('harvester.host.tabs.basics')" :weight="4" class="bordered-table">
<Basic
v-model="value"
:metrics="metrics"
:mode="mode"
/>
</Tab>
<Tab name="instance" :label="t('harvester.host.tabs.instance')" :weight="3" class="bordered-table">
<Instance :node="value" />
</Tab>
<Tab
v-if="hasHostNetworksSchema && vlanStatuses.length > 0"
name="network"
:label="t('harvester.host.tabs.network')"
:weight="2"
class="bordered-table"
>
<InfoBox
v-for="vlan in vlanStatuses"
:key="vlan.id"
>
<VlanStatus
:value="vlan"
:mode="mode"
/>
</InfoBox>
</Tab>
<Tab
v-if="hasBlockDevicesSchema"
name="disk"
:weight="1"
:label="t('harvester.host.tabs.storage')"
>
<div
v-if="longhornNode"
class="row mb-20"
>
<div class="col span-12">
<LabelValue
v-if="longhornNode.spec.tags.length"
:name="t('harvester.host.tags.label')"
>
<template #value>
<div class="mt-5">
<Tag v-for="(prop, key) in longhornNode.spec.tags" :key="key + prop" class="mr-5">
{{ prop }}
</Tag>
</div>
</template>
</LabelValue>
</div>
</div>
<ArrayListGrouped
v-model="newDisks"
:mode="mode"
:can-remove="false"
:initial-empty-row="false"
>
<template #default="props">
<Disk
v-model="props.row.value"
class="mb-20"
:mode="mode"
:disks="disks"
/>
</template>
</ArrayListGrouped>
</Tab>
<Tab
v-if="hasKsmtunedSchema"
name="ksmtuned"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.ksmtuned')"
>
<HarvesterKsmtuned :mode="mode" :node="value" />
</Tab>
<Tab
v-if="seederEnabled"
name="seeder"
:weight="-1"
:label="t('harvester.host.tabs.seeder')"
>
<HarvesterSeeder
:mode="mode"
:node="value"
:inventory="inventory"
/>
</Tab>
<Tab
label-key="harvester.virtualMachine.detail.tabs.events"
name="events"
:weight="-99"
>
<SortableTable
:rows="events"
:headers="eventHeaders"
key-field="id"
:search="false"
:table-actions="false"
:row-actions="false"
default-sort-by="date"
/>
</Tab>
</Tabbed>
</div>
</template>