01000101 9 år sedan
förälder
incheckning
ae09a40324

+ 6 - 2
cloudbridge/cloud/base/resources.py

@@ -353,8 +353,12 @@ class BaseMachineImage(
             interval=interval)
 
     def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
+        try:
+            return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
+                                                self.name, self.id)
+        except AttributeError:
+            return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
+                                                'UNKNOWN', 'UNKNOWN')
 
 
 class BaseAttachmentInfo(AttachmentInfo):

+ 3 - 1
cloudbridge/cloud/interfaces/resources.py

@@ -1138,10 +1138,12 @@ class Router(CloudResource):
         pass
 
     @abstractmethod
-    def detach_network(self):
+    def detach_network(self, network_id=None):
         """
         Detach this router from a network.
 
+        :warn: `network_id` is currently *required* for AWS resources.
+
         :rtype: ``bool``
         :return: ``True`` if successful.
         """

+ 6 - 2
cloudbridge/cloud/interfaces/services.py

@@ -160,7 +160,7 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name):
+    def find(self, name, limit=None, marker=None):
         """
         Searches for an instance by a given list of attributes.
 
@@ -641,7 +641,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, network_id, cidr_block, name=None):
+    def create(self, network_id, cidr_block, name=None, zone=None):
         """
         Create a new subnet within the supplied network.
 
@@ -656,6 +656,10 @@ class SubnetService(PageableObjectMixin, CloudService):
         :param name: An optional subnet name. The name will be set if the
                      provider supports it.
 
+        :type zone: ``str``
+        :param zone: **Currently only supported by AWS providers**. Specifies
+                     the availability / placement zone for the subnet.
+
         :rtype: ``object`` of :class:`.Subnet`
         :return:  A Subnet object
         """

+ 111 - 54
cloudbridge/cloud/providers/aws/resources.py

@@ -2,6 +2,7 @@
 DataTypes used by this provider
 """
 from datetime import datetime
+from time import sleep
 import hashlib
 import inspect
 import json
@@ -31,7 +32,6 @@ from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from cloudbridge.cloud.interfaces.services import VolumeService
 
 from botocore.exceptions import ClientError as EC2ResponseError
 from boto.s3.key import Key
@@ -42,8 +42,12 @@ class AWSMachineImage(BaseMachineImage):
 
     IMAGE_STATE_MAP = {
         'pending': MachineImageState.PENDING,
+        'transient': MachineImageState.PENDING,
         'available': MachineImageState.AVAILABLE,
-        'failed': MachineImageState.ERROR
+        'deregistered': MachineImageState.ERROR,
+        'failed': MachineImageState.ERROR,
+        'error': MachineImageState.ERROR,
+        'invalid': MachineImageState.ERROR
     }
 
     def __init__(self, provider, image):
@@ -71,8 +75,12 @@ class AWSMachineImage(BaseMachineImage):
 
         :rtype: ``str``
         :return: Name for this image as returned by the cloud middleware.
+                 Returns `None` if the image is deregistered.
         """
-        return self._ec2_image.name
+        try:
+            return self._ec2_image.name
+        except AttributeError:
+            return None
 
     @property
     def description(self):
@@ -80,9 +88,13 @@ class AWSMachineImage(BaseMachineImage):
         Get the image description.
 
         :rtype: ``str``
-        :return: Description for this image as returned by the cloud middleware
+        :return: Description for this image as returned by the cloud middleware.
+                 Returns `None` if the image is deregistered.
         """
-        return self._ec2_image.description
+        try:
+            return self._ec2_image.description
+        except AttributeError:
+            return None
 
     def delete(self):
         """
@@ -92,21 +104,18 @@ class AWSMachineImage(BaseMachineImage):
 
     @property
     def state(self):
-        return AWSMachineImage.IMAGE_STATE_MAP.get(
-            self._ec2_image.state, MachineImageState.UNKNOWN)
+        try:
+            return AWSMachineImage.IMAGE_STATE_MAP.get(
+                self._ec2_image.state, MachineImageState.UNKNOWN)
+        except AttributeError:
+            return MachineImageState.UNKNOWN
 
     def refresh(self):
         """
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        image = self._provider.compute.images.get(self.id)
-        if image:
-            # pylint:disable=protected-access
-            self._ec2_image = image._ec2_image
-        else:
-            # image no longer exists
-            self._ec2_image.state = "unknown"
+        self._ec2_image.reload()
 
 
 class AWSPlacementZone(BasePlacementZone):
@@ -235,7 +244,10 @@ class AWSInstance(BaseInstance):
 
         .. note:: an instance must have a (case sensitive) tag ``Name``
         """
-        return self._ec2_instance.tags.get('Name')
+        for tag in self._ec2_instance.tags or list():
+            if tag.get('Key') == 'Name':
+                return tag.get('Value')
+        return None
 
     @name.setter
     # pylint:disable=arguments-differ
@@ -243,14 +255,14 @@ class AWSInstance(BaseInstance):
         """
         Set the instance name.
         """
-        self._ec2_instance.add_tag('Name', value)
+        self._ec2_instance.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
 
     @property
     def public_ips(self):
         """
         Get all the public IP addresses for this instance.
         """
-        return [self._ec2_instance.ip_address]
+        return [self._ec2_instance.public_ip_address]
 
     @property
     def private_ips(self):
@@ -279,12 +291,22 @@ class AWSInstance(BaseInstance):
         Reboot this instance (using the cloud middleware API).
         """
         self._ec2_instance.reboot()
