cloudstack/ui/src/views/compute/DeployVM.vue
Nicolas Vazquez 8c8d115a1e
feature: Support Multi-arch Zones (#9619)
This introduces the multi-arch zones, allowing users to select the VM arch upon deployment. 

Multi-arch zone support in CloudStack can allow admins to mix x86_64 & arm64 hosts within the same zone with the following changes proposed:
- All hosts in a clusters need to be homogenous, wrt host CPU type (amd64 vs arm64) and hypevisor
- Arch-aware templates & ISOs:
   -  Add support for a new arch field (default set of: amd64 and arm64), when unspecified defaults to amd64 and for existing templates & iso
   -  Allow admins to edit the arch type of the registered template & iso
- Arch-aware clusters and host:
   - Add new attribute field for cluster and hosts (kvm host agents can automatically report this, arch of the first host of the cluster is cluster's architecture), defaults to amd64 when not specified
   - Allow admins to edit the arch of an existing cluster
- VM deployment form (UI):
   - In a multi-arch zone/env, the VM deployment form can allow some kind of template/iso filtration in the UI
   - Users should be able to select arch: amd64 & arm64; but this is shown only in a multi-arch zone (env)
- VM orchestration and lifecycle operations:
   - Use of VM/template's arch to correctly decide where to provision the VM (on the correct strictly arch-matching host/clusters) & other lifecycle operations (such as migration from/to arch-matching hosts)

Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
2024-09-06 12:14:54 +05:30

2961 lines
119 KiB
Vue

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<a-row :gutter="12">
<a-col :md="24" :lg="17">
<a-card :bordered="true" :title="$t('label.newinstance')">
<a-form
v-ctrl-enter="handleSubmit"
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
layout="vertical"
>
<a-steps direction="vertical" size="small">
<a-step
v-if="!isNormalUserOrProject"
:title="this.$t('label.assign.instance.another')">
<template #description>
<div style="margin-top: 15px">
{{ $t('label.assigning.vms') }}
<ownership-selection
@fetch-owner="fetchOwnerOptions"/>
</div>
</template>
</a-step>
<a-step :title="$t('label.select.deployment.infrastructure')" status="process">
<template #description>
<div style="margin-top: 15px">
<span>{{ $t('message.select.a.zone') }}</span><br/>
<a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
<div v-if="zones.length <= 8">
<a-row type="flex" :gutter="[16, 18]" justify="start">
<div v-for="(zoneItem, idx) in zones" :key="idx">
<a-radio-group
:key="idx"
:size="large"
v-model:value="form.zoneid"
@change="onSelectZoneId(zoneItem.id)">
<a-col :span="6">
<a-radio-button
:value="zoneItem.id"
style="border-width: 2px"
class="zone-radio-button">
<span>
<resource-icon
v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
:image="zoneItem.icon.base64image"
size="2x" />
<global-outlined size="2x" v-else />
{{ zoneItem.name }}
</span>
</a-radio-button>
</a-col>
</a-radio-group>
</div>
</a-row>
</div>
<a-select
v-else
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="onSelectZoneId"
:loading="loading.zones"
v-focus="true"
>
<a-select-option v-for="zone1 in zones" :key="zone1.id" :label="zone1.name">
<span>
<resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="2x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px" />
{{ zone1.name }}
</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.podid')"
name="podid"
ref="podid">
<a-select
v-model:value="form.podid"
showSearch
optionFilterProp="label"
:filterOption="filterOption"
:options="podSelectOptions"
:loading="loading.pods"
@change="onSelectPodId"
></a-select>
</a-form-item>
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.clusterid')"
name="clusterid"
ref="clusterid">
<a-select
v-model:value="form.clusterid"
showSearch
optionFilterProp="label"
:filterOption="filterOption"
:options="clusterSelectOptions"
:loading="loading.clusters"
@change="onSelectClusterId"
></a-select>
</a-form-item>
<a-form-item
v-if="!isNormalAndDomainUser"
:label="$t('label.hostid')"
name="hostid"
ref="hostid">
<a-select
v-model:value="form.hostid"
showSearch
optionFilterProp="label"
:filterOption="filterOption"
:options="hostSelectOptions"
:loading="loading.hosts"
@change="onSelectHostId"
></a-select>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.templateiso')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
<a-card
:tabList="tabList"
:activeTabKey="tabKey"
@tabChange="key => onTabChange(key, 'tabKey')">
<div v-if="tabKey === 'templateid'">
{{ $t('message.template.desc') }}
<div v-if="isZoneSelectedMultiArch" style="width: 100%; margin-top: 5px">
{{ $t('message.template.arch') }}
<a-select
style="width: 100%"
v-model:value="selectedArchitecture"
:defaultValue="architectureTypes.opts[0].id"
@change="arch => changeArchitecture(arch, true)">
<a-select-option v-for="opt in architectureTypes.opts" :key="opt.id">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</div>
<template-iso-selection
input-decorator="templateid"
:items="options.templates"
:selected="tabKey"
:loading="loading.templates"
:preFillContent="dataPreFill"
:key="templateKey"
@handle-search-filter="($event) => fetchAllTemplates($event)"
@update-template-iso="updateFieldValue" />
<div>
{{ $t('label.override.rootdisk.size') }}
<a-switch
v-model:checked="form.rootdisksizeitem"
:disabled="rootDiskSizeFixed > 0 || template.deployasis || showOverrideDiskOfferingOption"
@change="val => { showRootDiskSizeChanger = val }"
style="margin-left: 10px;"/>
<div v-if="template.deployasis"> {{ $t('message.deployasis') }} </div>
</div>
<disk-size-selection
v-if="showRootDiskSizeChanger"
input-decorator="rootdisksize"
:preFillContent="dataPreFill"
:isCustomized="true"
:minDiskSize="dataPreFill.minrootdisksize"
@update-disk-size="updateFieldValue"
style="margin-top: 10px;"/>
</div>
<div v-else>
{{ $t('message.iso.desc') }}
<div v-if="isZoneSelectedMultiArch" style="width: 100%; margin-top: 5px">
{{ $t('message.iso.arch') }}
<a-select
style="width: 100%"
v-model:value="selectedArchitecture"
:defaultValue="architectureTypes.opts[0].id"
@change="arch => changeArchitecture(arch, false)">
<a-select-option v-for="opt in architectureTypes.opts" :key="opt.id">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</div>
<template-iso-selection
input-decorator="isoid"
:items="options.isos"
:selected="tabKey"
:loading="loading.isos"
:preFillContent="dataPreFill"
@handle-search-filter="($event) => fetchAllIsos($event)"
@update-template-iso="updateFieldValue" />
<a-form-item :label="$t('label.hypervisor')">
<a-select
v-model:value="form.hypervisor"
:preFillContent="dataPreFill"
:options="hypervisorSelectOptions"
@change="value => hypervisor = value"
showSearch
optionFilterProp="label"
:filterOption="filterOption" />
</a-form-item>
</div>
</a-card>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.templateid" />
</a-form-item>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.isoid" />
</a-form-item>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.rootdisksize" />
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.serviceofferingid')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
<a-form-item v-if="zoneSelected && templateConfigurationExists" name="templateConfiguration" ref="templateConfiguration">
<template #label>
<tooltip-label :title="$t('label.configuration')" :tooltip="$t('message.ovf.configurations')"/>
</template>
<a-select
showSearch
optionFilterProp="label"
v-model:value="form.templateConfiguration"
defaultActiveFirstOption
:placeholder="$t('message.ovf.configurations')"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="onSelectTemplateConfigurationId"
>
<a-select-option v-for="opt in templateConfigurations" :key="opt.id" :label="opt.name || opt.description">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
<span v-if="selectedTemplateConfiguration && selectedTemplateConfiguration.description">{{ selectedTemplateConfiguration.description }}</span>
</a-form-item>
<compute-offering-selection
:compute-items="options.serviceOfferings"
:selected-template="template ? template : {}"
:row-count="rowCount.serviceOfferings"
:zoneId="zoneId"
:value="serviceOffering ? serviceOffering.id : ''"
:loading="loading.serviceOfferings"
:preFillContent="dataPreFill"
:minimum-cpunumber="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpunumber ? selectedTemplateConfiguration.cpunumber : 0"
:minimum-cpuspeed="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpuspeed ? selectedTemplateConfiguration.cpuspeed : 0"
:minimum-memory="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.memory ? selectedTemplateConfiguration.memory : 0"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && (serviceOffering.iscustomized || serviceOffering.iscustomizediops)"
cpuNumberInputDecorator="cpunumber"
cpuSpeedInputDecorator="cpuspeed"
memoryInputDecorator="memory"
:preFillContent="dataPreFill"
:computeOfferingId="instanceConfig.computeofferingid"
:isConstrained="isOfferingConstrained(serviceOffering)"
:minCpu="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.mincpunumber*1 : 0"
:maxCpu="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.maxcpunumber*1 : Number.MAX_SAFE_INTEGER"
:minMemory="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.minmemory*1 : 0"
:maxMemory="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.maxmemory*1 : Number.MAX_SAFE_INTEGER"
:isCustomized="serviceOffering.iscustomized"
:isCustomizedIOps="'iscustomizediops' in serviceOffering && serviceOffering.iscustomizediops"
@handler-error="handlerError"
@update-iops-value="updateIOPSValue"
@update-compute-cpunumber="updateFieldValue"
@update-compute-cpuspeed="updateFieldValue"
@update-compute-memory="updateFieldValue" />
<span v-if="serviceOffering && serviceOffering.iscustomized">
<a-form-item name="cpunumber" ref="cpunumber" class="form-item-hidden">
<a-input v-model:value="form.cpunumber"/>
</a-form-item>
<a-form-item
class="form-item-hidden"
v-if="(serviceOffering && !(serviceOffering.cpuspeed > 0))"
name="cpuspeed"
ref="cpuspeed">
<a-input v-model:value="form.cpuspeed"/>
</a-form-item>
<a-form-item class="form-item-hidden" name="memory" ref="memory">
<a-input v-model:value="form.memory"/>
</a-form-item>
</span>
<span v-if="tabKey!=='isoid'">
{{ $t('label.override.root.diskoffering') }}
<a-switch
v-model:checked="showOverrideDiskOfferingOption"
:checked="serviceOffering && !serviceOffering.diskofferingstrictness && showOverrideDiskOfferingOption"
:disabled="(serviceOffering && serviceOffering.diskofferingstrictness)"
@change="val => { updateOverrideRootDiskShowParam(val) }"
style="margin-left: 10px;"/>
</span>
<span v-if="tabKey!=='isoid' && serviceOffering && !serviceOffering.diskofferingstrictness">
<a-step
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
:items="template.childtemplates"
:diskOfferings="options.diskOfferings"
:zoneId="zoneId"
@select-multi-disk-offering="updateMultiDiskOffering($event)" />
</div>
</template>
</a-step>
<a-step
v-else
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
<disk-offering-selection
v-if="showOverrideDiskOfferingOption"
:items="options.diskOfferings"
:row-count="rowCount.diskOfferings"
:zoneId="zoneId"
:value="overrideDiskOffering ? overrideDiskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
:isRootDiskOffering="true"
@on-selected-root-disk-size="onSelectRootDiskSize"
@select-disk-offering-item="($event) => updateOverrideDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-selection>
<disk-size-selection
v-if="overrideDiskOffering && (overrideDiskOffering.iscustomized || overrideDiskOffering.iscustomizediops)"
input-decorator="rootdisksize"
:preFillContent="dataPreFill"
:minDiskSize="dataPreFill.minrootdisksize"
:rootDiskSelected="overrideDiskOffering"
:isCustomized="overrideDiskOffering.iscustomized"
@handler-error="handlerError"
@update-disk-size="updateFieldValue"
@update-root-disk-iops-value="updateIOPSValue"/>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.rootdisksize"/>
</a-form-item>
</div>
</template>
</a-step>
</span>
</div>
</template>
</a-step>
<a-step
:title="$t('label.data.disk')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
:items="template.childtemplates"
:diskOfferings="options.diskOfferings"
:zoneId="zoneId"
@select-multi-disk-offering="updateMultiDiskOffering($event)" />
</div>
</template>
</a-step>
<a-step
v-else
:title="tabKey === 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
<disk-offering-selection
:items="options.diskOfferings"
:row-count="rowCount.diskOfferings"
:zoneId="zoneId"
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
@on-selected-disk-size="onSelectDiskSize"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-selection>
<disk-size-selection
v-if="diskOffering && (diskOffering.iscustomized || diskOffering.iscustomizediops)"
input-decorator="size"
:preFillContent="dataPreFill"
:diskSelected="diskSelected"
:isCustomized="diskOffering.iscustomized"
@handler-error="handlerError"
@update-disk-size="updateFieldValue"
@update-iops-value="updateIOPSValue"/>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.size"/>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.networks')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="zone && zone.networktype !== 'Basic'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 5px">
<div style="margin-bottom: 10px">
{{ $t('message.network.selection') + ('createNetwork' in $store.getters.apis ? ' ' + $t('message.network.selection.new.network') : '') }}
</div>
<div v-if="vm.templateid && templateNics && templateNics.length > 0">
<instance-nics-network-select-list-view
:nics="templateNics"
:zoneid="selectedZone"
@select="handleNicsNetworkSelection" />
</div>
<div v-show="!(vm.templateid && templateNics && templateNics.length > 0)" >
<network-selection
:items="options.networks"
:row-count="rowCount.networks"
:value="networkOfferingIds"
:loading="loading.networks"
:zoneId="zoneId"
:preFillContent="dataPreFill"
@select-network-item="($event) => updateNetworks($event)"
@handle-search-filter="($event) => handleSearchFilter('networks', $event)"
></network-selection>
<network-configuration
v-if="networks.length > 0"
:items="networks"
:preFillContent="dataPreFill"
@update-network-config="($event) => updateNetworkConfig($event)"
@handler-error="($event) => hasError = $event"
@select-default-network-item="($event) => updateDefaultNetworks($event)"
></network-configuration>
</div>
</div>
</template>
</a-step>
<a-step
v-if="showSecurityGroupSection"
:title="$t('label.security.groups')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<security-group-selection
:zoneId="zoneId"
:value="securitygroupids"
:loading="loading.networks"
:preFillContent="dataPreFill"
@select-security-group-item="($event) => updateSecurityGroups($event)"></security-group-selection>
</template>
</a-step>
<a-step
v-if="isUserAllowedToListSshKeys"
:title="$t('label.sshkeypairs')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
<ssh-key-pair-selection
:items="options.sshKeyPairs"
:row-count="rowCount.sshKeyPairs"
:zoneId="zoneId"
:value="sshKeyPairs"
:loading="loading.sshKeyPairs"
:preFillContent="dataPreFill"
@select-ssh-key-pair-item="($event) => updateSshKeyPairs($event)"
@handle-search-filter="($event) => handleSearchFilter('sshKeyPairs', $event)"
/>
</div>
</template>
</a-step>
<a-step
:title="$t('label.ovf.properties')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="vm.templateid && templateProperties && Object.keys(templateProperties).length > 0">
<template #description>
<div v-for="(props, category) in templateProperties" :key="category">
<a-alert :message="'Category: ' + category + ' (' + props.length + ' properties)'" type="info" />
<div style="margin-left: 15px; margin-top: 10px">
<a-form-item
v-for="(property, propertyIndex) in props"
:key="propertyIndex"
:v-bind="property.key"
:name="'properties.' + escapePropertyKey(property.key)"
:ref="'properties.' + escapePropertyKey(property.key)">
<tooltip-label style="text-transform: capitalize" :title="property.label" :tooltip="property.description"/>
<span v-if="property.type && property.type==='boolean'">
<a-switch
v-model:checked="form.properties[escapePropertyKey(property.key)]"
:placeholder="property.description"
/>
</span>
<span v-else-if="property.type && (property.type==='int' || property.type==='real')">
<a-input-number
v-model:value="form.properties[escapePropertyKey(property.key)]"
:placeholder="property.description"
:min="getPropertyQualifiers(property.qualifiers, 'number-select').min"
:max="getPropertyQualifiers(property.qualifiers, 'number-select').max" />
</span>
<span v-else-if="property.type && property.type==='string' && property.qualifiers && property.qualifiers.startsWith('ValueMap')">
<a-select
showSearch
optionFilterProp="label"
v-model:value="form.properties[escapePropertyKey(property.key)]"
:placeholder="property.description"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
>
<a-select-option v-for="opt in getPropertyQualifiers(property.qualifiers, 'select')" :key="opt">
{{ opt }}
</a-select-option>
</a-select>
</span>
<span v-else-if="property.type && property.type==='string' && property.password">
<a-input-password
v-model:value="form.properties[escapePropertyKey(property.key)]"
:placeholder="property.description" />
</span>
<span v-else>
<a-input
v-model:value="form.properties[escapePropertyKey(property.key)]"
:placeholder="property.description" />
</span>
</a-form-item>
</div>
</div>
</template>
</a-step>
<a-step
:title="$t('label.advanced.mode')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description v-if="zoneSelected">
<span>
{{ $t('label.isadvanced') }}
<a-switch v-model:checked="showDetails" style="margin-left: 10px"/>
</span>
<div style="margin-top: 15px" v-if="showDetails">
<div
v-if="vm.templateid && ['KVM', 'VMware', 'XenServer'].includes(hypervisor) && !template.deployasis">
<a-form-item :label="$t('label.boottype')" name="boottype" ref="boottype">
<a-select
v-model:value="form.boottype"
@change="onBootTypeChange"
showSearch
optionFilterProp="label"
:filterOption="filterOption">
<a-select-option v-for="bootType in options.bootTypes" :key="bootType.id" :label="bootType.description">
{{ bootType.description }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('label.bootmode')" name="bootmode" ref="bootmode">
<a-select
v-model:value="form.bootmode"
showSearch
optionFilterProp="label"
:filterOption="filterOption">
<a-select-option v-for="bootMode in options.bootModes" :key="bootMode.id" :label="bootMode.description">
{{ bootMode.description }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<a-form-item
:label="$t('label.bootintosetup')"
v-if="zoneSelected && ((tabKey === 'isoid' && hypervisor === 'VMware') || (tabKey === 'templateid' && template && template.hypervisor === 'VMware'))"
name="bootintosetup"
ref="bootintosetup">
<a-switch v-model:checked="form.bootintosetup" />
</a-form-item>
<a-form-item name="dynamicscalingenabled" ref="dynamicscalingenabled">
<template #label>
<tooltip-label :title="$t('label.dynamicscalingenabled')" :tooltip="$t('label.dynamicscalingenabled.tooltip')"/>
</template>
<a-form-item name="dynamicscalingenabled" ref="dynamicscalingenabled">
<a-switch
v-model:checked="form.dynamicscalingenabled"
:checked="isDynamicallyScalable() && dynamicscalingenabled"
:disabled="!isDynamicallyScalable()"
@change="val => { dynamicscalingenabled = val }"/>
</a-form-item>
</a-form-item>
<a-form-item :label="$t('label.userdata')">
<a-card>
<div v-if="this.template && this.template.userdataid">
<a-text type="primary">
Userdata "{{ $t(this.template.userdataname) }}" is linked with template "{{ $t(this.template.name) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
</a-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
<a-input-group>
<a-table
size="small"
style="overflow-y: auto"
:columns="userDataParamCols"
:dataSource="templateUserDataParams"
:pagination="false"
:rowKey="record => record.key">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'value'">
<a-input v-model:value="templateUserDataValues[record.key]" />
</template>
</template>
</a-table>
</a-input-group>
</div>
</div>
<div v-if="this.iso && this.iso.userdataid">
<a-text type="primary">
Userdata "{{ $t(this.iso.userdataname) }}" is linked with ISO "{{ $t(this.iso.name) }}" with override policy "{{ $t(this.iso.userdatapolicy) }}"
</a-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.iso && this.iso.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
<a-input-group>
<a-table
size="small"
style="overflow-y: auto"
:columns="userDataParamCols"
:dataSource="templateUserDataParams"
:pagination="false"
:rowKey="record => record.key">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'value'">
<a-input v-model:value="templateUserDataValues[record.key]" />
</template>
</template>
</a-table>
</a-input-group>
</div>
</div><br/><br/>
<div v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE' || userdataDefaultOverridePolicy === 'APPEND' || !userdataDefaultOverridePolicy">
<span v-if="userdataDefaultOverridePolicy === 'ALLOWOVERRIDE'" >
{{ $t('label.userdata.do.override') }}
<a-switch v-model:checked="doUserdataOverride" style="margin-left: 10px"/>
</span>
<span v-if="userdataDefaultOverridePolicy === 'APPEND'">
{{ $t('label.userdata.do.append') }}
<a-switch v-model:checked="doUserdataAppend" style="margin-left: 10px"/>
</span>
<a-step
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="doUserdataOverride || doUserdataAppend || !userdataDefaultOverridePolicy" style="margin-top: 15px">
<a-card
:tabList="userdataTabList"
:activeTabKey="userdataTabKey"
@tabChange="key => onUserdataTabChange(key, 'userdataTabKey')">
<div v-if="userdataTabKey === 'userdataregistered'">
<a-step
v-if="isUserAllowedToListUserDatas"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
<user-data-selection
:items="options.userDatas"
:row-count="rowCount.userDatas"
:zoneId="zoneId"
:disabled="template.userdatapolicy === 'DENYOVERRIDE'"
:loading="loading.userDatas"
:preFillContent="dataPreFill"
@select-user-data-item="($event) => updateUserData($event)"
@handle-search-filter="($event) => handleSearchFilter('userData', $event)"
/>
<div v-if="userDataParams.length > 0">
<a-input-group>
<a-table
size="small"
style="overflow-y: auto"
:columns="userDataParamCols"
:dataSource="userDataParams"
:pagination="false"
:rowKey="record => record.key">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'value'">
<a-input v-model:value="userDataValues[record.key]" />
</template>
</template>
</a-table>
</a-input-group>
</div>
</div>
</template>
</a-step>
</div>
<div v-else>
<a-form-item name="userdata" ref="userdata" >
<a-textarea
placeholder="Userdata"
v-model:value="form.userdata">
</a-textarea>
</a-form-item>
</div>
</a-card>
</div>
</template>
</a-step>
</div>
</a-card>
</a-form-item>
<a-form-item :label="$t('label.affinity.groups')">
<affinity-group-selection
:items="options.affinityGroups"
:row-count="rowCount.affinityGroups"
:zoneId="zoneId"
:value="affinityGroupIds"
:loading="loading.affinityGroups"
:preFillContent="dataPreFill"
@select-affinity-group-item="($event) => updateAffinityGroups($event)"
@handle-search-filter="($event) => handleSearchFilter('affinityGroups', $event)"/>
</a-form-item>
<a-form-item name="nicmultiqueuenumber" ref="nicmultiqueuenumber" v-if="vm.templateid && ['KVM'].includes(hypervisor)">
<template #label>
<tooltip-label :title="$t('label.nicmultiqueuenumber')" :tooltip="$t('label.nicmultiqueuenumber.tooltip')"/>
</template>
<a-input-number
style="width: 100%;"
v-model:value="form.nicmultiqueuenumber" />
</a-form-item>
<a-form-item name="nicpackedvirtqueuesenabled" ref="nicpackedvirtqueuesenabled" v-if="vm.templateid && ['KVM'].includes(hypervisor)">
<template #label>
<tooltip-label :title="$t('label.nicpackedvirtqueuesenabled')" :tooltip="$t('label.nicpackedvirtqueuesenabled.tooltip')"/>
</template>
<a-switch
v-model:checked="form.nicpackedvirtqueuesenabled"
:checked="nicpackedvirtqueuesenabled"
@change="val => { nicpackedvirtqueuesenabled = val }"/>
</a-form-item>
<a-form-item name="iothreadsenabled" ref="iothreadsenabled" v-if="vm.templateid && ['KVM'].includes(hypervisor)">
<template #label>
<tooltip-label :title="$t('label.iothreadsenabled')" :tooltip="$t('label.iothreadsenabled.tooltip')"/>
</template>
<a-form-item name="iothreadsenabled" ref="iothreadsenabled">
<a-switch
v-model:checked="form.iothreadsenabled"
:checked="iothreadsenabled"
@change="val => { iothreadsenabled = val }"/>
</a-form-item>
</a-form-item>
<a-form-item name="iodriverpolicy" ref="iodriverpolicy" v-if="vm.templateid && ['KVM'].includes(hypervisor)">
<template #label>
<tooltip-label :title="$t('label.iodriverpolicy')" :tooltip="$t('label.iodriverpolicy.tooltip')"/>
</template>
<a-select
v-model:value="form.iodriverpolicy"
optionFilterProp="label"
:filterOption="filterOption">
<a-select-option v-for="iodriverpolicy in options.ioPolicyTypes" :key="iodriverpolicy.id" :label="iodriverpolicy.description">
{{ iodriverpolicy.description }}
</a-select-option>
</a-select>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.details')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description v-if="zoneSelected">
<div style="margin-top: 15px">
{{ $t('message.vm.review.launch') }}
<a-form-item :label="$t('label.name.optional')" name="name" ref="name">
<a-input v-model:value="form.name" />
</a-form-item>
<a-form-item :label="$t('label.group.optional')" name="group" ref="group">
<a-auto-complete
v-model:value="form.group"
:filterOption="filterOption"
:options="options.instanceGroups" />
</a-form-item>
<a-form-item :label="$t('label.keyboard')" name="keyboard" ref="keyboard">
<a-select
v-model:value="form.keyboard"
:options="keyboardSelectOptions"
showSearch
optionFilterProp="label"
:filterOption="filterOption"
></a-select>
</a-form-item>
<a-form-item :label="$t('label.action.start.instance')" name="startvm" ref="startvm">
<a-switch v-model:checked="form.startvm" />
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.license.agreements')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="vm.templateid && templateLicenses && templateLicenses.length > 0">
<template #description>
<div style="margin-top: 10px">
{{ $t('message.read.accept.license.agreements') }}
<a-form-item
style="margin-top: 10px"
v-for="(license, licenseIndex) in templateLicenses"
:key="licenseIndex"
:v-bind="license.id">
<template #label>
<tooltip-label style="text-transform: capitalize" :title="$t('label.agreement' + ' ' + (licenseIndex+1) + ': ' + license.name)"/>
</template>
<a-textarea
v-model:value="license.text"
:auto-size="{ minRows: 3, maxRows: 8 }"
readOnly />
<a-checkbox
style="margin-top: 10px"
v-model:checked="form.licensesaccepted">
{{ $t('label.i.accept.all.license.agreements') }}
</a-checkbox>
</a-form-item>
</div>
</template>
</a-step>
</a-steps>
<div class="card-footer">
<a-form-item name="stayonpage" ref="stayonpage">
<a-switch
class="form-item-hidden"
v-model:checked="form.stayonpage" />
</a-form-item>
<!-- ToDo extract as component -->
<a-button @click="() => $router.back()" :disabled="loading.deploy">
{{ $t('label.cancel') }}
</a-button>
<a-dropdown-button style="margin-left: 10px" type="primary" ref="submit" @click="handleSubmit" :loading="loading.deploy">
<rocket-outlined />
{{ this.form.startvm ? $t('label.launch.vm') : $t('label.create.vm') }}
<template #icon><down-outlined /></template>
<template #overlay>
<a-menu type="primary" @click="handleSubmitAndStay" theme="dark" class="btn-stay-on-page">
<a-menu-item type="primary" key="1">
<rocket-outlined />
{{ this.form.startvm ? $t('label.launch.vm.and.stay') : $t('label.create.vm.and.stay') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</div>
</a-form>
</a-card>
</a-col>
<a-col :md="24" :lg="7" v-if="!isMobile()">
<a-affix :offsetTop="75" class="vm-info-card">
<info-card :resource="vm" :title="$t('label.yourinstance')" @change-resource="(data) => resource = data" />
</a-affix>
</a-col>
</a-row>
</div>
</template>
<script>
import { ref, reactive, toRaw, nextTick, h } from 'vue'
import { Button } from 'ant-design-vue'
import { api } from '@/api'
import _ from 'lodash'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import store from '@/store'
import eventBus from '@/config/eventBus'
import OwnershipSelection from '@views/compute/wizard/OwnershipSelection'
import InfoCard from '@/components/view/InfoCard'
import ResourceIcon from '@/components/view/ResourceIcon'
import ComputeOfferingSelection from '@views/compute/wizard/ComputeOfferingSelection'
import ComputeSelection from '@views/compute/wizard/ComputeSelection'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import MultiDiskSelection from '@views/compute/wizard/MultiDiskSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import AffinityGroupSelection from '@views/compute/wizard/AffinityGroupSelection'
import NetworkSelection from '@views/compute/wizard/NetworkSelection'
import NetworkConfiguration from '@views/compute/wizard/NetworkConfiguration'
import SshKeyPairSelection from '@views/compute/wizard/SshKeyPairSelection'
import UserDataSelection from '@views/compute/wizard/UserDataSelection'
import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import InstanceNicsNetworkSelectListView from '@/components/view/InstanceNicsNetworkSelectListView.vue'
export default {
name: 'Wizard',
components: {
OwnershipSelection,
SshKeyPairSelection,
UserDataSelection,
NetworkConfiguration,
NetworkSelection,
AffinityGroupSelection,
TemplateIsoSelection,
DiskSizeSelection,
MultiDiskSelection,
DiskOfferingSelection,
InfoCard,
ComputeOfferingSelection,
ComputeSelection,
SecurityGroupSelection,
ResourceIcon,
TooltipLabel,
InstanceNicsNetworkSelectListView
},
props: {
visible: {
type: Boolean
},
preFillContent: {
type: Object,
default: () => {}
}
},
mixins: [mixin, mixinDevice],
data () {
return {
zoneId: '',
podId: null,
clusterId: null,
zoneSelected: false,
isZoneSelectedMultiArch: false,
dynamicscalingenabled: true,
templateKey: 0,
showRegisteredUserdata: true,
doUserdataOverride: false,
doUserdataAppend: false,
userdataDefaultOverridePolicy: 'ALLOWOVERRIDE',
vm: {
name: null,
zoneid: null,
zonename: null,
hypervisor: null,
templateid: null,
templatename: null,
keyboard: null,
keypairs: [],
group: null,
affinitygroupids: [],
affinitygroup: [],
serviceofferingid: null,
serviceofferingname: null,
ostypeid: null,
ostypename: null,
rootdisksize: null,
disksize: null
},
options: {
templates: {},
isos: {},
hypervisors: [],
serviceOfferings: [],
diskOfferings: [],
zones: [],
affinityGroups: [],
networks: [],
sshKeyPairs: [],
UserDatas: [],
pods: [],
clusters: [],
hosts: [],
groups: [],
keyboards: [],
bootTypes: [],
bootModes: [],
ioPolicyTypes: [],
dynamicScalingVmConfig: false
},
rowCount: {},
loading: {
deploy: false,
templates: false,
isos: false,
hypervisors: false,
serviceOfferings: false,
diskOfferings: false,
affinityGroups: false,
networks: false,
sshKeyPairs: false,
userDatas: false,
zones: false,
pods: false,
clusters: false,
hosts: false,
groups: false
},
owner: {
projectid: store.getters.project?.id,
domainid: store.getters.project?.id ? null : store.getters.userInfo.domainid,
account: store.getters.project?.id ? null : store.getters.userInfo.account
},
instanceConfig: {},
template: {},
defaultBootType: '',
defaultBootMode: '',
templateConfigurations: [],
templateNics: [],
templateLicenses: [],
templateProperties: {},
selectedTemplateConfiguration: {},
iso: {},
hypervisor: '',
serviceOffering: {},
diskOffering: {},
affinityGroups: [],
networks: [],
networksAdd: [],
zone: {},
sshKeyPairs: [],
sshKeyPair: {},
userData: {},
userDataParams: [],
userDataParamCols: [
{
title: this.$t('label.key'),
dataIndex: 'key'
},
{
title: this.$t('label.value'),
dataIndex: 'value',
key: 'value'
}
],
userDataValues: {},
templateUserDataCols: [
{
title: this.$t('label.userdata'),
dataIndex: 'userdata'
},
{
title: this.$t('label.userdatapolicy'),
dataIndex: 'userdataoverridepolicy'
}
],
templateUserDataParams: [],
templateUserDataValues: {},
overrideDiskOffering: {},
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
isoFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
initDataConfig: {},
defaultnetworkid: '',
networkConfig: [],
dataNetworkCreated: [],
tabKey: 'templateid',
userdataTabKey: 'userdataregistered',
dataPreFill: {},
showDetails: false,
showRootDiskSizeChanger: false,
showOverrideDiskOfferingOption: false,
securitygroupids: [],
rootDiskSizeFixed: 0,
hasError: false,
error: false,
diskSelected: {},
rootDiskSelected: {},
diskIOpsMin: 0,
diskIOpsMax: 0,
minIops: 0,
maxIops: 0,
zones: [],
selectedZone: '',
formModel: {},
nicToNetworkSelection: [],
selectedArchitecture: null,
architectureTypes: {
opts: [
{
id: 'x86_64',
description: 'AMD 64 bits (x86_64)'
}, {
id: 'aarch64',
description: 'ARM 64 bits (aarch64)'
}
]
}
}
},
computed: {
rootDiskSize () {
return this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0
},
isNormalAndDomainUser () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype)
},
isNormalUserOrProject () {
return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project.id
},
diskSize () {
const customRootDiskSize = _.get(this.instanceConfig, 'rootdisksize', null)
const customDataDiskSize = _.get(this.instanceConfig, 'size', null)
let computeOfferingDiskSize = _.get(this.serviceOffering, 'rootdisksize', null)
computeOfferingDiskSize = computeOfferingDiskSize > 0 ? computeOfferingDiskSize : null
const diskOfferingDiskSize = _.get(this.diskOffering, 'disksize', null)
const overrideDiskOfferingDiskSize = _.get(this.overrideDiskOffering, 'disksize', null)
let rootDiskSize
let dataDiskSize
if (this.vm.isoid != null) {
rootDiskSize = this.diskOffering?.iscustomized ? customDataDiskSize : diskOfferingDiskSize
} else {
rootDiskSize = this.overrideDiskOffering?.iscustomized ? customRootDiskSize : overrideDiskOfferingDiskSize || computeOfferingDiskSize || this.dataPreFill.minrootdisksize
dataDiskSize = this.diskOffering?.iscustomized ? customDataDiskSize : diskOfferingDiskSize
}
const size = []
if (rootDiskSize) {
size.push(`${rootDiskSize} GB (Root)`)
}
if (dataDiskSize) {
size.push(`${dataDiskSize} GB (Data)`)
}
return size.join(' | ')
},
rootDiskOffering () {
const rootDiskOffering = this.vm.isoid != null ? this.diskOffering : this.overrideDiskOffering
const id = _.get(rootDiskOffering, 'id', null)
const displayText = _.get(rootDiskOffering, 'displaytext', null)
return {
id: id,
displayText: `${displayText} (Root)`
}
},
dataDiskOffering () {
if (this.vm.isoid != null) {
return null
}
const id = _.get(this.diskOffering, 'id', null)
const displayText = _.get(this.diskOffering, 'displaytext', null)
return {
id: id,
displayText: `${displayText} (Data)`
}
},
affinityGroupIds () {
return _.map(this.affinityGroups, 'id')
},
params () {
return {
serviceOfferings: {
list: 'listServiceOfferings',
options: {
zoneid: _.get(this.zone, 'id'),
projectid: this.owner.projectid,
domainid: this.owner.domainid,
account: this.owner.account,
issystem: false,
page: 1,
pageSize: 10,
keyword: undefined
}
},
diskOfferings: {
list: 'listDiskOfferings',
options: {
zoneid: _.get(this.zone, 'id'),
projectid: this.owner.projectid,
domainid: this.owner.domainid,
account: this.owner.account,
page: 1,
pageSize: 10,
keyword: undefined
}
},
zones: {
list: 'listZones',
isLoad: true,
field: 'zoneid'
},
hypervisors: {
list: 'listHypervisors',
options: {
zoneid: _.get(this.zone, 'id')
},
field: 'hypervisor'
},
affinityGroups: {
list: 'listAffinityGroups',
options: {
page: 1,
pageSize: 10,
account: this.owner.account,
domainid: this.owner.domainid,
projectid: this.owner.projectid,
keyword: undefined,
listall: false
}
},
sshKeyPairs: {
list: 'listSSHKeyPairs',
options: {
page: 1,
pageSize: 10,
keyword: undefined,
listall: false
}
},
userDatas: {
list: 'listUserData',
options: {
page: 1,
pageSize: 10,
keyword: undefined,
listall: false
}
},
networks: {
list: 'listNetworks',
options: {
zoneid: _.get(this.zone, 'id'),
canusefordeploy: true,
projectid: store.getters.project.id || this.owner.projectid,
domainid: store.getters.project.id ? null : this.owner.domainid,
account: store.getters.project.id ? null : this.owner.account,
page: 1,
pageSize: 10,
keyword: undefined,
showIcon: true
}
},
pods: {
list: 'listPods',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id')
},
field: 'podid'
},
clusters: {
list: 'listClusters',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id'),
podid: this.podId
},
field: 'clusterid'
},
hosts: {
list: 'listHosts',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id'),
podid: this.podId,
clusterid: this.clusterId,
state: 'Up',
type: 'Routing'
},
field: 'hostid'
},
dynamicScalingVmConfig: {
list: 'listConfigurations',
options: {
zoneid: _.get(this.zone, 'id'),
name: 'enable.dynamic.scale.vm'
}
}
}
},
networkOfferingIds () {
return _.map(this.networks, 'id')
},
zoneSelectOptions () {
return this.options.zones.map((zone) => {
return {
label: zone.name,
value: zone.id
}
})
},
hypervisorSelectOptions () {
return this.options.hypervisors.map((hypervisor) => {
return {
label: hypervisor.name,
value: hypervisor.name
}
})
},
podSelectOptions () {
const options = this.options.pods.map((pod) => {
return {
label: pod.name,
value: pod.id
}
})
options.unshift({
label: this.$t('label.default'),
value: null
})
return options
},
clusterSelectOptions () {
const options = this.options.clusters.map((cluster) => {
return {
label: cluster.name,
value: cluster.id
}
})
options.unshift({
label: this.$t('label.default'),
value: null
})
return options
},
hostSelectOptions () {
const options = this.options.hosts.map((host) => {
return {
label: host.name,
value: host.id
}
})
options.unshift({
label: this.$t('label.default'),
value: null
})
return options
},
keyboardSelectOptions () {
const keyboardOpts = this.$config.keyboardOptions || {}
return Object.keys(keyboardOpts).map((keyboard) => {
return {
label: this.$t(keyboardOpts[keyboard]),
value: keyboard
}
})
},
templateConfigurationExists () {
return this.vm.templateid && this.templateConfigurations && this.templateConfigurations.length > 0
},
templateId () {
return this.$route.query.templateid || null
},
isoId () {
return this.$route.query.isoid || null
},
networkId () {
return this.$route.query.networkid || null
},
tabList () {
let tabList = []
if (this.templateId) {
tabList = [{
key: 'templateid',
tab: this.$t('label.templates')
}]
} else if (this.isoId) {
tabList = [{
key: 'isoid',
tab: this.$t('label.isos')
}]
} else {
tabList = [{
key: 'templateid',
tab: this.$t('label.templates')
},
{
key: 'isoid',
tab: this.$t('label.isos')
}]
}
return tabList
},
userdataTabList () {
let tabList = []
tabList = [{
key: 'userdataregistered',
tab: this.$t('label.userdata.registered')
},
{
key: 'userdatatext',
tab: this.$t('label.userdata.text')
}]
return tabList
},
showSecurityGroupSection () {
if (this.networks.length < 1) {
return false
}
for (const network of this.options.networks) {
if (this.form.networkids && this.form.networkids.includes(network.id)) {
for (const service of network.service) {
if (service.name === 'SecurityGroup') {
return true
}
}
}
}
return false
},
isUserAllowedToListSshKeys () {
return Boolean('listSSHKeyPairs' in this.$store.getters.apis)
},
isUserAllowedToListUserDatas () {
return Boolean('listUserData' in this.$store.getters.apis)
},
dynamicScalingVmConfigValue () {
return this.options.dynamicScalingVmConfig?.[0]?.value === 'true'
},
isCustomizedDiskIOPS () {
return this.diskSelected?.iscustomizediops || false
},
isCustomizedIOPS () {
return this.rootDiskSelected?.iscustomizediops || this.serviceOffering?.iscustomizediops || false
}
},
watch: {
'$route' (to, from) {
if (to.name === 'deployVirtualMachine') {
this.resetData()
}
},
formModel: {
deep: true,
handler (instanceConfig) {
this.instanceConfig = toRaw(instanceConfig)
Object.keys(instanceConfig).forEach(field => {
this.vm[field] = this.instanceConfig[field]
})
this.template = ''
for (const key in this.options.templates) {
var template = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === instanceConfig.templateid)
if (template) {
this.template = template
break
}
}
this.iso = ''
for (const key in this.options.isos) {
var iso = _.find(_.get(this.options.isos[key], 'iso', []), (option) => option.id === instanceConfig.isoid)
if (iso) {
this.iso = iso
break
}
}
if (instanceConfig.hypervisor) {
var hypervisorItem = _.find(this.options.hypervisors, (option) => option.name === instanceConfig.hypervisor)
this.hypervisor = hypervisorItem ? hypervisorItem.name : null
}
this.serviceOffering = _.find(this.options.serviceOfferings, (option) => option.id === instanceConfig.computeofferingid)
instanceConfig.overridediskofferingid = this.rootDiskSelected?.id || this.serviceOffering?.diskofferingid
if (instanceConfig.overridediskofferingid) {
this.overrideDiskOffering = _.find(this.options.diskOfferings, (option) => option.id === instanceConfig.overridediskofferingid)
} else {
this.overrideDiskOffering = null
}
if (iso && this.serviceOffering?.diskofferingid) {
this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === this.serviceOffering.diskofferingid)
} else if (!iso && this.diskSelected) {
this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === instanceConfig.diskofferingid)
}
this.zone = _.find(this.options.zones, (option) => option.id === this.instanceConfig.zoneid)
this.affinityGroups = _.filter(this.options.affinityGroups, (option) => _.includes(instanceConfig.affinitygroupids, option.id))
this.networks = this.getSelectedNetworksWithExistingConfig(_.filter(this.options.networks, (option) => _.includes(instanceConfig.networkids, option.id)))
this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === instanceConfig.diskofferingid)
this.sshKeyPair = _.find(this.options.sshKeyPairs, (option) => option.name === instanceConfig.keypair)
if (this.zone) {
this.vm.zoneid = this.zone.id
this.vm.zonename = this.zone.name
}
const pod = _.find(this.options.pods, (option) => option.id === instanceConfig.podid)
if (pod) {
this.vm.podid = pod.id
this.vm.podname = pod.name
}
const cluster = _.find(this.options.clusters, (option) => option.id === instanceConfig.clusterid)
if (cluster) {
this.vm.clusterid = cluster.id
this.vm.clustername = cluster.name
}
const host = _.find(this.options.hosts, (option) => option.id === instanceConfig.hostid)
if (host) {
this.vm.hostid = host.id
this.vm.hostname = host.name
}
if (this.diskSize) {
this.vm.disksizetotalgb = this.diskSize
} else {
this.vm.disksizetotalgb = null
}
if (this.networks) {
this.vm.networks = this.networks
this.vm.defaultnetworkid = this.defaultnetworkid
}
if (this.template) {
this.vm.templateid = this.template.id
this.vm.templatename = this.template.displaytext
this.vm.ostypeid = this.template.ostypeid
this.vm.ostypename = this.template.ostypename
}
if (this.iso) {
this.vm.isoid = this.iso.id
this.vm.templateid = this.iso.id
this.vm.templatename = this.iso.displaytext
this.vm.ostypeid = this.iso.ostypeid
this.vm.ostypename = this.iso.ostypename
if (this.hypervisor) {
this.vm.hypervisor = this.hypervisor
}
}
if (this.serviceOffering) {
this.vm.serviceofferingid = this.serviceOffering.id
this.vm.serviceofferingname = this.serviceOffering.displaytext
if (this.serviceOffering.cpunumber) {
this.vm.cpunumber = this.serviceOffering.cpunumber
}
if (this.serviceOffering.cpuspeed) {
this.vm.cpuspeed = this.serviceOffering.cpuspeed
}
if (this.serviceOffering.memory) {
this.vm.memory = this.serviceOffering.memory
}
}
if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
this.vm.diskofferingid = ''
this.vm.diskofferingname = ''
this.vm.diskofferingsize = ''
} else if (this.diskOffering) {
this.vm.diskofferingid = this.diskOffering.id
this.vm.diskofferingname = this.diskOffering.displaytext
this.vm.diskofferingsize = this.diskOffering.disksize
}
this.vm.rootdiskofferingid = this.rootDiskOffering?.id
this.vm.rootdiskofferingdisplaytext = this.rootDiskOffering?.displayText
this.vm.datadiskofferingid = this.dataDiskOffering?.id
this.vm.datadiskofferingdisplaytext = this.dataDiskOffering?.displayText
if (this.affinityGroups) {
this.vm.affinitygroup = this.affinityGroups
}
if (this.sshKeyPairs && this.sshKeyPairs.length > 0) {
this.vm.keypairs = this.sshKeyPairs
}
}
}
},
serviceOffering (oldValue, newValue) {
if (oldValue && newValue && oldValue.id !== newValue.id) {
this.dynamicscalingenabled = this.isDynamicallyScalable()
}
},
template (oldValue, newValue) {
if (oldValue && newValue && oldValue.id !== newValue.id) {
this.dynamicscalingenabled = this.isDynamicallyScalable()
this.doUserdataOverride = false
this.doUserdataAppend = false
}
},
created () {
this.initForm()
this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {}
this.fetchData()
},
provide () {
return {
vmFetchTemplates: this.fetchAllTemplates,
vmFetchIsos: this.fetchAllIsos,
vmFetchNetworks: this.fetchNetwork
}
},
methods: {
updateTemplateKey () {
this.templateKey += 1
},
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
zoneid: [{ required: true, message: `${this.$t('message.error.select')}` }],
hypervisor: [{ required: true, message: `${this.$t('message.error.select')}` }]
})
if (this.zoneSelected) {
this.form.startvm = true
}
if (this.zone && this.zone.networktype !== 'Basic') {
if (this.zoneSelected && this.vm.templateid && this.templateNics && this.templateNics.length > 0) {
this.templateNics.forEach((nic, nicIndex) => {
this.form['networkMap.nic-' + nic.InstanceID.toString()] = this.options.networks && this.options.networks.length > 0
? this.options.networks[Math.min(nicIndex, this.options.networks.length - 1)].id
: null
})
}
this.updateFormProperties()
if (this.vm.templateid && this.templateLicenses && this.templateLicenses.length > 0) {
this.rules.licensesaccepted = [{
validator: async (rule, value) => {
if (!value) {
return Promise.reject(this.$t('message.license.agreements.not.accepted'))
}
return Promise.resolve()
}
}]
}
}
},
getPropertyQualifiers (qualifiers, type) {
var result = ''
switch (type) {
case 'select':
result = []
if (qualifiers && qualifiers.includes('ValueMap')) {
result = qualifiers.replace('ValueMap', '').substr(1).slice(0, -1).split(',')
for (var i = 0; i < result.length; i++) {
result[i] = result[i].replace(/"/g, '')
}
}
break
case 'number-select':
var min = 0
var max = Number.MAX_SAFE_INTEGER
if (qualifiers) {
var match = qualifiers.match(/MinLen\((\d+)\)/)
if (match) {
min = parseInt(match[1])
}
match = qualifiers.match(/MaxLen\((\d+)\)/)
if (match) {
max = parseInt(match[1])
}
}
result = { min: min, max: max }
break
default:
}
return result
},
fillValue (field) {
this.form[field] = this.dataPreFill[field]
},
fetchZoneByQuery () {
return new Promise(resolve => {
let zones = []
let apiName = ''
const params = {}
if (this.templateId) {
apiName = 'listTemplates'
params.listall = true
params.templatefilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.templateId
} else if (this.isoId) {
params.listall = true
params.isofilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.isoId
apiName = 'listIsos'
} else if (this.networkId) {
params.listall = true
params.id = this.networkId
apiName = 'listNetworks'
}
if (!apiName) return resolve(zones)
api(apiName, params).then(json => {
let objectName
const responseName = [apiName.toLowerCase(), 'response'].join('')
for (const key in json[responseName]) {
if (key === 'count') {
continue
}
objectName = key
break
}
const data = json?.[responseName]?.[objectName] || []
zones = data.map(item => item.zoneid)
return resolve(zones)
}).catch(() => {
return resolve(zones)
})
})
},
async fetchData () {
const zones = await this.fetchZoneByQuery()
if (zones && zones.length === 1) {
this.selectedZone = zones[0]
this.dataPreFill.zoneid = zones[0]
}
if (this.dataPreFill.zoneid) {
this.fetchDataByZone(this.dataPreFill.zoneid)
} else {
this.fetchZones(null, zones)
_.each(this.params, (param, name) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
}
this.fetchBootTypes()
this.fetchBootModes()
this.fetchInstaceGroups()
this.fetchIoPolicyTypes()
nextTick().then(() => {
['name', 'keyboard', 'boottype', 'bootmode', 'userdata', 'iothreadsenabled', 'iodriverpolicy', 'nicmultiqueuenumber', 'nicpackedvirtqueues'].forEach(this.fillValue)
this.form.boottype = this.defaultBootType ? this.defaultBootType : this.options.bootTypes && this.options.bootTypes.length > 0 ? this.options.bootTypes[0].id : undefined
this.form.bootmode = this.defaultBootMode ? this.defaultBootMode : this.options.bootModes && this.options.bootModes.length > 0 ? this.options.bootModes[0].id : undefined
this.instanceConfig = toRaw(this.form)
})
},
isDynamicallyScalable () {
return this.serviceOffering && this.serviceOffering.dynamicscalingenabled && this.template && this.template.isdynamicallyscalable && this.dynamicScalingVmConfigValue
},
isOfferingConstrained (serviceOffering) {
return 'serviceofferingdetails' in serviceOffering && 'mincpunumber' in serviceOffering.serviceofferingdetails &&
'maxmemory' in serviceOffering.serviceofferingdetails && 'maxcpunumber' in serviceOffering.serviceofferingdetails &&
'minmemory' in serviceOffering.serviceofferingdetails
},
updateOverrideRootDiskShowParam (val) {
if (val) {
this.showRootDiskSizeChanger = false
} else {
this.rootDiskSelected = null
this.form.overridediskofferingid = undefined
}
this.showOverrideDiskOfferingOption = val
},
async fetchDataByZone (zoneId) {
this.fillValue('zoneid')
this.options.zones = await this.fetchZones(zoneId)
this.onSelectZoneId(zoneId)
},
fetchBootTypes () {
this.options.bootTypes = [
{ id: 'BIOS', description: 'BIOS' },
{ id: 'UEFI', description: 'UEFI' }
]
},
fetchBootModes (bootType) {
const bootModes = [
{ id: 'LEGACY', description: 'LEGACY' }
]
if (bootType === 'UEFI') {
bootModes.unshift(
{ id: 'SECURE', description: 'SECURE' }
)
}
this.options.bootModes = bootModes
},
fetchIoPolicyTypes () {
this.options.ioPolicyTypes = [
{ id: 'native', description: 'native' },
{ id: 'threads', description: 'threads' },
{ id: 'io_uring', description: 'io_uring' },
{ id: 'storage_specific', description: 'storage_specific' }
]
},
fetchInstaceGroups () {
this.options.instanceGroups = []
api('listInstanceGroups', {
account: this.$store.getters.project?.id ? null : this.$store.getters.userInfo.account,
domainid: this.$store.getters.project?.id ? null : this.$store.getters.userInfo.domainid,
listall: true
}).then(response => {
const groups = response.listinstancegroupsresponse.instancegroup || []
groups.forEach(x => {
this.options.instanceGroups.push({ label: x.name, value: x.name })
})
})
},
fetchNetwork () {
const param = this.params.networks
this.fetchOptions(param, 'networks')
},
resetData () {
this.vm = {
name: null,
zoneid: null,
zonename: null,
hypervisor: null,
templateid: null,
templatename: null,
keyboard: null,
keypair: null,
group: null,
affinitygroupids: [],
affinitygroup: [],
serviceofferingid: null,
serviceofferingname: null,
ostypeid: null,
ostypename: null,
rootdisksize: null,
disksize: null
}
this.zoneSelected = false
this.formRef.value.resetFields()
this.fetchData()
},
updateFieldValue (name, value) {
if (name === 'templateid') {
this.tabKey = 'templateid'
this.form.templateid = value
this.form.isoid = null
this.resetFromTemplateConfiguration()
let template = ''
for (const key in this.options.templates) {
var t = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === value)
if (t) {
this.template = t
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.updateTemplateParameters()
template = t
break
}
}
if (template) {
var size = template.size / (1024 * 1024 * 1024) || 0 // bytes to GB
this.dataPreFill.minrootdisksize = Math.ceil(size)
this.updateTemplateLinkedUserData(template.userdataid)
this.userdataDefaultOverridePolicy = template.userdatapolicy
this.form.dynamicscalingenabled = template.isdynamicallyscalable
this.defaultBootType = template.details?.UEFI ? 'UEFI' : 'BIOS'
this.form.boottype = this.defaultBootType
this.fetchBootModes(this.form.boottype)
this.defaultBootMode = template.details?.UEFI || this.options.bootModes?.[0]?.id || undefined
this.form.bootmode = this.defaultBootMode
this.form.iothreadsenabled = template.details && Object.prototype.hasOwnProperty.call(template.details, 'iothreads')
this.form.iodriverpolicy = template.details?.['io.policy']
this.form.keyboard = template.details?.keyboard
if (template.details['vmware-to-kvm-mac-addresses']) {
this.dataPreFill.macAddressArray = JSON.parse(template.details['vmware-to-kvm-mac-addresses'])
}
}
} else if (name === 'isoid') {
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.tabKey = 'isoid'
this.resetFromTemplateConfiguration()
this.form.isoid = value
this.form.templateid = null
this.updateTemplateLinkedUserData(this.iso.userdataid)
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
this.vm[name] = value
this.form[name] = value
} else {
this.form[name] = value
}
},
updateComputeOffering (id) {
this.form.computeofferingid = id
setTimeout(() => {
this.updateTemplateConfigurationOfferingDetails(id)
}, 500)
},
updateDiskOffering (id) {
if (id === '0') {
this.form.diskofferingid = undefined
return
}
this.form.diskofferingid = id
},
updateOverrideDiskOffering (id) {
if (id === '0') {
this.form.overridediskofferingid = undefined
return
}
this.form.overridediskofferingid = id
},
updateMultiDiskOffering (value) {
this.form.multidiskoffering = value
},
updateAffinityGroups (ids) {
this.form.affinitygroupids = ids
},
updateNetworks (ids) {
this.form.networkids = ids
},
updateDefaultNetworks (id) {
this.defaultnetworkid = id
this.form.defaultnetworkid = id
},
updateNetworkConfig (networks) {
this.networkConfig = networks
},
updateSshKeyPairs (names) {
this.form.keypairs = names
this.sshKeyPairs = names.map((sshKeyPair) => { return sshKeyPair.name })
},
updateUserData (id) {
if (id === '0') {
this.form.userdataid = undefined
return
}
this.form.userdataid = id
this.userDataParams = []
api('listUserData', { id: id }).then(json => {
const resp = json?.listuserdataresponse?.userdata || []
if (resp[0]) {
var params = resp[0].params
if (params) {
var dataParams = params.split(',')
}
var that = this
dataParams.forEach(function (val, index) {
that.userDataParams.push({
id: index,
key: val
})
})
}
})
},
updateTemplateLinkedUserData (id) {
if (id === '0') {
return
}
this.templateUserDataParams = []
api('listUserData', { id: id }).then(json => {
const resp = json.listuserdataresponse.userdata || []
if (resp.length > 0) {
var params = resp[0].params
if (params) {
var dataParams = params.split(',')
}
var that = this
that.templateUserDataParams = []
if (dataParams) {
dataParams.forEach(function (val, index) {
that.templateUserDataParams.push({
id: index,
key: val
})
})
}
}
})
},
escapePropertyKey (key) {
return key.split('.').join('\\002E')
},
updateSecurityGroups (securitygroupids) {
this.securitygroupids = securitygroupids || []
},
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
},
changeArchitecture (arch, isTemplate) {
this.selectedArchitecture = arch
if (isTemplate) {
this.fetchAllTemplates()
} else {
this.fetchAllIsos()
}
},
handleSubmitAndStay (e) {
this.form.stayonpage = true
this.handleSubmit(e.domEvent)
},
handleSubmit (e) {
console.log('wizard submit')
e.preventDefault()
if (this.loading.deploy) return
this.formRef.value.validate().then(async () => {
const values = toRaw(this.form)
if (!values.templateid && !values.isoid) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.template.iso')
})
return
} else if (values.isoid && (!values.diskofferingid || values.diskofferingid === '0')) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.step.3.continue')
})
return
}
if (!values.computeofferingid) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.step.2.continue')
})
return
}
if (this.error) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('error.form.message')
})
return
}
this.loading.deploy = true
let networkIds = []
let deployVmData = {}
// step 1 : select zone
deployVmData.zoneid = values.zoneid
deployVmData.podid = values.podid
deployVmData.clusterid = values.clusterid
deployVmData.hostid = values.hostid
deployVmData.keyboard = values.keyboard
if (!this.template?.deployasis) {
deployVmData.boottype = values.boottype
deployVmData.bootmode = values.bootmode
}
deployVmData.dynamicscalingenabled = values.dynamicscalingenabled
deployVmData.iothreadsenabled = values.iothreadsenabled
deployVmData.iodriverpolicy = values.iodriverpolicy
deployVmData.nicmultiqueuenumber = values.nicmultiqueuenumber
deployVmData.nicpackedvirtqueuesenabled = values.nicpackedvirtqueuesenabled
const isUserdataAllowed = !this.userdataDefaultOverridePolicy || (this.userdataDefaultOverridePolicy === 'ALLOWOVERRIDE' && this.doUserdataOverride) || (this.userdataDefaultOverridePolicy === 'APPEND' && this.doUserdataAppend)
if (isUserdataAllowed && values.userdata && values.userdata.length > 0) {
deployVmData.userdata = this.$toBase64AndURIEncoded(values.userdata)
}
// step 2: select template/iso
if (this.tabKey === 'templateid') {
deployVmData.templateid = values.templateid
values.hypervisor = null
} else {
deployVmData.templateid = values.isoid
}
if (this.showRootDiskSizeChanger && values.rootdisksize && values.rootdisksize > 0) {
deployVmData.rootdisksize = values.rootdisksize
} else if (this.rootDiskSizeFixed > 0 && !this.template.deployasis) {
deployVmData.rootdisksize = this.rootDiskSizeFixed
}
if (values.hypervisor && values.hypervisor.length > 0) {
deployVmData.hypervisor = values.hypervisor
}
deployVmData.startvm = values.startvm
// step 3: select service offering
deployVmData.serviceofferingid = values.computeofferingid
if (this.serviceOffering && this.serviceOffering.iscustomized) {
if (values.cpunumber) {
deployVmData['details[0].cpuNumber'] = values.cpunumber
}
if (values.cpuspeed) {
deployVmData['details[0].cpuSpeed'] = values.cpuspeed
}
if (values.memory) {
deployVmData['details[0].memory'] = values.memory
}
}
if (this.selectedTemplateConfiguration) {
deployVmData['details[0].configurationId'] = this.selectedTemplateConfiguration.id
}
if (!this.serviceOffering.diskofferingstrictness && values.overridediskofferingid && !values.isoid) {
deployVmData.overridediskofferingid = values.overridediskofferingid
if (values.rootdisksize && values.rootdisksize > 0) {
deployVmData.rootdisksize = values.rootdisksize
}
}
if (this.isCustomizedIOPS) {
deployVmData['details[0].minIops'] = this.minIops
deployVmData['details[0].maxIops'] = this.maxIops
}
// step 4: select disk offering
if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
if (values.multidiskoffering) {
let i = 0
Object.entries(values.multidiskoffering).forEach(([disk, offering]) => {
const diskKey = `datadiskofferinglist[${i}].datadisktemplateid`
const offeringKey = `datadiskofferinglist[${i}].diskofferingid`
deployVmData[diskKey] = disk
deployVmData[offeringKey] = offering
i++
})
}
} else {
deployVmData.diskofferingid = values.diskofferingid
if (values.size) {
deployVmData.size = values.size
}
}
if (this.isCustomizedDiskIOPS) {
deployVmData['details[0].minIopsDo'] = this.diskIOpsMin
deployVmData['details[0].maxIopsDo'] = this.diskIOpsMax
}
// step 5: select an affinity group
deployVmData.affinitygroupids = (values.affinitygroupids || []).join(',')
// step 6: select network
if (this.zone.networktype !== 'Basic') {
if (this.nicToNetworkSelection && this.nicToNetworkSelection.length > 0) {
for (var j in this.nicToNetworkSelection) {
var nicNetwork = this.nicToNetworkSelection[j]
deployVmData['nicnetworklist[' + j + '].nic'] = nicNetwork.nic
deployVmData['nicnetworklist[' + j + '].network'] = nicNetwork.network
}
} else {
const arrNetwork = []
networkIds = values.networkids
if (networkIds.length > 0) {
for (let i = 0; i < networkIds.length; i++) {
if (networkIds[i] === this.defaultnetworkid) {
const ipToNetwork = {
networkid: this.defaultnetworkid
}
arrNetwork.unshift(ipToNetwork)
} else {
const ipToNetwork = {
networkid: networkIds[i]
}
arrNetwork.push(ipToNetwork)
}
}
} else {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.step.4.continue')
})
this.loading.deploy = false
return
}
for (let j = 0; j < arrNetwork.length; j++) {
deployVmData['iptonetworklist[' + j + '].networkid'] = arrNetwork[j].networkid
if (this.networkConfig.length > 0) {
const networkConfig = this.networkConfig.filter((item) => item.key === arrNetwork[j].networkid)
if (networkConfig && networkConfig.length > 0) {
deployVmData['iptonetworklist[' + j + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined
deployVmData['iptonetworklist[' + j + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined
}
}
}
}
}
if (this.securitygroupids.length > 0) {
deployVmData.securitygroupids = this.securitygroupids.join(',')
}
// step 7: select ssh key pair
deployVmData.keypairs = this.sshKeyPairs.join(',')
if (isUserdataAllowed) {
deployVmData.userdataid = values.userdataid
}
if (values.name) {
deployVmData.name = values.name
deployVmData.displayname = values.name
}
if (values.group) {
deployVmData.group = values.group
}
// step 8: enter setup
if ('properties' in values) {
const keys = Object.keys(values.properties)
for (var i = 0; i < keys.length; ++i) {
const propKey = keys[i].split('\\002E').join('.')
deployVmData['properties[' + i + '].key'] = propKey
deployVmData['properties[' + i + '].value'] = values.properties[keys[i]]
}
}
if ('bootintosetup' in values) {
deployVmData.bootintosetup = values.bootintosetup
}
if (this.owner.account) {
deployVmData.account = this.owner.account
deployVmData.domainid = this.owner.domainid
} else if (this.owner.projectid) {
deployVmData.domainid = this.owner.domainid
deployVmData.projectid = this.owner.projectid
}
const title = this.$t('label.launch.vm')
const description = values.name || ''
const password = this.$t('label.password')
deployVmData = Object.fromEntries(
Object.entries(deployVmData).filter(([key, value]) => value !== undefined))
var idx = 0
if (this.templateUserDataValues) {
for (const [key, value] of Object.entries(this.templateUserDataValues)) {
deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value
idx++
}
}
if (isUserdataAllowed && this.userDataValues) {
for (const [key, value] of Object.entries(this.userDataValues)) {
deployVmData['userdatadetails[' + idx + '].' + `${key}`] = value
idx++
}
}
const httpMethod = deployVmData.userdata ? 'POST' : 'GET'
const args = httpMethod === 'POST' ? {} : deployVmData
const data = httpMethod === 'POST' ? deployVmData : {}
api('deployVirtualMachine', args, httpMethod, data).then(response => {
const jobId = response.deployvirtualmachineresponse.jobid
if (jobId) {
this.$pollJob({
jobId,
title,
description,
successMethod: result => {
const vm = result.jobresult.virtualmachine
const name = vm.displayname || vm.name || vm.id
if (vm.password) {
this.$notification.success({
message: password + ` ${this.$t('label.for')} ` + name,
description: vm.password,
btn: () => h(
Button,
{
type: 'primary',
size: 'small',
onClick: () => this.copyToClipboard(vm.password)
},
() => [this.$t('label.copy.password')]
),
duration: 0
})
}
eventBus.emit('vm-refresh-data')
},
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: {
isFetchData: false
}
})
}
// Sending a refresh in case it hasn't picked up the new VM
new Promise(resolve => setTimeout(resolve, 3000)).then(() => {
eventBus.emit('vm-refresh-data')
})
if (!values.stayonpage) {
this.$router.back()
}
}).catch(error => {
this.$notifyError(error)
this.loading.deploy = false
}).finally(() => {
this.form.stayonpage = false
this.loading.deploy = false
})
}).catch(err => {
this.formRef.value.scrollToField(err.errorFields[0].name)
if (err) {
if (err.licensesaccepted) {
this.$notification.error({
message: this.$t('message.license.agreements.not.accepted'),
description: this.$t('message.step.license.agreements.continue')
})
return
}
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('error.form.message')
})
}
})
},
fetchOwnerOptions (OwnerOptions) {
this.owner = {
projectid: null,
domainid: store.getters.userInfo.domainid,
account: store.getters.userInfo.account
}
if (OwnerOptions.selectedAccountType === this.$t('label.account')) {
if (!OwnerOptions.selectedAccount) {
return
}
this.owner.account = OwnerOptions.selectedAccount
this.owner.domainid = OwnerOptions.selectedDomain
this.owner.projectid = null
} else if (OwnerOptions.selectedAccountType === this.$t('label.project')) {
if (!OwnerOptions.selectedProject) {
return
}
this.owner.account = null
this.owner.domainid = null
this.owner.projectid = OwnerOptions.selectedProject
}
this.resetData()
},
fetchZones (zoneId, listZoneAllow) {
this.zones = []
return new Promise((resolve) => {
this.loading.zones = true
const param = this.params.zones
const args = { showicon: true }
if (zoneId) args.id = zoneId
api(param.list, args).then(json => {
const zoneResponse = json.listzonesresponse.zone || []
if (listZoneAllow && listZoneAllow.length > 0) {
zoneResponse.map(zone => {
if (listZoneAllow.includes(zone.id)) {
this.zones.push(zone)
}
})
} else {
this.zones = zoneResponse
}
resolve(this.zones)
}).catch(function (error) {
console.log(error.stack)
}).finally(() => {
this.loading.zones = false
})
})
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
}
this.loading[name] = true
param.loading = true
param.opts = []
const options = param.options || {}
if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'dynamicScalingVmConfig', 'hypervisors'].includes(name)) {
options.listall = true
}
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (Object.keys(responseItem).length === 0) {
this.rowCount[name] = 0
this.options[name] = []
return
}
if (!responseKey.includes('response')) {
return
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
this.rowCount[name] = response
return
}
param.opts = response
this.options[name] = response
if (name === 'hypervisors') {
const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null
this.dataPreFill.hypervisor = hypervisorFromResponse
this.form.hypervisor = hypervisorFromResponse
}
if (param.field) {
this.fillValue(param.field)
}
})
if (name === 'zones') {
let zoneid = ''
if (this.$route.query.zoneid) {
zoneid = this.$route.query.zoneid
} else if (this.options.zones.length === 1) {
zoneid = this.options.zones[0].id
}
if (zoneid) {
this.form.zoneid = zoneid
this.onSelectZoneId(zoneid)
}
}
})
}).catch(function (error) {
console.log(error.stack)
param.loading = false
}).finally(() => {
this.loading[name] = false
})
},
fetchTemplates (templateFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== templateFilter) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
if (this.isZoneSelectedMultiArch) {
args.arch = this.selectedArchitecture
}
args.account = store.getters.project?.id ? null : this.owner.account
args.domainid = store.getters.project?.id ? null : this.owner.domainid
args.projectid = store.getters.project?.id || this.owner.projectid
args.templatefilter = templateFilter
args.details = 'all'
args.showicon = 'true'
args.id = this.templateId
args.isvnf = false
return new Promise((resolve, reject) => {
api('listTemplates', args).then((response) => {
resolve(response)
}).catch((reason) => {
// ToDo: Handle errors
reject(reason)
})
})
},
fetchIsos (isoFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== isoFilter) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
if (this.isZoneSelectedMultiArch) {
args.arch = this.selectedArchitecture
}
args.isoFilter = isoFilter
args.bootable = true
args.showicon = 'true'
args.id = this.isoId
return new Promise((resolve, reject) => {
api('listIsos', args).then((response) => {
resolve(response)
}).catch((reason) => {
// ToDo: Handle errors
reject(reason)
})
})
},
fetchAllTemplates (params) {
const promises = []
const templates = {}
this.loading.templates = true
this.templateFilter.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
promises.push(this.fetchTemplates(filter, params))
})
this.options.templates = templates
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
templates[this.templateFilter[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.options.templates = { ...templates }
})
}).catch((reason) => {
console.log(reason)
}).finally(() => {
this.loading.templates = false
})
},
fetchAllIsos (params) {
const promises = []
const isos = {}
this.loading.isos = true
this.isoFilter.forEach((filter) => {
isos[filter] = { count: 0, iso: [] }
promises.push(this.fetchIsos(filter, params))
})
this.options.isos = isos
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
isos[this.isoFilter[idx]] = _.isEmpty(resItem.listisosresponse) ? { count: 0, iso: [] } : resItem.listisosresponse
this.options.isos = { ...isos }
})
}).catch((reason) => {
console.log(reason)
}).finally(() => {
this.loading.isos = false
})
},
filterOption (input, option) {
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
},
onSelectZoneId (value) {
this.dataPreFill = {}
this.zoneId = value
this.podId = null
this.clusterId = null
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.isZoneSelectedMultiArch = this.zone.ismultiarch
if (this.isZoneSelectedMultiArch) {
this.selectedArchitecture = this.architectureTypes.opts[0].id
}
this.zoneSelected = true
this.form.startvm = true
this.selectedZone = this.zoneId
this.form.zoneid = this.zoneId
this.form.clusterid = undefined
this.form.podid = undefined
this.form.hostid = undefined
this.form.templateid = undefined
this.form.isoid = undefined
this.tabKey = 'templateid'
if (this.isoId) {
this.tabKey = 'isoid'
}
_.each(this.params, (param, name) => {
if (this.networkId && name === 'networks') {
param.options = {
id: this.networkId
}
}
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones'])
}
})
if (this.tabKey === 'templateid') {
this.fetchAllTemplates()
} else {
this.fetchAllIsos()
}
this.updateTemplateKey()
this.formModel = toRaw(this.form)
},
onSelectPodId (value) {
this.podId = value
if (this.podId === null) {
this.form.podid = undefined
}
this.fetchOptions(this.params.clusters, 'clusters')
this.fetchOptions(this.params.hosts, 'hosts')
},
onSelectClusterId (value) {
this.clusterId = value
if (this.clusterId === null) {
this.form.clusterid = undefined
}
this.fetchOptions(this.params.hosts, 'hosts')
},
onSelectHostId (value) {
this.hostId = value
if (this.hostId === null) {
this.form.hostid = undefined
}
},
handleSearchFilter (name, options) {
this.params[name].options = { ...this.params[name].options, ...options }
this.fetchOptions(this.params[name], name)
},
onTabChange (key, type) {
this[type] = key
if (key === 'isoid') {
this.fetchAllIsos()
}
},
onUserdataTabChange (key, type) {
this[type] = key
this.userDataParams = []
},
fetchTemplateNics (template) {
var nics = []
this.nicToNetworkSelection = []
if (template && template.deployasisdetails && Object.keys(template.deployasisdetails).length > 0) {
var keys = Object.keys(template.deployasisdetails)
keys = keys.filter(key => key.startsWith('network-'))
for (var key of keys) {
var propertyMap = JSON.parse(template.deployasisdetails[key])
nics.push(propertyMap)
}
nics.sort(function (a, b) {
return a.InstanceID - b.InstanceID
})
if (this.options.networks && this.options.networks.length > 0) {
for (var i = 0; i < nics.length; ++i) {
var nic = nics[i]
nic.id = nic.InstanceID
var network = this.options.networks[Math.min(i, this.options.networks.length - 1)]
nic.selectednetworkid = network.id
nic.selectednetworkname = network.name
this.nicToNetworkSelection.push({ nic: nic.id, network: network.id })
}
}
}
return nics
},
groupBy (array, key) {
const result = {}
array.forEach(item => {
if (!result[item[key]]) {
result[item[key]] = []
}
result[item[key]].push(item)
})
return result
},
fetchTemplateProperties (template) {
var properties = []
if (template && template.deployasisdetails && Object.keys(template.deployasisdetails).length > 0) {
var keys = Object.keys(template.deployasisdetails)
keys = keys.filter(key => key.startsWith('property-'))
for (var key of keys) {
var propertyMap = JSON.parse(template.deployasisdetails[key])
properties.push(propertyMap)
}
properties.sort(function (a, b) {
return a.index - b.index
})
}
return this.groupBy(properties, 'category')
},
fetchTemplateConfigurations (template) {
var configurations = []
if (template && template.deployasisdetails && Object.keys(template.deployasisdetails).length > 0) {
var keys = Object.keys(template.deployasisdetails)
keys = keys.filter(key => key.startsWith('configuration-'))
for (var key of keys) {
var configuration = JSON.parse(template.deployasisdetails[key])
configuration.name = configuration.label
configuration.displaytext = configuration.label
configuration.iscustomized = true
configuration.cpunumber = 0
configuration.cpuspeed = 0
configuration.memory = 0
for (var harwareItem of configuration.hardwareItems) {
if (harwareItem.resourceType === 'Processor') {
configuration.cpunumber = harwareItem.virtualQuantity
configuration.cpuspeed = harwareItem.reservation
} else if (harwareItem.resourceType === 'Memory') {
configuration.memory = harwareItem.virtualQuantity
}
}
configurations.push(configuration)
}
configurations.sort(function (a, b) {
return a.index - b.index
})
}
return configurations
},
fetchTemplateLicenses (template) {
var licenses = []
if (template && template.deployasisdetails && Object.keys(template.deployasisdetails).length > 0) {
var keys = Object.keys(template.deployasisdetails)
const prefix = /eula-\d-/
keys = keys.filter(key => key.startsWith('eula-')).sort()
for (var key of keys) {
var license = {
id: this.escapePropertyKey(key.replace(' ', '-')),
name: key.replace(prefix, ''),
text: template.deployasisdetails[key]
}
licenses.push(license)
}
}
return licenses
},
deleteFrom (options, values) {
for (const value of values) {
delete options[value]
}
},
resetFromTemplateConfiguration () {
this.deleteFrom(this.instanceConfig, ['disksize', 'rootdisksize'])
this.deleteFrom(this.params.serviceOfferings.options, ['templateid', 'cpuspeed', 'cpunumber', 'memory'])
this.deleteFrom(this.dataPreFill, ['cpuspeed', 'cpunumber', 'memory'])
this.handleSearchFilter('serviceOfferings', {
page: 1,
pageSize: 10
})
},
handleTemplateConfiguration () {
if (!this.selectedTemplateConfiguration && !this.template.templatetag) {
return
}
let params = {
page: 1,
pageSize: 10
}
if (this.template.templatetag) {
params.templateid = this.template.id
}
if (this.selectedTemplateConfiguration && Object.keys(this.selectedTemplateConfiguration).length > 0) {
params = {
...params,
cpunumber: this.selectedTemplateConfiguration.cpunumber,
cpuspeed: this.selectedTemplateConfiguration.cpuspeed,
memory: this.selectedTemplateConfiguration.memory
}
this.dataPreFill.cpunumber = params.cpunumber
this.dataPreFill.cpuspeed = params.cpuspeed
this.dataPreFill.memory = params.memory
}
this.handleSearchFilter('serviceOfferings', params)
},
updateFormProperties () {
if (this.vm.templateid && this.templateProperties && Object.keys(this.templateProperties).length > 0) {
this.form.properties = {}
Object.keys(this.templateProperties).forEach((category, categoryIndex) => {
this.templateProperties[category].forEach((property, _) => {
if (property.type && property.type === 'boolean') {
this.form.properties[this.escapePropertyKey(property.key)] = property.value === 'TRUE'
} else if (property.type && (property.type === 'int' || property.type === 'real')) {
this.form.properties[this.escapePropertyKey(property.key)] = property.value
} else if (property.type && property.type === 'string' && property.qualifiers && property.qualifiers.startsWith('ValueMap')) {
this.form.properties[this.escapePropertyKey(property.key)] = property.value && property.value.length > 0
? property.value
: this.getPropertyQualifiers(property.qualifiers, 'select')[0]
} else if (property.type && property.type === 'string' && property.password) {
this.form.properties[this.escapePropertyKey(property.key)] = property.value
this.rules['properties.' + this.escapePropertyKey(property.key)] = [{
validator: async (rule, value) => {
if (!property.qualifiers) {
return Promise.resolve()
}
var minlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').min
var maxlength = this.getPropertyQualifiers(property.qualifiers, 'number-select').max
var errorMessage = ''
var isPasswordInvalidLength = function () {
return false
}
if (minlength) {
errorMessage = this.$t('message.validate.minlength').replace('{0}', minlength)
isPasswordInvalidLength = function () {
return !value || value.length < minlength
}
}
if (maxlength !== Number.MAX_SAFE_INTEGER) {
if (minlength) {
errorMessage = this.$t('message.validate.range.length').replace('{0}', minlength).replace('{1}', maxlength)
isPasswordInvalidLength = function () {
return !value || (maxlength < value.length || value.length < minlength)
}
} else {
errorMessage = this.$t('message.validate.maxlength').replace('{0}', maxlength)
isPasswordInvalidLength = function () {
return !value || value.length > maxlength
}
}
}
if (isPasswordInvalidLength()) {
return Promise.reject(errorMessage)
}
return Promise.resolve()
}
}]
} else {
this.form.properties[this.escapePropertyKey(property.key)] = property.value
}
})
})
}
},
updateTemplateParameters () {
if (this.template) {
this.templateNics = this.fetchTemplateNics(this.template)
this.templateConfigurations = this.fetchTemplateConfigurations(this.template)
this.templateLicenses = this.fetchTemplateLicenses(this.template)
this.templateProperties = this.fetchTemplateProperties(this.template)
this.selectedTemplateConfiguration = {}
setTimeout(() => {
if (this.templateConfigurationExists || this.template.templatetag) {
this.selectedTemplateConfiguration = this.templateConfigurationExists ? this.templateConfigurations[0] : {}
this.handleTemplateConfiguration()
if (this.selectedTemplateConfiguration) {
this.updateFieldValue('templateConfiguration', this.selectedTemplateConfiguration.id)
}
this.updateComputeOffering(null) // reset as existing selection may be incompatible
}
}, 500)
this.updateFormProperties()
}
},
onSelectTemplateConfigurationId (value) {
this.selectedTemplateConfiguration = _.find(this.templateConfigurations, (option) => option.id === value)
this.handleTemplateConfiguration()
this.updateComputeOffering(null)
},
updateTemplateConfigurationOfferingDetails (offeringId) {
this.rootDiskSizeFixed = 0
var offering = this.serviceOffering
if (!offering || offering.id !== offeringId) {
offering = _.find(this.options.serviceOfferings, (option) => option.id === offeringId)
}
if (offering && offering.iscustomized && this.templateConfigurationExists && this.selectedTemplateConfiguration) {
if ('cpunumber' in this.form.fieldsStore.fieldsMeta) {
this.updateFieldValue('cpunumber', this.selectedTemplateConfiguration.cpunumber)
}
if ((offering.cpuspeed == null || offering.cpuspeed === undefined) && 'cpuspeed' in this.form.fieldsStore.fieldsMeta) {
this.updateFieldValue('cpuspeed', this.selectedTemplateConfiguration.cpuspeed)
}
if ('memory' in this.form.fieldsStore.fieldsMeta) {
this.updateFieldValue('memory', this.selectedTemplateConfiguration.memory)
}
}
if (offering && offering.rootdisksize > 0) {
this.rootDiskSizeFixed = offering.rootdisksize
this.showRootDiskSizeChanger = false
}
this.form.rootdisksizeitem = this.showRootDiskSizeChanger && this.rootDiskSizeFixed > 0
this.formModel = toRaw(this.form)
},
handlerError (error) {
this.error = error
},
onSelectDiskSize (rowSelected) {
this.diskSelected = rowSelected
},
onSelectRootDiskSize (rowSelected) {
this.rootDiskSelected = rowSelected
},
updateIOPSValue (input, value) {
this[input] = value
},
onBootTypeChange (value) {
this.fetchBootModes(value)
this.defaultBootMode = this.options.bootModes?.[0]?.id || undefined
this.updateFieldValue('bootmode', this.defaultBootMode)
},
handleNicsNetworkSelection (nicToNetworkSelection) {
this.nicToNetworkSelection = nicToNetworkSelection
},
getSelectedNetworksWithExistingConfig (networks) {
for (var i in this.networks) {
var n = this.networks[i]
for (var c of this.networkConfig) {
if (n.id === c.key) {
n = { ...n, ...c }
networks[i] = n
break
}
}
}
return networks
},
copyToClipboard (txt) {
const parent = this
this.$copyText(txt, document.body, function (err) {
if (!err) {
parent.$message.success(parent.$t('label.copied.clipboard'))
}
})
}
}
}
</script>
<style lang="less" scoped>
.card-footer {
text-align: right;
margin-top: 2rem;
button + button {
margin-left: 8px;
}
}
.ant-list-item-meta-avatar {
font-size: 1rem;
}
.ant-collapse {
margin: 2rem 0;
}
</style>
<style lang="less">
@import url('../../style/index');
.ant-table-selection-column {
// Fix for the table header if the row selection use radio buttons instead of checkboxes
> div:empty {
width: 16px;
}
}
.ant-collapse-borderless > .ant-collapse-item {
border: 1px solid @border-color-split;
border-radius: @border-radius-base !important;
margin: 0 0 1.2rem;
}
.zone-radio-button {
width:100%;
min-width: 345px;
height: 60px;
display: flex;
padding-left: 20px;
align-items: center;
}
.vm-info-card {
.ant-card-body {
min-height: 250px;
max-height: calc(100vh - 150px);
overflow-y: auto;
scroll-behavior: smooth;
}
.resource-detail-item__label {
font-weight: normal;
}
.resource-detail-item__details, .resource-detail-item {
a {
color: rgba(0, 0, 0, 0.65);
cursor: default;
pointer-events: none;
}
}
}
.form-item-hidden {
display: none;
}
.btn-stay-on-page {
&.ant-dropdown-menu-dark {
.ant-dropdown-menu-item:hover {
background: transparent !important;
}
}
}
</style>