Ehsan Chiniforooshan 9 лет назад
Родитель
Сommit
adb15591ef

+ 3 - 0
.codeclimate.yml

@@ -44,6 +44,9 @@ engines:
     - 39ecd11c13fc7eaead1ba87449b43e00
     - 39ecd11c13fc7eaead1ba87449b43e00
     - 315c7e088d37fe4ce0e06836bf5ac0fc
     - 315c7e088d37fe4ce0e06836bf5ac0fc
     - 34f0d22f0a660fa3d387db57c93ae142
     - 34f0d22f0a660fa3d387db57c93ae142
+    - cbea9efa808e14c1dfa325d40cef496d
+    - f49f7714146a22266988d48fe07b7768
+    - 77f9e287b9d3fec73d1f36d81df46942
     config:
     config:
       languages:
       languages:
       - ruby
       - ruby

+ 11 - 0
CHANGELOG.rst

@@ -1,3 +1,14 @@
+0.1.1 - Aug 10, 2016.
+-------
+
+* For AWS, always launch instances into private networking (i.e., VPC)
+* Support for using OpenStack Keystone v3
+* Add functionality to manipulate routers and routes
+* Add FloatingIP resource type and integrate with Network service
+* Numerous documentation updates
+* For an OpenStack provider, add method to get the ec2 credentials for a user
+
+
 0.1.0 - Jan 30, 2016.
 0.1.0 - Jan 30, 2016.
 -------
 -------
 
 

+ 2 - 2
cloudbridge/__init__.py

@@ -2,12 +2,12 @@ import logging
 import sys
 import sys
 
 
 # Current version of the library
 # Current version of the library
-__version__ = '0.1.0'
+__version__ = '0.1.1'
 
 
 
 
 def get_version():
 def get_version():
     """
     """
-    Returns a string with the current version of the library (e.g., "0.1.0")
+    Return a string with the current version of the library (e.g., "0.1.0").
     """
     """
     return __version__
     return __version__
 
 

+ 34 - 0
cloudbridge/cloud/base/resources.py

@@ -20,12 +20,14 @@ from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import Region
 from cloudbridge.cloud.interfaces.resources import Region
+from cloudbridge.cloud.interfaces.resources import Router
 from cloudbridge.cloud.interfaces.resources import ResultList
 from cloudbridge.cloud.interfaces.resources import ResultList
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
 from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import Subnet
 from cloudbridge.cloud.interfaces.resources import Subnet
+from cloudbridge.cloud.interfaces.resources import FloatingIP
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import WaitStateException
 from cloudbridge.cloud.interfaces.resources import WaitStateException
@@ -676,3 +678,35 @@ class BaseSubnet(Subnet, BaseCloudResource):
                 # pylint:disable=protected-access
                 # pylint:disable=protected-access
                 self._provider == other._provider and
                 self._provider == other._provider and
                 self.id == other.id)
                 self.id == other.id)
+
+
+class BaseFloatingIP(FloatingIP, BaseCloudResource):
+
+    def __init__(self, provider):
+        super(BaseFloatingIP, self).__init__(provider)
+
+    def __repr__(self):
+        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
+                                            self.id, self.public_ip)
+
+    def __eq__(self, other):
+        return (isinstance(other, FloatingIP) and
+                # pylint:disable=protected-access
+                self._provider == other._provider and
+                self.id == other.id)
+
+
+class BaseRouter(Router, BaseCloudResource):
+
+    def __init__(self, provider):
+        super(BaseRouter, self).__init__(provider)
+
+    def __repr__(self):
+        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,
+                                            self.name)
+
+    def __eq__(self, other):
+        return (isinstance(other, Router) and
+                # pylint:disable=protected-access
+                self._provider == other._provider and
+                self.id == other.id)

+ 307 - 102
cloudbridge/cloud/interfaces/resources.py

@@ -160,7 +160,7 @@ class ObjectLifeCycleMixin(object):
         Obtain the provider associated with this object. Used internally
         Obtain the provider associated with this object. Used internally
         to access the provider config and get default timeouts/intervals.
         to access the provider config and get default timeouts/intervals.
 
 
-        :rtype: :class:``.CloudProvider``
+        :rtype: :class:`.CloudProvider`
         :return: The provider associated with this Resource
         :return: The provider associated with this Resource
         """
         """
         pass
         pass
@@ -208,16 +208,17 @@ class ObjectLifeCycleMixin(object):
 
 
         :type terminal_states: ``list`` of states
         :type terminal_states: ``list`` of states
         :param terminal_states: A list of terminal states after which the
         :param terminal_states: A list of terminal states after which the
-        object will not transition into a target state. A WaitStateException
-        will be raised if the object transition into a terminal state.
+                                object will not transition into a target state.
+                                A WaitStateException will be raised if the
+                                object transition into a terminal state.
 
 
-        :type timeout: int
+        :type timeout: ``int``
         :param timeout: The maximum length of time (in seconds) to wait for the
         :param timeout: The maximum length of time (in seconds) to wait for the
                         object to changed to desired state. If no timeout is
                         object to changed to desired state. If no timeout is
                         specified, the global default_wait_timeout defined in
                         specified, the global default_wait_timeout defined in
                         the provider config will apply.
                         the provider config will apply.
 
 
-        :type interval: int
+        :type interval: ``int``
         :param interval: How frequently to poll the object's state (in
         :param interval: How frequently to poll the object's state (in
                          seconds). If no interval is specified, the global
                          seconds). If no interval is specified, the global
                          default_wait_interval defined in the provider config
                          default_wait_interval defined in the provider config
@@ -240,11 +241,11 @@ class ObjectLifeCycleMixin(object):
         Will throw a ``WaitStateException`` if the object is not ready within
         Will throw a ``WaitStateException`` if the object is not ready within
         the specified timeout.
         the specified timeout.
 
 
-        :type timeout: int
+        :type timeout: ``int``
         :param timeout: The maximum length of time (in seconds) to wait for the
         :param timeout: The maximum length of time (in seconds) to wait for the
                         object to become ready.
                         object to become ready.
 
 
-        :type interval: int
+        :type interval: ``int``
         :param interval: How frequently to poll the object's ready state (in
         :param interval: How frequently to poll the object's ready state (in
                          seconds).
                          seconds).
 
 
@@ -434,7 +435,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the instance identifier.
         Get the instance identifier.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: ID for this instance as returned by the cloud middleware.
         :return: ID for this instance as returned by the cloud middleware.
         """
         """
         pass
         pass
@@ -444,7 +445,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the instance name.
         Get the instance name.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name for this instance as returned by the cloud middleware.
         :return: Name for this instance as returned by the cloud middleware.
         """
         """
         pass
         pass
@@ -462,7 +463,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get all the public IP addresses for this instance.
         Get all the public IP addresses for this instance.
 
 
-        :rtype: list
+        :rtype: ``list``
         :return: A list of public IP addresses associated with this instance.
         :return: A list of public IP addresses associated with this instance.
         """
         """
         pass
         pass
@@ -472,7 +473,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get all the private IP addresses for this instance.
         Get all the private IP addresses for this instance.
 
 
-        :rtype: list
+        :rtype: ``list``
         :return: A list of private IP addresses associated with this instance.
         :return: A list of private IP addresses associated with this instance.
         """
         """
         pass
         pass
@@ -485,7 +486,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         UUID. To get the full :class:``.InstanceType``
         UUID. To get the full :class:``.InstanceType``
         object, you can use the instance.instance_type property instead.
         object, you can use the instance.instance_type property instead.
 
 
-        :rtype: :class:``str``
+        :rtype: ``str``
         :return: Instance type name for this instance (e.g., ``m1.large``)
         :return: Instance type name for this instance (e.g., ``m1.large``)
         """
         """
         pass
         pass
@@ -495,7 +496,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Retrieve full instance type information for this instance.
         Retrieve full instance type information for this instance.
 
 
-        :rtype: :class:``.InstanceType``
+        :rtype: :class:`.InstanceType`
         :return: Instance type for this instance
         :return: Instance type for this instance
         """
         """
         pass
         pass
@@ -505,7 +506,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Reboot this instance (using the cloud middleware API).
         Reboot this instance (using the cloud middleware API).
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if the reboot was successful; ``False`` otherwise.
         :return: ``True`` if the reboot was successful; ``False`` otherwise.
         """
         """
         pass
         pass
@@ -515,7 +516,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Permanently terminate this instance.
         Permanently terminate this instance.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if the termination of the instance was successfully
         :return: ``True`` if the termination of the instance was successfully
                  initiated; ``False`` otherwise.
                  initiated; ``False`` otherwise.
         """
         """
@@ -526,7 +527,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the image ID for this instance.
         Get the image ID for this instance.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Image ID (i.e., AMI) this instance is using.
         :return: Image ID (i.e., AMI) this instance is using.
         """
         """
         pass
         pass
@@ -536,7 +537,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the placement zone ID where this instance is running.
         Get the placement zone ID where this instance is running.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Region/zone/placement where this instance is running.
         :return: Region/zone/placement where this instance is running.
         """
         """
         pass
         pass
@@ -556,7 +557,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the security groups associated with this instance.
         Get the security groups associated with this instance.
 
 
-        :rtype: list or :class:``SecurityGroup`` objects
+        :rtype: list or :class:`.SecurityGroup` objects
         :return: A list of SecurityGroup objects associated with this instance.
         :return: A list of SecurityGroup objects associated with this instance.
         """
         """
         pass
         pass
@@ -566,7 +567,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the IDs of the security groups associated with this instance.
         Get the IDs of the security groups associated with this instance.
 
 
-        :rtype: list or :class:``str`
+        :rtype: list or :class:``str``
         :return: A list of the SecurityGroup IDs associated with this instance.
         :return: A list of the SecurityGroup IDs associated with this instance.
         """
         """
         pass
         pass
@@ -576,7 +577,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Get the name of the key pair associated with this instance.
         Get the name of the key pair associated with this instance.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of the ssh key pair associated with this instance.
         :return: Name of the ssh key pair associated with this instance.
         """
         """
         pass
         pass
@@ -596,7 +597,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Add a public IP address to this instance.
         Add a public IP address to this instance.
 
 
-        :type ip_address: str
+        :type ip_address: ``str``
         :param ip_address: The IP address to associate with the instance.
         :param ip_address: The IP address to associate with the instance.
         """
         """
         pass
         pass
@@ -606,7 +607,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Remove a public IP address from this instance.
         Remove a public IP address from this instance.
 
 
-        :type ip_address: str
+        :type ip_address: ``str``
         :param ip_address: The IP address to remove from the instance.
         :param ip_address: The IP address to remove from the instance.
         """
         """
         pass
         pass
@@ -753,7 +754,7 @@ class LaunchConfig(object):
             # 2. Add a network ID for use with OpenStack
             # 2. Add a network ID for use with OpenStack
             lc.add_network_interface('5820c766-75fe-4fc6-96ef-798f67623238')
             lc.add_network_interface('5820c766-75fe-4fc6-96ef-798f67623238')
 
 
-        :type net_id: str
+        :type net_id: ``str``
         :param net_id: Network ID to launch an instance into. This is a
         :param net_id: Network ID to launch an instance into. This is a
                        preliminary implementation (pending full private cloud
                        preliminary implementation (pending full private cloud
                        support within CloudBridge) so native network IDs need
                        support within CloudBridge) so native network IDs need
@@ -856,6 +857,16 @@ class Network(CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractproperty
+    def external(self):
+        """
+        A flag to indicate if this network is capable of Internet-connectivity.
+
+        :rtype: ``bool``
+        :return: ``True`` if the network can be connected to the Internet.
+        """
+        pass
+
     @abstractproperty
     @abstractproperty
     def state(self):
     def state(self):
         """
         """
@@ -976,6 +987,196 @@ class Subnet(CloudResource):
         pass
         pass
 
 
 
 
+class FloatingIP(CloudResource):
+    """
+    Represents a floating (i.e., static) IP address.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractproperty
+    def id(self):
+        """
+        Get the address identifier.
+
+        :rtype: ``str``
+        :return: ID for this network. Will generally correspond to the cloud
+                 middleware's ID, but should be treated as an opaque value.
+        """
+        pass
+
+    @abstractproperty
+    def public_ip(self):
+        """
+        Public IP address.
+
+        :rtype: ``str``
+        :return: IP address.
+        """
+        pass
+
+    @abstractproperty
+    def private_ip(self):
+        """
+        Private IP address this address is attached to.
+
+        :rtype: ``str``
+        :return: IP address or ``None``.
+        """
+        pass
+
+    @abstractmethod
+    def in_use(self):
+        """
+        Whether the address is in use or not.
+
+        :rtype: ``bool``
+        :return: ``True`` if the address is attached to an instance.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self):
+        """
+        Delete this address.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+
+class RouterState(object):
+
+    """
+    Standard states for a router.
+
+    :cvar UNKNOWN: Router state unknown.
+    :cvar ATTACHED: Router is attached to a network and should be operational.
+    :cvar DETACHED: Router is detached from a network.
+
+    """
+    UNKNOWN = "unknown"
+    ATTACHED = "attached"
+    DETACHED = "detached"
+
+
+class Router(CloudResource):
+    """
+    Represents a private network router.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractproperty
+    def id(self):
+        """
+        Get the router identifier.
+
+        :rtype: ``str``
+        :return: ID for this router. Will generally correspond to the cloud
+                 middleware's ID, but should be treated as an opaque value.
+        """
+        pass
+
+    @abstractproperty
+    def name(self):
+        """
+        Get the router name, if available.
+
+        :rtype: ``str``
+        :return: Name for this router.
+        """
+        pass
+
+    @abstractmethod
+    def refresh(self):
+        """
+        Update this object.
+        """
+        pass
+
+    @abstractproperty
+    def state(self):
+        """
+        Router state: attached or detached to a network.
+
+        :rtype: ``str``
+        :return: ``attached`` or ``detached``.
+        """
+        pass
+
+    @abstractproperty
+    def network_id(self):
+        """
+        ID of the network to which the router is attached.
+
+        :rtype: ``str``
+        :return: ID for the attached network or ``None``.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self):
+        """
+        Delete this router.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+    @abstractmethod
+    def attach_network(self, network_id):
+        """
+        Attach this router to a network.
+
+        :type network_id: ``str``
+        :param network_id: The ID of a network to which to attach this router.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+    @abstractmethod
+    def detach_network(self):
+        """
+        Detach this router from a network.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+    @abstractmethod
+    def add_route(self, subnet_id):
+        """
+        Add a route to this router.
+
+        Note that a router must be attached to a network (to which the supplied
+        subnet belongs to) before a route can be added.
+
+        :type subnet_id: ``str``
+        :param subnet_id: The ID of a subnet to add to this router.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+    @abstractmethod
+    def remove_route(self, subnet_id):
+        """
+        Remove a route from this router.
+
+        :type subnet_id: ``str``
+        :param subnet_id: The ID of a subnet to remove to this router.
+
+        :rtype: ``bool``
+        :return: ``True`` if successful.
+        """
+        pass
+
+
 class AttachmentInfo(object):
 class AttachmentInfo(object):
     """
     """
     Contains attachment information for a volume.
     Contains attachment information for a volume.
@@ -1076,7 +1277,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
 
 
         :rtype: ``str``
         :rtype: ``str``
         :return: Description for this volume as returned by the cloud
         :return: Description for this volume as returned by the cloud
-        middleware.
+                 middleware.
         """
         """
         pass
         pass
 
 
@@ -1108,7 +1309,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
 
 
         :rtype: ``DateTime``
         :rtype: ``DateTime``
         :return: Creation time for this volume as returned by the cloud
         :return: Creation time for this volume as returned by the cloud
-        middleware.
+                 middleware.
         """
         """
         pass
         pass
 
 
@@ -1119,7 +1320,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
 
 
         :rtype: ``str``
         :rtype: ``str``
         :return: PlacementZone for this volume as returned by the cloud
         :return: PlacementZone for this volume as returned by the cloud
-        middleware.
+                 middleware.
         """
         """
         pass
         pass
 
 
@@ -1129,9 +1330,9 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         If available, get the source that this volume is based on (can be
         If available, get the source that this volume is based on (can be
         a Snapshot or an Image). Returns None if no source.
         a Snapshot or an Image). Returns None if no source.
 
 
-        :rtype: ``Snapshot`` or ``Image``
+        :rtype: ``Snapshot` or ``Image``
         :return: Snapshot or Image source for this volume as returned by the
         :return: Snapshot or Image source for this volume as returned by the