+        # Wait for the system to go down
+        timeout_total = 300
+        timeout_ival = 15
+        while self.state == InstanceState.RUNNING and timeout_total > 0:
+            sleep(timeout_ival)
+            timeout_total = timeout_total - timeout_ival
+            self.refresh()
+        # Wait for the system to be running
+        self._ec2_instance.wait_until_running()
 
     def terminate(self):
         """
         Permanently terminate this instance.
         """
         self._ec2_instance.terminate()
+        self._ec2_instance.wait_until_terminated()
 
     @property
     def image_id(self):
@@ -298,7 +320,7 @@ class AWSInstance(BaseInstance):
         """
         Get the placement zone id where this instance is running.
         """
-        return self._ec2_instance.placement
+        return self._ec2_instance.placement.get('AvailabilityZone')
 
     @property
     def security_groups(self):
@@ -308,15 +330,20 @@ class AWSInstance(BaseInstance):
         # boto instance.groups field returns a ``Group`` object so need to
         # convert that into a ``SecurityGroup`` object before creating a
         # cloudbridge SecurityGroup object
-        return [self._provider.security.security_groups.get(group.id)
-                for group in self._ec2_instance.groups]
+        return [
+            self._provider.security.security_groups.get(group_id)
+            for group_id in self.security_group_ids
+        ]
 
     @property
     def security_group_ids(self):
         """
         Get the security groups IDs associated with this instance.
         """
-        return [group.id for group in self._ec2_instance.groups]
+        return set([
+            group.get('GroupId') for group in
+            self._ec2_instance.security_groups
+        ])
 
     @property
     def key_pair_name(self):
@@ -329,36 +356,57 @@ class AWSInstance(BaseInstance):
         """
         Create a new image based on this instance.
         """
-        image_id = self._ec2_instance.create_image(name)
-        # Sometimes, the image takes a while to register, so retry a few times
-        # if the image cannot be found
-        retry_decorator = retry(retry_on_result=lambda result: result is None,
-                                stop_max_attempt_number=3, wait_fixed=1000)
-        image = retry_decorator(self._provider.compute.images.get)(image_id)
+        image = AWSMachineImage(self._provider,
+                                self._ec2_instance.create_image(Name=name))
+        # Wait for the image to exist
+        self._provider.ec2_conn.meta.client.get_waiter('image_exists').wait(
+            ImageIds=[image.id])
+        # Return the image
+        image.refresh()
         return image
 
     def add_floating_ip(self, ip_address):
         """
         Add an elastic IP address to this instance.
         """
-        if self._ec2_instance.vpc_id:
-            aid = self._provider._vpc_conn.get_all_addresses([ip_address])[0]
-            return self._provider._ec2_conn.associate_address(
-                self._ec2_instance.id, allocation_id=aid.allocation_id)
-        else:
-            return self._ec2_instance.use_ip(ip_address)
+        return self._provider.ec2_conn.meta.client.associate_address(**{
+            k: v for k, v in {
+                'InstanceId': self.id,
+                'PublicIp': None if self._ec2_instance.vpc_id else ip_address,
+                'AllocationId':
+                    None if not self._ec2_instance.vpc_id else
+                    ip_address.id if isinstance(ip_address, 'FloatingIP') else
+                    [
+                        x for x in
+                        self._provider.network.floating_ips()
+                        if x.public_ip == ip_address
+                    ][0].id
+            }.items() if v is not None
+        })
 
     def remove_floating_ip(self, ip_address):
         """
         Remove a elastic IP address from this instance.
         """
-        raise NotImplementedError(
-            'remove_floating_ip not implemented by this provider.')
+        return self._provider.ec2_conn.meta.client.disassociate_address(**{
+            k: v for k, v in {
+                'PublicIp': None if self._ec2_instance.vpc_id else ip_address,
+                'AssociationId':
+                    None if not self._ec2_instance.vpc_id else
+                    ip_address.association_id
+                    if isinstance(ip_address, 'FloatingIP') else
+                    [
+                        x for x in
+                        self._provider.network.floating_ips()
+                        if x.public_ip == ip_address
+                    ][0].association_id
+            }.items() if v is not None
+        })
 
     @property
     def state(self):
         return AWSInstance.INSTANCE_STATE_MAP.get(
-            self._ec2_instance.state, InstanceState.UNKNOWN)
+            self._ec2_instance.state['Name'], InstanceState.UNKNOWN)
 
     def refresh(self):
         """
@@ -366,11 +414,13 @@ class AWSInstance(BaseInstance):
         for its latest state.
         """
         try:
