You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/08/17 08:37:11 UTC
[cloudstack-primate] branch master updated: compute: vApps frontend
support (#550)
This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new b690814 compute: vApps frontend support (#550)
b690814 is described below
commit b6908141797d20789e0b8a61e32369e313bc598a
Author: Abhishek Kumar <ab...@gmail.com>
AuthorDate: Mon Aug 17 14:07:05 2020 +0530
compute: vApps frontend support (#550)
Support for vApp VM deployment for VMware
Backend PR - https://github.com/apache/cloudstack/pull/4250
Signed-off-by: Abhishek Kumar <ab...@gmail.com>
Co-authored-by: nvazquez <ni...@gmail.com>
---
src/config/section/image.js | 2 +-
src/locales/en.json | 7 +
src/views/compute/DeployVM.vue | 414 ++++++++++++++++++---
.../compute/wizard/ComputeOfferingSelection.vue | 50 ++-
src/views/image/RegisterOrUploadTemplate.vue | 12 +
5 files changed, 421 insertions(+), 64 deletions(-)
diff --git a/src/config/section/image.js b/src/config/section/image.js
index d8ad4cd..ad3a119 100644
--- a/src/config/section/image.js
+++ b/src/config/section/image.js
@@ -43,7 +43,7 @@ export default {
}
return fields
},
- details: ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled', 'sshkeyenabled', 'directdownload', 'isextractable', 'isdynamicallyscalable', 'ispublic', 'isfeatured', 'crosszones', 'type', 'account', 'domain', 'created', 'url'],
+ details: ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled', 'sshkeyenabled', 'directdownload', 'deployasis', 'isextractable', 'isdynamicallyscalable', 'ispublic', 'isfeatured', 'crosszones', 'type', 'account', 'domain', 'created', 'url'],
searchFilters: ['name', 'zoneid', 'tags'],
related: [{
name: 'vm',
diff --git a/src/locales/en.json b/src/locales/en.json
index 1fd3860..63cbba7 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -702,6 +702,7 @@
"label.demote.project.owner.user": "Demote user to Regular role",
"label.deleting.template": "Deleting template",
"label.deny": "Deny",
+"label.deployasis":"Deploy As-Is",
"label.deploymentplanner": "Deployment planner",
"label.description": "Description",
"label.destcidr": "Destination CIDR",
@@ -1142,6 +1143,7 @@
"label.isvolatile": "Volatile",
"label.item.listing": "Item listing",
"label.items": "items",
+"label.i.accept.all.license.agreements": "I accept all license agreement",
"label.japanese.keyboard": "Japanese keyboard",
"label.keep": "Keep",
"label.keep.colon": "Keep:",
@@ -1213,6 +1215,7 @@
"label.ldap.group.name": "LDAP Group",
"label.ldap.port": "LDAP port",
"label.level": "Level",
+"label.license.agreements": "License agreements",
"label.limit": "Limit",
"label.limitcpuuse": "CPU Cap",
"label.limits": "Configure Limits",
@@ -2869,6 +2872,7 @@
"message.launch.zone.description": "Zone is ready to launch; please proceed to the next step.",
"message.launch.zone.hint": "Configure network components and traffic including IP addresses.",
"message.ldap.group.import": "All The users from the given group name will be imported",
+"message.license.agreements.not.accepted": "License agreements not accepted",
"message.link.domain.to.ldap": "Enable autosync for this domain in LDAP",
"message.listnsp.not.return.providerid": "error: listNetworkServiceProviders API doesn't return VirtualRouter provider ID",
"message.listview.subselect.multi": "(Ctrl/Cmd-click)",
@@ -2920,6 +2924,7 @@
"message.number.zones": "<h2><span> # of </span> Zones</h2>",
"message.outofbandmanagement.action.maintenance": "Warning host is in maintenance mode",
"message.ovf.properties.available": "There are OVF properties available for customizing the selected appliance. Please edit the values accordingly.",
+"message.ovf.configurations": "OVF configurations available for the selected appliance. Please select the desired value. Incompatible compute offerings will get disbaled.",
"message.password.has.been.reset.to": "Password has been reset to",
"message.password.of.the.vm.has.been.reset.to": "Password of the VM has been reset to",
"message.pending.projects.1": "You have pending project invitations:",
@@ -2945,6 +2950,7 @@
"message.publicip.state.free": "The IP address is ready to be allocated.",
"message.publicip.state.releasing": "The IP address is being released for other network elements and is not ready for allocation.",
"message.question.are.you.sure.you.want.to.add": "Are you sure you want to add",
+"message.read.accept.license.agreements": "Please read and accept the terms for the license agreements.",
"message.read.admin.guide.scaling.up": "Please read the dynamic scaling section in the admin guide before scaling up.",
"message.recover.vm": "Please confirm that you would like to recover this VM.",
"message.redirecting.region": "Redirecting to region...",
@@ -3021,6 +3027,7 @@
"message.step.3.continue": "Please select a disk offering to continue",
"message.step.4.continue": "Please select at least one network to continue",
"message.step.4.desc": "Please select the primary network that your virtual instance will be connected to.",
+"message.step.license.agreements.continue": "Please aceept all license agreements to continue",
"message.storage.traffic": "Traffic between CloudStack's internal resources, including any components that communicate with the Management Server, such as hosts and CloudStack system VMs. Please configure storage traffic here.",
"message.success.enable.saml.auth": "Successfully enabled SAML Authorization",
"message.success.create.user": "Successfully created user",
diff --git a/src/views/compute/DeployVM.vue b/src/views/compute/DeployVM.vue
index 429de69..18f48cc 100644
--- a/src/views/compute/DeployVM.vue
+++ b/src/views/compute/DeployVM.vue
@@ -143,6 +143,32 @@
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
+ <a-form-item v-if="zoneSelected && templateConfigurationExists">
+ <span slot="label">
+ {{ $t('label.configuration') }}
+ <a-tooltip :title="$t('message.ovf.configurations')">
+ <a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
+ </a-tooltip>
+ </span>
+ <a-select
+ showSearch
+ optionFilterProp="children"
+ v-decorator="[
+ 'templateConfiguration'
+ ]"
+ defaultActiveFirstOption
+ :placeholder="'Something'"
+ :filterOption="(input, option) => {
+ return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }"
+ @change="onSelectTemplateConfigurationId"
+ >
+ <a-select-option v-for="opt in templateConfigurations" :key="opt.id">
+ {{ 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"
:row-count="rowCount.serviceOfferings"
@@ -150,11 +176,15 @@
: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"
+ v-show="!templateConfigurationExists"
cpunumber-input-decorator="cpunumber"
cpuspeed-input-decorator="cpuspeed"
memory-input-decorator="memory"
@@ -169,12 +199,12 @@
@update-compute-cpuspeed="updateFieldValue"
@update-compute-memory="updateFieldValue" />
<span v-if="serviceOffering && serviceOffering.iscustomized">
- <a-form-item class="form-item-hidden" >
+ <a-form-item class="form-item-hidden">
<a-input v-decorator="['cpunumber']"/>
</a-form-item>
<a-form-item
class="form-item-hidden"
- v-if="serviceOffering && !(serviceOffering.cpuspeed > 0)">
+ v-if="(serviceOffering && !(serviceOffering.cpuspeed > 0))">
<a-input v-decorator="['cpuspeed']"/>
</a-form-item>
<a-form-item class="form-item-hidden">
@@ -216,24 +246,55 @@
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
- <network-selection
- v-if="!networkId"
- :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)"
- @select-default-network-item="($event) => updateDefaultNetworks($event)"
- ></network-configuration>
+ <div v-if="vm.templateid && templateNics && templateNics.length > 0">
+ <a-form-item
+ v-for="(nic, nicIndex) in templateNics"
+ :key="nicIndex"
+ :v-bind="nic.name" >
+ <span slot="label">
+ {{ nic.elementName + ' - ' + nic.name }}
+ <a-tooltip :title="nic.networkDescription">
+ <a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
+ </a-tooltip>
+ </span>
+ <a-select
+ showSearch
+ optionFilterProp="children"
+ v-decorator="[
+ 'networkMap.nic-' + nic.InstanceID.toString(),
+ { initialValue: options.networks && options.networks.length > 0 ? options.networks[Math.min(nicIndex, options.networks.length - 1)].id : null }
+ ]"
+ :placeholder="nic.networkDescription"
+ :filterOption="(input, option) => {
+ return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }"
+ >
+ <a-select-option v-for="opt in options.networks" :key="opt.id">
+ {{ opt.name || opt.description }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </div>
+ <div v-else>
+ <network-selection
+ v-if="!networkId"
+ :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)"
+ @select-default-network-item="($event) => updateDefaultNetworks($event)"
+ ></network-configuration>
+ </div>
</div>
</template>
</a-step>
@@ -271,11 +332,11 @@
<a-step
:title="$t('label.ovf.properties')"
:status="zoneSelected ? 'process' : 'wait'"
- v-if="vm.templateid && template.properties && template.properties.length > 0">
+ v-if="vm.templateid && templateProperties && templateProperties.length > 0">
<template slot="description">
<div>
<a-form-item
- v-for="(property, propertyIndex) in template.properties"
+ v-for="(property, propertyIndex) in templateProperties"
:key="propertyIndex"
:v-bind="property.key" >
<span slot="label">
@@ -287,43 +348,42 @@
<span v-if="property.type && property.type==='boolean'">
<a-switch
- v-decorator="['properties.' + property.key, { initialValue: property.value==='TRUE'?true:false}]"
+ v-decorator="['properties.' + escapePropertyKey(property.key), { initialValue: property.value==='TRUE'?true:false}]"
:defaultChecked="property.value==='TRUE'?true:false"
:placeholder="property.description"
/>
</span>
<span v-else-if="property.type && (property.type==='int' || property.type==='real')">
<a-input-number
- v-decorator="['properties.'+property.key]"
+ v-decorator="['properties.'+ escapePropertyKey(property.key) ]"
:defaultValue="property.value"
:placeholder="property.description"
- :min="property.qualifiers && property.qualifiers.includes('MinValue') && property.qualifiers.includes('MaxValue')?property.qualifiers.split(',')[0].replace('MinValue(','').slice(0, -1):0"
- :max="property.qualifiers && property.qualifiers.includes('MinValue') && property.qualifiers.includes('MaxValue')?property.qualifiers.split(',')[1].replace('MaxValue(','').slice(0, -1):property.type==='real'?1:Number.MAX_SAFE_INTEGER" />
+ :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="children"
- v-decorator="['properties.' + property.key, { initialValue: property.value }]"
+ v-decorator="['properties.' + escapePropertyKey(property.key), { initialValue: property.value.length>0 ? property.value: getPropertyQualifiers(property.qualifiers, 'select')[0] }]"
:placeholder="property.description"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
>
- <a-select-option :v-if="property.value===''" key="">{{ }}</a-select-option>
- <a-select-option v-for="opt in property.qualifiers.replace('ValueMap','').substr(1).slice(0, -1).split(',')" :key="removeQuotes(opt)">
- {{ removeQuotes(opt) }}
+ <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-decorator="['properties.' + property.key, { initialValue: property.value }]"
+ v-decorator="['properties.' + escapePropertyKey(property.key), { initialValue: property.value }]"
:placeholder="property.description" />
</span>
<span v-else>
<a-input
- v-decorator="['properties.' + property.key, { initialValue: property.value }]"
+ v-decorator="['properties.' + escapePropertyKey(property.key), { initialValue: property.value }]"
:placeholder="property.description" />
</span>
</a-form-item>
@@ -409,6 +469,36 @@
</div>
</template>
</a-step>
+ <a-step
+ :title="$t('label.license.agreements')"
+ :status="zoneSelected ? 'process' : 'wait'"
+ v-if="vm.templateid && templateLicenses && templateLicenses.length > 0">
+ <template slot="description">
+ <div style="margin-top: 10px">
+ {{ $t('message.read.accept.license.agreements') }}
+ <a-form-item>
+ <div
+ style="margin-top: 10px"
+ v-for="(license, licenseIndex) in templateLicenses"
+ :key="licenseIndex"
+ :v-bind="license.id">
+ <span slot="label">
+ {{ 'Agreement ' + (licenseIndex+1) + ': ' + license.name }}
+ </span>
+ <a-textarea
+ :value="license.text"
+ :auto-size="{ minRows: 3, maxRows: 8 }"
+ readOnly />
+ </div>
+ <a-checkbox
+ style="margin-top: 10px"
+ v-decorator="['licensesaccepted']">
+ {{ $t('label.i.accept.all.license.agreements') }}
+ </a-checkbox>
+ </a-form-item>
+ </div>
+ </template>
+ </a-step>
</a-steps>
<div class="card-footer">
<!-- ToDo extract as component -->
@@ -550,6 +640,11 @@ export default {
},
instanceConfig: {},
template: {},
+ templateConfigurations: [],
+ templateNics: [],
+ templateLicenses: [],
+ templateProperties: [],
+ selectedTemplateConfiguration: {},
iso: {},
hypervisor: '',
serviceOffering: {},
@@ -775,6 +870,9 @@ export default {
}
})
},
+ templateConfigurationExists () {
+ return this.vm.templateid && this.templateConfigurations && this.templateConfigurations.length > 0
+ },
networkId () {
return this.$route.query.networkid || null
},
@@ -902,8 +1000,33 @@ export default {
}
},
methods: {
- removeQuotes (value) {
- return value.replace(/"/g, '')
+ 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 && qualifiers.includes('MinValue') && qualifiers.includes('MaxValue')) {
+ var arr = qualifiers.split(',')
+ if (arr.length > 1) {
+ min = arr[0].replace('MinValue(', '').slice(0, -1)
+ max = arr[1].replace('MaxValue(', '').slice(0, -1)
+ }
+ }
+ result = { min: min, max: max }
+ break
+ default:
+ }
+ return result
},
fillValue (field) {
this.form.getFieldDecorator([field], { initialValue: this.dataPreFill[field] })
@@ -1006,6 +1129,16 @@ export default {
for (const key in this.options.templates) {
var t = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === value)
if (t) {
+ this.templateConfigurations = []
+ this.selectedTemplateConfiguration = {}
+ this.templateNics = []
+ this.templateLicenses = []
+ this.templateProperties = []
+ this.updateTemplateParameters()
+ if (t.deployasis === true && !t.details && (!this.template || t.id !== this.template.id)) {
+ // Deploy as-is template without details detected, need to retrieve the template details
+ this.fetchTemplateDetails(t)
+ }
template = t
break
}
@@ -1015,6 +1148,11 @@ export default {
this.dataPreFill.minrootdisksize = Math.ceil(size)
}
} else if (name === 'isoid') {
+ this.templateConfigurations = []
+ this.selectedTemplateConfiguration = {}
+ this.templateNics = []
+ this.templateLicenses = []
+ this.templateProperties = []
this.tabKey = 'isoid'
this.form.setFieldsValue({
isoid: value,
@@ -1030,6 +1168,9 @@ export default {
this.form.setFieldsValue({
computeofferingid: id
})
+ setTimeout(() => {
+ this.updateTemplateConfigurationOfferingDetails(id)
+ }, 500)
},
updateDiskOffering (id) {
if (id === '0') {
@@ -1069,6 +1210,9 @@ export default {
keypair: name
})
},
+ escapePropertyKey (key) {
+ return key.split('.').join('\\002E')
+ },
updateSecurityGroups (securitygroupids) {
this.securitygroupids = securitygroupids
},
@@ -1096,6 +1240,20 @@ export default {
})
return
}
+ if (!values.computeofferingid) {
+ this.$notification.error({
+ message: this.$t('message.request.failed'),
+ description: this.$t('message.step.2.continue')
+ })
+ return
+ }
+ if ('licensesaccepted' in values && values.licensesaccepted !== true) {
+ this.$notification.error({
+ message: this.$t('message.license.agreements.not.accepted'),
+ description: this.$t('message.step.license.agreements.continue')
+ })
+ return
+ }
this.loading.deploy = true
@@ -1147,30 +1305,40 @@ export default {
// step 5: select an affinity group
deployVmData.affinitygroupids = (values.affinitygroupids || []).join(',')
// step 6: select network
- const arrNetwork = []
- networkIds = values.networkids
- if (networkIds.length > 0) {
- for (let i = 0; i < networkIds.length; i++) {
- if (networkIds[i] === this.defaultNetwork) {
- const ipToNetwork = {
- networkid: this.defaultNetwork
- }
- arrNetwork.unshift(ipToNetwork)
- } else {
- const ipToNetwork = {
- networkid: networkIds[i]
+ if ('networkMap' in values) {
+ const keys = Object.keys(values.networkMap)
+ for (var j = 0; j < keys.length; ++j) {
+ if (values.networkMap[keys[j]] && values.networkMap[keys[j]].length > 0) {
+ deployVmData['nicnetworklist[' + j + '].nic'] = keys[j].replace('nic-', '')
+ deployVmData['nicnetworklist[' + j + '].network'] = values.networkMap[keys[j]]
+ }
+ }
+ } else {
+ const arrNetwork = []
+ networkIds = values.networkids
+ if (networkIds.length > 0) {
+ for (let i = 0; i < networkIds.length; i++) {
+ if (networkIds[i] === this.defaultNetwork) {
+ const ipToNetwork = {
+ networkid: this.defaultNetwork
+ }
+ arrNetwork.unshift(ipToNetwork)
+ } else {
+ const ipToNetwork = {
+ networkid: networkIds[i]
+ }
+ arrNetwork.push(ipToNetwork)
}
- arrNetwork.push(ipToNetwork)
}
}
- }
- 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
+ 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
+ }
}
}
}
@@ -1185,13 +1353,15 @@ export default {
if ('properties' in values) {
const keys = Object.keys(values.properties)
for (var i = 0; i < keys.length; ++i) {
- deployVmData['properties[' + i + '].key'] = keys[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
}
+
const title = this.$t('label.launch.vm')
const description = values.name || ''
const password = this.$t('label.password')
@@ -1293,6 +1463,23 @@ export default {
this.loading[name] = false
})
},
+ fetchTemplateDetails (template) {
+ api('listTemplates', {
+ templateFilter: 'all',
+ id: template.id,
+ details: 'all'
+ }).then(response => {
+ if (response && response.listtemplatesresponse) {
+ const items = response.listtemplatesresponse.template
+ if (items && items.length > 0) {
+ this.template.details = items[0].details
+ this.updateTemplateParameters()
+ }
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ })
+ },
fetchTemplates (templateFilter, params) {
params = params || {}
if (params.keyword || params.category !== templateFilter) {
@@ -1301,6 +1488,7 @@ export default {
}
params.zoneid = _.get(this.zone, 'id')
params.templatefilter = templateFilter
+ params.details = 'min'
return new Promise((resolve, reject) => {
api('listTemplates', params).then((response) => {
@@ -1425,6 +1613,120 @@ export default {
.replace(/>/g, '>')
return reversedValue
+ },
+ fetchTemplateNics (template) {
+ var nics = []
+ if (template && template.details && Object.keys(template.details).length > 0) {
+ var keys = Object.keys(template.details)
+ keys = keys.filter(key => key.startsWith('ACS-network-'))
+ for (var key of keys) {
+ var propertyMap = JSON.parse(template.details[key])
+ nics.push(propertyMap)
+ }
+ nics.sort(function (a, b) {
+ return a.InstanceID - b.InstanceID
+ })
+ }
+ return nics
+ },
+ fetchTemplateProperties (template) {
+ var properties = []
+ if (template && template.details && Object.keys(template.details).length > 0) {
+ var keys = Object.keys(template.details)
+ keys = keys.filter(key => key.startsWith('ACS-property-'))
+ for (var key of keys) {
+ var propertyMap = JSON.parse(template.details[key])
+ properties.push(propertyMap)
+ }
+ properties.sort(function (a, b) {
+ return a.label.localeCompare(b.label)
+ })
+ }
+ return properties
+ },
+ fetchTemplateConfigurations (template) {
+ var configurations = []
+ if (template && template.details && Object.keys(template.details).length > 0) {
+ var keys = Object.keys(template.details)
+ keys = keys.filter(key => key.startsWith('ACS-configuration-'))
+ for (var key of keys) {
+ var configuration = JSON.parse(template.details[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.cpunumber - b.cpunumber
+ })
+ }
+ return configurations
+ },
+ fetchTemplateLicenses (template) {
+ var licenses = []
+ if (template && template.details && Object.keys(template.details).length > 0) {
+ var keys = Object.keys(template.details)
+ keys = keys.filter(key => key.startsWith('ACS-eula-'))
+ for (var key of keys) {
+ var license = {
+ id: this.escapePropertyKey(key.replace(' ', '-')),
+ name: key.replace('ACS-eula-', ''),
+ text: template.details[key]
+ }
+ licenses.push(license)
+ }
+ }
+ return licenses
+ },
+ 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 = {}
+ if (this.templateConfigurationExists) {
+ setTimeout(() => {
+ this.selectedTemplateConfiguration = this.templateConfigurations[0]
+ if ('templateConfiguration' in this.form.fieldsStore.fieldsMeta) {
+ this.updateFieldValue('templateConfiguration', this.selectedTemplateConfiguration.id)
+ }
+ this.updateComputeOffering(null) // reset as existing selection may be incompatible
+ }, 500)
+ }
+ }
+ },
+ onSelectTemplateConfigurationId (value) {
+ this.selectedTemplateConfiguration = _.find(this.templateConfigurations, (option) => option.id === value)
+ this.updateComputeOffering(null)
+ },
+ updateTemplateConfigurationOfferingDetails (offeringId) {
+ 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)
+ }
+ }
}
}
}
diff --git a/src/views/compute/wizard/ComputeOfferingSelection.vue b/src/views/compute/wizard/ComputeOfferingSelection.vue
index 271d1fb..7e0015b 100644
--- a/src/views/compute/wizard/ComputeOfferingSelection.vue
+++ b/src/views/compute/wizard/ComputeOfferingSelection.vue
@@ -81,6 +81,18 @@ export default {
zoneId: {
type: String,
default: () => ''
+ },
+ minimumCpunumber: {
+ type: Number,
+ default: 0
+ },
+ minimumCpuspeed: {
+ type: Number,
+ default: 0
+ },
+ minimumMemory: {
+ type: Number,
+ default: 0
}
},
data () {
@@ -115,36 +127,58 @@ export default {
computed: {
tableSource () {
return this.computeItems.map((item) => {
- var cpuNumberValue = item.cpunumber + ''
+ var maxCpuNumber = item.cpunumber
+ var maxCpuSpeed = item.cpuspeed
+ var maxMemory = item.memory
+ var cpuNumberValue = (item.cpunumber !== null && item.cpunumber !== undefined && item.cpunumber > 0) ? item.cpunumber + '' : ''
var cpuSpeedValue = (item.cpuspeed !== null && item.cpuspeed !== undefined && item.cpuspeed > 0) ? parseFloat(item.cpuspeed / 1000.0).toFixed(2) + '' : ''
- var ramValue = item.memory + ''
+ var ramValue = (item.memory !== null && item.memory !== undefined && item.memory > 0) ? item.memory + '' : ''
if (item.iscustomized === true) {
- cpuNumberValue = ''
- ramValue = ''
if ('serviceofferingdetails' in item &&
'mincpunumber' in item.serviceofferingdetails &&
'maxcpunumber' in item.serviceofferingdetails) {
+ maxCpuNumber = item.serviceofferingdetails.maxcpunumber
cpuNumberValue = item.serviceofferingdetails.mincpunumber + '-' + item.serviceofferingdetails.maxcpunumber
}
if ('serviceofferingdetails' in item &&
'minmemory' in item.serviceofferingdetails &&
'maxmemory' in item.serviceofferingdetails) {
+ maxMemory = item.serviceofferingdetails.maxmemory
ramValue = item.serviceofferingdetails.minmemory + '-' + item.serviceofferingdetails.maxmemory
}
}
+ var disabled = false
+ if (this.minimumCpunumber > 0 && ((item.iscustomized === false && maxCpuNumber !== this.minimumCpunumber) ||
+ (item.iscustomized === true && maxCpuNumber < this.minimumCpunumber))) {
+ disabled = true
+ }
+ if (disabled === false && this.minimumCpuspeed > 0 && maxCpuSpeed && maxCpuSpeed !== this.minimumCpuspeed) {
+ disabled = true
+ }
+ if (disabled === false && maxMemory && this.minimumMemory > 0 &&
+ ((item.iscustomized === false && maxMemory !== this.minimumMemory) ||
+ (item.iscustomized === true && maxMemory < this.minimumMemory))) {
+ disabled = true
+ }
return {
key: item.id,
name: item.name,
cpu: cpuNumberValue.length > 0 ? `${cpuNumberValue} CPU x ${cpuSpeedValue} Ghz` : '',
- ram: ramValue.length > 0 ? `${ramValue} MB` : ''
+ ram: ramValue.length > 0 ? `${ramValue} MB` : '',
+ disabled: disabled
}
})
},
rowSelection () {
return {
type: 'radio',
- selectedRowKeys: this.selectedRowKeys,
- onChange: this.onSelectRow
+ selectedRowKeys: this.selectedRowKeys || [],
+ onChange: this.onSelectRow,
+ getCheckboxProps: (record) => ({
+ props: {
+ disabled: record.disabled
+ }
+ })
}
}
},
@@ -152,6 +186,8 @@ export default {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
+ } else {
+ this.selectedRowKeys = []
}
},
loading () {
diff --git a/src/views/image/RegisterOrUploadTemplate.vue b/src/views/image/RegisterOrUploadTemplate.vue
index 9cb3e76..65c8bb9 100644
--- a/src/views/image/RegisterOrUploadTemplate.vue
+++ b/src/views/image/RegisterOrUploadTemplate.vue
@@ -190,6 +190,13 @@
</a-form-item>
</a-col>
</a-row>
+ <a-row :gutter="12" v-if="allowed && hyperVMWShow && currentForm !== 'Upload' && deployAsIsSupported">
+ <a-col :md="24" :lg="12">
+ <a-form-item :label="$t('label.deployasis')">
+ <a-switch v-decorator="['deployasis']" />
+ </a-form-item>
+ </a-col>
+ </a-row>
<a-row :gutter="12" v-if="allowed && hyperXenServerShow">
<a-form-item v-if="hyperXenServerShow" :label="$t('label.xenservertoolsversion61plus')">
<a-switch
@@ -440,6 +447,11 @@ export default {
mounted () {
this.fetchData()
},
+ computed: {
+ deployAsIsSupported () {
+ return this.apiConfig.params.filter(x => x.name === 'deployasis').length > 0
+ }
+ },
methods: {
fetchData () {
this.fetchZone()