-        cloud middleware.
+                 cloud middleware.
         """
         """
         pass
         pass
 
 
@@ -1150,15 +1351,15 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Attach this volume to an instance.
         Attach this volume to an instance.
 
 
-        :type instance: str or :class:``.Instance`` object
+        :type instance: ``str`` or :class:`.Instance` object
         :param instance: The ID of an instance or an ``Instance`` object to
         :param instance: The ID of an instance or an ``Instance`` object to
                          which this volume will be attached.
                          which this volume will be attached.
 
 
-        :type device: str
+        :type device: ``str``
         :param device: The device on the instance through which the
         :param device: The device on the instance through which the
                        volume will be exposed (e.g. /dev/sdh).
                        volume will be exposed (e.g. /dev/sdh).
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1168,7 +1369,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Detach this volume from an instance.
         Detach this volume from an instance.
 
 
-        :type force: bool
+        :type force: ``bool``
         :param force: Forces detachment if the previous detachment attempt
         :param force: Forces detachment if the previous detachment attempt
                       did not occur cleanly. This option is supported on select
                       did not occur cleanly. This option is supported on select
                       clouds only. This option can lead to data loss or a
                       clouds only. This option can lead to data loss or a
@@ -1179,7 +1380,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
                       use this option, you must perform file system check and
                       use this option, you must perform file system check and
                       repair procedures.
                       repair procedures.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1189,14 +1390,14 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Create a snapshot of this Volume.
         Create a snapshot of this Volume.
 
 
-        :type name: str
+        :type name: ``str``
         :param name: The name of this snapshot.
         :param name: The name of this snapshot.
 
 
-        :type description: str
+        :type description: ``str``
         :param description: A description of the snapshot.
         :param description: A description of the snapshot.
                             Limited to 256 characters.
                             Limited to 256 characters.
 
 
-        :rtype: :class:``.Snapshot``
+        :rtype: :class:`.Snapshot`
         :return: The created Snapshot object.
         :return: The created Snapshot object.
         """
         """
         pass
         pass
@@ -1206,7 +1407,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Delete this volume.
         Delete this volume.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1257,7 +1458,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
     @abstractmethod
     @abstractmethod
     def name(self, value):
     def name(self, value):
         """
         """
-        set the snapshot name.
+        Set the snapshot name.
         """
         """
         pass
         pass
 
 
@@ -1269,7 +1470,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
 
 
         :rtype: ``str``
         :rtype: ``str``
         :return: Description for this snapshot as returned by the cloud
         :return: Description for this snapshot as returned by the cloud
-        middleware.
+                 middleware.
         """
         """
         pass
         pass
 
 
@@ -1277,10 +1478,14 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
     @abstractmethod
     @abstractmethod
     def description(self, value):
     def description(self, value):
         """
         """
-        Set the snapshot description. Some cloud providers may not support this
-        property, and setting the description may have no effect. (Providers
-        that do not support this property will always return the snapshot name
-        as the description).
+        Set the snapshot description.
+
+        Some cloud providers may not support this property, and setting the
+        description may have no effect (providers that do not support this
+        property will always return the snapshot name as the description).
+
+        :type value: ``str``
+        :param value: The value for the snapshot description.
         """
         """
         pass
         pass
 
 
@@ -1312,7 +1517,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
 
 
         :rtype: ``DateTime``
         :rtype: ``DateTime``
         :return: Creation time for this snapshot as returned by the cloud
         :return: Creation time for this snapshot as returned by the cloud
-        middleware.
+                 middleware.
         """
         """
         pass
         pass
 
 
@@ -1321,18 +1526,18 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Create a new Volume from this Snapshot.
         Create a new Volume from this Snapshot.
 
 
-        :type placement: str
+        :type placement: ``str``
         :param placement: The availability zone where to create the Volume.
         :param placement: The availability zone where to create the Volume.
 
 
-        :type size: int
+        :type size: ``int``
         :param size: The size of the new volume, in GiB (optional). Defaults to
         :param size: The size of the new volume, in GiB (optional). Defaults to
                      the size of the snapshot.
                      the size of the snapshot.
 
 
-        :type volume_type: str
+        :type volume_type: ``str``
         :param volume_type: The type of the volume (optional). Availability and
         :param volume_type: The type of the volume (optional). Availability and
                             valid values depend on the provider.
                             valid values depend on the provider.
 
 
-        :type iops: int
+        :type iops: ``int``
         :param iops: The provisioned IOPs you want to associate with
         :param iops: The provisioned IOPs you want to associate with
                      this volume (optional). Availability depends on the
                      this volume (optional). Availability depends on the
                      provider.
                      provider.
@@ -1375,7 +1580,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
         """
         """
         Delete this snapshot.
         Delete this snapshot.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1401,7 +1606,7 @@ class KeyPair(CloudResource):
         """
         """
         Return the name of this key pair.
         Return the name of this key pair.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: A name of this ssh key pair.
         :return: A name of this ssh key pair.
         """
         """
         pass
         pass
@@ -1411,7 +1616,7 @@ class KeyPair(CloudResource):
         """
         """
         Unencrypted private key.
         Unencrypted private key.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Unencrypted private key or ``None`` if not available.
         :return: Unencrypted private key or ``None`` if not available.
         """
         """
         pass
         pass
@@ -1421,7 +1626,7 @@ class KeyPair(CloudResource):
         """
         """
         Delete this key pair.
         Delete this key pair.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1440,7 +1645,7 @@ class Region(CloudResource):
         """
         """
         The id for this region
         The id for this region
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: ID of the region.
         :return: ID of the region.
         """
         """
         pass
         pass
@@ -1450,7 +1655,7 @@ class Region(CloudResource):
         """
         """
         Name of the region.
         Name of the region.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of the region.
         :return: Name of the region.
         """
         """
         pass
         pass
@@ -1460,7 +1665,7 @@ class Region(CloudResource):
         """
         """
         Access information about placement zones within this region.
         Access information about placement zones within this region.
 
 
-        :rtype: iterable
+        :rtype: Iterable
         :return: Iterable of available placement zones in this region.
         :return: Iterable of available placement zones in this region.
         """
         """
         pass
         pass
@@ -1478,7 +1683,7 @@ class PlacementZone(CloudResource):
         """
         """
         Name of the placement zone.
         Name of the placement zone.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of the placement zone.
         :return: Name of the placement zone.
         """
         """
         pass
         pass
@@ -1488,7 +1693,7 @@ class PlacementZone(CloudResource):
         """
         """
         Name of the placement zone.
         Name of the placement zone.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of the placement zone.
         :return: Name of the placement zone.
         """
         """
         pass
         pass
@@ -1498,7 +1703,7 @@ class PlacementZone(CloudResource):
         """
         """
         A region this placement zone is associated with.
         A region this placement zone is associated with.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: The name of the region the zone is associated with.
         :return: The name of the region the zone is associated with.
         """
         """
         pass
         pass
@@ -1527,7 +1732,7 @@ class InstanceType(CloudResource):
         For example, General Purpose Instances or High-Memory Instances. If
         For example, General Purpose Instances or High-Memory Instances. If
         the provider does not support such a grouping, it may return ``None``.
         the provider does not support such a grouping, it may return ``None``.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of the instance family or ``None``.
         :return: Name of the instance family or ``None``.
         """
         """
         pass
         pass
@@ -1537,7 +1742,7 @@ class InstanceType(CloudResource):
         """
         """
         The number of VCPUs supported by this instance type.
         The number of VCPUs supported by this instance type.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Number of VCPUs.
         :return: Number of VCPUs.
         """
         """
         pass
         pass
@@ -1547,7 +1752,7 @@ class InstanceType(CloudResource):
         """
         """
         The amount of RAM (in MB) supported by this instance type.
         The amount of RAM (in MB) supported by this instance type.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Total RAM (in MB).
         :return: Total RAM (in MB).
         """
         """
         pass
         pass
@@ -1557,7 +1762,7 @@ class InstanceType(CloudResource):
         """
         """
         The size of this instance types's root disk (in GB).
         The size of this instance types's root disk (in GB).
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Size of root disk (in GB).
         :return: Size of root disk (in GB).
         """
         """
         pass
         pass
@@ -1567,7 +1772,7 @@ class InstanceType(CloudResource):
         """
         """
         The size of this instance types's total ephemeral storage (in GB).
         The size of this instance types's total ephemeral storage (in GB).
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Size of ephemeral disks (in GB).
         :return: Size of ephemeral disks (in GB).
         """
         """
         pass
         pass
@@ -1577,7 +1782,7 @@ class InstanceType(CloudResource):
         """
         """
         The total number of ephemeral disks on this instance type.
         The total number of ephemeral disks on this instance type.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Number of ephemeral disks available.
         :return: Number of ephemeral disks available.
         """
         """
         pass
         pass
@@ -1588,7 +1793,7 @@ class InstanceType(CloudResource):
         The total disk space available on this instance type
         The total disk space available on this instance type
         (root_disk + ephemeral).
         (root_disk + ephemeral).
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Size of total disk space (in GB).
         :return: Size of total disk space (in GB).
         """
         """
         pass
         pass
@@ -1599,7 +1804,7 @@ class InstanceType(CloudResource):
         A dictionary of extra data about this instance. May contain
         A dictionary of extra data about this instance. May contain
         nested dictionaries, but all key value pairs are strings or integers.
         nested dictionaries, but all key value pairs are strings or integers.
 
 
-        :rtype: dict
+        :rtype: ``dict``
         :return: Extra attributes for this instance type.
         :return: Extra attributes for this instance type.
         """
         """
         pass
         pass
@@ -1614,7 +1819,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Get the ID of this security group.
         Get the ID of this security group.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Security group ID.
         :return: Security group ID.
         """
         """
         pass
         pass
@@ -1624,7 +1829,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Return the name of this security group.
         Return the name of this security group.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: A name of this security group.
         :return: A name of this security group.
         """
         """
         pass
         pass
@@ -1634,7 +1839,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Return the description of this security group.
         Return the description of this security group.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: A description of this security group.
         :return: A description of this security group.
         """
         """
         pass
         pass
@@ -1644,7 +1849,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Get the list of rules for this security group.
         Get the list of rules for this security group.
 
 
-        :rtype: list of :class:``.SecurityGroupRule``
+        :rtype: list of :class:`.SecurityGroupRule`
         :return: A list of security group rule objects.
         :return: A list of security group rule objects.
         """
         """
         pass
         pass
@@ -1654,7 +1859,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Delete this security group.
         Delete this security group.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1663,29 +1868,30 @@ class SecurityGroup(CloudResource):
     def add_rule(self, ip_protocol=None, from_port=None, to_port=None,
     def add_rule(self, ip_protocol=None, from_port=None, to_port=None,
                  cidr_ip=None, src_group=None):
                  cidr_ip=None, src_group=None):
         """
         """