-            self._ec2_instance.update(validate=True)
+            self._ec2_instance.reload()
         except (EC2ResponseError, ValueError):
-            # The volume no longer exists and cannot be refreshed.
-            # set the status to unknown
-            self._ec2_instance.status = 'unknown'
+            # Silently fail for now
+            return
+
+    def wait_until_exists(self, timeout=None, interval=None):
+        self._ec2_instance.wait_until_running()
 
 
 class AWSVolume(BaseVolume):
@@ -605,7 +655,7 @@ class AWSSnapshot(BaseSnapshot):
         """
         Create a new Volume from this Snapshot.
         """
-        cb_vol = VolumeService(self._provider).create(
+        cb_vol = self._provider.block_store.volumes.create(
             name=self.name,
             size=size,
             zone=placement,
@@ -733,11 +783,11 @@ class AWSSecurityGroup(BaseSecurityGroup):
             return False
 
     def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        attr = inspect.getmembers(self, lambda a: not inspect.isroutine(a))
+        _js = {k: v for(k, v) in attr if not k.startswith('_')}
         json_rules = [r.to_json() for r in self.rules]
-        js['rules'] = [json.loads(r) for r in json_rules]
-        return json.dumps(js, sort_keys=True)
+        _js['rules'] = [json.loads(r) for r in json_rules]
+        return json.dumps(_js, sort_keys=True)
 
 
 class AWSSecurityGroupRule(BaseSecurityGroupRule):
@@ -1000,10 +1050,9 @@ class AWSNetwork(BaseNetwork):
     def subnets(self):
         return [AWSSubnet(self._provider, x) for x in self._vpc.subnets.all()]
 
-    def create_subnet(self, cidr_block, name=None):
-        subnet = AWSSubnet(
-            self._provider,
-            self._vpc.create_subnet(CidrBlock=cidr_block))
+    def create_subnet(self, cidr_block, name=None, zone=None):
+        subnet = self._provider.network.subnets.create(
+            self.id, cidr_block, name=name, zone=zone)
         if name:
             subnet.name = name
         return subnet
@@ -1056,6 +1105,11 @@ class AWSSubnet(BaseSubnet):
     def cidr_block(self):
         return self._subnet.cidr_block
 
+    @property
+    def availability_zone(self):
+        '''Returns availability / placement zone ID'''
+        return self._subnet.availability_zone
+
     @property
     def network_id(self):
         return self._subnet.vpc_id
@@ -1074,6 +1128,10 @@ class AWSFloatingIP(BaseFloatingIP):
     def id(self):
         return self._ip.allocation_id
 
+    @property
+    def association_id(self):
+        return self._ip.association_id
+
     @property
     def public_ip(self):
         return self._ip.public_ip
@@ -1110,14 +1168,14 @@ class AWSRouter(BaseRouter):
         :rtype: :class:`boto.vpc.routetable.RouteTable`
         :return: A RouteTable object.
         """
-        return self._provider.ec2_conn.route_tables.filter(
+        return list(self._provider.ec2_conn.route_tables.filter(
             Filters=[{
                 'Name': 'vpc-id',
                 'Values': [
                     self._provider.ec2_conn.Subnet(subnet_id).vpc_id
                 ]
             }]
-        )[0]
+        ).limit(1))[0]
 
     @property
     def id(self):
@@ -1172,7 +1230,7 @@ class AWSRouter(BaseRouter):
     def attach_network(self, network_id):
         return self._router.attach_to_vpc(VpcId=network_id)
 
-    def detach_network(self):
+    def detach_network(self, network_id):
         return self._router.detach_from_vpc(VpcId=network_id)
 
     def add_route(self, subnet_id):
@@ -1218,5 +1276,4 @@ class AWSLaunchConfig(BaseLaunchConfig):
         net = self.provider.network.get(net_id)
         sns = net.subnets()
         if sns:
-            sn = sns[0]
-        self.network_interfaces.append(sn.id)
+            self.network_interfaces.append(sns[0].id)

+ 104 - 219
cloudbridge/cloud/providers/aws/services.py

@@ -2,12 +2,9 @@
 import time
 import string
 
-from boto.ec2.blockdevicemapping import BlockDeviceMapping
-from boto.ec2.blockdevicemapping import BlockDeviceType
 from botocore.exceptions import ClientError as EC2ResponseError
 
 from cloudbridge.cloud.base.resources import ClientPagedResultList
-from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.services import BaseBlockStoreService
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseImageService
@@ -23,8 +20,6 @@ from cloudbridge.cloud.base.services import BaseSnapshotService
 from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.interfaces.resources import InstanceType
-from cloudbridge.cloud.interfaces.resources \
-    import InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 # from cloudbridge.cloud.interfaces.resources import Network
@@ -33,8 +28,6 @@ from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Volume
 
-import cloudbridge as cb
-
 import requests
 
 from .resources import AWSBucket
@@ -130,6 +123,8 @@ class EC2ServiceFilter(object):
             the service method
         '''
         res = getattr(self.provider.ec2_conn, method)(**kwargs)
+        if isinstance(res, list):
+            return [self.iface(self.provider, x) if x else None for x in res]
         return self.iface(self.provider, res) if res else None
 
     def delete(self, val, filter_name):
@@ -476,6 +471,20 @@ class AWSInstanceService(BaseInstanceService):
 
     def __init__(self, provider):
         super(AWSInstanceService, self).__init__(provider)
+        self.iface = EC2ServiceFilter(self.provider,
+                                      'instances', AWSInstance)
+
+    def get(self, instance_id):
+        """Returns an instance given its ID"""
+        return self.iface.get(instance_id, 'instance-id')
+
+    def list(self, limit=None, marker=None):
+        """List all instances associated with this account"""
+        return self.iface.list(limit=limit, marker=marker)
+
+    def find(self, name, limit=None, marker=None):
+        """Searches for an instance by name"""
+        return self.iface.find(name, 'tag:Name', limit=limit, marker=marker)
 
     def create(self, name, image, instance_type, zone=None,
                key_pair=None, security_groups=None, user_data=None,
@@ -488,110 +497,52 @@ class AWSInstanceService(BaseInstanceService):
         method will search for a default VPC and attempt to launch an instance
         into that VPC.
         """
+        # Get the images to use
         image_id = image.id if isinstance(image, MachineImage) else image
+        # Get the flavor / size
         instance_size = instance_type.id if \
             isinstance(instance_type, InstanceType) else instance_type
+        # Get the availability zone
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
+        # Get the key pair
         key_pair_name = key_pair.name if isinstance(
-            key_pair,
-            KeyPair) else key_pair
+            key_pair, KeyPair) else key_pair
+        # Process other launch items
+        dev_mappings = list()
+        sec_group_ids = list()
+        subnet_id = None
         if launch_config:
-            bdm = self._process_block_device_mappings(launch_config, zone_id)
-            subnet_id = self._get_net_id(launch_config)
-        else:
-            bdm = subnet_id = None
-        subnet_id, zone_id, security_group_ids = \
-            self._resolve_launch_options(subnet_id, zone_id, security_groups)
-
-        reservation = self.provider.ec2_conn.run_instances(
-            image_id=image_id, instance_type=instance_size,
-            min_count=1, max_count=1, placement=zone_id,
-            key_name=key_pair_name, security_group_ids=security_group_ids,
-            user_data=user_data, block_device_map=bdm, subnet_id=subnet_id)
-        instance = None
-        if reservation:
-            time.sleep(2)  # The instance does not always get created in time
-            instance = AWSInstance(self.provider, reservation.instances[0])
-            instance.name = name
-        return instance
-
-    def _resolve_launch_options(self, subnet_id, zone_id, security_groups):
-        """
-        Resolve inter-dependent launch options.
-
-        With launching into VPC only, try to figure out a default
-        VPC to launch into, making placement decisions along the way that
-        are implied from the zone a subnet exists in.
-        """
-        def _deduce_subnet_and_zone(vpc, zone_id=None):
-            """
-            Figure out subnet ID from a VPC (and zone_id, if not supplied).
-            """
-            if zone_id:
-                # A placement zone was specified. Choose the default
-                # subnet in that zone.
-                for sn in vpc.subnets():
-                    if sn._subnet.availability_zone == zone_id:
-                        subnet_id = sn.id
-            else:
-                # No zone was requested, so just pick one subnet
-                sn = vpc.subnets()[0]
-                subnet_id = sn.id
-                zone_id = sn._subnet.availability_zone
-            return subnet_id, zone_id
-
-        vpc_id = None
-        sg_ids = []
-        if subnet_id:
-            # Subnet was supplied - get the VPC so named SGs can be resolved
-            subnet = self.provider.vpc_conn.get_all_subnets(subnet_id)[0]
-            vpc_id = subnet.vpc_id
-            # zone_id must match zone where the requested subnet lives
-            if zone_id and subnet.availability_zone != zone_id:
-                raise ValueError("Requested placement zone ({0}) must match "
-                                 "specified subnet's availability zone ({1})."
-                                 .format(zone_id, subnet.availability_zone))
-        if security_groups:
-            # Try to get a subnet via specified SGs. This will work only if
-            # the specified SGs are within a VPC (which is a prerequisite to
-            # launch into VPC anyhow).
-            _sg_ids = self._process_security_groups(security_groups, vpc_id)
-            # Must iterate through all the SGs here because a SG name may
-            # exist in a VPC or EC2-Classic so opt for the VPC SG. This
-            # applies in the case no subnet was specified.
-            if not subnet_id:
-                for sg_id in _sg_ids:
-                    sg = self.provider.security.security_groups.get(sg_id)
-                    if sg._security_group.vpc_id:
-                        if sg_ids and sg_id not in sg_ids:
-                            raise ValueError("Multiple matches for VPC "
-                                             "security group(s) {0}."
-                                             .format(security_groups))
-                        else:
-                            sg_ids.append(sg_id)
-                        vpc = self.provider.network.get(
-                            sg._security_group.vpc_id)
-                        subnet_id, zone_id = _deduce_subnet_and_zone(
-                            vpc, zone_id)
-            else:
-                sg_ids = _sg_ids
-            if not subnet_id:
-                raise AttributeError("Supplied security group(s) ({0}) must "
-                                     "be associated with a VPC."
-                                     .format(security_groups))
-        if not subnet_id and not security_groups:
-            # No VPC/subnet was supplied, search for the default VPC.
-            for vpc in self.provider.network.list():
-                if vpc._vpc.is_default:
-                    vpc_id = vpc.id
-                    subnet_id, zone_id = _deduce_subnet_and_zone(vpc, zone_id)
-            if not vpc_id:
-                raise AttributeError("No default VPC exists. Supply a "
-                                     "subnet to launch into (via "
-                                     "launch_config param).")
-        return subnet_id, zone_id, sg_ids
-
-    def _process_security_groups(self, security_groups, vpc_id=None):
+            dev_mappings = \
+                self._parse_device_mappings(launch_config.block_devices)
+            sec_group_ids = self._parse_security_groups(security_groups)
+            subnet_id = launch_config.network_interfaces[0] \
+                if len(launch_config.network_interfaces) > 0 else None
+        # Create the instance
+        ress = self.iface.create('create_instances', **{
+            k: v for k, v in {
+                'ImageId': image_id,
+                'MinCount': 1,
+                'MaxCount': 1,
+                'KeyName': key_pair_name,
+                'SecurityGroupIds': sec_group_ids or None,
+                'UserData': user_data,
+                'InstanceType': instance_size,
+                'Placement': {
+                    'AvailabilityZone': zone_id or AWSSubnetService(
+                        self.provider).get(subnet_id).availability_zone
+                } if zone_id or subnet_id else None,
+                'BlockDeviceMappings': dev_mappings or None,
+                'SubnetId': subnet_id
+            }.items() if v is not None
+        })
+        if ress and len(ress) == 1:
+            # Wait until the resource exists
+            ress[0].wait_until_exists()
+            return ress[0]
+        raise ValueError(
+            'Expected a single object response, got a list: %s' % ress)
+
+    def _parse_security_groups(self, security_groups):
         """
         Process security groups to create a list of SG ID's for launching.
 
@@ -600,132 +551,63 @@ class AWSInstanceService(BaseInstanceService):
         :param security_groups: A list of ``SecurityGroup`` objects or a list
                                 of ``SecurityGroup`` names, which should be
                                 assigned to this instance.
-
-        :type vpc_id: ``str``
-        :param vpc_id: A VPC ID within which the supplied security groups exist
-
         :rtype: ``list``
         :return: A list of security group IDs.
         """
-        if isinstance(security_groups, list) and \
-                isinstance(security_groups[0], SecurityGroup):
-            sg_ids = [sg.id for sg in security_groups]
-        else:
-            # SG names were supplied, need to map them to SG IDs.
-            sg_ids = []
-            # If a VPC was specified, need to map to the SGs in the VPC.
-            flters = None
-            if vpc_id:
-                flters = {'vpc_id': vpc_id}
-            sgs = self.provider.ec2_conn.get_all_security_groups(
-                filters=flters)
-            sg_ids = [sg.id for sg in sgs if sg.name in security_groups]
-
+        sg_ids = list()
+        if not security_groups:
+            return list()
+        for secgroup in security_groups:
+            if isinstance(secgroup, SecurityGroup):
+                sg_ids.append(secgroup.id)
+            else:
+                sec_obj = AWSSecurityGroupService(self.provider).get(secgroup)
+                if not sec_obj:
+                    raise ValueError('Could not find launch_options '
+                                     'security group: %s <%s>' %
+                                     (secgroup, type(secgroup)))
+                sg_ids.append(sec_obj.id)
         return sg_ids
 
-    def _process_block_device_mappings(self, launch_config, zone=None):
+    @staticmethod
+    def _parse_device_mappings(block_devices):
         """
         Processes block device mapping information
         and returns a Boto BlockDeviceMapping object. If new volumes
         are requested (source is None and destination is VOLUME), they will be
         created and the relevant volume ids included in the mapping.
         """
-        bdm = BlockDeviceMapping()
+        bdml = list()
+        bdm = dict()
+        if not block_devices:
+            return list()
         # Assign letters from f onwards
         # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
         next_letter = iter(list(string.ascii_lowercase[6:]))
         # assign ephemeral devices from 0 onwards
         ephemeral_counter = 0
-        for device in launch_config.block_devices:
-            bd_type = BlockDeviceType()
-
-            if device.is_volume:
-                if device.is_root:
-                    bdm['/dev/sda1'] = bd_type
-                else:
-                    bdm['sd' + next(next_letter)] = bd_type
-
-                if isinstance(device.source, Snapshot):
-                    bd_type.snapshot_id = device.source.id
-                elif isinstance(device.source, Volume):
-                    bd_type.volume_id = device.source.id
-                elif isinstance(device.source, MachineImage):
-                    # Not supported
-                    pass
-                else:
-                    # source is None, but destination is volume, therefore
-                    # create a blank volume. If the Zone is None, this
-                    # could fail since the volume and instance may be created
-                    # in two different zones.
-                    if not zone:
-                        raise InvalidConfigurationException(
-                            "A zone must be specified when launching with a"
-                            " new blank volume block device mapping.")
-                    new_vol = self.provider.block_store.volumes.create(
-                        '',
-                        device.size,
-                        zone)
-                    bd_type.volume_id = new_vol.id
-                bd_type.delete_on_terminate = device.delete_on_terminate
-                if device.size:
-                    bd_type.size = device.size
+        for dev in block_devices:
+            if dev.is_volume:
+                # Generate the device path
+                bdm['DeviceName'] = \
+                    'dev/sd' + 'a1' if dev.is_root else next(next_letter)
+                bdm['Ebs'] = {
+                    'SnapshotId':
+                        dev.source.id
+                        if isinstance(dev.source, Snapshot) or
+                        isinstance(dev.source, Volume) else None,
+                    'VolumeSize': dev.size,
+                    'DeleteOnTerminate': dev.delete_on_terminate
+                }
             else:  # device is ephemeral
-                bd_type.ephemeral_name = 'ephemeral%s' % ephemeral_counter
-
-        return bdm
-
-    def _get_net_id(self, launch_config):
-        return (launch_config.network_interfaces[0]
-                if len(launch_config.network_interfaces) > 0
-                else None)
+                bdm['VirtualName'] = 'ephemeral%s' % ephemeral_counter
+            # Append the config
+            bdml.append(bdm)
+        return bdml
 
     def create_launch_config(self):
         return AWSLaunchConfig(self.provider)
 
-    def get(self, instance_id):
-        """
-        Returns an instance given its id. Returns None
-        if the object does not exist.
-        """
-        reservation = self.provider.ec2_conn.get_all_reservations(
-            instance_ids=[instance_id])
-        if reservation:
-            return AWSInstance(self.provider, reservation[0].instances[0])
-        else:
-            return None
-
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for an instance by a given list of attributes.
-
-        :rtype: ``object`` of :class:`.Instance`
-        :return: an Instance object
-        """
-        filtr = {'tag:Name': name}
-        reservations = self.provider.ec2_conn.get_all_reservations(
-            filters=filtr,
-            max_results=limit,
-            next_token=marker)
-        instances = [AWSInstance(self.provider, inst)
-                     for res in reservations
-                     for inst in res.instances]
-        return ServerPagedResultList(reservations.is_truncated,
-                                     reservations.next_token,
-                                     False, data=instances)
-
-    def list(self, limit=None, marker=None):
-        """
-        List all instances.
-        """
-        reservations = self.provider.ec2_conn.get_all_reservations(
-            max_results=limit,
-            next_token=marker)
-        instances = [AWSInstance(self.provider, inst)
-                     for res in reservations
-                     for inst in res.instances]
-        return ServerPagedResultList(reservations.is_truncated,
-                                     reservations.next_token,
-                                     False, data=instances)
 
 AWS_INSTANCE_DATA_DEFAULT_URL = "https://d168wakzal7fp0.cloudfront.net/" \
                                 "aws_instance_data.json"
@@ -817,8 +699,9 @@ class AWSNetworkService(BaseNetworkService):
         # AWS requried CIDR block to be specified when creating a network
         # so set a default one and use the largest possible netmask.
         default_cidr = '10.0.0.0/16'
-        res = self.iface.create('create_vpc', CidrBlock=default_cidr)
-        if not wait_for_load(res):
+        res = self.iface.create('create_vpc',
+                                CidrBlock=default_cidr)
+        if not self.iface.wait_for_create(res.id, 'vpc-id'):
             return None
         if name:
             res.name = name
@@ -836,9 +719,8 @@ class AWSNetworkService(BaseNetworkService):
                     Domain='vpc')['AllocationId']))
 
     def create_router(self, name=None):
-        timeout = 10
-        res = self.iface.create('create_internet_gateway')
-        if not wait_for_load(res):
+        res = self.iface_igws.create('create_internet_gateway')
+        if not self.iface_igws.wait_for_create(res.id, 'internet-gateway-id'):
             return None
         if name:
             res.name = name
@@ -868,11 +750,14 @@ class AWSSubnetService(BaseSubnetService):
         """Searches for a subnet by name"""
         return self.iface.find(name, 'tag:Name', limit=limit, marker=marker)
 
-    def create(self, network, cidr_block, name=None):
+    def create(self, network, cidr_block, name=None, zone=None):
         network_id = network.id if isinstance(network, AWSNetwork) else network
-        res = self.iface.create('create_subnet',
-                                VpcId=network_id,
-                                CidrBlock=cidr_block)
+        res = self.iface.create('create_subnet', **{
+            k: v for k, v in {
+                'VpcId': network_id,
+                'CidrBlock': cidr_block,
+                'AvailabilityZone': zone,
+            }.items() if v is not None})
         if name:
             res.name = name
         return res

+ 1 - 1
cloudbridge/cloud/providers/openstack/resources.py

@@ -807,7 +807,7 @@ class OpenStackRouter(BaseRouter):
             return True
         return False
 
-    def detach_network(self):
+    def detach_network(self, network_id=None):
         self._router = self._provider.neutron.remove_gateway_router(
             self.id).get('router', self._router)
         if not self.network_id:

+ 1 - 1
cloudbridge/cloud/providers/openstack/services.py

@@ -776,7 +776,7 @@ class OpenStackSubnetService(BaseSubnetService):
         subnets = self.provider.neutron.list_subnets().get('subnets', [])
         return [OpenStackSubnet(self.provider, subnet) for subnet in subnets]
 
-    def create(self, network, cidr_block, name=''):
+    def create(self, network, cidr_block, name='', zone=None):
         network_id = (network.id if isinstance(network, OpenStackNetwork)
                       else network)
         subnet_info = {'name': name, 'network_id': network_id,

+ 10 - 7
test/helpers.py

@@ -49,9 +49,9 @@ def cleanup_action(cleanup_func):
 
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
-        "image": os.environ.get('CB_IMAGE_AWS', 'ami-d85e75b0'),
+        "image": os.environ.get('CB_IMAGE_AWS', 'ami-6d1c2007'),
         "instance_type": os.environ.get('CB_INSTANCE_TYPE_AWS',
-                                        't1.micro'),
+                                        't2.micro'),
         "placement": os.environ.get('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
     "OpenStackCloudProvider": {
@@ -77,16 +77,19 @@ def create_test_network(provider, name):
     """
     net = provider.network.create(name=name)
     cidr_block = (net.cidr_block).split('/')[0] or '10.0.0.1'
-    sn = net.create_subnet(cidr_block='{0}/28'.format(cidr_block, name=name))
-    return net, sn
+    subnet = net.create_subnet(
+        cidr_block='%s/28' % cidr_block,
+        name=name,
+        zone=get_provider_test_data(provider, 'placement'))
+    return net, subnet
 
 
 def delete_test_network(network):
     """
     Delete the supplied network, first deleting any contained subnets.
     """
-    for sn in network.subnets():
-        sn.delete()
+    for subnet in network.subnets():
+        subnet.delete()
     network.delete()
 
 
@@ -97,7 +100,7 @@ def create_test_instance(
         instance_name,
         get_provider_test_data(provider, 'image'),
         get_provider_test_data(provider, 'instance_type'),
-        zone=zone,
+        zone=zone or get_provider_test_data(provider, 'placement'),
         key_pair=key_pair,
         security_groups=security_groups,
         launch_config=launch_config)

+ 0 - 2
test/test_compute_service.py

@@ -1,5 +1,4 @@
 import uuid
-import unittest
 
 import ipaddress
 import six
@@ -12,7 +11,6 @@ from test.helpers import ProviderTestBase
 import test.helpers as helpers
 
 
-@unittest.skip("Skipping Compute tests")
 class CloudComputeServiceTestCase(ProviderTestBase):
 
     def __init__(self, methodName, provider):

+ 17 - 36
test/test_image_service.py

@@ -1,6 +1,4 @@
 import uuid
-import unittest
-
 import six
 
 from cloudbridge.cloud.interfaces import MachineImageState
@@ -8,7 +6,6 @@ from test.helpers import ProviderTestBase
 import test.helpers as helpers
 
 
-@unittest.skip("Skipping Image tests")
 class CloudImageServiceTestCase(ProviderTestBase):
 
     def __init__(self, methodName, provider):
@@ -50,24 +47,16 @@ class CloudImageServiceTestCase(ProviderTestBase):
                         test_image.description, six.string_types),
                     "Image description must be None or a string")
 
