/**
* Copyright 2013-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.azure.internal;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Map;
import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
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.TargetException;
import net.roboconf.target.api.TargetHandler;
import net.roboconf.target.api.TargetHandlerParameters;
/**
* @author Linh-Manh Pham - LIG
*/
public class AzureIaasHandler implements TargetHandler {
public static final String TARGET_ID = "iaas-azure";
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 {
String instanceId;
try {
// 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());
final AzureProperties azureProperties = buildProperties( parameters.getTargetProperties());
// The following part enables to transmit data to the VM.
// When the VM is up, it will be able to read this data.
// TODO: Azure does not allow a VM name with spaces whereas graph configuration of Roboconf supports it. It conflicts.
// channelName = channelName.replaceAll("\\s+","-").toLowerCase();
String userData = DataHelpers.writeUserDataAsString(
parameters.getMessagingProperties(),
parameters.getDomain(),
parameters.getApplicationName(),
rootInstanceName );
String encodedUserData = new String( Base64.encodeBase64( userData.getBytes( "UTF-8" )), "UTF-8" );
replaceValueOfTagInXMLFile(azureProperties.getCreateCloudServiceTemplate(), "ServiceName", rootInstanceName );
replaceValueOfTagInXMLFile(azureProperties.getCreateCloudServiceTemplate(), "Location", azureProperties.getLocation());
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "CustomData", encodedUserData );
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "Name", rootInstanceName );
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "HostName", rootInstanceName );
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "RoleName", rootInstanceName );
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "RoleSize", azureProperties.getVMSize());
replaceValueOfTagInXMLFile(azureProperties.getCreateDeploymentTemplate(), "SourceImageName", azureProperties.getVMTemplate());
// Let send the request to Azure API to create a Cloud Service and a Deployment (a PersistentVMRole)
String baseURL = String.format("https://management.core.windows.net/%s/services", azureProperties.getSubscriptionId());
String requestHeaderContentType = "application/xml";
byte[] requestBodyCreateCloudService = convertFileToByte(azureProperties.getCreateCloudServiceTemplate());
byte[] requestBodyCreateDeployment = convertFileToByte(azureProperties.getCreateDeploymentTemplate());
String checkCloudServiceURL = baseURL+"/hostedservices/operations/isavailable/"+rootInstanceName;
String createCloudServiceURL = baseURL+"/hostedservices";
String createDeploymentURL = baseURL+"/hostedservices/"+rootInstanceName+"/deployments";
// Check if Cloud Service exist
String responseCheckCloudService = processGetRequest(
new URL(checkCloudServiceURL),
azureProperties.getKeyStoreFile(),
azureProperties.getKeyStorePassword());
boolean checkResult = getExistResutlFromXML(responseCheckCloudService, "Result");
// true means the name is still available
this.logger.info( "Response Result: Cloud Service Name is still available: " + checkResult);
// Create Cloud Service, Deployment & Add a Role (Linux VM), maybe add a second Role (another Linux VM)
int rescodeCreateCloudService = -1;
if (checkResult) {
rescodeCreateCloudService = processPostRequest(
new URL(createCloudServiceURL),
requestBodyCreateCloudService,
requestHeaderContentType,
azureProperties.getKeyStoreFile(),
azureProperties.getKeyStorePassword()); // rescode shoud be 201
}
this.logger.info( "Create Cloud Service: Response Code: " + rescodeCreateCloudService);
this.logger.info( "Creating Azure VM in progress: " + rootInstanceName);
if (rescodeCreateCloudService == 201) {
int rescodeCreateDeployment = processPostRequest(
new URL(createDeploymentURL),
requestBodyCreateDeployment,
requestHeaderContentType,
azureProperties.getKeyStoreFile(),
azureProperties.getKeyStorePassword()); // rescode shoud be 202
this.logger.info( "Create VM: Response Code: " + rescodeCreateDeployment);
}
instanceId = rootInstanceName;
// instanceID in this context should be rootInstanceName
} catch( Exception e ) {
throw new TargetException( e );
}
return instanceId;
}
/*
* (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 {
this.logger.fine( "Configuring machine '" + machineId + "': nothing to configure." );
}
/*
* (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 {
// TODO See #230
return false;
}
/*
* (non-Javadoc)
* @see net.roboconf.target.api.TargetHandler
* #terminateMachine(net.roboconf.target.api.TargetHandlerParameters, java.lang.String)
*/
@Override
public void terminateMachine( TargetHandlerParameters parameters, String instanceId ) throws TargetException {
// instanceID is CloudServiceName
try {
final AzureProperties azureProperties = buildProperties( parameters.getTargetProperties());
String baseURL = String.format("https://management.core.windows.net/%s/services", azureProperties.getSubscriptionId());
String deleteCloudServiceURL = baseURL+"/hostedservices/"+instanceId+"?comp=media";
// Delete Cloud Service, and also delete all the related things
int rescodeDeleteCloudService = processDeleteRequest(
new URL(deleteCloudServiceURL),
azureProperties.getKeyStoreFile(),
azureProperties.getKeyStorePassword()); // rescode shoud be 202
this.logger.info("Response Code: Delete VM: " + rescodeDeleteCloudService);
} catch( Exception 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 {
// Most likely feasible but not implemented for the moment
return null;
}
/**
* Validates the received properties and builds a Java bean from them.
* @param targetProperties the target properties
* @return a non-null bean
* @throws TargetException if properties are invalid
*/
static AzureProperties buildProperties( Map<String,String> targetProperties ) throws TargetException {
String[] properties = {
AzureConstants.AZURE_SUBSCRIPTION_ID,
AzureConstants.AZURE_KEY_STORE_FILE,
AzureConstants.AZURE_KEY_STORE_PASSWORD,
AzureConstants.AZURE_CREATE_CLOUD_SERVICE_TEMPLATE,
AzureConstants.AZURE_CREATE_DEPLOYMENT_TEMPLATE,
AzureConstants.AZURE_LOCATION,
AzureConstants.AZURE_VM_SIZE,
AzureConstants.AZURE_VM_TEMPLATE
};
for( String property : properties ) {
if( Utils.isEmptyOrWhitespaces( targetProperties.get( property )))
throw new TargetException( "The value for " + property + " cannot be null or empty." );
}
// Create a bean
AzureProperties azureProperties = new AzureProperties();
String s = targetProperties.get( AzureConstants.AZURE_SUBSCRIPTION_ID );
azureProperties.setSubscriptionId( s.trim());
s = targetProperties.get( AzureConstants.AZURE_KEY_STORE_FILE );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_KEY_STORE_PASSWORD );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_CREATE_CLOUD_SERVICE_TEMPLATE );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_CREATE_DEPLOYMENT_TEMPLATE );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_LOCATION );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_VM_SIZE );
azureProperties.setKeyStoreFile( s.trim());
s = targetProperties.get( AzureConstants.AZURE_VM_TEMPLATE );
azureProperties.setKeyStoreFile( s.trim());
return azureProperties;
}
private KeyStore getKeyStore(String keyStoreName, String password) throws IOException {
KeyStore ks = null;
FileInputStream fis = null;
try {
ks = KeyStore.getInstance("JKS");
char[] passwordArray = password.toCharArray();
fis = new FileInputStream(keyStoreName);
ks.load(fis, passwordArray);
} catch (Exception e) {
this.logger.severe( e.getMessage());
Utils.logException( this.logger, e );
} finally {
Utils.closeQuietly( fis );
}
return ks;
}
private SSLSocketFactory getSSLSocketFactory( String keyStoreName, String password )
throws GeneralSecurityException, IOException {
KeyStore ks = this.getKeyStore(keyStoreName, password);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(ks, password.toCharArray());
SSLContext context = SSLContext.getInstance("TLS");
context.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
return context.getSocketFactory();
}
private static boolean getExistResutlFromXML(String xmlStr, String nameOfNode)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
DocumentBuilder b;
b = f.newDocumentBuilder();
Document doc;
doc = b.parse(new ByteArrayInputStream(xmlStr.getBytes("UTF-8")));
NodeList nodes = doc.getElementsByTagName(nameOfNode);
String result = "false";
for (int i = 0; i < nodes.getLength(); i++) {
Element node = (Element) nodes.item(i);
result = node.getTextContent();
}
return Boolean.parseBoolean( result );
}
private String processGetRequest(URL url, String keyStore, String keyStorePassword)
throws GeneralSecurityException, IOException {
SSLSocketFactory sslFactory = this.getSSLSocketFactory(keyStore, keyStorePassword);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(sslFactory);
con.setRequestMethod("GET");
con.addRequestProperty("x-ms-version", "2014-04-01");
InputStream responseStream = (InputStream) con.getContent();
ByteArrayOutputStream os = new ByteArrayOutputStream();
Utils.copyStreamSafely( responseStream, os );
return os.toString( "UTF-8" );
}
private int processPostRequest(URL url, byte[] data, String contentType, String keyStore, String keyStorePassword)
throws GeneralSecurityException, IOException {
SSLSocketFactory sslFactory = this.getSSLSocketFactory(keyStore, keyStorePassword);
HttpsURLConnection con;
con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(sslFactory);
con.setDoOutput(true);
con.setRequestMethod("POST");
con.addRequestProperty("x-ms-version", "2014-04-01");
con.setRequestProperty("Content-Length", String.valueOf(data.length));
con.setRequestProperty("Content-Type", contentType);
DataOutputStream requestStream = new DataOutputStream( con.getOutputStream());
requestStream.write(data);
requestStream.flush();
requestStream.close();
return con.getResponseCode();
}
private int processDeleteRequest(URL url, String keyStore, String keyStorePassword)
throws GeneralSecurityException, IOException {
SSLSocketFactory sslFactory = this.getSSLSocketFactory(keyStore, keyStorePassword);
HttpsURLConnection con;
con = (HttpsURLConnection) url.openConnection();
con.setSSLSocketFactory(sslFactory);
con.setRequestMethod("DELETE");
con.addRequestProperty("x-ms-version", "2014-04-01");
return con.getResponseCode();
}
private byte[] convertFileToByte(String xmlFilePath) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
Utils.copyStream ( new File(xmlFilePath), os );
} catch( IOException e ) {
this.logger.severe( e.getMessage());
}
return os.toByteArray();
}
private void replaceValueOfTagInXMLFile(String filePath, String tagName, String replacingValue)
throws ParserConfigurationException, SAXException, IOException {
File fXmlFile = new File(filePath);
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(fXmlFile);
//optional, but recommended
//read this - http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
doc.getDocumentElement().normalize();
NodeList nList = doc.getElementsByTagName(tagName);
Node nNode = nList.item(0);
nNode.setTextContent(replacingValue);
// write the modified content into xml file
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer;
try {
transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(new File(filePath));
transformer.transform(source, result);
} catch (TransformerException e) {
this.logger.severe( e.getMessage());
}
}
}