-        Create a security group rule.
+        Create a security group rule. If the rule already exists, simply
+        returns it.
 
 
         You need to pass in either ``src_group`` OR ``ip_protocol``,
         You need to pass in either ``src_group`` OR ``ip_protocol``,
         ``from_port``, ``to_port``, and ``cidr_ip``. In other words, either
         ``from_port``, ``to_port``, and ``cidr_ip``. In other words, either
         you are authorizing another group or you are authorizing some
         you are authorizing another group or you are authorizing some
         ip-based rule.
         ip-based rule.
 
 
-        :type ip_protocol: str
+        :type ip_protocol: ``str``
         :param ip_protocol: Either ``tcp`` | ``udp`` | ``icmp``.
         :param ip_protocol: Either ``tcp`` | ``udp`` | ``icmp``.
 
 
-        :type from_port: int
+        :type from_port: ``int``
         :param from_port: The beginning port number you are enabling.
         :param from_port: The beginning port number you are enabling.
 
 
-        :type to_port: int
+        :type to_port: ``int``
         :param to_port: The ending port number you are enabling.
         :param to_port: The ending port number you are enabling.
 
 
-        :type cidr_ip: str or list of strings
+        :type cidr_ip: ``str`` or list of ``str``
         :param cidr_ip: The CIDR block you are providing access to.
         :param cidr_ip: The CIDR block you are providing access to.
 
 
-        :type src_group: :class:``.SecurityGroup``
+        :type src_group: :class:`.SecurityGroup`
         :param src_group: The Security Group object you are granting access to.
         :param src_group: The Security Group object you are granting access to.
 
 
-        :rtype: :class:``.SecurityGroupRule``
+        :rtype: :class:`.SecurityGroupRule`
         :return: Rule object if successful or ``None``.
         :return: Rule object if successful or ``None``.
         """
         """
         pass
         pass
@@ -1701,22 +1907,22 @@ class SecurityGroup(CloudResource):
         several rules exist for the group rule. In that case, use the
         several rules exist for the group rule. In that case, use the
         ``.rules`` property and filter the results as desired.
         ``.rules`` property and filter the results as desired.
 
 
-        :type ip_protocol: str
+        :type ip_protocol: ``str``
         :param ip_protocol: Either ``tcp`` | ``udp`` | ``icmp``.
         :param ip_protocol: Either ``tcp`` | ``udp`` | ``icmp``.
 
 
-        :type from_port: int
+        :type from_port: ``int``
         :param from_port: The beginning port number you are enabling.
         :param from_port: The beginning port number you are enabling.
 
 
-        :type to_port: int
+        :type to_port: ``int``
         :param to_port: The ending port number you are enabling.
         :param to_port: The ending port number you are enabling.
 
 
-        :type cidr_ip: str or list of strings
+        :type cidr_ip: ``str`` or list of ``str``
         :param cidr_ip: The CIDR block you are providing access to.
         :param cidr_ip: The CIDR block you are providing access to.
 
 
-        :type src_group: :class:``.SecurityGroup``
+        :type src_group: :class:`.SecurityGroup`
         :param src_group: The Security Group object you are granting access to.
         :param src_group: The Security Group object you are granting access to.
 
 
-        :rtype: :class:``.SecurityGroupRule``
+        :rtype: :class:`.SecurityGroupRule`
         :return: Role object if one can be found or ``None``.
         :return: Role object if one can be found or ``None``.
         """
         """
         pass
         pass
@@ -1737,7 +1943,7 @@ class SecurityGroupRule(CloudResource):
         Note that this may be a CloudBridge-specific ID if the underlying
         Note that this may be a CloudBridge-specific ID if the underlying
         provider does not support rule IDs.
         provider does not support rule IDs.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Role ID.
         :return: Role ID.
         """
         """
         pass
         pass
@@ -1747,7 +1953,7 @@ class SecurityGroupRule(CloudResource):
         """
         """
         IP protocol used. Either ``tcp`` | ``udp`` | ``icmp``.
         IP protocol used. Either ``tcp`` | ``udp`` | ``icmp``.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Active protocol.
         :return: Active protocol.
         """
         """
         pass
         pass
@@ -1757,7 +1963,7 @@ class SecurityGroupRule(CloudResource):
         """
         """
         Lowest port number opened as part of this rule.
         Lowest port number opened as part of this rule.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Lowest port number or 0 if not set.
         :return: Lowest port number or 0 if not set.
         """
         """
         pass
         pass
@@ -1767,7 +1973,7 @@ class SecurityGroupRule(CloudResource):
         """
         """
         Highest port number opened as part of this rule.
         Highest port number opened as part of this rule.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Highest port number or 0 if not set.
         :return: Highest port number or 0 if not set.
         """
         """
         pass
         pass
@@ -1777,7 +1983,7 @@ class SecurityGroupRule(CloudResource):
         """
         """
         CIDR block this security group is providing access to.
         CIDR block this security group is providing access to.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: CIDR block.
         :return: CIDR block.
         """
         """
         pass
         pass
@@ -1812,7 +2018,7 @@ class BucketObject(CloudResource):
         """
         """
         Get this object's id.
         Get this object's id.
 
 
-        :rtype: id
+        :rtype: ``str``
         :return: id of this object as returned by the cloud middleware.
         :return: id of this object as returned by the cloud middleware.
         """
         """
         pass
         pass
@@ -1822,7 +2028,7 @@ class BucketObject(CloudResource):
         """
         """
         Get this object's name.
         Get this object's name.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of this object as returned by the cloud middleware.
         :return: Name of this object as returned by the cloud middleware.
         """
         """
         pass
         pass
@@ -1832,7 +2038,7 @@ class BucketObject(CloudResource):
         """
         """
         Get this object's size.
         Get this object's size.
 
 
-        :rtype: int
+        :rtype: ``int``
         :return: Size of this object in bytes.
         :return: Size of this object in bytes.
         """
         """
         pass
         pass
@@ -1842,7 +2048,7 @@ class BucketObject(CloudResource):
         """
         """
         Get the date and time this object was last modified.
         Get the date and time this object was last modified.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Date and time formatted string %Y-%m-%dT%H:%M:%S.%f
         :return: Date and time formatted string %Y-%m-%dT%H:%M:%S.%f
         """
         """
         pass
         pass
@@ -1862,7 +2068,6 @@ class BucketObject(CloudResource):
     def save_content(self, target_stream):
     def save_content(self, target_stream):
         """
         """
         Save this object and write its contents to the ``target_stream``.
         Save this object and write its contents to the ``target_stream``.
-
         """
         """
         pass
         pass
 
 
@@ -1872,7 +2077,7 @@ class BucketObject(CloudResource):
         Set the contents of this object to the data read from the source
         Set the contents of this object to the data read from the source
         stream.
         stream.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1882,7 +2087,7 @@ class BucketObject(CloudResource):
         """
         """
         Delete this object.
         Delete this object.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass
@@ -1897,8 +2102,8 @@ class Bucket(PageableObjectMixin, CloudResource):
         """
         """
         Get this bucket's id.
         Get this bucket's id.
 
 
-        :rtype: id
-        :return: id of this bucket as returned by the cloud middleware.
+        :rtype: ``str``
+        :return: ID of this bucket as returned by the cloud middleware.
         """
         """
         pass
         pass
 
 
@@ -1907,7 +2112,7 @@ class Bucket(PageableObjectMixin, CloudResource):
         """
         """
         Get this bucket's name.
         Get this bucket's name.
 
 
-        :rtype: str
+        :rtype: ``str``
         :return: Name of this bucket as returned by the cloud middleware.
         :return: Name of this bucket as returned by the cloud middleware.
         """
         """
         pass
         pass
@@ -1917,7 +2122,7 @@ class Bucket(PageableObjectMixin, CloudResource):
         """
         """
         Retrieve a given object from this bucket.
         Retrieve a given object from this bucket.
 
 
-        :type key: str
+        :type key: ``str``
         :param key: the identifier of the object to retrieve
         :param key: the identifier of the object to retrieve
 
 
         :rtype: :class:``.BucketObject``
         :rtype: :class:``.BucketObject``
@@ -1940,11 +2145,11 @@ class Bucket(PageableObjectMixin, CloudResource):
         """
         """
         Delete this bucket.
         Delete this bucket.
 
 
-        :type delete_contents: bool
+        :type delete_contents: ``bool``
         :param delete_contents: If ``True``, all objects within the bucket
         :param delete_contents: If ``True``, all objects within the bucket
                                 will be deleted.
                                 will be deleted.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: ``True`` if successful.
         :return: ``True`` if successful.
         """
         """
         pass
         pass

+ 65 - 5
cloudbridge/cloud/interfaces/services.py

@@ -2,6 +2,7 @@
 Specifications for services available through a provider
 Specifications for services available through a provider
 """
 """
 from abc import ABCMeta, abstractmethod, abstractproperty
 from abc import ABCMeta, abstractmethod, abstractproperty
+
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 
 
 
 
@@ -489,7 +490,7 @@ class NetworkService(PageableObjectMixin, CloudService):
         :param network_id: The ID of the network to retrieve.
         :param network_id: The ID of the network to retrieve.
 
 
         :rtype: ``object`` of :class:`.Network`
         :rtype: ``object`` of :class:`.Network`
-        return: a Network object
+        :return: a Network object
         """
         """
         pass
         pass
 
 
@@ -554,6 +555,57 @@ class NetworkService(PageableObjectMixin, CloudService):
         """
         """
         pass
         pass
 
 
+    @abstractmethod
+    def floating_ips(self, network_id=None):
+        """
+        List floating (i.e., static) IP addresses.
+
+        :type network_id: ``str``
+        :param network_id: The ID of the network by which to filter the IPs.
+
+        :rtype: ``list`` of :class:`FloatingIP`
+        :return: list of floating IP objects
+        """
+        pass
+
+    @abstractmethod
+    def create_floating_ip(self):
+        """
+        Allocate a new floating (i.e., static) IP address.
+
+        :type network_id: ``str``
+        :param network_id: The ID of the network with which to associate the
+                           new IP address.
+
+        :rtype: :class:`FloatingIP`
+        :return: floating IP object
+        """
+        pass
+
+    @abstractmethod
+    def routers(self):
+        """
+        Get a list of available routers.
+
+        :rtype: ``list`` of :class: `Router`
+        :return: list of routers
+        """
+        pass
+
+    @abstractmethod
+    def create_router(self, name=None):
+        """
+        Create a new router/gateway.
+
+        :type name: ``str``
+        :param name: An optional router name. The name will be set if the
+                     provider supports it.
+
+        :rtype: :class:`Router`
+        :return: a newly created router object
+        """
+        pass
+
 
 
 class SubnetService(PageableObjectMixin, CloudService):
 class SubnetService(PageableObjectMixin, CloudService):
 
 
@@ -695,6 +747,9 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
         """
         """
         Create a new bucket.
         Create a new bucket.
 
 
+        If a bucket with the specified name already exists, return a reference
+        to that bucket.
+
         Example:
         Example:
 
 
         .. code-block:: python
         .. code-block:: python
@@ -818,13 +873,13 @@ class KeyPairService(PageableObjectMixin, CloudService):
     @abstractmethod
     @abstractmethod
     def create(self, name):
     def create(self, name):
         """
         """
-        Create a new keypair or return an existing one by the same name.
+        Create a new key pair or raise an exception if one already exists.
 
 
         :type name: str
         :type name: str
         :param name: The name of the key pair to be created.
         :param name: The name of the key pair to be created.
 
 
         :rtype: ``object`` of :class:`.KeyPair`
         :rtype: ``object`` of :class:`.KeyPair`
-        :return:  A keypair instance
+        :return:  A keypair instance or ``None``.
         """
         """
         pass
         pass
 
 
@@ -880,7 +935,7 @@ class SecurityGroupService(PageableObjectMixin, CloudService):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, name, description):
+    def create(self, name, description, network_id=None):
         """
         """
         Create a new SecurityGroup.
         Create a new SecurityGroup.
 
 
@@ -890,6 +945,11 @@ class SecurityGroupService(PageableObjectMixin, CloudService):
         :type description: str
         :type description: str
         :param description: The description of the new security group.
         :param description: The description of the new security group.
 
 
+        :type  network_id: ``str``
+        :param network_id: An optional network ID under which to create the
+                           security group that may be supported by some
+                           providers.
+
         :rtype: ``object`` of :class:`.SecurityGroup`
         :rtype: ``object`` of :class:`.SecurityGroup`
         :return:  A SecurityGroup instance or ``None`` if one was not created.
         :return:  A SecurityGroup instance or ``None`` if one was not created.
         """
         """
@@ -905,7 +965,7 @@ class SecurityGroupService(PageableObjectMixin, CloudService):
 
 
         :rtype: list of :class:`SecurityGroup`
         :rtype: list of :class:`SecurityGroup`
         :return: A list of SecurityGroup objects or an empty list if none
         :return: A list of SecurityGroup objects or an empty list if none
-        found.
+                 found.
         """
         """
         pass
         pass
 
 

+ 1 - 8
cloudbridge/cloud/providers/aws/provider.py

