/* * Copyright 2010 Cloud.com, Inc. * * 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 com.cloud.bridge.service.core.ec2; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import com.cloud.bridge.service.UserContext; import com.cloud.bridge.service.core.ec2.EC2DescribeImages; import com.cloud.bridge.service.core.ec2.EC2DescribeImagesResponse; import com.cloud.bridge.service.core.ec2.EC2DescribeInstances; import com.cloud.bridge.service.core.ec2.EC2DescribeInstancesResponse; import com.cloud.bridge.service.core.ec2.EC2Image; import com.cloud.bridge.service.core.ec2.EC2Instance; import com.cloud.bridge.service.core.ec2.EC2RebootInstances; import com.cloud.bridge.service.core.ec2.EC2StartInstances; import com.cloud.bridge.service.core.ec2.EC2StartInstancesResponse; import com.cloud.bridge.service.core.ec2.EC2StopInstances; import com.cloud.bridge.service.core.ec2.EC2StopInstancesResponse; import com.cloud.bridge.service.exception.EC2ServiceException; import com.cloud.bridge.service.exception.InternalErrorException; import com.cloud.bridge.util.ConfigurationHelper; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.*; import java.security.SignatureException; import java.text.ParseException; import java.util.Properties; import java.util.UUID; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; public class EC2Engine { protected final static Logger logger = Logger.getLogger(EC2Engine.class); private DocumentBuilderFactory dbf = null; private String managementServer = null; private String cloudAPIPort = null; private OfferingBundle M1Small = null; private OfferingBundle M1Large = null; private OfferingBundle M1Xlarge = null; private OfferingBundle C1Medium = null; private OfferingBundle C1Xlarge = null; private OfferingBundle M2Xlarge = null; private OfferingBundle M22Xlarge = null; private OfferingBundle M24Xlarge = null; private OfferingBundle CC14xlarge = null; // -> in milliseconds, time interval to wait before asynch check on a jobId's status private int pollInterval1 = 100; // for: createTemplate private int pollInterval2 = 100; // for: deployVirtualMachine private int pollInterval3 = 100; // for: createVolume private int pollInterval4 = 1000; // for: createSnapshot private int pollInterval5 = 100; // for: deleteSnapshot, deleteTemplate, deleteVolume, attachVolume, detachVolume private int pollInterval6 = 100; // for: startVirtualMachine, destroyVirtualMachine, stopVirtualMachine private int CLOUD_STACK_VERSION_2_0 = 200; private int CLOUD_STACK_VERSION_2_1 = 210; private int cloudStackVersion; public EC2Engine() { dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware( true ); try { loadConfigValues(); } catch( Exception e ) {} } /** * Which management server to we talk to? * Load a mapping form Amazon values for 'instanceType' to cloud defined * diskOfferingId and serviceOfferingId. */ private void loadConfigValues() throws IOException { File propertiesFile = ConfigurationHelper.findConfigurationFile("ec2-service.properties"); if (null != propertiesFile) { logger.info("Use EC2 properties file: " + propertiesFile.getAbsolutePath()); Properties EC2Prop = new Properties(); try { EC2Prop.load( new FileInputStream( propertiesFile )); } catch (FileNotFoundException e) { logger.warn("Unable to open properties file: " + propertiesFile.getAbsolutePath(), e); } catch (IOException e) { logger.warn("Unable to read properties file: " + propertiesFile.getAbsolutePath(), e); } managementServer = EC2Prop.getProperty( "managementServer" ); cloudAPIPort = EC2Prop.getProperty( "cloudAPIPort", null ); try { pollInterval1 = Integer.parseInt( EC2Prop.getProperty( "pollInterval1", "100" )); pollInterval2 = Integer.parseInt( EC2Prop.getProperty( "pollInterval2", "100" )); pollInterval3 = Integer.parseInt( EC2Prop.getProperty( "pollInterval3", "100" )); pollInterval4 = Integer.parseInt( EC2Prop.getProperty( "pollInterval4", "1000" )); pollInterval5 = Integer.parseInt( EC2Prop.getProperty( "pollInterval5", "100" )); pollInterval6 = Integer.parseInt( EC2Prop.getProperty( "pollInterval6", "100" )); } catch( Exception e ) { logger.warn("Invalid polling interval: " + e.toString() + " using default values"); } M1Small = new OfferingBundle(); M1Small.setDiskOfferingId( EC2Prop.getProperty( "m1.small.diskId", null )); M1Small.setServiceOfferingId( EC2Prop.getProperty( "m1.small.serviceId", null )); M1Large = new OfferingBundle(); M1Large.setDiskOfferingId( EC2Prop.getProperty( "m1.large.diskId", null )); M1Large.setServiceOfferingId( EC2Prop.getProperty( "m1.large.serviceId", null )); M1Xlarge = new OfferingBundle(); M1Xlarge.setDiskOfferingId( EC2Prop.getProperty( "m1.xlarge.diskId", null )); M1Xlarge.setServiceOfferingId( EC2Prop.getProperty( "m1.xlarge.serviceId", null )); C1Medium = new OfferingBundle(); C1Medium.setDiskOfferingId( EC2Prop.getProperty( "c1.medium.diskId", null )); C1Medium.setServiceOfferingId( EC2Prop.getProperty( "c1.medium.serviceId", null )); C1Xlarge = new OfferingBundle(); C1Xlarge.setDiskOfferingId( EC2Prop.getProperty( "c1.xlarge.diskId", null )); C1Xlarge.setServiceOfferingId( EC2Prop.getProperty( "c1.xlarge.serviceId", null )); M2Xlarge = new OfferingBundle(); M2Xlarge.setDiskOfferingId( EC2Prop.getProperty( "m2.xlarge.diskId", null )); M2Xlarge.setServiceOfferingId( EC2Prop.getProperty( "m2.xlarge.serviceId", null )); M22Xlarge = new OfferingBundle(); M22Xlarge.setDiskOfferingId( EC2Prop.getProperty( "m2.2xlarge.diskId", null )); M22Xlarge.setServiceOfferingId( EC2Prop.getProperty( "m2.2xlarge.serviceId", null )); M24Xlarge = new OfferingBundle(); M24Xlarge.setDiskOfferingId( EC2Prop.getProperty( "m2.4xlarge.diskId", null )); M24Xlarge.setServiceOfferingId( EC2Prop.getProperty( "m2.4xlarge.serviceId", null )); CC14xlarge = new OfferingBundle(); CC14xlarge.setDiskOfferingId( EC2Prop.getProperty( "cc1.4xlarge.diskId", null )); CC14xlarge.setServiceOfferingId( EC2Prop.getProperty( "cc1.4xlarge.serviceId", null )); cloudStackVersion = getCloudStackVersion(EC2Prop); } else logger.error( "ec2-service.properties not found" ); } private int getCloudStackVersion(Properties prop) { String versionProp = prop.getProperty( "cloudstackVersion", null ); if(versionProp!=null){ if(versionProp.equals("2.0")){ return CLOUD_STACK_VERSION_2_0; } else if(versionProp.equals("2.0.0")){ return CLOUD_STACK_VERSION_2_0; } else if(versionProp.equals("2.1.0")){ return CLOUD_STACK_VERSION_2_1; } else if(versionProp.equals("2.1.0")){ return CLOUD_STACK_VERSION_2_1; } } return CLOUD_STACK_VERSION_2_1; } /** * We have to generate the URL explicity here because we are using the given inputs as the * Cloud API keys. Make sure the account represented by the two parameters exists by performing * a harmless operation. If the parameters are bogus then the signature generated will be * wrong and the Cloud.com's Management server will send us a "401 unauthorized" failure * (which results in an exception being thrown). * * @param accessKey - Cloud.com's API access key * @param secretKey - Cloud.com's API secret key * @return true if the account exists, false otherwise */ public boolean validateAccount( String accessKey, String secretKey ) { try { StringBuffer sigOver = new StringBuffer(); sigOver.append( "apikey=" ).append( safeURLencode( accessKey )); sigOver.append( "&" ).append( "command=listAccounts" ); String signature = calculateRFC2104HMAC( sigOver.toString().toLowerCase(), secretKey ); StringBuffer apiCommand = new StringBuffer(); apiCommand.append( getServerURL()); apiCommand.append( "command=listAccounts" ); apiCommand.append( "&apikey=" ).append( safeURLencode( accessKey )); apiCommand.append( "&signature=" ).append( safeURLencode( signature )); resolveURL( apiCommand.toString(), "listAccounts", true ); return true; } catch( EC2ServiceException error ) { logger.error( "validateAccount - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "validateAccount - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * For several API calls we have to return the owner ID. For that we simply * call "listAccounts" with the callers Cloud keys and use the first match. For * most users this will work since they will only be able to access their own * account. * * @return the unique account name matching the currently used Cloud API access & secret keys * or an empty string is returned (which is required in the XML responses). */ public String getAccountName() { try { String query = new String( "command=listAccounts" ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listAccounts", true ); NodeList match = cloudResp.getElementsByTagName( "name" ); if ( 0 < match.getLength()) { Node item = match.item(0); return new String( item.getFirstChild().getNodeValue()); } else return ""; } catch( EC2ServiceException error ) { logger.error( "getAccountName - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "getAccountName - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Remember to pre-sort the parameters in alphabetical order. * * @param request * @return was the security group created? */ public boolean createSecurityGroup(EC2SecurityGroup request) { if (null == request.getDescription() || null == request.getName()) throw new EC2ServiceException( "Both name & description are required", 400 ); try { String query = new String( "command=createNetworkGroup" + "&description=" + safeURLencode( request.getDescription()) + "&name=" + safeURLencode( request.getName())); resolveURL( genAPIURL( query, genQuerySignature( query )), "createNetworkGroup", true ); return true; } catch( EC2ServiceException error ) { logger.error( "EC2 CreateSecurityGroup - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 CreateSecurityGroup - " + e.toString()); throw new InternalErrorException( e.toString()); } } public boolean deleteSecurityGroup(EC2SecurityGroup request) { if (null == request.getName()) throw new EC2ServiceException( "Name is a required parameter", 400 ); try { String query = new String( "command=deleteNetworkGroup&name=" + safeURLencode( request.getName())); resolveURL( genAPIURL( query, genQuerySignature( query )), "deleteNetworkGroup", true ); return true; } catch( EC2ServiceException error ) { logger.error( "EC2 DeleteSecurityGroup - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DeleteSecurityGroup - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2DescribeSecurityGroupsResponse handleRequest(EC2DescribeSecurityGroups request) { try { return listSecurityGroups( request.getGroupSet()); } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeSecurityGroups - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeSecurityGroups - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * The Cload Stack API only handles one item out of the EC2 request at a time. * Place authorizw and revoke into one function since it appears that they are practically identical. * Both authorizeNetworkGroupIngress and revokeNetworkGroupIngress are asynchonrous * * @param request - ip permission parameters * @param command - { authorizeNetworkGroupIngress | revokeNetworkGroupIngress } */ public boolean securityGroupRequest(EC2AuthorizeRevokeSecurityGroup request, String command ) { if (null == request.getName()) throw new EC2ServiceException( "Name is a required parameter", 400 ); StringBuffer url = new StringBuffer(); // -> used to derive URL for Cloud Stack StringBuffer sorted = new StringBuffer(); // -> used for signature generation StringBuffer params = new StringBuffer(); // -> used to construct common set of parameters EC2IpPermission[] items = request.getIpPermissionSet(); try { for( int i=0; i < items.length; i++ ) { // -> cidrList is before command for the sorted version used for signature generation String cidrList = constructCIDRList( items[i].getIpRangeSet()); url.append( "command=" + command ); if ( null != cidrList) { url.append( "&" + cidrList ); // &cidrList sorted.append( cidrList ); // cidrList before command sorted.append( "&command=" + command ); } else sorted.append( "command=" + command ); String protocol = items[i].getProtocol(); if ( protocol.equalsIgnoreCase( "icmp" )) { params.append( "&icmpCode=" + items[i].getToPort()); params.append( "&icmpType=" + items[i].getFromPort()); params.append( "&networkGroupName=" + safeURLencode( request.getName())); params.append( "&protocol=" + safeURLencode( protocol )); } else { params.append( "&endPort=" + items[i].getToPort()); params.append( "&networkGroupName=" + safeURLencode( request.getName())); params.append( "&protocol=" + safeURLencode( protocol )); params.append( "&startPort=" + items[i].getFromPort()); } // -> sorted is: cidrList=&command=... while url is: command=&cidrList=... String netParams = params.toString(); url.append( netParams ); sorted.append( netParams ); String userList = constructNetworkUserList( items[i].getUserSet()); if (null != userList) { url.append( userList ); sorted.append( userList ); } Document cloudResp = resolveURL( genAPIURL( url.toString(), genQuerySignature( sorted.toString())), command, true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (!waitForAsynch( jobId )) throw new EC2ServiceException( command + " failed" ); } else throw new InternalErrorException( "InternalError" ); url.setLength( 0 ); sorted.setLength( 0 ); params.setLength( 0 ); } return true; } catch( EC2ServiceException error ) { logger.error( "EC2 " + command + "- " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 " + command + " - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Cloud Stack API takes a comma separated list of IP ranges as one parameter. * * @throws UnsupportedEncodingException */ private String constructCIDRList( String[] ipRanges ) throws UnsupportedEncodingException { if (null == ipRanges || 0 == ipRanges.length) return null; StringBuffer cidrList = new StringBuffer(); for( int i=0; i < ipRanges.length; i++ ) { if (0 < i) cidrList.append( "," ); cidrList.append( ipRanges[i] ); } String temp = safeURLencode( cidrList.toString()); return new String( "cidrList=" + temp ); } /** * The Cloud Stack API uses array notation to pass along a set of these values. * * @throws UnsupportedEncodingException */ private String constructNetworkUserList( EC2SecurityGroup[] groups ) throws UnsupportedEncodingException { if (null == groups || 0 == groups.length) return null; StringBuffer userList = new StringBuffer(); for( int i=0; i < groups.length; i++ ) { userList.append( "&userNetworkGroupList["+i+"].account=" + safeURLencode( groups[i].getAccount())); userList.append( "&userNetworkGroupList["+i+"].group=" + safeURLencode( groups[i].getName())); } return userList.toString(); } public EC2DescribeSnapshotsResponse handleRequest(EC2DescribeSnapshots request) { EC2DescribeVolumesResponse volumes = new EC2DescribeVolumesResponse(); try { // -> query to get the volume size for each snapshot EC2DescribeSnapshotsResponse response = listSnapshots( request.getSnapshotSet()); EC2Snapshot[] snapshots = response.getSnapshotSet(); for( int i=0; i < snapshots.length; i++ ) { volumes = listVolumes( snapshots[i].getVolumeId(), null, volumes ); EC2Volume[] volSet = volumes.getVolumeSet(); if (0 < volSet.length) snapshots[i].setVolumeSize( volSet[0].getSize()); volumes.reset(); } return response; } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeSnapshots - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeSnapshots - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2Snapshot createSnapshot( String volumeId ) { EC2DescribeVolumesResponse volumes = new EC2DescribeVolumesResponse(); try { String query = new String( "command=createSnapshot&volumeId=" + volumeId ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "createSnapshot", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); EC2Snapshot shot = waitForSnapshot( jobId ); shot.setVolumeId( volumeId ); volumes = listVolumes( volumeId, null, volumes ); EC2Volume[] volSet = volumes.getVolumeSet(); if (0 < volSet.length) shot.setVolumeSize( volSet[0].getSize()); return shot; } else throw new InternalErrorException( "InternalError" ); } catch( EC2ServiceException error ) { logger.error( "EC2 CreateSnapshot - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 CreateSnapshot - " + e.toString()); throw new InternalErrorException( e.toString()); } } public boolean deleteSnapshot( String snapshotId ) { try { String query = new String( "command=deleteSnapshot&id=" + snapshotId ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "deleteSnapshot", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (waitForAsynch( jobId )) return true; } else throw new InternalErrorException( "InternalError" ); return false; } catch( EC2ServiceException error ) { logger.error( "EC2 DeleteSnapshot - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DeleteSnapshot - " + e.toString()); throw new InternalErrorException( e.toString()); } } public boolean modifyImageAttribute( EC2Image request ) { EC2DescribeImagesResponse images = new EC2DescribeImagesResponse(); try { images = listTemplates( request.getId(), images ); EC2Image[] imageSet = images.getImageSet(); String query = new String( "command=updateTemplate" + "&displayText=" + safeURLencode( request.getDescription()) + "&id=" + request.getId() + "&name=" + safeURLencode( imageSet[0].getName())); resolveURL( genAPIURL( query, genQuerySignature( query )), "updateTemplate", true ); return true; } catch( EC2ServiceException error ) { logger.error( "EC2 ModifyImage - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 ModifyImage - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * We only support the imageSet version of this call or when no search parameters are passed * which results in asking for all templates. */ public EC2DescribeImagesResponse handleRequest(EC2DescribeImages request) { EC2DescribeImagesResponse images = new EC2DescribeImagesResponse(); try { String[] templateIds = request.getImageSet(); if ( 0 == templateIds.length ) { return listTemplates( null, images ); } for( int i=0; i < templateIds.length; i++ ) { images = listTemplates( templateIds[i], images ); } return images; } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeImages - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeImages - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Amazon API just gives us the instanceId to create the template from. * But our createTemplate function requires the volumeId and osTypeId. * So to get that we must make the following sequence of cloud API calls: * 1) listVolumes&virtualMachineId= -- gets the volumeId * 2) listVirtualMachinees&id= -- gets the templateId * 3) listTemplates&id= -- gets the osTypeId * * If we have to start and stop the VM in question then this function is * going to take a long time to complete. */ public EC2CreateImageResponse handleRequest(EC2CreateImage request) { EC2CreateImageResponse response = null; boolean needsRestart = false; String volumeId = null; try { // [A] Creating a template from a VM volume should be from the ROOT volume // Also for this to work the VM must be in a Stopped state so we 'reboot' it if its not EC2DescribeVolumesResponse volumes = new EC2DescribeVolumesResponse(); volumes = listVolumes( null, request.getInstanceId(), volumes ); EC2Volume[] volSet = volumes.getVolumeSet(); for( int i=0; i < volSet.length; i++ ) { String volType = volSet[i].getType(); if (volType.equalsIgnoreCase( "ROOT" )) { String vmState = volSet[i].getVMState(); if (vmState.equalsIgnoreCase( "running" ) || vmState.equalsIgnoreCase( "starting" )) { needsRestart = true; if (!stopVirtualMachine( request.getInstanceId())) throw new EC2ServiceException( "CreateImage - instance must be in a stopped state", 400 ); } volumeId = volSet[i].getId(); break; } } // [B] The parameters must be in sorted order for proper signature generation EC2DescribeInstancesResponse instances = new EC2DescribeInstancesResponse(); instances = lookupInstances( request.getInstanceId(), instances ); EC2Instance[] instanceSet = instances.getInstanceSet(); String templateId = instanceSet[0].getTemplateId(); EC2DescribeImagesResponse images = new EC2DescribeImagesResponse(); images = listTemplates( templateId, images ); EC2Image[] imageSet = images.getImageSet(); String osTypeId = imageSet[0].getOsTypeId(); String description = request.getDescription(); String query = new String( "command=createTemplate" + "&displayText=" + (null == description ? "" : safeURLencode( description )) + "&name=" + safeURLencode( request.getName()) + "&osTypeId=" + osTypeId + "&volumeId=" + volumeId ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "createTemplate", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); response = waitForTemplate( jobId ); } else throw new InternalErrorException( "InternalError" ); // [C] If we stopped the virtual machine now we need to restart it if (needsRestart) { if (!startVirtualMachine( request.getInstanceId())) throw new EC2ServiceException( "CreateImage - restarting instance " + request.getInstanceId() + " failed", 400 ); } return response; } catch( EC2ServiceException error ) { logger.error( "EC2 CreateImage - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 CreateImage - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * EC2CreateImageResponse has the same content that the class EC2RegisterImageResponse * would have. */ public EC2CreateImageResponse handleRequest(EC2RegisterImage request) { try { if (null == request.getFormat() || null == request.getName() || null == request.getOsTypeName() || null == request.getLocation() || null == request.getZoneName()) throw new EC2ServiceException( "Missing parameter - location/architecture/name", 400 ); // -> the parameters must be in sorted order for proper signature generation String query = new String( "command=registerTemplate" + "&displayText=" + (null == request.getDescription() ? safeURLencode( request.getName()) : safeURLencode( request.getDescription())) + "&format=" + safeURLencode( request.getFormat()) + "&name=" + safeURLencode( request.getName()) + "&osTypeId=" + safeURLencode( toOSTypeId( request.getOsTypeName())) + "&url=" + safeURLencode( request.getLocation()) + "&zoneId=" + toZoneId( request.getZoneName())); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "registerTemplate", true ); EC2CreateImageResponse image = new EC2CreateImageResponse(); NodeList match = cloudResp.getElementsByTagName( "id" ); if ( 0 < match.getLength()) { Node item = match.item(0); image.setId( item.getFirstChild().getNodeValue()); } return image; } catch( EC2ServiceException error ) { logger.error( "EC2 RegisterImage - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 RegisterImage - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Our implementation is different from Amazon in that we do delete the template * when we deregister it. The cloud API has not deregister call. */ public boolean deregisterImage( EC2Image image ) { try { String query = new String( "command=deleteTemplate&id=" + image.getId()); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "deleteTemplate", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (waitForAsynch( jobId )) return true; } else throw new InternalErrorException( "InternalError" ); return false; } catch( EC2ServiceException error ) { logger.error( "EC2 DeregisterImage - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DeregisterImage - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2DescribeInstancesResponse handleRequest(EC2DescribeInstances request) { try { return listVirtualMachines( request.getInstancesSet()); } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeInstances - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeInstances - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2DescribeAvailabilityZonesResponse handleRequest(EC2DescribeAvailabilityZones request) { try { return listZones( request.getZoneSet()); } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeAvailabilityZones - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeAvailabilityZones - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2DescribeVolumesResponse handleRequest(EC2DescribeVolumes request) { EC2DescribeVolumesResponse volumes = new EC2DescribeVolumesResponse(); try { String[] volumeIds = request.getVolumeSet(); if ( 0 == volumeIds.length ) { return listVolumes( null, null, volumes ); } for( int i=0; i < volumeIds.length; i++ ) { volumes = listVolumes( volumeIds[i], null, volumes ); } return volumes; } catch( EC2ServiceException error ) { logger.error( "EC2 DescribeVolumes - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DescribeVolumes - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2Volume attachVolume(EC2Volume request) { try { request = mapDeviceToCloudDeviceId( request ); StringBuffer params = new StringBuffer(); params.append( "command=attachVolume" ); params.append( "&deviceId=" + request.getDeviceId()); params.append( "&id=" + request.getId()); params.append( "&virtualMachineId=" + request.getInstanceId()); String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "attachVolume", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (waitForAsynch( jobId )) request.setStatus( "attached" ); } else throw new InternalErrorException( "InternalError" ); return request; } catch( EC2ServiceException error ) { logger.error( "EC2 AttachVolume - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 AttachVolume - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2Volume detachVolume(EC2Volume request) { try { StringBuffer params = new StringBuffer(); params.append( "command=detachVolume" ); params.append( "&id=" + request.getId()); String query = params.toString(); // -> do first cause afterwards the volume has not volume associated with it // -> if instanceId is not given on the request then we need to look it up for the response if (null == request.getInstanceId()) { EC2DescribeVolumesResponse volumes = new EC2DescribeVolumesResponse(); volumes = listVolumes( request.getId(), null, volumes ); EC2Volume[] volSet = volumes.getVolumeSet(); if (0 < volSet.length) request.setInstanceId( volSet[0].getInstanceId()); } Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "detachVolume", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (waitForAsynch( jobId )) request.setStatus( "detached" ); } else throw new InternalErrorException( "InternalError" ); return request; } catch( EC2ServiceException error ) { logger.error( "EC2 DetachVolume - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DetachVolume - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2Volume handleRequest(EC2CreateVolume request) { String offerId = null; String snapshotId = null; try { if (null == (snapshotId = request.getSnapshotId())) { // -> match up the required volume size with the existing disk offerings DiskOfferings offerings = listDiskOfferings(); DiskOffer[] availDisks = offerings.getOfferSet(); for( int i=0; i < availDisks.length; i++ ) { // -> both values here are in GBs, (for now we just take the first that fits) if (availDisks[i].getSize() >= request.getSize()) { offerId = availDisks[i].getId(); break; } } if (null == offerId) throw new EC2ServiceException( "NoMatchingDiskOffering", 400 ); } // -> no volume name is given in the Amazon request but is required in the cloud API String volName = safeURLencode( UUID.randomUUID().toString()); StringBuffer params = new StringBuffer(); params.append( "command=createVolume" ); if (null != offerId) params.append( "&diskOfferingId=" + offerId ); params.append( "&name=" + volName ); if (null != snapshotId) params.append( "&snapshotId=" + snapshotId ); params.append( "&zoneId=" + toZoneId( request.getZoneName())); String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "createVolume", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); return waitForVolume( jobId ); } else throw new InternalErrorException( "InternalError" ); } catch( EC2ServiceException error ) { logger.error( "EC2 CreateVolume - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 CreateVolume - " + e.toString()); throw new InternalErrorException( e.toString()); } } public EC2Volume deleteVolume(EC2Volume request) { try { StringBuffer params = new StringBuffer(); params.append( "command=deleteVolume" ); params.append( "&id=" + request.getId()); String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "deleteVolume", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); String jobId = new String( item.getFirstChild().getNodeValue()); if (waitForAsynch( jobId )) request.setStatus( "deleted" ); } else throw new InternalErrorException( "InternalError" ); return request; } catch( EC2ServiceException error ) { logger.error( "EC2 DeleteVolume - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 DeleteVolume - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Note that more than one VM can be requested rebooted at once. * The Amazon API does not wait around for the result of the operation. */ public boolean handleRequest(EC2RebootInstances request) { EC2Instance[] vms = null; // -> reboot is not allowed on destroyed (i.e., terminated) instances try { EC2DescribeInstancesResponse previousState = listVirtualMachines( request.getInstancesSet()); vms = previousState.getInstanceSet(); // -> send reboot requests for each item for( int i=0; i < vms.length; i++ ) { if (vms[i].getState().equalsIgnoreCase( "Destroyed" )) continue; String query = new String( "command=rebootVirtualMachine&id=" + vms[i].getId()); resolveURL( genAPIURL( query, genQuerySignature( query )), "rebootVirtualMachine", true ); } return true; } catch( EC2ServiceException error ) { logger.error( "EC2 RebootInstances - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 RebootInstances - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * The Amazon API allows one or multiple VMs to be started with a single request. * To do this efficiently we first make all the asynch deployVirtualMachine requests * required before we start waiting for any to complete. We save all the jobId values * returned from each request in an array. Once all the deployVirtualMachine requests * have been made then we enter a polling state waiting for all the asynch requests to * complete. */ public EC2RunInstancesResponse handleRequest(EC2RunInstances request) { EC2RunInstancesResponse instances = new EC2RunInstancesResponse(); NodeList match = null; Node item = null; int createInstances = 0; int canCreateInstances = -1; int waitFor = 0; int countCreated = 0; // [A] Can the user create the minimum required number of instances? try { canCreateInstances = calculateAllowedInstances(); if (-1 == canCreateInstances) canCreateInstances = request.getMaxCount(); if (canCreateInstances < request.getMinCount()) { logger.info( "EC2 RunInstances - min count too big (" + request.getMinCount() + "), " + canCreateInstances + " left to allocate"); throw new EC2ServiceException( "InstanceLimitExceeded - only " + canCreateInstances + " instance(s) left to allocate", 400 ); } if ( canCreateInstances < request.getMaxCount()) createInstances = canCreateInstances; else createInstances = request.getMaxCount(); EC2Instance[] vms = new EC2Instance[ createInstances ]; String[] jobIds = new String[ createInstances ]; // [B] Create n separate VM instances OfferingBundle offer = instanceTypeToOfferBundle( request.getInstanceType()); StringBuffer params = new StringBuffer(); params.append( "command=deployVirtualMachine" ); params.append( "&diskOfferingId=" + offer.getDiskOfferingId()); if (null != request.getGroupId()) params.append( "&group=" + request.getGroupId()); params.append( "&serviceOfferingId=" + offer.getServiceOfferingId()); params.append( "&templateId=" + request.getTemplateId()); if (null != request.getUserData()) params.append( "&userData=" + safeURLencode( request.getUserData())); // temp hack //String vlanId = hackGetVlanId(); //if (null != vlanId) params.append( "&vlanId=" + vlanId ); // temp hack params.append( "&zoneId=" + toZoneId( request.getZoneName())); String query = params.toString(); String apiCommand = genAPIURL( query, genQuerySignature( query )); for( int i=0; i < createInstances; i++ ) { Document cloudResp = resolveURL( apiCommand, "deployVirtualMachine", true ); vms[i] = new EC2Instance(); match = cloudResp.getElementsByTagName( "jobid" ); if (0 < match.getLength()) { item = match.item(0); jobIds[i] = new String( item.getFirstChild().getNodeValue()); waitFor++; } } // [C] Wait until all the requested starts have completed or failed // -> did they all succeeded? vms = waitForDeploy( vms, jobIds, waitFor ); for( int k=0; k < vms.length; k++ ) { if (null != vms[k].getId()) { // request.getInstanceType() can return null vms[k].setServiceOffering( serviceOfferingIdToInstanceType( offer.getServiceOfferingId())); instances.addInstance( vms[k] ); countCreated++; } } if (0 == countCreated) throw new EC2ServiceException( "InsufficientInstanceCapacity" ); return instances; } catch( EC2ServiceException error ) { logger.error( "EC2 RunInstances - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 RunInstances - deploy 2 " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Note that more than one VM can be requested started at once. * The Amazon API returns the previous state and the modified state as the * result of this API. */ public EC2StartInstancesResponse handleRequest(EC2StartInstances request) { EC2StartInstancesResponse instances = new EC2StartInstancesResponse(); EC2Instance[] vms = null; Node item = null; int waitFor = 0; // -> first determine the current state of each VM (becomes it previous state) try { EC2DescribeInstancesResponse previousState = listVirtualMachines( request.getInstancesSet()); vms = previousState.getInstanceSet(); String[] jobIds = new String[ vms.length ]; // -> send start requests for each item for( int i=0; i < vms.length; i++ ) { // -> if its already running then we are done vms[i].setPreviousState( vms[i].getState()); if (vms[i].getState().equalsIgnoreCase( "Running" ) || vms[i].getState().equalsIgnoreCase( "Destroyed" )) continue; String query = new String( "command=startVirtualMachine&id=" + vms[i].getId()); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "startVirtualMachine", true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if (0 < match.getLength()) { item = match.item(0); jobIds[i] = new String( item.getFirstChild().getNodeValue()); waitFor++; } } // -> wait until all the requested starts have completed or failed vms = waitForStartStop( vms, jobIds, waitFor, "running" ); for( int k=0; k < vms.length; k++ ) instances.addInstance( vms[k] ); return instances; } catch( EC2ServiceException error ) { logger.error( "EC2 StartInstances - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 StartInstances - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Note that more than one VM can be requested stopped at once. * The Amazon API returns the previous state and the modified state as the * result of this API. */ public EC2StopInstancesResponse handleRequest(EC2StopInstances request) { EC2StopInstancesResponse instances = new EC2StopInstancesResponse(); EC2Instance[] vms = null; String query = null; Node item = null; int waitFor = 0; // -> first determine the current state of each VM (becomes it previous state) try { EC2DescribeInstancesResponse previousState = listVirtualMachines( request.getInstancesSet()); vms = previousState.getInstanceSet(); String[] jobIds = new String[ vms.length ]; // -> send stop requests for each item for( int i=0; i < vms.length; i++ ) { vms[i].setPreviousState( vms[i].getState()); if ( request.getDestroyInstances()) { if (vms[i].getState().equalsIgnoreCase( "Destroyed" )) continue; } else { if (vms[i].getState().equalsIgnoreCase( "Stopped" ) || vms[i].getState().equalsIgnoreCase( "Destroyed" )) continue; } if ( request.getDestroyInstances()) query = new String( "command=destroyVirtualMachine&id=" + vms[i].getId()); else query = new String( "command=stopVirtualMachine&id=" + vms[i].getId()); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), query, true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if (0 < match.getLength()) { item = match.item(0); jobIds[i] = new String( item.getFirstChild().getNodeValue()); waitFor++; } } // -> wait until all the requested stops have completed or failed vms = waitForStartStop( vms, jobIds, waitFor, (request.getDestroyInstances() ? "destroyed" : "stopped")); for( int k=0; k < vms.length; k++ ) instances.addInstance( vms[k] ); return instances; } catch( EC2ServiceException error ) { logger.error( "EC2 StopInstances - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 StopInstances - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Wait until one specific VM has stopped */ private boolean stopVirtualMachine( String instanceId ) throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException { EC2Instance[] vms = new EC2Instance[1]; String[] jobIds = new String[1]; vms[0] = new EC2Instance(); vms[0].setState( "running" ); vms[0].setPreviousState( "running" ); String query = new String( "command=stopVirtualMachine&id=" + instanceId ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), query, true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); jobIds[0] = new String( item.getFirstChild().getNodeValue()); } else throw new EC2ServiceException( "Internal Server Error", 500 ); vms = waitForStartStop( vms, jobIds, 1, "stopped" ); return vms[0].getState().equalsIgnoreCase( "stopped" ); } private boolean startVirtualMachine( String instanceId ) throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException { EC2Instance[] vms = new EC2Instance[1]; String[] jobIds = new String[1]; vms[0] = new EC2Instance(); vms[0].setState( "stopped" ); vms[0].setPreviousState( "stopped" ); String query = new String( "command=startVirtualMachine&id=" + instanceId ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), query, true ); NodeList match = cloudResp.getElementsByTagName( "jobid" ); if ( 0 < match.getLength()) { Node item = match.item(0); jobIds[0] = new String( item.getFirstChild().getNodeValue()); } else throw new EC2ServiceException( "Internal Server Error", 500 ); vms = waitForStartStop( vms, jobIds, 1, "running" ); return vms[0].getState().equalsIgnoreCase( "running" ); } /** * RunInstances includes a min and max count of requested instances to create. * We have to be able to create the min number for the user or none at all. So * here we determine what the user has left to create. * * @return -1 means no limit exists, other positive numbers give max number left that * the user can create. */ private int calculateAllowedInstances() throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException, ParseException { if ( cloudStackVersion >= CLOUD_STACK_VERSION_2_1 ) { NodeList match = null; Node item = null; int maxAllowed = -1; // -> get the user limits on instances String query = new String( "command=listResourceLimits&resourceType=0" ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listResourceLimits", true ); match = cloudResp.getElementsByTagName( "max" ); if ( 0 < match.getLength()) { item = match.item(0); maxAllowed = Integer.parseInt( item.getFirstChild().getNodeValue()); if (-1 == maxAllowed) return -1; // no limit // -> how many instances has the user already created? EC2DescribeInstancesResponse existingVMS = listVirtualMachines( null ); EC2Instance[] vmsList = existingVMS.getInstanceSet(); return (maxAllowed - vmsList.length); } else return 0; } else return -1; } /** * createTemplate is an asynchronus call so here we poll (sleeping so we do not hammer the server) * until the operation completes. * * @param jobId - parameter returned from the createTemplate request */ private EC2CreateImageResponse waitForTemplate( String jobId ) throws EC2ServiceException, IOException, SAXException, ParserConfigurationException, SignatureException { EC2CreateImageResponse image = new EC2CreateImageResponse(); NodeList match = null; Node item = null; int status = 0; while( true ) { String query = "command=queryAsyncJobResult&jobId=" + jobId; Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "queryAsyncJobResult", true ); match = cloudResp.getElementsByTagName( "jobstatus" ); if ( 0 < match.getLength()) { item = match.item(0); status = Integer.parseInt( new String( item.getFirstChild().getNodeValue())); } else status = 2; switch( status ) { case 0: // in progress, slow down the hits from this polling a little try { Thread.sleep( pollInterval1 ); } catch( Exception e ) {} break; case 2: default: match = cloudResp.getElementsByTagName( "jobresult" ); if ( 0 < match.getLength()) { item = match.item(0); if (null != item && null != item.getFirstChild()) throw new EC2ServiceException( "InternalError - template action failed: " + item.getFirstChild().getNodeValue(), 500 ); } throw new EC2ServiceException( "InternalError - template action failed", 500 ); case 1: // Successfully completed match = cloudResp.getElementsByTagName( "id" ); if (0 < match.getLength()) { item = match.item(0); image.setId( item.getFirstChild().getNodeValue()); } return image; } } } /** * Waiting for VMs to deploy, all the information to return to the user is * returned once all the asynch commands finish. Note that multiple deployVirtualMachine commands * can be outstanding and here we wait for all of them to complete either successfully * or by failure. On a failure we continue to wait for the other pending requests. * * @param vms - used to change the state of a new vm * @param jobIds - entries are null after completed, is an array of deployVirtualMachine requests to complete * @param waitCount - number of asynch jobs to wait for, the number of entries in jobIds array * * @return the same array given as 'vms' parameter but with modified vm state */ private EC2Instance[] waitForDeploy( EC2Instance[] vms, String[] jobIds, int waitCount ) throws EC2ServiceException, IOException, SAXException, ParserConfigurationException, DOMException, ParseException, SignatureException { NodeList match = null; Node item = null; int status = 0; int j = 0; while( waitCount > 0 ) { if (null != jobIds[j]) { String query = "command=queryAsyncJobResult&jobId=" + jobIds[j]; Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "queryAsyncJobResult", true ); match = cloudResp.getElementsByTagName( "jobstatus" ); if ( 0 < match.getLength()) { item = match.item(0); status = Integer.parseInt( new String( item.getFirstChild().getNodeValue())); } else status = 2; switch( status ) { case 0: // in progress, slow down the hits from this polling a little try { Thread.sleep( pollInterval2 ); } catch( Exception e ) {} break; case 2: // operation failed -- keep waiting for others in progress default: match = cloudResp.getElementsByTagName( "jobresult" ); if ( 0 < match.getLength()) { item = match.item(0); if (null != item && null != item.getFirstChild()) logger.info( "InternalError - deploy action failed: " + item.getFirstChild().getNodeValue()); } waitCount--; jobIds[j] = null; break; case 1: // Successfully completed match = cloudResp.getElementsByTagName( "id" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setId( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "name" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setName( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "zonename" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setZoneName( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "templateid" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setTemplateId( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "group" ); if (0 < match.getLength()) { item = match.item(0); if (null != item && null != item.getFirstChild()) vms[j].setGroup( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "state" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setState( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "created" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setCreated( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "ipaddress" ); if (0 < match.getLength()) { item = match.item(0); vms[j].setIpAddress( item.getFirstChild().getNodeValue()); } waitCount--; jobIds[j] = null; break; } } // -> go over the array repeatedly until all are done j++; if (j >= jobIds.length) j=0; } return vms; } /** * Each Asynch response had different XML tags to that forces a different waitFor function * to create the required return object. * * @param jobId */ private EC2Volume waitForVolume( String jobId ) throws EC2ServiceException, IOException, SAXException, ParserConfigurationException, DOMException, ParseException, SignatureException { EC2Volume vol = new EC2Volume(); NodeList match = null; Node item = null; int status = 0; while( true ) { String query = "command=queryAsyncJobResult&jobId=" + jobId; Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "queryAsyncJobResult", true ); match = cloudResp.getElementsByTagName( "jobstatus" ); if ( 0 < match.getLength()) { item = match.item(0); status = Integer.parseInt( new String( item.getFirstChild().getNodeValue())); } else status = 2; switch( status ) { case 0: // in progress, slow down the hits from this polling a little try { Thread.sleep( pollInterval3 ); } catch( Exception e ) {} break; case 2: default: match = cloudResp.getElementsByTagName( "jobresult" ); if ( 0 < match.getLength()) { item = match.item(0); if (null != item && null != item.getFirstChild()) throw new EC2ServiceException( "InternalError - volume action failed: " + item.getFirstChild().getNodeValue(), 500 ); } throw new EC2ServiceException( "InternalError - volume action failed", 500 ); case 1: // Successfully completed match = cloudResp.getElementsByTagName( "id" ); if (0 < match.getLength()) { item = match.item(0); vol.setId( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "zonename" ); if (0 < match.getLength()) { item = match.item(0); vol.setZoneName( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "size" ); if (0 < match.getLength()) { item = match.item(0); vol.setSize( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "created" ); if (0 < match.getLength()) { item = match.item(0); vol.setCreated( item.getFirstChild().getNodeValue()); } vol.setStatus( "available" ); return vol; } } } private EC2Snapshot waitForSnapshot( String jobId ) throws EC2ServiceException, IOException, SAXException, ParserConfigurationException, DOMException, ParseException, SignatureException { EC2Snapshot shot = new EC2Snapshot(); NodeList match = null; Node item = null; int status = 0; while( true ) { String query = "command=queryAsyncJobResult&jobId=" + jobId; Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "queryAsyncJobResult", true ); match = cloudResp.getElementsByTagName( "jobstatus" ); if ( 0 < match.getLength()) { item = match.item(0); status = Integer.parseInt( new String( item.getFirstChild().getNodeValue())); } else status = 2; switch( status ) { case 0: // in progress, seems to take a long time try { Thread.sleep( pollInterval4 ); } catch( Exception e ) {} break; case 2: default: match = cloudResp.getElementsByTagName( "jobresult" ); if ( 0 < match.getLength()) { item = match.item(0); if (null != item && null != item.getFirstChild()) throw new EC2ServiceException( "InternalError - snapshot action failed: " + item.getFirstChild().getNodeValue(), 500 ); } throw new EC2ServiceException( "InternalError - snapshot action failed", 500 ); case 1: // Successfully completed match = cloudResp.getElementsByTagName( "id" ); if (0 < match.getLength()) { item = match.item(0); shot.setId( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "name" ); if (0 < match.getLength()) { item = match.item(0); shot.setName( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "snapshottype" ); if (0 < match.getLength()) { item = match.item(0); shot.setType( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "account" ); if (0 < match.getLength()) { item = match.item(0); shot.setAccount( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "created" ); if (0 < match.getLength()) { item = match.item(0); shot.setCreated( item.getFirstChild().getNodeValue()); } return shot; } } } /** * Wait for the asynch request to finish and we just need to know if it succeeded or not * * @param jobId * @return true - the requested action was successful, false - it failed */ private boolean waitForAsynch( String jobId ) throws SignatureException { while( true ) { String result = checkAsyncResult( jobId ); //System.out.println( "waitForAsynch: " + result ); if ( -1 != result.indexOf( "s:1" )) { // -> requested action has finished return true; } else if (-1 != result.indexOf( "s:2" )) { // -> requested action has failed return false; } // -> slow down the hits from this polling a little try { Thread.sleep( pollInterval5 ); } catch( Exception e ) {} } } /** * Waiting for VMs to start or stop is identical. * * @param vms - used to change the state * @param jobIds - entries are null after completed * @param waitCount - number of asynch jobs to wait for * * @return the same array given as 'vms' parameter but with modified vm state */ private EC2Instance[] waitForStartStop( EC2Instance[] vms, String[] jobIds, int waitCount, String newState ) throws SignatureException { int j=0; while( waitCount > 0 ) { if (null != jobIds[j]) { String result = checkAsyncResult( jobIds[j] ); if ( -1 != result.indexOf( "s:1" )) { // -> requested action has finished vms[j].setState( newState ); waitCount--; jobIds[j] = null; } else if (-1 != result.indexOf( "s:2" )) { // -> state of the vm has not changed vms[j].setState( vms[j].getPreviousState()); waitCount--; jobIds[j] = null; } // -> slow down the hits from this polling a little try { Thread.sleep( pollInterval6 ); } catch( Exception e ) {} } // -> go over the array repeatedly until all are done j++; if (j >= jobIds.length) j=0; } return vms; } /** * Performs the cloud API listVirtualMachines one or more times. * * @param virtualMachineIds - an array of instances we are interested in getting information on */ private EC2DescribeInstancesResponse listVirtualMachines( String[] virtualMachineIds ) throws IOException, ParserConfigurationException, SAXException, ParseException, EC2ServiceException, SignatureException { EC2DescribeInstancesResponse instances = new EC2DescribeInstancesResponse(); if (null == virtualMachineIds || 0 == virtualMachineIds.length) { return lookupInstances( null, instances ); } for( int i=0; i < virtualMachineIds.length; i++ ) { instances = lookupInstances( virtualMachineIds[i], instances ); } return instances; } /** * Get one or more templates depending on the volumeId parameter. * * @param volumeId - if interested in one specific volume, null if want to list all volumes * @param instanceId - if interested in volumes for a specific instance, null if instance is not important */ private EC2DescribeVolumesResponse listVolumes( String volumeId, String instanceId, EC2DescribeVolumesResponse volumes ) throws IOException, SAXException, ParserConfigurationException, EC2ServiceException, SignatureException, ParseException { Node parent = null; StringBuffer params = new StringBuffer(); params.append( "command=listVolumes" ); if (null != volumeId ) params.append( "&id=" + volumeId ); if (null != instanceId) params.append( "&virtualMachineId=" + instanceId ); String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listVolumes", true ); NodeList match = cloudResp.getElementsByTagName( "volume" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); EC2Volume vol = new EC2Volume(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); if (name.equalsIgnoreCase( "id" )) vol.setId( value ); else if (name.equalsIgnoreCase( "size" )) vol.setSize( value ); else if (name.equalsIgnoreCase( "zonename" )) vol.setZoneName( value ); else if (name.equalsIgnoreCase( "virtualmachineid")) vol.setInstanceId( value ); else if (name.equalsIgnoreCase( "created" )) vol.setCreated( value ); else if (name.equalsIgnoreCase( "type" )) vol.setType( value ); else if (name.equalsIgnoreCase( "vmstate" )) vol.setVMState( value ); else if (name.equalsIgnoreCase( "state" )) vol.setStatus( value ); } } volumes.addVolume( vol ); } } return volumes; } /** * Translate the given zone name into the required zoneId. Query for * a list of all zones and match the zone name given. Amazon uses zone * names while the Cloud API often requires the zoneId. * * @param zoneName - (e.g., 'AH'), if null return the first zone in the available list * * @return the zoneId that matches the given zone name */ private String toZoneId( String zoneName ) throws EC2ServiceException, SignatureException, IOException, SAXException, ParserConfigurationException { EC2DescribeAvailabilityZonesResponse zones = null; String[] interestedZones = null; if ( null != zoneName) { interestedZones = new String[1]; interestedZones[0] = zoneName; } zones = listZones( interestedZones ); if (null == zones.getZoneIdAt( 0 )) throw new EC2ServiceException( "Unknown zoneName value - " + zoneName, 400 ); return zones.getZoneIdAt( 0 ); } /** * Convert from the Amazon instanceType strings to the Cloud APIs diskOfferingId and * serviceOfferingId based on the loaded map. * * @param instanceType - if null we return the M1Small instance type * * @return an OfferingBundle */ private OfferingBundle instanceTypeToOfferBundle( String instanceType ) { OfferingBundle found = null; if (null == instanceType) found = M1Small; else if (instanceType.equalsIgnoreCase( "m1.small" )) found = M1Small; else if (instanceType.equalsIgnoreCase( "m1.large" )) found = M1Large; else if (instanceType.equalsIgnoreCase( "m1.xlarge" )) found = M1Xlarge; else if (instanceType.equalsIgnoreCase( "c1.medium" )) found = C1Medium; else if (instanceType.equalsIgnoreCase( "c1.xlarge" )) found = C1Xlarge; else if (instanceType.equalsIgnoreCase( "m2.xlarge" )) found = M2Xlarge; else if (instanceType.equalsIgnoreCase( "m2.2xlarge" )) found = M22Xlarge; else if (instanceType.equalsIgnoreCase( "m2.4xlarge" )) found = M24Xlarge; else if (instanceType.equalsIgnoreCase( "cc1.4xlarge")) found = CC14xlarge; else throw new EC2ServiceException( "Unsupported - unknown: " + instanceType, 501 ); if ( null == found.getDiskOfferingId() || null == found.getServiceOfferingId()) throw new EC2ServiceException( "Unsupported - not configured properly: " + instanceType, 500 ); else return found; } /** * Convert from the Cloud serviceOfferingId to the Amazon instanceType strings based * on the loaded map. * * @param serviceOfferingId * @return A valid value for the Amazon defined instanceType */ private String serviceOfferingIdToInstanceType( String serviceOfferingId ) { serviceOfferingId = serviceOfferingId.trim(); if (M1Small.getServiceOfferingId().equals( serviceOfferingId )) return "m1.small"; else if (M1Large.getServiceOfferingId().equals( serviceOfferingId )) return "m1.large"; else if (M1Xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "m1.xlarge"; else if (C1Medium.getServiceOfferingId().equals( serviceOfferingId )) return "c1.medium"; else if (C1Xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "c1.xlarge"; else if (M2Xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "m2.xlarge"; else if (M22Xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "m2.2xlarge"; else if (M24Xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "m2.4xlarge"; else if (CC14xlarge.getServiceOfferingId().equals( serviceOfferingId )) return "cc1.4xlarge"; else { logger.warn( "No instanceType match for serverOfferingId: [" + serviceOfferingId + "]" ); return "m1.small"; } } /** * Match the value in the 'description' field of the listOsTypes response to get * the osTypeId. * * @param osTypeName * @return the Cloud.com API osTypeId */ private String toOSTypeId( String osTypeName ) throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException { Node parent = null; String id = null; Document cloudResp = resolveURL( genAPIURL( "command=listOsTypes", genQuerySignature( "command=listOsTypes" )), "listOsTypes", true ); NodeList match = cloudResp.getElementsByTagName( "ostype" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String nodeName = child.getNodeName(); if (nodeName.equalsIgnoreCase( "id" )) id = child.getFirstChild().getNodeValue(); if (nodeName.equalsIgnoreCase( "description" )) { String description = child.getFirstChild().getNodeValue(); if (-1 != description.indexOf( osTypeName )) return id; } } } } throw new EC2ServiceException( "Unknown osTypeName value - " + osTypeName, 400 ); } /** * More than one place we need to access the defined list of zones. If given a specific * list of zones of interest, then only values from those zones are returned. * * @param interestedZones - can be null, should be a subset of all zones * * @return EC2DescribeAvailabilityZonesResponse */ private EC2DescribeAvailabilityZonesResponse listZones( String[] interestedZones ) throws IOException, SAXException, ParserConfigurationException, EC2ServiceException, SignatureException { EC2DescribeAvailabilityZonesResponse zones = new EC2DescribeAvailabilityZonesResponse(); Node parent = null; String id = null; String name = null; String sortedQuery = new String( "available=true&command=listZones" ); // -> used to generate the signature Document cloudResp = resolveURL( genAPIURL( "command=listZones&available=true", genQuerySignature( sortedQuery )), "listZones", true ); NodeList match = cloudResp.getElementsByTagName( "zone" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String nodeName = child.getNodeName(); if (nodeName.equalsIgnoreCase( "id" )) id = child.getFirstChild().getNodeValue(); else if (nodeName.equalsIgnoreCase( "name" )) name = child.getFirstChild().getNodeValue(); } // -> are we asking about specific zones? if ( null != interestedZones && 0 < interestedZones.length ) { for( int j=0; j < interestedZones.length; j++ ) { if (interestedZones[j].equalsIgnoreCase( name )) { zones.addZone( id, name ); break; } } } else zones.addZone( id, name ); } } return zones; } /** * Get information on one or more virtual machines depending on the instanceId parameter. * * @param instanceId - if null then return information on all existing instances, otherwise * just return information on the matching instance. * @param instances - a container object to fill with one or more EC2Instance objects * * @return the same object passed in as the "instances" parameter modified with one or more * EC2Instance objects loaded. */ private EC2DescribeInstancesResponse lookupInstances( String instanceId, EC2DescribeInstancesResponse instances ) throws IOException, ParserConfigurationException, SAXException, ParseException, EC2ServiceException, SignatureException { Node parent = null; StringBuffer params = new StringBuffer(); params.append( "command=listVirtualMachines" ); if (null != instanceId) params.append( "&id=" + instanceId ); String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listVirtualMachines", true ); NodeList match = cloudResp.getElementsByTagName( "virtualmachine" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); EC2Instance vm = new EC2Instance(); for( int j=0; j < numChild; j++ ) { // -> each template has many child tags Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); //System.out.println( "vm: " + name + " = " + value ); if (name.equalsIgnoreCase( "id" )) vm.setId( value ); else if (name.equalsIgnoreCase( "name" )) vm.setName( value ); else if (name.equalsIgnoreCase( "zonename" )) vm.setZoneName( value ); else if (name.equalsIgnoreCase( "templateid")) vm.setTemplateId( value ); else if (name.equalsIgnoreCase( "group" )) vm.setGroup( value ); else if (name.equalsIgnoreCase( "state" )) vm.setState( value ); else if (name.equalsIgnoreCase( "created" )) vm.setCreated( value ); else if (name.equalsIgnoreCase( "ipaddress" )) vm.setIpAddress( value ); else if (name.equalsIgnoreCase( "serviceofferingid" )) vm.setServiceOffering( serviceOfferingIdToInstanceType( value )); } } instances.addInstance( vm ); } } return instances; } /** * Get one or more templates depending on the templateId parameter. * * @param templateId - if null then return information on all existing templates, otherwise * just return information on the matching template. * @param images - a container object to fill with one or more EC2Image objects * * @return the same object passed in as the "images" parameter modified with one or more * EC2Image objects loaded. */ private EC2DescribeImagesResponse listTemplates( String templateId, EC2DescribeImagesResponse images ) throws IOException, ParserConfigurationException, SAXException, EC2ServiceException, SignatureException { Node parent = null; StringBuffer params = new StringBuffer(); params.append( "command=listTemplates" ); if (null != templateId) params.append( "&id=" + templateId ); params.append( "&templateFilter=executable" ); // -> a required parameter String query = params.toString(); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listTemplates", true ); NodeList match = cloudResp.getElementsByTagName( "template" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); EC2Image template = new EC2Image(); for( int j=0; j < numChild; j++ ) { // -> each template has many child tags Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); if (name.equalsIgnoreCase( "id" )) template.setId( value ); else if (name.equalsIgnoreCase( "name" )) template.setName( value ); else if (name.equalsIgnoreCase( "displaytext")) template.setDescription( value ); else if (name.equalsIgnoreCase( "ostypeid" )) template.setOsTypeId( value ); else if (name.equalsIgnoreCase( "ispublic" )) template.setIsPublic( value.equalsIgnoreCase( "true" )); else if (name.equalsIgnoreCase( "isready" )) template.setIsReady( value.equalsIgnoreCase( "true" )); } } images.addImage( template ); } } return images; } /** * Return information on all defined disk offerings. */ private DiskOfferings listDiskOfferings() throws IOException, ParserConfigurationException, SAXException, ParseException, EC2ServiceException, SignatureException { DiskOfferings offerings = new DiskOfferings(); Node parent = null; String query = new String( "command=listDiskOfferings" ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listDiskOfferings", true ); NodeList match = cloudResp.getElementsByTagName( "diskoffering" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); DiskOffer df = new DiskOffer(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); if (name.equalsIgnoreCase( "id" )) df.setId( value ); else if (name.equalsIgnoreCase( "name" )) df.setName( value ); else if (name.equalsIgnoreCase( "disksize" )) df.setSize( value ); else if (name.equalsIgnoreCase( "created" )) df.setCreated( value ); } } offerings.addOffer( df ); } } return offerings; } /** * Return information on all defined service offerings. */ private ServiceOfferings listServiceOfferings() throws IOException, ParserConfigurationException, SAXException, ParseException, EC2ServiceException, SignatureException { ServiceOfferings offerings = new ServiceOfferings(); Node parent = null; String query = new String( "command=listServiceOfferings" ); Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "listServiceOfferings", true ); NodeList match = cloudResp.getElementsByTagName( "serviceoffering" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); ServiceOffer sf = new ServiceOffer(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); if (name.equalsIgnoreCase( "id" )) sf.setId( value ); else if (name.equalsIgnoreCase( "name" )) sf.setName( value ); else if (name.equalsIgnoreCase( "memory" )) sf.setMemory( value ); else if (name.equalsIgnoreCase( "cpunumber" )) sf.setCPUNumber( value ); else if (name.equalsIgnoreCase( "cpuspeed" )) sf.setCPUSpeed( value ); else if (name.equalsIgnoreCase( "created" )) sf.setCreated( value ); } } offerings.addOffer( sf ); } } return offerings; } public EC2DescribeSecurityGroupsResponse listSecurityGroups( String[] interestedGroups ) throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException, ParseException { EC2DescribeSecurityGroupsResponse groupSet = new EC2DescribeSecurityGroupsResponse(); Node parent = null; Document cloudResp = resolveURL( genAPIURL( "command=listNetworkGroups", genQuerySignature( "command=listNetworkGroups" )), "listNetworkGroups", true ); NodeList match = cloudResp.getElementsByTagName( "networkgroup" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); EC2SecurityGroup group = new EC2SecurityGroup(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); //System.out.println( "listNetworkGroups " + name + "=" + value ); if (name.equalsIgnoreCase( "name" )) group.setName( value ); else if (name.equalsIgnoreCase( "description" )) group.setDescription( value ); else if (name.equalsIgnoreCase( "account" )) group.setAccount( value ); else if (name.equalsIgnoreCase( "ingressrule" )) group.addIpPermission( toPermission( child.getChildNodes())); } } // -> are we asking about specific security groups? if ( null != interestedGroups && 0 < interestedGroups.length ) { for( int j=0; j < interestedGroups.length; j++ ) { if (interestedGroups[j].equalsIgnoreCase( group.getName())) { groupSet.addGroup( group ); break; } } } else groupSet.addGroup( group ); } } return groupSet; } /** * Temporary hack to just get a valid VLANID */ private String hackGetVlanId() throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException, ParseException { Node parent = null; Document cloudResp = resolveURL( genAPIURL( "command=listVlanIpRanges&vlantype=DirectAttached", genQuerySignature( "command=listVlanIpRanges&vlantype=DirectAttached" )), "listVlanIpRanges", true ); NodeList match = cloudResp.getElementsByTagName( "vlaniprange" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); System.out.println( "listVlanIpRanges " + name + "=" + value ); if (name.equalsIgnoreCase( "id" )) return value; } } } } return null; } /** * Ingress Rules are one more level down the XML tree. */ private EC2IpPermission toPermission( NodeList rule ) { int numChild = rule.getLength(); EC2IpPermission perm = new EC2IpPermission(); String account = null; String groupName = null; for( int i=0; i < numChild; i++ ) { Node child = rule.item(i); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); //System.out.println( "ingress rule: " + name + "=" + value ); if (name.equalsIgnoreCase( "protocol" )) perm.setProtocol( value ); else if (name.equalsIgnoreCase( "startport" )) perm.setFromPort( Integer.parseInt( value )); else if (name.equalsIgnoreCase( "endport" )) perm.setToPort( Integer.parseInt( value )); else if (name.equalsIgnoreCase( "cidr" )) perm.addIpRange( value ); else if (name.equalsIgnoreCase( "account" )) account = value; else if (name.equalsIgnoreCase( "networkgroupname" )) groupName = value ; if (null != account && null != groupName) { EC2SecurityGroup group = new EC2SecurityGroup(); group.setAccount( account ); group.setName( groupName ); account = null; groupName = null; perm.addUser( group ); } } } return perm; } /** * If given a specific list of snapshots of interest, then only values from those snapshots are returned. * * @param interestedShots - can be null, should be a subset of all snapshots */ private EC2DescribeSnapshotsResponse listSnapshots( String[] interestedShots ) throws EC2ServiceException, UnsupportedEncodingException, SignatureException, IOException, SAXException, ParserConfigurationException, ParseException { EC2DescribeSnapshotsResponse snapshots = new EC2DescribeSnapshotsResponse(); Node parent = null; Document cloudResp = resolveURL( genAPIURL( "command=listSnapshots", genQuerySignature( "command=listSnapshots" )), "listSnapshots", true ); NodeList match = cloudResp.getElementsByTagName( "snapshot" ); int length = match.getLength(); for( int i=0; i < length; i++ ) { if (null != (parent = match.item(i))) { NodeList children = parent.getChildNodes(); int numChild = children.getLength(); EC2Snapshot shot = new EC2Snapshot(); for( int j=0; j < numChild; j++ ) { Node child = children.item(j); String name = child.getNodeName(); if (null != child.getFirstChild()) { String value = child.getFirstChild().getNodeValue(); if (name.equalsIgnoreCase( "id" )) shot.setId( value ); else if (name.equalsIgnoreCase( "name" )) shot.setName( value ); else if (name.equalsIgnoreCase( "volumeid" )) shot.setVolumeId( value ); else if (name.equalsIgnoreCase( "snapshottype" )) shot.setType( value ); else if (name.equalsIgnoreCase( "created" )) shot.setCreated( value ); } } // -> are we asking about specific snapshots? if ( null != interestedShots && 0 < interestedShots.length ) { for( int j=0; j < interestedShots.length; j++ ) { if (interestedShots[j].equalsIgnoreCase( shot.getId())) { snapshots.addSnapshot( shot ); break; } } } else snapshots.addSnapshot( shot ); } } return snapshots; } /** * Has the job finished? * * @param jobId * @return A string with one or two values, "s:status r:result" if both present. */ private String checkAsyncResult( String jobId ) throws EC2ServiceException, InternalErrorException, SignatureException { StringBuffer checkResult = new StringBuffer(); NodeList match = null; Node item = null; String status = null; String result = null; try { String query = "command=queryAsyncJobResult&jobId=" + jobId; Document cloudResp = resolveURL( genAPIURL( query, genQuerySignature( query )), "queryAsyncJobResult", true ); match = cloudResp.getElementsByTagName( "jobstatus" ); if (0 < match.getLength()) { item = match.item(0); status = new String( item.getFirstChild().getNodeValue()); } match = cloudResp.getElementsByTagName( "jobresult" ); if (0 < match.getLength()) { item = match.item(0); result = new String( item.getFirstChild().getNodeValue()); } checkResult.append( " " ); if (null != status) checkResult.append( "s:" ).append( status ); if (null != result) checkResult.append( " r:" ).append( result ); return checkResult.toString(); } catch( EC2ServiceException error ) { logger.error( "EC2 checkAsyncResult - " + error.toString()); throw error; } catch( Exception e ) { logger.error( "EC2 checkAsyncResult - " + e.toString()); throw new InternalErrorException( e.toString()); } } /** * Translate the device name string into a Cloud Stack deviceId. * deviceId 3 is reserved for CDROM and 0 for the ROOT disk * * @param request * @return a modified EC2Volume object */ private EC2Volume mapDeviceToCloudDeviceId( EC2Volume request ) { String device = request.getDevice(); if (device.equalsIgnoreCase( "/dev/sdb" )) request.setDeviceId( 1 ); else if (device.equalsIgnoreCase( "/dev/sdc" )) request.setDeviceId( 2 ); else if (device.equalsIgnoreCase( "/dev/sde" )) request.setDeviceId( 4 ); else if (device.equalsIgnoreCase( "/dev/sdf" )) request.setDeviceId( 5 ); else if (device.equalsIgnoreCase( "/dev/sdg" )) request.setDeviceId( 6 ); else if (device.equalsIgnoreCase( "/dev/sdh" )) request.setDeviceId( 7 ); else if (device.equalsIgnoreCase( "/dev/sdi" )) request.setDeviceId( 8 ); else if (device.equalsIgnoreCase( "/dev/sdj" )) request.setDeviceId( 9 ); else if (device.equalsIgnoreCase( "/dev/xvdb" )) request.setDeviceId( 1 ); else if (device.equalsIgnoreCase( "/dev/xvdc" )) request.setDeviceId( 2 ); else if (device.equalsIgnoreCase( "/dev/xvde" )) request.setDeviceId( 4 ); else if (device.equalsIgnoreCase( "/dev/xvdf" )) request.setDeviceId( 5 ); else if (device.equalsIgnoreCase( "/dev/xvdg" )) request.setDeviceId( 6 ); else if (device.equalsIgnoreCase( "/dev/xvdh" )) request.setDeviceId( 7 ); else if (device.equalsIgnoreCase( "/dev/xvdi" )) request.setDeviceId( 8 ); else if (device.equalsIgnoreCase( "/dev/xvdj" )) request.setDeviceId( 9 ); else throw new EC2ServiceException( device + " is not supported" ); return request; } private String genAPIURL( String query, String signature ) throws UnsupportedEncodingException { UserContext ctx = UserContext.current(); if ( null != ctx ) { StringBuffer apiCommand = new StringBuffer(); apiCommand.append( getServerURL()); apiCommand.append( query ); apiCommand.append( "&apikey=" ).append( safeURLencode( ctx.getAccessKey())); apiCommand.append( "&signature=" ).append( safeURLencode( signature )); return apiCommand.toString(); } else return null; } /** * Calculated the signature that is required for the Developer API. * * @param sortedCommand - must be in sorted order, and URL encoded. * * @return a hash of the lower-cased query string using the HMAC SHA-1 algorithm with the secret key */ private String genQuerySignature( String sortedCommand ) throws UnsupportedEncodingException, SignatureException { StringBuffer sigOver = new StringBuffer(); sigOver.append( "apikey=" ).append( safeURLencode( UserContext.current().getAccessKey() )); sigOver.append( "&" ).append( sortedCommand ); return calculateRFC2104HMAC( sigOver.toString().toLowerCase(), UserContext.current().getSecretKey()); } /** * Create a signature by the following method: * new String( Base64( SHA1( key, byte array ))) * * @param signIt - the data to generate a keyed HMAC over * @param secretKey - the user's unique key for the HMAC operation * * @return String - the recalculated string */ private String calculateRFC2104HMAC( String signIt, String secretKey ) throws SignatureException { String result = null; try { SecretKeySpec key = new SecretKeySpec( secretKey.getBytes(), "HmacSHA1" ); Mac hmacSha1 = Mac.getInstance( "HmacSHA1" ); hmacSha1.init( key ); byte [] rawHmac = hmacSha1.doFinal( signIt.getBytes()); result = new String( Base64.encodeBase64( rawHmac )); } catch( Exception e ) { throw new SignatureException( "Failed to generate keyed HMAC on soap request: " + e.getMessage()); } return result.trim(); } /** * Every function in the engine is going to need to resolve a cloud API URL into an XML document. * * @param uri - a complete cloud client API uri * @returns a parsed XML document from the cloud API server. */ private Document resolveURL( String uri, String command, boolean logRequest ) throws IOException, SAXException, ParserConfigurationException, EC2ServiceException { if (logRequest) logger.debug( "Cloud API call + [" + uri + "]" ); String errorMsg = null; URL cloudapi = new URL( uri ); HttpURLConnection connect = (HttpURLConnection) cloudapi.openConnection(); Integer code = new Integer( connect.getResponseCode()); if (400 <= code.intValue()) { if ( null != (errorMsg = connect.getResponseMessage())) throw new EC2ServiceException( code.toString() + " " + errorMsg, code ); else throw new EC2ServiceException( command + " cloud API HTTP Error: " + code.toString(), code ); } DocumentBuilder db = dbf.newDocumentBuilder(); return db.parse( connect.getInputStream()); } /** * Normalized URL encoding uses "%20" for spaces instead of "+" signs. */ private String safeURLencode( String param ) throws UnsupportedEncodingException { return URLEncoder.encode( param, "UTF-8" ).replaceAll( "\\+", "%20" ); } private String getServerURL() { if ( null == cloudAPIPort ) return new String( "http://" + managementServer + "/client/api?" ); else return new String( "http://" + managementServer + ":" + cloudAPIPort + "/client/api?" ); } }