From 7391e3cc7bade76b7d121a86ec0e68986221c928 Mon Sep 17 00:00:00 2001 From: Alix Lourme Date: Fri, 3 Aug 2018 14:27:07 +0200 Subject: [PATCH] Fix #15 - Cinder volume attachment --- README.md | 3 + cloud-openstack-server/pom.xml | 5 + .../clouds/openstack/CreateImageOptions.java | 157 +++++++++++++++++ .../clouds/openstack/OpenstackApi.java | 58 +++++- .../openstack/OpenstackCloudClient.java | 11 +- .../clouds/openstack/OpenstackCloudImage.java | 113 ++++++------ .../openstack/OpenstackCloudInstance.java | 166 ++++++++++++------ 7 files changed, 388 insertions(+), 125 deletions(-) create mode 100644 cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/CreateImageOptions.java diff --git a/README.md b/README.md index 999fd24..63ccdd6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ You can specify idle time on the agent cloud profile, after which the instance s | *security_group* | true | [Security group](https://docs.openstack.org/nova/latest/admin/security-groups.html), ex: `default` | | *key_pair* | false | [Key pair](https://docs.openstack.org/horizon/latest/user/configure-access-and-security-for-instances.html), ex: `my-key` ; required for SSH connection on created instances (like TeamCity Agent Push feature) | | *auto_floating_ip* | false | Boolean (`false` by default) for [floating ip](https://docs.openstack.org/ocata/user-guide/cli-manage-ip-addresses.html) association ; first from pool used | +| *volume* | false | [Volume](https://docs.openstack.org/cinder/latest/cli/cli-manage-volumes.html) to attach (name and device separated by comma), ex: `some-volume,/dev/vdc` | *user_script* | false | Script executed on instance start | | *availability_zone* | false | Region for server instance (if not the global configured) @@ -115,6 +116,8 @@ openstack-test-teamcity-plugin: network: networkProviderName security_group: default key_pair: yourKey + auto_floating_ip: true + volume: some-volume,/dev/vdc ``` ``` diff --git a/cloud-openstack-server/pom.xml b/cloud-openstack-server/pom.xml index 2c2f959..5cad4d1 100644 --- a/cloud-openstack-server/pom.xml +++ b/cloud-openstack-server/pom.xml @@ -72,6 +72,11 @@ openstack-neutron ${jclouds.version} + + org.apache.jclouds.api + openstack-cinder + ${jclouds.version} + org.jetbrains.teamcity diff --git a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/CreateImageOptions.java b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/CreateImageOptions.java new file mode 100644 index 0000000..3028228 --- /dev/null +++ b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/CreateImageOptions.java @@ -0,0 +1,157 @@ + +package jetbrains.buildServer.clouds.openstack; + +import java.util.concurrent.ScheduledExecutorService; + +import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import jetbrains.buildServer.serverSide.ServerPaths; +import jetbrains.buildServer.util.StringUtil; + +public class CreateImageOptions { + + @NotNull + private OpenstackApi openstackApi; + @NotNull + private String imageId; + @NotNull + private String imageName; + @NotNull + private String openstackImageName; + @NotNull + private String flavorName; + @Nullable + private String volumeName; + @Nullable + private String volumeDevice; + @NotNull + private boolean autoFloatingIp; + @NotNull + private CreateServerOptions createServerOptions; + @Nullable + private String userScriptPath; + @NotNull + private ServerPaths serverPaths; + @NotNull + private ScheduledExecutorService scheduledExecutorService; + + protected CreateImageOptions openstackApi(@NotNull final OpenstackApi openstackApi) { + this.openstackApi = openstackApi; + return this; + } + + protected CreateImageOptions imageId(@NotNull final String imageId) { + this.imageId = imageId; + return this; + } + + protected CreateImageOptions imageName(@NotNull final String imageName) { + this.imageName = imageName; + return this; + } + + protected CreateImageOptions openstackImageName(@NotNull final String openstackImageName) { + this.openstackImageName = openstackImageName; + return this; + } + + protected CreateImageOptions flavorName(@NotNull final String flavorName) { + this.flavorName = flavorName; + return this; + } + + /** + * Volume should be "volumeName,volumeDevice" + * + * @param volume volume name and volume device + * @return CreateImageOptions + */ + protected CreateImageOptions volume(@Nullable final String volume) { + if (StringUtil.isNotEmpty(volume)) { + String[] volumeNameDevice = volume.split(","); + if (volumeNameDevice.length > 0) { + this.volumeName = volumeNameDevice[0].trim(); + } + if (volumeNameDevice.length > 1) { + this.volumeDevice = volumeNameDevice[1].trim(); + } + } + return this; + } + + protected CreateImageOptions autoFloatingIp(@NotNull final boolean autoFloatingIp) { + this.autoFloatingIp = autoFloatingIp; + return this; + } + + protected CreateImageOptions userScriptPath(@Nullable final String userScriptPath) { + this.userScriptPath = userScriptPath; + return this; + } + + protected CreateImageOptions createServerOptions(@NotNull final CreateServerOptions createServerOptions) { + this.createServerOptions = createServerOptions; + return this; + } + + protected CreateImageOptions serverPaths(@NotNull final ServerPaths serverPaths) { + this.serverPaths = serverPaths; + return this; + } + + protected CreateImageOptions scheduledExecutorService(@NotNull final ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + return this; + } + + protected OpenstackApi getOpenstackApi() { + return openstackApi; + } + + protected String getImageId() { + return imageId; + } + + protected String getImageName() { + return imageName; + } + + protected String getOpenstackImageName() { + return openstackImageName; + } + + protected String getFlavorName() { + return flavorName; + } + + protected String getVolumeName() { + return volumeName; + } + + protected String getVolumeDevice() { + return volumeDevice; + } + + protected boolean isAutoFloatingIp() { + return autoFloatingIp; + } + + protected CreateServerOptions getCreateServerOptions() { + return createServerOptions; + } + + protected String getUserScriptPath() { + return userScriptPath; + } + + protected ServerPaths getServerPaths() { + return serverPaths; + } + + protected ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + +} diff --git a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackApi.java b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackApi.java index 00c6e8c..2ed376a 100644 --- a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackApi.java +++ b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackApi.java @@ -5,6 +5,9 @@ import org.jclouds.ContextBuilder; import org.jclouds.location.reference.LocationConstants; +import org.jclouds.openstack.cinder.v1.CinderApi; +import org.jclouds.openstack.cinder.v1.CinderApiMetadata; +import org.jclouds.openstack.cinder.v1.domain.Volume; import org.jclouds.openstack.keystone.config.KeystoneProperties; import org.jclouds.openstack.neutron.v2.NeutronApi; import org.jclouds.openstack.neutron.v2.NeutronApiMetadata; @@ -14,7 +17,11 @@ import org.jclouds.openstack.nova.v2_0.NovaApiMetadata; import org.jclouds.openstack.nova.v2_0.domain.Flavor; import org.jclouds.openstack.nova.v2_0.domain.Image; -import org.jclouds.openstack.nova.v2_0.features.ServerApi; +import org.jclouds.openstack.nova.v2_0.domain.Server; +import org.jclouds.openstack.nova.v2_0.domain.ServerCreated; +import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import jetbrains.buildServer.util.StringUtil; @@ -24,6 +31,7 @@ public class OpenstackApi { private final NeutronApi neutronApi; private final NovaApi novaApi; + private final CinderApi cinderApi; public OpenstackApi(String endpointUrl, String identity, String password, String region) { @@ -53,9 +61,34 @@ public OpenstackApi(String endpointUrl, String identity, String password, String novaApi = ContextBuilder.newBuilder(new NovaApiMetadata()).endpoint(endpointUrl).credentials(identityObject.getCredendials(), password) .overrides(overrides).buildApi(NovaApi.class); + + cinderApi = ContextBuilder.newBuilder(new CinderApiMetadata()).endpoint(endpointUrl).credentials(identityObject.getCredendials(), password) + .overrides(overrides).buildApi(CinderApi.class); + + } + + @Nullable + public Server getServer(@NotNull final String id) { + return novaApi.getServerApi(region).get(id); + } + + @NotNull + public ServerCreated createServer(@NotNull final String name, @NotNull final String imageId, @NotNull final String flavorId, + @NotNull final CreateServerOptions options) { + return novaApi.getServerApi(region).create(name, imageId, flavorId, options); + } - public String getImageIdByName(String name) { + public void deleteServer(@NotNull final String id) { + novaApi.getServerApi(region).delete(id); + } + + public void attachVolumeToServer(@NotNull final String serverId, @NotNull final String volumeId, @NotNull final String volumeDevice) { + novaApi.getVolumeAttachmentApi(region).get().attachVolumeToServerAsDevice(volumeId, serverId, volumeDevice); + } + + @Nullable + public String getImageIdByName(@NotNull final String name) { List images = novaApi.getImageApi(region).listInDetail().concat().toList(); for (Image image : images) { if (image.getName().equals(name)) @@ -64,7 +97,8 @@ public String getImageIdByName(String name) { return null; } - public String getFlavorIdByName(String name) { + @Nullable + public String getFlavorIdByName(@NotNull final String name) { List flavors = novaApi.getFlavorApi(region).listInDetail().concat().toList(); for (Flavor flavor : flavors) { if (flavor.getName().equals(name)) @@ -73,7 +107,8 @@ public String getFlavorIdByName(String name) { return null; } - public String getNetworkIdByName(String name) { + @Nullable + public String getNetworkIdByName(@NotNull final String name) { List networks = neutronApi.getNetworkApi(region).list().concat().toList(); for (Network network : networks) { if (network.getName().equals(name)) @@ -82,14 +117,21 @@ public String getNetworkIdByName(String name) { return null; } - public ServerApi getNovaServerApi() { - return novaApi.getServerApi(region); + @Nullable + public String getVolumeIdByName(@NotNull final String name) { + List volumes = cinderApi.getVolumeApi(region).list().toList(); + for (Volume volume : volumes) { + if (volume.getName().equals(name)) + return volume.getId(); + } + return null; } - public void associateFloatingIp(String serverId, String ip) { + public void associateFloatingIp(@NotNull final String serverId, @NotNull final String ip) { novaApi.getFloatingIPApi(region).get().addToServer(ip, serverId); } + @Nullable public String getFloatingIpAvailable() { for (FloatingIP ip : neutronApi.getFloatingIPApi(region).list().concat().toList()) { if (StringUtil.isEmpty(ip.getFixedIpAddress())) { @@ -105,7 +147,7 @@ public String getFloatingIpAvailable() { * @param url endpoint * @return 2 or 3 */ - protected static String getKeystoneVersion(String url) { + protected static String getKeystoneVersion(@NotNull final String url) { final String def = "3"; if (StringUtil.isEmpty(url)) { return def; diff --git a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClient.java b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClient.java index f1e7a69..0790569 100644 --- a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClient.java +++ b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudClient.java @@ -78,6 +78,7 @@ public OpenstackCloudClient(@NotNull final CloudClientParameters params, @NotNul final String networkName = StringUtil.trim(entry.getValue().get("network")); final String securityGroupName = StringUtil.trim(entry.getValue().get("security_group")); final String keyPair = StringUtil.trim(entry.getValue().get("key_pair")); + final String volume = StringUtil.trim(entry.getValue().get("volume")); final String userScriptPath = entry.getValue().get("user_script"); Boolean autoFloatingIp = (Boolean) (Object) entry.getValue().get("auto_floating_ip"); // Evil, but Yaml parse Boolean only for this autoFloatingIp = ObjectUtils.chooseNotNull(autoFloatingIp, false); // Can be null if not defined @@ -91,11 +92,13 @@ public OpenstackCloudClient(@NotNull final CloudClientParameters params, @NotNul } LOG.debug(String.format( - "Adding cloud image: imageName=%s, openstackImageName=%s, flavorName=%s, networkName=%s, networkId=%s, securityGroupName=%s, keyPair=%s, floatingIp=%s", - imageName, openstackImageName, flavorName, networkName, networkId, securityGroupName, keyPair, autoFloatingIp)); + "Adding cloud image: imageName=%s, openstackImageName=%s, flavorName=%s, networkName=%s, networkId=%s, securityGroupName=%s, keyPair=%s, floatingIp=%s, volume=%s", + imageName, openstackImageName, flavorName, networkName, networkId, securityGroupName, keyPair, autoFloatingIp, volume)); - final OpenstackCloudImage image = new OpenstackCloudImage(openstackApi, imageIdGenerator.next(), imageName, openstackImageName, - flavorName, autoFloatingIp, options, userScriptPath, serverPaths, factory.createExecutorService(imageName)); + final OpenstackCloudImage image = new OpenstackCloudImage(new CreateImageOptions().openstackApi(openstackApi) + .imageId(imageIdGenerator.next()).imageName(imageName).openstackImageName(openstackImageName).flavorName(flavorName) + .volume(volume).autoFloatingIp(autoFloatingIp).userScriptPath(userScriptPath).serverPaths(serverPaths) + .createServerOptions(options).scheduledExecutorService(factory.createExecutorService(imageName))); cloudImages.add(image); diff --git a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImage.java b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImage.java index 4e8c36b..42ee361 100644 --- a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImage.java +++ b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudImage.java @@ -7,7 +7,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.jclouds.openstack.nova.v2_0.features.ServerApi; import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,6 +31,10 @@ public class OpenstackCloudImage implements CloudImage { private final String openstackImageName; @NotNull private final String flavorName; + @Nullable + private final String volumeName; + @Nullable + private final String volumeDevice; @NotNull private final boolean autoFloatingIp; @NotNull @@ -50,20 +53,19 @@ public class OpenstackCloudImage implements CloudImage { @Nullable private final CloudErrorInfo errorInfo; - public OpenstackCloudImage(@NotNull final OpenstackApi openstackApi, @NotNull final String imageId, @NotNull final String imageName, - @NotNull final String openstackImageName, @NotNull final String flavorId, @NotNull boolean autoFloatingIp, - @NotNull final CreateServerOptions options, @Nullable final String userScriptPath, @NotNull final ServerPaths serverPaths, - @NotNull final ScheduledExecutorService executor) { - this.openstackApi = openstackApi; - this.imageId = imageId; - this.imageName = imageName; - this.openstackImageName = openstackImageName; - this.flavorName = flavorId; - this.autoFloatingIp = autoFloatingIp; - this.options = options; - this.userScriptPath = userScriptPath; - this.serverPaths = serverPaths; - this.executor = executor; + public OpenstackCloudImage(@NotNull final CreateImageOptions cio) { + this.openstackApi = cio.getOpenstackApi(); + this.imageId = cio.getImageId(); + this.imageName = cio.getImageName(); + this.openstackImageName = cio.getOpenstackImageName(); + this.flavorName = cio.getFlavorName(); + this.volumeName = cio.getVolumeName(); + this.volumeDevice = cio.getVolumeDevice(); + this.autoFloatingIp = cio.isAutoFloatingIp(); + this.userScriptPath = cio.getUserScriptPath(); + this.serverPaths = cio.getServerPaths(); + this.options = cio.getCreateServerOptions(); + this.executor = cio.getScheduledExecutorService(); this.errorInfo = null; // FIXME: need to use this, really. @@ -76,21 +78,48 @@ public OpenstackCloudImage(@NotNull final OpenstackApi openstackApi, @NotNull fi }, true), 3, 3, TimeUnit.SECONDS); } + private void forgetInstance(@NotNull final OpenstackCloudInstance instance) { + instances.remove(instance.getInstanceId()); + } + + @Nullable + public OpenstackCloudInstance findInstanceById(@NotNull final String instanceId) { + return instances.get(instanceId); + } + @NotNull - public ServerApi getNovaServerApi() { - return openstackApi.getNovaServerApi(); + public Collection getInstances() { + return Collections.unmodifiableCollection(instances.values()); } - public String getFloatingIpAvailable() { - return openstackApi.getFloatingIpAvailable(); + @Nullable + @Override + public Integer getAgentPoolId() { + return null; } - public void associateFloatingIp(String serverId, String ip) { - openstackApi.associateFloatingIp(serverId, ip); + @Nullable + public CloudErrorInfo getErrorInfo() { + return errorInfo; } - private void forgetInstance(@NotNull final OpenstackCloudInstance instance) { - instances.remove(instance.getInstanceId()); + @NotNull + public synchronized OpenstackCloudInstance startNewInstance(@NotNull final CloudInstanceUserData data) { + final String instanceId = instanceIdGenerator.next(); + final OpenstackCloudInstance instance = new OpenstackCloudInstance(this, instanceId, openstackApi, serverPaths, executor); + + instances.put(instanceId, instance); + instance.start(data); + + return instance; + } + + void dispose() { + for (final OpenstackCloudInstance instance : instances.values()) { + instance.terminate(); + } + instances.clear(); + executor.shutdown(); } @NotNull @@ -138,43 +167,13 @@ public String getUserScriptPath() { return this.userScriptPath; } - @NotNull - public Collection getInstances() { - return Collections.unmodifiableCollection(instances.values()); - } - - @Nullable - public OpenstackCloudInstance findInstanceById(@NotNull final String instanceId) { - return instances.get(instanceId); - } - @Nullable - @Override - public Integer getAgentPoolId() { - return null; + public String getVolumeName() { + return this.volumeName; } @Nullable - public CloudErrorInfo getErrorInfo() { - return errorInfo; - } - - @NotNull - public synchronized OpenstackCloudInstance startNewInstance(@NotNull final CloudInstanceUserData data) { - final String instanceId = instanceIdGenerator.next(); - final OpenstackCloudInstance instance = new OpenstackCloudInstance(this, instanceId, serverPaths, executor); - - instances.put(instanceId, instance); - instance.start(data); - - return instance; - } - - void dispose() { - for (final OpenstackCloudInstance instance : instances.values()) { - instance.terminate(); - } - instances.clear(); - executor.shutdown(); + public String getVolumeDevice() { + return this.volumeDevice; } } diff --git a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudInstance.java b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudInstance.java index 8606b30..5303d71 100644 --- a/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudInstance.java +++ b/cloud-openstack-server/src/main/java/jetbrains/buildServer/clouds/openstack/OpenstackCloudInstance.java @@ -9,12 +9,12 @@ import java.util.concurrent.atomic.AtomicReference; import org.jclouds.openstack.nova.v2_0.domain.Server; +import org.jclouds.openstack.nova.v2_0.domain.Server.Status; import org.jclouds.openstack.nova.v2_0.domain.ServerCreated; import org.jclouds.openstack.nova.v2_0.options.CreateServerOptions; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.google.common.base.Strings; import com.intellij.openapi.diagnostic.Logger; import jetbrains.buildServer.clouds.CloudConstants; @@ -39,6 +39,8 @@ public class OpenstackCloudInstance implements CloudInstance { @NotNull private final OpenstackCloudImage cloudImage; @NotNull + private final OpenstackApi openstackApi; + @NotNull private final Date startDate; @Nullable private volatile CloudErrorInfo errorInfo; @@ -46,12 +48,15 @@ public class OpenstackCloudInstance implements CloudInstance { private ServerCreated serverCreated; @NotNull private final ScheduledExecutorService executor; + + @Nullable private String ip; private final AtomicReference status = new AtomicReference<>(InstanceStatus.SCHEDULED_TO_START); - public OpenstackCloudInstance(@NotNull final OpenstackCloudImage image, @NotNull final String instanceId, @NotNull ServerPaths serverPaths, - @NotNull ScheduledExecutorService executor) { + public OpenstackCloudInstance(@NotNull final OpenstackCloudImage image, @NotNull final String instanceId, + @NotNull final OpenstackApi openstackApi, @NotNull ServerPaths serverPaths, @NotNull ScheduledExecutorService executor) { + this.openstackApi = openstackApi; this.cloudImage = image; this.instanceId = instanceId; this.serverPaths = serverPaths; @@ -63,7 +68,7 @@ public OpenstackCloudInstance(@NotNull final OpenstackCloudImage image, @NotNull public synchronized void updateStatus() { LOG.debug(String.format("Pinging %s for status", getName())); if (serverCreated != null) { - Server server = cloudImage.getNovaServerApi().get(serverCreated.getId()); + Server server = openstackApi.getServer(serverCreated.getId()); if (server != null) { Server.Status currentStatus = server.getStatus(); LOG.debug(String.format("Getting instance status from openstack for %s, result is %s", getName(), currentStatus)); @@ -137,6 +142,7 @@ public Date getStartedTime() { return startDate; } + @Nullable public String getNetworkIdentity() { return ip; } @@ -167,7 +173,7 @@ public void terminate() { setStatus(InstanceStatus.SCHEDULED_TO_STOP); try { if (serverCreated != null) { - cloudImage.getNovaServerApi().delete(serverCreated.getId()); + openstackApi.deleteServer(serverCreated.getId()); setStatus(InstanceStatus.STOPPING); } } catch (final Exception e) { @@ -189,69 +195,117 @@ public StartAgentCommand(@NotNull final CloudInstanceUserData data) { this.userData = data; } - private byte[] readUserScriptFile(File userScriptFile) throws IOException { - try { - String userScript = FileUtil.readText(userScriptFile); - // this is userScript actually, but CreateServerOptionscalls it userData - return userScript.trim().getBytes(StandardCharsets.UTF_8); - } catch (IOException e) { - throw new IOException(String.format("Error in reading user script: %s", e.getMessage()), e); - } - } - public void run() { try { - String floatingIp = null; - if (cloudImage.isAutoFloatingIp()) { - // Floating ip should be in meta-data before instance start - // If multiple instances start in parallel, perhaps same ip could be retrieved - // So an ip reservation mechanism should implemented in this case - LOG.debug("Retrieve floating ip for future instance association"); - floatingIp = cloudImage.getFloatingIpAvailable(); - if (StringUtil.isEmpty(floatingIp)) { - throw new OpenstackException("Floating ip could not be found, cancel instance start"); - } - LOG.debug(String.format("Floating ip: %s", floatingIp)); - userData.addAgentConfigurationParameter(OpenstackCloudParameters.AGENT_CLOUD_IP, floatingIp); - } + // If floating ip, retrieve one and store in meta-data + String floatingIp = retrieveAndStoreInMetaDataFloatingIp(); - String openstackImageId = cloudImage.getOpenstackImageId(); - String flavorId = cloudImage.getFlavorId(); CreateServerOptions options = cloudImage.getImageOptions(); options.metadata(userData.getCustomAgentConfigurationParameters()); - // TODO: that code should be in OpenstackCloudImage but as we make it possible to change userScript without touching teamcity, that - // hack takes place, sorry - String userScriptPath = cloudImage.getUserScriptPath(); - if (!Strings.isNullOrEmpty(userScriptPath)) { - File pluginData = serverPaths.getPluginDataDirectory(); - File userScriptFile = new File(new File(pluginData, OpenstackCloudParameters.PLUGIN_SHORT_NAME), userScriptPath); - options.userData(readUserScriptFile(userScriptFile)).configDrive(true); - } + // If user script, read it and store in meta-data + retrieveAndStoreInMetaDataUserScriptFile(options); - LOG.debug(String.format("Creating openstack instance with flavorId=%s, imageId=%s, options=%s", flavorId, openstackImageId, options)); - serverCreated = cloudImage.getNovaServerApi().create(getName(), openstackImageId, flavorId, options); - - if (cloudImage.isAutoFloatingIp()) { - LOG.debug(String.format("Associating floating ip to serverId %s", serverCreated.getId())); - // Associating floating IP. Require fixed IP so wait until found - final long maxWait = 120000; - final long beginWait = System.currentTimeMillis(); - while (cloudImage.getNovaServerApi().get(serverCreated.getId()).getAddresses().isEmpty()) { - if (System.currentTimeMillis() > (beginWait + maxWait)) { - throw new OpenstackException(String.format("Waiting fixed ip fails, taking more than %s ms", maxWait)); - } - LOG.debug(String.format("(Waiting fixed ip before floating ip association on serverId: %s)", serverCreated.getId())); - Thread.sleep(1000); - } - cloudImage.associateFloatingIp(serverCreated.getId(), floatingIp); - ip = floatingIp; - } + LOG.debug(String.format("Creating openstack instance with flavorId=%s, imageId=%s, options=%s", cloudImage.getFlavorId(), + cloudImage.getOpenstackImageId(), options)); + serverCreated = openstackApi.createServer(getName(), cloudImage.getOpenstackImageId(), cloudImage.getFlavorId(), options); + + // Attach volume + attachVolume(); + + // Associate floating ip + ip = associateFloatingIp(floatingIp); setStatus(InstanceStatus.STARTING); } catch (final Exception e) { processError(e); } } + + private String associateFloatingIp(String floatingIp) throws InterruptedException, OpenstackException { + if (!cloudImage.isAutoFloatingIp()) { + return null; + } + LOG.debug(String.format("Associating floating ip to serverId %s", serverCreated.getId())); + // Associating floating IP. Require fixed IP so wait until found + final long maxWait = 120000; + final long beginWait = System.currentTimeMillis(); + while (openstackApi.getServer(serverCreated.getId()).getAddresses().isEmpty()) { + if (System.currentTimeMillis() > (beginWait + maxWait)) { + throw new OpenstackException(String.format("Waiting fixed ip fails, taking more than %s ms", maxWait)); + } + LOG.debug(String.format("(Waiting fixed ip before floating ip association on serverId: %s)", serverCreated.getId())); + Thread.sleep(1000); + } + openstackApi.associateFloatingIp(serverCreated.getId(), floatingIp); // NOSONAR : Floating IP can't be null here + return floatingIp; + } + + private void attachVolume() throws OpenstackException, InterruptedException { + String volumeName = cloudImage.getVolumeName(); + if (StringUtil.isEmpty(volumeName)) { + return; + } + String volumeDevice = cloudImage.getVolumeDevice(); + LOG.debug(String.format("Attach volume '%s' (with device '%s') to serverId %s", volumeName, volumeDevice, serverCreated.getId())); + String volumeId = openstackApi.getVolumeIdByName(volumeName); + if (StringUtil.isEmpty(volumeId)) { + throw new OpenstackException(String.format("Volume Id can't be retrieved for volume name: %s", volumeName)); + } + if (StringUtil.isEmpty(volumeDevice)) { + throw new OpenstackException(String.format("Volume device can't be empty for volume name: %s", volumeName)); + } + while (!Status.ACTIVE.equals(openstackApi.getServer(serverCreated.getId()).getStatus())) { + final long maxWait = 120000; + final long beginWait = System.currentTimeMillis(); + if (System.currentTimeMillis() > (beginWait + maxWait)) { + throw new OpenstackException(String.format("Waiting fixed ip fails, taking more than %s ms", maxWait)); + } + LOG.debug(String.format("(Waiting instance available before volume attachment on serverId: %s)", serverCreated.getId())); + Thread.sleep(1000); + } + openstackApi.attachVolumeToServer(serverCreated.getId(), volumeId, volumeDevice); + } + + private String retrieveAndStoreInMetaDataFloatingIp() throws OpenstackException { + if (!cloudImage.isAutoFloatingIp()) { + return null; + } + // Floating ip should be in meta-data before instance start + // If multiple instances start in parallel, perhaps same ip could be retrieved + // So an ip reservation mechanism should implemented in this case + LOG.debug("Retrieve floating ip for future instance association"); + String floatingIp = openstackApi.getFloatingIpAvailable(); + if (StringUtil.isEmpty(floatingIp)) { + throw new OpenstackException("Floating ip could not be found, cancel instance start"); + } + LOG.debug(String.format("Floating ip: %s", floatingIp)); + userData.addAgentConfigurationParameter(OpenstackCloudParameters.AGENT_CLOUD_IP, floatingIp); + return floatingIp; + + } + + private void retrieveAndStoreInMetaDataUserScriptFile(CreateServerOptions options) throws IOException { + // TODO: that code should be in OpenstackCloudImage but as we make it possible to change userScript without touching teamcity, that + // hack takes place, sorry + String userScriptPath = cloudImage.getUserScriptPath(); + if (StringUtil.isEmpty(userScriptPath)) { + return; + } + LOG.debug(String.format("Read user script: %s", userScriptPath)); + File pluginData = serverPaths.getPluginDataDirectory(); + File userScriptFile = new File(new File(pluginData, OpenstackCloudParameters.PLUGIN_SHORT_NAME), userScriptPath); + options.userData(readUserScriptFile(userScriptFile)).configDrive(true); + } + + private byte[] readUserScriptFile(File userScriptFile) throws IOException { + try { + String userScript = FileUtil.readText(userScriptFile); + // this is userScript actually, but CreateServerOptionscalls it userData + return userScript.trim().getBytes(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IOException(String.format("Error in reading user script: %s", e.getMessage()), e); + } + } } }