@@ -117,9 +117,6 @@ class AWSCloudProvider(BaseCloudProvider):
         ec2_conn = boto.connect_ec2(
         ec2_conn = boto.connect_ec2(
             aws_access_key_id=self.a_key,
             aws_access_key_id=self.a_key,
             aws_secret_access_key=self.s_key,
             aws_secret_access_key=self.s_key,
-            # api_version is needed for availability
-            # zone support for EC2
-            api_version='2012-06-01' if self.cloud_type == 'aws' else None,
             is_secure=self.ec2_is_secure,
             is_secure=self.ec2_is_secure,
             region=region,
             region=region,
             port=self.ec2_port,
             port=self.ec2_port,
@@ -136,9 +133,6 @@ class AWSCloudProvider(BaseCloudProvider):
         vpc_conn = boto.connect_vpc(
         vpc_conn = boto.connect_vpc(
             aws_access_key_id=self.a_key,
             aws_access_key_id=self.a_key,
             aws_secret_access_key=self.s_key,
             aws_secret_access_key=self.s_key,
-            # api_version is needed for availability
-            # zone support for EC2
-            api_version='2012-06-01' if self.cloud_type == 'aws' else None,
             is_secure=self.ec2_is_secure,
             is_secure=self.ec2_is_secure,
             region=r,
             region=r,
             port=self.ec2_port,
             port=self.ec2_port,
@@ -177,8 +171,7 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
         self.s3mock.start()
         self.s3mock.start()
         HTTPretty.register_uri(
         HTTPretty.register_uri(
             method="GET",
             method="GET",
-            uri="https://swift.rc.nectar.org.au:8888/v1/"
-            "AUTH_377/cloud-bridge/aws/instance_data.json",
+            uri="https://d168wakzal7fp0.cloudfront.net/aws_instance_data.json",
             body="""
             body="""
 [
 [
   {
   {

+ 189 - 12
cloudbridge/cloud/providers/aws/resources.py

@@ -7,19 +7,24 @@ from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseKeyPair
+from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import BaseMachineImage
 from cloudbridge.cloud.base.resources import BaseMachineImage
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BaseRegion
 from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 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 SnapshotState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from datetime import datetime
 from datetime import datetime
@@ -335,7 +340,12 @@ class AWSInstance(BaseInstance):
         """
         """
         Add an elastic IP address to this instance.
         Add an elastic IP address to this instance.
         """
         """
-        return self._ec2_instance.use_ip(ip_address)
+        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)
 
 
     def remove_floating_ip(self, ip_address):
     def remove_floating_ip(self, ip_address):
         """
         """
@@ -631,15 +641,27 @@ class AWSSecurityGroup(BaseSecurityGroup):
         :rtype: :class:``.SecurityGroupRule``
         :rtype: :class:``.SecurityGroupRule``
         :return: Rule object if successful or ``None``.
         :return: Rule object if successful or ``None``.
         """
         """
-        if self._security_group.authorize(
-                ip_protocol=ip_protocol,
-                from_port=from_port,
-                to_port=to_port,
-                cidr_ip=cidr_ip,
-                # pylint:disable=protected-access
-                src_group=src_group._security_group if src_group else None):
-            return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
-                                 src_group)
+        try:
+            if not isinstance(src_group, SecurityGroup):
+                src_group = self._provider.security.security_groups.get(
+                    src_group)
+
+            if self._security_group.authorize(
+                    ip_protocol=ip_protocol,
+                    from_port=from_port,
+                    to_port=to_port,
+                    cidr_ip=cidr_ip,
+                    # pylint:disable=protected-access
+                    src_group=src_group._security_group if src_group
+                    else None):
+                return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
+                                     src_group)
+        except EC2ResponseError as ec2e:
+            if ec2e.code == "InvalidPermission.Duplicate":
+                return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
+                                     src_group)
+            else:
+                raise ec2e
         return None
         return None
 
 
     def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
     def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
@@ -758,7 +780,8 @@ class AWSBucketObject(BaseBucketObject):
         """
         """
         Get the date and time this object was last modified.
         Get the date and time this object was last modified.
         """
         """
-        lm = datetime.strptime(self._key.last_modified, "%Y-%m-%dT%H:%M:%S.%fZ")
+        lm = datetime.strptime(self._key.last_modified,
+                               "%Y-%m-%dT%H:%M:%S.%fZ")
         return lm.strftime("%Y-%m-%dT%H:%M:%S.%f")
         return lm.strftime("%Y-%m-%dT%H:%M:%S.%f")
 
 
     def iter_content(self):
     def iter_content(self):
@@ -874,7 +897,7 @@ class AWSNetwork(BaseNetwork):
     # docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html
     # docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html
     _NETWORK_STATE_MAP = {
     _NETWORK_STATE_MAP = {
         'pending': NetworkState.PENDING,
         'pending': NetworkState.PENDING,
-        'available': VolumeState.AVAILABLE,
+        'available': NetworkState.AVAILABLE,
     }
     }
 
 
     def __init__(self, provider, network):
     def __init__(self, provider, network):
@@ -902,6 +925,14 @@ class AWSNetwork(BaseNetwork):
         """
         """
         self._vpc.add_tag('Name', value)
         self._vpc.add_tag('Name', value)
 
 
+    @property
+    def external(self):
+        """
+        For AWS, all VPC networks can be connected to the Internet so always
+        return ``True``.
+        """
+        return True
+
     @property
     @property
     def state(self):
     def state(self):
         return AWSNetwork._NETWORK_STATE_MAP.get(
         return AWSNetwork._NETWORK_STATE_MAP.get(
@@ -971,3 +1002,149 @@ class AWSSubnet(BaseSubnet):
 
 
     def delete(self):
     def delete(self):
         return self._provider.vpc_conn.delete_subnet(subnet_id=self.id)
         return self._provider.vpc_conn.delete_subnet(subnet_id=self.id)
+
+
+class AWSFloatingIP(BaseFloatingIP):
+
+    def __init__(self, provider, floating_ip):
+        super(AWSFloatingIP, self).__init__(provider)
+        self._ip = floating_ip
+
+    @property
+    def id(self):
+        return self._ip.allocation_id
+
+    @property
+    def public_ip(self):
+        return self._ip.public_ip
+
+    @property
+    def private_ip(self):
+        return self._ip.private_ip_address
+
+    def in_use(self):
+        return True if self._ip.instance_id else False
+
+    def delete(self):
+        return self._ip.delete()
+
+
+class AWSRouter(BaseRouter):
+
+    def __init__(self, provider, router):
+        super(AWSRouter, self).__init__(provider)
+        self._router = router
+        self._ROUTE_CIDR = '0.0.0.0/0'
+
+    def _route_table(self, subnet_id):
+        """
+        Get the route table for the VPC to which the supplied subnet belongs.
+
+        Note that only the first route table will be returned in case more
+        exist.
+
+        :type subnet_id: ``str``
+        :param subnet_id: Filter the route table by the network in which the
+                          given subnet belongs.
+
+        :rtype: :class:`boto.vpc.routetable.RouteTable`
+        :return: A RouteTable object.
+        """
+        sn = self._provider.vpc_conn.get_all_subnets([subnet_id])[0]
+        return self._provider.vpc_conn.get_all_route_tables(
+            filters={'vpc-id': sn.vpc_id})[0]
+
+    @property
+    def id(self):
+        return self._router.id
+
+    @property
+    def name(self):
+        """
+        Get the router name.
+
+        .. note:: the router must have a (case sensitive) tag ``Name``
+        """
+        return self._router.tags.get('Name')
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the router name.
+        """
+        self._router.add_tag('Name', value)
+
+    def refresh(self):
+        self._router = self._provider.vpc_conn.get_all_internet_gateways(
+            [self.id])[0]
+
+    @property
+    def state(self):
+        self.refresh()  # Explicitly refresh the local object
+        if self._router.attachments and \
+           self._router.attachments[0].state == 'available':
+            return RouterState.ATTACHED
+        return RouterState.DETACHED
+
+    @property
+    def network_id(self):
+        if self.state == RouterState.ATTACHED:
+            return self._router.attachments[0].vpc_id
+        return None
+
+    def delete(self):
+        return self._provider._vpc_conn.delete_internet_gateway(self.id)
+
+    def attach_network(self, network_id):
+        return self._provider.vpc_conn.attach_internet_gateway(
+            self.id, network_id)
+
+    def detach_network(self):
+        return self._provider.vpc_conn.detach_internet_gateway(
+            self.id, self.network_id)
+
+    def add_route(self, subnet_id):
+        """
+        Add a default route to this router.
+
+        For AWS, routes are added to a route table. A route table is assoc.
+        with a network vs. a subnet so we retrieve the network via the subnet.
+        Note that the subnet must belong to the same network as the router
+        is attached to.
+
+        Further, only a single route can be added, targeting the Internet
+        (i.e., destination CIDR block ``0.0.0.0/0``).
+        """
+        rt = self._route_table(subnet_id)
+        return self._provider.vpc_conn.create_route(
+            rt.id, self._ROUTE_CIDR, self.id)
+
+    def remove_route(self, subnet_id):
+        """
+        Remove the default Internet route from this router.
+
+        .. seealso:: ``add_route`` method
+        """
+        rt = self._route_table(subnet_id)
+        return self._provider.vpc_conn.delete_route(rt.id, self._ROUTE_CIDR)
+
+
+class AWSLaunchConfig(BaseLaunchConfig):
+
+    def __init__(self, provider):
+        super(AWSLaunchConfig, self).__init__(provider)
+
+    def add_network_interface(self, net_id):
+        """
+        Extract a subnet within the network identified by ``net_id``.
+
+        AWS requires a subnet ID to be supplied vs. a network (i.e., VPC) ID
+        so just pull out one subnet within the network (currently, the first
+        one).
+        """
+        net = self.provider.network.get(net_id)
+        sns = net.subnets()
+        if sns:
+            sn = sns[0]
+        self.network_interfaces.append(sn.id)

+ 168 - 26
cloudbridge/cloud/providers/aws/services.py

@@ -1,14 +1,13 @@
 """
 """
 Services implemented by the AWS provider.
 Services implemented by the AWS provider.
 """
 """
+import time
 import string
 import string
 
 
 from boto.ec2.blockdevicemapping import BlockDeviceMapping
 from boto.ec2.blockdevicemapping import BlockDeviceMapping
 from boto.ec2.blockdevicemapping import BlockDeviceType
 from boto.ec2.blockdevicemapping import BlockDeviceType
 from boto.exception import EC2ResponseError
 from boto.exception import EC2ResponseError
-import requests
 
 
-from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.services import BaseBlockStoreService
 from cloudbridge.cloud.base.services import BaseBlockStoreService
@@ -25,9 +24,9 @@ from cloudbridge.cloud.base.services import BaseSecurityService
 from cloudbridge.cloud.base.services import BaseSnapshotService
 from cloudbridge.cloud.base.services import BaseSnapshotService
 from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.base.services import BaseVolumeService
+from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources \
 from cloudbridge.cloud.interfaces.resources \
     import InvalidConfigurationException
     import InvalidConfigurationException
-from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import MachineImage
 # from cloudbridge.cloud.interfaces.resources import Network
 # from cloudbridge.cloud.interfaces.resources import Network
@@ -36,13 +35,18 @@ from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import Volume
 
 
+import requests
+
 from .resources import AWSBucket
 from .resources import AWSBucket
+from .resources import AWSFloatingIP
 from .resources import AWSInstance
 from .resources import AWSInstance
 from .resources import AWSInstanceType
 from .resources import AWSInstanceType
 from .resources import AWSKeyPair
 from .resources import AWSKeyPair
+from .resources import AWSLaunchConfig
 from .resources import AWSMachineImage
 from .resources import AWSMachineImage
 from .resources import AWSNetwork
 from .resources import AWSNetwork
 from .resources import AWSRegion
 from .resources import AWSRegion
+from .resources import AWSRouter
 from .resources import AWSSecurityGroup
 from .resources import AWSSecurityGroup
 from .resources import AWSSnapshot
 from .resources import AWSSnapshot
 from .resources import AWSSubnet
 from .resources import AWSSubnet
@@ -122,7 +126,7 @@ class AWSKeyPairService(BaseKeyPairService):
 
 
     def create(self, name):
     def create(self, name):
         """
         """
-        Create a new key pair or return an existing one by the same name.
+        Create a new key pair or raise an exception if one already exists.
 
 
         :type name: str
         :type name: str
         :param name: The name of the key pair to be created.
         :param name: The name of the key pair to be created.
@@ -130,11 +134,10 @@ class AWSKeyPairService(BaseKeyPairService):
         :rtype: ``object`` of :class:`.KeyPair`
         :rtype: ``object`` of :class:`.KeyPair`
         :return:  A key pair instance or ``None`` if one was not be created.
         :return:  A key pair instance or ``None`` if one was not be created.
         """
         """
-        kp = self.get(name)
-        if kp:
-            return kp
         kp = self.provider.ec2_conn.create_key_pair(name)
         kp = self.provider.ec2_conn.create_key_pair(name)
-        return AWSKeyPair(self.provider, kp)
+        if kp:
+            return AWSKeyPair(self.provider, kp)
+        return None
 
 
 
 
 class AWSSecurityGroupService(BaseSecurityGroupService):
 class AWSSecurityGroupService(BaseSecurityGroupService):
@@ -166,7 +169,7 @@ class AWSSecurityGroupService(BaseSecurityGroupService):
         return ClientPagedResultList(self.provider, sgs,
         return ClientPagedResultList(self.provider, sgs,
                                      limit=limit, marker=marker)
                                      limit=limit, marker=marker)
 
 
-    def create(self, name, description):
+    def create(self, name, description, network_id=None):
         """
         """
         Create a new SecurityGroup.
         Create a new SecurityGroup.
 
 
@@ -176,10 +179,15 @@ class AWSSecurityGroupService(BaseSecurityGroupService):
         :type description: str
         :type description: str
         :param description: The description of the new security group.
         :param description: The description of the new security group.
 
 
+        :type  network_id: ``str``
+        :param network_id: The ID of the VPC to create the security group in,
+                           if any.
+
         :rtype: ``object`` of :class:`.SecurityGroup`
         :rtype: ``object`` of :class:`.SecurityGroup`
         :return:  A SecurityGroup instance or ``None`` if one was not created.
         :return:  A SecurityGroup instance or ``None`` if one was not created.
         """
         """
-        sg = self.provider.ec2_conn.create_security_group(name, description)
+        sg = self.provider.ec2_conn.create_security_group(name, description,
+                                                          network_id)
         if sg:
         if sg:
             return AWSSecurityGroup(self.provider, sg)
             return AWSSecurityGroup(self.provider, sg)
         return None
         return None
@@ -189,8 +197,9 @@ class AWSSecurityGroupService(BaseSecurityGroupService):
         Get all security groups associated with your account.
         Get all security groups associated with your account.
         """
         """
         try:
         try:
+            flters = {'group-name': name}
             security_groups = self.provider.ec2_conn.get_all_security_groups(
             security_groups = self.provider.ec2_conn.get_all_security_groups(
-                groupnames=[name])
+                filters=flters)
         except EC2ResponseError:
         except EC2ResponseError:
             security_groups = []
             security_groups = []
         return [AWSSecurityGroup(self.provider, sg) for sg in security_groups]
         return [AWSSecurityGroup(self.provider, sg) for sg in security_groups]
@@ -462,6 +471,10 @@ class AWSInstanceService(BaseInstanceService):
                **kwargs):
                **kwargs):
         """
         """
         Creates a new virtual machine instance.
         Creates a new virtual machine instance.
+
+        If no VPC/subnet was specified (via ``launch_config`` parameter), this
+        method will search for a default VPC and attempt to launch an instance
+        into that VPC.
         """
         """
         image_id = image.id if isinstance(image, MachineImage) else image
         image_id = image.id if isinstance(image, MachineImage) else image
         instance_size = instance_type.id if \
         instance_size = instance_type.id if \
@@ -470,30 +483,134 @@ class AWSInstanceService(BaseInstanceService):
         key_pair_name = key_pair.name if isinstance(
         key_pair_name = key_pair.name if isinstance(
             key_pair,
             key_pair,
             KeyPair) else key_pair
             KeyPair) else key_pair
-        if security_groups:
-            if isinstance(security_groups, list) and \
-                    isinstance(security_groups[0], SecurityGroup):
-                security_groups_list = [sg.name for sg in security_groups]
-            else:
-                security_groups_list = security_groups
-        else:
-            security_groups_list = None
         if launch_config:
         if launch_config:
             bdm = self._process_block_device_mappings(launch_config, zone_id)
             bdm = self._process_block_device_mappings(launch_config, zone_id)
-            net_id = self._get_net_id(launch_config)
+            subnet_id = self._get_net_id(launch_config)
         else:
         else:
-            bdm = net_id = None
+            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(
         reservation = self.provider.ec2_conn.run_instances(
             image_id=image_id, instance_type=instance_size,
             image_id=image_id, instance_type=instance_size,
             min_count=1, max_count=1, placement=zone_id,
             min_count=1, max_count=1, placement=zone_id,
-            key_name=key_pair_name, security_groups=security_groups_list,
-            user_data=user_data, block_device_map=bdm, subnet_id=net_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:
         if reservation:
+            time.sleep(2)  # The instance does not always get created in time
             instance = AWSInstance(self.provider, reservation.instances[0])
             instance = AWSInstance(self.provider, reservation.instances[0])
             instance.name = name
             instance.name = name
         return instance
         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):
+        """
+        Process security groups to create a list of SG ID's for launching.
+
+        :type security_groups: A ``list`` of ``SecurityGroup`` objects or a
+                               list of ``str`` names
+        :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]
+
+        return sg_ids
+
     def _process_block_device_mappings(self, launch_config, zone=None):
     def _process_block_device_mappings(self, launch_config, zone=None):
         """
         """
         Processes block device mapping information
         Processes block device mapping information
@@ -551,7 +668,7 @@ class AWSInstanceService(BaseInstanceService):
                 else None)
                 else None)
 
 
     def create_launch_config(self):
     def create_launch_config(self):
-        return BaseLaunchConfig(self.provider)
+        return AWSLaunchConfig(self.provider)
 
 
     def get(self, instance_id):
     def get(self, instance_id):
         """
         """
@@ -598,8 +715,8 @@ class AWSInstanceService(BaseInstanceService):
                                      reservations.next_token,
                                      reservations.next_token,
                                      False, data=instances)
                                      False, data=instances)
 
 
-AWS_INSTANCE_DATA_DEFAULT_URL = "https://swift.rc.nectar.org.au:8888/v1/" \
-                                "AUTH_377/cloud-bridge/aws/instance_data.json"
+AWS_INSTANCE_DATA_DEFAULT_URL = "https://d168wakzal7fp0.cloudfront.net/" \
+                                "aws_instance_data.json"
 
 
 
 
 class AWSInstanceTypesService(BaseInstanceTypesService):
 class AWSInstanceTypesService(BaseInstanceTypesService):
@@ -672,6 +789,7 @@ class AWSNetworkService(BaseNetworkService):
         network = self.provider.vpc_conn.create_vpc(cidr_block=default_cidr)
         network = self.provider.vpc_conn.create_vpc(cidr_block=default_cidr)
         cb_network = AWSNetwork(self.provider, network)
         cb_network = AWSNetwork(self.provider, network)
         if name:
         if name:
+            time.sleep(2)  # The net does not always get created fast enough
             cb_network.name = name
             cb_network.name = name
         return cb_network
         return cb_network
 
 
@@ -679,6 +797,29 @@ class AWSNetworkService(BaseNetworkService):
     def subnets(self):
     def subnets(self):
         return self._subnet_svc
         return self._subnet_svc
 
 
+    def floating_ips(self, network_id=None):
+        fltrs = None
+        if network_id:
+            fltrs = {'network-interface-id': network_id}
+        al = self.provider.vpc_conn.get_all_addresses(filters=fltrs)
+        return [AWSFloatingIP(self.provider, a) for a in al]
+
+    def create_floating_ip(self):
+        ip = self.provider.ec2_conn.allocate_address(domain='vpc')
+        return AWSFloatingIP(self.provider, ip)
+
+    def routers(self):
+        routers = self.provider.vpc_conn.get_all_internet_gateways()
+        return [AWSRouter(self.provider, r) for r in routers]
+
+    def create_router(self, name=None):
+        router = self.provider.vpc_conn.create_internet_gateway()
+        cb_router = AWSRouter(self.provider, router)
+        if name:
+            time.sleep(2)  # Some time is required
+            cb_router.name = name
+        return cb_router
+
 
 
 class AWSSubnetService(BaseSubnetService):
 class AWSSubnetService(BaseSubnetService):
 
 
@@ -705,6 +846,7 @@ class AWSSubnetService(BaseSubnetService):
         subnet = self.provider.vpc_conn.create_subnet(network_id, cidr_block)
         subnet = self.provider.vpc_conn.create_subnet(network_id, cidr_block)
         cb_subnet = AWSSubnet(self.provider, subnet)
         cb_subnet = AWSSubnet(self.provider, subnet)
         if name:
         if name:
+            time.sleep(2)  # The subnet does not always get created in time
             cb_subnet.name = name
             cb_subnet.name = name
         return cb_subnet
         return cb_subnet
 
 

+ 24 - 10
cloudbridge/cloud/providers/openstack/provider.py

@@ -28,6 +28,7 @@ class OpenStackCloudProvider(BaseCloudProvider):
 
 
     def __init__(self, config):
     def __init__(self, config):
         super(OpenStackCloudProvider, self).__init__(config)
         super(OpenStackCloudProvider, self).__init__(config)
+        self.cloud_type = 'openstack'
 
 
         # Initialize cloud connection fields
         # Initialize cloud connection fields
         self.username = self._get_config_value(
         self.username = self._get_config_value(
@@ -117,15 +118,15 @@ class OpenStackCloudProvider(BaseCloudProvider):
         :return: A Keystone session object.
         :return: A Keystone session object.
         """
         """
         def connect_v2():
         def connect_v2():
-            from keystoneclient.auth.identity import Password as password_v2
-            auth = password_v2(self.auth_url, username=self.username,
+            from keystoneclient.auth.identity import Password as Password_v2
+            auth = Password_v2(self.auth_url, username=self.username,
                                password=self.password,
                                password=self.password,
                                tenant_name=self.tenant_name)
                                tenant_name=self.tenant_name)
             return session.Session(auth=auth)
             return session.Session(auth=auth)
 
 
         def connect_v3():
         def connect_v3():
-            from keystoneclient.auth.identity.v3 import Password as password_v3
-            auth = password_v3(auth_url=self.auth_url,
+            from keystoneclient.auth.identity.v3 import Password as Password_v3
+            auth = Password_v3(auth_url=self.auth_url,
                                username=self.username,
                                username=self.username,
                                password=self.password,
                                password=self.password,
                                user_domain_name=self.user_domain_name,
                                user_domain_name=self.user_domain_name,
@@ -288,12 +289,25 @@ class OpenStackCloudProvider(BaseCloudProvider):
         Get an OpenStack Swift (object store) client object for the given
         Get an OpenStack Swift (object store) client object for the given
         cloud.
         cloud.
         """
         """
-        os_options = {'region_name': self.swift_region_name}
-        return swift_client.Connection(
-            authurl=self.swift_auth_url, auth_version='2',
-            user=self.swift_username, key=self.swift_password,
-            tenant_name=self.swift_tenant_name,
-            os_options=os_options)
+        def connect_v2():
+            os_options = {'region_name': self.swift_region_name}
+            return swift_client.Connection(
+                authurl=self.swift_auth_url, auth_version='2',
+                user=self.swift_username, key=self.swift_password,
+                tenant_name=self.swift_tenant_name,
+                os_options=os_options)
+
+        def connect_v3():
+            os_options = {'region_name': self.swift_region_name,
+                          'user_domain_name': self.user_domain_name,
+                          'project_domain_name': self.project_domain_name,
+                          'project_name': self.project_name}
+            return swift_client.Connection(
+                authurl=self.swift_auth_url, auth_version='3',
+                user=self.swift_username, key=self.swift_password,
+                os_options=os_options)
+
+        return connect_v3() if self._keystone_version == 3 else connect_v2()
 
 
     def _connect_neutron(self):
     def _connect_neutron(self):
         """
         """

+ 122 - 2
cloudbridge/cloud/providers/openstack/resources.py

@@ -11,16 +11,20 @@ from cloudbridge.cloud.base.resources import BaseMachineImage
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BaseRegion
 from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 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 SnapshotState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 import inspect
 import inspect
 import json
 import json
@@ -660,6 +664,10 @@ class OpenStackNetwork(BaseNetwork):
     def name(self):
     def name(self):
         return self._network.get('name', None)
         return self._network.get('name', None)
 
 
+    @property
+    def external(self):
+        return self._network.get('router:external', False)
+
     @property
     @property
     def state(self):
     def state(self):
         return OpenStackNetwork._NETWORK_STATE_MAP.get(
         return OpenStackNetwork._NETWORK_STATE_MAP.get(
@@ -674,7 +682,7 @@ class OpenStackNetwork(BaseNetwork):
     def delete(self):
     def delete(self):
         if self.id in str(self._provider.neutron.list_networks()):
         if self.id in str(self._provider.neutron.list_networks()):
             self._provider.neutron.delete_network(self.id)
             self._provider.neutron.delete_network(self.id)
-        # Adhear to the interface docs
+        # Adhere to the interface docs
         if self.id not in str(self._provider.neutron.list_networks()):
         if self.id not in str(self._provider.neutron.list_networks()):
             return True
             return True
 
 
@@ -723,11 +731,106 @@ class OpenStackSubnet(BaseSubnet):
     def delete(self):
     def delete(self):
         if self.id in str(self._provider.neutron.list_subnets()):
         if self.id in str(self._provider.neutron.list_subnets()):
             self._provider.neutron.delete_subnet(self.id)
             self._provider.neutron.delete_subnet(self.id)
-        # Adhear to the interface docs
+        # Adhere to the interface docs
         if self.id not in str(self._provider.neutron.list_subnets()):
         if self.id not in str(self._provider.neutron.list_subnets()):
             return True
             return True
 
 
 
 
+class OpenStackFloatingIP(BaseFloatingIP):
+
+    def __init__(self, provider, floating_ip):
+        super(OpenStackFloatingIP, self).__init__(provider)
+        self._ip = floating_ip
+
+    @property
+    def id(self):
+        return self._ip.get('id', None)
+
+    @property
+    def public_ip(self):
+        return self._ip.get('floating_ip_address', None)
+
+    @property
+    def private_ip(self):
+        return self._ip.get('fixed_ip_address', None)
+
+    def in_use(self):
+        return True if self._ip.get('status', None) == 'ACTIVE' else False
+
+    def delete(self):
+        self._provider.neutron.delete_floatingip(self.id)
+        # Adhere to the interface docs
+        if self.id not in str(self._provider.neutron.list_floatingips()):
+            return True
+
+
+class OpenStackRouter(BaseRouter):
+
+    def __init__(self, provider, router):
+        super(OpenStackRouter, self).__init__(provider)
+        self._router = router
+
+    @property
+    def id(self):
+        return self._router.get('id', None)
+
+    @property
+    def name(self):
+        return self._router.get('name', None)
+
+    def refresh(self):
+        self._router = self._provider.neutron.show_router(self.id)['router']
+
+    @property
+    def state(self):
+        if self._router.get('external_gateway_info'):
+            return RouterState.ATTACHED
+        return RouterState.DETACHED
+
+    @property
+    def network_id(self):
+        if self.state == RouterState.ATTACHED:
+            return self._router.get('external_gateway_info', {}).get(
+                'network_id', None)
+        return None
+
+    def delete(self):
+        self._provider.neutron.delete_router(self.id)
+        # Adhere to the interface docs
+        if self.id not in str(self._provider.neutron.list_routers()):
+            return True
+
+    def attach_network(self, network_id):
+        self._router = self._provider.neutron.add_gateway_router(
+            self.id, {'network_id': network_id}).get('router', self._router)
+        if self.network_id and self.network_id == network_id:
+            return True
+        return False
+
+    def detach_network(self):
+        self._router = self._provider.neutron.remove_gateway_router(
+            self.id).get('router', self._router)
+        if not self.network_id:
+            return True
+        return False
+
+    def add_route(self, subnet_id):
+        router_interface = {'subnet_id': subnet_id}
+        ret = self._provider.neutron.add_interface_router(
+            self.id, router_interface)
+        if subnet_id in ret.get('subnet_ids', ""):
+            return True
+        return False
+
+    def remove_route(self, subnet_id):
+        router_interface = {'subnet_id': subnet_id}
+        ret = self._provider.neutron.remove_interface_router(
+            self.id, router_interface)
+        if subnet_id in ret.get('subnet_ids', ""):
+            return True
+        return False
+
+
 class OpenStackKeyPair(BaseKeyPair):
 class OpenStackKeyPair(BaseKeyPair):
 
 
     def __init__(self, provider, key_pair):
     def __init__(self, provider, key_pair):
@@ -787,7 +890,17 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
         :return: Rule object if successful or ``None``.
         :return: Rule object if successful or ``None``.
         """
         """
         if src_group:
         if src_group:
+            if not isinstance(src_group, SecurityGroup):
+                src_group = self._provider.security.security_groups.get(
+                    src_group)
             for protocol in ['udp', 'tcp']:
             for protocol in ['udp', 'tcp']:
+                existing_rule = self.get_rule(ip_protocol=ip_protocol,
+                                              from_port=1,
+                                              to_port=65535,
+                                              src_group=src_group)
+                if existing_rule:
+                    return existing_rule
+
                 rule = self._provider.nova.security_group_rules.create(
                 rule = self._provider.nova.security_group_rules.create(
                     parent_group_id=self._security_group.id,
                     parent_group_id=self._security_group.id,
                     ip_protocol=protocol,
                     ip_protocol=protocol,
@@ -800,6 +913,13 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
                 return OpenStackSecurityGroupRule(self._provider,
                 return OpenStackSecurityGroupRule(self._provider,
                                                   rule.to_dict(), self)
                                                   rule.to_dict(), self)
         else:
         else:
+            existing_rule = self.get_rule(ip_protocol=ip_protocol,
+                                          from_port=from_port,
+                                          to_port=to_port,
+                                          cidr_ip=cidr_ip)
+            if existing_rule:
+                return existing_rule
+
             rule = self._provider.nova.security_group_rules.create(
             rule = self._provider.nova.security_group_rules.create(
                 parent_group_id=self._security_group.id,
                 parent_group_id=self._security_group.id,
                 ip_protocol=ip_protocol,
                 ip_protocol=ip_protocol,

+ 76 - 8
cloudbridge/cloud/providers/openstack/services.py

@@ -5,7 +5,6 @@ import fnmatch
 import re
 import re
 
 
 from cinderclient.exceptions import NotFound as CinderNotFound
 from cinderclient.exceptions import NotFound as CinderNotFound
-from novaclient.exceptions import NotFound as NovaNotFound
 
 
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
@@ -32,13 +31,17 @@ from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 
+from novaclient.exceptions import NotFound as NovaNotFound
+
 from .resources import OpenStackBucket
 from .resources import OpenStackBucket
+from .resources import OpenStackFloatingIP
 from .resources import OpenStackInstance
 from .resources import OpenStackInstance
 from .resources import OpenStackInstanceType
 from .resources import OpenStackInstanceType
 from .resources import OpenStackKeyPair
 from .resources import OpenStackKeyPair
 from .resources import OpenStackMachineImage
 from .resources import OpenStackMachineImage
 from .resources import OpenStackNetwork
 from .resources import OpenStackNetwork
 from .resources import OpenStackRegion
 from .resources import OpenStackRegion
+from .resources import OpenStackRouter
 from .resources import OpenStackSecurityGroup
 from .resources import OpenStackSecurityGroup
 from .resources import OpenStackSnapshot
 from .resources import OpenStackSnapshot
 from .resources import OpenStackSubnet
 from .resources import OpenStackSubnet
@@ -74,6 +77,40 @@ class OpenStackSecurityService(BaseSecurityService):
         """
         """
         return self._security_groups
         return self._security_groups
 
 
+    def get_ec2_credentials(self):
+        """
+        A provider specific method than returns the ec2 credentials for the
+        current user.
+        """
+        keystone = self.provider.keystone
+        if hasattr(keystone, 'ec2'):
+            user_creds = [cred for cred in keystone.ec2.list(keystone.user_id)
+                          if cred.tenant_id == keystone.tenant_id]
+            if user_creds:
+                return user_creds[0]
+        return None
+
+    def get_ec2_endpoints(self):
+        """
+        A provider specific method than returns the ec2 endpoints if
+        available.
+        """
+        service_catalog = self.provider.keystone.service_catalog.get_data()
+        current_region = self.provider.compute.regions.current.id
+        ec2_url = [endpoint.get('publicURL')
+                   for svc in service_catalog
+                   for endpoint in svc.get('endpoints', [])
+                   if endpoint.get('region', None) ==
+                   current_region and svc.get('type', None) == 'ec2']
+        s3_url = [endpoint.get('publicURL')
+                  for svc in service_catalog
+                  for endpoint in svc.get('endpoints', [])
+                  if endpoint.get('region', None) ==
+                  current_region and svc.get('type', None) == 's3']
+
+        return {'ec2_endpoint': ec2_url[0] if ec2_url else None,
+                's3_endpoint': s3_url[0] if s3_url else None}
+
 
 
 class OpenStackKeyPairService(BaseKeyPairService):
 class OpenStackKeyPairService(BaseKeyPairService):
 
 
@@ -116,7 +153,7 @@ class OpenStackKeyPairService(BaseKeyPairService):
 
 
     def create(self, name):
     def create(self, name):
         """
         """
-        Create a new key pair or return an existing one by the same name.
+        Create a new key pair or raise an exception if one already exists.
 
 
         :type name: str
         :type name: str
         :param name: The name of the key pair to be created.
         :param name: The name of the key pair to be created.
@@ -124,11 +161,10 @@ class OpenStackKeyPairService(BaseKeyPairService):
         :rtype: ``object`` of :class:`.KeyPair`
         :rtype: ``object`` of :class:`.KeyPair`
         :return:  A key pair instance or ``None`` if one was not be created.
         :return:  A key pair instance or ``None`` if one was not be created.
         """
         """
-        kp = self.get(name)
-        if kp:
-            return kp
         kp = self.provider.nova.keypairs.create(name)
         kp = self.provider.nova.keypairs.create(name)
-        return OpenStackKeyPair(self.provider, kp)
+        if kp:
+            return OpenStackKeyPair(self.provider, kp)
+        return None
 
 
 
 
 class OpenStackSecurityGroupService(BaseSecurityGroupService):
 class OpenStackSecurityGroupService(BaseSecurityGroupService):
@@ -160,7 +196,7 @@ class OpenStackSecurityGroupService(BaseSecurityGroupService):
         return ClientPagedResultList(self.provider, sgs,
         return ClientPagedResultList(self.provider, sgs,
                                      limit=limit, marker=marker)
                                      limit=limit, marker=marker)
 
 
-    def create(self, name, description):
+    def create(self, name, description, network_id=None):
         """
         """
         Create a new security group under the current account.
         Create a new security group under the current account.
 
 
@@ -170,6 +206,10 @@ class OpenStackSecurityGroupService(BaseSecurityGroupService):
         :type description: str
         :type description: str
         :param description: The description of the new security group.
         :param description: The description of the new security group.
 
 
+        :type  network_id: ``None``
+        :param network_id: Not applicable for OpenStack (yet) so any value is
+                           ignored.
+
         :rtype: ``object`` of :class:`.SecurityGroup`
         :rtype: ``object`` of :class:`.SecurityGroup`
         :return: a SecurityGroup object
         :return: a SecurityGroup object
         """
         """
@@ -409,7 +449,9 @@ class OpenStackObjectStoreService(BaseObjectStoreService):
         _, container_list = self.provider.swift.get_account(
         _, container_list = self.provider.swift.get_account(
             prefix=bucket_id)
             prefix=bucket_id)
         if container_list:
         if container_list:
-            return OpenStackBucket(self.provider, container_list[0])
+            return OpenStackBucket(self.provider,
+                                   next((c for c in container_list
+                                         if c['name'] == bucket_id), None))
         else:
         else:
             return None
             return None
 
 
@@ -688,6 +730,32 @@ class OpenStackNetworkService(BaseNetworkService):
     def subnets(self):
     def subnets(self):
         return self._subnet_svc
         return self._subnet_svc
 
 
+    def floating_ips(self, network_id=None):
+        if network_id:
+            al = self.provider.neutron.list_floatingips(
+                floating_network_id=network_id)['floatingips']
+        else:
+            al = self.provider.neutron.list_floatingips()['floatingips']
+        return [OpenStackFloatingIP(self.provider, a) for a in al]
+
+    def create_floating_ip(self):
+        # OpenStack requires a floating IP to be associated with a pool,
+        # so just choose the first one available...
+        ip_pool_name = self.provider.nova.floating_ip_pools.list()[0].name
+        ip = self.provider.nova.floating_ips.create(ip_pool_name)
+        # Nova returns a different object than Neutron so fetch the Neutron one
+        ip = self.provider.neutron.list_floatingips(id=ip.id)['floatingips'][0]
+        return OpenStackFloatingIP(self.provider, ip)
+
+    def routers(self):
+        routers = self.provider.neutron.list_routers().get('routers')
+        return [OpenStackRouter(self.provider, r) for r in routers]
+
+    def create_router(self, name=None):
+        router = self.provider.neutron.create_router(
+            {'router': {'name': name}})
+        return OpenStackRouter(self.provider, router.get('router'))
+
 
 
 class OpenStackSubnetService(BaseSubnetService):
 class OpenStackSubnetService(BaseSubnetService):
 
 

+ 63 - 7
docs/getting_started.rst

@@ -30,9 +30,9 @@ AWS:
     config = {'aws_access_key': 'AKIAJW2XCYO4AF55XFEQ',
     config = {'aws_access_key': 'AKIAJW2XCYO4AF55XFEQ',
               'aws_secret_key': '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'}
               'aws_secret_key': 'duBG5EHH5eD9H/wgqF+nNKB1xRjISTVs9L/EsTWA'}
     provider = CloudProviderFactory().create_provider(ProviderList.AWS, config)
     provider = CloudProviderFactory().create_provider(ProviderList.AWS, config)
-    image_id = 'ami-d85e75b0'  # Ubuntu 14.04
+    image_id = 'ami-2d39803a'  # Ubuntu 14.04 (HVM)
 
 
-OpenStack:
+OpenStack (with Keystone authentication v2):
 
 
 .. code-block:: python
 .. code-block:: python
 
 
@@ -47,6 +47,22 @@ OpenStack:
                                                       config)
                                                       config)
     image_id = 'c1f4b7bc-a563-4feb-b439-a2e071d861aa'  # Ubuntu 14.04 @ NeCTAR
     image_id = 'c1f4b7bc-a563-4feb-b439-a2e071d861aa'  # Ubuntu 14.04 @ NeCTAR
 
 
+OpenStack (with Keystone authentication v3):
+
+.. code-block:: python
+
+    from cloudbridge.cloud.factory import CloudProviderFactory, ProviderList
+
+    config = {'os_username': 'username',
+              'os_password': 'password',
+              'os_auth_url': 'authentication URL',
+              'os_user_domain_name': 'domain name',
+              'os_project_domain_name': 'project domain name',
+              'os_project_name': 'project name'}
+    provider = CloudProviderFactory().create_provider(ProviderList.OPENSTACK,
+                                                      config)
+    image_id = '97755049-ee4f-4515-b92f-ca00991ee99a'  # Ubuntu 14.04 @ Jetstream
+
 List some resources
 List some resources
 -------------------
 -------------------
 Once you have a reference to a provider, explore the cloud platform:
 Once you have a reference to a provider, explore the cloud platform:
@@ -75,6 +91,23 @@ on disk as a read-only file.
     import os
     import os
     os.chmod('cloudbridge_intro.pem', 0400)
     os.chmod('cloudbridge_intro.pem', 0400)
 
 
+Configure a private network
+---------------------------
+We want to provision our instance into a private network to give us flexibility
+in the future. Also, providers these days are increasingly requiring use of
+private networks. Setting up a private network requires several steps:
+(1) create a network; (2) create a subnet within the network; (3) create a
+router; (4) attach the router to an external network; and (5) add a route to
+the router that links with with a subnet.
+
+.. code-block:: python
+
+    net = provider.network.create('cloudbridge_intro')
+    sn = net.create_subnet('10.0.0.1/28', 'cloudbridge-intro')
+    router = provider.network.create_router('cloudbridge-intro')
+    router.attach_network(net.id)
+    router.add_route(sn.id)
+
 Create a security group
 Create a security group
 -----------------------
 -----------------------
 Next, we need to create a security group and add a rule to allow ssh access.
 Next, we need to create a security group and add a rule to allow ssh access.
@@ -82,13 +115,14 @@ Next, we need to create a security group and add a rule to allow ssh access.
 .. code-block:: python
 .. code-block:: python
 
 
     sg = provider.security.security_groups.create(
     sg = provider.security.security_groups.create(
-        'cloudbridge_intro', 'A security group used by CloudBridge')
+        'cloudbridge_intro', 'A security group used by CloudBridge', net.id)
     sg.add_rule('tcp', 22, 22, '0.0.0.0/0')
     sg.add_rule('tcp', 22, 22, '0.0.0.0/0')
 
 
 Launch an instance
 Launch an instance
 ------------------
 ------------------
-Before we can launch an instance, we need to decide what image to use so let's
-get a base Ubuntu image ``ami-d85e75b0`` and launch an instance.
+We can now launch an instance using the created key pair and security group.
+We will launch an instance type that has at least 2 CPUs and 4GB RAM. We will
+also add the network interface as a launch argument.
 
 
 .. code-block:: python
 .. code-block:: python
 
 
@@ -96,14 +130,26 @@ get a base Ubuntu image ``ami-d85e75b0`` and launch an instance.
     inst_type = sorted([t for t in provider.compute.instance_types.list()
     inst_type = sorted([t for t in provider.compute.instance_types.list()
                         if t.vcpus >= 2 and t.ram >= 4],
                         if t.vcpus >= 2 and t.ram >= 4],
                        key=lambda x: x.vcpus*x.ram)[0]
                        key=lambda x: x.vcpus*x.ram)[0]
+    lc = provider.compute.instances.create_launch_config()
+    lc.add_network_interface(net.id)
     inst = provider.compute.instances.create(
     inst = provider.compute.instances.create(
         name='CloudBridge-intro', image=img, instance_type=inst_type,
         name='CloudBridge-intro', image=img, instance_type=inst_type,
-        key_pair=kp, security_groups=[sg])
+        key_pair=kp, security_groups=[sg], launch_config=lc)
     # Wait until ready
     # Wait until ready
-    inst.wait_till_ready()
+    inst.wait_till_ready()  # This is a blocking call
     # Show instance state
     # Show instance state
     inst.state
     inst.state
     # 'running'
     # 'running'
+
+Assign a public IP address
+--------------------------
+To access the instance, let's assign a public IP address to the instance. For
+this step, we'll first need to allocate a floating IP address for our account
+and then associate it with the instance.
+
+    fip = provider.network.create_floating_ip()
+    inst.add_floating_ip(fip.public_ip)
+    inst.refresh()
     inst.public_ips
     inst.public_ips
     # [u'54.166.125.219']
     # [u'54.166.125.219']
 
 
@@ -117,8 +163,18 @@ To wrap things up, let's clean up all the resources we have created
 .. code-block:: python
 .. code-block:: python
 
 
     inst.terminate()
     inst.terminate()
+    from cloudbridge.cloud.interfaces import InstanceState
+    inst.wait_for([InstanceState.TERMINATED, InstanceState.UNKNOWN],
+                   terminal_states=[InstanceState.ERROR])  # Blocking call
+    fip.delete()
     sg.delete()
     sg.delete()
     kp.delete()
     kp.delete()
+    os.remove('cloudbridge_intro.pem')
+    router.remove_route(sn.id)
+    router.detach_network()
+    router.delete()
+    sn.delete()
+    net.delete()
 
 
 And that's it - a full circle in a few lines of code. You can now try
 And that's it - a full circle in a few lines of code. You can now try
 the same with a different provider. All you will need to change is the
 the same with a different provider. All you will need to change is the

+ 71 - 23
docs/topics/launch.rst

@@ -7,14 +7,14 @@ need a ``provider`` object (see `this page <setup.html>`_).
 
 
 Common launch data
 Common launch data
 ------------------
 ------------------
-Before launching an instane, you need to decide on what image to launch
+Before launching an instance, you need to decide on what image to launch
 as well as what type of instance. We will create those objects here are use
 as well as what type of instance. We will create those objects here are use
 them in both options below. The specified image ID is a base Ubuntu image on
 them in both options below. The specified image ID is a base Ubuntu image on
 AWS so feel free to change it as desired.
 AWS so feel free to change it as desired.
 
 
 .. code-block:: python
 .. code-block:: python
 
 
-    img = provider.compute.images.get('ami-d85e75b0')
+    img = provider.compute.images.get('ami-d85e75b0')  # Ubuntu 14.04 on AWS
     inst_type = provider.compute.instance_types.find(name='m1.small')[0]
     inst_type = provider.compute.instance_types.find(name='m1.small')[0]
 
 
 When launching an instance, you can also specify several optional arguments
 When launching an instance, you can also specify several optional arguments
@@ -29,24 +29,18 @@ guide).
     kp = provider.security.key_pairs.find(name='cloudbridge_intro')[0]
     kp = provider.security.key_pairs.find(name='cloudbridge_intro')[0]
     sg = provider.security.security_groups.list()[0]
     sg = provider.security.security_groups.list()[0]
 
 
-Launch with classic networking
-------------------------------
-Launching an instance with the traditional networking model is straighforward,
-only needing to specify the basic parameters:
+Private networking setup
+------------------------
+Private networking gives you control over the networking setup for your
+instance(s) and is considered the preferred method for launching instances.
 
 
-.. code-block:: python
-
-    inst = provider.compute.instances.create(
-        name='CloudBridge-basic', image=img, instance_type=inst_type,
-        key_pair=kp, security_groups=[sg])
-
-Launch with private networking
-------------------------------
+Create a new private network
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 To start, we will create a private network and a corresponding subnet into
 To start, we will create a private network and a corresponding subnet into
 which an instance will be launched. When creating the subnet, we need to
 which an instance will be launched. When creating the subnet, we need to
-set the address pool. For the AWS cloud, the subnet address pool needs to
-belong to the private network address space; for OpenStack, any address pool
-is acceptable. On AWS, we can obtain the private network address space via
+set the address pool. For OpenStack, any address pool is acceptable while for
+the AWS cloud, the subnet address pool needs to belong to the private network
+address space; we can obtain the private network address space via
 network object's ``cidr_block`` field (e.g., ``10.0.0.0/16``). Let's crate a
 network object's ``cidr_block`` field (e.g., ``10.0.0.0/16``). Let's crate a
 subnet starting from the beginning of the block and allow up to 32 IP addresses
 subnet starting from the beginning of the block and allow up to 32 IP addresses
 into the subnet (``/27``):
 into the subnet (``/27``):
@@ -57,7 +51,23 @@ into the subnet (``/27``):
     net.cidr_block  # '10.0.0.0/16'
     net.cidr_block  # '10.0.0.0/16'
     sn = net.create_subnet('10.0.0.1/27', "CloudBridge-subnet")
     sn = net.create_subnet('10.0.0.1/27', "CloudBridge-subnet")
 
 
-Once we hace created a private network, we'll define a launch configuration
+Note that it may be necessary to also create a route for this new network. If
+that's the case, take a look at the
+`Getting Started <../getting_started.html>`_ document for an example.
+
+Retrieve an existing private network
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you already have existing networks, we can simply reuse an existing one:
+
+.. code-block:: python
+
+    provider.network.list()  # Find a desired network ID
+    net = provider.network.get('desired network ID')
+    sn = net.subnets()[0]  # Get a handle on a desired subnet
+
+Launch an instance
+------------------
+Once we have a handle on a private network, we'll define a launch configuration
 object to aggregate all the launch configuration options. The launch config
 object to aggregate all the launch configuration options. The launch config
 can contain other launch options, such as the block storage mappings (see
 can contain other launch options, such as the block storage mappings (see
 below). Finally, we can launch the instance:
 below). Finally, we can launch the instance:
@@ -65,14 +75,39 @@ below). Finally, we can launch the instance:
 .. code-block:: python
 .. code-block:: python
 
 
     lc = provider.compute.instances.create_launch_config()
     lc = provider.compute.instances.create_launch_config()
-    lc.add_network_interface(sn.id)
+    lc.add_network_interface(net.id)
     inst = provider.compute.instances.create(
     inst = provider.compute.instances.create(
         name='CloudBridge-VPC', image=img,  instance_type=inst_type,
         name='CloudBridge-VPC', image=img,  instance_type=inst_type,
         launch_config=lc, key_pair=kp, security_groups=[sg])
         launch_config=lc, key_pair=kp, security_groups=[sg])
 
 
+.. warning::
+
+    CloudBridge version 0.1.0 does not uniformly deal with network abstractions
+    for AWS and OpenStack providers. AWS takes a subnet ID for it's launch
+    config while OpenStack takes a network ID. As a result, the user needs to
+    make this distinction in their code and supply the correct value. For
+    example, for AWS, above code needs to look like the following:
+    ``lc.add_network_interface(sn.id)``. This has been corrected in newer code.
+
+Launch with default networking
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Launching an instance with the default networking model is straightforward,
+only needing to specify the basic parameters. This will only work for the case
+a default network exists for your account, which is provider-dependent and may
+not necessarily exist.
+
+For the case of AWS, an instance will be launched into the VPC where the
+specified security group belongs to. If no security group is specified, the
+instance will get launched into the *default* VPC, assuming such VPC exists.
+
+.. code-block:: python
+
+    inst = provider.compute.instances.create(
+        name='CloudBridge-basic', image=img, instance_type=inst_type,
+        key_pair=kp, security_groups=[sg])
 
 
 Block device mapping
 Block device mapping
---------------------
+~~~~~~~~~~~~~~~~~~~~
 Optionally, you may want to provide a block device mapping at launch,
 Optionally, you may want to provide a block device mapping at launch,
 specifying volume or ephemeral storage mappings for the instance. While volumes
 specifying volume or ephemeral storage mappings for the instance. While volumes
 can also be attached and mapped after instance boot using the volume service,
 can also be attached and mapped after instance boot using the volume service,
@@ -90,15 +125,28 @@ refer to :class:`.LaunchConfig`.
         name='CloudBridge-BDM', image=img,  instance_type=inst_type,
         name='CloudBridge-BDM', image=img,  instance_type=inst_type,
         launch_config=lc, key_pair=kp, security_groups=[sg])
         launch_config=lc, key_pair=kp, security_groups=[sg])
 
 
-where img is the :class:`.Image` object to use for the root volume.
+where ``img`` is the :class:`.Image` object to use for the root volume.
 
 
+After launch
+------------
 After an instance has launched, you can access its properties:
 After an instance has launched, you can access its properties:
 
 
 .. code-block:: python
 .. code-block:: python
 
 
     # Wait until ready
     # Wait until ready
-    inst.wait_till_ready()
+    inst.wait_till_ready()  # This is a blocking call
     inst.state
     inst.state
     # 'running'
     # 'running'
+
+Depending on the provider's networking setup, it may be necessary to explicitly
+assign a floating IP address to your instance. This can be done as follows:
+
+.. code-block:: python
+
+    # List all the IP addresses and find the desired one
+    provider.network.floating_ips()
+    # Assign the desired IP to the instance
+    inst.add_floating_ip('149.165.168.143')
+    inst.refresh()
     inst.public_ips
     inst.public_ips
-    # [u'54.166.125.219']
+    # [u'149.165.168.143']

+ 2 - 1
docs/topics/provider_development.rst

@@ -7,7 +7,8 @@ for CloudBridge.
 1. We start off by creating a new folder for the provider within the
 1. We start off by creating a new folder for the provider within the
 ``cloudbridge/cloud/providers`` folder. In this case: ``gce``. Further, install
 ``cloudbridge/cloud/providers`` folder. In this case: ``gce``. Further, install
 the native cloud provider Python library, here
 the native cloud provider Python library, here
-``pip install google-api-python-client``.
+``pip install google-api-python-client==1.4.2`` and a couple of its requirements
+``oauth2client==1.5.2`` and ``pycrypto==2.6.1``.
 
 
 2. Add a ``provider.py`` file. This file will contain the main implementation
 2. Add a ``provider.py`` file. This file will contain the main implementation
 of the cloud provider and will be the entry point that CloudBridge uses for all
 of the cloud provider and will be the entry point that CloudBridge uses for all

+ 43 - 2
test/helpers.py

@@ -6,6 +6,7 @@ import unittest
 from six import reraise
 from six import reraise
 
 
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.factory import CloudProviderFactory
+from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 
 
 
@@ -76,6 +77,25 @@ def get_provider_test_data(provider, key):
     return None
     return None
 
 
 
 
+def create_test_network(provider, name):
+    """
+    Create a network with one subnet, returning the network and subnet objects.
+    """
+    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
+
+
+def delete_test_network(network):
+    """
+    Delete the supplied network, first deleting any contained subnets.
+    """
+    for sn in network.subnets():
+        sn.delete()
+    network.delete()
+
+
 def create_test_instance(
 def create_test_instance(
         provider, instance_name, zone=None, launch_config=None,
         provider, instance_name, zone=None, launch_config=None,
         key_pair=None, security_groups=None):
         key_pair=None, security_groups=None):
@@ -89,16 +109,37 @@ def create_test_instance(
         launch_config=launch_config)
         launch_config=launch_config)
 
 
 
 
-def get_test_instance(provider, name, key_pair=None, security_groups=None):
+def get_test_instance(provider, name, key_pair=None, security_groups=None,
+                      network=None):
+    launch_config = None
+    if network:
+        launch_config = provider.compute.instances.create_launch_config()
+        launch_config.add_network_interface(network.id)
     instance = create_test_instance(
     instance = create_test_instance(
         provider,
         provider,
         name,
         name,
         key_pair=key_pair,
         key_pair=key_pair,
-        security_groups=security_groups)
+        security_groups=security_groups,
+        launch_config=launch_config)
     instance.wait_till_ready()
     instance.wait_till_ready()
     return instance
     return instance
 
 
 
 
+def cleanup_test_resources(instance=None, network=None, security_group=None,
+                           key_pair=None):
+    if instance:
+        instance.terminate()
+        instance.wait_for(
+            [InstanceState.TERMINATED, InstanceState.UNKNOWN],
+            terminal_states=[InstanceState.ERROR])
+    if security_group:
+        security_group.delete()
+    if key_pair:
+        key_pair.delete()
+    if network:
+        delete_test_network(network)
+
+
 class ProviderTestBase(object):
 class ProviderTestBase(object):
 
 
     """
     """

+ 10 - 4
test/test_block_store_service.py

@@ -98,8 +98,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         instance_name = "CBVolOps-{0}-{1}".format(
         instance_name = "CBVolOps-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        test_instance = helpers.get_test_instance(self.provider, instance_name)
-        with helpers.cleanup_action(lambda: test_instance.terminate()):
+        net, _ = helpers.create_test_network(self.provider, instance_name)
+        test_instance = helpers.get_test_instance(self.provider, instance_name,
+                                                  network=net)
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                test_instance, net)):
             name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
             name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
             test_vol = self.provider.block_store.volumes.create(
             test_vol = self.provider.block_store.volumes.create(
                 name, 1, test_instance.zone_id)
                 name, 1, test_instance.zone_id)
@@ -122,8 +125,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
         vol_desc = 'newvoldesc1'
         vol_desc = 'newvoldesc1'
-        test_instance = helpers.get_test_instance(self.provider, instance_name)
-        with helpers.cleanup_action(lambda: test_instance.terminate()):
+        net, _ = helpers.create_test_network(self.provider, instance_name)
+        test_instance = helpers.get_test_instance(self.provider, instance_name,
+                                                  network=net)
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                test_instance, net)):
             name = "CBUnitTestVolProps-{0}".format(uuid.uuid4())
             name = "CBUnitTestVolProps-{0}".format(uuid.uuid4())
             test_vol = self.provider.block_store.volumes.create(
             test_vol = self.provider.block_store.volumes.create(
                 name, 1, test_instance.zone_id, description=vol_desc)
                 name, 1, test_instance.zone_id, description=vol_desc)

+ 19 - 24
test/test_compute_service.py

@@ -21,17 +21,11 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         name = "CBInstCrud-{0}-{1}".format(
         name = "CBInstCrud-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        inst = helpers.create_test_instance(self.provider, name)
-
-        def cleanup_inst(instance):
-            instance.terminate()
-            instance.wait_for(
-                [InstanceState.TERMINATED, InstanceState.UNKNOWN],
-                terminal_states=[InstanceState.ERROR])
-
-        with helpers.cleanup_action(lambda: cleanup_inst(inst)):
-            inst.wait_till_ready()
+        net, _ = helpers.create_test_network(self.provider, name)
+        inst = helpers.get_test_instance(self.provider, name, network=net)
 
 
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                inst, net)):
             all_instances = self.provider.compute.instances.list()
             all_instances = self.provider.compute.instances.list()
 
 
             list_instances = [i for i in all_instances if i.name == name]
             list_instances = [i for i in all_instances if i.name == name]
@@ -98,22 +92,17 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         name = "CBInstProps-{0}-{1}".format(
         name = "CBInstProps-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
+        net, _ = helpers.create_test_network(self.provider, name)
         kp = self.provider.security.key_pairs.create(name=name)
         kp = self.provider.security.key_pairs.create(name=name)
         sg = self.provider.security.security_groups.create(
         sg = self.provider.security.security_groups.create(
-            name=name, description=name)
-
+            name=name, description=name, network_id=net.id)
         test_instance = helpers.get_test_instance(self.provider,
         test_instance = helpers.get_test_instance(self.provider,
                                                   name, key_pair=kp,
                                                   name, key_pair=kp,
-                                                  security_groups=[sg])
-
-        def cleanup(inst, kp, sg):
-            inst.terminate()
-            inst.wait_for([InstanceState.TERMINATED, InstanceState.UNKNOWN],
-                          terminal_states=[InstanceState.ERROR])
-            kp.delete()
-            sg.delete()
+                                                  security_groups=[sg],
+                                                  network=net)
 
 
-        with helpers.cleanup_action(lambda: cleanup(test_instance, kp, sg)):
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                test_instance, net, sg, kp)):
             self.assertTrue(
             self.assertTrue(
                 test_instance.id in repr(test_instance),
                 test_instance.id in repr(test_instance),
                 "repr(obj) should contain the object id so that the object"
                 "repr(obj) should contain the object id so that the object"
@@ -155,7 +144,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             ip_private = test_instance.private_ips[0] \
             ip_private = test_instance.private_ips[0] \
                 if test_instance.private_ips else None
                 if test_instance.private_ips else None
             ip_address = test_instance.public_ips[0] \
             ip_address = test_instance.public_ips[0] \
-                if test_instance.public_ips else ip_private
+                if test_instance.public_ips and test_instance.public_ips[0] \
+                else ip_private
             self.assertIsNotNone(
             self.assertIsNotNone(
                 ip_address,
                 ip_address,
                 "Instance must have either a public IP or a private IP")
                 "Instance must have either a public IP or a private IP")
@@ -296,6 +286,9 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         for _ in range(inst_type.num_ephemeral_disks):
         for _ in range(inst_type.num_ephemeral_disks):
             lc.add_ephemeral_device()
             lc.add_ephemeral_device()
 
 
+        net, _ = helpers.create_test_network(self.provider, name)
+        lc.add_network_interface(net.id)
+
         inst = helpers.create_test_instance(
         inst = helpers.create_test_instance(
             self.provider,
             self.provider,
             name,
             name,
@@ -304,12 +297,14 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 'placement'),
                 'placement'),
             launch_config=lc)
             launch_config=lc)
 
 
-        def cleanup(instance):
+        def cleanup(instance, net):
             instance.terminate()
             instance.terminate()
             instance.wait_for(
             instance.wait_for(
                 [InstanceState.TERMINATED, InstanceState.UNKNOWN],
                 [InstanceState.TERMINATED, InstanceState.UNKNOWN],
                 terminal_states=[InstanceState.ERROR])
                 terminal_states=[InstanceState.ERROR])
-        with helpers.cleanup_action(lambda: cleanup(inst)):
+            helpers.delete_test_network(net)
+
+        with helpers.cleanup_action(lambda: cleanup(inst, net)):
             try:
             try:
                 inst.wait_till_ready()
                 inst.wait_till_ready()
             except WaitStateException as e:
             except WaitStateException as e:

+ 5 - 2
test/test_image_service.py

@@ -22,8 +22,11 @@ class CloudImageServiceTestCase(ProviderTestBase):
         instance_name = "CBImageTest-{0}-{1}".format(
         instance_name = "CBImageTest-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        test_instance = helpers.get_test_instance(self.provider, instance_name)
-        with helpers.cleanup_action(lambda: test_instance.terminate()):
+        net, _ = helpers.create_test_network(self.provider, instance_name)
+        test_instance = helpers.get_test_instance(self.provider, instance_name,
+                                                  network=net)
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                test_instance, net)):
             name = "CBUnitTestListImg-{0}".format(uuid.uuid4())
             name = "CBUnitTestListImg-{0}".format(uuid.uuid4())
             test_image = test_instance.create_image(name)
             test_image = test_instance.create_image(name)
 
 

