mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<script>
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import { BadgeState } from '@components/BadgeState';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { HCI } from '../../types';
|
||||
import { LONGHORN_SYSTEM } from './index';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabelValue,
|
||||
BadgeState,
|
||||
Banner,
|
||||
RadioGroup,
|
||||
RadioButton,
|
||||
Tags,
|
||||
},
|
||||
|
||||
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,
|
||||
}];
|
||||
},
|
||||
|
||||
mountedMessage() {
|
||||
const state = this.blockDevice?.metadata?.state || {};
|
||||
|
||||
if (state?.error) {
|
||||
return state?.message;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
isProvisioned() {
|
||||
return this.blockDevice?.spec.fileSystem.provisioned;
|
||||
},
|
||||
|
||||
forceFormattedDisabled() {
|
||||
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
||||
|
||||
const systems = ['ext4', 'XFS'];
|
||||
|
||||
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
||||
return true;
|
||||
} else if (systems.includes(fileSystem)) {
|
||||
return false;
|
||||
} else if (!fileSystem) {
|
||||
return true;
|
||||
} else {
|
||||
return !this.canEditPath;
|
||||
}
|
||||
},
|
||||
|
||||
canEditPath() {
|
||||
if (this.mountedMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.value.isNew && !this.value.originPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
isFormatted() {
|
||||
return !!this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||
},
|
||||
|
||||
formattedBannerLabel() {
|
||||
const system = this.blockDevice?.status?.deviceStatus?.fileSystem?.type;
|
||||
|
||||
const label = this.t('harvester.host.disk.lastFormattedAt.info');
|
||||
|
||||
if (system) {
|
||||
return `${ label } ${ this.t('harvester.host.disk.fileSystem.info', { system }) }`;
|
||||
} else {
|
||||
return label;
|
||||
}
|
||||
},
|
||||
|
||||
provisionPhase() {
|
||||
return this.blockDevice?.provisionPhase || {};
|
||||
},
|
||||
|
||||
blockDevice() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const name = this.value?.name;
|
||||
|
||||
return this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ name }`) || {};
|
||||
},
|
||||
|
||||
isCorrupted() {
|
||||
return this.blockDevice?.status?.deviceStatus?.fileSystem?.corrupted;
|
||||
},
|
||||
|
||||
isFormatting() {
|
||||
return this.blockDevice.isFormatting;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.$emit('input', this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="disk" @input="update">
|
||||
<div class="mt-10" />
|
||||
<Banner
|
||||
v-if="mountedMessage && isProvisioned"
|
||||
color="error"
|
||||
:label="mountedMessage"
|
||||
/>
|
||||
<Banner
|
||||
v-if="isFormatting"
|
||||
color="info"
|
||||
:label="t('harvester.host.disk.fileSystem.formatting')"
|
||||
/>
|
||||
<Banner
|
||||
v-else-if="isFormatted && !isCorrupted"
|
||||
color="info"
|
||||
:label="formattedBannerLabel"
|
||||
/>
|
||||
<div v-if="!value.isNew">
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<Tags
|
||||
v-model="value.tags"
|
||||
:label="t('harvester.host.disk.tags.label')"
|
||||
:add-label="t('harvester.host.disk.tags.addLabel')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</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 flex span-12">
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.storageAvailable.label')"
|
||||
:value="value.storageAvailable"
|
||||
/>
|
||||
<LabelValue
|
||||
:name="t('harvester.host.disk.storageScheduled.label')"
|
||||
:value="value.storageScheduled"
|
||||
/>
|
||||
<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-12">
|
||||
<LabeledInput
|
||||
v-model="value.displayName"
|
||||
:label="t('generic.name')"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="(value.isNew && !isFormatted) || isCorrupted" class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.forceFormatted"
|
||||
:mode="mode"
|
||||
name="forceFormatted"
|
||||
label-key="harvester.host.disk.forceFormatted.label"
|
||||
:labels="[t('generic.no'),t('harvester.host.disk.forceFormatted.yes')]"
|
||||
:options="[false, true]"
|
||||
:disabled="forceFormattedDisabled"
|
||||
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
||||
>
|
||||
<template #1="{option, listeners}">
|
||||
<RadioButton
|
||||
:label="option.label"
|
||||
:val="option.value"
|
||||
:value="value.forceFormatted"
|
||||
:disabled="forceFormattedDisabled && !value.forceFormatted"
|
||||
v-on="listeners"
|
||||
/>
|
||||
</template>
|
||||
</RadioGroup>
|
||||
</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,216 @@
|
||||
<script>
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { HCI } from '../../types';
|
||||
|
||||
export const ksmtunedMode = [{
|
||||
value: 'standard',
|
||||
label: 'Standard'
|
||||
}, {
|
||||
value: 'high',
|
||||
label: 'High-Perfomanace'
|
||||
}, {
|
||||
value: 'customized',
|
||||
label: 'Customized'
|
||||
}];
|
||||
|
||||
export const ksmtunedRunOption = [{
|
||||
label: 'Run',
|
||||
value: 'run'
|
||||
}, {
|
||||
label: 'Stop',
|
||||
value: 'stop'
|
||||
}, {
|
||||
label: 'Prune',
|
||||
value: 'prune'
|
||||
}];
|
||||
|
||||
export default {
|
||||
name: 'HarvesterKsmtuned',
|
||||
components: {
|
||||
Checkbox, LabeledInput, LabeledSelect, RadioGroup, UnitInput
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
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;
|
||||
});
|
||||
|
||||
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
||||
this.spec = this.ksmtuned.spec;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ksmtuned: {},
|
||||
spec: {},
|
||||
thresCoef: 30,
|
||||
ksmtunedMode,
|
||||
ksmtunedRunOption,
|
||||
enableMergeAcrossNodes: true
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.saveKsmtuned, 'saveKsmtuned');
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCustomizedMode() {
|
||||
return this.spec.mode === 'customized';
|
||||
},
|
||||
|
||||
showKsmt() {
|
||||
return this.spec.run === 'run';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveKsmtuned() {
|
||||
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
||||
this.$set(this.ksmtuned, 'spec', this.spec);
|
||||
|
||||
await this.ksmtuned.save().catch((reason) => {
|
||||
if (reason?.type === 'error') {
|
||||
this.$store.dispatch('growl/error', {
|
||||
title: this.t('harvester.notification.title.error'),
|
||||
message: reason?.message
|
||||
}, { root: true });
|
||||
|
||||
return Promise.reject(new Error('saveKsmtuned error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LabeledSelect
|
||||
v-model="spec.run"
|
||||
:label="t('harvester.host.ksmtuned.run')"
|
||||
:options="ksmtunedRunOption"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
|
||||
<template v-if="showKsmt">
|
||||
<UnitInput
|
||||
v-model="spec.thresCoef"
|
||||
v-int-number
|
||||
:label="t('harvester.host.ksmtuned.thresCoef')"
|
||||
suffix="%"
|
||||
:delay="0"
|
||||
required
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
/>
|
||||
|
||||
<Checkbox v-model="enableMergeAcrossNodes" :mode="mode" class="check mb-20" type="checkbox" :label="t('harvester.host.ksmtuned.enableMergeNodes')" />
|
||||
|
||||
<h3>
|
||||
<t k="harvester.host.ksmtuned.modeLink" :raw="true" />
|
||||
</h3>
|
||||
<RadioGroup
|
||||
v-model="spec.mode"
|
||||
class="mb-20"
|
||||
:name="t('harvester.host.ksmtuned.mode')"
|
||||
:options="ksmtunedMode"
|
||||
/>
|
||||
|
||||
<template v-if="isCustomizedMode">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="spec.ksmtunedParameters.boost"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.host.ksmtuned.parameters.boost')"
|
||||
:tooltip="t('harvester.host.ksmtuned.parameters.description.boost')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="spec.ksmtunedParameters.decay"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.host.ksmtuned.parameters.decay')"
|
||||
:tooltip="t('harvester.host.ksmtuned.parameters.description.decay')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="spec.ksmtunedParameters.minPages"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.host.ksmtuned.parameters.minPages')"
|
||||
:tooltip="t('harvester.host.ksmtuned.parameters.description.minPages')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="spec.ksmtunedParameters.maxPages"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.host.ksmtuned.parameters.maxPages')"
|
||||
:tooltip="t('harvester.host.ksmtuned.parameters.description.maxPages')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="spec.ksmtunedParameters.sleepMsec"
|
||||
required
|
||||
type="number"
|
||||
:label="t('harvester.host.ksmtuned.parameters.sleepMsec')"
|
||||
:tooltip="t('harvester.host.ksmtuned.parameters.description.sleepMsec')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,370 @@
|
||||
<script>
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
import { base64Encode, base64Decode } from '@shell/utils/crypto';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
const _NEW = '_NEW';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterSeeder',
|
||||
|
||||
components: {
|
||||
Checkbox,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
ModalWithCard,
|
||||
NameNsDescription,
|
||||
Banner,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
registerAfterHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
inventory: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const enableInventory = !!this.inventory?.id;
|
||||
|
||||
return {
|
||||
enableInventory,
|
||||
value: this.inventory,
|
||||
secret: {},
|
||||
errors: [],
|
||||
newSecretSelected: false,
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerAfterHook(this.saveInventory, 'saveInventory');
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
this.secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
type: SECRET,
|
||||
data: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
metadata: {
|
||||
namespace: '',
|
||||
name: '',
|
||||
describe: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
secretOption() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const out = this.$store.getters[`${ inStore }/all`](SECRET).filter((s) => {
|
||||
return s.data?.username && s.data?.password;
|
||||
}).map( (s) => {
|
||||
return {
|
||||
label: s.id,
|
||||
value: s.id
|
||||
};
|
||||
});
|
||||
|
||||
// if (!(this.disableCreate || this.mode === _VIEW) && this.isCreatable) {
|
||||
out.unshift({
|
||||
label: this.t('harvester.virtualMachine.createSSHKey'),
|
||||
value: _NEW,
|
||||
});
|
||||
// }
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
selectedSecret: {
|
||||
get() {
|
||||
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 '';
|
||||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if (value === _NEW) {
|
||||
this.newSecretSelected = true;
|
||||
} else {
|
||||
const [namespace, name] = value.split('/');
|
||||
|
||||
this.$set(this.value.spec.baseboardSpec.connection.authSecretRef, 'namespace', namespace);
|
||||
this.$set(this.value.spec.baseboardSpec.connection.authSecretRef, 'name', name);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
username: {
|
||||
get() {
|
||||
return base64Decode(this.secret?.data?.username);
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.$set(this.secret.data, 'username', base64Encode(value));
|
||||
}
|
||||
},
|
||||
|
||||
password: {
|
||||
get() {
|
||||
return base64Decode(this.secret?.data?.password);
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.$set(this.secret.data, 'password', base64Encode(value));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveInventory() {
|
||||
if (this.enableInventory) {
|
||||
const errors = [];
|
||||
|
||||
if (!this.value.spec.baseboardSpec.connection.host) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.host.label') }, true));
|
||||
}
|
||||
|
||||
if (!this.value.spec.baseboardSpec.connection.port) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.port.label') }, true));
|
||||
}
|
||||
|
||||
if (!this.selectedSecret) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.secret.label') }, true));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(exceptionToErrorsArray(errors));
|
||||
}
|
||||
|
||||
if (!this.value.id) {
|
||||
this.value.metadata.annotations['metal.harvesterhci.io/local-node-name'] = this.node.id;
|
||||
}
|
||||
|
||||
this.value.metadata.annotations['metal.harvesterhci.io/local-inventory'] = 'true';
|
||||
|
||||
return await this.value.save();
|
||||
} else if (this.value.id) {
|
||||
return await this.value.remove();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
|
||||
show() {
|
||||
this.isOpen = true;
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.isOpen = false;
|
||||
this.newSecretSelected = false;
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
async saveSecret(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
if (!this.username) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.username') }, true));
|
||||
}
|
||||
|
||||
if (!this.password) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.password') }, true));
|
||||
}
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
buttonCb(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.secret.save();
|
||||
|
||||
if (res.id) {
|
||||
this.secretOption.push({
|
||||
label: res.id,
|
||||
value: res.id
|
||||
});
|
||||
}
|
||||
|
||||
this.selectedSecret = res.id;
|
||||
|
||||
buttonCb(true);
|
||||
this.cancel();
|
||||
} catch (err) {
|
||||
this.errors = [err.message];
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
newSecretSelected(val) {
|
||||
if (val) {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</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 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 v-if="enableInventory">
|
||||
<div class="row mt-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="value.spec.baseboardSpec.connection.host"
|
||||
:label="t('harvester.seeder.inventory.host.label')"
|
||||
:placeholder="t('harvester.seeder.inventory.host.placeholder')"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
<Checkbox
|
||||
v-model="value.spec.baseboardSpec.connection.insecureTLS"
|
||||
class="mt-5"
|
||||
:mode="mode"
|
||||
:label="t('harvester.seeder.inventory.insecureTLS.label')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model.number="value.spec.baseboardSpec.connection.port"
|
||||
:label="t('harvester.seeder.inventory.port.label')"
|
||||
:placeholder="t('harvester.seeder.inventory.port.placeholder')"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="selectedSecret"
|
||||
:label="t('harvester.seeder.inventory.secret.label')"
|
||||
:mode="mode"
|
||||
:options="secretOption"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<RadioGroup
|
||||
v-model="value.spec.events.enabled"
|
||||
name="enabled"
|
||||
:options="[true, false]"
|
||||
:label="t('harvester.seeder.inventory.event.label')"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.spec.events.enabled"
|
||||
class="col span-6"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model="value.spec.events.pollingInterval"
|
||||
:label="t('harvester.seeder.inventory.pollingInterval.label')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalWithCard
|
||||
v-if="isOpen"
|
||||
width="80%"
|
||||
:errors="errors"
|
||||
name="secretModal"
|
||||
@finish="saveSecret"
|
||||
@close="cancel"
|
||||
>
|
||||
<template #title>
|
||||
{{ t('harvester.seeder.inventory.secret.create.title') }}
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<NameNsDescription
|
||||
v-model="secret"
|
||||
:namespaced="true"
|
||||
mode="create"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="username"
|
||||
:label="t('harvester.virtualMachine.input.username')"
|
||||
class="mb-20"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="password"
|
||||
type="password"
|
||||
:label="t('harvester.virtualMachine.input.password')"
|
||||
class="mb-20"
|
||||
required
|
||||
/>
|
||||
</template>
|
||||
</ModalWithCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,688 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import Footer from '@shell/components/form/Footer';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||
import ButtonDropdown from '@shell/components/ButtonDropdown';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { LONGHORN, SECRET } from '@shell/config/types';
|
||||
import { HCI } from '../../types';
|
||||
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 { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import KeyValue from '@shell/components/form/KeyValue';
|
||||
import Loading from '@shell/components/Loading.vue';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { Banner } from '@components/Banner';
|
||||
import HarvesterDisk from './HarvesterDisk';
|
||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||
import HarvesterSeeder from './HarvesterSeeder';
|
||||
import Tags from '../../components/DiskTags';
|
||||
|
||||
export const LONGHORN_SYSTEM = 'longhorn-system';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditNode',
|
||||
components: {
|
||||
Footer,
|
||||
Tabbed,
|
||||
Tab,
|
||||
LabeledInput,
|
||||
NameNsDescription,
|
||||
ArrayListGrouped,
|
||||
HarvesterDisk,
|
||||
HarvesterKsmtuned,
|
||||
ButtonDropdown,
|
||||
KeyValue,
|
||||
Banner,
|
||||
Tags,
|
||||
Loading,
|
||||
HarvesterSeeder,
|
||||
MessageLink,
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
blockDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BLOCK_DEVICE }),
|
||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||
};
|
||||
|
||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY)) {
|
||||
hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
|
||||
}
|
||||
|
||||
await allHash(hash);
|
||||
|
||||
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) => {
|
||||
const corrupted = d?.status?.deviceStatus?.fileSystem?.corrupted;
|
||||
|
||||
return {
|
||||
isNew: true,
|
||||
name: d?.metadata?.name,
|
||||
originPath: d?.spec?.fileSystem?.mountPoint,
|
||||
path: d?.spec?.fileSystem?.mountPoint,
|
||||
blockDevice: d,
|
||||
displayName: d?.displayName,
|
||||
forceFormatted: corrupted ? true : d?.spec?.fileSystem?.forceFormatted || false,
|
||||
};
|
||||
});
|
||||
|
||||
const disks = [...this.longhornDisks, ...provisionedBlockDevices];
|
||||
|
||||
this.disks = disks;
|
||||
this.newDisks = clone(disks);
|
||||
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||
|
||||
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||
const seeder = addons.find(addon => addon.id === `harvester-system/${ ADD_ONS.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 = await this.$store.dispatch(`${ inStore }/clone`, { resource: 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() {
|
||||
const customName = this.value.metadata?.annotations?.[HCI_LABELS_ANNOTATIONS.HOST_CUSTOM_NAME] || '';
|
||||
const consoleUrl = this.value.metadata?.annotations?.[HCI_LABELS_ANNOTATIONS.HOST_CONSOLE_URL] || '';
|
||||
|
||||
return {
|
||||
customName,
|
||||
consoleUrl,
|
||||
disks: [],
|
||||
newDisks: [],
|
||||
blockDevice: [],
|
||||
blockDeviceOpts: [],
|
||||
filteredLabels: clone(this.value.filteredSystemLabels),
|
||||
inventory: {},
|
||||
originValue: clone(this.value),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
removedDisks() {
|
||||
const out = this.disks.filter((d) => {
|
||||
return !findBy(this.newDisks, 'name', d.name);
|
||||
}) || [];
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
showFormattedWarning() {
|
||||
const out = this.newDisks.filter(d => d.forceFormatted && d.isNew) || [];
|
||||
|
||||
return out.length > 0;
|
||||
},
|
||||
|
||||
hasKsmtunedSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||
},
|
||||
|
||||
hasBlockDevicesSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.BLOCK_DEVICE);
|
||||
},
|
||||
|
||||
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 }`);
|
||||
},
|
||||
|
||||
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/${ ADD_ONS.HARVESTER_SEEDER }`);
|
||||
|
||||
return seeder ? seeder?.spec?.enabled : false;
|
||||
},
|
||||
|
||||
toEnableSeederAddon() {
|
||||
const { cluster } = this.$router?.currentRoute?.params || {};
|
||||
|
||||
return {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||
params: {
|
||||
resource: `${ HCI.ADD_ONS }`,
|
||||
namespace: 'harvester-system',
|
||||
cluster,
|
||||
id: `${ ADD_ONS.HARVESTER_SEEDER }`
|
||||
},
|
||||
query: { mode: _EDIT }
|
||||
};
|
||||
},
|
||||
|
||||
hasAddonSchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
||||
},
|
||||
|
||||
hasSeederAddon() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||
|
||||
return addons.find(addon => addon.id === `harvester-system/${ ADD_ONS.HARVESTER_SEEDER }`);
|
||||
},
|
||||
|
||||
hasInventorySchema() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
customName(neu) {
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.HOST_CUSTOM_NAME, neu);
|
||||
},
|
||||
|
||||
consoleUrl(neu) {
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.HOST_CONSOLE_URL, neu);
|
||||
},
|
||||
|
||||
newDisks() {
|
||||
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
|
||||
if (this.registerAfterHook) {
|
||||
this.registerAfterHook(this.saveDisk);
|
||||
this.registerAfterHook(this.saveLonghornNode);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addDisk(id) {
|
||||
const removedDisk = findBy(this.removedDisks, 'blockDevice.id', id);
|
||||
|
||||
if (removedDisk) {
|
||||
return this.newDisks.push(removedDisk);
|
||||
}
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const disk = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, id);
|
||||
const mountPoint = disk?.spec?.fileSystem?.mountPoint;
|
||||
const lastFormattedAt = disk?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||
|
||||
let forceFormatted = true;
|
||||
const systems = ['ext4', 'XFS'];
|
||||
|
||||
if (disk.childParts?.length > 0) {
|
||||
forceFormatted = true;
|
||||
} else if (lastFormattedAt) {
|
||||
forceFormatted = false;
|
||||
} else if (systems.includes(disk?.status?.deviceStatus?.fileSystem?.type)) {
|
||||
forceFormatted = false;
|
||||
}
|
||||
|
||||
const name = disk?.metadata?.name;
|
||||
|
||||
this.newDisks.push({
|
||||
name,
|
||||
path: mountPoint,
|
||||
allowScheduling: false,
|
||||
evictionRequested: false,
|
||||
storageReserved: 0,
|
||||
isNew: true,
|
||||
originPath: disk?.spec?.fileSystem?.mountPoint,
|
||||
blockDevice: disk,
|
||||
displayName: disk?.displayName,
|
||||
forceFormatted,
|
||||
});
|
||||
},
|
||||
|
||||
async saveDisk() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const addDisks = this.newDisks.filter(d => d.isNew);
|
||||
const removeDisks = this.disks.filter(d => !findBy(this.newDisks, 'name', d.name) && d.blockDevice);
|
||||
|
||||
if (addDisks.length === 0 && removeDisks.length === 0) {
|
||||
return Promise.resolve();
|
||||
} else if (addDisks.length !== 0 && removeDisks.length === 0) {
|
||||
const updatedDisks = addDisks.filter((d) => {
|
||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||
const { provisioned, forceFormatted } = blockDevice.spec.fileSystem;
|
||||
|
||||
if (provisioned && forceFormatted === d.forceFormatted) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (updatedDisks.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(addDisks.map((d) => {
|
||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||
|
||||
blockDevice.spec.fileSystem.provisioned = true;
|
||||
blockDevice.spec.fileSystem.forceFormatted = d.forceFormatted;
|
||||
|
||||
return blockDevice.save();
|
||||
}));
|
||||
|
||||
await Promise.all(removeDisks.map((d) => {
|
||||
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||
|
||||
blockDevice.spec.fileSystem.provisioned = false;
|
||||
|
||||
return blockDevice.save();
|
||||
}));
|
||||
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.host.disk.notification.success', { name: this.value.metadata?.name || '' }),
|
||||
}, { root: true });
|
||||
} catch (err) {
|
||||
return Promise.reject(exceptionToErrorsArray(err));
|
||||
}
|
||||
},
|
||||
|
||||
canRemove(row) {
|
||||
return !!row?.value?.blockDevice;
|
||||
},
|
||||
|
||||
onRemove(scope) {
|
||||
scope.remove();
|
||||
},
|
||||
|
||||
updateHostLabels(labels) {
|
||||
this.filteredLabels = labels;
|
||||
},
|
||||
|
||||
selectable(opt) {
|
||||
if ( opt.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
getBlockDeviceOpts() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||
|
||||
const out = blockDevices
|
||||
.filter((d) => {
|
||||
const addedToNodeCondition = findBy(d?.status?.conditions || [], 'type', 'AddedToNode');
|
||||
const isAdded = findBy(this.newDisks, 'name', d.metadata.name);
|
||||
const isRemoved = findBy(this.removedDisks, 'name', d.metadata.name);
|
||||
|
||||
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||
|
||||
if (deviceType !== 'disk' || d?.status?.state !== 'Active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((!findBy(this.disks || [], 'name', d.metadata.name) &&
|
||||
d?.spec?.nodeName === this.value.id &&
|
||||
(!addedToNodeCondition || addedToNodeCondition?.status === 'False') &&
|
||||
!d.spec?.fileSystem?.provisioned &&
|
||||
!isAdded) ||
|
||||
isRemoved
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((d) => {
|
||||
const devPath = d.spec?.devPath;
|
||||
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
|
||||
const size = formatSi(sizeBytes, { increment: 1024 });
|
||||
const parentDevice = d.status?.deviceStatus?.parentDevice;
|
||||
const isChildAdded = this.newDisks.find(newDisk => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
|
||||
const name = d.displayName;
|
||||
|
||||
let label = `${ name } (Type: ${ deviceType }, Size: ${ size })`;
|
||||
|
||||
if (parentDevice) {
|
||||
label = `- ${ label }`;
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
value: d.id,
|
||||
action: this.addDisk,
|
||||
kind: !parentDevice ? 'group' : '',
|
||||
disabled: !!isChildAdded,
|
||||
group: parentDevice || devPath,
|
||||
isParent: !!parentDevice,
|
||||
};
|
||||
});
|
||||
|
||||
return sortBy(out, ['group', 'isParent', 'label']);
|
||||
},
|
||||
|
||||
ddButtonAction() {
|
||||
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||
},
|
||||
|
||||
willSave() {
|
||||
const filteredLabels = this.filteredLabels || {};
|
||||
|
||||
this.value.metadata.labels = {
|
||||
...this.value.metadata.labels,
|
||||
...filteredLabels,
|
||||
};
|
||||
|
||||
const originLabels = this.value.filteredSystemLabels;
|
||||
|
||||
Object.keys(originLabels).map((key) => {
|
||||
if (!filteredLabels[key]) {
|
||||
delete this.value.metadata.labels[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async saveLonghornNode() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const disks = this.longhornNode?.spec?.disks || {};
|
||||
|
||||
this.newDisks.map((disk) => {
|
||||
(disks[disk.name] || {}).tags = disk.tags;
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
const retrySave = async() => {
|
||||
try {
|
||||
await this.longhornNode.save();
|
||||
} catch (err) {
|
||||
if ((err.status === 409 || err.status === 403) && count < 3) {
|
||||
count++;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/find`, {
|
||||
type: LONGHORN.NODES,
|
||||
id: this.longhornNode.id,
|
||||
opt: { force: true },
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, '5000'));
|
||||
await retrySave();
|
||||
} else {
|
||||
return Promise.reject(exceptionToErrorsArray(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await retrySave();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else id="node">
|
||||
<div class="content">
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:namespaced="false"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Tabbed ref="tabbed" class="mt-15" :side-tabs="true">
|
||||
<Tab name="basics" :weight="100" :label="t('harvester.host.tabs.basics')">
|
||||
<LabeledInput
|
||||
v-model="customName"
|
||||
:label="t('harvester.host.detail.customName')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model="consoleUrl"
|
||||
:label="t('harvester.host.detail.consoleUrl')"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasBlockDevicesSchema"
|
||||
name="disk"
|
||||
:weight="80"
|
||||
:label="t('harvester.host.tabs.storage')"
|
||||
>
|
||||
<div
|
||||
v-if="longhornNode"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<Tags
|
||||
v-model="longhornNode.spec.tags"
|
||||
:label="t('harvester.host.tags.label')"
|
||||
:add-label="t('harvester.host.tags.addLabel')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ArrayListGrouped
|
||||
v-model="newDisks"
|
||||
:mode="mode"
|
||||
:initial-empty-row="false"
|
||||
>
|
||||
<template #default="props">
|
||||
<HarvesterDisk
|
||||
v-model="props.row.value"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:disks="disks"
|
||||
/>
|
||||
</template>
|
||||
<template #add>
|
||||
<ButtonDropdown
|
||||
v-if="!isView"
|
||||
:button-label="t('harvester.host.disk.add')"
|
||||
:dropdown-options="blockDeviceOpts"
|
||||
size="sm"
|
||||
:selectable="selectable"
|
||||
@click-action="e=>addDisk(e.value)"
|
||||
@dd-button-action="ddButtonAction"
|
||||
>
|
||||
<template #option="option">
|
||||
<template v-if="option.kind === 'group'">
|
||||
<b>
|
||||
{{ option.label }}
|
||||
</b>
|
||||
</template>
|
||||
<div v-else>
|
||||
{{ option.label }}
|
||||
</div>
|
||||
</template>
|
||||
</ButtonDropdown>
|
||||
</template>
|
||||
<template #remove-button="scope">
|
||||
<button
|
||||
v-if="canRemove(scope.row, scope.i) && !isView"
|
||||
type="button"
|
||||
class="btn role-link close btn-sm"
|
||||
@click="() => onRemove(scope)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
<span v-else />
|
||||
</template>
|
||||
</ArrayListGrouped>
|
||||
</Tab>
|
||||
<Tab v-if="hasKsmtunedSchema" name="Ksmtuned" :weight="70" :label="t('harvester.host.tabs.ksmtuned')">
|
||||
<HarvesterKsmtuned :mode="mode" :node="value" :register-before-hook="registerBeforeHook" />
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="hasAddonSchema"
|
||||
name="seeder"
|
||||
:weight="60"
|
||||
:label="t('harvester.host.tabs.seeder')"
|
||||
>
|
||||
<HarvesterSeeder
|
||||
v-if="seederEnabled && hasInventorySchema"
|
||||
:mode="mode"
|
||||
:node="value"
|
||||
:register-after-hook="registerAfterHook"
|
||||
:inventory="inventory"
|
||||
/>
|
||||
<div v-else-if="seederEnabled && !hasInventorySchema">
|
||||
<Banner
|
||||
color="info"
|
||||
:label="t('harvester.seeder.banner.noInventory')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Banner
|
||||
v-if="hasSeederAddon"
|
||||
color="info"
|
||||
>
|
||||
<MessageLink
|
||||
:to="toEnableSeederAddon"
|
||||
prefix-label="harvester.seeder.banner.enable.prefix"
|
||||
middle-label="harvester.seeder.banner.enable.middle"
|
||||
suffix-label="harvester.seeder.banner.enable.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
<Banner
|
||||
v-else
|
||||
color="warning"
|
||||
:label="t('harvester.seeder.banner.noAddon')"
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab name="labels" label-key="harvester.host.tabs.labels">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
:value="filteredLabels"
|
||||
:add-label="t('labels.addLabel')"
|
||||
:mode="mode"
|
||||
:title="t('labels.labels.title')"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@input="updateHostLabels"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
<Banner
|
||||
v-if="showFormattedWarning"
|
||||
color="warning"
|
||||
:label="t('harvester.host.disk.forceFormatted.toolTip')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer class="footer" :mode="mode" :errors="errors" @save="save" @done="done" />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
#node {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
flex-grow: 1
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
position: relative;
|
||||
}
|
||||
.nicOption {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user