/**
* 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.jclouds.internal;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.jclouds.ContextBuilder;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.ComputeServiceContext;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.Template;
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.TargetException;
import net.roboconf.target.api.TargetHandler;
import net.roboconf.target.api.TargetHandlerParameters;
/**
* FIXME: add user data support.
* Like key pairs, userData is not a generic mechanism with JClouds.
* However, running a script after may be better.
*
* @author Vincent ZURCZAK - Linagora
*/
public class JCloudsHandler implements TargetHandler {
public static final String TARGET_ID = "jclouds";
static final String PROVIDER_ID = "jclouds.provider-id";
static final String IDENTITY = "jclouds.identity";
static final String CREDENTIAL = "jclouds.credential";
static final String ENDPOINT = "jclouds.endpoint";
static final String IMAGE_NAME = "jclouds.image-name";
static final String SECURITY_GROUP = "jclouds.security-group";
static final String KEY_PAIR = "jclouds.key-pair";
static final String HARDWARE_NAME = "jclouds.hardware-name";
private final Logger logger = Logger.getLogger( getClass().getName());
/*
* (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." );
Map<String,String> targetProperties = parameters.getTargetProperties();
final String providerId = targetProperties.get( PROVIDER_ID );
ComputeService computeService = jcloudContext( targetProperties );
// 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." );
String rootInstanceName = InstanceHelpers.findRootInstancePath( parameters.getScopedInstancePath());
String machineId = null;
try {
// Create a template from an image and a flavor/hardware
Image image = null;
String imageName = targetProperties.get( IMAGE_NAME );
for( Image i : computeService.listImages()) {
if( i.getName().equalsIgnoreCase( imageName )) {
image = i;
break;
}
}
if( image == null )
throw new TargetException( "No image named '" + imageName + "' was found." );
Hardware hardware = null;
String hardwareName = targetProperties.get( HARDWARE_NAME );
for( Hardware h : computeService.listHardwareProfiles()) {
if( h.getName().equalsIgnoreCase( hardwareName )) {
hardware = h;
break;
}
}
if( hardware == null )
throw new TargetException( "No hardware named '" + hardwareName + "' was found." );
Template template = computeService.templateBuilder().fromImage( image ).hardwareId( hardware.getId()).build();
template.getOptions().securityGroups( targetProperties.get( SECURITY_GROUP ));
template.getOptions().userMetadata( "Application Name", parameters.getApplicationName());
template.getOptions().userMetadata( "Root Instance Name", rootInstanceName );
template.getOptions().userMetadata( "Domain", parameters.getDomain());
template.getOptions().userMetadata( "Created by", "Roboconf" );
// Specify our own key pair if the current provider supports it
String keyPairName = targetProperties.get( KEY_PAIR );
try {
if( ! Utils.isEmptyOrWhitespaces( keyPairName )) {
Method keyPairMethod = template.getOptions().getClass().getMethod( "keyPair", String.class );
keyPairMethod.invoke( template.getOptions(), keyPairName );
}
} catch( Exception e ) {
throw new TargetException( "Provider: " + providerId + " does not support specifying key pairs.", e );
}
String vmName = (parameters.getApplicationName() + "." + rootInstanceName).replaceAll( "\\.|\\s+", "-" );
Set<? extends NodeMetadata> nodes = computeService.createNodesInGroup( vmName, 1, template );
machineId = nodes.iterator().next().getId();
} catch( RunNodesException e ) {
throw new TargetException( "An error occurred while creating a new node with JClouds on provider " + providerId + ".", e );
} finally {
if( computeService != null )
computeService.getContext().close();
}
return machineId;
}
/*
* (non-Javadoc)
* @see net.roboconf.target.api.TargetHandler#configureMachine(
* net.roboconf.target.api.TargetHandlerParameters, java.lang.String, net.roboconf.core.model.beans.Instance)
*/
@Override
public void configureMachine( TargetHandlerParameters parameters, String machineId, Instance scopedInstance )
throws TargetException {
// It may require to be configured from the DM => add the right marker
scopedInstance.data.put( Instance.READY_FOR_CFG_MARKER, "true" );
this.logger.fine( "Configuring machine '" + machineId + "'..." );
}
/*
* (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 {
ComputeService computeService = jcloudContext( parameters.getTargetProperties());
return computeService.getNodeMetadata( machineId ) != null;
}
/*
* (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 {
this.logger.fine( "Terminating machine " + machineId );
ComputeService computeService = jcloudContext( parameters.getTargetProperties());
computeService.destroyNode( machineId );
computeService.getContext().close();
}
/* (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 {
ComputeService computeService = jcloudContext( parameters.getTargetProperties());
NodeMetadata metadata = computeService.getNodeMetadata( machineId );
String result = null;
if( metadata != null
&& metadata.getPublicAddresses() != null
&& ! metadata.getPublicAddresses().isEmpty())
result = metadata.getPublicAddresses().iterator().next();
return result;
}
/**
* Creates a JCloud context.
* @param targetProperties the target properties
* @return a non-null object
* @throws TargetException if the target properties are invalid
*/
ComputeService jcloudContext( Map<String,String> targetProperties ) throws TargetException {
validate( targetProperties );
ComputeServiceContext context = ContextBuilder
.newBuilder( targetProperties.get( PROVIDER_ID ))
.endpoint( targetProperties.get( ENDPOINT ))
.credentials( targetProperties.get( IDENTITY ), targetProperties.get( CREDENTIAL ))
.buildView( ComputeServiceContext.class );
return context.getComputeService();
}
/**
* Validates the 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( PROVIDER_ID, targetProperties );
checkProperty( ENDPOINT, targetProperties );
checkProperty( IMAGE_NAME, targetProperties );
checkProperty( SECURITY_GROUP, targetProperties );
checkProperty( IDENTITY, targetProperties );
checkProperty( CREDENTIAL, targetProperties );
checkProperty( HARDWARE_NAME, targetProperties );
}
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." );
}
}