+ 101 - 2
test/test_network_service.py

@@ -1,7 +1,7 @@
+import test.helpers as helpers
 import uuid
 import uuid
-
 from test.helpers import ProviderTestBase
 from test.helpers import ProviderTestBase
-import test.helpers as helpers
+from cloudbridge.cloud.interfaces.resources import RouterState
 
 
 
 
 class CloudNetworkServiceTestCase(ProviderTestBase):
 class CloudNetworkServiceTestCase(ProviderTestBase):
@@ -47,6 +47,11 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     len(list_subnetl) == 1,
                     len(list_subnetl) == 1,
                     "List subnets does not return the expected subnet %s" %
                     "List subnets does not return the expected subnet %s" %
                     subnet_name)
                     subnet_name)
+                # test get method
+                sn = self.provider.network.subnets.get(subnet.id)
+                self.assertTrue(
+                    subnet.id == sn.id,
+                    "GETting subnet should return the same subnet")
 
 
             subnetl = self.provider.network.subnets.list()
             subnetl = self.provider.network.subnets.list()
             found_subnet = [n for n in subnetl if n.name == subnet_name]
             found_subnet = [n for n in subnetl if n.name == subnet_name]
@@ -55,6 +60,38 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Subnet {0} should have been deleted but still exists."
                 "Subnet {0} should have been deleted but still exists."
                 .format(subnet_name))
                 .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()):
