/** * Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * Licensed 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 net.roboconf.target.openstack.internal; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.API_URL; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.DELETE_ON_TERMINATION; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.FLOATING_IP_POOL; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.OBJ_STORAGE_DOMAINS; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.VOLUME_DELETE_OT_PREFIX; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.VOLUME_MOUNT_POINT_PREFIX; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.VOLUME_NAME_PREFIX; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.VOLUME_SIZE_GB_PREFIX; import static net.roboconf.target.openstack.internal.OpenstackIaasHandler.VOLUME_TYPE_PREFIX; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import org.jclouds.openstack.nova.v2_0.NovaApi; import org.jclouds.openstack.nova.v2_0.domain.FloatingIP; 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.Volume; import org.jclouds.openstack.nova.v2_0.extensions.FloatingIPApi; import org.jclouds.openstack.nova.v2_0.extensions.VolumeApi; import org.jclouds.openstack.nova.v2_0.extensions.VolumeAttachmentApi; import org.jclouds.openstack.nova.v2_0.options.CreateVolumeOptions; import org.jclouds.openstack.swift.v1.SwiftApi; import org.jclouds.openstack.swift.v1.domain.Container; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.utils.Utils; import net.roboconf.target.api.AbstractThreadedTargetHandler.MachineConfigurator; import net.roboconf.target.api.TargetException; /** * A machine configurator for Openstack. * @author Vincent Zurczak - Linagora * @author Amadou Diarra - Université Joseph Fourier */ public class OpenstackMachineConfigurator implements MachineConfigurator { /** * A set of locks to prevent concurrent access to the pool of floating IP addresses. * <p> * The idea is to have one lock per Openstack URL. It could be improved by associating * user names too, but this does not seem necessary at the moment. * </p> */ private static final ConcurrentHashMap<String,Object> URL_TO_LOCK = new ConcurrentHashMap<> (); /** * The steps of a workflow. * <ul> * <li>WAITING_VM: we wait for the VM to be active.</li> * <li>ASSOCIATE_FLOATING_IP: a floating IP has to be associated, if necessary and if possible.</li> * <li>OBJ_STORAGE: create domains for object storage.</li> * <li>CREATE_VOLUME: create volumes.</li> * <li>ATTACH_VOLUME: attach volumes to the VM.</li> * <li>COMPLETE: there is nothing to do anymore.</li> * </ul> * * @author Vincent Zurczak - Linagora */ public static enum State { WAITING_VM, ASSOCIATE_FLOATING_IP, COMPLETE, OBJ_STORAGE, CREATE_VOLUME, ATTACH_VOLUME; } private final Instance scopedInstance; private final String machineId, applicationName; private final Map<String, String> targetProperties; private final Logger logger = Logger.getLogger(getClass().getName()); private final Map<String,String> storageIdToVolumeId = new HashMap<> (); private final Map<String,Boolean> volumeIdToAttached = new HashMap<> (); private NovaApi novaApi; private State state = State.WAITING_VM; /** * Constructor. * @param targetProperties * @param machineId */ public OpenstackMachineConfigurator( Map<String, String> targetProperties, String machineId, String applicationName, Instance scopedInstance ) { this.machineId = machineId; this.applicationName = applicationName; this.targetProperties = targetProperties; this.scopedInstance = scopedInstance; } @Override public Instance getScopedInstance() { return this.scopedInstance; } @Override public void close() throws IOException { if( this.novaApi != null) this.novaApi.close(); } @Override public boolean configure() throws TargetException { if( this.novaApi == null ) this.novaApi = OpenstackIaasHandler.novaApi( this.targetProperties ); if( this.state == State.WAITING_VM ) { if( checkVmIsOnline()) this.state = State.ASSOCIATE_FLOATING_IP; } if( this.state == State.ASSOCIATE_FLOATING_IP ) { if( associateFloatingIp()) this.state = State.OBJ_STORAGE; } if( this.state == State.OBJ_STORAGE ) { if( prepareObjectStorage()) this.state = State.CREATE_VOLUME; } if( this.state == State.CREATE_VOLUME ) { if( createVolumes()) this.state = State.ATTACH_VOLUME; } if( this.state == State.ATTACH_VOLUME ) { if( attachVolumes()) this.state = State.COMPLETE; } return this.state == State.COMPLETE; } /** * Checks whether a VM is created. * @return true if it is online, false otherwise */ private boolean checkVmIsOnline() { String zoneName = OpenstackIaasHandler.findZoneName( this.novaApi, this.targetProperties ); Server server = this.novaApi.getServerApiForZone( zoneName ).get(this.machineId); return Status.ACTIVE.equals(server.getStatus()); } /** * Associates a floating IP to the VM (if necessary and if possible). * @return true if this operation successfully completed, false otherwise */ private boolean associateFloatingIp() { // Associating a floating IP requires a client-side synchronization // since Openstack does not provide it. Indeed, it can associate a floating IP to a new // server, even if this address was already associated with another one. String floatingIpPool = this.targetProperties.get( FLOATING_IP_POOL ); if (Utils.isEmptyOrWhitespaces(floatingIpPool)) return true; // Protected section to prevent using a same IP for several machines. // An action is already in progress for this URL? // Then return immediately, we will try in the next scheduled run. String url = this.targetProperties.get( API_URL ); if (URL_TO_LOCK.putIfAbsent(url, new Object()) != null) return false; // Deal with the association. boolean done = false; try { // Find a floating IP String availableIp = null; String zoneName = OpenstackIaasHandler.findZoneName( this.novaApi, this.targetProperties ); FloatingIPApi floatingIPApi = this.novaApi.getFloatingIPExtensionForZone( zoneName ).get(); for(FloatingIP ip : floatingIPApi.list().toList()) { if (ip.getFixedIp() == null) { availableIp = ip.getIp(); break; } } // And associate it if (availableIp != null) { floatingIPApi.addToServer(availableIp, this.machineId); this.scopedInstance.data.put( OpenstackIaasHandler.FLOATING_IP, availableIp ); } else { this.logger.warning("No floating IP was available in Openstack (pool '" + floatingIpPool + "')."); } done = true; } finally { URL_TO_LOCK.remove(url); } return done; } /** * Configures the object storage. * @return true if the configuration is over * @throws TargetException */ public boolean prepareObjectStorage() throws TargetException { String domains = this.targetProperties.get( OBJ_STORAGE_DOMAINS ); if( ! Utils.isEmptyOrWhitespaces( domains )) { // Get the Swift API String zoneName = OpenstackIaasHandler.findZoneName( this.novaApi, this.targetProperties ); SwiftApi swiftApi = OpenstackIaasHandler.swiftApi( this.targetProperties ); try { // List domains List<String> existingDomainNames = new ArrayList<> (); for( Container container : swiftApi.getContainerApi( zoneName ).list()) { existingDomainNames.add( container.getName()); } // Create missing domains List<String> domainsToCreate = Utils.splitNicely( domains, "," ); domainsToCreate.removeAll( existingDomainNames ); for( String domainName : domainsToCreate ) { this.logger.info( "Creating container " + domainName + " (object storage)..." ); swiftApi.getContainerApi( zoneName ).create( domainName ); } } catch( Exception e ) { throw new TargetException( e ); } finally { // Release the API try { swiftApi.close(); } catch( IOException e ) { throw new TargetException( e ); } } } return true; } /** * Creates block storage volumes in Openstack infrastructure. * <p> * Volume creation goes in a single row. For a given VM, we should enter this method * only once. * </p> * * @throws TargetException */ public boolean createVolumes() throws TargetException { String zoneName = OpenstackIaasHandler.findZoneName( this.novaApi, this.targetProperties ); for( String storageId : OpenstackIaasHandler.findStorageIds( this.targetProperties )) { // Prepare the parameters String name = OpenstackIaasHandler.findStorageProperty( this.targetProperties, storageId, VOLUME_NAME_PREFIX ); name = OpenstackIaasHandler.expandVolumeName( name, this.applicationName, this.scopedInstance.getName()); VolumeApi volumeApi = this.novaApi.getVolumeExtensionForZone( zoneName ).get(); // If the volume should not volatile (i.e. not deleted on termination), we try to reuse it, if it exists. String deleteOnT = OpenstackIaasHandler.findStorageProperty( this.targetProperties, storageId, VOLUME_DELETE_OT_PREFIX ); boolean deleteOnTermination = Boolean.parseBoolean( deleteOnT ); String volumeId = null; if( ! deleteOnTermination ) { for( Volume vol : volumeApi.list()) { if( name.equals( vol.getName())) { this.logger.info( "Volume " + name + " (" + vol.getId() + ") already exists and is not volatile. It will be reused." ); volumeId = vol.getId(); break; } } } // Otherwise, create it. if( volumeId == null ) { String volumeType = OpenstackIaasHandler.findStorageProperty( this.targetProperties, storageId, VOLUME_TYPE_PREFIX ); String volumeSize = OpenstackIaasHandler.findStorageProperty( this.targetProperties, storageId, VOLUME_SIZE_GB_PREFIX ); int vsize = Integer.parseInt( volumeSize ); CreateVolumeOptions options = CreateVolumeOptions.Builder.name( name ); if( ! Utils.isEmptyOrWhitespaces( volumeType )) options = options.volumeType( volumeType ); if( deleteOnTermination ) { Map<String,String> metadata = new HashMap<>( 1 ); metadata.put( DELETE_ON_TERMINATION, "true" ); options = options.metadata( metadata ); } Volume volume = volumeApi.create( vsize, options ); volumeId = volume.getId(); } if( Utils.isEmptyOrWhitespaces( volumeId )) throw new TargetException( "Volume " + name + " was not found and could not be created." ); this.logger.info( "Volume " + volumeId + " was successfully created." ); this.storageIdToVolumeId.put( storageId, volumeId ); } return true; } /** * Attaches the created volume to a device. * <p> * Attachment can be iterative, as not all the volumes may be available * at the same moment. * </p> * */ public boolean attachVolumes() { boolean allAttached = true; String zoneName = OpenstackIaasHandler.findZoneName( this.novaApi, this.targetProperties ); for( Map.Entry<String,String> entry : this.storageIdToVolumeId.entrySet()) { String volumeId = entry.getValue(); String storageId = entry.getKey(); VolumeApi volumeApi = this.novaApi.getVolumeExtensionForZone( zoneName ).get(); Volume createdVolume = volumeApi.get( volumeId ); // Already attached? Skip... Boolean attached = this.volumeIdToAttached.get( volumeId ); if( attached != null && attached ) continue; // Otherwise, try to attach it, if possible. if( createdVolume.getStatus() == Volume.Status.AVAILABLE ) { String device = OpenstackIaasHandler.findStorageProperty( this.targetProperties, storageId, VOLUME_MOUNT_POINT_PREFIX ); VolumeAttachmentApi volumeAttachmentApi = this.novaApi.getVolumeAttachmentExtensionForZone( zoneName ).get(); volumeAttachmentApi.attachVolumeToServerAsDevice( volumeId, this.machineId, device ); // Notice: there is no way, unlike in AWS, to specify a volume should be deleted when the // server terminates. This option is only available with BlockDeviceMapping, which means // booting a server from a volume (which is not the same than attaching a volume to a server). // See https://review.openstack.org/#/c/67067/ // BlockDeviceMapping would rather be used when creating the server with Nova. // And it is a real mess to understand. // FIXME: how about when attachment fails (e.g. if the volume is already attached to another VM)? this.volumeIdToAttached.put( volumeId, Boolean.TRUE ); this.logger.info( "Volume " + volumeId + " was successfully attached to " + this.scopedInstance.getName()); } // Not available, we will attach it later. else { this.logger.fine( "Volume " + volumeId + " is not yet available to be attached to " + this.scopedInstance.getName()); allAttached = false; } } return allAttached; } }