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,36 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
import { TextAreaAutoGrow } from '@components/Form/TextArea';
export default {
name: 'HarvesterAdditionalCA',
components: { FileSelector, TextAreaAutoGrow },
mixins: [CreateEditView],
methods: { onKeySelected: createOnSelected('value.value') },
};
</script>
<template>
<div>
<div class="row mb-20">
<div class="col span-12">
<FileSelector
class="btn btn-sm bg-primary mb-10"
:label="t('generic.readFromFile')"
@selected="onKeySelected"
/>
<div>
<TextAreaAutoGrow
v-model="value.value"
:min-height="254"
/>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,85 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
export default {
name: 'HarvesterAutoRotateRKE2Certs',
components: { RadioGroup, UnitInput },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return { parseDefaultValue };
},
created() {
this.update();
},
methods: {
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
}
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div class="row">
<div class="col span-12">
<RadioGroup
v-model="parseDefaultValue.enable"
class="mb-20"
name="model"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@input="update"
/>
<UnitInput
v-if="parseDefaultValue.enable"
v-model="parseDefaultValue.expiringInHours"
v-int-number
class="mb-20"
:min="1"
:max="8759"
:required="true"
:suffix="parseDefaultValue.expiringInHours === 1 ? 'Hour' : 'Hours'"
:label="t('harvester.setting.autoRotateRKE2Certs.expiringInHours')"
:mode="mode"
@input="update"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep .radio-group {
display: flex;
.radio-container {
margin-right: 30px;
}
}
</style>
@@ -0,0 +1,199 @@
<script>
import Tip from '@shell/components/Tip';
import { HCI_SETTING } from '../../config/settings';
import Password from '@shell/components/form/Password';
import MessageLink from '@shell/components/MessageLink';
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterEditBackupTarget',
components: {
LabeledInput, LabeledSelect, Tip, Password, MessageLink
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = { type: '', endpoint: '' };
}
if (!parseDefaultValue.type) {
parseDefaultValue.type = 's3';
}
return {
parseDefaultValue,
errors: []
};
},
computed: {
typeOption() {
return [{
value: 'nfs',
label: 'NFS'
}, {
value: 's3',
label: 'S3'
}];
},
virtualHostedStyleType() {
return [{
value: true,
label: 'True'
}, {
value: false,
label: 'False'
}];
},
isS3() {
return this.parseDefaultValue.type === 's3';
},
endpointPlaceholder() {
return this.isS3 ? '' : 'nfs://server:/path/';
},
toCA() {
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
}
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = { type: '', endpoint: '' };
}
this.$set(this, 'parseDefaultValue', parseDefaultValue);
this.update();
},
deep: true
}
},
created() {
this.update();
},
methods: {
update() {
if (!this.isS3) {
delete this.parseDefaultValue.accessKeyId;
delete this.parseDefaultValue.secretAccessKey;
delete this.parseDefaultValue.bucketName;
delete this.parseDefaultValue.bucketRegion;
delete this.parseDefaultValue.virtualHostedStyle;
delete this.parseDefaultValue.cert;
}
const value = JSON.stringify(this.parseDefaultValue);
if (!this.parseDefaultValue.type) {
this.$delete(this.value, 'value');
} else {
this.$set(this.value, 'value', value);
}
},
useDefault() {
const parseDefaultValue = { type: '', endpoint: '' };
this.$set(this, 'parseDefaultValue', parseDefaultValue);
}
}
};
</script>
<template>
<div class="row" @input="update">
<div class="col span-12">
<LabeledSelect v-model="parseDefaultValue.type" class="mb-20" :label="t('harvester.fields.type')" :options="typeOption" @input="update" />
<LabeledInput v-model="parseDefaultValue.endpoint" class="mb-5" :placeholder="endpointPlaceholder" :mode="mode" label="Endpoint" />
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.backup.backupTargetTip')" />
<template v-if="isS3">
<LabeledInput
v-model="parseDefaultValue.bucketName"
class="mb-20"
:mode="mode"
label="Bucket Name"
required
/>
<LabeledInput
v-model="parseDefaultValue.bucketRegion"
class="mb-20"
:mode="mode"
label="Bucket Region"
required
/>
<LabeledInput
v-model="parseDefaultValue.accessKeyId"
:placeholder="t('harvester.setting.placeholder.accessKeyId')"
class="mb-20"
:mode="mode"
label="Access Key ID"
required
/>
<Password
v-model="parseDefaultValue.secretAccessKey"
class="mb-20"
:mode="mode"
:placeholder="t('harvester.setting.placeholder.secretAccessKey')"
label="Secret Access Key"
required
/>
<LabeledSelect v-model="parseDefaultValue.virtualHostedStyle" class="mb-20" label="Virtual Hosted-Style" :options="virtualHostedStyleType" @input="update" />
<div class="mb-20">
<Tip icon="icon icon-info">
<MessageLink
:to="toCA"
target="_blank"
prefix-label="harvester.setting.message.ca.prefix"
middle-label="harvester.setting.message.ca.middle"
suffix-label="harvester.setting.message.ca.suffix"
/>
</Tip>
</div>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
p {
display: flex;
align-items: center;
}
.icon-h-question {
font-size: 24px;
}
.tip {
font-size: 15px;
}
.goCA {
margin: 0 3px;
}
</style>
@@ -0,0 +1,377 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { _EDIT } from '@shell/config/query-params';
import { randomStr } from '@shell/utils/string';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import KeyValue from '@shell/components/form/KeyValue';
import InfoBox from '@shell/components/InfoBox';
import { clone } from '@shell/utils/object';
export default {
name: 'HarvesterContainerdRegistry',
components: {
InfoBox,
KeyValue,
LabeledInput,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
registerBeforeHook: {
type: Function,
required: true,
},
},
data() {
const originMirror = {
Endpoints: [],
Rewrites: {}
};
const originConfig = {
Auth: {
Username: '',
Password: '',
Auth: '',
IdentityToken: ''
},
TLS: { InsecureSkipVerify: false }
};
let originValue = {};
const baseValue = {
Mirrors: { '': clone(originMirror) },
Configs: {}
};
try {
originValue = JSON.parse(this.value.value);
} catch (error) {
originValue = baseValue;
}
if (!Object.keys(originValue).length) {
originValue = baseValue;
}
const _mirrors = originValue.Mirrors || {};
const _configs = originValue.Configs || {};
const mirrorsKeys = Object.keys(_mirrors);
const configsKeys = Object.keys(_configs);
const mirrors = mirrorsKeys.map((key) => {
return {
key,
value: originValue.Mirrors[key],
idx: randomStr(5).toLowerCase()
};
});
const configs = configsKeys.map((key) => {
if (!originValue.Configs[key]?.Auth) {
originValue.Configs[key].Auth = originConfig.Auth;
}
return {
key,
value: originValue.Configs[key],
idx: randomStr(5).toLowerCase()
};
});
return {
mirrors,
configs,
originMirror,
originConfig,
mirrorsKeys,
configsKeys,
errors: [],
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
insecureSkipVerifyOption() {
return [{
label: 'True',
value: true
}, {
label: 'False',
value: false
}];
},
},
methods: {
willSave() {
const errors = [];
if (this.value.value) {
try {
JSON.parse(this.value.value);
this.mirrors.forEach((mirror) => {
if (!mirror.key) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.mirrors.registryName') }, true));
}
if (mirror.value.Endpoints.length === 0) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.mirrors.endpoints') }, true));
}
});
this.configs.forEach((config) => {
if (!config.key) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.configs.registryEDQNorIP') }, true));
}
});
} catch (e) {
}
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
update() {
const _mirrors = {};
const _configs = {};
this.mirrors.forEach((mirror) => {
_mirrors[mirror.key] = mirror.value;
});
this.configs.forEach((config) => {
_configs[config.key] = config.value;
});
const out = {
Mirrors: _mirrors,
Configs: _configs
};
if (!Object.keys(_mirrors).length) {
delete out.Mirrors;
}
if (!Object.keys(_configs).length) {
delete out.Configs;
}
const value = Object.keys(out).length ? JSON.stringify(out) : '';
this.$set(this.value, 'value', value);
},
addMirror() {
this.mirrors.push({
key: '', value: clone(this.originMirror), idx: randomStr(5).toLowerCase()
});
this.update();
},
addConfig() {
this.configs.push({
key: '', value: clone(this.originConfig), idx: randomStr(5).toLowerCase()
});
this.update();
},
remove(type, idx) {
this[type].splice(idx, 1);
this.update();
}
},
watch: {
value: {
handler(value) {
if (!value.value) { // useDefaultVale
this.$set(this, 'mirrors', []);
this.$set(this, 'configs', []);
this.update();
}
},
deep: true,
}
}
};
</script>
<template>
<div>
<h3>{{ t('harvester.setting.containerdRegistry.mirrors.mirrors') }}</h3>
<div>
<InfoBox v-for="mirror, idx in mirrors" :key="mirror.idx" class="box">
<button type="button" class="role-link btn btn-sm remove" @click="remove('mirrors', idx)">
<i class="icon icon-2x icon-x" />
</button>
<div class="row mb-20">
<div class="col span-12">
<LabeledInput
v-model="mirror.key"
:mode="mode"
required
label-key="harvester.setting.containerdRegistry.mirrors.registryName"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
</div>
<div class="mb-20">
<LabeledSelect
:key="mirror.idx"
v-model="mirror.value.Endpoints"
:mode="mode"
required
label-key="harvester.setting.containerdRegistry.mirrors.endpoints"
:multiple="true"
:taggable="true"
:searchable="true"
:options="[]"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
<div class="row mb-20">
<KeyValue
v-model="mirror.value.Rewrites"
:add-label="t('harvester.setting.containerdRegistry.mirrors.rewrite.addRewrite')"
:mode="mode"
:title="t('harvester.setting.containerdRegistry.mirrors.rewrite.rewrite')"
:read-allowed="false"
:value-can-be-empty="true"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
</infobox>
</div>
<button class="btn btn-sm role-primary" @click.self="addMirror">
{{ t('harvester.setting.containerdRegistry.mirrors.addMirror') }}
</button>
<hr class="divider mt-20 mb-20" />
<h3>{{ t('harvester.setting.containerdRegistry.configs.configs') }}</h3>
<div>
<InfoBox v-for="config, idx in configs" :key="config.idx" class="box">
<button type="button" class="role-link btn btn-sm remove" @click="remove('configs', idx)">
<i class="icon icon-2x icon-x" />
</button>
<div class="row mb-20">
<div class="col span-12">
<div class="col span-12">
<LabeledInput
v-model="config.key"
:mode="mode"
:placeholder="t('harvester.setting.containerdRegistry.configs.registryPlaceholder')"
label-key="harvester.setting.containerdRegistry.configs.registryEDQNorIP"
@input="update"
/>
</div>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="config.value.Auth.Username"
:mode="mode"
label-key="harvester.setting.containerdRegistry.configs.username"
@input="update"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="config.value.Auth.Password"
:mode="mode"
label-key="harvester.setting.containerdRegistry.configs.password"
@input="update"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model="config.value.Auth.Auth"
:mode="mode"
type="multiline"
:min-height="150"
label-key="harvester.setting.containerdRegistry.configs.auth"
@input="update"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="config.value.Auth.IdentityToken"
:mode="mode"
type="multiline"
:min-height="150"
label-key="harvester.setting.containerdRegistry.configs.identityToken"
@input="update"
/>
</div>
</div>
<div class="row">
<LabeledSelect
v-model="config.value.TLS.InsecureSkipVerify"
:mode="mode"
label-key="harvester.setting.containerdRegistry.configs.insecureSkipVerify"
:options="insecureSkipVerifyOption"
@input="update"
/>
</div>
</infobox>
<button class="btn btn-sm role-primary" @click="addConfig">
{{ t('harvester.setting.containerdRegistry.configs.addConfig') }}
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
padding-top: 40px;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
@@ -0,0 +1,249 @@
<script>
import { _EDIT } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import InfoBox from '@shell/components/InfoBox';
import { CSI_DRIVER, VOLUME_SNAPSHOT_CLASS } from '../../types';
import { allHash } from '@shell/utils/promise';
const LONGHORN_DRIVER = 'driver.longhorn.io';
export default {
name: 'HarvesterCsiDriver',
components: {
InfoBox,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
registerBeforeHook: {
type: Function,
required: true,
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = {
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
volumeSnapshotClass: this.$store.dispatch(`${ inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS })
};
await allHash(hash);
},
data() {
const initValue = this.value.value || this.value.default;
const configArr = this.parseValue(initValue);
return { configArr };
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
provisioners() {
const inStore = this.$store.getters['currentProduct'].inStore;
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return csiDrivers.filter((provisioner) => {
return !this.configArr.map(config => config.key).includes(provisioner.name);
}).map((provisioner) => {
return provisioner.name;
});
},
disableAdd() {
const inStore = this.$store.getters['currentProduct'].inStore;
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return this.configArr.length >= csiDrivers.length;
}
},
methods: {
getVolumeSnapshotOptions(provisioner) {
const inStore = this.$store.getters['currentProduct'].inStore;
const volumeSnapshotClass = this.$store.getters[`${ inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
return volumeSnapshotClass.filter((snapshot) => {
return snapshot.driver === provisioner;
}).map((snapshot) => {
return {
label: snapshot.name,
value: snapshot.name
};
});
},
parseValue(value) {
const out = [];
let csiConfigJson = {};
try {
csiConfigJson = JSON.parse(value);
} catch (e) {
new Error('json error');
}
for (const [key, value] of Object.entries(csiConfigJson)) {
out.push({
key,
value,
});
}
return out;
},
update() {
const out = {};
this.configArr.map((config) => {
out[config.key] = config.value;
});
const value = this.configArr.length ? JSON.stringify(out) : '';
this.$set(this.value, 'value', value);
},
willSave() {
this.update();
const errors = [];
try {
this.configArr.forEach((config) => {
if (!config.key) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.provisioner') }, true));
}
if (!config.value.volumeSnapshotClassName) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true));
}
if (!config.value.backupVolumeSnapshotClassName) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName') }, true));
}
});
} catch (e) {
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
remove(idx) {
this.configArr.splice(idx, 1);
},
disableEdit(driver) {
return driver === LONGHORN_DRIVER;
},
add() {
this.configArr.push({
key: '',
value: { volumeSnapshotClassName: '', backupVolumeSnapshotClassName: '' }
});
},
useDefault() {
const configArr = this.parseValue(this.value.default);
this.$set(this, 'configArr', configArr);
this.update();
}
}
};
</script>
<template>
<div>
<InfoBox v-for="(driver, idx) in configArr" :key="driver.key" class="box">
<button :disabled="disableEdit(driver.key)" type="button" class="role-link btn btn-sm remove" @click="remove(idx)">
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model="driver.key"
:mode="mode"
required
:disabled="disableEdit(driver.key)"
label-key="harvester.setting.csiDriverConfig.provisioner"
:searchable="true"
:options="provisioners"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model="driver.value.volumeSnapshotClassName"
:mode="mode"
required
:disabled="disableEdit(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)"
:label="t('harvester.setting.csiDriverConfig.volumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model="driver.value.backupVolumeSnapshotClassName"
:mode="mode"
required
:disabled="disableEdit(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}"
@input="update"
/>
</div>
</div>
</infobox>
<button class="btn btn-sm role-primary" :disabled="disableAdd" @click.self="add">
{{ t('generic.add') }}
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
padding-top: 40px;
}
.remove {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>
@@ -0,0 +1,43 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import UnitInput from '@shell/components/form/UnitInput';
export default {
name: 'DefaultVMTerminationGracePeriodSeconds',
components: { UnitInput },
mixins: [CreateEditView],
data() {
return { terminationGracePeriodSeconds: this.value.value || this.value.default };
},
methods: {
update() {
this.$set(this.value, 'value', String(this.terminationGracePeriodSeconds));
},
useDefault() {
this.$set(this, 'terminationGracePeriodSeconds', Number(this.value.default));
this.update();
},
},
};
</script>
<template>
<div
class="row"
>
<div class="col span-12">
<UnitInput
v-model="terminationGracePeriodSeconds"
:suffix="terminationGracePeriodSeconds === 1 ? 'Second' : 'Seconds'"
:label="t('harvester.virtualMachine.terminationGracePeriodSeconds.label')"
:mode="mode"
@input="update"
/>
</div>
</div>
</template>
@@ -0,0 +1,87 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'HarvesterHttpProxy',
components: { Banner, LabeledInput },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return {
parseDefaultValue,
errors: []
};
},
created() {
this.update();
},
methods: {
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
}
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div>
<Banner color="warning">
<t k="harvester.setting.httpProxy.warning" :raw="true" />
</Banner>
<div class="row" @input="update">
<div class="col span-12">
<template>
<LabeledInput
v-model="parseDefaultValue.httpProxy"
class="mb-20"
:mode="mode"
label="http-proxy"
/>
<LabeledInput
v-model="parseDefaultValue.httpsProxy"
class="mb-20"
:mode="mode"
label="https-proxy"
/>
<LabeledInput
v-model="parseDefaultValue.noProxy"
class="mb-20"
:mode="mode"
label="no-proxy"
/>
</template>
</div>
</div>
</div>
</template>
@@ -0,0 +1,73 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import ArrayList from '@shell/components/form/ArrayList.vue';
export default {
name: 'HarvesterNtpServersConfig',
components: { ArrayList },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = { ntpServers: [] };
}
return { parseDefaultValue };
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = { ntpServers: [] };
}
this.$set(this, 'parseDefaultValue', parseDefaultValue);
this.update();
},
deep: true
}
},
methods: {
useDefault() {
const parseDefaultValue = { ntpServers: [] };
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
},
},
};
</script>
<template>
<div class="row">
<div class="col span-12">
<template>
<ArrayList
v-model="parseDefaultValue.ntpServers"
:title="t('harvester.host.ntp.label')"
:protip="t('harvester.host.ntp.tips')"
:value-placeholder="t('harvester.host.ntp.placeholder')"
:mode="mode"
@input="update"
/>
</template>
</div>
</div>
</template>
@@ -0,0 +1,94 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import UnitInput from '@shell/components/form/UnitInput';
export default {
name: 'HarvesterOvercommitConfig',
components: { UnitInput },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return {
parseDefaultValue,
errors: []
};
},
created() {
this.update();
},
methods: {
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
}
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div class="row">
<div class="col span-12">
<template>
<UnitInput
v-model="parseDefaultValue.cpu"
v-int-number
label-key="harvester.generic.cpu"
suffix="%"
:delay="0"
required
:mode="mode"
class="mb-20"
@input="update"
/>
<UnitInput
v-model="parseDefaultValue.memory"
v-int-number
label-key="harvester.generic.memory"
suffix="%"
:delay="0"
required
:mode="mode"
class="mb-20"
@input="update"
/>
<UnitInput
v-model="parseDefaultValue.storage"
v-int-number
label-key="harvester.generic.storage"
suffix="%"
:delay="0"
required
:mode="mode"
class="mb-20"
@input="update"
/>
</template>
</div>
</div>
</template>
@@ -0,0 +1,117 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import FileSelector from '@shell/components/form/FileSelector';
export default {
name: 'HarvesterSSLCertificates',
components: { FileSelector },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return {
parseDefaultValue,
caFileName: '',
publicCertificateFileName: '',
privateKeyFileName: ''
};
},
methods: {
onKeySelectedCa(type, file) {
const { name, value } = file;
this.$set(this.parseDefaultValue, type, value);
this.$set(this, `${ type }FileName`, name);
const _value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', _value);
}
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div>
<div class="row mb-20">
<div class="col span-12">
<div class="mb-10">
{{ t('harvester.setting.sslCertificates.publicCertificate') }}
</div>
<div class="chooseFile">
<FileSelector
:include-file-name="true"
class="btn btn-sm bg-primary mr-20"
label="Choose File"
@selected="onKeySelectedCa('publicCertificate', $event)"
/>
<span :class="{ 'text-muted': !publicCertificateFileName }">{{ publicCertificateFileName ? publicCertificateFileName : t('harvester.generic.noFileChosen') }}</span>
</div>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<div class="mb-10">
{{ t('harvester.setting.sslCertificates.privateKey') }}
</div>
<div class="chooseFile">
<FileSelector
:include-file-name="true"
class="btn btn-sm bg-primary mr-20"
label="Choose File"
@selected="onKeySelectedCa('privateKey', $event)"
/>
<span :class="{ 'text-muted': !privateKeyFileName }">{{ privateKeyFileName ? privateKeyFileName : t('harvester.generic.noFileChosen') }}</span>
</div>
</div>
</div>
<div class="row mb-20">
<div class="col span-12">
<div class="mb-10">
{{ t('harvester.setting.sslCertificates.ca') }}
</div>
<div class="chooseFile">
<FileSelector
:include-file-name="true"
class="btn btn-sm bg-primary mr-20"
label="Choose File"
@selected="onKeySelectedCa('ca', $event)"
/>
<span :class="{ 'text-muted': !caFileName }">{{ caFileName ? caFileName : t('harvester.generic.noFileChosen') }}</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chooseFile {
display: flex;
align-items: center;
}
</style>
@@ -0,0 +1,161 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { _EDIT } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterSslParameters',
components: {
LabeledInput,
LabeledSelect,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
registerBeforeHook: {
type: Function,
required: true,
},
},
data() {
let parsedDefaultValue = {};
try {
parsedDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parsedDefaultValue = JSON.parse(this.value.default);
}
const protocols = parsedDefaultValue.protocols && (parsedDefaultValue.protocols || '').split(' ');
return {
parsedDefaultValue: {
protocols,
ciphers: parsedDefaultValue.ciphers,
},
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
protocolOptions() {
return [{
label: 'TLSv1.3',
value: 'TLSv1.3',
}, {
label: 'TLSv1.2',
value: 'TLSv1.2',
}, {
label: `TLSv1.1 (${ this.t('generic.deprecated') })`,
value: 'TLSv1.1',
}, {
label: `TLSv1 (${ this.t('generic.deprecated') })`,
value: 'TLSv1',
}, {
label: `SSLv3 (${ this.t('generic.deprecated') })`,
value: 'SSLv3',
}, {
label: `SSLv2 (${ this.t('generic.deprecated') })`,
value: 'SSLv2',
}];
},
},
methods: {
update() {
const out = {
protocols: (this.parsedDefaultValue.protocols || []).join(' '),
ciphers: this.parsedDefaultValue.ciphers,
};
const value = JSON.stringify(out);
this.$set(this.value, 'value', value);
},
willSave() {
const errors = [];
const ciphers = this.parsedDefaultValue.ciphers;
const protocols = this.parsedDefaultValue.protocols || [];
if (ciphers && protocols.length === 0) {
errors.push(this.t('validation.required', { key: this.t('harvester.sslParameters.protocols.label') }, true));
}
if (!ciphers && protocols.length > 0) {
errors.push(this.t('validation.required', { key: this.t('harvester.sslParameters.ciphers.label') }, true));
}
const regex = /^(:?[A-Z0-9]+(?:-[A-Z0-9]+)+)+$/gm;
if (ciphers && (!ciphers.match(regex) || ciphers.startsWith(':'))) {
errors.push(this.t('validation.invalid', { key: this.t('harvester.sslParameters.ciphers.label') }, true));
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
},
watch: {
'value.value': {
handler(value) {
if (value === this.value.default) {
this.parsedDefaultValue.protocols = [];
this.parsedDefaultValue.ciphers = '';
}
},
deep: true,
},
}
};
</script>
<template>
<div>
<div class="row mt-10">
<div class="col span-12">
<LabeledSelect
v-model="parsedDefaultValue.protocols"
:mode="mode"
label-key="harvester.sslParameters.protocols.label"
:multiple="true"
:options="protocolOptions"
@input="update"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<LabeledInput
v-model="parsedDefaultValue.ciphers"
:mode="mode"
label-key="harvester.sslParameters.ciphers.label"
@input="update"
/>
</div>
</div>
</div>
</template>
@@ -0,0 +1,240 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import ArrayList from '@shell/components/form/ArrayList';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { _EDIT } from '@shell/config/query-params';
import { Banner } from '@components/Banner';
import Tip from '@shell/components/Tip';
import { HCI } from '../../types';
import { allHash } from '@shell/utils/promise';
import { NODE } from '@shell/config/types';
export default {
name: 'HarvesterEditStorageNetwork',
components: {
ArrayList,
Tip,
Banner,
LabeledInput,
LabeledSelect,
RadioGroup
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
});
},
data() {
let parsedDefaultValue = {};
let openVlan = false;
try {
parsedDefaultValue = JSON.parse(this.value.value);
openVlan = true;
} catch (error) {
parsedDefaultValue = {
vlan: '',
clusterNetwork: '',
range: '',
exclude: []
};
}
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
return {
openVlan,
errors: [],
exclude,
parsedDefaultValue,
defaultAddValue: ''
};
},
created() {
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return clusterNetworks.map((n) => {
const disabled = !n.isReadyForStorageNetwork;
return {
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
value: n.id,
disabled,
};
});
},
},
methods: {
update() {
const exclude = this.exclude.filter(ip => ip);
if (Array.isArray(exclude) && exclude.length > 0) {
this.parsedDefaultValue.exclude = exclude;
} else {
delete this.parsedDefaultValue.exclude;
}
const valueString = JSON.stringify(this.parsedDefaultValue);
if (this.openVlan) {
this.$set(this.value, 'value', valueString);
} else {
this.$set(this.value, 'value', '');
}
},
willSave() {
this.update();
const errors = [];
if (this.openVlan) {
const valid = !!/^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/.test(this.parsedDefaultValue.range);
if (!valid) {
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
}
if (!this.parsedDefaultValue.vlan) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
}
if (!this.parsedDefaultValue.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
}
if (this.exclude) {
const hasInvalidCIDR = this.exclude.find((cidr) => {
return !isValidCIDR(cidr);
});
if (hasInvalidCIDR) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
}
}
} else {
return Promise.resolve();
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
},
};
</script>
<template>
<div
:class="mode"
@input="update"
>
<Banner color="warning">
<t k="harvester.setting.storageNetwork.warning" :raw="true" />
</Banner>
<RadioGroup
v-model="openVlan"
class="mb-20"
name="model"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@input="update"
/>
<div v-if="openVlan">
<LabeledInput
v-model.number="parsedDefaultValue.vlan"
class="mb-20"
:mode="mode"
required
label-key="harvester.setting.storageNetwork.vlan"
/>
<LabeledSelect
v-model="parsedDefaultValue.clusterNetwork"
label-key="harvester.setting.storageNetwork.clusterNetwork"
class="mb-20"
required
:options="clusterNetworkOptions"
@input="update"
/>
<LabeledInput
v-model="parsedDefaultValue.range"
class="mb-5"
:mode="mode"
required
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
label-key="harvester.setting.storageNetwork.range.label"
/>
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.setting.storageNetwork.tip')">
<t k="harvester.setting.storageNetwork.tip" :raw="true" />
</Tip>
<ArrayList
v-model="exclude"
:show-header="true"
:default-add-value="defaultAddValue"
:mode="mode"
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
@input="update"
>
<template v-slot:column-headers>
<div class="box">
<div class="key">
{{ t('harvester.setting.storageNetwork.exclude.label') }}
</div>
</div>
</template>
<template v-slot:columns="scope">
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@input="update"
/>
</div>
</template>
</ArrayList>
</div>
</div>
</template>
@@ -0,0 +1,134 @@
<script>
import { _EDIT } from '@shell/config/query-params';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterSupportBundleImage',
components: { LabeledInput, LabeledSelect },
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => {
return {};
},
},
},
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return { parseDefaultValue };
},
created() {
this.update();
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
computed: {
imagePolicyOptions() {
return [{
label: this.t('generic.imagePullPolicy.always'),
value: 'Always',
}, {
label: this.t('generic.imagePullPolicy.ifNotPresent'),
value: 'IfNotPresent',
}, {
label: this.t('generic.imagePullPolicy.never'),
value: 'Never'
}];
}
},
methods: {
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
},
willSave() {
const errors = [];
if (!this.parseDefaultValue.repository) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.supportBundleImage.repo') }, true));
}
if (!this.parseDefaultValue.tag) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.supportBundleImage.tag') }, true));
}
if (errors.length > 0) {
return Promise.reject(errors);
} else {
return Promise.resolve();
}
},
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div class="row" @input="update">
<div class="col span-12">
<template>
<LabeledInput
v-model="parseDefaultValue.repository"
class="mb-20"
:mode="mode"
required
label-key="harvester.setting.supportBundleImage.repo"
/>
<LabeledInput
v-model="parseDefaultValue.tag"
class="mb-20"
:mode="mode"
required
label-key="harvester.setting.supportBundleImage.tag"
/>
<LabeledSelect
v-model="parseDefaultValue.imagePullPolicy"
class="mb-20"
required
label-key="harvester.setting.supportBundleImage.imagePullPolicy"
:options="imagePolicyOptions"
@input="update"
/>
</template>
</div>
</div>
</template>
@@ -0,0 +1,73 @@
<script>
import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HarvesterBundleNamespaces',
components: { LabeledSelect },
mixins: [CreateEditView],
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
},
data() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || '';
if (namespacesStr) {
namespaces = namespacesStr.split(',');
}
return { namespaces };
},
computed: {
namespaceOptions() {
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
return {
label: N.id,
value: N.id
};
});
}
},
methods: {
update() {
const namespaceStr = this.namespaces.join(',');
this.$set(this.value, 'value', namespaceStr);
}
},
watch: {
'value.value': {
handler(neu) {
if (neu === this.value.default || !neu) {
this.namespaces = [];
}
},
deep: true
}
}
};
</script>
<template>
<div class="row">
<div class="col span-12">
<LabeledSelect
v-model="namespaces"
:multiple="true"
label-key="nameNsDescription.namespace.label"
:mode="mode"
:options="namespaceOptions"
@input="update"
/>
</div>
</div>
</template>
@@ -0,0 +1,81 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
export default {
name: 'HarvesterVMForceDeletePolicy',
components: { LabeledInput, RadioGroup },
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
return { parseDefaultValue };
},
created() {
this.update();
},
methods: {
update() {
const value = JSON.stringify(this.parseDefaultValue);
this.$set(this.value, 'value', value);
}
},
watch: {
value: {
handler(neu) {
const parseDefaultValue = JSON.parse(neu.value);
this.$set(this, 'parseDefaultValue', parseDefaultValue);
},
deep: true
}
}
};
</script>
<template>
<div class="row" @input="update">
<div class="col span-12">
<RadioGroup
v-model="parseDefaultValue.enable"
class="mb-20"
name="model"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@input="update"
/>
<LabeledInput
v-if="parseDefaultValue.enable"
v-model.number="parseDefaultValue.period"
v-int-number
class="mb-20"
:mode="mode"
label-key="harvester.setting.vmForceDeletionPolicy.period"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep .radio-group {
display: flex;
.radio-container {
margin-right: 30px;
}
}
</style>