diff --git a/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java b/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java index 389800bc859..15d63587490 100644 --- a/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java +++ b/api/src/main/java/com/cloud/agent/api/storage/OVFHelper.java @@ -19,6 +19,7 @@ package com.cloud.agent.api.storage; import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; @@ -38,7 +39,9 @@ import org.apache.commons.lang.math.NumberUtils; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.cloud.agent.api.to.DatadiskTO; @@ -69,6 +72,92 @@ public class OVFHelper { } } + /** + * Get the text value of a node's child with name "childNodeName", null if not present + * Example: + * + * Text value + * + */ + private String getChildNodeValue(Node node, String childNodeName) { + if (node != null && node.hasChildNodes()) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node value = childNodes.item(i); + if (value != null && value.getNodeName().equals(childNodeName)) { + return value.getTextContent(); + } + } + } + return null; + } + + /** + * Create OVFProperty class from the parsed node. Note that some fields may not be present. + * The key attribute is required + */ + protected OVFPropertyTO createOVFPropertyFromNode(Node node) { + Element property = (Element) node; + String key = property.getAttribute("ovf:key"); + if (StringUtils.isBlank(key)) { + return null; + } + + String value = property.getAttribute("ovf:value"); + String type = property.getAttribute("ovf:type"); + String qualifiers = property.getAttribute("ovf:qualifiers"); + String userConfigurableStr = property.getAttribute("ovf:userConfigurable"); + boolean userConfigurable = StringUtils.isNotBlank(userConfigurableStr) && + userConfigurableStr.equalsIgnoreCase("true"); + String passStr = property.getAttribute("ovf:password"); + boolean password = StringUtils.isNotBlank(passStr) && passStr.equalsIgnoreCase("true"); + String label = getChildNodeValue(node, "Label"); + String description = getChildNodeValue(node, "Description"); + return new OVFPropertyTO(key, type, value, qualifiers, userConfigurable, label, description, password); + } + + /** + * Retrieve OVF properties from a parsed OVF file, with attribute 'ovf:userConfigurable' set to true + */ + private List getConfigurableOVFPropertiesFromDocument(Document doc) { + List props = new ArrayList<>(); + NodeList properties = doc.getElementsByTagName("Property"); + if (properties != null) { + for (int i = 0; i < properties.getLength(); i++) { + Node node = properties.item(i); + if (node == null) { + continue; + } + OVFPropertyTO prop = createOVFPropertyFromNode(node); + if (prop != null && prop.isUserConfigurable()) { + props.add(prop); + } + } + } + return props; + } + + /** + * Get properties from OVF file located on ovfFilePath + */ + public List getOVFPropertiesFromFile(String ovfFilePath) throws ParserConfigurationException, IOException, SAXException { + if (StringUtils.isBlank(ovfFilePath)) { + return new ArrayList<>(); + } + File ovfFile = new File(ovfFilePath); + final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ovfFile); + return getConfigurableOVFPropertiesFromDocument(doc); + } + + /** + * Get properties from OVF XML string + */ + protected List getOVFPropertiesXmlString(final String ovfFilePath) throws ParserConfigurationException, IOException, SAXException { + InputSource is = new InputSource(new StringReader(ovfFilePath)); + final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is); + return getConfigurableOVFPropertiesFromDocument(doc); + } + public List getOVFVolumeInfo(final String ovfFilePath) { if (StringUtils.isBlank(ovfFilePath)) { return new ArrayList(); diff --git a/api/src/main/java/com/cloud/agent/api/storage/OVFProperty.java b/api/src/main/java/com/cloud/agent/api/storage/OVFProperty.java new file mode 100644 index 00000000000..ac9ae7721b0 --- /dev/null +++ b/api/src/main/java/com/cloud/agent/api/storage/OVFProperty.java @@ -0,0 +1,33 @@ +// +// 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. +// + +package com.cloud.agent.api.storage; + +public interface OVFProperty { + + Long getTemplateId(); + String getKey(); + String getType(); + String getValue(); + String getQualifiers(); + Boolean isUserConfigurable(); + String getLabel(); + String getDescription(); + Boolean isPassword(); +} \ No newline at end of file diff --git a/api/src/main/java/com/cloud/agent/api/storage/OVFPropertyTO.java b/api/src/main/java/com/cloud/agent/api/storage/OVFPropertyTO.java new file mode 100644 index 00000000000..abf743ae713 --- /dev/null +++ b/api/src/main/java/com/cloud/agent/api/storage/OVFPropertyTO.java @@ -0,0 +1,134 @@ +// +// 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. +// + +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.LogLevel; + +/** + * Used to represent travel objects like: + * + * + * Select the route/gateway type. + * Choose "Default Route" to route all traffic through the Management gateway. Use this option when enabling Smart Licensing registration at initial deployment. + * Choose "Remote HTTP and SSH Client Routes" to route only traffic destined for the management client(s), when they are on remote networks. + * + */ +public class OVFPropertyTO implements OVFProperty { + + private String key; + private String type; + @LogLevel(LogLevel.Log4jLevel.Off) + private String value; + private String qualifiers; + private Boolean userConfigurable; + private String label; + private String description; + private Boolean password; + + public OVFPropertyTO() { + } + + public OVFPropertyTO(String key, String value, boolean password) { + this.key = key; + this.value = value; + this.password = password; + } + + public OVFPropertyTO(String key, String type, String value, String qualifiers, boolean userConfigurable, + String label, String description, boolean password) { + this.key = key; + this.type = type; + this.value = value; + this.qualifiers = qualifiers; + this.userConfigurable = userConfigurable; + this.label = label; + this.description = description; + this.password = password; + } + + @Override + public Long getTemplateId() { + return null; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getQualifiers() { + return qualifiers; + } + + public void setQualifiers(String qualifiers) { + this.qualifiers = qualifiers; + } + + public Boolean isUserConfigurable() { + return userConfigurable; + } + + public void setUserConfigurable(Boolean userConfigurable) { + this.userConfigurable = userConfigurable; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean isPassword() { + return password; + } + + public void setPassword(Boolean password) { + this.password = password; + } +} diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index e5623acb374..d25ffe3aa30 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -20,7 +20,10 @@ import java.util.List; import java.util.Map; import java.util.HashMap; +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.template.VirtualMachineTemplate.BootloaderType; +import com.cloud.utils.Pair; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; @@ -75,6 +78,8 @@ public class VirtualMachineTO { Map guestOsDetails = new HashMap(); Map extraConfig = new HashMap<>(); + @LogLevel(LogLevel.Log4jLevel.Off) + Pair> ovfProperties; public VirtualMachineTO(long id, String instanceName, VirtualMachine.Type type, int cpus, Integer speed, long minRam, long maxRam, BootloaderType bootloader, String os, boolean enableHA, boolean limitCpuUse, String vncPassword) { @@ -367,4 +372,12 @@ public class VirtualMachineTO { public Map getExtraConfig() { return extraConfig; } + + public Pair> getOvfProperties() { + return ovfProperties; + } + + public void setOvfProperties(Pair> ovfProperties) { + this.ovfProperties = ovfProperties; + } } diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 74090ec40e6..99eb827c122 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -217,7 +217,8 @@ public interface UserVmService { Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException, + Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFProperties) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** @@ -298,7 +299,8 @@ public interface UserVmService { List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException, + Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFProperties) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** @@ -376,7 +378,8 @@ public interface UserVmService { UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, - Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap) + Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, + Map templateOvfPropertiesMap) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index d9e2e7f1fee..fb44a8a11f4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -199,6 +199,7 @@ public class ApiConstants { public static final String ISO_GUEST_OS_NONE = "None"; public static final String JOB_ID = "jobid"; public static final String JOB_STATUS = "jobstatus"; + public static final String LABEL = "label"; public static final String LASTNAME = "lastname"; public static final String LEVEL = "level"; public static final String LENGTH = "length"; @@ -233,6 +234,7 @@ public class ApiConstants { public static final String OS_NAME_FOR_HYPERVISOR = "osnameforhypervisor"; public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate"; public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled"; + public static final String OVF_PROPERTIES = "ovfproperties"; public static final String PARAMS = "params"; public static final String PARENT_ID = "parentid"; public static final String PARENT_DOMAIN_ID = "parentdomainid"; @@ -277,6 +279,7 @@ public class ApiConstants { public static final String RESPONSE = "response"; public static final String REVERTABLE = "revertable"; public static final String REGISTERED = "registered"; + public static final String QUALIFIERS = "qualifiers"; public static final String QUERY_FILTER = "queryfilter"; public static final String SCHEDULE = "schedule"; public static final String SCOPE = "scope"; @@ -334,6 +337,7 @@ public class ApiConstants { public static final String USER_ID = "userid"; public static final String USE_SSL = "ssl"; public static final String USERNAME = "username"; + public static final String USER_CONFIGURABLE = "userconfigurable"; public static final String USER_SECURITY_GROUP_LIST = "usersecuritygrouplist"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String Update_IN_SEQUENCE = "updateinsequence"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplateOVFProperties.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplateOVFProperties.java new file mode 100644 index 00000000000..2a620c9abe5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplateOVFProperties.java @@ -0,0 +1,68 @@ +// 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. +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.TemplateOVFPropertyResponse; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = ListTemplateOVFProperties.APINAME, + description = "List template OVF properties if available.", + responseObject = TemplateOVFPropertyResponse.class, + authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User}) +public class ListTemplateOVFProperties extends BaseListCmd { + + public static final String APINAME = "listTemplateOvfProperties"; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class, + description = "the template ID", required = true) + private Long templateId; + + public Long getTemplateId() { + return templateId; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + ListResponse response = _queryService.listTemplateOVFProperties(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 06acc32eb6e..ec1dc8174cc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -24,6 +24,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.cloud.agent.api.LogLevel; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ACL; @@ -205,6 +206,11 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG @Parameter(name = ApiConstants.COPY_IMAGE_TAGS, type = CommandType.BOOLEAN, since = "4.13", description = "if true the image tags (if any) will be copied to the VM, default value is false") private Boolean copyImageTags; + @Parameter(name = ApiConstants.OVF_PROPERTIES, type = CommandType.MAP, since = "4.13", + description = "used to specify the OVF properties.") + @LogLevel(LogLevel.Log4jLevel.Off) + private Map vmOvfProperties; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -253,6 +259,19 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG return customparameterMap; } + public Map getVmOVFProperties() { + Map map = new HashMap<>(); + if (MapUtils.isNotEmpty(vmOvfProperties)) { + Collection parameterCollection = vmOvfProperties.values(); + Iterator iterator = parameterCollection.iterator(); + while (iterator.hasNext()) { + HashMap entry = (HashMap)iterator.next(); + map.put(entry.get("key"), entry.get("value")); + } + } + return map; + } + public String getGroup() { return group; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateOVFPropertyResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateOVFPropertyResponse.java new file mode 100644 index 00000000000..83455a3fe6e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateOVFPropertyResponse.java @@ -0,0 +1,124 @@ +// 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. +package org.apache.cloudstack.api.response; + +import com.cloud.agent.api.storage.OVFProperty; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = OVFProperty.class) +public class TemplateOVFPropertyResponse extends BaseResponse { + + @SerializedName(ApiConstants.KEY) + @Param(description = "the ovf property key") + private String key; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "the ovf property type") + private String type; + + @SerializedName(ApiConstants.VALUE) + @Param(description = "the ovf property value") + private String value; + + @SerializedName(ApiConstants.PASSWORD) + @Param(description = "is the ovf property a password") + private Boolean password; + + @SerializedName(ApiConstants.QUALIFIERS) + @Param(description = "the ovf property qualifiers") + private String qualifiers; + + @SerializedName(ApiConstants.USER_CONFIGURABLE) + @Param(description = "is the ovf property user configurable") + private Boolean userConfigurable; + + @SerializedName(ApiConstants.LABEL) + @Param(description = "the ovf property label") + private String label; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the ovf property label") + private String description; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getQualifiers() { + return qualifiers; + } + + public void setQualifiers(String qualifiers) { + this.qualifiers = qualifiers; + } + + public Boolean getUserConfigurable() { + return userConfigurable; + } + + public void setUserConfigurable(Boolean userConfigurable) { + this.userConfigurable = userConfigurable; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getPassword() { + return password; + } + + public void setPassword(Boolean password) { + this.password = password; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java index a83fe426b56..81fc2f37b0d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java @@ -202,7 +202,7 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements private Boolean requiresHvm; public TemplateResponse() { - tags = new LinkedHashSet(); + tags = new LinkedHashSet<>(); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index e5ef658dce8..68dc31f6708 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -43,6 +43,7 @@ import org.apache.cloudstack.api.command.user.project.ListProjectsCmd; import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; import org.apache.cloudstack.api.command.user.securitygroup.ListSecurityGroupsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; +import org.apache.cloudstack.api.command.user.template.ListTemplateOVFProperties; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; @@ -71,6 +72,7 @@ import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.StorageTagResponse; +import org.apache.cloudstack.api.response.TemplateOVFPropertyResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -166,4 +168,6 @@ public interface QueryService { ListResponse searchForHostTags(ListHostTagsCmd cmd); ListResponse listManagementServers(ListMgmtsCmd cmd); + + ListResponse listTemplateOVFProperties(ListTemplateOVFProperties cmd); } diff --git a/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java b/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java new file mode 100644 index 00000000000..8aa9852fb25 --- /dev/null +++ b/api/src/test/java/com/cloud/agent/api/storage/OVFHelperTest.java @@ -0,0 +1,55 @@ +// 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. +package com.cloud.agent.api.storage; + +import org.junit.Assert; +import org.junit.Test; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.util.List; + +public class OVFHelperTest { + + private String ovfFileProductSection = + "" + + "VM Arguments" + + "" + + "" + + "This will enable the SSHD service and configure the specified public key" + + "" + + "" + + "" + + "This allows to pass any text to the appliance. The value should be encoded in base64" + + "" + + ""; + + private OVFHelper ovfHelper = new OVFHelper(); + + @Test + public void testGetOVFPropertiesValidOVF() throws IOException, SAXException, ParserConfigurationException { + List props = ovfHelper.getOVFPropertiesXmlString(ovfFileProductSection); + Assert.assertEquals(2, props.size()); + } + + @Test(expected = SAXParseException.class) + public void testGetOVFPropertiesInvalidOVF() throws IOException, SAXException, ParserConfigurationException { + ovfHelper.getOVFPropertiesXmlString(ovfFileProductSection + "xxxxxxxxxxxxxxxxx"); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java index fdc47b4c99f..9859c3f83d0 100644 --- a/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/storage/DownloadAnswer.java @@ -20,9 +20,11 @@ package com.cloud.agent.api.storage; import java.io.File; +import java.util.List; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; +import com.cloud.agent.api.LogLevel; import com.cloud.storage.VMTemplateStorageResourceAssoc; import com.cloud.storage.VMTemplateStorageResourceAssoc.Status; @@ -36,6 +38,8 @@ public class DownloadAnswer extends Answer { private long templateSize = 0L; private long templatePhySicalSize = 0L; private String checkSum; + @LogLevel(LogLevel.Log4jLevel.Off) + private List ovfProperties; public String getCheckSum() { return checkSum; @@ -146,4 +150,11 @@ public class DownloadAnswer extends Answer { return templatePhySicalSize; } + public List getOvfProperties() { + return ovfProperties; + } + + public void setOvfProperties(List ovfProperties) { + this.ovfProperties = ovfProperties; + } } diff --git a/core/src/main/java/com/cloud/storage/template/OVAProcessor.java b/core/src/main/java/com/cloud/storage/template/OVAProcessor.java index f29efb46b52..d771c67acec 100644 --- a/core/src/main/java/com/cloud/storage/template/OVAProcessor.java +++ b/core/src/main/java/com/cloud/storage/template/OVAProcessor.java @@ -26,6 +26,8 @@ import java.util.Map; import javax.naming.ConfigurationException; import javax.xml.parsers.DocumentBuilderFactory; +import com.cloud.agent.api.storage.OVFPropertyTO; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -106,6 +108,11 @@ public class OVAProcessor extends AdapterBase implements Processor { try { OVFHelper ovfHelper = new OVFHelper(); List disks = ovfHelper.getOVFVolumeInfo(ovfFile); + List ovfProperties = ovfHelper.getOVFPropertiesFromFile(ovfFile); + if (CollectionUtils.isNotEmpty(ovfProperties)) { + s_logger.info("Found " + ovfProperties.size() + " configurable OVF properties"); + info.ovfProperties = ovfProperties; + } } catch (Exception e) { s_logger.info("The ovf file " + ovfFile + " is invalid ", e); throw new InternalErrorException("OVA package has bad ovf file " + e.getMessage(), e); diff --git a/core/src/main/java/com/cloud/storage/template/Processor.java b/core/src/main/java/com/cloud/storage/template/Processor.java index c8ee18109a1..4bb714a7ab9 100644 --- a/core/src/main/java/com/cloud/storage/template/Processor.java +++ b/core/src/main/java/com/cloud/storage/template/Processor.java @@ -21,7 +21,9 @@ package com.cloud.storage.template; import java.io.File; import java.io.IOException; +import java.util.List; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.exception.InternalErrorException; import com.cloud.storage.Storage.ImageFormat; import com.cloud.utils.component.Adapter; @@ -52,6 +54,7 @@ public interface Processor extends Adapter { public long virtualSize; public String filename; public boolean isCorrupted; + public List ovfProperties; } long getVirtualSize(File file) throws IOException; diff --git a/engine/schema/src/main/java/com/cloud/storage/TemplateOVFPropertyVO.java b/engine/schema/src/main/java/com/cloud/storage/TemplateOVFPropertyVO.java new file mode 100644 index 00000000000..425b1f22e45 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/TemplateOVFPropertyVO.java @@ -0,0 +1,167 @@ +// 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. + +package com.cloud.storage; + +import com.cloud.agent.api.storage.OVFProperty; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "template_ovf_properties") +public class TemplateOVFPropertyVO implements OVFProperty { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "template_id") + private Long templateId; + + @Column(name = "key") + private String key; + + @Column(name = "type") + private String type; + + @Column(name = "value") + private String value; + + @Column(name = "qualifiers") + private String qualifiers; + + @Column(name = "password") + private Boolean password; + + @Column(name = "user_configurable") + private Boolean userConfigurable; + + @Column(name = "label") + private String label; + + @Column(name = "description") + private String description; + + public TemplateOVFPropertyVO() { + } + + public TemplateOVFPropertyVO(Long templateId, String key, String type, String value, String qualifiers, + Boolean userConfigurable, String label, String description, Boolean password) { + this.templateId = templateId; + this.key = key; + this.type = type; + this.value = value; + this.qualifiers = qualifiers; + this.userConfigurable = userConfigurable; + this.label = label; + this.description = description; + this.password = password; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Long getTemplateId() { + return templateId; + } + + public void setTemplateId(Long templateId) { + this.templateId = templateId; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getQualifiers() { + return qualifiers; + } + + public void setQualifiers(String qualifiers) { + this.qualifiers = qualifiers; + } + + @Override + public Boolean isUserConfigurable() { + return userConfigurable; + } + + public void setUserConfigurable(Boolean userConfigurable) { + this.userConfigurable = userConfigurable; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean isPassword() { + return password; + } + + public void setPassword(Boolean password) { + this.password = password; + } + + @Override + public String toString() { + return String.format("PROP - templateId=%s> key=%s value=%s type=%s qual=%s conf=%s label=%s desc=%s password=%s", + templateId, key, value, type, qualifiers, userConfigurable, label, description, password); + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDao.java new file mode 100644 index 00000000000..eb78f2023ac --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDao.java @@ -0,0 +1,31 @@ +// 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. + +package com.cloud.storage.dao; + +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.utils.db.GenericDao; + +import java.util.List; + +public interface TemplateOVFPropertiesDao extends GenericDao { + + boolean existsOption(long templateId, String key); + TemplateOVFPropertyVO findByTemplateAndKey(long templateId, String key); + void saveOptions(List opts); + List listByTemplateId(long templateId); +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDaoImpl.java new file mode 100644 index 00000000000..cf6a280b034 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/TemplateOVFPropertiesDaoImpl.java @@ -0,0 +1,78 @@ +// 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. + +package com.cloud.storage.dao; + +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class TemplateOVFPropertiesDaoImpl extends GenericDaoBase implements TemplateOVFPropertiesDao { + + private final static Logger s_logger = Logger.getLogger(TemplateOVFPropertiesDaoImpl.class); + + SearchBuilder OptionsSearchBuilder; + + public TemplateOVFPropertiesDaoImpl() { + super(); + OptionsSearchBuilder = createSearchBuilder(); + OptionsSearchBuilder.and("templateid", OptionsSearchBuilder.entity().getTemplateId(), SearchCriteria.Op.EQ); + OptionsSearchBuilder.and("key", OptionsSearchBuilder.entity().getKey(), SearchCriteria.Op.EQ); + OptionsSearchBuilder.done(); + } + + @Override + public boolean existsOption(long templateId, String key) { + return findByTemplateAndKey(templateId, key) != null; + } + + @Override + public TemplateOVFPropertyVO findByTemplateAndKey(long templateId, String key) { + SearchCriteria sc = OptionsSearchBuilder.create(); + sc.setParameters("templateid", templateId); + sc.setParameters("key", key); + return findOneBy(sc); + } + + @Override + public void saveOptions(List opts) { + if (CollectionUtils.isEmpty(opts)) { + return; + } + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + for (TemplateOVFPropertyVO opt : opts) { + persist(opt); + } + txn.commit(); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = OptionsSearchBuilder.create(); + sc.setParameters("templateid", templateId); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 1cea7aa4b11..3e0d67b61a4 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -287,4 +287,5 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql b/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql index 904e76e8df0..cdd17da9ae5 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41200to41300.sql @@ -386,3 +386,19 @@ CREATE TABLE `cloud`.`direct_download_certificate_host_map` ( CONSTRAINT `fk_direct_download_certificate_host_map__certificate_id` FOREIGN KEY (`certificate_id`) REFERENCES `direct_download_certificate` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- [Vmware] Allow configuring appliances on the VM instance wizard when OVF properties are available +CREATE TABLE `cloud`.`template_ovf_properties` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `template_id` bigint(20) unsigned NOT NULL, + `key` VARCHAR(100) NOT NULL, + `type` VARCHAR(45) DEFAULT NULL, + `value` VARCHAR(100) DEFAULT NULL, + `password` TINYINT(1) NOT NULL DEFAULT '0', + `qualifiers` TEXT DEFAULT NULL, + `user_configurable` TINYINT(1) NOT NULL DEFAULT '0', + `label` TEXT DEFAULT NULL, + `description` TEXT DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_template_ovf_properties__template_id` FOREIGN KEY (`template_id`) REFERENCES `vm_template`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java index 1c6f1e70660..dec9b76dbc8 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/image/BaseImageStoreDriverImpl.java @@ -27,7 +27,12 @@ import java.util.Map; import javax.inject.Inject; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.storage.Upload; +import com.cloud.storage.dao.TemplateOVFPropertiesDao; +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.utils.crypt.DBEncryptionUtil; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; @@ -99,6 +104,8 @@ public abstract class BaseImageStoreDriverImpl implements ImageStoreDriver { AccountDao _accountDao; @Inject ResourceLimitService _resourceLimitMgr; + @Inject + TemplateOVFPropertiesDao templateOvfPropertiesDao; protected String _proxy = null; @@ -162,6 +169,29 @@ public abstract class BaseImageStoreDriverImpl implements ImageStoreDriver { } } + /** + * Persist OVF properties as template details for template with id = templateId + */ + private void persistOVFProperties(List ovfProperties, long templateId) { + List listToPersist = new ArrayList<>(); + for (OVFPropertyTO property : ovfProperties) { + if (!templateOvfPropertiesDao.existsOption(templateId, property.getKey())) { + TemplateOVFPropertyVO option = new TemplateOVFPropertyVO(templateId, property.getKey(), property.getType(), + property.getValue(), property.getQualifiers(), property.isUserConfigurable(), + property.getLabel(), property.getDescription(), property.isPassword()); + if (property.isPassword()) { + String encryptedPassword = DBEncryptionUtil.encrypt(property.getValue()); + option.setValue(encryptedPassword); + } + listToPersist.add(option); + } + } + if (CollectionUtils.isNotEmpty(listToPersist)) { + s_logger.debug("Persisting " + listToPersist.size() + " OVF properties for template " + templateId); + templateOvfPropertiesDao.saveOptions(listToPersist); + } + } + protected Void createTemplateAsyncCallback(AsyncCallbackDispatcher callback, CreateContext context) { if (s_logger.isDebugEnabled()) { @@ -170,10 +200,14 @@ public abstract class BaseImageStoreDriverImpl implements ImageStoreDriver { DownloadAnswer answer = callback.getResult(); DataObject obj = context.data; DataStore store = obj.getDataStore(); + List ovfProperties = answer.getOvfProperties(); TemplateDataStoreVO tmpltStoreVO = _templateStoreDao.findByStoreTemplate(store.getId(), obj.getId()); if (tmpltStoreVO != null) { if (tmpltStoreVO.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOADED) { + if (CollectionUtils.isNotEmpty(ovfProperties)) { + persistOVFProperties(ovfProperties, obj.getId()); + } if (s_logger.isDebugEnabled()) { s_logger.debug("Template is already in DOWNLOADED state, ignore further incoming DownloadAnswer"); } @@ -213,6 +247,9 @@ public abstract class BaseImageStoreDriverImpl implements ImageStoreDriver { templateDaoBuilder.setChecksum(answer.getCheckSum()); _templateDao.update(obj.getId(), templateDaoBuilder); } + if (CollectionUtils.isNotEmpty(ovfProperties)) { + persistOVFProperties(ovfProperties, obj.getId()); + } CreateCmdResult result = new CreateCmdResult(null, null); caller.complete(result); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index 4777b738391..072ab9f6fed 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -23,14 +23,22 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import javax.inject.Inject; import com.cloud.agent.api.MigrateVmToPoolCommand; import com.cloud.agent.api.UnregisterVMCommand; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.agent.api.to.VolumeTO; import com.cloud.dc.ClusterDetailsDao; import com.cloud.storage.StoragePool; +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VMTemplateStorageResourceAssoc; +import com.cloud.storage.dao.TemplateOVFPropertiesDao; +import com.cloud.storage.dao.VMTemplatePoolDao; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; @@ -43,6 +51,7 @@ import org.apache.cloudstack.storage.command.StorageSubSystemCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; import org.apache.log4j.Logger; @@ -151,6 +160,10 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co PrimaryDataStoreDao _storagePoolDao; @Inject VolumeDataFactory _volFactory; + @Inject + private VMTemplatePoolDao templateSpoolDao; + @Inject + private TemplateOVFPropertiesDao templateOVFPropertiesDao; protected VMwareGuru() { super(); @@ -354,9 +367,66 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co } else { to.setPlatformEmulator(guestOsMapping.getGuestOsName()); } + + List ovfProperties = new ArrayList<>(); + for (String detailKey : details.keySet()) { + if (detailKey.startsWith(ApiConstants.OVF_PROPERTIES)) { + String ovfPropKey = detailKey.replace(ApiConstants.OVF_PROPERTIES + "-", ""); + TemplateOVFPropertyVO templateOVFPropertyVO = templateOVFPropertiesDao.findByTemplateAndKey(vm.getTemplateId(), ovfPropKey); + if (templateOVFPropertyVO == null) { + s_logger.warn(String.format("OVF property %s not found on template, discarding", ovfPropKey)); + continue; + } + String ovfValue = details.get(detailKey); + boolean isPassword = templateOVFPropertyVO.isPassword(); + OVFPropertyTO propertyTO = new OVFPropertyTO(ovfPropKey, ovfValue, isPassword); + ovfProperties.add(propertyTO); + } + } + + if (CollectionUtils.isNotEmpty(ovfProperties)) { + removeOvfPropertiesFromDetails(ovfProperties, details); + String templateInstallPath = null; + List rootDiskList = vm.getDisks().stream().filter(x -> x.getType() == Volume.Type.ROOT).collect(Collectors.toList()); + if (rootDiskList.size() != 1) { + throw new CloudRuntimeException("Did not find only one root disk for VM " + vm.getHostName()); + } + + DiskTO rootDiskTO = rootDiskList.get(0); + DataStoreTO dataStore = rootDiskTO.getData().getDataStore(); + StoragePoolVO storagePoolVO = _storagePoolDao.findByUuid(dataStore.getUuid()); + long dataCenterId = storagePoolVO.getDataCenterId(); + List pools = _storagePoolDao.listByDataCenterId(dataCenterId); + for (StoragePoolVO pool : pools) { + VMTemplateStoragePoolVO ref = templateSpoolDao.findByPoolTemplate(pool.getId(), vm.getTemplateId()); + if (ref != null && ref.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOADED) { + templateInstallPath = ref.getInstallPath(); + break; + } + } + + if (templateInstallPath == null) { + throw new CloudRuntimeException("Did not find the template install path for template " + + vm.getTemplateId() + " on zone " + dataCenterId); + } + + Pair> pair = new Pair<>(templateInstallPath, ovfProperties); + to.setOvfProperties(pair); + } + return to; } + /* + Remove OVF properties from details to be sent to hypervisor (avoid duplicate data) + */ + private void removeOvfPropertiesFromDetails(List ovfProperties, Map details) { + for (OVFPropertyTO propertyTO : ovfProperties) { + String key = propertyTO.getKey(); + details.remove(ApiConstants.OVF_PROPERTIES + "-" + key); + } + } + /** * Decide in which cases nested virtualization should be enabled based on (1){@code globalNestedV}, (2){@code globalNestedVPerVM}, (3){@code localNestedV}
* Nested virtualization should be enabled when one of this cases: diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java index 834900f67d3..02f758ba844 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/resource/VmwareResource.java @@ -44,6 +44,19 @@ import java.util.UUID; import javax.naming.ConfigurationException; import javax.xml.datatype.XMLGregorianCalendar; +import com.cloud.agent.api.storage.OVFPropertyTO; +import com.cloud.utils.crypt.DBEncryptionUtil; +import com.vmware.vim25.ArrayUpdateOperation; +import com.vmware.vim25.VAppOvfSectionInfo; +import com.vmware.vim25.VAppOvfSectionSpec; +import com.vmware.vim25.VAppProductInfo; +import com.vmware.vim25.VAppProductSpec; +import com.vmware.vim25.VAppPropertyInfo; +import com.vmware.vim25.VAppPropertySpec; +import com.vmware.vim25.VmConfigInfo; +import com.vmware.vim25.VmConfigSpec; +import org.apache.commons.collections.CollectionUtils; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; @@ -2229,6 +2242,23 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa // config video card configureVideoCard(vmMo, vmSpec, vmConfigSpec); + // Set OVF properties (if available) + Pair> ovfPropsMap = vmSpec.getOvfProperties(); + VmConfigInfo templateVappConfig = null; + List ovfProperties = null; + if (ovfPropsMap != null) { + String vmTemplate = ovfPropsMap.first(); + s_logger.info("Find VM template " + vmTemplate); + VirtualMachineMO vmTemplateMO = dcMo.findVm(vmTemplate); + templateVappConfig = vmTemplateMO.getConfigInfo().getVAppConfig(); + ovfProperties = ovfPropsMap.second(); + // Set OVF properties (if available) + if (CollectionUtils.isNotEmpty(ovfProperties)) { + s_logger.info("Copying OVF properties from template and setting them to the values the user provided"); + copyVAppConfigsFromTemplate(templateVappConfig, ovfProperties, vmConfigSpec); + } + } + // // Configure VM // @@ -2316,6 +2346,87 @@ public class VmwareResource implements StoragePoolResource, ServerResource, Vmwa } } + + /** + * Set the ovf section spec from existing vApp configuration + */ + protected List copyVAppConfigOvfSectionFromOVF(VmConfigInfo vAppConfig) { + List ovfSection = vAppConfig.getOvfSection(); + List specs = new ArrayList<>(); + for (VAppOvfSectionInfo info : ovfSection) { + VAppOvfSectionSpec spec = new VAppOvfSectionSpec(); + spec.setInfo(info); + spec.setOperation(ArrayUpdateOperation.ADD); + specs.add(spec); + } + return specs; + } + + private Map> getOVFMap(List props) { + Map> map = new HashMap<>(); + for (OVFPropertyTO prop : props) { + Pair pair = new Pair<>(prop.getValue(), prop.isPassword()); + map.put(prop.getKey(), pair); + } + return map; + } + + /** + * Set the properties section from existing vApp configuration and values set on ovfProperties + */ + protected List copyVAppConfigPropertySectionFromOVF(VmConfigInfo vAppConfig, List ovfProperties) { + List productFromOvf = vAppConfig.getProperty(); + List specs = new ArrayList<>(); + Map> ovfMap = getOVFMap(ovfProperties); + for (VAppPropertyInfo info : productFromOvf) { + VAppPropertySpec spec = new VAppPropertySpec(); + if (ovfMap.containsKey(info.getId())) { + Pair pair = ovfMap.get(info.getId()); + String value = pair.first(); + boolean isPassword = pair.second(); + info.setValue(isPassword ? DBEncryptionUtil.decrypt(value) : value); + } + spec.setInfo(info); + spec.setOperation(ArrayUpdateOperation.ADD); + specs.add(spec); + } + return specs; + } + + /** + * Set the product section spec from existing vApp configuration + */ + protected List copyVAppConfigProductSectionFromOVF(VmConfigInfo vAppConfig) { + List productFromOvf = vAppConfig.getProduct(); + List specs = new ArrayList<>(); + for (VAppProductInfo info : productFromOvf) { + VAppProductSpec spec = new VAppProductSpec(); + spec.setInfo(info); + spec.setOperation(ArrayUpdateOperation.ADD); + specs.add(spec); + } + return specs; + } + + /** + * Set the vApp configuration to vmConfig spec, copying existing configuration from vAppConfig + * and seting properties values from ovfProperties + */ + protected void copyVAppConfigsFromTemplate(VmConfigInfo vAppConfig, + List ovfProperties, + VirtualMachineConfigSpec vmConfig) throws Exception { + VmConfigSpec vmConfigSpec = new VmConfigSpec(); + vmConfigSpec.getEula().addAll(vAppConfig.getEula()); + vmConfigSpec.setInstallBootStopDelay(vAppConfig.getInstallBootStopDelay()); + vmConfigSpec.setInstallBootRequired(vAppConfig.isInstallBootRequired()); + vmConfigSpec.setIpAssignment(vAppConfig.getIpAssignment()); + vmConfigSpec.getOvfEnvironmentTransport().addAll(vAppConfig.getOvfEnvironmentTransport()); + vmConfigSpec.getProduct().addAll(copyVAppConfigProductSectionFromOVF(vAppConfig)); + vmConfigSpec.getProperty().addAll(copyVAppConfigPropertySectionFromOVF(vAppConfig, ovfProperties)); + vmConfigSpec.getOvfSection().addAll(copyVAppConfigOvfSectionFromOVF(vAppConfig)); + vmConfig.setVAppConfig(vmConfigSpec); + } + private String appendFileType(String path, String fileType) { if (path.toLowerCase().endsWith(fileType.toLowerCase())) { return path; diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/storage/resource/VmwareStorageProcessor.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/storage/resource/VmwareStorageProcessor.java index 9d6aca4f03c..420ac440d67 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/storage/resource/VmwareStorageProcessor.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/storage/resource/VmwareStorageProcessor.java @@ -34,6 +34,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import com.vmware.vim25.VmConfigInfo; import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; @@ -517,12 +518,18 @@ public class VmwareStorageProcessor implements StorageProcessor { hyperHost.importVmFromOVF(srcFileName, vmName, datastoreMo, "thin"); VirtualMachineMO vmMo = hyperHost.findVmOnHyperHost(vmName); + VmConfigInfo vAppConfig; if (vmMo == null) { String msg = "Failed to import OVA template. secondaryStorage: " + secondaryStorageUrl + ", templatePathAtSecondaryStorage: " + templatePathAtSecondaryStorage + ", templateName: " + templateName + ", templateUuid: " + templateUuid; s_logger.error(msg); throw new Exception(msg); + } else { + vAppConfig = vmMo.getConfigInfo().getVAppConfig(); + if (vAppConfig != null) { + s_logger.info("Found vApp configuration"); + } } OVAProcessor processor = new OVAProcessor(); @@ -536,7 +543,9 @@ public class VmwareStorageProcessor implements StorageProcessor { // the same template may be deployed with multiple copies at per-datastore per-host basis, // save the original template name from CloudStack DB as the UUID to associate them. vmMo.setCustomFieldValue(CustomFieldConstants.CLOUD_UUID, templateName); - vmMo.markAsTemplate(); + if (vAppConfig == null) { + vmMo.markAsTemplate(); + } } else { vmMo.destroy(); diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index ee56cbb1c13..5515efebc15 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -31,6 +31,9 @@ import java.util.stream.Stream; import javax.inject.Inject; +import com.cloud.agent.api.storage.OVFProperty; +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.storage.dao.TemplateOVFPropertiesDao; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; import org.apache.cloudstack.affinity.AffinityGroupResponse; @@ -70,6 +73,7 @@ import org.apache.cloudstack.api.command.user.project.ListProjectsCmd; import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; import org.apache.cloudstack.api.command.user.securitygroup.ListSecurityGroupsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; +import org.apache.cloudstack.api.command.user.template.ListTemplateOVFProperties; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; @@ -98,6 +102,7 @@ import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.StorageTagResponse; +import org.apache.cloudstack.api.response.TemplateOVFPropertyResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -387,6 +392,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject ManagementServerHostDao managementServerHostDao; + @Inject + TemplateOVFPropertiesDao templateOVFPropertiesDao; + /* * (non-Javadoc) * @@ -3838,6 +3846,29 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return response; } + @Override + public ListResponse listTemplateOVFProperties(ListTemplateOVFProperties cmd) { + ListResponse response = new ListResponse<>(); + List result = new ArrayList<>(); + Long templateId = cmd.getTemplateId(); + List ovfProperties = templateOVFPropertiesDao.listByTemplateId(templateId); + for (OVFProperty property : ovfProperties) { + TemplateOVFPropertyResponse propertyResponse = new TemplateOVFPropertyResponse(); + propertyResponse.setKey(property.getKey()); + propertyResponse.setType(property.getType()); + propertyResponse.setValue(property.getValue()); + propertyResponse.setQualifiers(property.getQualifiers()); + propertyResponse.setUserConfigurable(property.isUserConfigurable()); + propertyResponse.setLabel(property.getLabel()); + propertyResponse.setDescription(property.getDescription()); + propertyResponse.setPassword(property.isPassword()); + propertyResponse.setObjectName("ovfproperty"); + result.add(propertyResponse); + } + response.setResponses(result); + return response; + } + @Override public String getConfigComponentName() { return QueryService.class.getSimpleName(); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 0e7374369c3..4ccfce9edb4 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -28,7 +28,9 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.vm.UserVmManager; import org.apache.cloudstack.affinity.AffinityGroupResponse; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants.VMDetails; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.NicExtraDhcpOptionResponse; @@ -311,7 +313,10 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation resourceDetails = new HashMap(); for (UserVmDetailVO userVmDetailVO : vmDetails) { - resourceDetails.put(userVmDetailVO.getName(), userVmDetailVO.getValue()); + if (!userVmDetailVO.getName().startsWith(ApiConstants.OVF_PROPERTIES) || + (UserVmManager.DisplayVMOVFProperties.value() && userVmDetailVO.getName().startsWith(ApiConstants.OVF_PROPERTIES))) { + resourceDetails.put(userVmDetailVO.getName(), userVmDetailVO.getValue()); + } } // Remove blacklisted settings if user is not admin if (caller.getType() != Account.ACCOUNT_TYPE_ADMIN) { diff --git a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java index f381ce093ee..1b936e10d82 100644 --- a/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java +++ b/server/src/main/java/com/cloud/network/as/AutoScaleManagerImpl.java @@ -1325,18 +1325,18 @@ public class AutoScaleManagerImpl extends ManagerBase implements AutoScale vm = _userVmService.createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, - null, true, null, null, null, null, null, null); + null, true, null, null, null, null, null, null, null); } else { if (zone.isSecurityGroupEnabled()) { vm = _userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, null, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, - null, null, true, null, null, null, null, null, null); + null, null, true, null, null, null, null, null, null, null); } else { vm = _userVmService.createAdvancedVirtualMachine(zone, serviceOffering, template, null, owner, "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), "autoScaleVm-" + asGroup.getId() + "-" + getCurrentTimeStampString(), - null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, addrs, true, null, null, null, null, null, null); + null, null, null, HypervisorType.XenServer, HTTPMethod.GET, null, null, null, addrs, true, null, null, null, null, null, null, null); } } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 305c0f16b29..f8b440a83bb 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -446,6 +446,7 @@ import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd; import org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd; +import org.apache.cloudstack.api.command.user.template.ListTemplateOVFProperties; import org.apache.cloudstack.api.command.user.template.ListTemplatePermissionsCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; @@ -3104,6 +3105,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(RevokeTemplateDirectDownloadCertificateCmd.class); cmdList.add(ListMgmtsCmd.class); cmdList.add(GetUploadParamsForIsoCmd.class); + cmdList.add(ListTemplateOVFProperties.class); // Out-of-band management APIs for admins cmdList.add(EnableOutOfBandManagementForHostCmd.class); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index fc2f558797e..717ff5a0fd2 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -44,12 +44,14 @@ import com.cloud.utils.Pair; * */ public interface UserVmManager extends UserVmService { - static final String EnableDynamicallyScaleVmCK = "enable.dynamic.scale.vm"; - static final String AllowUserExpungeRecoverVmCK ="allow.user.expunge.recover.vm"; - static final ConfigKey EnableDynamicallyScaleVm = new ConfigKey("Advanced", Boolean.class, EnableDynamicallyScaleVmCK, "false", + String EnableDynamicallyScaleVmCK = "enable.dynamic.scale.vm"; + String AllowUserExpungeRecoverVmCK ="allow.user.expunge.recover.vm"; + ConfigKey EnableDynamicallyScaleVm = new ConfigKey("Advanced", Boolean.class, EnableDynamicallyScaleVmCK, "false", "Enables/Disables dynamically scaling a vm", true, ConfigKey.Scope.Zone); - static final ConfigKey AllowUserExpungeRecoverVm = new ConfigKey("Advanced", Boolean.class, AllowUserExpungeRecoverVmCK, "false", + ConfigKey AllowUserExpungeRecoverVm = new ConfigKey("Advanced", Boolean.class, AllowUserExpungeRecoverVmCK, "false", "Determines whether users can expunge or recover their vm", true, ConfigKey.Scope.Account); + ConfigKey DisplayVMOVFProperties = new ConfigKey("Advanced", Boolean.class, "vm.display.ovf.properties", "false", + "Set display of VMs OVF properties as part of VM details", true); static final int MAX_USER_DATA_LENGTH_BYTES = 2048; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 1e47e5a90af..7f9f3d44974 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -40,6 +40,8 @@ import java.util.stream.Stream; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.storage.dao.TemplateOVFPropertiesDao; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.affinity.AffinityGroupService; @@ -484,6 +486,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private DpdkHelper dpdkHelper; @Inject private ResourceTagDao resourceTagDao; + @Inject + private TemplateOVFPropertiesDao templateOVFPropertiesDao; private ScheduledExecutorService _executor = null; private ScheduledExecutorService _vmIpFetchExecutor = null; @@ -531,7 +535,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private static final ConfigKey VmDestroyForcestop = new ConfigKey("Advanced", Boolean.class, "vm.destroy.forcestop", "false", "On destroy, force-stop takes this value ", true); - @Override public UserVmVO getVirtualMachine(long vmId) { return _vmDao.findById(vmId); @@ -2482,6 +2485,15 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } } + for (String detailName : details.keySet()) { + if (detailName.startsWith(ApiConstants.OVF_PROPERTIES)) { + String ovfPropKey = detailName.replace(ApiConstants.OVF_PROPERTIES + "-", ""); + TemplateOVFPropertyVO ovfPropertyVO = templateOVFPropertiesDao.findByTemplateAndKey(vmInstance.getTemplateId(), ovfPropKey); + if (ovfPropertyVO != null && ovfPropertyVO.isPassword()) { + details.put(detailName, DBEncryptionUtil.encrypt(details.get(detailName))); + } + } + } vmInstance.setDetails(details); _vmDao.saveDetails(vmInstance); } @@ -3074,7 +3086,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir public UserVm createBasicSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, - Map customParametes, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, + Map customParametes, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -3122,7 +3135,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap); + userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParametes, customId, dhcpOptionMap, + dataDiskTemplateToDiskOfferingMap, userVmOVFProperties); } @@ -3131,7 +3145,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir public UserVm createAdvancedSecurityGroupVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, - List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException, ConcurrentOperationException, + List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -3233,7 +3248,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, securityGroupIdList, group, httpmethod, - userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap); + userData, sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, dataDiskTemplateToDiskOfferingMap, + userVmOVFProperties); } @Override @@ -3241,7 +3257,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir public UserVm createAdvancedVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate template, List networkIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, String sshKeyPair, Map requestedIps, IpAddresses defaultIps, Boolean displayvm, String keyboard, List affinityGroupIdList, - Map customParametrs, String customId, Map> dhcpOptionsMap, Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, + Map customParametrs, String customId, Map> dhcpOptionsMap, Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFPropertiesMap) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -3338,7 +3355,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir verifyExtraDhcpOptionsNetwork(dhcpOptionsMap, networkList); return createVirtualMachine(zone, serviceOffering, template, hostName, displayName, owner, diskOfferingId, diskSize, networkList, null, group, httpmethod, userData, - sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, dataDiskTemplateToDiskOfferingMap); + sshKeyPair, hypervisor, caller, requestedIps, defaultIps, displayvm, keyboard, affinityGroupIdList, customParametrs, customId, dhcpOptionsMap, + dataDiskTemplateToDiskOfferingMap, userVmOVFPropertiesMap); } private void verifyExtraDhcpOptionsNetwork(Map> dhcpOptionsMap, List networkList) throws InvalidParameterValueException { @@ -3370,7 +3388,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private UserVm createVirtualMachine(DataCenter zone, ServiceOffering serviceOffering, VirtualMachineTemplate tmplt, String hostName, String displayName, Account owner, Long diskOfferingId, Long diskSize, List networkList, List securityGroupIdList, String group, HTTPMethod httpmethod, String userData, String sshKeyPair, HypervisorType hypervisor, Account caller, Map requestedIps, IpAddresses defaultIps, Boolean isDisplayVm, String keyboard, - List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map datadiskTemplateToDiskOfferringMap) throws InsufficientCapacityException, ResourceUnavailableException, + List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, + Map datadiskTemplateToDiskOfferringMap, + Map userVmOVFPropertiesMap) throws InsufficientCapacityException, ResourceUnavailableException, ConcurrentOperationException, StorageUnavailableException, ResourceAllocationException { _accountMgr.checkAccess(caller, null, true, owner); @@ -3744,7 +3764,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } UserVmVO vm = commitUserVm(zone, template, hostName, displayName, owner, diskOfferingId, diskSize, userData, caller, isDisplayVm, keyboard, accountId, userId, offering, - isIso, sshPublicKey, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, dhcpOptionMap, datadiskTemplateToDiskOfferringMap); + isIso, sshPublicKey, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, dhcpOptionMap, + datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap); // Assign instance to the group try { @@ -3804,7 +3825,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir private UserVmVO commitUserVm(final DataCenter zone, final VirtualMachineTemplate template, final String hostName, final String displayName, final Account owner, final Long diskOfferingId, final Long diskSize, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOfferingVO offering, final boolean isIso, final String sshPublicKey, final LinkedHashMap networkNicMap, - final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> extraDhcpOptionMap, final Map dataDiskTemplateToDiskOfferingMap) throws InsufficientCapacityException { + final long id, final String instanceName, final String uuidName, final HypervisorType hypervisorType, final Map customParameters, final Map> extraDhcpOptionMap, final Map dataDiskTemplateToDiskOfferingMap, + Map userVmOVFPropertiesMap) throws InsufficientCapacityException { return Transaction.execute(new TransactionCallbackWithException() { @Override public UserVmVO doInTransaction(TransactionStatus status) throws InsufficientCapacityException { @@ -3890,6 +3913,29 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } vm.setDetail(VmDetailConstants.DEPLOY_VM, "true"); + + if (MapUtils.isNotEmpty(userVmOVFPropertiesMap)) { + for (String key : userVmOVFPropertiesMap.keySet()) { + String detailKey = ApiConstants.OVF_PROPERTIES + "-" + key; + String value = userVmOVFPropertiesMap.get(key); + + // Sanitize boolean values to expected format and encrypt passwords + if (StringUtils.isNotBlank(value)) { + if (value.equalsIgnoreCase("True")) { + value = "True"; + } else if (value.equalsIgnoreCase("False")) { + value = "False"; + } else { + TemplateOVFPropertyVO ovfPropertyVO = templateOVFPropertiesDao.findByTemplateAndKey(vm.getTemplateId(), key); + if (ovfPropertyVO.isPassword()) { + value = DBEncryptionUtil.encrypt(value); + } + } + } + vm.setDetail(detailKey, value); + } + } + _vmDao.saveDetails(vm); s_logger.debug("Allocating in the DB for vm"); @@ -4230,7 +4276,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Map details = userVmDetailsDao.listDetailsKeyPairs(vm.getId()); vm.setDetails(details); - // add userdata info into vm profile Nic defaultNic = _networkModel.getDefaultNic(vm.getId()); if(defaultNic != null) { @@ -5005,19 +5050,22 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Boolean displayVm = cmd.isDisplayVm(); String keyboard = cmd.getKeyboard(); Map dataDiskTemplateToDiskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap(); + Map userVmOVFProperties = cmd.getVmOVFProperties(); if (zone.getNetworkType() == NetworkType.Basic) { if (cmd.getNetworkIds() != null) { throw new InvalidParameterValueException("Can't specify network Ids in Basic zone"); } else { vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId, size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData , sshKeyPairName , cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), - cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap); + cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), + dataDiskTemplateToDiskOfferingMap, userVmOVFProperties); } } else { if (zone.isSecurityGroupEnabled()) { vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, cmd.getNetworkIds(), getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, - cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap); + cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), + dataDiskTemplateToDiskOfferingMap, userVmOVFProperties); } else { if (cmd.getSecurityGroupIdList() != null && !cmd.getSecurityGroupIdList().isEmpty()) { @@ -5025,7 +5073,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } vm = createAdvancedVirtualMachine(zone, serviceOffering, template, cmd.getNetworkIds(), owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, sshKeyPairName, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), - cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap); + cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties); } } // check if this templateId has a child ISO @@ -6688,7 +6736,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {EnableDynamicallyScaleVm, AllowUserExpungeRecoverVm, VmIpFetchWaitInterval, VmIpFetchTrialMax, VmIpFetchThreadPoolMax, - VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig}; + VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties}; } @Override diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java index 2ace37f675a..bd5c59131b6 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/DownloadManagerImpl.java @@ -38,6 +38,7 @@ import java.util.concurrent.Executors; import javax.naming.ConfigurationException; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.storage.template.Processor; import com.cloud.storage.template.S3TemplateDownloader; import com.cloud.storage.template.TemplateDownloader; @@ -61,6 +62,7 @@ import org.apache.cloudstack.storage.command.DownloadProgressCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType; import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource; import org.apache.cloudstack.storage.resource.SecondaryStorageResource; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.storage.DownloadAnswer; @@ -124,6 +126,7 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager private long templatePhysicalSize; private final long id; private final ResourceType resourceType; + private List ovfProperties; public DownloadJob(TemplateDownloader td, String jobId, long id, String tmpltName, ImageFormat format, boolean hvm, Long accountId, String descr, String cksum, String installPathPrefix, ResourceType resourceType) { @@ -218,6 +221,14 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager public void setCheckSum(String checksum) { this.checksum = checksum; } + + public List getOvfProperties() { + return ovfProperties; + } + + public void setOvfProperties(List ovfProperties) { + this.ovfProperties = ovfProperties; + } } public static final Logger s_logger = Logger.getLogger(DownloadManagerImpl.class); @@ -471,6 +482,9 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager } dnld.setTemplatesize(info.virtualSize); dnld.setTemplatePhysicalSize(info.size); + if (CollectionUtils.isNotEmpty(info.ovfProperties)) { + dnld.setOvfProperties(info.ovfProperties); + } break; } } @@ -771,6 +785,9 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager answer = new DownloadAnswer(jobId, getDownloadPct(jobId), getDownloadError(jobId), getDownloadStatus2(jobId), getDownloadLocalPath(jobId), getInstallPath(jobId), getDownloadTemplateSize(jobId), getDownloadTemplatePhysicalSize(jobId), getDownloadCheckSum(jobId)); + if (CollectionUtils.isNotEmpty(dj.getOvfProperties())) { + answer.setOvfProperties(dj.getOvfProperties()); + } jobs.remove(jobId); return answer; default: diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index c1b79b23978..4f186cabf52 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -5819,6 +5819,11 @@ textarea { overflow: hidden; } +.multi-wizard .select-container .select .ovf-property { + max-width: 352px; + padding-left: 21px; +} + .multi-wizard .select-container .select .select-desc .name { margin: 0 0 5px; font-weight: bold; diff --git a/ui/index.html b/ui/index.html index c855e67f4f9..7c8aec3dced 100644 --- a/ui/index.html +++ b/ui/index.html @@ -433,6 +433,15 @@
+ + +
+
+ +
+
+
+
diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 53e9814fd4d..4ce59e08599 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1277,6 +1277,7 @@ var dictionary = { "label.outofbandmanagement.username":"Username", "label.override.guest.traffic":"Override Guest-Traffic", "label.override.public.traffic":"Override Public-Traffic", +"label.ovf.properties":"OVF Properties", "label.ovm.traffic.label":"OVM traffic label", "label.ovm3.cluster":"Native Clustering", "label.ovm3.pool":"Native Pooling", @@ -2253,6 +2254,7 @@ var dictionary = { "message.outofbandmanagement.disable":"Disable Out-of-band Management", "message.outofbandmanagement.enable":"Enable Out-of-band Management", "message.outofbandmanagement.issue":"Issue Out-of-band Management Power Action", +"message.ovf.properties.available":"There are OVF properties available for customizing the selected appliance. Please edit the values accordingly.", "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:", diff --git a/ui/scripts/instanceWizard.js b/ui/scripts/instanceWizard.js index 1513d84955c..a6fdfbb3b95 100644 --- a/ui/scripts/instanceWizard.js +++ b/ui/scripts/instanceWizard.js @@ -16,7 +16,7 @@ // under the License. (function($, cloudStack) { - var zoneObjs, hypervisorObjs, featuredTemplateObjs, communityTemplateObjs, myTemplateObjs, sharedTemplateObjs, featuredIsoObjs, communityIsoObjs, myIsoObjs, sharedIsoObjs, serviceOfferingObjs, community, networkObjs; + var zoneObjs, hypervisorObjs, featuredTemplateObjs, communityTemplateObjs, myTemplateObjs, sharedTemplateObjs, featuredIsoObjs, communityIsoObjs, myIsoObjs, sharedIsoObjs, serviceOfferingObjs, community, networkObjs, ovfProps; var selectedZoneObj, selectedTemplateObj, selectedHypervisor, selectedDiskOfferingObj; var selectedTemplateOrIso; //'select-template', 'select-iso' var step6ContainerType = 'nothing-to-select'; //'nothing-to-select', 'select-network', 'select-security-group', 'select-advanced-sg'(advanced sg-enabled zone) @@ -533,7 +533,6 @@ // if the user is leveraging a template, then we can show custom IOPS, if applicable var canShowCustomIopsForServiceOffering = (args.currentData["select-template"] != "select-iso" ? true : false); - // get serviceOfferingObjs var zoneid = args.currentData["zoneid"]; $(window).removeData("cloudStack.module.instanceWizard.serviceOfferingObjs"); @@ -960,11 +959,41 @@ }); } }); + + $.ajax({ + url: createURL("listTemplateOvfProperties&id=" + selectedTemplateObj.id), + dataType: "json", + async: false, + success: function(json) { + ovfProps = json.listtemplateovfpropertiesresponse.ovfproperty; + } + }); + + var $step = $('.step.sshkeyPairs:visible'); + if (ovfProps == null || ovfProps.length === 0) { + $step.addClass('next-skip-ovf-properties'); + } else { + $step.removeClass('next-skip-ovf-properties'); + } + }, + + // Step PRE-8: Configure OVF Properties (if available) for the template + function(args) { + args.response.success({ + data: { + ovfProperties: ovfProps + } + }); }, // Step 8: Review function(args) { - return false; + var $step = $('.step.review:visible'); + if (ovfProps == null || ovfProps.length === 0) { + $step.addClass('previous-skip-ovf-properties'); + } else { + $step.removeClass('previous-skip-ovf-properties'); + } } ], action: function(args) { @@ -1004,6 +1033,25 @@ hypervisor : selectedHypervisor }); + var deployOvfProperties = []; + if (ovfProps != null && ovfProps.length > 0) { + $(ovfProps).each(function(index, prop) { + var selectField = args.$wizard.find('select[id="ovf-property-'+prop.key+'"]'); + var inputField = args.$wizard.find('input[id="ovf-property-'+prop.key+'"]'); + var propValue = inputField.val() ? inputField.val() : selectField.val(); + if (propValue !== undefined) { + deployOvfProperties.push({ + key: prop.key, + value: propValue + }); + } + }); + for (var k = 0; k < deployOvfProperties.length; k++) { + deployVmData["ovfproperties[" + k + "].key"] = deployOvfProperties[k].key; + deployVmData["ovfproperties[" + k + "].value"] = deployOvfProperties[k].value; + } + } + if (args.$wizard.find('input[name=rootDiskSize]').parent().css('display') != 'none') { if (args.$wizard.find('input[name=rootDiskSize]').val().length > 0) { $.extend(deployVmData, { diff --git a/ui/scripts/templates.js b/ui/scripts/templates.js index 6124ab13c1b..d15a2c18898 100644 --- a/ui/scripts/templates.js +++ b/ui/scripts/templates.js @@ -1822,6 +1822,21 @@ } } }, + tabFilter: function (args) { + $.ajax({ + url: createURL("listTemplateOvfProperties&id=" + args.context.templates[0].id), + dataType: "json", + async: false, + success: function(json) { + ovfprops = json.listtemplateovfpropertiesresponse.ovfproperty; + } + }); + var hiddenTabs = []; + if (ovfprops == null || ovfprops.length === 0) { + hiddenTabs.push("ovfpropertiestab"); + } + return hiddenTabs; + }, tabs: { details: { title: 'label.details', @@ -2583,7 +2598,57 @@ } } }) - } + }, + + /** + * OVF properties tab (only displayed when OVF properties are available) + */ + ovfpropertiestab: { + title: 'label.ovf.properties', + listView: { + id: 'ovfproperties', + fields: { + label: { + label: 'label.label' + }, + description: { + label: 'label.description' + }, + value: { + label: 'label.value' + } + }, + hideSearchBar: true, + dataProvider: function(args) { + $.ajax({ + url: createURL("listTemplateOvfProperties"), + data: { + id: args.context.templates[0].id + }, + success: function(json) { + var ovfprops = json.listtemplateovfpropertiesresponse.ovfproperty; + var listDetails = []; + for (index in ovfprops){ + var prop = ovfprops[index]; + var det = {}; + det['label'] = prop['label']; + det['description'] = prop['description']; + det['value'] = prop['value']; + listDetails.push(det); + } + args.response.success({ + data: listDetails + }); + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + + } + } + } } } } diff --git a/ui/scripts/ui-custom/instanceWizard.js b/ui/scripts/ui-custom/instanceWizard.js index b240c37f499..4fa7e3fc76c 100644 --- a/ui/scripts/ui-custom/instanceWizard.js +++ b/ui/scripts/ui-custom/instanceWizard.js @@ -123,6 +123,98 @@ }); }; + var makeSelectsOvfProperties = function (data, fields) { + var $selects = $('
'); + + $(data).each(function() { + var item = this; + var key = item[fields.key]; + var type = item[fields.type]; + var value = item[fields.value]; + var qualifiers = item[fields.qualifiers]; + var label = item[fields.label]; + var description = item[fields.description]; + var password = item[fields.password]; + + var propertyField; + + var fieldType = password ? "password" : "text"; + if (type && type.toUpperCase() == "BOOLEAN") { + propertyField = $('') + .attr({type: fieldType, min: minValue, max:maxValue}) + .addClass('name').val(_s(this[fields.value])); + } else { + propertyField = $('') + .attr({type: fieldType}) + .addClass('name').val(_s(this[fields.value])) + } + } else if (type && type.toUpperCase() == "STRING") { + if (qualifiers) { + propertyField = $('') + .attr({maxlength : length, type: fieldType}) + .addClass('name').val(_s(this[fields.value])) + } + } else { + propertyField = $('') + .attr({type: fieldType}) + .addClass('name').val(_s(this[fields.value])) + } + } else { + propertyField = $('') + .attr({type: fieldType}) + .addClass('name').val(_s(this[fields.value])) + } + + var $select = $('
') + .addClass('select') + .append( + $('
') + .addClass('select-desc') + .addClass('ovf-property') + .append($('
').addClass('name').html(_s(this[fields.label]))) + .append(propertyField) + .append($('
').addClass('desc').html(_s(this[fields.description]))) + .data('json-obj', this) + ); + $selects.append($select); + }); + + cloudStack.evenOdd($selects, 'div.select', { + even: function($elem) { + $elem.addClass('even'); + }, + odd: function($elem) { + $elem.addClass('odd'); + } + }); + + return $selects.children(); + }; + var makeSelects = function(name, data, fields, options, selectedObj, selectedObjNonEditable) { var $selects = $('
'); options = options ? options : {}; @@ -1233,10 +1325,31 @@ }; }, + 'ovfProperties': function($step, formData) { + return { + response: { + success: function(args) { + $step.find('.content .select-container').append( + makeSelectsOvfProperties(args.data.ovfProperties, { + key: 'key', + type: 'type', + value: 'value', + qualifiers: 'qualifiers', + label: 'label', + description : 'description', + password : 'password' + }) + ); + } + } + }; + }, + 'review': function($step, formData) { $step.find('[wizard-field]').each(function() { var field = $(this).attr('wizard-field'); var fieldName; + var $input = $wizard.find('[wizard-field=' + field + ']').filter(function() { return ($(this).is(':selected') || $(this).is(':checked') || @@ -1425,6 +1538,37 @@ } } + // Step 7 - Skip OVF properties tab if there are no OVF properties for the template + if ($activeStep.hasClass('sshkeyPairs')) { + if ($activeStep.hasClass('next-skip-ovf-properties')) { + showStep(8); + } + } + + // Optional Step - Pre-step 8 + if ($activeStep.hasClass('ovf-properties')) { + var ok = true; + if ($activeStep.find('input').length > 0) { //if no checkbox is checked + $.each($activeStep.find('input'), function(index, item) { + var item = $activeStep.find('input#' + item.id); + var internalCheck = true; + if (this.maxLength && this.maxLength !== -1) { + internalCheck = item.val().length <= this.maxLength; + } else if (this.min && this.max) { + var numberValue = parseFloat(item.val()); + internalCheck = numberValue >= this.min && numberValue <= this.max; + } + ok = ok && internalCheck; + }); + } + if (!ok) { + cloudStack.dialog.notice({ + message: 'Please enter valid values for every property' + }); + return false; + } + } + if (!$form.valid()) { if ($form.find('input.error:visible, select.error:visible').length) { return false; @@ -1459,6 +1603,12 @@ } } + if ($activeStep.hasClass('review')) { + if ($activeStep.hasClass('previous-skip-ovf-properties')) { + showStep(7); + } + } + return false; } diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/ClusterMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/ClusterMO.java index a6e42850c5e..ce9e9816280 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/ClusterMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/ClusterMO.java @@ -391,7 +391,7 @@ public class ClusterMO extends BaseMO implements VmwareHypervisorHost { @Override public boolean createBlankVm(String vmName, String vmInternalCSName, int cpuCount, int cpuSpeedMHz, int cpuReservedMHz, boolean limitCpuUse, int memoryMB, - int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, Pair controllerInfo, Boolean systemVm) throws Exception { + int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, Pair controllerInfo, Boolean systemVm) throws Exception { if (s_logger.isTraceEnabled()) s_logger.trace("vCenter API trace - createBlankVm(). target MOR: " + _mor.getValue() + ", vmName: " + vmName + ", cpuCount: " + cpuCount + ", cpuSpeedMhz: " + diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java index 74cef80dbe5..cc50b3d5bc1 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HostMO.java @@ -771,7 +771,7 @@ public class HostMO extends BaseMO implements VmwareHypervisorHost { @Override public boolean createBlankVm(String vmName, String vmInternalCSName, int cpuCount, int cpuSpeedMHz, int cpuReservedMHz, boolean limitCpuUse, int memoryMB, - int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, Pair controllerInfo, Boolean systemVm) throws Exception { + int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, Pair controllerInfo, Boolean systemVm) throws Exception { if (s_logger.isTraceEnabled()) s_logger.trace("vCenter API trace - createBlankVm(). target MOR: " + _mor.getValue() + ", vmName: " + vmName + ", cpuCount: " + cpuCount + ", cpuSpeedMhz: " + diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java index f003d6a9f0f..2eaa55a5768 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/HypervisorHostHelper.java @@ -1439,8 +1439,8 @@ public class HypervisorHostHelper { } public static boolean createBlankVm(VmwareHypervisorHost host, String vmName, String vmInternalCSName, int cpuCount, int cpuSpeedMHz, int cpuReservedMHz, - boolean limitCpuUse, int memoryMB, int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, - Pair controllerInfo, Boolean systemVm) throws Exception { + boolean limitCpuUse, int memoryMB, int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, + Pair controllerInfo, Boolean systemVm) throws Exception { if (s_logger.isInfoEnabled()) s_logger.info("Create blank VM. cpuCount: " + cpuCount + ", cpuSpeed(MHz): " + cpuSpeedMHz + ", mem(Mb): " + memoryMB); @@ -1508,6 +1508,7 @@ public class HypervisorHostHelper { videoDeviceSpec.setOperation(VirtualDeviceConfigSpecOperation.ADD); vmConfig.getDeviceChange().add(videoDeviceSpec); + if (host.createVm(vmConfig)) { // Here, when attempting to find the VM, we need to use the name // with which we created it. This is the only such place where diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java index 6a42f7f9aea..a1205c258ee 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VirtualMachineMO.java @@ -2963,10 +2963,18 @@ public class VirtualMachineMO extends BaseMO { List devices = (List)_context.getVimClient(). getDynamicProperty(_mor, "config.hardware.device"); if(devices != null && devices.size() > 0) { + long isoDevices = devices.stream() + .filter(x -> x instanceof VirtualCdrom && x.getBacking() instanceof VirtualCdromIsoBackingInfo) + .count(); for(VirtualDevice device : devices) { - if(device instanceof VirtualCdrom && device.getBacking() instanceof VirtualCdromIsoBackingInfo && - ((VirtualCdromIsoBackingInfo)device.getBacking()).getFileName().equals(filename)) { - return device; + if(device instanceof VirtualCdrom && device.getBacking() instanceof VirtualCdromIsoBackingInfo) { + if (((VirtualCdromIsoBackingInfo)device.getBacking()).getFileName().equals(filename)) { + return device; + } else if (isoDevices == 1L){ + s_logger.warn(String.format("VM ISO filename %s differs from the expected filename %s", + ((VirtualCdromIsoBackingInfo)device.getBacking()).getFileName(), filename)); + return device; + } } } } diff --git a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VmwareHypervisorHost.java b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VmwareHypervisorHost.java index 0767ec09f9a..6f0cd2291b4 100644 --- a/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VmwareHypervisorHost.java +++ b/vmware-base/src/main/java/com/cloud/hypervisor/vmware/mo/VmwareHypervisorHost.java @@ -58,8 +58,8 @@ public interface VmwareHypervisorHost { boolean createVm(VirtualMachineConfigSpec vmSpec) throws Exception; boolean createBlankVm(String vmName, String vmInternalCSName, int cpuCount, int cpuSpeedMHz, int cpuReservedMHz, boolean limitCpuUse, int memoryMB, - int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, - Pair controllerInfo, Boolean systemVm) throws Exception; + int memoryReserveMB, String guestOsIdentifier, ManagedObjectReference morDs, boolean snapshotDirToParent, + Pair controllerInfo, Boolean systemVm) throws Exception; void importVmFromOVF(String ovfFilePath, String vmName, DatastoreMO dsMo, String diskOption) throws Exception;