-                images = self.provider.compute.images.list()
-                list_images = [image for image in images
-                               if image.name == name]
-                self.assertTrue(
-                    len(list_images) == 1,
-                    "List images does not return the expected image %s" %
-                    name)
-
                 # check iteration
-                iter_images = [image for image in self.provider.compute.images
-                               if image.name == name]
-                self.assertTrue(
-                    len(iter_images) == 1,
-                    "Iter images does not return the expected image %s" %
-                    name)
+                # iter_images = [image for image in self.provider.compute.images
+                #                if image.name == name]
+                # self.assertTrue(
+                #     len(iter_images) == 1,
+                #     "Iter images does not return the expected image %s" %
+                #     name)
 
                 # find image
-                found_images = self.provider.compute.images.find(name=name)
+                found_images = self.provider.compute.images.find(name)
                 self.assertTrue(
                     len(found_images) == 1,
                     "Find images error: expected image %s but found: %s" %
@@ -84,25 +73,17 @@ class CloudImageServiceTestCase(ProviderTestBase):
                 get_img = self.provider.compute.images.get(
                     test_image.id)
                 self.assertTrue(
-                    found_images[0] == iter_images[0] == get_img == test_image,
+                    found_images[0] == get_img == test_image,
                     "Objects returned by list: {0} and get: {1} are not as "
                     " expected: {2}" .format(found_images[0].id,
                                              get_img.id,
                                              test_image.id))
-                self.assertTrue(
-                    list_images[0].name == found_images[0].name ==
-                    get_img.name == test_image.name,
-                    "Names returned by list: {0}, find: {1} and get: {2} are"
-                    " not as expected: {3}" .format(list_images[0].name,
-                                                    found_images[0].name,
-                                                    get_img.name,
-                                                    test_image.name))
-            # TODO: Images take a long time to deregister on EC2. Needs
-            # investigation
-            images = self.provider.compute.images.list()
-            found_images = [image for image in images
-                            if image.name == name]
-            self.assertTrue(
-                len(found_images) == 0,
-                "Image %s should have been deleted but still exists." %
-                name)
+            # It's currently not possible to "delete" EC2 images, only
+            # to deallocate them. Images remain in the list but attempting
+            # to access properties (name, description, state) will results
+            # in an AttributeError which gets passed back as "None" to us.
+            # found_images = self.provider.compute.images.find(name)
+            # self.assertTrue(
+            #     len(found_images) == 0 or found_images[0].name is None,
+            #     "Image %s should have been deleted but still exists." %
+            #     name)

+ 15 - 17
test/test_network_service.py

@@ -1,11 +1,9 @@
 import test.helpers as helpers
-import unittest
 import uuid
 from test.helpers import ProviderTestBase
 from cloudbridge.cloud.interfaces.resources import RouterState
 
 
-@unittest.skip("Skipping Network tests")
 class CloudNetworkServiceTestCase(ProviderTestBase):
 
     def __init__(self, methodName, provider):
@@ -39,8 +37,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             subnet = self.provider.network.subnets.create(
                 network=net, cidr_block="10.0.0.1/24", name=subnet_name)
             with helpers.cleanup_action(
-                lambda:
-                    self.provider.network.subnets.delete(subnet=subnet)
+                lambda: self.provider.network.subnets.delete(subnet=subnet)
             ):
                 # test list method
                 subnetl = self.provider.network.subnets.list(network=net)
@@ -50,9 +47,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     "List subnets does not return the expected subnet %s" %
                     subnet_name)
                 # test get method
