Adding the vSphere maching-config

Also fixed a bug with the vsphere credentials.

rancher/dashboard#2969
rancher/dashboard#3326
This commit is contained in:
Cody Jackson 2021-06-03 15:31:22 -07:00
parent 7aff3b8358
commit 52244cf2dd
8 changed files with 1196 additions and 8 deletions

View File

@ -140,6 +140,7 @@ product:
suffix:
percent: "%"
milliCpus: mili CPUs
cores: Cores
cpus: CPUs
ib: iB
mib: MiB
@ -1020,6 +1021,83 @@ cluster:
=1 {# vCPU}
other {# vCPUs}
}, {disk} GB Disk ({value})
vsphere:
hostOptions:
any: Any
vAppOptions:
label: vApp Options
description: Choose OVF environment properties
disable: Do not use vApp
auto: Use vApp to configure networks with network protocol profiles
manual: Provide a custom vApp config
restoreType: Restore Type
transport:
label: OVF environment transport
tooltip: com.vmware.guestInfo or iso
placeholder: e.g. com.vmware.guestInfo
protocol:
label: vApp IP protocol
tooltip: IPv4 or IPv6
placeholder: e.g. IPv4
allocation:
label: vApp IP allocation policy
tooltip: dhcp, fixed, transient or fixedAllocated
placeholder: e.g. fixedAllocated
properties:
label: vApp properties
add: Add Property
keyPlaceholder: e.g. guestinfo.interface.0.ip.0.address
valuePlaceholder: e.g. ip:VM Network, expression or string
networks:
label: Networks
add: Add Network
guestinfo:
label: Configuration Parameters used for guestinfo
add: Add Parameter
keyPlaceholder: e.g. guestinfo.hostname
valuePlaceholder: e.g. myrancherhost
creationMethods:
template: 'Deploy from template: Data Center'
library: 'Deploy from template: Content Library'
vm: 'Clone an existing virtual machine'
legacy: 'Install from boot2docker ISO (Legacy)'
scheduling:
label: Scheduling
description: Choose what hypervisor the virtual machine will be scheduled to
dataCenter: Data Center
resourcePool: Resource Pool
dataStore: Data Store
folder: Folder
host:
label: Host
note: Specific host to create VM on (leave blank for standalone ESXi or for cluster with DRS)
instanceOptions:
label: Instance Options
description: Choose the size and OS of the virtual machine
cpus: CPUs
memory: Memory
disk: Disk
creationMethod: Creation method
template: Template
contentLibrary: Content library
libraryTemplate: Library template
virtualMachine: Virtual machine
osIsoUrl:
label: OS ISO URL
placeholder: 'Default: Latest rancheros-vmware image'
cloudInit:
label: Cloud Init
placeholder: e.g. http://my_host/cloud-config.yml
note: Cloud-init file or url to set in the guestinfo
cloudConfigYaml: Cloud Config YAML
tags:
label: Tags
description: Tags allow you to attach metadata to objects in the vSphere inventory to make it easier to sort and search for these objects.
addTag: Add Tag
customAttributes:
label: Custom attributes (legacy)
description: Custom attributes allow you to attach metadata to objects in the vSphere inventory to make it easier to sort and search for these objects.
add: Add custom attribute
machinePool:
name:

View File

@ -6,6 +6,15 @@ export default {
components: { LabeledInput },
mixins: [CreateEditView],
watch: {
value: {
deep: true,
handler(neu) {
this.$emit('validationChanged', !!neu);
}
}
},
methods: {
test() {
// Vsphere doesn't have a test function. The credential has to be created before we can make calls.

View File

@ -76,6 +76,11 @@ export default {
type: [String, Number, Object, Array],
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data() {
@ -280,8 +285,8 @@ export default {
</div>
<div v-if="showAdd && !isView" class="footer">
<slot v-if="showAdd" name="add">
<button type="button" class="btn role-tertiary add" @click="add()">
{{ addLabel }}
<button type="button" class="btn role-tertiary add" :disabled="loading" @click="add()">
<i v-if="loading" class="mr-5 icon icon-spinner icon-spin icon-lg" /> {{ addLabel }}
</button>
</slot>
</div>

View File

@ -0,0 +1,77 @@
<script>
import ArrayList from '@/components/form/ArrayList';
import Select from '@/components/form/Select';
export default {
components: { ArrayList, Select },
props: {
value: {
type: Array,
required: true
},
options: {
default: null,
type: Array
},
selectProps: {
type: Object,
default: null,
},
arrayListProps: {
type: Object,
default: null
},
loading: {
type: Boolean,
default: false
}
},
computed: {
filteredOptions() {
return this.options
.filter(option => !this.value.includes(option.value));
},
addAllowed() {
return this.filteredOptions.length > 0;
}
},
methods: {
updateRow(index, value) {
this.value.splice(index, 1, value);
this.$emit(value);
},
calculateOptions(value) {
const valueOption = this.options.find(o => o.value === value);
if (valueOption) {
return [valueOption, ...this.filteredOptions];
}
return this.filteredOptions;
}
}
};
</script>
<template>
<ArrayList
v-bind="arrayListProps"
:value="value"
class="array-list-select"
:add-allowed="addAllowed || loading"
:loading="loading"
@input="$emit('input', $event)"
>
<template v-slot:columns="scope">
<Select :value="scope.row.value" v-bind="selectProps" :options="calculateOptions(scope.row.value)" @input="updateRow(scope.i, $event)" />
</template>
</ArrayList>
</template>
<style lang="scss" scoped>
::v-deep .unlabeled-select {
height: 61px;
}
</style>

View File

@ -73,6 +73,11 @@ export default {
default: true,
},
keyOptionUnique: {
type: Boolean,
default: false,
},
keyPlaceholder: {
type: String,
default() {
@ -211,6 +216,10 @@ export default {
type: Array,
default: () => [': ', '='],
},
loading: {
default: false,
type: Boolean
},
},
data() {
@ -279,6 +288,19 @@ export default {
containerStyle() {
return `grid-template-columns: repeat(${ 2 + this.extraColumns.length }, 1fr)${ this.removeAllowed ? ' 50px' : '' };`;
},
usedKeyOptions() {
return this.rows.map(row => row[this.keyName]);
},
filteredKeyOptions() {
if (this.keyOptionUnique) {
return this.keyOptions
.filter(option => !this.usedKeyOptions.includes(option.value));
}
return this.keyOptions;
}
},
created() {
@ -418,6 +440,16 @@ export default {
this.queueUpdate();
},
calculateOptions(value) {
const valueOption = this.keyOptions.find(o => o.value === value);
if (valueOption) {
return [valueOption, ...this.filteredKeyOptions];
}
return this.filteredKeyOptions;
},
get,
}
@ -476,7 +508,8 @@ export default {
:searchable="true"
:clearable="false"
:taggable="keyTaggable"
:options="keyOptions"
:options="calculateOptions(row[keyName])"
@input="queueUpdate"
/>
<input
v-else
@ -545,8 +578,8 @@ export default {
<div v-if="(addAllowed || readAllowed) && !isView" class="footer">
<slot name="add" :add="add">
<button v-if="addAllowed" type="button" class="btn role-tertiary add" @click="add()">
{{ addLabel }}
<button v-if="addAllowed" type="button" class="btn role-tertiary add" :disabled="loading || (keyOptions && filteredKeyOptions.length === 0)" @click="add()">
<i v-if="loading" class="mr-5 icon icon-spinner icon-spin icon-lg" /> {{ addLabel }}
</button>
<FileSelector
v-if="readAllowed"

View File

@ -25,6 +25,10 @@ export default {
default: true,
type: Boolean
},
loading: {
default: false,
type: Boolean
},
localizedLabel: {
default: false,
type: Boolean
@ -62,7 +66,7 @@ export default {
selectable: {
default: (opt) => {
if ( opt ) {
if ( opt.disabled || opt.kind === 'group' || opt.kind === 'divider' ) {
if ( opt.disabled || opt.kind === 'group' || opt.kind === 'divider' || opt.loading ) {
return false;
}
}
@ -240,7 +244,7 @@ export default {
:append-to-body="appendToBody"
:calculate-position="withPopper"
:class="{ 'no-label': !(label || '').length }"
:disabled="isView || disabled"
:disabled="isView || disabled || loading"
:get-option-key="
(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))
"
@ -252,7 +256,7 @@ export default {
:reduce="(x) => reduce(x)"
:searchable="isSearchable"
:selectable="selectable"
:value="value != null ? value : ''"
:value="value != null && !loading ? value : ''"
v-on="$listeners"
@search:blur="onBlur"
@search:focus="onFocus"
@ -274,6 +278,7 @@ export default {
<slot :name="slot" v-bind="scope" />
</template>
</v-select>
<i v-if="loading" class="icon icon-spinner icon-spin icon-lg" />
<LabeledTooltip
v-if="tooltip && !focused"
:hover="hoverTooltip"
@ -285,6 +290,14 @@ export default {
<style lang='scss' scoped>
.labeled-select {
position: relative;
.icon-spinner {
position: absolute;
left: calc(50% - .5em);
top: calc(50% - .5em);
}
.labeled-container {
padding: $input-padding-sm 0 1px $input-padding-sm;

View File

@ -0,0 +1,928 @@
<script>
import Loading from '@/components/Loading';
import CreateEditView from '@/mixins/create-edit-view';
import LabeledSelect from '@/components/form/LabeledSelect';
// import Checkbox from '@/components/form/Checkbox';
import { SECRET } from '@/config/types';
import { stringify } from '@/utils/error';
import Banner from '@/components/Banner';
import UnitInput from '@/components/form/UnitInput';
import Card from '@/components/Card';
import RadioGroup from '@/components/form/RadioGroup';
import KeyValue from '@/components/form/KeyValue';
import LabeledInput from '@/components/form/LabeledInput';
import ArrayListSelect from '@/components/form/ArrayListSelect';
import YamlEditor from '@/components/YamlEditor';
import { get, set } from '@/utils/object';
import { integerString, keyValueStrings } from '@/utils/computed';
import { _CREATE } from '@/config/query-params';
const SENTINEL = '__SENTINEL__';
const VAPP_MODE = {
DISABLED: 'disabled',
AUTO: 'auto',
MANUAL: 'manual'
};
const CREATION_METHOD = {
TEMPLATE: 'template',
LIBRARY: 'library',
VM: 'vm',
LEGACY: 'legacy'
};
const BOOT_2_DOCKER_URL = 'https://releases.rancher.com/os/latest/rancheros-vmware.iso';
const INITIAL_VAPP_OPTIONS = {
vappIpprotocol: '',
vappIpallocationpolicy: '',
vappTransport: '',
vappProperty: []
};
const DEFAULT_CFGPARAM = ['disk.enableUUID=TRUE'];
const getDefaultVappOptions = (networks) => {
return {
vappIpprotocol: 'IPv4',
vappIpallocationpolicy: 'fixedAllocated',
vappTransport: 'com.vmware.guestInfo',
vappProperty: networksToVappProperties(networks)
};
};
const stringsToParams = (params, str) => {
const index = str.indexOf('=');
if ( index > -1 ) {
params.push({
key: str.slice(0, index),
value: str.slice(index + 1),
});
}
return params;
};
const networksToVappProperties = (networks) => {
if (networks.length === 0) {
return [];
}
return networks.reduce(networkToVappProperties, [
`guestinfo.dns.servers=\${dns:${ networks[0] }}`,
`guestinfo.dns.domains=\${searchPath:${ networks[0] }}`
]);
};
const networkToVappProperties = (props, network, i) => {
const n = i.toString();
props.push(
`guestinfo.interface.${ n }.ip.0.address=ip:${ network }`,
`guestinfo.interface.${ n }.ip.0.netmask=\${netmask:${ network }}`,
`guestinfo.interface.${ n }.route.0.gateway=\${gateway:${ network }}`
);
return props;
};
const getInitialVappMode = (c) => {
const vappProperty = c.vappProperty || [];
if (
!c.vappIpprotocol &&
!c.vappIpallocationpolicy &&
!c.vappTransport &&
vappProperty.length === 0
) {
return VAPP_MODE.DISABLED;
}
const d = getDefaultVappOptions(c.network);
if (
c.vappIpprotocol === d.vappIpprotocol &&
c.vappIpallocationpolicy === d.vappIpallocationpolicy &&
c.vappTransport === d.vappTransport &&
vappProperty.length === d.vappProperty.length &&
vappProperty.join() === d.vappProperty.join()
) {
return VAPP_MODE.AUTO;
}
return VAPP_MODE.MANUAL;
};
/**
* passing 'datacenter' yields
*
* {
* datacenter() {
* return this.datacenterResults || [];
* },
* datacenterLoading() {
* return this.datacenterLoading === null;
* }
* }
*/
function createOptionHelpers(name) {
return {
[name]() {
return this[`${ name }Results`] || [];
},
[`${ name }Loading`]() {
return this[`${ name }Results`] === null;
}
};
}
export default {
components: {
ArrayListSelect, Card, KeyValue, Loading, LabeledInput, LabeledSelect, Banner, UnitInput, RadioGroup, YamlEditor
},
mixins: [CreateEditView],
props: {
credentialId: {
type: String,
required: true,
},
},
async fetch() {
this.errors = [];
this.credential = await this.$store.dispatch('management/find', { type: SECRET, id: this.cloudCredentialId });
},
data() {
const vAppOptions = [
{
label: this.t('cluster.machineConfig.vsphere.vAppOptions.disable'),
value: VAPP_MODE.DISABLED
},
{
label: this.t('cluster.machineConfig.vsphere.vAppOptions.auto'),
value: VAPP_MODE.AUTO
},
{
label: this.t('cluster.machineConfig.vsphere.vAppOptions.manual'),
value: VAPP_MODE.MANUAL
},
];
const creationMethods = [
{
label: this.t('cluster.machineConfig.vsphere.creationMethods.template'),
value: CREATION_METHOD.TEMPLATE
},
// This is currently broken in the backend. Once fixed we can add this back
// {
// label: this.t('cluster.machineConfig.vsphere.creationMethods.library'),
// value: CREATION_METHOD.LIBRARY
// },
{
label: this.t('cluster.machineConfig.vsphere.creationMethods.vm'),
value: CREATION_METHOD.VM
},
{
label: this.t('cluster.machineConfig.vsphere.creationMethods.legacy'),
value: CREATION_METHOD.LEGACY
},
];
if (this.mode === _CREATE) {
this.$set(this.value, 'creationType', creationMethods[0].value);
this.$set(this.value, 'cpuCount', '2');
this.$set(this.value, 'diskSize', '20000');
this.$set(this.value, 'memorySize', '2048');
this.$set(this.value, 'hostsystem', '');
this.$set(this.value, 'cloudConfig', '#cloud-config\n\n');
this.$set(this.value, 'cfgparam', DEFAULT_CFGPARAM);
this.$set(this.value, 'vappProperty', this.value.vappProperty);
Object.entries(INITIAL_VAPP_OPTIONS).forEach(([key, value]) => {
this.$set(this.value, key, value);
});
}
return {
credential: null,
creationMethods,
dataCentersResults: null,
resourcePoolsResults: null,
dataStoresResults: null,
dataStoreClustersResults: null,
foldersResults: null,
hostsResults: null,
templatesResults: null,
contentLibrariesResults: null,
libraryTemplatesResults: null,
virtualMachinesResults: null,
showTemplatesResults: null,
tagsResults: null,
attributeKeysResults: null,
networksResults: null,
vAppOptions,
vappMode: getInitialVappMode(this.value)
};
},
computed: {
...createOptionHelpers('dataCenters'),
...createOptionHelpers('resourcePools'),
...createOptionHelpers('dataStores'),
...createOptionHelpers('dataStoreClusters'),
...createOptionHelpers('folders'),
...createOptionHelpers('hosts'),
...createOptionHelpers('templates'),
...createOptionHelpers('contentLibraries'),
...createOptionHelpers('libraryTemplates'),
...createOptionHelpers('virtualMachines'),
...createOptionHelpers('showTemplates'),
...createOptionHelpers('tags'),
...createOptionHelpers('attributeKeys'),
...createOptionHelpers('networks'),
showTemplate() {
return this.value.creationType === 'template';
},
showContentLibrary() {
return this.value.creationType === 'library';
},
showVirtualMachines() {
return this.value.creationType === 'vm';
},
showIso() {
return this.value.creationType === 'legacy';
},
showManual() {
return this.vappMode === 'manual';
},
cloudCredentialId() {
return this.credentialId.split('/')[1];
},
host: {
get() {
return this.value.hostsystem === '' ? SENTINEL : this.value.hostsystem;
},
set(value) {
const newValue = value === SENTINEL ? '' : value;
this.$set(this.value, 'hostsystem', newValue);
}
},
customAttribute: keyValueStrings('value.customAttribute'),
vappProperty: keyValueStrings('value.vappProperty'),
cfgparam: keyValueStrings('value.cfgparam'),
cpuCount: integerString('value.cpuCount'),
memorySize: integerString('value.memorySize'),
diskSize: integerString('value.diskSize'),
showCloudConfigYaml() {
return this.value.creationType !== 'legacy';
},
},
watch: {
cloudCredentialId() {
this.$fetch();
},
credential() {
this.loadDataCenters();
},
dataCentersResults() {
this.loadResourcePools();
this.loadDataStores();
this.loadFolders();
this.loadHosts();
this.loadTemplates();
this.loadTags();
this.loadCustomAttributes();
// This is currently broken in the backend. Once fixed we can add this back
// this.loadContentLibraries();
this.loadLibraryTemplates();
this.loadVirtualMachines();
this.loadNetworks();
},
'value.contentLibrary'() {
this.loadLibraryTemplates();
},
'value.creationType'(value) {
this.$set(this.value, 'cloneFrom', '');
const boot2dockerUrl = value === CREATION_METHOD.LEGACY ? BOOT_2_DOCKER_URL : '';
this.$set(this.value, 'boot2dockerUrl', boot2dockerUrl);
},
vappMode(value) {
if (value === VAPP_MODE.AUTO) {
const defaultVappOptions = getDefaultVappOptions(this.value.network);
return this.updateVappOptions(defaultVappOptions);
}
this.updateVappOptions(INITIAL_VAPP_OPTIONS);
}
},
methods: {
stringify,
async requestOptions(resource, secretId, dataCenter, library) {
const datacenterLessResources = ['tag-categories', 'tags', 'data-centers', 'custom-attributes'];
if (!secretId || (!datacenterLessResources.includes(resource) && !dataCenter)) {
return [];
}
const queryParams = Object.entries({
secretId,
dataCenter,
library
})
.filter(entry => entry[1])
.map(entry => `${ entry[0] }=${ entry[1] }`)
.join('&');
const url = `/meta/vsphere/${ resource }?${ queryParams }`;
const result = await this.$store.dispatch('management/request', {
url,
redirectUnauthorized: false,
}, { root: true });
return result.data;
},
async loadDataCenters() {
const options = await this.requestOptions('data-centers', this.cloudCredentialId);
const content = this.mapPathOptionsToContent(options);
const valueInContent = content.find(c => c.value === this.value.datacenter );
if (!valueInContent) {
this.$set(this.value, 'datacenter', options[0]);
this.$set(this.value, 'cloneFrom', undefined);
this.$set(this.value, 'useDataStoreCluster', false);
}
this.$set(this, 'dataCentersResults', content);
},
async loadTags() {
const categoriesPromise = this.requestOptions('tag-categories', this.cloudCredentialId);
const optionsPromise = this.requestOptions('tags', this.cloudCredentialId);
const [categories, options] = await Promise.all([categoriesPromise, optionsPromise]);
const content = this.mapTagsToContent(options).map(option => ({
...option,
category: categories.find(c => c.name === option.category)
}));
this.resetValueIfNecessary('tag', content, options, true);
this.$set(this, 'tagsResults', content);
},
async loadCustomAttributes() {
const options = await this.requestOptions('custom-attributes', this.cloudCredentialId);
this.$set(this, 'attributeKeysResults', this.mapCustomAttributesToContent(options));
},
async loadHosts() {
const options = await this.requestOptions(
'hosts',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapHostOptionsToContent(options);
this.resetValueIfNecessary('hostsystem', content, options);
this.$set(this, 'hostsResults', content);
},
async loadResourcePools() {
const options = await this.requestOptions(
'resource-pools',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPoolOptionsToContent(options);
this.resetValueIfNecessary('pool', content, options);
this.$set(this, 'resourcePoolsResults', content);
},
async loadDataStores() {
const options = await this.requestOptions(
'data-stores',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('datastore', content, options);
this.$set(this, 'dataStoresResults', content);
},
async loadDataStoreClusters() {
const options = await this.requestOptions(
'data-store-clusters',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('datastoreCluster', content, options);
this.$set(this, 'dataStoreClustersResults', content);
},
async loadFolders() {
const options = await this.requestOptions(
'folders',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapFolderOptionsToContent(options);
this.resetValueIfNecessary('folder', content, options);
this.$set(this, 'foldersResults', content);
},
async loadNetworks() {
const options = await this.requestOptions(
'networks',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('network', content, options, true);
this.$set(this, 'networksResults', content);
},
async loadContentLibraries() {
const options = await this.requestOptions(
'content-libraries',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('contentLibrary', content, options);
this.$set(this, 'contentLibrariesResults', content);
},
async loadLibraryTemplates() {
const contentLibrary = this.value.contentLibrary;
if (!contentLibrary) {
return [];
}
const options = await this.requestOptions(
'library-templates',
this.cloudCredentialId,
undefined,
contentLibrary
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('cloneFrom', content, options);
this.$set(this, 'libraryTemplatesResults', content);
},
async loadVirtualMachines() {
const options = await this.requestOptions(
'virtual-machines',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('cloneFrom', content, options);
this.$set(this, 'virtualMachinesResults', content);
},
async loadTemplates() {
const options = await this.requestOptions(
'templates',
this.cloudCredentialId,
this.value.datacenter
);
const content = this.mapPathOptionsToContent(options);
this.resetValueIfNecessary('cloneFrom', content, options);
this.$set(this, 'templatesResults', content);
},
resetValueIfNecessary(key, content, options, isArray = false) {
const isValueInContent = () => {
if (isArray) {
return this.value[key].every(value => content.find(c => c.value === value));
}
return content.find(c => c.value === this.value[key] );
};
if (!isValueInContent()) {
const value = isArray ? [] : content[0]?.value;
this.$set(this.value, key, value);
}
},
mapPathOptionsToContent(pathOptions) {
return pathOptions.map((pathOption) => {
const split = pathOption.split('/');
return {
label: split[split.length - 1],
value: pathOption
};
});
},
mapHostOptionsToContent(hostOptions) {
return this.mapPathOptionsToContent(hostOptions)
.map(c => c.value === '' ? {
label: this.t('cluster.machineConfig.vsphere.hostOptions.any'),
value: SENTINEL
} : c);
},
mapFolderOptionsToContent(folderOptions) {
return folderOptions.map(option => ({
label: option || '\u00A0',
value: option || ''
}));
},
mapPoolOptionsToContent(pathOptions) {
return pathOptions.map((pathOption) => {
const splitOptions = pathOption.split('/');
const label = splitOptions.slice(2).join('/');
return {
label,
value: pathOption
};
});
},
mapCustomAttributesToContent(customAttributes) {
return customAttributes.map(customAttribute => ({
label: customAttribute.name,
value: customAttribute.key.toString()
}));
},
mapTagsToContent(tags) {
return tags.map(tag => ({
...tag,
label: `${ tag.category } / ${ tag.name }`,
value: tag.id
}));
},
initKeyValueParams(pairsKey, paramsKey) {
set(this, paramsKey, (get(this, pairsKey) || []).reduce(stringsToParams, []));
},
updateVappOptions(opts) {
this.$set(this.value, 'vappIpprotocol', opts.vappIpprotocol);
this.$set(this.value, 'vappIpallocationpolicy', opts.vappIpallocationpolicy);
this.$set(this.value, 'vappTransport', opts.vappTransport);
this.$set(this.value, 'vappProperty', opts.vappProperty);
this.initKeyValueParams('value.vappProperty', 'initVappArray');
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" :delayed="true" />
<div v-else-if="errors.length">
<div
v-for="(err, idx) in errors"
:key="idx"
>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
</div>
<div v-else>
<Card class="m-0 mt-20" :show-highlight-border="false" :show-actions="false">
<h4 slot="title" class="text-default-text mb-5">
{{ t('cluster.machineConfig.vsphere.scheduling.label') }}
<p class="text-muted text-small">
{{ t('cluster.machineConfig.vsphere.scheduling.description') }}
</p>
</h4>
<div slot="body">
<div class="row">
<div class="col span-6">
<LabeledSelect
v-model="value.datacenter"
:loading="dataCentersLoading"
:mode="mode"
:options="dataCenters"
:label="t('cluster.machineConfig.vsphere.scheduling.dataCenter')"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model="value.pool"
:loading="resourcePoolsLoading"
:mode="mode"
:options="resourcePools"
:label="t('cluster.machineConfig.vsphere.scheduling.resourcePool')"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<LabeledSelect
v-model="value.datastore"
:loading="dataStoresLoading"
:mode="mode"
:options="dataStores"
:label="t('cluster.machineConfig.vsphere.scheduling.dataStore')"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model="value.folder"
:loading="foldersLoading"
:mode="mode"
:options="folders"
:label="t('cluster.machineConfig.vsphere.scheduling.folder')"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-12">
<LabeledSelect
v-model="host"
:loading="hostsLoading"
:mode="mode"
:options="hosts"
:label="t('cluster.machineConfig.vsphere.scheduling.host.label')"
/>
<p class="text-muted mt-5">
{{ t('cluster.machineConfig.vsphere.scheduling.host.note') }}
</p>
</div>
</div>
</div>
</Card>
<Card class="m-0 mt-20" :show-highlight-border="false" :show-actions="false">
<h4 slot="title" class="text-default-text mb-5">
{{ t('cluster.machineConfig.vsphere.instanceOptions.label') }}
<p class="text-muted text-small">
{{ t('cluster.machineConfig.vsphere.instanceOptions.description') }}
</p>
</h4>
<div slot="body">
<div class="row mt-10">
<div class="col span-6">
<UnitInput
v-model="cpuCount"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.instanceOptions.cpus')"
:suffix="t('suffix.cores')"
/>
</div>
<div class="col span-6">
<UnitInput
v-model="memorySize"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.instanceOptions.memory')"
:suffix="t('suffix.mib')"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-6">
<UnitInput
v-model="diskSize"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.instanceOptions.disk')"
:suffix="t('suffix.mib')"
/>
</div>
</div>
<div class="row mt-10">
<div class="col" :class="showContentLibrary ? 'span-4' : 'span-6'">
<LabeledSelect
v-model="value.creationType"
:mode="mode"
:options="creationMethods"
:label="t('cluster.machineConfig.vsphere.instanceOptions.creationMethod')"
/>
</div>
<div v-if="showTemplate" class="col span-6">
<LabeledSelect
v-model="value.cloneFrom"
:loading="templatesLoading"
:mode="mode"
:options="templates"
:label="t('cluster.machineConfig.vsphere.instanceOptions.template')"
/>
</div>
<div v-if="showContentLibrary" class="col span-4">
<LabeledSelect
v-model="value.contentLibrary"
:loading="contentLibrariesLoading"
:mode="mode"
:options="contentLibraries"
:label="t('cluster.machineConfig.vsphere.instanceOptions.contentLibrary')"
/>
</div>
<div v-if="showContentLibrary" class="col span-4">
<LabeledSelect
v-model="value.cloneFrom"
:loading="libraryTemplatesLoading"
:mode="mode"
:options="libraryTemplates"
:searchable="true"
:label="t('cluster.machineConfig.vsphere.instanceOptions.libraryTemplate')"
/>
</div>
<div v-if="showVirtualMachines" class="col span-6">
<LabeledSelect
v-model="value.cloneFrom"
:loading="virtualMachinesLoading"
:mode="mode"
:options="virtualMachines"
:label="t('cluster.machineConfig.vsphere.instanceOptions.virtualMachine')"
/>
</div>
<div v-if="showIso" class="col span-6">
<LabeledInput
v-model="value.boot2dockerUrl"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.instanceOptions.osIsoUrl.label')"
:placeholder="t('cluster.machineConfig.vsphere.instanceOptions.osIsoUrl.placeholder')"
/>
</div>
</div>
<div v-if="showIso" class="row mt-10">
<div class="col span-12">
<LabeledInput
v-model="value.cloudinit"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.instanceOptions.cloudInit.label')"
:placeholder="t('cluster.machineConfig.vsphere.instanceOptions.cloudInit.placeholder')"
/>
<p class="text-muted mt-5">
{{ t('cluster.machineConfig.vsphere.instanceOptions.cloudInit.note') }}
</p>
</div>
</div>
<div v-if="showCloudConfigYaml" class="row mt-10">
<div class="col span-12">
<label class="text-label mt-0">{{ t('cluster.machineConfig.vsphere.instanceOptions.cloudConfigYaml') }}</label>
<YamlEditor
ref="yaml-additional"
v-model="value.cloudConfig"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
initial-yaml-values="# Additional Manifest YAML"
class="yaml-editor"
/>
</div>
</div>
<div class="row mt-10">
<div class="col span-12">
<label class="text-label mt-0">
{{ t('cluster.machineConfig.vsphere.networks.label') }}
</label>
<ArrayListSelect v-model="value.network" :options="networks" :array-list-props="{ addLabel: t('cluster.machineConfig.vsphere.networks.add') }" :loading="networksLoading" />
</div>
</div>
<div class="row mt-10">
<div class="col span-12">
<label class="text-label mt-0">
{{ t('cluster.machineConfig.vsphere.guestinfo.label') }}
</label>
<KeyValue v-model="cfgparam" :add-label="t('cluster.machineConfig.vsphere.guestinfo.add')" :key-placeholder="t('cluster.machineConfig.vsphere.guestinfo.keyPlaceholder')" :value-placeholder="t('cluster.machineConfig.vsphere.guestinfo.valuePlaceholder')" :read-allowed="false" />
</div>
</div>
</div>
</Card>
<Card class="m-0 mt-20" :show-highlight-border="false" :show-actions="false">
<h4 slot="title" class="text-default-text mb-5">
{{ t('cluster.machineConfig.vsphere.tags.label') }}
<p class="text-muted text-small">
{{ t('cluster.machineConfig.vsphere.tags.description') }}
</p>
</h4>
<div slot="body">
<ArrayListSelect v-model="value.tag" :options="tags" :array-list-props="{ addLabel: t('cluster.machineConfig.vsphere.tags.addTag') }" :loading="tagsLoading" />
</div>
</Card>
<Card class="m-0 mt-20" :show-highlight-border="false" :show-actions="false">
<h4 slot="title" class="text-default-text mb-5">
{{ t('cluster.machineConfig.vsphere.customAttributes.label') }}
<p class="text-muted text-small">
{{ t('cluster.machineConfig.vsphere.customAttributes.description') }}
</p>
</h4>
<div slot="body">
<KeyValue
v-model="customAttribute"
:key-options="attributeKeys"
:options="tags"
:add-label="t('cluster.machineConfig.vsphere.customAttributes.add')"
:read-allowed="false"
:loading="attributeKeysLoading"
:key-taggable="false"
:key-option-unique="true"
/>
</div>
</Card>
<Card class="m-0 mt-20" :show-highlight-border="false" :show-actions="false">
<h4 slot="title" class="text-default-text mb-5">
{{ t('cluster.machineConfig.vsphere.vAppOptions.label') }}
<p class="text-muted text-small">
{{ t('cluster.machineConfig.vsphere.vAppOptions.description') }}
</p>
</h4>
<div slot="body">
<div class="row mb-10">
<div class="col span-6">
<RadioGroup
v-model="vappMode"
name="restoreMode"
:label="t('cluster.machineConfig.vsphere.vAppOptions.restoreType')"
:options="vAppOptions"
/>
</div>
</div>
<div v-if="showManual" class="row mb-10">
<div class="col span-4">
<LabeledInput
v-model="value.vappTransport"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.vAppOptions.transport.label')"
:tooltip="t('cluster.machineConfig.vsphere.vAppOptions.transport.tooltip')"
:placeholder="t('cluster.machineConfig.vsphere.vAppOptions.transport.placeholder')"
/>
</div>
<div class="col span-4">
<LabeledInput
v-model="value.vappIpprotocol"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.vAppOptions.protocol.label')"
:tooltip="t('cluster.machineConfig.vsphere.vAppOptions.protocol.tooltip')"
:placeholder="t('cluster.machineConfig.vsphere.vAppOptions.protocol.placeholder')"
/>
</div>
<div class="col span-4">
<LabeledInput
v-model="value.vappIpallocationpolicy"
:mode="mode"
:label="t('cluster.machineConfig.vsphere.vAppOptions.allocation.label')"
:tooltip="t('cluster.machineConfig.vsphere.vAppOptions.allocation.tooltip')"
:placeholder="t('cluster.machineConfig.vsphere.vAppOptions.allocation.placeholder')"
/>
</div>
</div>
<div v-if="showManual" class="row">
<div class="col span-12">
<KeyValue
v-model="vappProperty"
:title="t('cluster.machineConfig.vsphere.vAppOptions.properties.label')"
:key-placeholder="t('cluster.machineConfig.vsphere.vAppOptions.properties.keyPlaceholder')"
:value-placeholder="t('cluster.machineConfig.vsphere.vAppOptions.properties.valuePlaceholder')"
:add-label="t('cluster.machineConfig.vsphere.vAppOptions.properties.add')"
:read-allowed="false"
/>
</div>
</div>
</div>
</Card>
</div>
</template>

45
utils/computed.js Normal file
View File

@ -0,0 +1,45 @@
const { set, get } = require('@/utils/object');
/**
* Creates a computed property that handles converting strings to numbers and numbers to strings. Particularly when dealing with UnitInput.
* @param {*} path The path of the real value
* @returns the computed property
*/
export function integerString(path) {
return {
get() {
return Number.parseFloat(get(this, path));
},
set(value) {
set(this, path, value.toString(10));
}
};
}
/**
* Creates a computed property that handles converting strings a list of strings that look like ['key=value'] into { key: value } and back
* @param {*} path The path of the real value
* @param {*} delimeter the character/s used between the key/value. Default value '='.
* @returns the computed property
*/
export function keyValueStrings(path, delimeter = '=') {
return {
get() {
const result = {};
get(this, path).forEach((entry) => {
const [key, value] = entry.split(delimeter);
result[key] = value;
});
return result;
},
set(value) {
const newValue = Object.entries(value).map(([key, value]) => `${ key }${ delimeter }${ value }`);
set(this, path, newValue);
}
};
}