/** * 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 java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.jclouds.ContextBuilder; import org.jclouds.openstack.neutron.v2.NeutronApi; 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.ServerCreated; import org.jclouds.openstack.nova.v2_0.domain.Volume; import org.jclouds.openstack.nova.v2_0.domain.VolumeAttachment; 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.CreateServerOptions; import org.jclouds.openstack.swift.v1.SwiftApi; import org.jclouds.openstack.v2_0.domain.Resource; import com.google.common.base.Predicate; import net.roboconf.core.agents.DataHelpers; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.helpers.InstanceHelpers; import net.roboconf.core.utils.Utils; import net.roboconf.target.api.AbstractThreadedTargetHandler; import net.roboconf.target.api.TargetException; import net.roboconf.target.api.TargetHandlerParameters; /** * @author Pierre-Yves Gibello - Linagora * @author Amadou Diarra - UJF */ public class OpenstackIaasHandler extends AbstractThreadedTargetHandler { public static final String TARGET_ID = "iaas-openstack"; static final String FLOATING_IP = TARGET_ID + ".floating-ip"; static final String TPL_VOLUME_NAME = "%NAME%"; static final String TPL_VOLUME_APP = "%APP%"; static final String DELETE_ON_TERMINATION = "delete.on.termination"; private static final String PROVIDER_NOVA = "openstack-nova"; private static final String PROVIDER_NEUTRON = "openstack-neutron"; private static final String PROVIDER_SWIFT = "openstack-swift"; // "Basic" options static final String IMAGE_NAME = "openstack.image-name"; static final String TENANT_NAME = "openstack.tenant-name"; static final String KEY_PAIR = "openstack.key-pair"; static final String FLAVOR_NAME = "openstack.flavor-name"; static final String SECURITY_GROUP = "openstack.security-group"; static final String API_URL = "openstack.keystone-url"; static final String USER = "openstack.user"; static final String PASSWORD = "openstack.password"; static final String FLOATING_IP_POOL = "openstack.floating-ip-pool"; static final String NETWORK_ID = "openstack.network-id"; static final String REGION_NAME = "openstack.region-name"; // Storage has several options static final String USE_BLOCK_STORAGE = "openstack.use-block-storage"; static final String VOLUME_MOUNT_POINT_PREFIX = "openstack.volume-mount-point."; static final String VOLUME_NAME_PREFIX = "openstack.volume-name."; static final String VOLUME_SIZE_GB_PREFIX = "openstack.volume-size."; static final String VOLUME_DELETE_OT_PREFIX = "openstack.delete-volume-on-termination."; static final String VOLUME_TYPE_PREFIX = "openstack.volume-type."; // Object storage static final String OBJ_STORAGE_DOMAINS = "openstack.obj-storage"; static final Map<String,String> DEFAULTS = new HashMap<> (); static { DEFAULTS.put( VOLUME_MOUNT_POINT_PREFIX, "/dev/vdb" ); DEFAULTS.put( VOLUME_NAME_PREFIX, "roboconf-" + TPL_VOLUME_APP + "-" + TPL_VOLUME_NAME ); DEFAULTS.put( VOLUME_SIZE_GB_PREFIX, "5" ); DEFAULTS.put( VOLUME_DELETE_OT_PREFIX, "false" ); } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler#getTargetId() */ @Override public String getTargetId() { return TARGET_ID; } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #createMachine(net.roboconf.target.api.TargetHandlerParameters) */ @Override public String createMachine( TargetHandlerParameters parameters ) throws TargetException { this.logger.fine( "Creating a new machine." ); // For IaaS, we only expect root instance names to be passed if( InstanceHelpers.countInstances( parameters.getScopedInstancePath()) > 1 ) throw new TargetException( "Only root instances can be passed in arguments." ); // Validate all the properties here String rootInstanceName = InstanceHelpers.findRootInstancePath( parameters.getScopedInstancePath()); Map<String,String> targetProperties = parameters.getTargetProperties(); validateAll( targetProperties, parameters.getApplicationName(), rootInstanceName ); // Prepare the work NovaApi novaApi = OpenstackIaasHandler.novaApi( targetProperties ); String zoneName = findZoneName( novaApi, targetProperties ); String vmName = parameters.getApplicationName() + "." + rootInstanceName; // Find flavor and image IDs String flavorId = null; String flavorName = targetProperties.get( OpenstackIaasHandler.FLAVOR_NAME ); for( Resource res : novaApi.getFlavorApiForZone( zoneName ).list().concat()) { if( res.getName().equalsIgnoreCase( flavorName )) { flavorId = res.getId(); break; } } if( flavorId == null ) throw new TargetException( "No flavor named '" + flavorName + "' was found." ); String imageId = null; String imageName = targetProperties.get( OpenstackIaasHandler.IMAGE_NAME ); for( Resource res : novaApi.getImageApiForZone( zoneName ).list().concat()) { if( res.getName().equalsIgnoreCase( imageName )) { imageId = res.getId(); break; } } if( imageId == null ) throw new TargetException( "No image named '" + imageName + "' was found." ); // Prepare the server creation Map<String,String> metadata = new HashMap<>(3); metadata.put( "Application Name", parameters.getApplicationName()); metadata.put( "Root Instance Name", rootInstanceName ); metadata.put( "Created by", "Roboconf" ); try { String userData = DataHelpers.writeUserDataAsString( parameters.getMessagingProperties(), parameters.getDomain(), parameters.getApplicationName(), rootInstanceName ); CreateServerOptions options = CreateServerOptions.Builder .keyPairName( targetProperties.get( OpenstackIaasHandler.KEY_PAIR )) .securityGroupNames( targetProperties.get( OpenstackIaasHandler.SECURITY_GROUP )) .userData( userData.getBytes( "UTF-8" )) .metadata( metadata ); String networkId = targetProperties.get( OpenstackIaasHandler.NETWORK_ID ); if( ! Utils.isEmptyOrWhitespaces( networkId )) options = options.networks( networkId ); ServerCreated server = novaApi.getServerApiForZone( zoneName ).create( vmName, imageId, flavorId, options); String machineId = server.getId(); novaApi.close(); return machineId; } catch( Exception e ) { throw new TargetException( e ); } } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #isMachineRunning(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public boolean isMachineRunning( TargetHandlerParameters parameters, String machineId ) throws TargetException { NovaApi novaApi = novaApi( parameters.getTargetProperties()); String zoneName = findZoneName( novaApi, parameters.getTargetProperties()); Server server = novaApi.getServerApiForZone( zoneName ).get( machineId ); boolean running = false; if( server != null ) running = server.getStatus() == Status.ACTIVE || server.getStatus() == Status.REBOOT; return running; } /* * (non-Javadoc) * @see net.roboconf.target.api.AbstractThreadedTargetHandler#machineConfigurator( * net.roboconf.target.api.TargetHandlerParameters, java.lang.String, net.roboconf.core.model.beans.Instance) */ @Override public MachineConfigurator machineConfigurator( TargetHandlerParameters parameters, String machineId, Instance scopedInstance ) { return new OpenstackMachineConfigurator( parameters.getTargetProperties(), machineId, parameters.getApplicationName(), scopedInstance ); } /* * (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #terminateMachine(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public void terminateMachine( TargetHandlerParameters parameters, String machineId ) throws TargetException { try { this.logger.info( "Terminating Openstack machine. Machine ID: " + machineId ); cancelMachineConfigurator( machineId ); NovaApi novaApi = novaApi( parameters.getTargetProperties()); String zoneName = findZoneName( novaApi, parameters.getTargetProperties()); // List the attached volumes, if any. Set<String> volumeIds = new HashSet<> (); VolumeAttachmentApi volumeAttachmentApi = novaApi.getVolumeAttachmentExtensionForZone( zoneName ).get(); for( VolumeAttachment vol : volumeAttachmentApi.listAttachmentsOnServer( machineId )) { volumeIds.add( vol.getVolumeId()); } // Delete the VM novaApi.getServerApiForZone( zoneName ).delete( machineId ); // Delete the volumes? VolumeApi volumeApi = novaApi.getVolumeExtensionForZone( zoneName ).get(); for( String volumeId : volumeIds ) { Volume volume = volumeApi.get( volumeId ); if( volume == null ) { this.logger.warning( "Volume " + volumeId + " was not found. Deletion check is aborted for this volume." ); continue; } String del = volume.getMetadata().get( DELETE_ON_TERMINATION ); if( Boolean.parseBoolean( del )) { this.logger.info( "Deleting volume " + volumeId ); volumeApi.delete( volumeId ); } else { this.logger.info( "Orphan volume " + volumeId + " is kept and will not be deleted." ); } } novaApi.close(); } catch( IOException e ) { throw new TargetException( e ); } } /* (non-Javadoc) * @see net.roboconf.target.api.TargetHandler * #retrievePublicIpAddress(net.roboconf.target.api.TargetHandlerParameters, java.lang.String) */ @Override public String retrievePublicIpAddress( TargetHandlerParameters parameters, String machineId ) throws TargetException { NovaApi novaApi = novaApi( parameters.getTargetProperties()); String zoneName = findZoneName( novaApi, parameters.getTargetProperties()); String result = null; Server server = novaApi.getServerApiForZone( zoneName ).get( machineId ); if( server != null ) { result = server.getAccessIPv4(); // Nothing found? Check floating IPs if( result == null ) { FloatingIPApi floatingIPApi = novaApi.getFloatingIPExtensionForZone( zoneName ).get(); List<FloatingIP> ips = floatingIPApi.list().filter( new InstancePredicate( machineId )).toList(); if( ips.size() > 0 ) result = ips.get( 0 ).getIp(); } } return result; } /** * A predicate that finds the floating IPs associated with a given server. * @author Vincent Zurczak - Linagora */ static class InstancePredicate implements Predicate<FloatingIP> { private final String instanceId; /** * Constructor. * @param instanceId */ public InstancePredicate( String instanceId ) { this.instanceId = instanceId; } @Override public boolean apply( FloatingIP input ) { return Objects.equals( input.getInstanceId(), this.instanceId ); } } /** * Creates a JCloud context for Nova. * @param targetProperties the target properties * @return a non-null object * @throws TargetException if the target properties are invalid */ static NovaApi novaApi( Map<String,String> targetProperties ) throws TargetException { validate( targetProperties ); return ContextBuilder .newBuilder( PROVIDER_NOVA ) .endpoint( targetProperties.get( API_URL )) .credentials( identity( targetProperties ), targetProperties.get( PASSWORD )) .buildApi( NovaApi.class ); } /** * Creates a JCloud context for Swift. * @param targetProperties the target properties * @return a non-null object * @throws TargetException if the target properties are invalid */ static SwiftApi swiftApi( Map<String,String> targetProperties ) throws TargetException { validate( targetProperties ); return ContextBuilder .newBuilder( PROVIDER_SWIFT ) .endpoint( targetProperties.get( API_URL )) .credentials( identity( targetProperties ), targetProperties.get( PASSWORD )) .buildApi( SwiftApi.class ); } /** * Creates a JCloud context for Neutron. * @param targetProperties the target properties * @return a non-null object * @throws TargetException if the target properties are invalid */ // TODO: never used!!!??? static NeutronApi neutronApi( Map<String,String> targetProperties ) throws TargetException { validate( targetProperties ); return ContextBuilder .newBuilder( PROVIDER_NEUTRON ) .endpoint( targetProperties.get( API_URL )) .credentials( identity( targetProperties ), targetProperties.get( PASSWORD )) .buildApi( NeutronApi.class ); } /** * @param novaApi the nova client * @param targetProperties the target properties (not null) * @return a zone name (either the specified one, or the first found otherwise) */ static String findZoneName( NovaApi novaApi, Map<String,String> targetProperties ) { String zoneName = targetProperties.get( REGION_NAME ); if( Utils.isEmptyOrWhitespaces( zoneName )) zoneName = novaApi.getConfiguredZones().iterator().next(); return zoneName; } /** * Validates the basic target properties. * @param targetProperties the properties * @throws TargetException if an error occurred during the validation */ static void validate( Map<String,String> targetProperties ) throws TargetException { checkProperty( API_URL, targetProperties ); checkProperty( IMAGE_NAME, targetProperties ); checkProperty( TENANT_NAME, targetProperties ); checkProperty( FLAVOR_NAME, targetProperties ); checkProperty( SECURITY_GROUP, targetProperties ); checkProperty( KEY_PAIR, targetProperties ); checkProperty( USER, targetProperties ); checkProperty( PASSWORD, targetProperties ); } /** * Validates the target properties, including storage ones. * @param targetProperties * @param appName * @param instanceName * @throws TargetException */ static void validateAll( Map<String,String> targetProperties, String appName, String instanceName ) throws TargetException { // Basic checks validate( targetProperties ); // Storage checks Set<String> mountPoints = new HashSet<> (); Set<String> volumeNames = new HashSet<> (); for( String s : findStorageIds( targetProperties )) { // Unit tests should guarantee there is a default value for the "mount point". String mountPoint = findStorageProperty( targetProperties, s, VOLUME_MOUNT_POINT_PREFIX ); if( mountPoints.contains( mountPoint )) throw new TargetException( "Mount point '" + mountPoint + "' is already used by another volume for this VM." ); mountPoints.add( mountPoint ); // Same thing for the volume name String volumeName = findStorageProperty( targetProperties, s, VOLUME_NAME_PREFIX ); volumeName = expandVolumeName( volumeName, appName, instanceName ); if( volumeNames.contains( volumeName )) throw new TargetException( "Volume name '" + volumeName + "' is already used by another volume for this VM." ); volumeNames.add( volumeName ); // Validate volume size String volumesize = findStorageProperty( targetProperties, s, VOLUME_SIZE_GB_PREFIX ); try { Integer.valueOf( volumesize ); } catch( NumberFormatException e ) { throw new TargetException( "The volume size must be a valid integer.", e ); } } } /** * @param targetProperties the target properties (assumed to be valid) * @return the identity */ static String identity( Map<String,String> targetProperties ) { return targetProperties.get( TENANT_NAME ) + ":" + targetProperties.get( USER ); } /** * Finds the storage IDs (used as property suffixes). * @param targetProperties * @return a non-null list */ static List<String> findStorageIds( Map<String,String> targetProperties ) { List<String> result = new ArrayList<> (); String prop = targetProperties.get( USE_BLOCK_STORAGE ); if( ! Utils.isEmptyOrWhitespaces( prop )) { for( String s : Utils.splitNicely( prop, "," )) { if( ! Utils.isEmptyOrWhitespaces( s )) result.add( s ); } } return result; } /** * Finds a storage property for a given storage ID. * @param targetProperties * @param storageId * @param propertyPrefix one of the constants defined in this class * @return the property's value, or the default value otherwise, if one exists */ static String findStorageProperty( Map<String,String> targetProperties, String storageId, String propertyPrefix ) { String property = propertyPrefix + storageId; String value = targetProperties.get( property ); return Utils.isEmptyOrWhitespaces( value ) ? DEFAULTS.get( propertyPrefix ) : value.trim(); } /** * Updates a volume name by replacing template variables. * @param nameTemplate (not null) * @param appName (not null) * @param instanceName (not null) * @return a non-null string */ static String expandVolumeName( String nameTemplate, String appName, String instanceName ) { String name = nameTemplate.replace( TPL_VOLUME_NAME, instanceName ); name = name.replace( TPL_VOLUME_APP, appName ); name = name.replaceAll( "[\\W_-]", "-" ); return name; } private static void checkProperty( String propertyName, Map<String,String> targetProperties ) throws TargetException { if( ! targetProperties.containsKey( propertyName )) throw new TargetException( "Property '" + propertyName + "' is missing." ); if( Utils.isEmptyOrWhitespaces( targetProperties.get( propertyName ))) throw new TargetException( "Property '" + propertyName + "' must have a value." ); } }