+                ipl = self.provider.network.floating_ips()
+                self.assertTrue(
+                    ip in ipl,
+                    "Floating IP address {0} should exist in the list {1}"
+                    .format(ip.id, ipl))
+                # 2016-08: address filtering not implemented in moto
+                # empty_ipl = self.provider.network.floating_ips('dummy-net')
+                # self.assertFalse(
+                #     empty_ipl,
+                #     "Bogus network should not have any floating IPs: {0}"
+                #     .format(empty_ipl))
+                self.assertIn(
+                    ip.public_ip, repr(ip),
+                    "repr(obj) should contain the address public IP value.")
+                self.assertFalse(
+                    ip.private_ip,
+                    "Floating IP should not have a private IP value ({0})."
+                    .format(ip.private_ip))
+                self.assertFalse(
+                    ip.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]
+            self.assertTrue(
+                len(found_ip) == 0,
+                "Floating IP {0} should have been deleted but still exists."
+                .format(ip_id))
+
         netl = self.provider.network.list()
         netl = self.provider.network.list()
         found_net = [n for n in netl if n.name == name]
         found_net = [n for n in netl if n.name == name]
         self.assertEqual(
         self.assertEqual(
@@ -101,3 +138,65 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     cidr, sn.cidr_block,
                     cidr, sn.cidr_block,
                     "Subnet's CIDR %s should match the specified one %s." % (
                     "Subnet's CIDR %s should match the specified one %s." % (
                         sn.cidr_block, cidr))
                         sn.cidr_block, cidr))
