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 9c1bdf7913d6..d1b945844cf3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -158,6 +158,7 @@ public class ApiConstants { public static final String DISK = "disk"; public static final String DISK_OFFERING_ID = "diskofferingid"; public static final String NEW_DISK_OFFERING_ID = "newdiskofferingid"; + public static final String ORCHESTRATOR_REQUIRES_PREPARE_VM = "orchestratorrequirespreparevm"; public static final String OVERRIDE_DISK_OFFERING_ID = "overridediskofferingid"; public static final String DISK_KBS_READ = "diskkbsread"; public static final String DISK_KBS_WRITE = "diskkbswrite"; @@ -428,6 +429,7 @@ public class ApiConstants { public static final String POST_URL = "postURL"; public static final String POWER_STATE = "powerstate"; public static final String PRECEDENCE = "precedence"; + public static final String PREPARE_VM = "preparevm"; public static final String PRIVATE_INTERFACE = "privateinterface"; public static final String PRIVATE_IP = "privateip"; public static final String PRIVATE_PORT = "privateport"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java index 077715d6f962..85d5a5b29157 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java @@ -17,16 +17,18 @@ package org.apache.cloudstack.api.response; -import org.apache.cloudstack.extension.Extension; -import com.cloud.serializer.Param; -import com.google.gson.annotations.SerializedName; +import java.util.Date; +import java.util.List; +import java.util.Map; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.extension.Extension; -import java.util.Date; -import java.util.List; -import java.util.Map; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; @EntityReference(value = Extension.class) public class ExtensionResponse extends BaseResponse { @@ -59,6 +61,10 @@ public class ExtensionResponse extends BaseResponse { @Param(description = "True if the extension is added by admin") private Boolean userDefined; + @SerializedName(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM) + @Parameter(description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not") + private Boolean orchestratorRequiresPrepareVm; + @SerializedName(ApiConstants.STATE) @Param(description = "The state of the extension") private String state; @@ -98,6 +104,10 @@ public void setUserDefined(Boolean userDefined) { this.userDefined = userDefined; } + public void setOrchestratorRequiresPrepareVm(Boolean orchestratorRequiresPrepareVm) { + this.orchestratorRequiresPrepareVm = orchestratorRequiresPrepareVm; + } + public void setState(String state) { this.state = state; } diff --git a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java index 77f45234240e..b94d18c537ea 100644 --- a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java @@ -20,17 +20,21 @@ import java.util.Map; +import com.cloud.agent.api.to.VirtualMachineTO; + public class PrepareExternalProvisioningAnswer extends Answer { Map serverDetails; + VirtualMachineTO virtualMachineTO; public PrepareExternalProvisioningAnswer() { super(); } - public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, Map serverDetails, String details) { + public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, Map externalDetails, VirtualMachineTO virtualMachineTO, String details) { super(cmd, true, details); - this.serverDetails = serverDetails; + this.serverDetails = externalDetails; + this.virtualMachineTO = virtualMachineTO; } public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, boolean success, String details) { @@ -40,4 +44,8 @@ public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, public Map getServerDetails() { return serverDetails; } + + public VirtualMachineTO getVirtualMachineTO() { + return virtualMachineTO; + } } diff --git a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java index fd4790222718..3224c595ffd4 100644 --- a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java +++ b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java @@ -28,19 +28,14 @@ public class PrepareExternalProvisioningCommand extends Command { Long clusterId; Map externalDetails; - public PrepareExternalProvisioningCommand(VirtualMachineTO vmUUID, Long clusterId) { - this.virtualMachineTO = vmUUID; - this.clusterId = clusterId; + public PrepareExternalProvisioningCommand(VirtualMachineTO vmTO) { + this.virtualMachineTO = vmTO; } public VirtualMachineTO getVirtualMachineTO() { return virtualMachineTO; } - public Long getClusterId() { - return clusterId; - } - public Map getExternalDetails() { return externalDetails; } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index a4d9c0723974..456d33b54f9a 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -71,7 +71,9 @@ import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; import org.apache.cloudstack.framework.jobs.AsyncJob; import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext; import org.apache.cloudstack.framework.jobs.AsyncJobManager; @@ -124,6 +126,8 @@ import com.cloud.agent.api.PingRoutingCommand; import com.cloud.agent.api.PlugNicAnswer; import com.cloud.agent.api.PlugNicCommand; +import com.cloud.agent.api.PrepareExternalProvisioningAnswer; +import com.cloud.agent.api.PrepareExternalProvisioningCommand; import com.cloud.agent.api.PrepareForMigrationAnswer; import com.cloud.agent.api.PrepareForMigrationCommand; import com.cloud.agent.api.RebootAnswer; @@ -440,6 +444,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac private VolumeDataFactory volumeDataFactory; @Inject ExtensionsManager extensionsManager; + @Inject + ExtensionDetailsDao extensionDetailsDao; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -1161,6 +1167,127 @@ protected void updateVmMetadataManufacturerAndProduct(VirtualMachineTO vmTO, VMI vmTO.setMetadataProductName(metadataProduct); } + protected void updateExternalVmDetailsFromPrepareAnswer(VirtualMachineTO vmTO, UserVmVO userVmVO, + Map newDetails) { + if (newDetails != null || newDetails.equals(vmTO.getDetails())) { + return; + } + vmTO.setDetails(newDetails); + userVmVO.setDetails(newDetails); + _userVmDao.saveDetails(userVmVO); + } + + protected void updateExternalVmDataFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) { + final String vncPassword = updatedTO.getVncPassword(); + final Map details = updatedTO.getDetails(); + if ((vncPassword == null || vncPassword.equals(vmTO.getVncPassword())) && + (details == null || details.equals(vmTO.getDetails()))) { + return; + } + UserVmVO userVmVO = _userVmDao.findById(vmTO.getId()); + if (userVmVO == null) { + return; + } + if (vncPassword != null && !vncPassword.equals(userVmVO.getPassword())) { + userVmVO.setVncPassword(vncPassword); + vmTO.setVncPassword(vncPassword); + } + updateExternalVmDetailsFromPrepareAnswer(vmTO, userVmVO, updatedTO.getDetails()); + } + + protected void updateExternalVmNicsFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) { + if (ObjectUtils.anyNull(vmTO.getNics(), updatedTO.getNics())) { + return; + } + Map originalNicsByUuid = new HashMap<>(); + for (NicTO nic : vmTO.getNics()) { + originalNicsByUuid.put(nic.getNicUuid(), nic); + } + for (NicTO updatedNicTO : updatedTO.getNics()) { + final String nicUuid = updatedNicTO.getNicUuid(); + NicTO originalNicTO = originalNicsByUuid.get(nicUuid); + if (originalNicTO == null) { + continue; + } + final String mac = updatedNicTO.getMac(); + final String ip4 = updatedNicTO.getIp(); + final String ip6 = updatedNicTO.getIp6Address(); + if (Objects.equals(mac, originalNicTO.getMac()) && + Objects.equals(ip4, originalNicTO.getIp()) && + Objects.equals(ip6, originalNicTO.getIp6Address())) { + continue; + } + NicVO nicVO = _nicsDao.findByUuid(nicUuid); + if (nicVO == null) { + continue; + } + logger.debug("Updating {} during External VM preparation", nicVO); + if (ip4 != null && !ip4.equals(nicVO.getIPv4Address())) { + nicVO.setIPv4Address(ip4); + originalNicTO.setIp(ip4); + } + if (ip6 != null && !ip6.equals(nicVO.getIPv6Address())) { + nicVO.setIPv6Address(ip6); + originalNicTO.setIp6Address(ip6); + } + if (mac != null && !mac.equals(nicVO.getMacAddress())) { + nicVO.setMacAddress(mac); + originalNicTO.setMac(mac); + } + _nicsDao.update(nicVO.getId(), nicVO); + } + } + + protected void updateExternalVmFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) { + if (updatedTO == null) { + return; + } + updateExternalVmDataFromPrepareAnswer(vmTO, updatedTO); + updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO); + } + + protected void processPrepareExternalProvisioning(Host host, VirtualMachineTO virtualMachineTO, + VirtualMachineTemplate template) { + if (host == null || !HypervisorType.External.equals(host.getHypervisorType()) || + template.getExtensionId() == null) { + return; + } + ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(template.getExtensionId(), + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM); + if (detailsVO == null || !Boolean.parseBoolean(detailsVO.getValue())) { + return; + } + Map vmDetails = virtualMachineTO.getExternalDetails(); + Map externalDetails = extensionsManager.getExternalAccessDetails(host, + vmDetails); + PrepareExternalProvisioningCommand cmd = new PrepareExternalProvisioningCommand(virtualMachineTO); + cmd.setExternalDetails(externalDetails); + Answer answer = null; + CloudRuntimeException cre = new CloudRuntimeException("Failed to prepare VM"); + try { + answer = _agentMgr.send(host.getId(), cmd); + } catch (AgentUnavailableException | OperationTimedoutException e) { + logger.error("Failed PrepareExternalProvisioningCommand due to : {}", e.getMessage(), e); + throw cre; + } + if (answer == null) { + logger.error("Invalid answer received for PrepareExternalProvisioningCommand"); + throw cre; + } + if (!(answer instanceof PrepareExternalProvisioningAnswer)) { + logger.error("Unexpected answer received for PrepareExternalProvisioningCommand: [result: {}, details: {}]", + answer.getResult(), answer.getDetails()); + throw cre; + } + PrepareExternalProvisioningAnswer prepareAnswer = (PrepareExternalProvisioningAnswer)answer; + if (!prepareAnswer.getResult()) { + logger.error("Unexpected answer received for PrepareExternalProvisioningCommand: [result: {}, details: {}]", + answer.getResult(), answer.getDetails()); + throw cre; + } + updateExternalVmFromPrepareAnswer(virtualMachineTO, prepareAnswer.getVirtualMachineTO()); + } + @Override public void orchestrateStart(final String vmUuid, final Map params, final DeploymentPlan planToDeploy, final DeploymentPlanner planner) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException { @@ -1334,6 +1461,7 @@ public void orchestrateStart(final String vmUuid, final Map sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 1c72dab8ce9c..432e1dd2a8c1 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -41,7 +41,6 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.ControlledEntity.ACLType; -import org.apache.cloudstack.agent.manager.ExternalAgentManagerImpl; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; @@ -53,14 +52,12 @@ import org.apache.cloudstack.framework.config.ConfigKey.Scope; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.dao.NetworkPermissionDao; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -76,15 +73,12 @@ import com.cloud.agent.api.CleanupPersistentNetworkResourceAnswer; import com.cloud.agent.api.CleanupPersistentNetworkResourceCommand; import com.cloud.agent.api.Command; -import com.cloud.agent.api.PrepareExternalProvisioningAnswer; -import com.cloud.agent.api.PrepareExternalProvisioningCommand; import com.cloud.agent.api.SetupPersistentNetworkAnswer; import com.cloud.agent.api.SetupPersistentNetworkCommand; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.NicTO; -import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.agent.api.to.deployasis.OVFNetworkTO; import com.cloud.alert.AlertManager; import com.cloud.api.query.dao.DomainRouterJoinDao; @@ -131,11 +125,7 @@ import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; -import com.cloud.host.dao.HostDetailsDao; -import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.hypervisor.HypervisorGuru; -import com.cloud.hypervisor.HypervisorGuruManager; import com.cloud.network.IpAddress; import com.cloud.network.IpAddressManager; import com.cloud.network.Ipv6Service; @@ -264,7 +254,6 @@ import com.cloud.vm.VirtualMachine.Type; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineProfile; -import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; @@ -273,7 +262,6 @@ import com.cloud.vm.dao.NicSecondaryIpDao; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.googlecode.ipv6.IPv6Address; @@ -297,8 +285,6 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @Inject UserVmDao _userVmDao; @Inject - UserVmDetailsDao userVmDetailsDao; - @Inject AlertManager _alertMgr; @Inject ConfigurationManager _configMgr; @@ -372,10 +358,6 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra private ASNumberDao asNumberDao; @Inject private BGPService bgpService; - @Inject - private HypervisorGuruManager hvGuruMgr; - @Inject - ExtensionsManager extensionsManager; @Override public List getNetworkGurus() { @@ -443,8 +425,6 @@ public void setDhcpProviders(final List dhcpProviders) { @Inject HostDao _hostDao; @Inject - HostDetailsDao hostDetailsDao; - @Inject NetworkServiceMapDao _ntwkSrvcDao; @Inject VpcManager _vpcMgr; @@ -2168,8 +2148,6 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy final Integer networkRate = _networkModel.getNetworkRate(network.getId(), vmProfile.getId()); final NetworkGuru guru = AdapterBase.getAdapterByName(networkGurus, network.getGuruName()); - - prepareNicIfExternalProvisionerInvolved(vmProfile, dest, nicId); final NicVO nic = _nicDao.findById(nicId); NicProfile profile = null; @@ -2238,67 +2216,6 @@ public NicProfile prepareNic(final VirtualMachineProfile vmProfile, final Deploy return profile; } - private void prepareNicIfExternalProvisionerInvolved(VirtualMachineProfile vmProfile, DeployDestination dest, long nicId) { - if (!Hypervisor.HypervisorType.External.equals(vmProfile.getHypervisorType())) { - return; - } - if (userVmDetailsDao.findDetail(vmProfile.getId(), VmDetailConstants.DEPLOY_VM) == null) { - return; - } - HypervisorGuru hvGuru = hvGuruMgr.getGuru(vmProfile.getHypervisorType()); - VirtualMachineTO vmTO = hvGuru.implement(vmProfile); - - HostVO host = _hostDao.findById(dest.getHost().getId()); - PrepareExternalProvisioningCommand command = new PrepareExternalProvisioningCommand(vmTO, host.getClusterId()); - Map externalDetails = extensionsManager.getExternalAccessDetails(host, vmTO.getExternalDetails()); - command.setExternalDetails(externalDetails); - final PrepareExternalProvisioningAnswer prepareExternalProvisioningAnswer; - try { - Long hostID = dest.getHost().getId(); - final Answer answer = _agentMgr.send(hostID, command); - - if (!(answer instanceof PrepareExternalProvisioningAnswer)) { - String errorMsg = String.format("Trying to prepare the instance on external hypervisor for the CloudStack instance %s failed: %s", vmProfile.getUuid(), answer.getDetails()); - logger.debug(errorMsg); - throw new CloudRuntimeException(errorMsg); - } - - prepareExternalProvisioningAnswer = (PrepareExternalProvisioningAnswer) answer; - } catch (AgentUnavailableException | OperationTimedoutException e) { - String errorMsg = String.format("Trying to prepare the instance on external hypervisor for the CloudStack instance %s failed: %s", vmProfile.getUuid(), e); - logger.debug(errorMsg); - throw new CloudRuntimeException(errorMsg); - } - - if (prepareExternalProvisioningAnswer == null || !prepareExternalProvisioningAnswer.getResult()) { - if (prepareExternalProvisioningAnswer != null && StringUtils.isNotBlank(prepareExternalProvisioningAnswer.getDetails())) { - throw new CloudRuntimeException(String.format("Unable to prepare the instance on external system due to %s", prepareExternalProvisioningAnswer.getDetails())); - } else { - throw new CloudRuntimeException("Unable to prepare the instance on external system, please check the access details"); - } - } - - Map serverDetails = prepareExternalProvisioningAnswer.getServerDetails(); - if (ExternalAgentManagerImpl.expectMacAddressFromExternalProvisioner.valueIn(host.getClusterId())) { - String macAddress = serverDetails.get(VmDetailConstants.MAC_ADDRESS); - if (StringUtils.isEmpty(macAddress)) { - throw new CloudRuntimeException("Unable to fetch macaddress from the external provisioner while preparing the instance"); - } - final NicVO nic = _nicDao.findById(nicId); - nic.setMacAddress(macAddress); - _nicDao.update(nicId, nic); - } - - if (MapUtils.isNotEmpty(serverDetails)) { - UserVmVO userVm = _userVmDao.findById(vmProfile.getId()); - _userVmDao.loadDetails(userVm); - Map details = userVm.getDetails(); - details.putAll(serverDetails); - userVm.setDetails(details); - _userVmDao.saveDetails(userVm); - } - } - @Override public Map getExtraDhcpOptions(long nicId) { List nicExtraDhcpOptionVOList = _nicExtraDhcpOptionDao.listByNicId(nicId); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java index 982b567b1295..d6f6f295455e 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java @@ -70,6 +70,11 @@ public class CreateExtensionCmd extends BaseCmd { description = "Relative path for entry point for extension") private String entryPoint; + @Parameter(name = ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + type = CommandType.BOOLEAN, + description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not") + private Boolean orchestratorRequiresPrepareVm; + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "State of the extension") private String state; @@ -98,6 +103,10 @@ public String getEntryPoint() { return entryPoint; } + public Boolean isOrchestratorRequiresPrepareVm() { + return orchestratorRequiresPrepareVm; + } + public String getState() { return state; } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java index 39f81e4a5b41..713e7550a1eb 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java @@ -58,6 +58,11 @@ public class UpdateExtensionCmd extends BaseCmd { description = "Description of the extension") private String description; + @Parameter(name = ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + type = CommandType.BOOLEAN, + description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not") + private Boolean orchestratorRequiresPrepareVm; + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "State of the extension") private String state; @@ -85,6 +90,10 @@ public String getDescription() { return description; } + public Boolean isOrchestratorRequiresPrepareVm() { + return orchestratorRequiresPrepareVm; + } + public String getState() { return state; } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java index 6909eeb95241..3b35cd88714e 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -475,12 +475,14 @@ public Extension createExtension(CreateExtensionCmd cmd) { final String description = cmd.getDescription(); final String typeStr = cmd.getType(); String entryPoint = cmd.getEntryPoint(); + final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); final String stateStr = cmd.getState(); ExtensionVO extensionByName = extensionDao.findByName(name); if (extensionByName != null) { throw new CloudRuntimeException("Extension by name already exists"); } - if (!EnumUtils.isValidEnum(Extension.Type.class, typeStr)) { + final Extension.Type type = EnumUtils.getEnum(Extension.Type.class, typeStr); + if (type == null) { throw new CloudRuntimeException(String.format("Invalid type specified - %s", typeStr)); } if (StringUtils.isBlank(entryPoint)) { @@ -496,10 +498,14 @@ public Extension createExtension(CreateExtensionCmd cmd) { throw new InvalidParameterValueException("Invalid state specified"); } } + if (orchestratorRequiresPrepareVm != null && !Extension.Type.Orchestrator.equals(type)) { + throw new InvalidParameterValueException(String.format("%s is applicable only with %s type", + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, type.name())); + } final String entryPointFinal = entryPoint; final Extension.State stateFinal = state; ExtensionVO extensionVO = Transaction.execute((TransactionCallbackWithException) status -> { - ExtensionVO extension = new ExtensionVO(name, description, EnumUtils.getEnum(Extension.Type.class, typeStr), + ExtensionVO extension = new ExtensionVO(name, description, type, entryPointFinal, stateFinal); extension = extensionDao.persist(extension); @@ -509,6 +515,13 @@ public Extension createExtension(CreateExtensionCmd cmd) { for (Map.Entry entry : details.entrySet()) { detailsVOList.add(new ExtensionDetailsVO(extension.getId(), entry.getKey(), entry.getValue())); } + } + if (orchestratorRequiresPrepareVm != null) { + detailsVOList.add(new ExtensionDetailsVO(extension.getId(), + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, String.valueOf(orchestratorRequiresPrepareVm), + false)); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { extensionDetailsDao.saveDetails(detailsVOList); } CallContext.current().setEventResourceId(extension.getId()); @@ -584,6 +597,7 @@ public List listExtensions(ListExtensionsCmd cmd) { public Extension updateExtension(UpdateExtensionCmd cmd) { final long id = cmd.getId(); final String description = cmd.getDescription(); + final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); final String stateStr = cmd.getState(); final Map details = cmd.getDetails(); final Boolean cleanupDetails = cmd.isCleanupDetails(); @@ -600,6 +614,10 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { extensionVO.getName())); } } + if (orchestratorRequiresPrepareVm != null && !Extension.Type.Orchestrator.equals(extensionVO.getType())) { + throw new InvalidParameterValueException(String.format("%s is applicable only with %s type", + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, extensionVO.getType().name())); + } if (StringUtils.isNotBlank(stateStr) && !stateStr.equalsIgnoreCase(extensionVO.getState().name())) { try { Extension.State state = Extension.State.valueOf(stateStr); @@ -615,15 +633,41 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { throw new CloudRuntimeException(String.format("Failed to updated the extension: %s", extensionVO.getName())); } - if (Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details)) { - extensionDetailsDao.removeDetails(extensionVO.getId()); - List detailsVOList = new ArrayList<>(); - if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { - for (Map.Entry entry : details.entrySet()) { - detailsVOList.add(new ExtensionDetailsVO(extensionVO.getId(), entry.getKey(), entry.getValue())); + final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details); + if (needToUpdateAllDetails || orchestratorRequiresPrepareVm != null) { + if (needToUpdateAllDetails) { + Map hiddenDetails = + extensionDetailsDao.listDetailsKeyPairs(id, false); + List detailsVOList = new ArrayList<>(); + if (orchestratorRequiresPrepareVm != null) { + hiddenDetails.put(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + String.valueOf(orchestratorRequiresPrepareVm)); + } + if (MapUtils.isNotEmpty(hiddenDetails)) { + hiddenDetails.forEach((key, value) -> detailsVOList.add( + new ExtensionDetailsVO(id, key, value, false))); + } + if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { + details.forEach((key, value) -> detailsVOList.add( + new ExtensionDetailsVO(id, key, value))); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionDetailsDao.saveDetails(detailsVOList); + } else if (Boolean.TRUE.equals(cleanupDetails)) { + extensionDetailsDao.removeDetails(id); + } + } else { + ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id, + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM); + if (detailsVO == null) { + extensionDetailsDao.persist(new ExtensionDetailsVO(id, + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + String.valueOf(orchestratorRequiresPrepareVm), false)); + } else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) { + detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm)); + extensionDetailsDao.update(detailsVO.getId(), detailsVO); } } - extensionDetailsDao.saveDetails(detailsVOList); } return extensionVO; }); @@ -789,12 +833,22 @@ public ExtensionResponse createExtensionResponse(Extension extension, response.setResources(resourcesResponse); } } + Map hiddenDetails; if (viewDetails.contains(ApiConstants.ExtensionDetails.all) || viewDetails.contains(ApiConstants.ExtensionDetails.external)) { - Map extensionDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(), true); - if (MapUtils.isNotEmpty(extensionDetails)) { - response.setDetails(extensionDetails); + Pair, Map> extensionDetails = + extensionDetailsDao.listDetailsKeyPairsWithVisibility(extension.getId()); + if (MapUtils.isNotEmpty(extensionDetails.first())) { + response.setDetails(extensionDetails.first()); } + hiddenDetails = extensionDetails.second(); + } else { + hiddenDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(), + List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)); + } + if (hiddenDetails.containsKey(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)) { + response.setOrchestratorRequiresPrepareVm(Boolean.parseBoolean( + hiddenDetails.get(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))); } response.setObjectName(Extension.class.getSimpleName().toLowerCase()); return response; @@ -1040,19 +1094,61 @@ public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { customAction.getName())); } } + final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details); + final boolean needToUpdateParameters = Boolean.TRUE.equals(cleanupParameters) || CollectionUtils.isNotEmpty(parametersFinal); + if (needToUpdateAllDetails || needToUpdateParameters) { + if (needToUpdateAllDetails) { + Map hiddenDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairs(id, false); + List detailsVOList = new ArrayList<>(); + if (Boolean.TRUE.equals(cleanupParameters)) { + hiddenDetails.remove(ApiConstants.PARAMETERS); + } else if (CollectionUtils.isNotEmpty(parametersFinal)) { + hiddenDetails.put(ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal)); + } + if (MapUtils.isNotEmpty(hiddenDetails)) { + hiddenDetails.forEach((key, value) -> detailsVOList.add( + new ExtensionCustomActionDetailsVO(id, key, value, false))); + } + if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { + details.forEach((key, value) -> detailsVOList.add( + new ExtensionCustomActionDetailsVO(id, key, value))); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionCustomActionDetailsDao.saveDetails(detailsVOList); + } else if (Boolean.TRUE.equals(cleanupDetails)) { + extensionCustomActionDetailsDao.removeDetails(id); + } + } else { + if (Boolean.TRUE.equals(cleanupParameters)) { + extensionCustomActionDetailsDao.removeDetail(id, ApiConstants.PARAMETERS); + } else if (CollectionUtils.isNotEmpty(parametersFinal)) { + ExtensionCustomActionDetailsVO detailsVO = extensionCustomActionDetailsDao.findDetail(id, + ApiConstants.PARAMETERS); + if (detailsVO == null) { + extensionCustomActionDetailsDao.persist(new ExtensionCustomActionDetailsVO(id, + ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal), false)); + } else { + detailsVO.setValue(ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal)); + extensionCustomActionDetailsDao.update(detailsVO.getId(), detailsVO); + } + } + } + } List detailsVOList = new ArrayList<>(); if (Boolean.TRUE.equals(cleanupParameters) || CollectionUtils.isNotEmpty(parametersFinal)) { - extensionCustomActionDetailsDao.removeDetail(customAction.getId(), ApiConstants.PARAMETERS); + extensionCustomActionDetailsDao.removeDetail(id, ApiConstants.PARAMETERS); if (CollectionUtils.isNotEmpty(parametersFinal)) { detailsVOList.add(new ExtensionCustomActionDetailsVO( - customAction.getId(), + id, ApiConstants.PARAMETERS, ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal), false )); } } - if (Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details)) { if (CollectionUtils.isNotEmpty(detailsVOList)) { ExtensionCustomActionDetailsVO paramDetails = @@ -1061,7 +1157,6 @@ public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { detailsVOList.add(paramDetails); } } - extensionCustomActionDetailsDao.removeDetails(customAction.getId()); if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { details.forEach((key, value) -> detailsVOList.add( new ExtensionCustomActionDetailsVO(customAction.getId(), key, value))); diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java index a18671e15e80..4c4930e7fe1f 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java @@ -74,6 +74,7 @@ import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.json.JsonMergeUtil; import com.cloud.utils.script.Script; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; @@ -93,6 +94,7 @@ public class SimpleExternalProvisioner extends ManagerBase implements ExternalPr private static final String ENTRY_POINT_DIR_CONFIG_NAME = "extensions.file.path"; private static final String DATA_DIR_CONFIG_NAME = "extensions.data.file.path"; private static final String DEFAULT_EXTENSIONS_DIRECTORY = "/usr/share/cloudstack-management/extensions"; + private static final String DEFAULT_EXTENSIONS_DATA_DIRECTORY = "/var/lib/cloudstack/management/extensions"; @Inject UserVmDao _uservmDao; @@ -161,6 +163,9 @@ protected boolean checkExtensionsDirectory() { extensionsDirectory); return false; } + if (!extensionsDirectory.equals(dir.getAbsolutePath())) { + extensionsDirectory = dir.getAbsolutePath(); + } logger.info("Extensions directory path: {}", extensionsDirectory); return true; } @@ -168,7 +173,9 @@ protected boolean checkExtensionsDirectory() { protected void createOrCheckExtensionsDataDirectory() throws ConfigurationException { String dataDir = getServerProperty(DATA_DIR_CONFIG_NAME); if (StringUtils.isBlank(dataDir)) { - throw new ConfigurationException("Extensions data directory path is blank"); + logger.warn("Extensions data directory path is blank, using default: {}", + DEFAULT_EXTENSIONS_DATA_DIRECTORY); + dataDir = DEFAULT_EXTENSIONS_DATA_DIRECTORY; } File dir = new File(dataDir); if (!dir.exists()) { @@ -265,15 +272,16 @@ public PrepareExternalProvisioningAnswer prepareExternalProvisioning(String host return new PrepareExternalProvisioningAnswer(cmd, false, output); } if (StringUtils.isEmpty(output)) { - return new PrepareExternalProvisioningAnswer(cmd, true, ""); + return new PrepareExternalProvisioningAnswer(cmd, result.first(), ""); } - Map resultMap = null; try { - resultMap = StringUtils.parseJsonToMap(output); - } catch (CloudRuntimeException e) { - logger.warn("Failed to parse the output from preparing external provisioning operation as part of VM deployment"); + String merged = JsonMergeUtil.mergeJsonPatch(GsonHelper.getGson().toJson(vmTO), result.second()); + VirtualMachineTO virtualMachineTO = GsonHelper.getGson().fromJson(merged, VirtualMachineTO.class); + return new PrepareExternalProvisioningAnswer(cmd, null, virtualMachineTO, null); + } catch (Exception e) { + logger.warn("Failed to parse the output from preparing external provisioning operation as part of VM deployment: {}", e.getMessage(), e); + return new PrepareExternalProvisioningAnswer(cmd, false, "Failed to parse VM"); } - return new PrepareExternalProvisioningAnswer(cmd, resultMap, null); } @Override diff --git a/scripts/vm/hypervisor/external/simpleExternalProvisioner/provisioner.sh b/scripts/vm/hypervisor/external/simpleExternalProvisioner/provisioner.sh index cb48cf19a1b2..6aa96610ef47 100644 --- a/scripts/vm/hypervisor/external/simpleExternalProvisioner/provisioner.sh +++ b/scripts/vm/hypervisor/external/simpleExternalProvisioner/provisioner.sh @@ -29,14 +29,34 @@ generate_random_mac() { prepare() { parse_json "$1" || exit 1 - local mac_address - mac_address=$(generate_random_mac) - - local response - response=$(jq -n --arg mac "$mac_address" \ - '{status: "success", mac_address: $mac}') - - echo "$response" + local input_json="$1" + local nics_json + nics_json=$(echo "$input_json" | jq '.["cloudstack.vm.details"].nics') + + # If NICs array is empty, return empty string + if [ "$(echo "$nics_json" | jq 'length // 0')" -eq 0 ]; then + echo "" + return + fi + + local result='{"nics":[' + local first=1 + + while IFS= read -r uuid; do + local mac + mac=$(generate_random_mac) + + if [ $first -eq 1 ]; then + first=0 + else + result+=',' + fi + + result+='{"uuid":"'"$uuid"'","mac":"'"$mac"'"}' + done < <(echo "$nics_json" | jq -r '.[].uuid') + + result+=']}' + echo "$result" } create() { diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index dd6e487a0a87..bbb3c9ff7d4a 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -825,6 +825,7 @@ "label.directdownload": "Direct download", "label.direction": "Direction", "label.disable.autoscale.vmgroup": "Disable AutoScaling Group", +"label.disable.custom.action": "Disable Custom Action", "label.disable.extension": "Disable Extension", "label.disable.host": "Disable host", "label.disable.network.offering": "Disable Network offering", @@ -938,6 +939,7 @@ "label.elastic": "Elastic", "label.email": "Email", "label.enable.autoscale.vmgroup": "Enable AutoScaling Group", +"label.enable.custom.action": "Enable Custom Action", "label.enable.extension": "Enable Extension", "label.enable.host": "Enable Host", "label.enable.network.offering": "Enable Network offering", @@ -1680,6 +1682,7 @@ "label.optional": "Optional", "label.options": "Options", "label.orchestrator": "Orchestrator", +"label.orchestratorrequirespreparevm": "Requires Prepare Instance", "label.order": "Order", "label.os": "Operating System", "label.oscategoryid": "OS category", @@ -2985,6 +2988,7 @@ "message.confirm.delete.traffic.type": "Please confirm that you would like to delete traffic type.", "message.confirm.destroy.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to stop this router. Please confirm that you would like to destroy this router.", "message.confirm.disable.autoscale.vmgroup": "Please confirm that you want to disable this autoscaling group.", +"message.confirm.disable.custom.action": "Please confirm that you want to disable this custom action.", "message.confirm.disable.extension": "Please confirm that you want to disable this extension.", "message.confirm.disable.host": "Please confirm that you want to disable the host.", "message.confirm.disable.network.offering": "Are you sure you want to disable this Network offering?", @@ -2993,6 +2997,7 @@ "message.confirm.disable.vpc.offering": "Are you sure you want to disable this VPC offering?", "message.confirm.disable.webhook": "Please confirm that you want to disable this webhook.", "message.confirm.enable.autoscale.vmgroup": "Please confirm that you want to enable this autoscaling group.", +"message.confirm.enable.custom.action": "Please confirm that you want to enable this custom action.", "message.confirm.enable.extension": "Please confirm that you want to enable this extension.", "message.confirm.enable.host": "Please confirm that you want to enable the host.", "message.confirm.enable.network.offering": "Are you sure you want to enable this Network offering?", @@ -3702,6 +3707,7 @@ "message.success.update.bgp.peer": "Successfully updated BGP peer", "message.success.update.bucket": "Successfully updated bucket", "message.success.update.condition": "Successfully updated condition", +"message.success.update.custom.action": "Successfully updated Custom Action", "message.success.update.extension": "Successfully updated Extension", "message.success.update.sharedfs": "Successfully updated Shared FileSystem", "message.success.update.ipaddress": "Successfully updated IP address", diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 46f8795c1e00..f9485ae94adf 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -61,6 +61,9 @@ {{ resource.instancename }} + + {{ $t('label.inbuilt') }} + {{ $t(resource.type.toLowerCase()) }} diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index ef276927aa18..f6f9fc21c670 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -115,7 +115,7 @@ - {{ $t('label.inbuilt') }} + {{ $t('label.inbuilt') }} diff --git a/ui/src/config/section/extension.js b/ui/src/config/section/extension.js index ec26f75efb6a..714923b15a4b 100644 --- a/ui/src/config/section/extension.js +++ b/ui/src/config/section/extension.js @@ -44,7 +44,7 @@ export default { }, 'created'] return fields }, - details: ['name', 'id', 'type', 'details', 'entrypoint', 'entrypointready', 'isuserdefined', 'created'], + details: ['name', 'id', 'type', 'details', 'entrypoint', 'entrypointready', 'isuserdefined', 'orchestratorrequirespreparevm', 'created'], filters: ['orchestrator'], tabs: [{ name: 'details', diff --git a/ui/src/config/section/extension/customaction.js b/ui/src/config/section/extension/customaction.js index d493b1d84027..7fe3d4572f9b 100644 --- a/ui/src/config/section/extension/customaction.js +++ b/ui/src/config/section/extension/customaction.js @@ -57,6 +57,30 @@ export default { popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/extension/UpdateCustomAction.vue'))) }, + { + api: 'updateCustomAction', + icon: 'play-circle-outlined', + label: 'label.enable.custom.action', + message: 'message.confirm.enable.custom.action', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { enabled: true }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return !record.enabled } + }, + { + api: 'updateCustomAction', + icon: 'pause-circle-outlined', + label: 'label.disable.custom.action', + message: 'message.confirm.disable.custom.action', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { enabled: false }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return record.enabled } + }, { api: 'deleteCustomAction', icon: 'delete-outlined', diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 5796cf18f607..d639f5871a50 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -165,7 +165,7 @@ :key="templateKey" @handle-search-filter="filters => fetchAllTemplates(filters)" @update-template-iso="updateFieldValue" /> -
+
{{ $t('label.override.rootdisk.size') }} - + {{ $t('label.override.root.diskoffering') }} @@ -575,7 +575,7 @@ ref="bootintosetup"> - + @@ -815,7 +815,7 @@ :filterOption="filterOption" > - + @@ -1489,6 +1489,9 @@ export default { }, guestOsCategoriesSelectionDisallowed () { return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId) + }, + isTemplateHypervisorExternal () { + return !!this.template && this.template.hypervisor === 'External' } }, watch: { @@ -2376,7 +2379,7 @@ export default { deployVmData.projectid = this.owner.projectid } - if (this.imageType === 'templateid' && this.template === 'External' && values.externaldetails) { + if (this.imageType === 'templateid' && this.template && this.template.hypervisor === 'External' && values.externaldetails) { Object.entries(values.externaldetails).forEach(([key, value]) => { deployVmData['externaldetails[0].' + key] = value }) diff --git a/ui/src/views/extension/CreateExtension.vue b/ui/src/views/extension/CreateExtension.vue index 01142fffcd2d..61e8cb431c38 100644 --- a/ui/src/views/extension/CreateExtension.vue +++ b/ui/src/views/extension/CreateExtension.vue @@ -75,6 +75,12 @@ + + + +