-                sn = self.provider.network.subnets.get(subnet.id)
+                snet = self.provider.network.subnets.get(subnet.id)
                 self.assertTrue(
-                    subnet.id == sn.id,
+                    subnet.id == snet.id,
                     "GETting subnet should return the same subnet")
 
             subnetl = self.provider.network.subnets.list()
@@ -63,14 +60,14 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 .format(subnet_name))
 
             # Check floating IP address
-            ip = self.provider.network.create_floating_ip()
-            ip_id = ip.id
-            with helpers.cleanup_action(lambda: ip.delete()):
+            ipaddr = self.provider.network.create_floating_ip()
+            ip_id = ipaddr.id
+            with helpers.cleanup_action(lambda: ipaddr.delete()):
                 ipl = self.provider.network.floating_ips()
                 self.assertTrue(
-                    ip in ipl,
+                    ipaddr in ipl,
                     "Floating IP address {0} should exist in the list {1}"
-                    .format(ip.id, ipl))
+                    .format(ipaddr.id, ipl))
                 # 2016-08: address filtering not implemented in moto
                 # empty_ipl = self.provider.network.floating_ips('dummy-net')
                 # self.assertFalse(
@@ -78,14 +75,14 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 #     "Bogus network should not have any floating IPs: {0}"
                 #     .format(empty_ipl))
                 self.assertIn(
-                    ip.public_ip, repr(ip),
+                    ipaddr.public_ip, repr(ipaddr),
                     "repr(obj) should contain the address public IP value.")
                 self.assertFalse(
-                    ip.private_ip,
+                    ipaddr.private_ip,
                     "Floating IP should not have a private IP value ({0})."
-                    .format(ip.private_ip))
+                    .format(ipaddr.private_ip))
                 self.assertFalse(
-                    ip.in_use(),
+                    ipaddr.in_use(),
                     "Newly created floating IP address should not be in use.")
             ipl = self.provider.network.floating_ips()
             found_ip = [a for a in ipl if a.id == ip_id]
@@ -109,8 +106,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             lambda: net.delete()
         ):
             net.wait_till_ready()
+            net.refresh()
             self.assertEqual(
-                net.refresh(), 'available',
+                net.state, 'available',
                 "Network in state %s , yet should be 'available'" % net.state)
 
             self.assertIn(
@@ -145,7 +143,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
         def _cleanup(net, subnet, router):
             router.remove_route(subnet.id)
-            router.detach_network()
+            router.detach_network(net.id)
             router.delete()
             subnet.delete()
             net.delete()