+
+    def test_crud_router(self):
+
+        def _cleanup(net, subnet, router):
+            router.remove_route(subnet.id)
+            router.detach_network()
+            router.delete()
+            subnet.delete()
+            net.delete()
+
+        name = 'cbtestrouter-{0}'.format(uuid.uuid4())
+        router = self.provider.network.create_router(name=name)
+        net = self.provider.network.create(name=name)
+        cidr = '10.0.1.0/24'
+        sn = net.create_subnet(cidr_block=cidr, name=name)
+        with helpers.cleanup_action(lambda: _cleanup(net, sn, router)):
+            # Check basic router properties
+            self.assertIn(
+                router, self.provider.network.routers(),
+                "Router {0} should exist in the router list {1}.".format(
+                    router.id, self.provider.network.routers()))
+            self.assertIn(
+                router.id, repr(router),
+                "repr(obj) should contain the object id so that the object"
+                " can be reconstructed, but does not.")
+            self.assertEqual(
+                router.name, name,
+                "Router {0} name should be {1}.".format(router.name, name))
+            self.assertEqual(
+                router.state, RouterState.DETACHED,
+                "Router {0} state {1} should be {2}.".format(
+                    router.id, router.state, RouterState.DETACHED))
+            self.assertFalse(
+                router.network_id,
+                "Router {0} should not be assoc. with a network {1}".format(
+                    router.id, router.network_id))
+
+            # Check router connectivity
+            # On OpenStack only one network is external and on AWS every
+            # network is external, yet we need to use the one we've created?!
+            if self.provider.PROVIDER_ID == 'openstack':
+                for n in self.provider.network.list():
+                    if n.external:
+                        external_net = n
+                        break
+            else:
+                external_net = net
+            router.attach_network(external_net.id)
+            router.refresh()
+            self.assertEqual(
+                router.network_id, external_net.id,
+                "Router should be attached to network {0}, not {1}".format(
+                    external_net.id, router.network_id))
+            router.add_route(sn.id)
+            # TODO: add a check for routes after that's been implemented
+
+        routerl = self.provider.network.routers()
+        found_router = [r for r in routerl if r.name == name]
+        self.assertEqual(
+            len(found_router), 0,
+            "Router {0} should have been deleted but still exists."
+            .format(name))

