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(/&gt;/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()