+ 20 - 8
test/test_security_service.py

@@ -49,13 +49,9 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 "Get key pair did not return the expected key {0}."
                 "Get key pair did not return the expected key {0}."
                 .format(name))
                 .format(name))
 
 
-            # FIXME: This test doesn't work if the server generates the id
-            # and does not care about name uniqueness (e.g. azure)
-#             recreated_kp = self.provider.security.key_pairs.create(name=name)
-#             self.assertTrue(
-#                 recreated_kp == kp,
-#                 "Recreating key pair did not return the expected key {0}."
-#                 .format(name))
+            # Recreating existing keypair should raise an exception
+            with self.assertRaises(Exception):
+                self.provider.security.key_pairs.create(name=name)
         kpl = self.provider.security.key_pairs.list()
         kpl = self.provider.security.key_pairs.list()
         found_kp = [k for k in kpl if k.id == kp.id]
         found_kp = [k for k in kpl if k.id == kp.id]
         self.assertTrue(
         self.assertTrue(
@@ -203,7 +199,23 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             "Security group {0} should have been deleted but still exists."
             "Security group {0} should have been deleted but still exists."
             .format(name))
             .format(name))
 
 
-    def test_security_group_group_role(self):
+    def test_security_group_rule_add_twice(self):
+        """Test whether adding the same rule twice succeeds."""
+        name = 'cbtestsecuritygroupB-{0}'.format(uuid.uuid4())
+        sg = self.provider.security.security_groups.create(
+            name=name, description=name)
+        with helpers.cleanup_action(lambda: sg.delete()):
+            rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
+                               cidr_ip='0.0.0.0/0')
+            # attempting to add the same rule twice should succeed
+            same_rule = sg.add_rule(ip_protocol='tcp', from_port=1111,
+                                    to_port=1111, cidr_ip='0.0.0.0/0')
+            self.assertTrue(
+                rule == same_rule,
+                "Expected rule {0} not found in security group: {0}".format(
+                    same_rule, sg.rules))
+
+    def test_security_group_group_rule(self):
         """Test for proper creation of a security group rule."""
         """Test for proper creation of a security group rule."""
         name = 'cbtestsecuritygroup-c'
         name = 'cbtestsecuritygroup-c'
         sg = self.provider.security.security_groups.create(
         sg = self.provider.security.security_groups.create(