2
0
Эх сурвалжийг харах

Merge branch 'master' into master

Enis Afgan 7 жил өмнө
parent
commit
6883bcade1
53 өөрчлөгдсөн 5013 нэмэгдсэн , 853 устгасан
  1. 5 0
      .gitignore
  2. 12 1
      .travis.yml
  3. 9 9
      README.rst
  4. 95 0
      cloudbridge/cloud/base/helpers.py
  5. 2 1
      cloudbridge/cloud/base/provider.py
  6. 75 40
      cloudbridge/cloud/base/resources.py
  7. 15 55
      cloudbridge/cloud/base/services.py
  8. 6 7
      cloudbridge/cloud/factory.py
  9. 10 0
      cloudbridge/cloud/interfaces/exceptions.py
  10. 4 4
      cloudbridge/cloud/interfaces/provider.py
  11. 185 17
      cloudbridge/cloud/interfaces/resources.py
  12. 43 138
      cloudbridge/cloud/interfaces/services.py
  13. 1 0
      cloudbridge/cloud/providers/aws/helpers.py
  14. 1 2
      cloudbridge/cloud/providers/aws/provider.py
  15. 127 30
      cloudbridge/cloud/providers/aws/resources.py
  16. 126 122
      cloudbridge/cloud/providers/aws/services.py
  17. 5 0
      cloudbridge/cloud/providers/azure/__init__.py
  18. 706 0
      cloudbridge/cloud/providers/azure/azure_client.py
  19. 41 0
      cloudbridge/cloud/providers/azure/helpers.py
  20. 126 0
      cloudbridge/cloud/providers/azure/provider.py
  21. 1664 0
      cloudbridge/cloud/providers/azure/resources.py
  22. 1045 0
      cloudbridge/cloud/providers/azure/services.py
  23. 3 3
      cloudbridge/cloud/providers/openstack/helpers.py
  24. 1 6
      cloudbridge/cloud/providers/openstack/provider.py
  25. 147 22
      cloudbridge/cloud/providers/openstack/resources.py
  26. 108 136
      cloudbridge/cloud/providers/openstack/services.py
  27. 177 162
      docs/extras/_images/object_relationships_detailed.svg
  28. 24 8
      docs/getting_started.rst
  29. 1 1
      docs/topics/install.rst
  30. 1 1
      docs/topics/launch.rst
  31. 7 7
      docs/topics/networking.rst
  32. 3 3
      docs/topics/object_storage.rst
  33. 0 1
      docs/topics/overview.rst
  34. 24 1
      docs/topics/setup.rst
  35. 1 1
      docs/topics/testing.rst
  36. 18 0
      setup.cfg
  37. 16 6
      setup.py
  38. 19 0
      test/fixtures/custom_amis.json
  39. 40 3
      test/helpers/__init__.py
  40. 8 16
      test/helpers/standard_interface_tests.py
  41. 5 3
      test/test_block_store_service.py
  42. 2 1
      test/test_cloud_factory.py
  43. 2 1
      test/test_cloud_helpers.py
  44. 20 7
      test/test_compute_service.py
  45. 2 0
      test/test_image_service.py
  46. 4 2
      test/test_interface.py
  47. 44 29
      test/test_network_service.py
  48. 2 0
      test/test_object_life_cycle.py
  49. 3 1
      test/test_object_store_service.py
  50. 2 0
      test/test_region_service.py
  51. 16 1
      test/test_security_service.py
  52. 2 1
      test/test_vm_types_service.py
  53. 8 4
      tox.ini

+ 5 - 0
.gitignore

@@ -58,3 +58,8 @@ target/
 
 *.DS_Store
 /venv/
+
+bootstrap.py
+ISB-*
+launch.json
+settings.json

+ 12 - 1
.travis.yml

@@ -1,5 +1,9 @@
 dist: trusty
 language: python
+cache:
+  directories:
+    - $HOME/.cache/pip
+    - $TRAVIS_BUILD_DIR/.tox
 os:
   - linux
 #  - osx
@@ -10,14 +14,20 @@ matrix:
   include:
     - python: 2.7
       env: TOX_ENV=py27-aws
+    - python: 2.7
+      env: TOX_ENV=py27-azure
     - python: 2.7
       env: TOX_ENV=py27-openstack
     - python: 3.6
       env: TOX_ENV=py36-aws
+    - python: 3.6
+      env: TOX_ENV=py36-azure
     - python: 3.6
       env: TOX_ENV=py36-openstack
     - python: pypy-5.3.1
       env: TOX_ENV=pypy-aws
+    - python: pypy-5.3.1
+      env: TOX_ENV=pypy-azure
     - python: pypy-5.3.1
       env: TOX_ENV=pypy-openstack
 before_install:
@@ -54,7 +64,7 @@ install:
     - pip install codecov
 script:
     - tox -e $TOX_ENV
-after_success:
+after_script:
     - |
       case "$TRAVIS_EVENT_TYPE" in
         push|pull_request)
@@ -71,6 +81,7 @@ after_success:
            ;;
         *)
            echo "Build triggered through API or CRON job. Running regardless of changes"
+           coveralls & codecov & wait
            ;;
       esac
 

+ 9 - 9
README.rst

@@ -28,23 +28,23 @@ conditional code for each cloud.
 
 .. |aws-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/1
               :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/3
+.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/4
               :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/5
+.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/7
               :target: https://travis-ci.org/gvlproject/cloudbridge
 
-.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/2
+.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/3
              :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/4
+.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/6
              :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/6
+.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/9
              :target: https://travis-ci.org/gvlproject/cloudbridge
 
-.. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/azure_dev/2
+.. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/2
                 :target: https://travis-ci.org/gvlproject/cloudbridge/branches
-.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/azure_dev/5
+.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/5
                 :target: https://travis-ci.org/gvlproject/cloudbridge/branches
-.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/azure_dev/8
+.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/8
                 :target: https://travis-ci.org/gvlproject/cloudbridge/branches
 
 .. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/gce/3
@@ -65,7 +65,7 @@ Build Status
 +--------------------------+--------------+--------------+--------------+
 | **openstack**            | |os-py27|    | |os-py36|    | |os-pypy|    |
 +--------------------------+--------------+--------------+--------------+
-| **azure (alpha)**        | |azure-py27| | |azure-py36| | |azure-py36| |
+| **azure (beta)**         | |azure-py27| | |azure-py36| | |azure-py36| |
 +--------------------------+--------------+--------------+--------------+
 | **gce (alpha)**          | |gce-py27|   | |gce-py36|   | |gce-pypy|   |
 +--------------------------+--------------+--------------+--------------+

+ 95 - 0
cloudbridge/cloud/base/helpers.py

@@ -0,0 +1,95 @@
+import sys
+import traceback
+from contextlib import contextmanager
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization as crypt_serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+from six import reraise
+
+
+def generate_key_pair():
+    """
+    This method generates a keypair and returns it as a tuple
+    of (public, private) keys.
+    The public key format is OpenSSH and private key format is PEM.
+    """
+    key_pair = rsa.generate_private_key(
+        backend=default_backend(),
+        public_exponent=65537,
+        key_size=2048)
+    private_key = key_pair.private_bytes(
+        crypt_serialization.Encoding.PEM,
+        crypt_serialization.PrivateFormat.PKCS8,
+        crypt_serialization.NoEncryption()).decode('utf-8')
+    public_key = key_pair.public_key().public_bytes(
+        crypt_serialization.Encoding.OpenSSH,
+        crypt_serialization.PublicFormat.OpenSSH).decode('utf-8')
+    return public_key, private_key
+
+
+def filter_by(prop_name, kwargs, objs):
+    """
+    Utility method for filtering a list of objects by a property.
+    If the given property has a non empty value in kwargs, then
+    the list of objs is filtered by that value. Otherwise, the
+    list of objs is returned as is.
+    """
+    prop_val = kwargs.pop(prop_name, None)
+    if prop_val:
+        match = (o for o in objs if getattr(o, prop_name) == prop_val)
+        return match
+    return objs
+
+
+def generic_find(filter_names, kwargs, objs):
+    """
+    Utility method for filtering a list of objects by a list of filters.
+    """
+    matches = objs
+    for name in filter_names:
+        matches = filter_by(name, kwargs, matches)
+
+    # All kwargs should have been popped at this time.
+    if len(kwargs) > 0:
+        raise TypeError(
+            "Unrecognised parameters for search: %s. Supported attributes: %s"
+            % (kwargs, filter_names))
+
+    return matches
+
+
+@contextmanager
+def cleanup_action(cleanup_func):
+    """
+    Context manager to carry out a given
+    cleanup action after carrying out a set
+    of tasks, or when an exception occurs.
+    If any errors occur during the cleanup
+    action, those are ignored, and the original
+    traceback is preserved.
+
+    :params func: This function is called if
+    an exception occurs or at the end of the
+    context block. If any exceptions raised
+        by func are ignored.
+    Usage:
+        with cleanup_action(lambda e: print("Oops!")):
+            do_something()
+    """
+    try:
+        yield
+    except Exception:
+        ex_class, ex_val, ex_traceback = sys.exc_info()
+        try:
+            cleanup_func()
+        except Exception as e:
+            print("Error during exception cleanup: {0}".format(e))
+            traceback.print_exc()
+        reraise(ex_class, ex_val, ex_traceback)
+    try:
+        cleanup_func()
+    except Exception as e:
+        print("Error during cleanup: {0}".format(e))
+        traceback.print_exc()

+ 2 - 1
cloudbridge/cloud/base/provider.py

@@ -148,7 +148,8 @@ class BaseCloudProvider(CloudProvider):
 
         :return: a configuration value for the supplied ``key``
         """
-        log.info("Getting config key: %s with default: %s", key, default_value)
+        log.debug("Getting config key %s, with supplied default value: %s",
+                  key, default_value)
         if isinstance(self.config, dict) and self.config.get(key):
             return self.config.get(key, default_value)
         elif hasattr(self.config, key) and getattr(self.config, key):

+ 75 - 40
cloudbridge/cloud/base/resources.py

@@ -9,6 +9,7 @@ import re
 import shutil
 import time
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidConfigurationException
 from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
@@ -19,6 +20,9 @@ from cloudbridge.cloud.interfaces.resources import BucketContainer
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from cloudbridge.cloud.interfaces.resources import CloudResource
 from cloudbridge.cloud.interfaces.resources import FloatingIP
+from cloudbridge.cloud.interfaces.resources import FloatingIPContainer
+from cloudbridge.cloud.interfaces.resources import FloatingIpState
+from cloudbridge.cloud.interfaces.resources import GatewayContainer
 from cloudbridge.cloud.interfaces.resources import GatewayState
 from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import InstanceState
@@ -454,29 +458,31 @@ class BaseLaunchConfig(LaunchConfig):
         InvalidConfigurationException if the configuration is incorrect.
         """
         if source is None and not size:
-            log.exception("Raised InvalidConfigurationException, no"
-                          " size argument specified.")
+            log.exception("InvalidConfigurationException raised: "
+                          "no size argument specified.")
             raise InvalidConfigurationException(
-                "A size must be specified for a blank new volume")
+                "A size must be specified for a blank new volume.")
 
         if source and \
                 not isinstance(source, (Snapshot, Volume, MachineImage)):
-            log.exception("InvalidConfigurationException raised, "
+            log.exception("InvalidConfigurationException raised: "
                           "source argument not specified correctly.")
             raise InvalidConfigurationException(
-                "Source must be a Snapshot, Volume, MachineImage or None")
+                "Source must be a Snapshot, Volume, MachineImage, or None.")
         if size:
             if not isinstance(size, six.integer_types) or not size > 0:
-                log.exception("InvalidConfigurationException raised, "
-                              " size argument must be greater than 0.")
+                log.exception("InvalidConfigurationException raised: "
+                              "size argument must be an integer greater than "
+                              "0. Got type %s and value %s.", type(size), size)
                 raise InvalidConfigurationException(
-                    "The size must be None or a number greater than 0")
+                    "The size must be None or an integer greater than 0.")
 
         if is_root:
             for bd in self.block_devices:
                 if bd.is_root:
-                    log.exception("InvalidConfigurationException raised,"
-                                  "%s has already been marked as root", bd)
+                    log.exception("InvalidConfigurationException raised: "
+                                  "%s has already been marked as the root "
+                                  "block device.", bd)
                     raise InvalidConfigurationException(
                         "An existing block device: {0} has already been"
                         " marked as root. There can only be one root device.")
@@ -591,6 +597,7 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
     def __init__(self, provider, key_pair):
         super(BaseKeyPair, self).__init__(provider)
         self._key_pair = key_pair
+        self._private_material = None
 
     def __eq__(self, other):
         return (isinstance(other, KeyPair) and
@@ -612,6 +619,15 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         """
         return self._key_pair.name
 
+    @property
+    def material(self):
+        return self._private_material
+
+    @material.setter
+    # pylint:disable=arguments-differ
+    def material(self, value):
+        self._private_material = value
+
     def delete(self):
         """
         Delete this KeyPair.
@@ -699,28 +715,11 @@ class BaseVMFirewallRuleContainer(BasePageableObjectMixin,
             return None
 
     def find(self, **kwargs):
-        matches = self
-
-        def filter_by(prop_name, rules):
-            prop_val = kwargs.pop(prop_name, None)
-            if prop_val:
-                match = [r for r in rules if getattr(r, prop_name) == prop_val]
-                return match
-            return rules
-
-        matches = filter_by('name', matches)
-        matches = filter_by('direction', matches)
-        matches = filter_by('protocol', matches)
-        matches = filter_by('from_port', matches)
-        matches = filter_by('to_port', matches)
-        matches = filter_by('cidr', matches)
-        matches = filter_by('src_dest_fw', matches)
-        matches = filter_by('src_dest_fw_id', matches)
-        limit = kwargs.pop('limit', None)
-        marker = kwargs.pop('marker', None)
-
-        return ClientPagedResultList(self._provider, matches,
-                                     limit=limit, marker=marker)
+        obj_list = self
+        filters = ['name', 'direction', 'protocol', 'from_port', 'to_port',
+                   'cidr', 'src_dest_fw', 'src_dest_fw_id']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
     def delete(self, rule_id):
         rule = self.get(rule_id)
@@ -849,10 +848,6 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
                 "data.html#object-key-guidelines" % name)
 
     def save_content(self, target_stream):
-        """
-        Download this object and write its
-        contents to the target_stream.
-        """
         shutil.copyfileobj(self.iter_content(), target_stream)
 
     def __eq__(self, other):
@@ -918,6 +913,13 @@ class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
         return self.__provider
 
 
+class BaseGatewayContainer(GatewayContainer, BasePageableObjectMixin):
+
+    def __init__(self, provider, network):
+        self._network = network
+        self._provider = provider
+
+
 class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
     CB_DEFAULT_NETWORK_NAME = os.environ.get('CB_DEFAULT_NETWORK_NAME',
@@ -974,18 +976,50 @@ class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
             interval=interval)
 
 
-class BaseFloatingIP(BaseCloudResource, FloatingIP):
+class BaseFloatingIPContainer(FloatingIPContainer, BasePageableObjectMixin):
+
+    def __init__(self, provider, gateway):
+        self.__provider = provider
+        self.gateway = gateway
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name', 'public_ip']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
+
+    def delete(self, fip_id):
+        floating_ip = self.get(fip_id)
+        if floating_ip:
+            floating_ip.delete()
+
+
+class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
 
     def __init__(self, provider):
         super(BaseFloatingIP, self).__init__(provider)
 
     @property
     def name(self):
-        """
-        VM firewall rules don't support names, so pass
-        """
+        # VM firewall rules don't support names, so pass
         return self.public_ip
 
+    @property
+    def state(self):
+        return (FloatingIpState.IN_USE if self.in_use
+                else FloatingIpState.AVAILABLE)
+
+    def wait_till_ready(self, timeout=None, interval=None):
+        self.wait_for(
+            [FloatingIpState.AVAILABLE, FloatingIpState.IN_USE],
+            terminal_states=[FloatingIpState.ERROR],
+            timeout=timeout,
+            interval=interval)
+
     def __repr__(self):
         return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
                                             self.id, self.public_ip)
@@ -1024,6 +1058,7 @@ class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
 
     def __init__(self, provider):
         super(BaseInternetGateway, self).__init__(provider)
+        self.__provider = provider
 
     def __repr__(self):
         return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,

+ 15 - 55
cloudbridge/cloud/base/services.py

@@ -3,13 +3,11 @@ Base implementation for services available through a provider
 """
 import logging
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.interfaces.resources import Router
-
 from cloudbridge.cloud.interfaces.services import BucketService
 from cloudbridge.cloud.interfaces.services import CloudService
 from cloudbridge.cloud.interfaces.services import ComputeService
-from cloudbridge.cloud.interfaces.services import FloatingIPService
-from cloudbridge.cloud.interfaces.services import GatewayService
 from cloudbridge.cloud.interfaces.services import ImageService
 from cloudbridge.cloud.interfaces.services import InstanceService
 from cloudbridge.cloud.interfaces.services import KeyPairService
@@ -26,6 +24,7 @@ from cloudbridge.cloud.interfaces.services import VMTypeService
 from cloudbridge.cloud.interfaces.services import VolumeService
 
 from .resources import BasePageableObjectMixin
+from .resources import ClientPagedResultList
 
 log = logging.getLogger(__name__)
 
@@ -129,15 +128,10 @@ class BaseVMTypeService(
         return next(vm_type, None)
 
     def find(self, **kwargs):
-        name = kwargs.get('name')
-        log.info("Searching for VMTypeService with the: name %s ...", name)
-        if name:
-            return [itype for itype in self if itype.name == name]
-        else:
-            log.exception("TypeError exception raised. Invalid parameters "
-                          "used for search.")
-            raise TypeError(
-                "Invalid parameters for search. Supported attributes: {name}")
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
 
 class BaseInstanceService(
@@ -153,8 +147,11 @@ class BaseRegionService(
     def __init__(self, provider):
         super(BaseRegionService, self).__init__(provider)
 
-    def find(self, name):
-        return [region for region in self if region.name == name]
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
 
 class BaseNetworkingService(NetworkingService, BaseCloudService):
@@ -188,40 +185,10 @@ class BaseSubnetService(
         super(BaseSubnetService, self).__init__(provider)
 
     def find(self, **kwargs):
-        name = kwargs.get('name')
-        log.info("Searching for SubnetService with the name: %s ...", name)
-        if name:
-            return [subnet for subnet in self if subnet.name == name]
-        else:
-            log.exception("TypeError exception raised. Invalid parameters "
-                          "used for search.")
-            raise TypeError(
-                "Invalid parameters for search. Supported attributes: {name}")
-
-
-class BaseFloatingIPService(
-        BasePageableObjectMixin, FloatingIPService, BaseCloudService):
-
-    def __init__(self, provider):
-        super(BaseFloatingIPService, self).__init__(provider)
-
-    def find(self, **kwargs):
-        if 'name' in kwargs:
-            name = kwargs.get('name')
-            log.info("Searching for FloatingIPService with the "
-                     "name: %s...", name)
-            if name:
-                return [fip for fip in self if fip.name == name]
-        else:
-            log.exception("TypeError exception raised. Invalid parameters "
-                          "used for search.")
-            raise TypeError(
-                "Invalid parameters for search. Supported attributes: {name}")
-
-    def delete(self, fip_id):
-        floating_ip = self.get(fip_id)
-        if floating_ip:
-            floating_ip.delete()
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
 
 class BaseRouterService(
@@ -240,10 +207,3 @@ class BaseRouterService(
             if router:
                 log.info("Router %s successful deleted.", router)
                 router.delete()
-
-
-class BaseGatewayService(
-        GatewayService, BaseCloudService):
-
-    def __init__(self, provider):
-        super(BaseGatewayService, self).__init__(provider)

+ 6 - 7
cloudbridge/cloud/factory.py

@@ -15,6 +15,7 @@ log = logging.getLogger(__name__)
 class ProviderList(object):
     AWS = 'aws'
     OPENSTACK = 'openstack'
+    AZURE = 'azure'
 
 
 class CloudProviderFactory(object):
@@ -81,13 +82,13 @@ class CloudProviderFactory(object):
         Imports and registers providers from the given module name.
         Raises an ImportError if the import does not succeed.
         """
-        log.info("Importing providers from %s", module_name)
+        log.debug("Importing providers from %s", module_name)
         module = importlib.import_module(
             "{0}.{1}".format(providers.__name__,
                              module_name))
         classes = inspect.getmembers(module, inspect.isclass)
         for _, cls in classes:
-            log.info("Registering the provider: %s", cls)
+            log.debug("Registering the provider: %s", cls)
             self.register_provider_class(cls)
 
     def list_providers(self):
@@ -108,7 +109,7 @@ class CloudProviderFactory(object):
         """
         if not self.provider_list:
             self.discover_providers()
-        log.info("List of available providers: %s", self.provider_list)
+        log.debug("List of available providers: %s", self.provider_list)
         return self.provider_list
 
     def create_provider(self, name, config):
@@ -129,8 +130,7 @@ class CloudProviderFactory(object):
         :return:  a concrete provider instance
         :rtype: ``object`` of :class:`.CloudProvider`
         """
-        log.info("Searching provider with the name %s on %s",
-                 name, config)
+        log.info("Creating '%s' provider", name)
         provider_class = self.get_provider_class(name)
         if provider_class is None:
             log.exception("A provider with the name %s could not "
@@ -138,8 +138,7 @@ class CloudProviderFactory(object):
             raise NotImplementedError(
                 'A provider with name {0} could not be'
                 ' found'.format(name))
-        log.debug("Found provider name: %s with these config "
-                  " details: %s", name, config)
+        log.debug("Created '%s' provider", name)
         return provider_class(config)
 
     def get_provider_class(self, name, get_mock=False):

+ 10 - 0
cloudbridge/cloud/interfaces/exceptions.py

@@ -53,6 +53,7 @@ class InvalidNameException(CloudBridgeBaseException):
     a CloudBridge resource.An example would be setting uppercase
     letters, which are not allowed in a resource name.
     """
+
     def __init__(self, msg):
         super(InvalidNameException, self).__init__(msg)
 
@@ -68,3 +69,12 @@ class InvalidValueException(CloudBridgeBaseException):
         super(InvalidValueException, self).__init__(
             "Param %s has been given an unrecognised value %s" %
             (param, value))
+
+
+class DuplicateResourceException(CloudBridgeBaseException):
+    """
+    Marker interface for any attempt to create a CloudBridge resource that
+    already exists. For example, creating a KeyPair with the same name will
+    result in a DuplicateResourceException.
+    """
+    pass

+ 4 - 4
cloudbridge/cloud/interfaces/provider.py

@@ -33,9 +33,9 @@ class CloudProvider(object):
         """
         Returns the config object associated with this provider. This object
         is a subclass of :class:`dict` and will contain the properties
-        provided at initialization time. In addition, it also contains extra
-        provider-wide properties such as the default result limit for list()
-        queries.
+        provided at initialization time, grouped under `cloud_properties` and
+        `credentials` keys. In addition, it also contains extra provider-wide
+        properties such as the default result limit for `list()` queries.
 
         Example:
 
@@ -43,7 +43,7 @@ class CloudProvider(object):
 
             config = { 'aws_access_key' : '<my_key>' }
             provider = factory.create_provider(ProviderList.AWS, config)
-            print(provider.config.get('aws_access_key'))
+            print(provider.config['credentials'].get('aws_access_key'))
             print(provider.config.default_result_limit))
             # change provider result limit
             provider.config.default_result_limit = 100

+ 185 - 17
cloudbridge/cloud/interfaces/resources.py

@@ -528,6 +528,16 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         pass
 
+    @abstractproperty
+    def subnet_id(self):
+        """
+        Get the subnet ID where this instance is placed.
+
+        :rtype: ``str``
+        :return: Subnet ID to which this instance is connected.
+        """
+        pass
+
 #     @abstractproperty
 #     def mac_address(self):
 #         """
@@ -583,8 +593,12 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         Add a public IP address to this instance.
 
-        :type floating_ip: :class:``.FloatingIP``
-        :param floating_ip: The FloatingIP to associate with the instance.
+        :type floating_ip: :class:``.FloatingIP`` or floating IP ID
+        :param floating_ip: The FloatingIP object to associate with the
+                            instance. Note that is not the actual public IP
+                            address but the CloudBridge object encapsulating
+                            the IP or the respective provider ID that
+                            identifies the address.
         """
         pass
 
@@ -593,8 +607,12 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         Remove a public IP address from this instance.
 
-        :type floating_ip: :class:``.FloatingIP``
-        :param floating_ip: The IP address to remove from the instance.
+        :type floating_ip: :class:``.FloatingIP`` or floating IP ID
+        :param floating_ip: The FloatingIP object to remove from the
+                            instance. Note that is not the actual public IP
+                            address but the CloudBridge object encapsulating
+                            the IP or the respective provider ID that
+                            identifies the address.
         """
         pass
 
@@ -790,8 +808,8 @@ class NetworkState(object):
     :cvar UNKNOWN: Network state unknown.
     :cvar PENDING: Network is being created.
     :cvar AVAILABLE: Network is available.
-    :cvar DOWN = Network is not operational.
-    :cvar ERROR = Network errored.
+    :cvar DOWN: Network is not operational.
+    :cvar ERROR: Network errored.
     """
     UNKNOWN = "unknown"
     PENDING = "pending"
@@ -882,17 +900,26 @@ class Network(ObjectLifeCycleMixin, CloudResource):
         """
         pass
 
+    @abstractproperty
+    def gateways(self):
+        """
+        Provides access to the internet gateways attached to this network.
+
+        :rtype: :class:`.GatewayContainer`
+        :return: A GatewayContainer object
+        """
+        pass
 
-class SubnetState(object):
 
+class SubnetState(object):
     """
     Standard states for a subnet.
 
     :cvar UNKNOWN: Subnet state unknown.
     :cvar PENDING: Subnet is being created.
     :cvar AVAILABLE: Subnet is available.
-    :cvar DOWN = Subnet is not operational.
-    :cvar ERROR = Subnet errored.
+    :cvar DOWN: Subnet is not operational.
+    :cvar ERROR: Subnet errored.
     """
     UNKNOWN = "unknown"
     PENDING = "pending"
@@ -950,7 +977,93 @@ class Subnet(ObjectLifeCycleMixin, CloudResource):
         pass
 
 
-class FloatingIP(CloudResource):
+class FloatingIPContainer(PageableObjectMixin):
+    """
+    Base interface for a FloatingIP Service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, fip_id):
+        """
+        Returns a FloatingIP given its ID or ``None`` if not found.
+
+        :type fip_id: ``str``
+        :param fip_id: The ID of the FloatingIP to retrieve.
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return: a FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List floating (i.e., static) IP addresses.
+
+        :rtype: ``list`` of :class:`.FloatingIP`
+        :return: list of FloatingIP objects
+        """
+        pass
+
+    @abstractmethod
+    def find(self, **kwargs):
+        """
+        Searches for a FloatingIP by a given list of attributes.
+
+        Supported attributes: name, public_ip
+
+        Example:
+
+        .. code-block:: python
+
+            fip = provider.networking.gateways.get('id').floating_ips.find(
+                        public_ip='public_ip')
+
+
+        :rtype: List of ``object`` of :class:`.FloatingIP`
+        :return: A list of FloatingIP objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self):
+        """
+        Allocate a new floating (i.e., static) IP address.
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return:  A FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, fip_id):
+        """
+        Delete an existing FloatingIP.
+
+        :type fip_id: ``str``
+        :param fip_id: The ID of the FloatingIP to be deleted.
+        """
+        pass
+
+
+class FloatingIpState(object):
+
+    """
+    Standard states for a floating ip.
+
+    :cvar UNKNOWN: Floating IP state unknown.
+    :cvar AVAILABLE: Floating IP is available.
+    :cvar IN_USE: Floating IP is attached to a device.
+    :cvar ERROR: Floating IP is in an error state.
+    """
+    UNKNOWN = "unknown"
+    AVAILABLE = "available"
+    IN_USE = "in_use"
+    ERROR = "error"
+
+
+class FloatingIP(ObjectLifeCycleMixin, CloudResource):
     """
     Represents a floating (i.e., static) IP address.
     """
@@ -1099,7 +1212,6 @@ class Router(CloudResource):
 
 
 class GatewayState(object):
-
     """
     Standard states for a gateway.
 
@@ -1115,6 +1227,50 @@ class GatewayState(object):
     ERROR = "error"
 
 
+class GatewayContainer(PageableObjectMixin):
+    """
+    Manage internet gateway resources.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get_or_create_inet_gateway(self, name=None):
+        """
+        Creates new or returns an existing internet gateway for a network.
+
+        The returned gateway object can subsequently be attached to a router to
+        provide internet routing to a network.
+
+        :type  name: ``str``
+        :param name: The gateway name. This applies only if creating a gateway
+                     and if the provider supports it.
+
+        :rtype: ``object``  of :class:`.InternetGateway` or ``None``
+        :return: an InternetGateway object of ``None`` if not found.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, gateway):
+        """
+        Delete a gateway.
+
+        :type gateway: :class:`.Gateway` object
+        :param gateway: Gateway object to delete.
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List all available internet gateways.
+
+        :rtype: ``list`` of :class:`.InternetGateway` or ``None``
+        :return: Current list of internet gateways.
+        """
+        pass
+
+
 class Gateway(CloudResource):
     """
     Represents a gateway resource.
@@ -1139,6 +1295,16 @@ class Gateway(CloudResource):
         """
         pass
 
+    @abstractproperty
+    def floating_ips(self):
+        """
+        Provides access to floating IPs connected to this internet gateway.
+
+        :rtype: :class:`.FloatingIPContainer`
+        :return: A FloatingIPContainer object
+        """
+        pass
+
 
 class InternetGateway(ObjectLifeCycleMixin, Gateway):
     """
@@ -1612,10 +1778,10 @@ class VMType(CloudResource):
     @abstractproperty
     def ram(self):
         """
-        The amount of RAM (in MB) supported by this VM type.
+        The amount of RAM (in GB) supported by this VM type.
 
-        :rtype: ``int``
-        :return: Total RAM (in MB).
+        :rtype: ``float``
+        :return: Total RAM (in GB).
         """
         pass
 
@@ -1763,7 +1929,7 @@ class VMFirewallRuleContainer(PageableObjectMixin):
             fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
 
         You need to pass in either ``src_dest_fw`` OR ``protocol`` AND
-        ``from_port``, ``to_port``, ``cidr_ip``. In other words, either
+        ``from_port``, ``to_port``, ``cidr``. In other words, either
         you are authorizing another group or you are authorizing some
         IP-based rule.
 
@@ -2135,9 +2301,11 @@ class BucketContainer(PageableObjectMixin):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
-        Searches for an object by a given name
+        Searches for an object by a given list of attributes.
+
+        Supported attributes: name
 
         :rtype: List of ``objects`` of :class:`.BucketObject`
         :return: A list of BucketObjects matching the supplied attributes.

+ 43 - 138
cloudbridge/cloud/interfaces/services.py

@@ -160,26 +160,15 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for an instance by a given list of attributes.
 
+        Supported attributes: name
+
         :type  name: ``str``
         :param name: The name to search for
 
-        :type  limit: ``int``
-        :param limit: The maximum number of objects to return. Note that the
-                      maximum is not guaranteed to be honoured, and a lower
-                      maximum may be enforced depending on the provider. In
-                      such a case, the returned ResultList's is_truncated
-                      property can be used to determine whether more records
-                      are available.
-
-        :type  marker: ``str``
-        :param marker: The marker is an opaque identifier used to assist
-                       in paging through very long lists of objects. It is
-                       returned on each invocation of the list method.
-
         :rtype: List of ``object`` of :class:`.Instance`
         :return: A list of Instance objects matching the supplied attributes.
         """
@@ -318,10 +307,12 @@ class VolumeService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a volume by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: ``object`` of :class:`.Volume`
         :return: a Volume object or ``None`` if not found.
         """
@@ -383,10 +374,12 @@ class SnapshotService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a snapshot by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: list of :class:`.Snapshot`
         :return: a Snapshot object or an empty list if none found.
         """
@@ -519,10 +512,12 @@ class ImageService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for an image by a given list of attributes
 
+        Supported attributes: name
+
         :rtype: ``object`` of :class:`.Image`
         :return:  an Image instance
         """
@@ -547,7 +542,6 @@ class ImageService(PageableObjectMixin, CloudService):
 
 
 class NetworkingService(CloudService):
-
     """
     Base service interface for networking.
 
@@ -586,17 +580,6 @@ class NetworkingService(CloudService):
         """
         pass
 
-    @abstractproperty
-    def gateways(self):
-        """
-        Provides access to all Gateway related services, such as
-        Internet Gateways.
-
-        :rtype: :class:`.GatewayService`
-        :return: a Router service object
-        """
-        pass
-
 
 class NetworkService(PageableObjectMixin, CloudService):
 
@@ -629,10 +612,12 @@ class NetworkService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a network by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: List of ``object`` of :class:`.Network`
         :return: A list of Network objects matching the supplied attributes.
         """
@@ -730,10 +715,12 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a subnet by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: List of ``object`` of :class:`.Subnet`
         :return: A list of Subnet objects matching the supplied attributes.
         """
@@ -797,69 +784,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
 
-class FloatingIPService(PageableObjectMixin, CloudService):
-
-    """
-    Base interface for a FloatingIP Service.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get(self, fip_id):
-        """
-        Returns a FloatingIP given its ID or ``None`` if not found.
-
-        :type fip_id: ``str``
-        :param fip_id: The ID of the FloatingIP to retrieve.
-
-        :rtype: ``object`` of :class:`.FloatingIP`
-        :return: a FloatingIP object
-        """
-        pass
-
-    @abstractmethod
-    def list(self, limit=None, marker=None):
-        """
-        List floating (i.e., static) IP addresses.
-
-        :rtype: ``list`` of :class:`.FloatingIP`
-        :return: list of FloatingIP objects
-        """
-        pass
-
-    @abstractmethod
-    def find(self, name):
-        """
-        Searches for a FloatingIP by a given list of attributes.
-
-        :rtype: List of ``object`` of :class:`.FloatingIP`
-        :return: A list of FloatingIP objects matching the supplied attributes.
-        """
-        pass
-
-    @abstractmethod
-    def create(self):
-        """
-        Allocate a new floating (i.e., static) IP address.
-
-        :rtype: ``object`` of :class:`.FloatingIP`
-        :return:  A FloatingIP object
-        """
-        pass
-
-    @abstractmethod
-    def delete(self, fip_id):
-        """
-        Delete an existing FloatingIP.
-
-        :type fip_id: ``str``
-        :param fip_id: The ID of the FloatingIP to be deleted.
-        """
-        pass
-
-
 class RouterService(PageableObjectMixin, CloudService):
-
     """
     Manage networking router actions and resources.
     """
@@ -889,10 +814,12 @@ class RouterService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a router by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: List of ``object`` of :class:`.Router`
         :return: A list of Router objects matching the supplied attributes.
         """
@@ -926,45 +853,6 @@ class RouterService(PageableObjectMixin, CloudService):
         pass
 
 
-class GatewayService(CloudService):
-
-    """
-    Manage internet gateway resources.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get_or_create_inet_gateway(self, name):
-        """
-        Creates and returns a new internet gateway or returns an existing
-        singleton gateway, depending on the cloud provider. The returned
-        gateway object can subsequently be attached to a router to provide
-        internet routing to a network. If the gateway is no longer required,
-        clients should call gateway.delete() to delete the gateway. On some
-        cloud providers this will result in the gateway being deleted. On
-        others, it will result in a no-op if the cloud has only a single/public
-        gateway.
-
-        :type  name: ``str``
-        :param name: The gateway name. The name will be set if the provider
-                     supports it.
-
-        :rtype: ``object``  of :class:`.InternetGateway` or ``None``
-        :return: an InternetGateway object of ``None`` if not found.
-        """
-        pass
-
-    @abstractmethod
-    def delete(self, gateway):
-        """
-        Delete a gateway.
-
-        :type gateway: :class:`.Gateway` object
-        :param gateway: Gateway object to delete.
-        """
-        pass
-
-
 class BucketService(PageableObjectMixin, CloudService):
 
     """
@@ -995,10 +883,12 @@ class BucketService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a bucket by a given list of attributes.
 
+        Supported attributes: name
+
         Example:
 
         .. code-block:: python
@@ -1149,23 +1039,32 @@ class KeyPairService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Searches for a key pair by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: ``object`` of :class:`.KeyPair`
         :return:  a KeyPair object
         """
         pass
 
     @abstractmethod
-    def create(self, name):
+    def create(self, name, public_key_material=None):
         """
         Create a new key pair or raise an exception if one already exists.
+        If the public_key_material is provided, the material will be imported
+        to create the new keypair. Otherwise, a new public and private key
+        pair will be generated.
 
         :type name: str
         :param name: The name of the key pair to be created.
 
+        :type public_key_material: str
+        :param public_key_material: The key-pair material to import in OpenSSH
+                                    format.
+
         :rtype: ``object`` of :class:`.KeyPair`
         :return:  A keypair instance or ``None``.
         """
@@ -1242,10 +1141,12 @@ class VMFirewallService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         Get VM firewalls associated with your account filtered by name.
 
+        Supported attributes: name
+
         :type name: str
         :param name: The name of the VM firewall to retrieve.
 
@@ -1302,6 +1203,8 @@ class VMTypeService(PageableObjectMixin, CloudService):
         """
         Searches for an instance by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: ``object`` of :class:`.VMType`
         :return: an Instance object
         """
@@ -1349,10 +1252,12 @@ class RegionService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name):
+    def find(self, **kwargs):
         """
         Searches for a region by a given list of attributes.
 
+        Supported attributes: name
+
         :rtype: ``object`` of :class:`.Region`
         :return: a Region object
         """

+ 1 - 0
cloudbridge/cloud/providers/aws/helpers.py

@@ -1,5 +1,6 @@
 """A set of AWS-specific helper methods used by the framework."""
 import logging as log
+
 from boto3.resources.params import create_request_parameters
 
 from botocore import xform_name

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

@@ -23,8 +23,7 @@ from .services import AWSStorageService
 class AWSCloudProvider(BaseCloudProvider):
     '''AWS cloud provider interface'''
     PROVIDER_ID = 'aws'
-    AWS_INSTANCE_DATA_DEFAULT_URL = "https://d168wakzal7fp0.cloudfront.net/" \
-                                    "aws_instance_data.json"
+    AWS_INSTANCE_DATA_DEFAULT_URL = "http://cloudve.org/cb-aws-vmtypes.json"
 
     def __init__(self, config):
         super(AWSCloudProvider, self).__init__(config)

+ 127 - 30
cloudbridge/cloud/providers/aws/resources.py

@@ -3,14 +3,18 @@ DataTypes used by this provider
 """
 import hashlib
 import inspect
+import logging
 
 from botocore.exceptions import ClientError
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketContainer
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseFloatingIP
+from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
+from cloudbridge.cloud.base.resources import BaseGatewayContainer
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInternetGateway
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -39,9 +43,12 @@ from cloudbridge.cloud.interfaces.resources import SubnetState
 from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
 
+from .helpers import BotoEC2Service
 from .helpers import find_tag_value
 from .helpers import trim_empty_params
 
+log = logging.getLogger(__name__)
+
 
 class AWSMachineImage(BaseMachineImage):
 
@@ -189,7 +196,7 @@ class AWSVMType(BaseVMType):
 
     @property
     def extra_data(self):
-        return {key: val for key, val in enumerate(self._inst_dict)
+        return {key: val for key, val in self._inst_dict.items()
                 if key not in ["instance_type", "family", "vCPU", "memory"]}
 
 
@@ -230,11 +237,13 @@ class AWSInstance(BaseInstance):
 
     @property
     def public_ips(self):
-        return [self._ec2_instance.public_ip_address]
+        return ([self._ec2_instance.public_ip_address]
+                if self._ec2_instance.public_ip_address else [])
 
     @property
     def private_ips(self):
-        return [self._ec2_instance.private_ip_address]
+        return ([self._ec2_instance.private_ip_address]
+                if self._ec2_instance.private_ip_address else [])
 
     @property
     def vm_type_id(self):
@@ -259,6 +268,10 @@ class AWSInstance(BaseInstance):
     def zone_id(self):
         return self._ec2_instance.placement.get('AvailabilityZone')
 
+    @property
+    def subnet_id(self):
+        return self._ec2_instance.subnet_id
+
     @property
     def vm_firewalls(self):
         return [
@@ -289,24 +302,30 @@ class AWSInstance(BaseInstance):
         image.refresh()
         return image
 
+    def _get_fip(self, floating_ip):
+        """Get a floating IP object based on the supplied allocation ID."""
+        return AWSFloatingIP(
+            self._provider, list(self._provider.ec2_conn.vpc_addresses.filter(
+                AllocationIds=[floating_ip]))[0])
+
     def add_floating_ip(self, floating_ip):
-        fip = (
-            floating_ip if isinstance(floating_ip, AWSFloatingIP) else
-            self._provider.networking.floating_ips.get(floating_ip))
+        fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
+               else self._get_fip(floating_ip))
         params = trim_empty_params({
             'InstanceId': self.id,
-            'PublicIp': None if self._ec2_instance.vpc_id else fip.public_ip,
+            'PublicIp': None if self._ec2_instance.vpc_id else
+            fip.public_ip,
             # pylint:disable=protected-access
             'AllocationId': fip._ip.allocation_id})
         self._provider.ec2_conn.meta.client.associate_address(**params)
         self.refresh()
 
     def remove_floating_ip(self, floating_ip):
-        fip = (
-            floating_ip if isinstance(floating_ip, AWSFloatingIP) else
-            self._provider.networking.floating_ips.get(floating_ip))
+        fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
+               else self._get_fip(floating_ip))
         params = trim_empty_params({
-            'PublicIp': None if self._ec2_instance.vpc_id else fip.public_ip,
+            'PublicIp': None if self._ec2_instance.vpc_id else
+            fip.public_ip,
             # pylint:disable=protected-access
             'AssociationId': fip._ip.association_id})
         self._provider.ec2_conn.meta.client.disassociate_address(**params)
@@ -543,14 +562,6 @@ class AWSKeyPair(BaseKeyPair):
     def __init__(self, provider, key_pair):
         super(AWSKeyPair, self).__init__(provider, key_pair)
 
-    @property
-    def material(self):
-        # boto3 object will only have this field if the value is not empty
-        if hasattr(self._key_pair, 'key_material'):
-            return self._key_pair.key_material
-        else:
-            return None
-
 
 class AWSVMFirewall(BaseVMFirewall):
 
@@ -753,7 +764,7 @@ class AWSBucketObject(BaseBucketObject):
 
     @property
     def size(self):
-        return self._obj.size
+        return self._obj.content_length
 
     @property
     def last_modified(self):
@@ -823,17 +834,16 @@ class AWSBucketContainer(BaseBucketContainer):
         else:
             # pylint:disable=protected-access
             boto_objs = self.bucket._bucket.objects.all()
-        objects = [AWSBucketObject(self._provider, obj)
-                   for obj in boto_objs]
-
+        objects = [self.get(obj.key) for obj in boto_objs]
         return ClientPagedResultList(self._provider, objects,
                                      limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        objects = [obj for obj in self if obj.name == name]
-
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=None, marker=None)
 
     def create(self, name):
         # pylint:disable=protected-access
@@ -882,6 +892,7 @@ class AWSNetwork(BaseNetwork):
     def __init__(self, provider, network):
         super(AWSNetwork, self).__init__(provider)
         self._vpc = network
+        self._gtw_container = AWSGatewayContainer(provider, self)
 
     @property
     def id(self):
@@ -938,6 +949,10 @@ class AWSNetwork(BaseNetwork):
             VpcIds=[self.id])
         self.refresh()
 
+    @property
+    def gateways(self):
+        return self._gtw_container
+
 
 class AWSSubnet(BaseSubnet):
 
@@ -1000,6 +1015,31 @@ class AWSSubnet(BaseSubnet):
             self._subnet.state = SubnetState.UNKNOWN
 
 
+class AWSFloatingIPContainer(BaseFloatingIPContainer):
+
+    def __init__(self, provider, gateway):
+        super(AWSFloatingIPContainer, self).__init__(provider, gateway)
+        self.svc = BotoEC2Service(provider=self._provider,
+                                  cb_resource=AWSFloatingIP,
+                                  boto_collection_name='vpc_addresses')
+
+    def get(self, fip_id):
+        log.debug("Getting AWS Floating IP Service with the id: %s", fip_id)
+        return self.svc.get(fip_id)
+
+    def list(self, limit=None, marker=None):
+        log.debug("Listing all floating IPs under gateway %s", self.gateway)
+        return self.svc.list(limit=limit, marker=marker)
+
+    def create(self):
+        log.debug("Creating a floating IP under gateway %s", self.gateway)
+        ip = self._provider.ec2_conn.meta.client.allocate_address(
+            Domain='vpc')
+        return AWSFloatingIP(
+            self._provider,
+            self._provider.ec2_conn.VpcAddress(ip.get('AllocationId')))
+
+
 class AWSFloatingIP(BaseFloatingIP):
 
     def __init__(self, provider, floating_ip):
@@ -1025,6 +1065,9 @@ class AWSFloatingIP(BaseFloatingIP):
     def delete(self):
         self._ip.release()
 
+    def refresh(self):
+        self._ip.reload()
+
 
 class AWSRouter(BaseRouter):
 
@@ -1081,8 +1124,10 @@ class AWSRouter(BaseRouter):
     def attach_gateway(self, gateway):
         gw_id = (gateway.id if isinstance(gateway, AWSInternetGateway)
                  else gateway)
-        return self._provider.ec2_conn.meta.client.attach_internet_gateway(
-            InternetGatewayId=gw_id, VpcId=self._route_table.vpc_id)
+        if self._route_table.create_route(
+                DestinationCidrBlock='0.0.0.0/0', GatewayId=gw_id):
+            return True
+        return False
 
     def detach_gateway(self, gateway):
         gw_id = (gateway.id if isinstance(gateway, AWSInternetGateway)
@@ -1091,12 +1136,58 @@ class AWSRouter(BaseRouter):
             InternetGatewayId=gw_id, VpcId=self._route_table.vpc_id)
 
 
+class AWSGatewayContainer(BaseGatewayContainer):
+
+    def __init__(self, provider, network):
+        super(AWSGatewayContainer, self).__init__(provider, network)
+        self.svc = BotoEC2Service(provider=provider,
+                                  cb_resource=AWSInternetGateway,
+                                  boto_collection_name='internet_gateways')
+
+    def get_or_create_inet_gateway(self, name=None):
+        log.debug("Get or create inet gateway %s on net %s", name,
+                  self._network)
+        if name:
+            AWSInternetGateway.assert_valid_resource_name(name)
+
+        network_id = self._network.id if isinstance(
+            self._network, AWSNetwork) else self._network
+        # Don't filter by name because it may conflict with at least the
+        # default VPC that most accounts have but that network is typically
+        # without a name.
+        gtw = self.svc.find(filter_name='attachment.vpc-id',
+                            filter_value=network_id)
+        if gtw:
+            return gtw[0]  # There can be only one gtw attached to a VPC
+        # Gateway does not exist so create one and attach to the supplied net
+        cb_gateway = self.svc.create('create_internet_gateway')
+        if name:
+            cb_gateway.name = name
+        cb_gateway._gateway.attach_to_vpc(VpcId=network_id)
+        return cb_gateway
+
+    def delete(self, gateway):
+        log.debug("Service deleting AWS Gateway %s", gateway)
+        gateway_id = gateway.id if isinstance(
+            gateway, AWSInternetGateway) else gateway
+        gateway = self.svc.get(gateway_id)
+        if gateway:
+            gateway.delete()
+
+    def list(self, limit=None, marker=None):
+        log.debug("Listing current AWS internet gateways for net %s.",
+                  self._network.id)
+        fltr = [{'Name': 'attachment.vpc-id', 'Values': [self._network.id]}]
+        return self.svc.list(limit=None, marker=None, Filters=fltr)
+
+
 class AWSInternetGateway(BaseInternetGateway):
 
     def __init__(self, provider, gateway):
         super(AWSInternetGateway, self).__init__(provider)
         self._gateway = gateway
         self._gateway.state = ''
+        self._fips_container = AWSFloatingIPContainer(provider, self)
 
     @property
     def id(self):
@@ -1132,8 +1223,14 @@ class AWSInternetGateway(BaseInternetGateway):
         return None
 
     def delete(self):
+        if self.network_id:
+            self._gateway.detach_from_vpc(VpcId=self.network_id)
         self._gateway.delete()
 
+    @property
+    def floating_ips(self):
+        return self._fips_container
+
 
 class AWSLaunchConfig(BaseLaunchConfig):
 

+ 126 - 122
cloudbridge/cloud/providers/aws/services.py

@@ -4,11 +4,10 @@ import string
 
 from botocore.exceptions import ClientError
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.services import BaseBucketService
 from cloudbridge.cloud.base.services import BaseComputeService
-from cloudbridge.cloud.base.services import BaseFloatingIPService
-from cloudbridge.cloud.base.services import BaseGatewayService
 from cloudbridge.cloud.base.services import BaseImageService
 from cloudbridge.cloud.base.services import BaseInstanceService
 from cloudbridge.cloud.base.services import BaseKeyPairService
@@ -24,7 +23,7 @@ from cloudbridge.cloud.base.services import BaseVMFirewallService
 from cloudbridge.cloud.base.services import BaseVMTypeService
 from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.interfaces.exceptions \
-    import InvalidConfigurationException
+    import DuplicateResourceException, InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import PlacementZone
@@ -37,11 +36,8 @@ import requests
 
 from .helpers import BotoEC2Service
 from .helpers import BotoS3Service
-
 from .resources import AWSBucket
-from .resources import AWSFloatingIP
 from .resources import AWSInstance
-from .resources import AWSInternetGateway
 from .resources import AWSKeyPair
 from .resources import AWSLaunchConfig
 from .resources import AWSMachineImage
@@ -90,16 +86,34 @@ class AWSKeyPairService(BaseKeyPairService):
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for Key Pair %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='key-name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for Key Pair %s", name)
+        return self.svc.find(filter_name='key-name', filter_value=name)
 
-    def create(self, name):
+    def create(self, name, public_key_material=None):
         log.debug("Creating Key Pair Service %s", name)
         AWSKeyPair.assert_valid_resource_name(name)
-        return self.svc.create('create_key_pair', KeyName=name)
+        private_key = None
+        if not public_key_material:
+            public_key_material, private_key = cb_helpers.generate_key_pair()
+        try:
+            kp = self.svc.create('import_key_pair', KeyName=name,
+                                 PublicKeyMaterial=public_key_material)
+            kp.material = private_key
+            return kp
+        except ClientError as e:
+            if e.response['Error']['Code'] == 'InvalidKeyPair.Duplicate':
+                raise DuplicateResourceException(
+                    'Keypair already exists with name {0}'.format(name))
+            else:
+                raise e
 
 
 class AWSVMFirewallService(BaseVMFirewallService):
@@ -125,11 +139,16 @@ class AWSVMFirewallService(BaseVMFirewallService):
         return self.svc.create('create_security_group', GroupName=name,
                                Description=description, VpcId=network_id)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for Firewall Service %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='group-name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for Firewall Service %s", name)
+        return self.svc.find(filter_name='group-name', filter_value=name)
 
     def delete(self, firewall_id):
         log.info("Deleting Firewall Service with the id %s", firewall_id)
@@ -174,12 +193,16 @@ class AWSVolumeService(BaseVolumeService):
                   volume_id)
         return self.svc.get(volume_id)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Volume Service %s with "
-                  "the params  [Limit: %s Marker: %s]", name,
-                  limit, marker)
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Volume Service %s", name)
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
@@ -219,12 +242,16 @@ class AWSSnapshotService(BaseSnapshotService):
                   snapshot_id)
         return self.svc.get(snapshot_id)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Snapshot Service %s with "
-                  " the params [Limit: %s Marker: %s]", name,
-                  limit, marker)
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Snapshot Service %s", name)
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
@@ -287,21 +314,19 @@ class AWSBucketService(BaseBucketService):
         # For all other responses, it's assumed that the bucket does not exist.
         return None
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Bucket %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        buckets = [bucket
-                   for bucket in self
-                   if name == bucket.name]
-        return ClientPagedResultList(self.provider, buckets,
-                                     limit=limit, marker=marker)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=None, marker=None)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
     def create(self, name, location=None):
         log.debug("Creating AWS Bucket with the params "
-                  "[name: %s id: %s description: %s]", name, location)
+                  "[name: %s, location: %s]", name, location)
         AWSBucket.assert_valid_resource_name(name)
         loc_constraint = location or self.provider.region_name
         # Due to an API issue in S3, specifying us-east-1 as a
@@ -314,7 +339,7 @@ class AWSBucketService(BaseBucketService):
             return self.svc.create('create_bucket', Bucket=name,
                                    CreateBucketConfiguration={
                                        'LocationConstraint': loc_constraint
-                                    })
+                                   })
 
 
 class AWSImageService(BaseImageService):
@@ -329,11 +354,16 @@ class AWSImageService(BaseImageService):
         log.debug("Getting AWS Image Service with the id: %s", image_id)
         return self.svc.get(image_id)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Image Service %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Image Service %s", name)
+        return self.svc.find(filter_name='name', filter_value=name)
 
     def list(self, filter_by_owner=True, limit=None, marker=None):
         return self.svc.list(Owners=['self'] if filter_by_owner else [],
@@ -381,7 +411,7 @@ class AWSInstanceService(BaseInstanceService):
                   "[name: %s image: %s type: %s subnet: %s zone: %s "
                   "key pair: %s firewalls: %s user data: %s config %s "
                   "others: %s]", name, image, vm_type, subnet, zone,
-                  key_pair, vm_firewalls, user_data, launch_config, **kwargs)
+                  key_pair, vm_firewalls, user_data, launch_config, kwargs)
         AWSInstance.assert_valid_resource_name(name)
 
         image_id = image.id if isinstance(image, MachineImage) else image
@@ -493,7 +523,8 @@ class AWSInstanceService(BaseInstanceService):
                         raise InvalidConfigurationException(
                             "The source is none and the destination is a"
                             " volume. Therefore, you must specify a size.")
-                ebs_def['DeleteOnTermination'] = device.delete_on_terminate
+                ebs_def['DeleteOnTermination'] = device.delete_on_terminate \
+                    or True
                 if device.size:
                     ebs_def['VolumeSize'] = device.size
                 if ebs_def:
@@ -511,9 +542,15 @@ class AWSInstanceService(BaseInstanceService):
     def get(self, instance_id):
         return self.svc.get(instance_id)
 
-    def find(self, name, limit=None, marker=None):
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
@@ -583,9 +620,7 @@ class AWSNetworkingService(BaseNetworkingService):
         super(AWSNetworkingService, self).__init__(provider)
         self._network_service = AWSNetworkService(self.provider)
         self._subnet_service = AWSSubnetService(self.provider)
-        self._fip_service = AWSFloatingIPService(self.provider)
         self._router_service = AWSRouterService(self.provider)
-        self._gateway_service = AWSGatewayService(self.provider)
 
     @property
     def networks(self):
@@ -595,18 +630,10 @@ class AWSNetworkingService(BaseNetworkingService):
     def subnets(self):
         return self._subnet_service
 
-    @property
-    def floating_ips(self):
-        return self._fip_service
-
     @property
     def routers(self):
         return self._router_service
 
-    @property
-    def gateways(self):
-        return self._gateway_service
-
 
 class AWSNetworkService(BaseNetworkService):
 
@@ -624,11 +651,16 @@ class AWSNetworkService(BaseNetworkService):
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Network Service %s with the "
-                  " params [Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Network Service %s", name)
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def create(self, name, cidr_block):
         log.debug("Creating AWS Network Service with the params "
@@ -659,16 +691,21 @@ class AWSSubnetService(BaseSubnetService):
         network_id = network.id if isinstance(network, AWSNetwork) else network
         if network_id:
             return self.svc.find(
-                filter_name='VpcId', filter_value=network_id,
+                filter_name='vpc-id', filter_value=network_id,
                 limit=limit, marker=marker)
         else:
             return self.svc.list(limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Subnet Service %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Subnet Service %s", name)
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def create(self, name, network, cidr_block, zone):
         log.debug("Creating AWS Subnet Service with the params "
@@ -691,6 +728,14 @@ class AWSSubnetService(BaseSubnetService):
             snl = self.svc.find('availabilityZone', zone)
         else:
             snl = self.svc.list()
+
+        # Find first available default subnet by sorted order
+        # of availability zone. (e.g. prefer us-east-1a over 1e,
+        # This is because newer zones tend to have less compatibility
+        # with different instance types. (e.g. c5.large not available
+        # on us-east-1e as of 14 Dec. 2017
+        # pylint:disable=protected-access
+        snl.sort(key=lambda sn: sn._subnet.availability_zone)
         for sn in snl:
             # pylint:disable=protected-access
             if sn._subnet.default_for_az:
@@ -724,30 +769,6 @@ class AWSSubnetService(BaseSubnetService):
         self.svc.delete(subnet_id)
 
 
-class AWSFloatingIPService(BaseFloatingIPService):
-
-    def __init__(self, provider):
-        super(AWSFloatingIPService, self).__init__(provider)
-        self.svc = BotoEC2Service(provider=self.provider,
-                                  cb_resource=AWSFloatingIP,
-                                  boto_collection_name='vpc_addresses')
-
-    def get(self, router_id):
-        log.debug("Getting AWS Floating IP Service with the id: %s",
-                  router_id)
-        return self.svc.get(router_id)
-
-    def list(self, limit=None, marker=None):
-        return self.svc.list(limit=limit, marker=marker)
-
-    def create(self):
-        ip = self.provider.ec2_conn.meta.client.allocate_address(
-            Domain='vpc')
-        return AWSFloatingIP(
-            self.provider,
-            self.provider.ec2_conn.VpcAddress(ip.get('AllocationId')))
-
-
 class AWSRouterService(BaseRouterService):
     """For AWS, a CloudBridge router corresponds to an AWS Route Table."""
 
@@ -761,11 +782,16 @@ class AWSRouterService(BaseRouterService):
         log.debug("Getting AWS Router Service with the id: %s", router_id)
         return self.svc.get(router_id)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for AWS Router Service %s with the params "
-                  "[Limit: %s Marker: %s]", name, limit, marker)
-        return self.svc.find(filter_name='tag:Name', filter_value=name,
-                             limit=limit, marker=marker)
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        log.debug("Searching for AWS Router Service %s", name)
+        return self.svc.find(filter_name='tag:Name', filter_value=name)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
@@ -781,25 +807,3 @@ class AWSRouterService(BaseRouterService):
         if name:
             cb_router.name = name
         return cb_router
-
-
-class AWSGatewayService(BaseGatewayService):
-
-    def __init__(self, provider):
-        super(AWSGatewayService, self).__init__(provider)
-        self.svc = BotoEC2Service(provider=self.provider,
-                                  cb_resource=AWSInternetGateway,
-                                  boto_collection_name='internet_gateways')
-
-    def get_or_create_inet_gateway(self, name):
-        AWSInternetGateway.assert_valid_resource_name(name)
-
-        cb_gateway = self.svc.create('create_internet_gateway')
-        cb_gateway.name = name
-        return cb_gateway
-
-    def delete(self, gateway_id):
-        log.debug("Deleting AWS Gateway Service with the id %s", gateway_id)
-        gateway = self.svc.get(gateway_id)
-        if gateway:
-            gateway.delete()

+ 5 - 0
cloudbridge/cloud/providers/azure/__init__.py

@@ -0,0 +1,5 @@
+"""
+Exports from this provider
+"""
+
+from .provider import AzureCloudProvider  # noqa

+ 706 - 0
cloudbridge/cloud/providers/azure/azure_client.py

@@ -0,0 +1,706 @@
+import datetime
+import logging
+from io import BytesIO
+
+from azure.common.credentials import ServicePrincipalCredentials
+from azure.mgmt.compute import ComputeManagementClient
+from azure.mgmt.network import NetworkManagementClient
+from azure.mgmt.resource import ResourceManagementClient
+from azure.mgmt.resource.subscriptions import SubscriptionClient
+from azure.mgmt.storage import StorageManagementClient
+from azure.storage.blob import BlobPermissions
+from azure.storage.blob import BlockBlobService
+from azure.storage.table import TableService
+
+from . import helpers as azure_helpers
+
+log = logging.getLogger(__name__)
+
+IMAGE_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                    '{resourceGroupName}/providers/Microsoft.Compute/' \
+                    'images/{imageName}'
+NETWORK_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                     '{resourceGroupName}/providers/Microsoft.Network' \
+                     '/virtualNetworks/{virtualNetworkName}'
+NETWORK_INTERFACE_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
+                                'resourceGroups/{resourceGroupName}' \
+                                '/providers/Microsoft.Network/' \
+                                'networkInterfaces/{networkInterfaceName}'
+PUBLIC_IP_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups' \
+                        '/{resourceGroupName}/providers/Microsoft.Network' \
+                        '/publicIPAddresses/{publicIpAddressName}'
+SNAPSHOT_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                       '{resourceGroupName}/providers/Microsoft.Compute/' \
+                       'snapshots/{snapshotName}'
+SUBNET_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                     '{resourceGroupName}/providers/Microsoft.Network' \
+                     '/virtualNetworks/{virtualNetworkName}/subnets' \
+                     '/{subnetName}'
+VM_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                       '{resourceGroupName}/providers/Microsoft.Compute/' \
+                       'virtualMachines/{vmName}'
+VM_FIREWALL_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
+                             'resourceGroups/{resourceGroupName}/' \
+                             'providers/Microsoft.Network/' \
+                             'networkSecurityGroups/' \
+                             '{networkSecurityGroupName}'
+VM_FIREWALL_RULE_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
+                             'resourceGroups/{resourceGroupName}/' \
+                             'providers/Microsoft.Network/' \
+                             'networkSecurityGroups/' \
+                             '{networkSecurityGroupName}/' \
+                             'securityRules/{securityRuleName}'
+VOLUME_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
+                     '{resourceGroupName}/providers/Microsoft.Compute/' \
+                     'disks/{diskName}'
+
+IMAGE_NAME = 'imageName'
+NETWORK_NAME = 'virtualNetworkName'
+NETWORK_INTERFACE_NAME = 'networkInterfaceName'
+PUBLIC_IP_NAME = 'publicIpAddressName'
+SNAPSHOT_NAME = 'snapshotName'
+SUBNET_NAME = 'subnetName'
+VM_NAME = 'vmName'
+VM_FIREWALL_NAME = 'networkSecurityGroupName'
+VM_FIREWALL_RULE_NAME = 'securityRuleName'
+VOLUME_NAME = 'diskName'
+
+
+class AzureClient(object):
+    """
+    Azure client is the wrapper on top of azure python sdk
+    """
+    def __init__(self, config):
+        self._config = config
+        self.subscription_id = config.get('azure_subscription_id')
+        self._credentials = ServicePrincipalCredentials(
+            client_id=config.get('azure_client_id'),
+            secret=config.get('azure_secret'),
+            tenant=config.get('azure_tenant')
+        )
+
+        self._resource_client = None
+        self._storage_client = None
+        self._network_management_client = None
+        self._subscription_client = None
+        self._compute_client = None
+        self._access_key_result = None
+        self._block_blob_service = None
+        self._table_service = None
+
+        log.debug("azure subscription : %s", self.subscription_id)
+
+    @property
+    def access_key_result(self):
+        if not self._access_key_result:
+            self._access_key_result = self.storage_client.storage_accounts. \
+                list_keys(self.resource_group, self.storage_account)
+        return self._access_key_result
+
+    @property
+    def resource_group(self):
+        return self._config.get('azure_resource_group')
+
+    @property
+    def storage_account(self):
+        return self._config.get('azure_storage_account')
+
+    @property
+    def region_name(self):
+        return self._config.get('azure_region_name')
+
+    @property
+    def public_key_storage_table_name(self):
+        return self._config.get('azure_public_key_storage_table_name')
+
+    @property
+    def storage_client(self):
+        if not self._storage_client:
+            self._storage_client = \
+                StorageManagementClient(self._credentials,
+                                        self.subscription_id)
+        return self._storage_client
+
+    @property
+    def subscription_client(self):
+        if not self._subscription_client:
+            self._subscription_client = SubscriptionClient(self._credentials)
+        return self._subscription_client
+
+    @property
+    def resource_client(self):
+        if not self._resource_client:
+            self._resource_client = \
+                ResourceManagementClient(self._credentials,
+                                         self.subscription_id)
+        return self._resource_client
+
+    @property
+    def compute_client(self):
+        if not self._compute_client:
+            self._compute_client = \
+                ComputeManagementClient(self._credentials,
+                                        self.subscription_id)
+        return self._compute_client
+
+    @property
+    def network_management_client(self):
+        if not self._network_management_client:
+            self._network_management_client = NetworkManagementClient(
+                self._credentials, self.subscription_id)
+        return self._network_management_client
+
+    @property
+    def blob_service(self):
+        if not self._block_blob_service:
+            self._block_blob_service = BlockBlobService(
+                self.storage_account,
+                self.access_key_result.keys[0].value)
+        return self._block_blob_service
+
+    @property
+    def table_service(self):
+        if not self._table_service:
+            self._table_service = TableService(
+                self.storage_account,
+                self.access_key_result.keys[0].value)
+        if not self._table_service. \
+                exists(table_name=self.public_key_storage_table_name):
+            self._table_service.create_table(
+                self.public_key_storage_table_name)
+        return self._table_service
+
+    def get_resource_group(self, name):
+        return self.resource_client.resource_groups.get(name)
+
+    def create_resource_group(self, name, parameters):
+        return self.resource_client.resource_groups. \
+            create_or_update(name, parameters)
+
+    def get_storage_account(self, storage_account):
+        return self.storage_client.storage_accounts. \
+            get_properties(self.resource_group, storage_account)
+
+    def create_storage_account(self, name, params):
+        return self.storage_client.storage_accounts. \
+            create(self.resource_group, name.lower(), params).result()
+
+    def list_locations(self):
+        return self.subscription_client.subscriptions. \
+            list_locations(self.subscription_id)
+
+    def list_vm_firewall(self):
+        return self.network_management_client.network_security_groups. \
+            list(self.resource_group)
+
+    def create_vm_firewall(self, name, parameters):
+        return self.network_management_client.network_security_groups. \
+            create_or_update(self.resource_group, name,
+                             parameters).result()
+
+    def update_vm_firewall_tags(self, fw_id, tags):
+        url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
+                                             fw_id)
+        name = url_params.get(VM_FIREWALL_NAME)
+        return self.network_management_client.network_security_groups. \
+            create_or_update(self.resource_group, name,
+                             {'tags': tags,
+                              'location': self.region_name}).result()
+
+    def get_vm_firewall(self, fw_id):
+        url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
+                                             fw_id)
+        fw_name = url_params.get(VM_FIREWALL_NAME)
+        return self.network_management_client.network_security_groups. \
+            get(self.resource_group, fw_name)
+
+    def delete_vm_firewall(self, fw_id):
+        url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
+                                             fw_id)
+        name = url_params.get(VM_FIREWALL_NAME)
+        self.network_management_client \
+            .network_security_groups.delete(self.resource_group, name).wait()
+
+    def create_vm_firewall_rule(self, fw_id,
+                                rule_name, parameters):
+        url_params = azure_helpers.parse_url(VM_FIREWALL_RESOURCE_ID,
+                                             fw_id)
+        vm_firewall_name = url_params.get(VM_FIREWALL_NAME)
+        return self.network_management_client.security_rules. \
+            create_or_update(self.resource_group, vm_firewall_name,
+                             rule_name, parameters).result()
+
+    def delete_vm_firewall_rule(self, fw_rule_id, vm_firewall):
+        url_params = azure_helpers.parse_url(VM_FIREWALL_RULE_RESOURCE_ID,
+                                             fw_rule_id)
+        name = url_params.get(VM_FIREWALL_RULE_NAME)
+        return self.network_management_client.security_rules. \
+            delete(self.resource_group, vm_firewall, name).result()
+
+    def list_containers(self, prefix=None):
+        return self.blob_service.list_containers(prefix=prefix)
+
+    def create_container(self, container_name):
+        self.blob_service.create_container(container_name)
+        return self.blob_service.get_container_properties(container_name)
+
+    def get_container(self, container_name):
+        return self.blob_service.get_container_properties(container_name)
+
+    def delete_container(self, container_name):
+        self.blob_service.delete_container(container_name)
+
+    def list_blobs(self, container_name, prefix=None):
+        return self.blob_service.list_blobs(container_name, prefix=prefix)
+
+    def get_blob(self, container_name, blob_name):
+        return self.blob_service.get_blob_properties(container_name, blob_name)
+
+    def create_blob_from_text(self, container_name, blob_name, text):
+        self.blob_service.create_blob_from_text(container_name,
+                                                blob_name, text)
+
+    def create_blob_from_file(self, container_name, blob_name, file_path):
+        self.blob_service.create_blob_from_path(container_name,
+                                                blob_name, file_path)
+
+    def delete_blob(self, container_name, blob_name):
+        self.blob_service.delete_blob(container_name, blob_name)
+
+    def get_blob_url(self, container_name, blob_name, expiry_time):
+        expiry_date = datetime.datetime.now() + datetime.timedelta(
+            seconds=expiry_time)
+        sas = self.blob_service.generate_blob_shared_access_signature(
+            container_name, blob_name, permission=BlobPermissions.READ,
+            expiry=expiry_date)
+        return self.blob_service.make_blob_url(container_name, blob_name,
+                                               sas_token=sas)
+
+    def get_blob_content(self, container_name, blob_name):
+        out_stream = BytesIO()
+        self.blob_service.get_blob_to_stream(container_name,
+                                             blob_name, out_stream)
+        return out_stream
+
+    def create_empty_disk(self, disk_name, params):
+        return self.compute_client.disks.create_or_update(
+            self.resource_group,
+            disk_name,
+            params
+        ).result()
+
+    def create_snapshot_disk(self, disk_name, params):
+        return self.compute_client.disks.create_or_update(
+            self.resource_group,
+            disk_name,
+            params
+        ).result()
+
+    def get_disk(self, disk_id):
+        url_params = azure_helpers.parse_url(VOLUME_RESOURCE_ID,
+                                             disk_id)
+        disk_name = url_params.get(VOLUME_NAME)
+        return self.compute_client.disks.get(self.resource_group, disk_name)
+
+    def list_disks(self):
+        return self.compute_client.disks. \
+            list_by_resource_group(self.resource_group)
+
+    def delete_disk(self, disk_id):
+        url_params = azure_helpers.parse_url(VOLUME_RESOURCE_ID,
+                                             disk_id)
+        disk_name = url_params.get(VOLUME_NAME)
+        self.compute_client.disks.delete(self.resource_group, disk_name).wait()
+
+    def update_disk_tags(self, disk_id, tags):
+        url_params = azure_helpers.parse_url(VOLUME_RESOURCE_ID,
+                                             disk_id)
+        disk_name = url_params.get(VOLUME_NAME)
+        return self.compute_client.disks.update(
+            self.resource_group,
+            disk_name,
+            {'tags': tags},
+            raw=True
+        )
+
+    def list_snapshots(self):
+        return self.compute_client.snapshots. \
+            list_by_resource_group(self.resource_group)
+
+    def get_snapshot(self, snapshot_id):
+        url_params = azure_helpers.parse_url(SNAPSHOT_RESOURCE_ID,
+                                             snapshot_id)
+        snapshot_name = url_params.get(SNAPSHOT_NAME)
+        return self.compute_client.snapshots.get(self.resource_group,
+                                                 snapshot_name)
+
+    def create_snapshot(self, snapshot_name, params):
+        return self.compute_client.snapshots.create_or_update(
+            self.resource_group,
+            snapshot_name,
+            params
+        ).result()
+
+    def delete_snapshot(self, snapshot_id):
+        url_params = azure_helpers.parse_url(SNAPSHOT_RESOURCE_ID,
+                                             snapshot_id)
+        snapshot_name = url_params.get(SNAPSHOT_NAME)
+        self.compute_client.snapshots.delete(self.resource_group,
+                                             snapshot_name).wait()
+
+    def update_snapshot_tags(self, snapshot_id, tags):
+        url_params = azure_helpers.parse_url(SNAPSHOT_RESOURCE_ID,
+                                             snapshot_id)
+        snapshot_name = url_params.get(SNAPSHOT_NAME)
+        return self.compute_client.snapshots.update(
+            self.resource_group,
+            snapshot_name,
+            {'tags': tags},
+            raw=True
+        )
+
+    def create_image(self, name, params):
+        return self.compute_client.images. \
+            create_or_update(self.resource_group, name,
+                             params).result()
+
+    def delete_image(self, image_id):
+        url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
+                                             image_id)
+        name = url_params.get(IMAGE_NAME)
+        self.compute_client.images.delete(self.resource_group, name).wait()
+
+    def list_images(self):
+        return self.compute_client.images. \
+            list_by_resource_group(self.resource_group)
+
+    def get_image(self, image_id):
+        url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
+                                             image_id)
+        name = url_params.get(IMAGE_NAME)
+        return self.compute_client.images.get(self.resource_group, name)
+
+    def update_image_tags(self, image_id, tags):
+        url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
+                                             image_id)
+        name = url_params.get(IMAGE_NAME)
+        return self.compute_client.images. \
+            create_or_update(self.resource_group, name,
+                             {
+                                 'tags': tags,
+                                 'location': self.region_name
+                             }).result()
+
+    def list_vm_types(self):
+        return self.compute_client.virtual_machine_sizes. \
+            list(self.region_name)
+
+    def list_networks(self):
+        return self.network_management_client.virtual_networks.list(
+            self.resource_group)
+
+    def get_network(self, network_id):
+        url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID,
+                                             network_id)
+        network_name = url_params.get(NETWORK_NAME)
+        return self.network_management_client.virtual_networks.get(
+            self.resource_group, network_name)
+
+    def create_network(self, name, params):
+        return self.network_management_client.virtual_networks. \
+            create_or_update(self.resource_group,
+                             name,
+                             parameters=params).result()
+
+    def delete_network(self, network_id):
+        url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
+        network_name = url_params.get(NETWORK_NAME)
+        return self.network_management_client.virtual_networks. \
+            delete(self.resource_group, network_name).wait()
+
+    def update_network_tags(self, network_id, tags):
+        url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
+        network_name = url_params.get(NETWORK_NAME)
+        return self.network_management_client.virtual_networks. \
+            create_or_update(self.resource_group,
+                             network_name, tags).result()
+
+    def get_network_id_for_subnet(self, subnet_id):
+        url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID, subnet_id)
+        network_id = NETWORK_RESOURCE_ID
+        for key, val in url_params.items():
+            network_id = network_id.replace("{" + key + "}", val)
+        return network_id
+
+    def list_subnets(self, network_id):
+        url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
+        network_name = url_params.get(NETWORK_NAME)
+        return self.network_management_client.subnets. \
+            list(self.resource_group, network_name)
+
+    def get_subnet(self, subnet_id):
+        url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID,
+                                             subnet_id)
+        network_name = url_params.get(NETWORK_NAME)
+        subnet_name = url_params.get(SUBNET_NAME)
+        return self.network_management_client.subnets. \
+            get(self.resource_group, network_name, subnet_name)
+
+    def create_subnet(self, network_id, subnet_name, params):
+        url_params = azure_helpers.parse_url(NETWORK_RESOURCE_ID, network_id)
+        network_name = url_params.get(NETWORK_NAME)
+        result_create = self.network_management_client \
+            .subnets.create_or_update(
+                self.resource_group,
+                network_name,
+                subnet_name,
+                params
+            )
+        subnet_info = result_create.result()
+
+        return subnet_info
+
+    def delete_subnet(self, subnet_id):
+        url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID,
+                                             subnet_id)
+        network_name = url_params.get(NETWORK_NAME)
+        subnet_name = url_params.get(SUBNET_NAME)
+        result_delete = self.network_management_client \
+            .subnets.delete(
+                self.resource_group,
+                network_name,
+                subnet_name
+            )
+        result_delete.wait()
+
+    def create_floating_ip(self, public_ip_name, public_ip_parameters):
+        return self.network_management_client.public_ip_addresses. \
+            create_or_update(self.resource_group,
+                             public_ip_name,
+                             public_ip_parameters).result()
+
+    def get_floating_ip(self, public_ip_id):
+        url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
+                                             public_ip_id)
+        public_ip_name = url_params.get(PUBLIC_IP_NAME)
+        return self.network_management_client. \
+            public_ip_addresses.get(self.resource_group, public_ip_name)
+
+    def delete_floating_ip(self, public_ip_id):
+        url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
+                                             public_ip_id)
+        public_ip_name = url_params.get(PUBLIC_IP_NAME)
+        self.network_management_client. \
+            public_ip_addresses.delete(self.resource_group,
+                                       public_ip_name).wait()
+
+    def list_floating_ips(self):
+        return self.network_management_client.public_ip_addresses.list(
+            self.resource_group)
+
+    def list_vm(self):
+        return self.compute_client.virtual_machines.list(
+            self.resource_group
+        )
+
+    def restart_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        return self.compute_client.virtual_machines.restart(
+            self.resource_group, vm_name).wait()
+
+    def delete_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        return self.compute_client.virtual_machines.delete(
+            self.resource_group, vm_name).wait()
+
+    def get_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        return self.compute_client.virtual_machines.get(
+            self.resource_group,
+            vm_name,
+            expand='instanceView'
+        )
+
+    def create_vm(self, vm_name, params):
+        return self.compute_client.virtual_machines. \
+            create_or_update(self.resource_group,
+                             vm_name, params).result()
+
+    def update_vm(self, vm_id, params):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        return self.compute_client.virtual_machines. \
+            create_or_update(self.resource_group,
+                             vm_name, params, raw=True)
+
+    def deallocate_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        self.compute_client. \
+            virtual_machines.deallocate(self.resource_group,
+                                        vm_name).wait()
+
+    def generalize_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        self.compute_client.virtual_machines. \
+            generalize(self.resource_group, vm_name)
+
+    def start_vm(self, vm_id):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        self.compute_client.virtual_machines. \
+            start(self.resource_group,
+                  vm_name).wait()
+
+    def update_vm_tags(self, vm_id, tags):
+        url_params = azure_helpers.parse_url(VM_RESOURCE_ID,
+                                             vm_id)
+        vm_name = url_params.get(VM_NAME)
+        self.compute_client.virtual_machines. \
+            create_or_update(self.resource_group,
+                             vm_name, tags).result()
+
+    def delete_nic(self, nic_id):
+        nic_params = azure_helpers.\
+            parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
+        nic_name = nic_params.get(NETWORK_INTERFACE_NAME)
+        self.network_management_client. \
+            network_interfaces.delete(self.resource_group,
+                                      nic_name).wait()
+
+    def get_nic(self, nic_id):
+        nic_params = azure_helpers.\
+            parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
+        nic_name = nic_params.get(NETWORK_INTERFACE_NAME)
+        return self.network_management_client. \
+            network_interfaces.get(self.resource_group, nic_name)
+
+    def update_nic(self, nic_id, params):
+        nic_params = azure_helpers.\
+            parse_url(NETWORK_INTERFACE_RESOURCE_ID, nic_id)
+        nic_name = nic_params.get(NETWORK_INTERFACE_NAME)
+        async_nic_creation = self.network_management_client. \
+            network_interfaces.create_or_update(
+                self.resource_group,
+                nic_name,
+                params
+            )
+        nic_info = async_nic_creation.result()
+        return nic_info
+
+    def create_nic(self, nic_name, params):
+        return self.network_management_client. \
+            network_interfaces.create_or_update(
+                self.resource_group,
+                nic_name,
+                params
+            ).result()
+
+    def create_public_key(self, entity):
+        return self.table_service. \
+            insert_or_replace_entity(self.public_key_storage_table_name,
+                                     entity)
+
+    def get_public_key(self, name):
+        entities = self.table_service. \
+            query_entities(self.public_key_storage_table_name,
+                           "Name eq '{0}'".format(name), num_results=1)
+
+        return entities.items[0] if len(entities.items) > 0 else None
+
+    def delete_public_key(self, entity):
+        self.table_service.delete_entity(self.public_key_storage_table_name,
+                                         entity.PartitionKey, entity.RowKey)
+
+    def list_public_keys(self, partition_key, limit=None, marker=None):
+        entities = self.table_service. \
+            query_entities(self.public_key_storage_table_name,
+                           "PartitionKey eq '{0}'".format(partition_key),
+                           marker=marker, num_results=limit)
+        return (entities.items, entities.next_marker)
+
+    def delete_route_table(self, route_table_name):
+        self.network_management_client. \
+            route_tables.delete(self.resource_group, route_table_name
+                                ).wait()
+
+    def attach_subnet_to_route_table(self, subnet_id, route_table_id):
+        url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID,
+                                             subnet_id)
+        network_name = url_params.get(NETWORK_NAME)
+        subnet_name = url_params.get(SUBNET_NAME)
+
+        subnet_info = self.network_management_client.subnets.get(
+            self.resource_group,
+            network_name,
+            subnet_name
+        )
+        if subnet_info:
+            subnet_info.route_table = {
+                'id': route_table_id
+            }
+
+            result_create = self.network_management_client. \
+                subnets.create_or_update(
+                 self.resource_group,
+                 network_name,
+                 subnet_name,
+                 subnet_info)
+            subnet_info = result_create.result()
+
+        return subnet_info
+
+    def detach_subnet_to_route_table(self, subnet_id, route_table_id):
+        url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID,
+                                             subnet_id)
+        network_name = url_params.get(NETWORK_NAME)
+        subnet_name = url_params.get(SUBNET_NAME)
+
+        subnet_info = self.network_management_client.subnets.get(
+            self.resource_group,
+            network_name,
+            subnet_name
+        )
+
+        if subnet_info and subnet_info.route_table.id == route_table_id:
+            subnet_info.route_table = None
+
+            result_create = self.network_management_client. \
+                subnets.create_or_update(
+                 self.resource_group,
+                 network_name,
+                 subnet_name,
+                 subnet_info)
+            subnet_info = result_create.result()
+
+        return subnet_info
+
+    def list_route_tables(self):
+        return self.network_management_client. \
+            route_tables.list(self.resource_group)
+
+    def get_route_table(self, router_id):
+        return self.network_management_client. \
+            route_tables.get(self.resource_group, router_id)
+
+    def create_route_table(self, route_table_name, params):
+        return self.network_management_client. \
+            route_tables.create_or_update(
+             self.resource_group,
+             route_table_name, params).result()
+
+    def update_route_table_tags(self, route_table_name, tags):
+        self.network_management_client.route_tables. \
+            create_or_update(self.resource_group,
+                             route_table_name, tags).result()

+ 41 - 0
cloudbridge/cloud/providers/azure/helpers.py

@@ -0,0 +1,41 @@
+from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
+
+
+def filter_by_tag(list_items, filters):
+    """
+    This function filter items on the tags
+    :param list_items:
+    :param filters:
+    :return:
+    """
+    filtered_list = []
+    if filters:
+        for obj in list_items:
+            for key in filters:
+                if obj.tags and filters[key] in obj.tags.get(key, ''):
+                    filtered_list.append(obj)
+
+        return filtered_list
+    else:
+        return list_items
+
+
+def parse_url(template_url, original_url):
+    """
+    In Azure all the resource IDs are returned as URIs.
+    ex: '/subscriptions/{subscriptionId}/resourceGroups/' \
+       '{resourceGroupName}/providers/Microsoft.Compute/' \
+       'virtualMachines/{vmName}'
+    This function splits the resource ID based on the template url passed
+    and returning the dictionary.
+    """
+    template_url_parts = template_url.split('/')
+    original_url_parts = original_url.split('/')
+    if len(template_url_parts) != len(original_url_parts):
+        raise InvalidValueException(template_url, original_url)
+    resource_param = {}
+    for key, value in zip(template_url_parts, original_url_parts):
+        if key.startswith('{') and key.endswith('}'):
+            resource_param.update({key[1:-1]: value})
+
+    return resource_param

+ 126 - 0
cloudbridge/cloud/providers/azure/provider.py

@@ -0,0 +1,126 @@
+import logging
+import os
+
+from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.providers.azure.azure_client import AzureClient
+from cloudbridge.cloud.providers.azure.services \
+    import AzureComputeService, AzureNetworkingService, \
+    AzureSecurityService, AzureStorageService
+
+from msrestazure.azure_exceptions import CloudError
+
+log = logging.getLogger(__name__)
+
+
+class AzureCloudProvider(BaseCloudProvider):
+    PROVIDER_ID = 'azure'
+
+    def __init__(self, config):
+        super(AzureCloudProvider, self).__init__(config)
+
+        # mandatory config values
+        self.subscription_id = self. \
+            _get_config_value('azure_subscription_id',
+                              os.environ.get('AZURE_SUBSCRIPTION_ID', None))
+        self.client_id = self._get_config_value(
+            'azure_client_id', os.environ.get('AZURE_CLIENT_ID', None))
+        self.secret = self._get_config_value(
+            'azure_secret', os.environ.get('AZURE_SECRET', None))
+        self.tenant = self._get_config_value(
+            'azure_tenant', os.environ.get('AZURE_TENANT', None))
+
+        # optional config values
+        self.region_name = self._get_config_value(
+            'azure_region_name', os.environ.get('AZURE_REGION_NAME',
+                                                'eastus'))
+        self.resource_group = self._get_config_value(
+            'azure_resource_group', os.environ.get('AZURE_RESOURCE_GROUP',
+                                                   'cloudbridge'))
+        # Storage account name is limited to a max length of 24 characters
+        # so take part of the client id to keep it unique
+        self.storage_account = self._get_config_value(
+            'azure_storage_account',
+            os.environ.get('AZURE_STORAGE_ACCOUNT',
+                           'storageacc' + self.client_id[-12:]))
+
+        self.vm_default_user_name = self._get_config_value(
+            'azure_vm_default_user_name', os.environ.get
+            ('AZURE_VM_DEFAULT_USER_NAME', 'cbuser'))
+
+        self.public_key_storage_table_name = self._get_config_value(
+            'azure_public_key_storage_table_name', os.environ.get
+            ('AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME', 'cbcerts'))
+
+        self._azure_client = None
+
+        self._security = AzureSecurityService(self)
+        self._storage = AzureStorageService(self)
+        self._compute = AzureComputeService(self)
+        self._networking = AzureNetworkingService(self)
+
+    @property
+    def compute(self):
+        return self._compute
+
+    @property
+    def networking(self):
+        return self._networking
+
+    @property
+    def security(self):
+        return self._security
+
+    @property
+    def storage(self):
+        return self._storage
+
+    @property
+    def azure_client(self):
+        if not self._azure_client:
+
+            # create a dict with both optional and mandatory configuration
+            # values to pass to the azureclient class, rather
+            # than passing the provider object and taking a dependency.
+
+            provider_config = {
+                'azure_subscription_id': self.subscription_id,
+                'azure_client_id': self.client_id,
+                'azure_secret': self.secret,
+                'azure_tenant': self.tenant,
+                'azure_region_name': self.region_name,
+                'azure_resource_group': self.resource_group,
+                'azure_storage_account': self.storage_account,
+                'azure_public_key_storage_table_name':
+                    self.public_key_storage_table_name
+            }
+
+            self._azure_client = AzureClient(provider_config)
+            self._initialize()
+        return self._azure_client
+
+    def _initialize(self):
+        """
+        Verifying that resource group and storage account exists
+        if not create one with the name provided in the
+        configuration
+        """
+        try:
+            self._azure_client.get_resource_group(self.resource_group)
+        except CloudError:
+            resource_group_params = {'location': self.region_name}
+            self._azure_client.create_resource_group(self.resource_group,
+                                                     resource_group_params)
+
+        try:
+            self._azure_client.get_storage_account(self.storage_account)
+        except CloudError:
+            storage_account_params = {
+                'sku': {
+                    'name': 'Standard_LRS'
+                },
+                'kind': 'storage',
+                'location': self.region_name,
+            }
+            self._azure_client. \
+                create_storage_account(self.storage_account,
+                                       storage_account_params)

+ 1664 - 0
cloudbridge/cloud/providers/azure/resources.py

@@ -0,0 +1,1664 @@
+"""
+DataTypes used by this provider
+"""
+import collections
+import logging
+import uuid
+
+from azure.common import AzureException
+from azure.mgmt.network.models import NetworkSecurityGroup
+
+import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.resources import BaseAttachmentInfo, \
+    BaseBucket, BaseBucketContainer, BaseBucketObject, BaseFloatingIP, \
+    BaseFloatingIPContainer, BaseGatewayContainer, BaseInstance, \
+    BaseInternetGateway, BaseKeyPair, BaseLaunchConfig, \
+    BaseMachineImage, BaseNetwork, BasePlacementZone, BaseRegion, BaseRouter, \
+    BaseSnapshot, BaseSubnet, BaseVMFirewall, BaseVMFirewallRule, \
+    BaseVMFirewallRuleContainer, BaseVMType, BaseVolume, ClientPagedResultList
+from cloudbridge.cloud.interfaces import InstanceState, VolumeState
+from cloudbridge.cloud.interfaces.resources import Instance, \
+    MachineImageState, NetworkState, RouterState, \
+    SnapshotState, SubnetState, TrafficDirection
+
+from msrestazure.azure_exceptions import CloudError
+
+import pysftp
+
+log = logging.getLogger(__name__)
+
+
+class AzureVMFirewall(BaseVMFirewall):
+    def __init__(self, provider, vm_firewall):
+        super(AzureVMFirewall, self).__init__(provider, vm_firewall)
+        self._vm_firewall = vm_firewall
+        self._vm_firewall.tags = self._vm_firewall.tags or {}
+        self._rule_container = AzureVMFirewallRuleContainer(provider, self)
+
+    @property
+    def network_id(self):
+        return None
+
+    @property
+    def resource_id(self):
+        return self._vm_firewall.id
+
+    @property
+    def id(self):
+        return self._vm_firewall.id
+
+    @property
+    def name(self):
+        return self._vm_firewall.tags.get('Name', self._vm_firewall.name)
+
+    @name.setter
+    def name(self, value):
+        self.assert_valid_resource_name(value)
+        self._vm_firewall.tags.update(Name=value)
+        self._provider.azure_client.update_vm_firewall_tags(
+            self.id, self._vm_firewall.tags)
+
+    @property
+    def description(self):
+        return self._vm_firewall.tags.get('Description')
+
+    @description.setter
+    def description(self, value):
+        self._vm_firewall.tags.update(Description=value)
+        self._provider.azure_client.\
+            update_vm_firewall_tags(self.id,
+                                    self._vm_firewall.tags)
+
+    @property
+    def rules(self):
+        return self._rule_container
+
+    def delete(self):
+        self._provider.azure_client.delete_vm_firewall(self.id)
+
+    def refresh(self):
+        """
+        Refreshes the security group with tags if required.
+        """
+        try:
+            self._vm_firewall = self._provider.azure_client. \
+                get_vm_firewall(self.id)
+            if not self._vm_firewall.tags:
+                self._vm_firewall.tags = {}
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The security group no longer exists and cannot be refreshed.
+
+    def to_json(self):
+        js = super(AzureVMFirewall, self).to_json()
+        json_rules = [r.to_json() for r in self.rules]
+        js['rules'] = json_rules
+        if js.get('network_id'):
+            js.pop('network_id')  # Omit for consistency across cloud providers
+        return js
+
+
+class AzureVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
+
+    def __init__(self, provider, firewall):
+        super(AzureVMFirewallRuleContainer, self).__init__(provider, firewall)
+
+    def list(self, limit=None, marker=None):
+        # Filter out firewall rules with priority < 3500 because values
+        # between 3500 and 4096 are assumed to be owned by cloudbridge
+        # default rules.
+        # pylint:disable=protected-access
+        rules = [AzureVMFirewallRule(self.firewall, rule) for rule
+                 in self.firewall._vm_firewall.security_rules
+                 if rule.priority < 3500]
+        return ClientPagedResultList(self._provider, rules,
+                                     limit=limit, marker=marker)
+
+    def create(self, direction, protocol=None, from_port=None, to_port=None,
+               cidr=None, src_dest_fw=None):
+        if protocol and from_port and to_port:
+            return self._create_rule(direction, protocol, from_port,
+                                     to_port, cidr)
+        elif src_dest_fw:
+            result = None
+            fw = (self._provider.security.vm_firewalls.get(src_dest_fw)
+                  if isinstance(src_dest_fw, str) else src_dest_fw)
+            for rule in fw.rules:
+                result = self._create_rule(
+                    rule.direction, rule.protocol, rule.from_port,
+                    rule.to_port, rule.cidr)
+            return result
+        else:
+            return None
+
+    def _create_rule(self, direction, protocol, from_port, to_port, cidr):
+
+        # If cidr is None, default values is set as 0.0.0.0/0
+        if not cidr:
+            cidr = '0.0.0.0/0'
+
+        count = len(self.firewall._vm_firewall.security_rules) + 1
+        rule_name = "Rule - " + str(count)
+        priority = 1000 + count
+        destination_port_range = str(from_port) + "-" + str(to_port)
+        source_port_range = '*'
+        destination_address_prefix = "*"
+        access = "Allow"
+        direction = ("Inbound" if direction == TrafficDirection.INBOUND
+                     else "Outbound")
+        parameters = {"priority": priority,
+                      "protocol": protocol,
+                      "source_port_range": source_port_range,
+                      "source_address_prefix": cidr,
+                      "destination_port_range": destination_port_range,
+                      "destination_address_prefix": destination_address_prefix,
+                      "access": access,
+                      "direction": direction}
+        result = self._provider.azure_client. \
+            create_vm_firewall_rule(self.firewall.id,
+                                    rule_name, parameters)
+        # pylint:disable=protected-access
+        self.firewall._vm_firewall.security_rules.append(result)
+        return AzureVMFirewallRule(self.firewall, result)
+
+
+# Tuple for port range
+PortRange = collections.namedtuple('PortRange', ['from_port', 'to_port'])
+
+
+class AzureVMFirewallRule(BaseVMFirewallRule):
+    def __init__(self, parent_fw, rule):
+        super(AzureVMFirewallRule, self).__init__(parent_fw, rule)
+
+    @property
+    def id(self):
+        return self._rule.id
+
+    @property
+    def direction(self):
+        return (TrafficDirection.INBOUND if self._rule.direction == "Inbound"
+                else TrafficDirection.OUTBOUND)
+
+    @property
+    def name(self):
+        return self._rule.name
+
+    @property
+    def protocol(self):
+        return self._rule.protocol
+
+    @property
+    def from_port(self):
+        return self._port_range_tuple.from_port
+
+    @property
+    def to_port(self):
+        return self._port_range_tuple.to_port
+
+    @property
+    def _port_range_tuple(self):
+        if self._rule.destination_port_range == '*':
+            return PortRange(1, 65535)
+        destination_port_range = self._rule.destination_port_range
+        port_range_split = destination_port_range.split('-', 1)
+        return PortRange(int(port_range_split[0]), int(port_range_split[1]))
+
+    @property
+    def cidr(self):
+        return self._rule.source_address_prefix
+
+    @property
+    def src_dest_fw_id(self):
+        return self.firewall.id
+
+    @property
+    def src_dest_fw(self):
+        return self.firewall
+
+    def delete(self):
+        vm_firewall = self.firewall.name
+        self._provider.azure_client. \
+            delete_vm_firewall_rule(self.id, vm_firewall)
+        for i, o in enumerate(self.firewall._vm_firewall.security_rules):
+            if o.id == self.id:
+                del self.firewall._vm_firewall.security_rules[i]
+                break
+
+
+class AzureBucketObject(BaseBucketObject):
+    def __init__(self, provider, container, key):
+        super(AzureBucketObject, self).__init__(provider)
+        self._container = container
+        self._key = key
+
+    @property
+    def id(self):
+        return self._key.name
+
+    @property
+    def name(self):
+        """
+        Get this object's name.
+        """
+        return self._key.name
+
+    @property
+    def size(self):
+        """
+        Get this object's size.
+        """
+        return self._key.properties.content_length
+
+    @property
+    def last_modified(self):
+
+        """
+        Get the date and time this object was last modified.
+        """
+        return self._key.properties.last_modified. \
+            strftime("%Y-%m-%dT%H:%M:%S.%f")
+
+    def iter_content(self):
+        """
+        Returns this object's content as an
+        iterable.
+        """
+        content_stream = self._provider.azure_client. \
+            get_blob_content(self._container.name, self._key.name)
+        if content_stream:
+            content_stream.seek(0)
+        return content_stream
+
+    def upload(self, data):
+        """
+        Set the contents of this object to the data read from the source
+        string.
+        """
+        try:
+            self._provider.azure_client.create_blob_from_text(
+                self._container.name, self.name, data)
+            return True
+        except AzureException as azureEx:
+            log.exception(azureEx)
+            return False
+
+    def upload_from_file(self, path):
+        """
+        Store the contents of the file pointed by the "path" variable.
+        """
+        try:
+            self._provider.azure_client.create_blob_from_file(
+                self._container.name, self.name, path)
+            return True
+        except AzureException as azureEx:
+            log.exception(azureEx)
+            return False
+
+    def delete(self):
+        """
+        Delete this object.
+
+        :rtype: bool
+        :return: True if successful
+        """
+        self._provider.azure_client.delete_blob(self._container.name,
+                                                self.name)
+
+    def generate_url(self, expires_in=0):
+        """
+        Generate a URL to this object.
+        """
+        return self._provider.azure_client.get_blob_url(
+            self._container.name, self.name, expires_in)
+
+
+class AzureBucket(BaseBucket):
+    def __init__(self, provider, bucket):
+        super(AzureBucket, self).__init__(provider)
+        self._bucket = bucket
+        self._object_container = AzureBucketContainer(provider, self)
+
+    @property
+    def id(self):
+        return self._bucket.name
+
+    @property
+    def name(self):
+        """
+        Get this bucket's name.
+        """
+        return self._bucket.name
+
+    def delete(self, delete_contents=True):
+        """
+        Delete this bucket.
+        """
+        self._provider.azure_client.delete_container(self.name)
+
+    def exists(self, name):
+        """
+        Determine if an object with given name exists in this bucket.
+        """
+        return True if self.get(name) else False
+
+    @property
+    def objects(self):
+        return self._object_container
+
+
+class AzureBucketContainer(BaseBucketContainer):
+
+    def __init__(self, provider, bucket):
+        super(AzureBucketContainer, self).__init__(provider, bucket)
+
+    def get(self, key):
+        """
+        Retrieve a given object from this bucket.
+        """
+        try:
+            obj = self._provider.azure_client.get_blob(self.bucket.name, key)
+            return AzureBucketObject(self._provider, self.bucket, obj)
+        except AzureException as azureEx:
+            log.exception(azureEx)
+            return None
+
+    def list(self, limit=None, marker=None, prefix=None):
+        """
+        List all objects within this bucket.
+
+        :rtype: BucketObject
+        :return: List of all available BucketObjects within this bucket.
+        """
+        objects = [AzureBucketObject(self._provider, self.bucket, obj)
+                   for obj in
+                   self._provider.azure_client.list_blobs(
+                       self.bucket.name, prefix=prefix)]
+        return ClientPagedResultList(self._provider, objects,
+                                     limit=limit, marker=marker)
+
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
+
+    def create(self, name):
+        self._provider.azure_client.create_blob_from_text(
+            self.bucket.name, name, '')
+        return self.get(name)
+
+
+class AzureVolume(BaseVolume):
+    VOLUME_STATE_MAP = {
+        'InProgress': VolumeState.CREATING,
+        'Creating': VolumeState.CREATING,
+        'Unattached': VolumeState.AVAILABLE,
+        'Attached': VolumeState.IN_USE,
+        'Deleting': VolumeState.CONFIGURING,
+        'Updating': VolumeState.CONFIGURING,
+        'Deleted': VolumeState.DELETED,
+        'Failed': VolumeState.ERROR,
+        'Canceled': VolumeState.ERROR
+    }
+
+    def __init__(self, provider, volume):
+        super(AzureVolume, self).__init__(provider)
+        self._volume = volume
+        self._description = None
+        self._state = 'unknown'
+        self._update_state()
+        if not self._volume.tags:
+            self._volume.tags = {}
+
+    def _update_state(self):
+        if not self._volume.provisioning_state == 'Succeeded':
+            self._state = self._volume.provisioning_state
+        elif self._volume.managed_by:
+            self._state = 'Attached'
+        else:
+            self._state = 'Unattached'
+
+    @property
+    def id(self):
+        return self._volume.id
+
+    @property
+    def resource_id(self):
+        return self._volume.id
+
+    @property
+    def tags(self):
+        return self._volume.tags
+
+    @property
+    def name(self):
+        """
+        Get the volume name.
+
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return self._volume.tags.get('Name', self._volume.name)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the volume name.
+        """
+        # self._volume.name = value
+        self.assert_valid_resource_name(value)
+        self._volume.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_disk_tags(self.id,
+                             self._volume.tags)
+
+    @property
+    def description(self):
+        return self._volume.tags.get('Description', None)
+
+    @description.setter
+    def description(self, value):
+        self._volume.tags.update(Description=value)
+        self._provider.azure_client. \
+            update_disk_tags(self.id,
+                             self._volume.tags)
+
+    @property
+    def size(self):
+        return self._volume.disk_size_gb
+
+    @property
+    def create_time(self):
+        return self._volume.time_created.strftime("%Y-%m-%dT%H:%M:%S.%f")
+
+    @property
+    def zone_id(self):
+        return self._volume.location
+
+    @property
+    def source(self):
+        return self._volume.creation_data.source_uri
+
+    @property
+    def attachments(self):
+        """
+        Azure does not have option to specify the device name
+        while attaching disk to VM. It is automatically populated
+        and is not returned. As a result this method ignores
+        the device name parameter and passes None
+        to the BaseAttachmentInfo
+        :return:
+        """
+        if self._volume.managed_by:
+            return BaseAttachmentInfo(self, self._volume.managed_by, None)
+        else:
+            return None
+
+    def attach(self, instance, device=None):
+        """
+        Attach this volume to an instance.
+        """
+        instance_id = instance.id if isinstance(
+            instance,
+            Instance) else instance
+        vm = self._provider.azure_client.get_vm(instance_id)
+
+        vm.storage_profile.data_disks.append({
+            'lun': len(vm.storage_profile.data_disks),
+            'name': self._volume.name,
+            'create_option': 'attach',
+            'managed_disk': {
+                'id': self.resource_id
+            }
+        })
+        self._provider.azure_client.update_vm(instance_id, vm)
+
+    def detach(self, force=False):
+        """
+        Detach this volume from an instance.
+        """
+        for vm in self._provider.azure_client.list_vm():
+            for item in vm.storage_profile.data_disks:
+                if item.managed_disk and \
+                                item.managed_disk.id == self.resource_id:
+                    vm.storage_profile.data_disks.remove(item)
+                    self._provider.azure_client.update_vm(vm.id, vm)
+
+    def create_snapshot(self, name, description=None):
+        """
+        Create a snapshot of this Volume.
+        """
+        return self._provider.storage.snapshots.create(name, self)
+
+    def delete(self):
+        """
+        Delete this volume.
+        """
+        self._provider.azure_client.delete_disk(self.id)
+
+    @property
+    def state(self):
+        return AzureVolume.VOLUME_STATE_MAP.get(
+            self._state, VolumeState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this volume by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._volume = self._provider.azure_client. \
+                get_disk(self.id)
+            self._update_state()
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The volume no longer exists and cannot be refreshed.
+            # set the state to unknown
+            self._state = 'unknown'
+
+
+class AzureSnapshot(BaseSnapshot):
+    SNAPSHOT_STATE_MAP = {
+        'InProgress': SnapshotState.PENDING,
+        'Succeeded': SnapshotState.AVAILABLE,
+        'Failed': SnapshotState.ERROR,
+        'Canceled': SnapshotState.ERROR,
+        'Updating': SnapshotState.CONFIGURING,
+        'Deleting': SnapshotState.CONFIGURING,
+        'Deleted': SnapshotState.UNKNOWN
+    }
+
+    def __init__(self, provider, snapshot):
+        super(AzureSnapshot, self).__init__(provider)
+        self._snapshot = snapshot
+        self._description = None
+        self._state = self._snapshot.provisioning_state
+        if not self._snapshot.tags:
+            self._snapshot.tags = {}
+
+    @property
+    def id(self):
+        return self._snapshot.id
+
+    @property
+    def resource_id(self):
+        return self._snapshot.id
+
+    @property
+    def name(self):
+        """
+        Get the snapshot name.
+
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return self._snapshot.tags.get('Name', self._snapshot.name)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the snapshot name.
+        """
+        self.assert_valid_resource_name(value)
+        self._snapshot.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_snapshot_tags(self.id,
+                                 self._snapshot.tags)
+
+    @property
+    def description(self):
+        return self._snapshot.tags.get('Description', None)
+
+    @description.setter
+    def description(self, value):
+        self._snapshot.tags.update(Description=value)
+        self._provider.azure_client. \
+            update_snapshot_tags(self.id,
+                                 self._snapshot.tags)
+
+    @property
+    def size(self):
+        return self._snapshot.disk_size_gb
+
+    @property
+    def volume_id(self):
+        return self._snapshot.creation_data.source_resource_id
+
+    @property
+    def create_time(self):
+        return self._snapshot.time_created.strftime("%Y-%m-%dT%H:%M:%S.%f")
+
+    @property
+    def state(self):
+        return AzureSnapshot.SNAPSHOT_STATE_MAP.get(
+            self._state, SnapshotState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this snapshot by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._snapshot = self._provider.azure_client. \
+                get_snapshot(self.id)
+            self._state = self._snapshot.provisioning_state
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The snapshot no longer exists and cannot be refreshed.
+            # set the state to unknown
+            self._state = 'unknown'
+
+    def delete(self):
+        """
+        Delete this snapshot.
+        """
+        self._provider.azure_client.delete_snapshot(self.id)
+
+    def create_volume(self, placement=None,
+                      size=None, volume_type=None, iops=None):
+        """
+        Create a new Volume from this Snapshot.
+        """
+        return self._provider.storage.volumes. \
+            create(self.name, self.size,
+                   zone=placement, snapshot=self)
+
+
+class AzureMachineImage(BaseMachineImage):
+    IMAGE_STATE_MAP = {
+        'InProgress': MachineImageState.PENDING,
+        'Succeeded': MachineImageState.AVAILABLE,
+        'Failed': MachineImageState.ERROR
+    }
+
+    def __init__(self, provider, image):
+        super(AzureMachineImage, self).__init__(provider)
+        self._image = image
+        self._state = self._image.provisioning_state
+
+        if not self._image.tags:
+            self._image.tags = {}
+
+    @property
+    def id(self):
+        """
+        Get the image identifier.
+
+        :rtype: ``str``
+        :return: ID for this instance as returned by the cloud middleware.
+        """
+        return self._image.id
+
+    @property
+    def resource_id(self):
+        return self._image.id
+
+    @property
+    def name(self):
+        """
+        Get the image name.
+
+        :rtype: ``str``
+        :return: Name for this image as returned by the cloud middleware.
+        """
+        return self._image.tags.get('Name', self._image.name)
+
+    @name.setter
+    def name(self, value):
+        """
+        Set the image name.
+        """
+        self.assert_valid_resource_name(value)
+        self._image.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_image_tags(self.id, self._image.tags)
+
+    @property
+    def description(self):
+        """
+        Get the image description.
+
+        :rtype: ``str``
+        :return: Description for this image as returned by the cloud middleware
+        """
+        return self._image.tags.get('Description', None)
+
+    @description.setter
+    def description(self, value):
+        """
+        Set the image name.
+        """
+        self._image.tags.update(Description=value)
+        self._provider.azure_client. \
+            update_image_tags(self.id, self._image.tags)
+
+    @property
+    def min_disk(self):
+        """
+        Returns the minimum size of the disk that's required to
+        boot this image (in GB).
+        This value is not retuned in azure api
+        as this is a limitation with Azure Compute API
+
+        :rtype: ``int``
+        :return: The minimum disk size needed by this image
+        """
+        return self._image.storage_profile.os_disk.disk_size_gb or 0
+
+    def delete(self):
+        """
+        Delete this image
+        """
+        self._provider.azure_client.delete_image(self.id)
+
+    @property
+    def state(self):
+        return AzureMachineImage.IMAGE_STATE_MAP.get(
+            self._state, MachineImageState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this instance by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._image = self._provider.azure_client.get_image(self.id)
+            self._state = self._image.provisioning_state
+        except CloudError as cloudError:
+            log.exception(cloudError.message)
+            # image no longer exists
+            self._state = "unknown"
+
+
+class AzureGatewayContainer(BaseGatewayContainer):
+    def __init__(self, provider, network):
+        super(AzureGatewayContainer, self).__init__(provider, network)
+        # Azure doesn't have a notion of a route table or an internet
+        # gateway as OS and AWS so create placeholder objects of the
+        # AzureInternetGateway here.
+        # http://bit.ly/2BqGdVh
+        # Singleton returned by the list method
+        self.gateway_singleton = AzureInternetGateway(self._provider, None,
+                                                      network)
+
+    def get_or_create_inet_gateway(self, name=None):
+        if name:
+            AzureInternetGateway.assert_valid_resource_name(name)
+        gateway = AzureInternetGateway(self._provider, None, self._network)
+        if name:
+            gateway.name = name
+        return gateway
+
+    def list(self, limit=None, marker=None):
+        return [self.gateway_singleton]
+
+    def delete(self, gateway):
+        pass
+
+
+class AzureNetwork(BaseNetwork):
+    NETWORK_STATE_MAP = {
+        'InProgress': NetworkState.PENDING,
+        'Succeeded': NetworkState.AVAILABLE,
+    }
+
+    def __init__(self, provider, network):
+        super(AzureNetwork, self).__init__(provider)
+        self._network = network
+        self._state = self._network.provisioning_state
+        if not self._network.tags:
+            self._network.tags = {}
+        self._gateway_service = AzureGatewayContainer(provider, self)
+
+    @property
+    def id(self):
+        return self._network.id
+
+    @property
+    def resource_id(self):
+        return self._network.id
+
+    @property
+    def name(self):
+        """
+        Get the network name.
+
+        .. note:: the network must have a (case sensitive) tag ``Name``
+        """
+        return self._network.tags.get('Name', self._network.name)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the network name.
+        """
+        self.assert_valid_resource_name(value)
+        self._network.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_network_tags(self.id, self._network)
+
+    @property
+    def external(self):
+        """
+        For Azure, all VPC networks can be connected to the Internet so always
+        return ``True``.
+        """
+        return True
+
+    @property
+    def state(self):
+        return AzureNetwork.NETWORK_STATE_MAP.get(
+            self._state, NetworkState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this network by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._network = self._provider.azure_client.\
+                get_network(self.id)
+            self._state = self._network.provisioning_state
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The network no longer exists and cannot be refreshed.
+            # set the state to unknown
+            self._state = 'unknown'
+
+    @property
+    def cidr_block(self):
+        """
+        Address space associated with this network
+        :return:
+        """
+        return self._network.address_space.address_prefixes[0]
+
+    def delete(self):
+        """
+        Delete an existing network.
+        """
+        self._provider.azure_client.delete_network(self.id)
+
+    @property
+    def subnets(self):
+        """
+        List all the subnets in this network
+        :return:
+        """
+        return self._provider.networking.subnets.list(network=self.id)
+
+    def create_subnet(self, cidr_block, name=None, zone=None):
+        """
+        Create the subnet with cidr_block
+        :param cidr_block:
+        :param name:
+        :param zone:
+        :return:
+        """
+        return self._provider.networking.subnets. \
+            create(network=self.id, cidr_block=cidr_block, name=name)
+
+    @property
+    def gateways(self):
+        return self._gateway_service
+
+
+class AzureFloatingIPContainer(BaseFloatingIPContainer):
+
+    def __init__(self, provider, gateway, network_id):
+        super(AzureFloatingIPContainer, self).__init__(provider, gateway)
+        self._network_id = network_id
+
+    def get(self, fip_id):
+        log.debug("Getting Azure Floating IP container with the id: %s",
+                  fip_id)
+        fip = [fip for fip in self if fip.id == fip_id]
+        return fip[0] if fip else None
+
+    def list(self, limit=None, marker=None):
+        floating_ips = [AzureFloatingIP(self._provider, floating_ip,
+                                        self._network_id)
+                        for floating_ip in self._provider.azure_client.
+                        list_floating_ips()]
+        return ClientPagedResultList(self._provider, floating_ips,
+                                     limit=limit, marker=marker)
+
+    def create(self):
+        public_ip_address_name = "{0}-{1}".format(
+            'public_ip', uuid.uuid4().hex[:6])
+        public_ip_parameters = {
+            'location': self._provider.azure_client.region_name,
+            'public_ip_allocation_method': 'Static'
+        }
+        floating_ip = self._provider.azure_client.\
+            create_floating_ip(public_ip_address_name, public_ip_parameters)
+        return AzureFloatingIP(self._provider, floating_ip, self._network_id)
+
+
+class AzureFloatingIP(BaseFloatingIP):
+
+    def __init__(self, provider, floating_ip, network_id):
+        super(AzureFloatingIP, self).__init__(provider)
+        self._ip = floating_ip
+        self._network_id = network_id
+
+    @property
+    def id(self):
+        return self._ip.id
+
+    @property
+    def resource_id(self):
+        return self._ip.id
+
+    @property
+    def public_ip(self):
+        return self._ip.ip_address
+
+    @property
+    def private_ip(self):
+        return self._ip.ip_configuration.private_ip_address \
+            if self._ip.ip_configuration else None
+
+    @property
+    def in_use(self):
+        return True if self._ip.ip_configuration else False
+
+    def delete(self):
+        """
+        Delete an existing floating ip.
+        """
+        self._provider.azure_client.delete_floating_ip(self.id)
+
+    def refresh(self):
+        net = self._provider.networking.networks.get(self._network_id)
+        gw = net.gateways.get_or_create_inet_gateway()
+        fip = gw.floating_ips.get(self.id)
+        self._ip = fip._ip
+
+
+class AzureRegion(BaseRegion):
+    def __init__(self, provider, azure_region):
+        super(AzureRegion, self).__init__(provider)
+        self._azure_region = azure_region
+
+    @property
+    def id(self):
+        return self._azure_region.name
+
+    @property
+    def name(self):
+        return self._azure_region.name
+
+    @property
+    def zones(self):
+        """
+            Access information about placement zones within this region.
+            As Azure does not have this feature, mapping the region
+            name as zone id and name.
+        """
+        return [AzurePlacementZone(self._provider,
+                                   self._azure_region.name,
+                                   self._azure_region.name)]
+
+
+class AzurePlacementZone(BasePlacementZone):
+    """
+    As Azure does not provide zones (limited support), we are mapping the
+    region information in the zones.
+    """
+    def __init__(self, provider, zone, region):
+        super(AzurePlacementZone, self).__init__(provider)
+        self._azure_zone = zone
+        self._azure_region = region
+
+    @property
+    def id(self):
+        """
+            Get the zone id
+            :rtype: ``str``
+            :return: ID for this zone as returned by the cloud middleware.
+        """
+        return self._azure_zone
+
+    @property
+    def name(self):
+        """
+            Get the zone name.
+            :rtype: ``str``
+            :return: Name for this zone as returned by the cloud middleware.
+        """
+        return self._azure_region
+
+    @property
+    def region_name(self):
+        """
+            Get the region that this zone belongs to.
+            :rtype: ``str``
+            :return: Name of this zone's region as returned by the
+            cloud middleware
+        """
+        return self._azure_region
+
+
+class AzureSubnet(BaseSubnet):
+    _SUBNET_STATE_MAP = {
+        'InProgress': SubnetState.PENDING,
+        'Succeeded': SubnetState.AVAILABLE,
+    }
+
+    def __init__(self, provider, subnet):
+        super(AzureSubnet, self).__init__(provider)
+        self._subnet = subnet
+        self._state = self._subnet.provisioning_state
+
+    @property
+    def id(self):
+        return self._subnet.id
+
+    @property
+    def resource_id(self):
+        return self._subnet.id
+
+    @property
+    def name(self):
+        """
+        Get the subnet name.
+
+        .. note:: the subnet must have a (case sensitive) tag ``Name``
+        """
+        return self._subnet.name
+
+    @property
+    def zone(self):
+        # pylint:disable=protected-access
+        region = self._provider.compute.regions.get(
+            self._network._network.location)
+        return region.zones[0]
+
+    @property
+    def cidr_block(self):
+        return self._subnet.address_prefix
+
+    @property
+    def network_id(self):
+        return self._provider.azure_client.get_network_id_for_subnet(self.id)
+
+    @property
+    def _network(self):
+        return self._provider.networking.networks.get(self.network_id)
+
+    def delete(self):
+        self._provider.azure_client.delete_subnet(self.id)
+
+    @property
+    def state(self):
+        return self._SUBNET_STATE_MAP.get(self._state, NetworkState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this network by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._subnet = self._provider.azure_client. \
+                get_subnet(self.id)
+            self._state = self._subnet.provisioning_state
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The subnet no longer exists and cannot be refreshed.
+            # set the state to unknown
+            self._state = 'unknown'
+
+
+class AzureInstance(BaseInstance):
+
+    INSTANCE_STATE_MAP = {
+        'InProgress': InstanceState.PENDING,
+        'Creating': InstanceState.PENDING,
+        'VM running': InstanceState.RUNNING,
+        'Updating': InstanceState.CONFIGURING,
+        'Deleted': InstanceState.DELETED,
+        'Stopping': InstanceState.CONFIGURING,
+        'Deleting': InstanceState.CONFIGURING,
+        'Stopped': InstanceState.STOPPED,
+        'Canceled': InstanceState.ERROR,
+        'Failed': InstanceState.ERROR,
+        'VM stopped': InstanceState.STOPPED,
+        'VM deallocated': InstanceState.STOPPED,
+        'VM deallocating': InstanceState.CONFIGURING,
+        'VM stopping': InstanceState.CONFIGURING,
+        'VM starting': InstanceState.CONFIGURING
+    }
+
+    def __init__(self, provider, vm_instance):
+        super(AzureInstance, self).__init__(provider)
+        self._vm = vm_instance
+        self._update_state()
+        if not self._vm.tags:
+            self._vm.tags = {}
+
+    @property
+    def _nic_ids(self):
+        return (nic.id for nic in self._vm.network_profile.network_interfaces)
+
+    @property
+    def _nics(self):
+        return (self._provider.azure_client.get_nic(nic_id)
+                for nic_id in self._nic_ids)
+
+    @property
+    def _public_ip_ids(self):
+        return (ip_config.public_ip_address.id
+                for nic in self._nics
+                for ip_config in nic.ip_configurations
+                if nic.ip_configurations and ip_config.public_ip_address)
+
+    @property
+    def id(self):
+        """
+        Get the instance identifier.
+        """
+        return self._vm.id
+
+    @property
+    def resource_id(self):
+        return self._vm.id
+
+    @property
+    def name(self):
+        """
+        Get the instance name.
+
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return self._vm.tags.get('Name', self._vm.name)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the instance name.
+        """
+        self.assert_valid_resource_name(value)
+        self._vm.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_vm_tags(self.id, self._vm)
+
+    @property
+    def public_ips(self):
+        """
+        Get all the public IP addresses for this instance.
+        """
+        return [self._provider.azure_client.get_floating_ip(pip).ip_address
+                for pip in self._public_ip_ids]
+
+    @property
+    def private_ips(self):
+        """
+        Get all the private IP addresses for this instance.
+        """
+        return [ip_config.private_ip_address
+                for nic in self._nics
+                for ip_config in nic.ip_configurations
+                if nic.ip_configurations and ip_config.private_ip_address]
+
+    @property
+    def vm_type_id(self):
+        """
+        Get the instance type name.
+        """
+        return self._vm.hardware_profile.vm_size
+
+    @property
+    def vm_type(self):
+        """
+        Get the instance type.
+        """
+        return self._provider.compute.vm_types.find(
+            name=self.vm_type_id)[0]
+
+    def reboot(self):
+        """
+        Reboot this instance (using the cloud middleware API).
+        """
+        self._provider.azure_client.restart_vm(self.id)
+
+    def delete(self):
+        """
+        Permanently terminate this instance.
+        After deleting the VM. we are deleting the network interface
+        associated to the instance, public ip addresses associated to
+        the instance and also removing OS disk and data disks where
+        tag with name 'delete_on_terminate' has value True.
+        """
+        self._provider.azure_client.deallocate_vm(self.id)
+        self._provider.azure_client.delete_vm(self.id)
+        for public_ip_id in self._public_ip_ids:
+            self._provider.azure_client.delete_floating_ip(public_ip_id)
+        for nic_id in self._nic_ids:
+            self._provider.azure_client.delete_nic(nic_id)
+        for data_disk in self._vm.storage_profile.data_disks:
+            if data_disk.managed_disk:
+                disk = self._provider.azure_client.\
+                    get_disk(data_disk.managed_disk.id)
+                if disk and disk.tags \
+                        and disk.tags.get('delete_on_terminate',
+                                          'False') == 'True':
+                    self._provider.azure_client.\
+                        delete_disk(data_disk.managed_disk.id)
+        if self._vm.storage_profile.os_disk.managed_disk:
+            self._provider.azure_client. \
+                delete_disk(self._vm.storage_profile.os_disk.managed_disk.id)
+
+    @property
+    def image_id(self):
+        """
+        Get the image ID for this instance.
+        """
+        return self._vm.storage_profile.image_reference.id
+
+    @property
+    def zone_id(self):
+        """
+        Get the placement zone id where this instance is running.
+        """
+        return self._vm.location
+
+    @property
+    def subnet_id(self):
+        """
+        Return the first subnet id associated with the first network iface.
+
+        An Azure instance can have multiple network interfaces attached with
+        each interface having at most one subnet. This method will return only
+        the subnet of the first attached network interface.
+        """
+        for nic_id in self._nic_ids:
+            nic = self._provider.azure_client.get_nic(nic_id)
+            for ipc in nic.ip_configurations:
+                return ipc.subnet.id
+
+    @property
+    def vm_firewalls(self):
+        return [self._provider.security.vm_firewalls.get(group_id)
+                for group_id in self.vm_firewall_ids]
+
+    @property
+    def vm_firewall_ids(self):
+        return [nic.network_security_group.id
+                for nic in self._nics
+                if nic.network_security_group]
+
+    @property
+    def key_pair_name(self):
+        """
+        Get the name of the key pair associated with this instance.
+        """
+        return self._vm.tags.get('Key_Pair')
+
+    def create_image(self, name, private_key_path=None):
+        """
+        Create a new image based on this instance. Documentation for create
+        image available at https://docs.microsoft.com/en-us/azure/virtual-ma
+        chines/linux/capture-image. In azure, we need to deprovision the VM
+        before capturing.
+        To deprovision, login to the VM and execute the `waagent deprovision`
+        command. To do this programmatically, use pysftp to ssh into the VM
+        and executing deprovision command. To SSH into the VM programmatically
+        however, we need to pass private key file path, so we have modified the
+        CloudBridge interface to pass the private key file path
+        """
+
+        self.assert_valid_resource_name(name)
+
+        if not self._state == 'VM generalized':
+            if not self._state == 'VM running':
+                self._provider.azure_client.start_vm(self.id)
+
+            # if private_key_path:
+            self._deprovision(private_key_path)
+            self._provider.azure_client.deallocate_vm(self.id)
+            self._provider.azure_client.generalize_vm(self.id)
+
+        create_params = {
+            'location': self._provider.region_name,
+            'source_virtual_machine': {
+                'id': self.resource_id
+            },
+            'tags': {'Name': name}
+        }
+
+        image = self._provider.azure_client.create_image(name, create_params)
+        return AzureMachineImage(self._provider, image)
+
+    def _deprovision(self, private_key_path):
+        cnopts = pysftp.CnOpts()
+        cnopts.hostkeys = None
+        if private_key_path:
+            with pysftp.\
+                    Connection(self.public_ips[0],
+                               username=self._provider.vm_default_user_name,
+                               cnopts=cnopts,
+                               private_key=private_key_path) as sftp:
+                sftp.execute('sudo waagent -deprovision -force')
+                sftp.close()
+
+    def add_floating_ip(self, floating_ip):
+        """
+        Attaches public ip to the instance.
+        """
+        floating_ip_id = floating_ip.id if isinstance(
+            floating_ip, AzureFloatingIP) else floating_ip
+        nic = next(self._nics)
+        nic.ip_configurations[0].public_ip_address = {
+            'id': floating_ip_id
+        }
+        self._provider.azure_client.update_nic(nic.id, nic)
+
+    def remove_floating_ip(self, floating_ip):
+        """
+        Remove a public IP address from this instance.
+        """
+        floating_ip_id = floating_ip.id if isinstance(
+            floating_ip, AzureFloatingIP) else floating_ip
+        nic = next(self._nics)
+        for ip_config in nic.ip_configurations:
+            if ip_config.public_ip_address.id == floating_ip_id:
+                nic.ip_configurations[0].public_ip_address = None
+                self._provider.azure_client.update_nic(nic.id, nic)
+
+    def add_vm_firewall(self, fw):
+        '''
+        :param fw:
+        :return: None
+
+        This method adds the security group to VM instance.
+        In Azure, security group added to Network interface.
+        Azure supports to add only one security group to
+        network interface, we are adding the provided security group
+        if not associated any security group to NIC
+        else replacing the existing security group.
+        '''
+        fw = (self._provider.security.vm_firewalls.get(fw)
+              if isinstance(fw, str) else fw)
+        nic = next(self._nics)
+        if not nic.network_security_group:
+            nic.network_security_group = NetworkSecurityGroup()
+            nic.network_security_group.id = fw.resource_id
+        else:
+            existing_fw = self._provider.security.\
+                vm_firewalls.get(nic.network_security_group.id)
+            new_fw = self._provider.security.vm_firewalls.\
+                create('{0}-{1}'.format(fw.name, existing_fw.name),
+                       'Merged security groups {0} and {1}'.
+                       format(fw.name, existing_fw.name))
+            new_fw.add_rule(src_dest_fw=fw)
+            new_fw.add_rule(src_dest_fw=existing_fw)
+            nic.network_security_group.id = new_fw.resource_id
+
+        self._provider.azure_client.update_nic(nic.id, nic)
+
+    def remove_vm_firewall(self, fw):
+
+        '''
+        :param fw:
+        :return: None
+
+        This method removes the security group to VM instance.
+        In Azure, security group added to Network interface.
+        Azure supports to add only one security group to
+        network interface, we are removing the provided security group
+        if it associated to NIC
+        else we are ignoring.
+        '''
+
+        nic = next(self._nics)
+        fw = (self._provider.security.vm_firewalls.get(fw)
+              if isinstance(fw, str) else fw)
+        if nic.network_security_group and \
+                nic.network_security_group.id == fw.resource_id:
+            nic.network_security_group = None
+            self._provider.azure_client.update_nic(nic.id, nic)
+
+    def _update_state(self):
+        """
+        Azure python sdk list operation does not return the current
+        staus of the instance. We have to explicity call the get method
+        for each instance to get the instance status(instance_view).
+        This is the limitation with azure rest api
+        :return:
+        """
+        if not self._vm.instance_view:
+            self.refresh()
+        if self._vm.instance_view and len(
+                self._vm.instance_view.statuses) > 1:
+            self._state = \
+                self._vm.instance_view.statuses[1].display_status
+        else:
+            self._state = \
+                self._vm.provisioning_state
+
+    @property
+    def state(self):
+        return AzureInstance.INSTANCE_STATE_MAP.get(
+            self._state, InstanceState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this instance by re-querying the cloud provider
+        for its latest state.
+        """
+        try:
+            self._vm = self._provider.azure_client.get_vm(self.id)
+            if not self._vm.tags:
+                self._vm.tags = {}
+            self._update_state()
+        except (CloudError, ValueError) as cloudError:
+            log.exception(cloudError.message)
+            # The volume no longer exists and cannot be refreshed.
+            # set the state to unknown
+            self._state = 'unknown'
+
+
+class AzureLaunchConfig(BaseLaunchConfig):
+
+    def __init__(self, provider):
+        super(AzureLaunchConfig, self).__init__(provider)
+
+
+class AzureVMType(BaseVMType):
+
+    def __init__(self, provider, vm_type):
+        super(AzureVMType, self).__init__(provider)
+        self._vm_type = vm_type
+
+    @property
+    def id(self):
+        return self._vm_type.name
+
+    @property
+    def name(self):
+        return self._vm_type.name
+
+    @property
+    def family(self):
+        """
+        Python sdk does not return family details.
+        So, as of now populating it with 'Unknown'
+        """
+        return "Unknown"
+
+    @property
+    def vcpus(self):
+        return self._vm_type.number_of_cores
+
+    @property
+    def ram(self):
+        return int(self._vm_type.memory_in_mb) / 1024
+
+    @property
+    def size_root_disk(self):
+        return self._vm_type.os_disk_size_in_mb / 1024
+
+    @property
+    def size_ephemeral_disks(self):
+        return self._vm_type.resource_disk_size_in_mb / 1024
+
+    @property
+    def num_ephemeral_disks(self):
+        """
+        Azure by default adds one ephemeral disk. We can not add
+        more ephemeral disks to VM explicitly
+        So, returning it as Zero.
+        """
+        return 0
+
+    @property
+    def extra_data(self):
+        return {
+                    'max_data_disk_count':
+                    self._vm_type.max_data_disk_count
+               }
+
+
+class AzureKeyPair(BaseKeyPair):
+
+    def __init__(self, provider, key_pair):
+        super(AzureKeyPair, self).__init__(provider, key_pair)
+
+    @property
+    def id(self):
+        return self._key_pair.Name
+
+    @property
+    def name(self):
+        return self._key_pair.Name
+
+    def delete(self):
+        self._provider.azure_client.delete_public_key(self._key_pair)
+
+
+class AzureRouter(BaseRouter):
+    def __init__(self, provider, route_table):
+        super(AzureRouter, self).__init__(provider)
+        self._route_table = route_table
+        if not self._route_table.tags:
+            self._route_table.tags = {}
+
+    @property
+    def id(self):
+        return self._route_table.name
+
+    @property
+    def resource_id(self):
+        return self._route_table.id
+
+    @property
+    def name(self):
+        """
+        Get the router name.
+
+        .. note:: the router must have a (case sensitive) tag ``Name``
+        """
+        return self._route_table.tags.get('Name', self._route_table.name)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the router name.
+        """
+        self.assert_valid_resource_name(value)
+        self._route_table.tags.update(Name=value)
+        self._provider.azure_client. \
+            update_route_table_tags(self._route_table.name,
+                                    self._route_table)
+
+    def refresh(self):
+        self._route_table = self._provider.azure_client. \
+            get_route_table(self._route_table.name)
+
+    @property
+    def state(self):
+        self.refresh()  # Explicitly refresh the local object
+        if self._route_table.subnets:
+            return RouterState.ATTACHED
+        return RouterState.DETACHED
+
+    @property
+    def network_id(self):
+        return None
+
+    def delete(self):
+        self._provider.azure_client.delete_route_table(self.name)
+
+    def attach_subnet(self, subnet):
+        self._provider.azure_client. \
+            attach_subnet_to_route_table(subnet.id,
+                                         self.resource_id)
+        self.refresh()
+
+    def detach_subnet(self, subnet):
+        self._provider.azure_client. \
+            detach_subnet_to_route_table(subnet.id,
+                                         self.resource_id)
+        self.refresh()
+
+    def attach_gateway(self, gateway):
+        pass
+
+    def detach_gateway(self, gateway):
+        pass
+
+
+class AzureInternetGateway(BaseInternetGateway):
+    def __init__(self, provider, gateway, gateway_net):
+        super(AzureInternetGateway, self).__init__(provider)
+        self._gateway = gateway
+        self._name = None
+        self._network_id = gateway_net.id if isinstance(
+            gateway_net, AzureNetwork) else gateway_net
+        self._state = ''
+        self._fips_container = AzureFloatingIPContainer(
+            provider, self, self._network_id)
+
+    @property
+    def id(self):
+        return self._name
+
+    @property
+    def name(self):
+        """
+        Get the gateway name.
+
+        .. note:: the gateway must have a (case sensitive) tag ``Name``
+        """
+        return self._name
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the router name.
+        """
+        self.assert_valid_resource_name(value)
+        self._name = value
+
+    def refresh(self):
+        pass
+
+    @property
+    def state(self):
+        return self._state
+
+    @property
+    def network_id(self):
+        return self._network_id
+
+    def delete(self):
+        pass
+
+    @property
+    def floating_ips(self):
+        return self._fips_container

+ 1045 - 0
cloudbridge/cloud/providers/azure/services.py

@@ -0,0 +1,1045 @@
+import base64
+import logging
+import uuid
+
+from azure.common import AzureException
+from azure.mgmt.compute.models import DiskCreateOption
+
+import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.resources import ClientPagedResultList, \
+    ServerPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketService, \
+    BaseComputeService, \
+    BaseImageService, BaseInstanceService, BaseKeyPairService, \
+    BaseNetworkService, BaseNetworkingService, BaseRegionService, \
+    BaseRouterService, BaseSecurityService, BaseSnapshotService, \
+    BaseStorageService, BaseSubnetService, BaseVMFirewallService, \
+    BaseVMTypeService, BaseVolumeService
+from cloudbridge.cloud.interfaces.exceptions import \
+    DuplicateResourceException, InvalidValueException
+from cloudbridge.cloud.interfaces.resources import MachineImage, \
+    Network, PlacementZone, Snapshot, Subnet, VMFirewall, VMType, Volume
+
+from msrestazure.azure_exceptions import CloudError
+
+from . import helpers as azure_helpers
+from .resources import AzureBucket, \
+    AzureInstance, AzureKeyPair, \
+    AzureLaunchConfig, AzureMachineImage, AzureNetwork, \
+    AzureRegion, AzureRouter, AzureSnapshot, AzureSubnet, \
+    AzureVMFirewall, AzureVMType, AzureVolume
+
+log = logging.getLogger(__name__)
+
+
+class AzureSecurityService(BaseSecurityService):
+    def __init__(self, provider):
+        super(AzureSecurityService, self).__init__(provider)
+
+        # Initialize provider services
+        self._key_pairs = AzureKeyPairService(provider)
+        self._vm_firewalls = AzureVMFirewallService(provider)
+
+    @property
+    def key_pairs(self):
+        return self._key_pairs
+
+    @property
+    def vm_firewalls(self):
+        return self._vm_firewalls
+
+
+class AzureVMFirewallService(BaseVMFirewallService):
+    def __init__(self, provider):
+        super(AzureVMFirewallService, self).__init__(provider)
+
+    def get(self, fw_id):
+        try:
+            fws = self.provider.azure_client.get_vm_firewall(fw_id)
+            return AzureVMFirewall(self.provider, fws)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def list(self, limit=None, marker=None):
+        fws = [AzureVMFirewall(self.provider, fw)
+               for fw in self.provider.azure_client.list_vm_firewall()]
+        return ClientPagedResultList(self.provider, fws, limit, marker)
+
+    def create(self, name, description, network_id=None):
+        AzureVMFirewall.assert_valid_resource_name(name)
+        parameters = {"location": self.provider.region_name,
+                      'tags': {'Name': name}}
+
+        if description:
+            parameters['tags'].update(Description=description)
+
+        fw = self.provider.azure_client.create_vm_firewall(name, parameters)
+
+        # Add default rules to negate azure default rules.
+        # See: https://github.com/gvlproject/cloudbridge/issues/106
+        # pylint:disable=protected-access
+        for rule in fw.default_security_rules:
+            rule_name = "cb-override-" + rule.name
+            # Transpose rules to priority 4001 onwards, because
+            # only 0-4096 are allowed for custom rules
+            rule.priority = rule.priority - 61440
+            rule.access = "Deny"
+            self._provider.azure_client.create_vm_firewall_rule(
+                fw.id, rule_name, rule)
+
+        # Add a new custom rule allowing all outbound traffic to the internet
+        parameters = {"priority": 3000,
+                      "protocol": "*",
+                      "source_port_range": "*",
+                      "source_address_prefix": "*",
+                      "destination_port_range": "*",
+                      "destination_address_prefix": "Internet",
+                      "access": "Allow",
+                      "direction": "Outbound"}
+        result = self._provider.azure_client.create_vm_firewall_rule(
+            fw.id, "cb-default-internet-outbound", parameters)
+        fw.security_rules.append(result)
+
+        cb_fw = AzureVMFirewall(self.provider, fw)
+        return cb_fw
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        fws = [AzureVMFirewall(self.provider, vm_firewall)
+               for vm_firewall in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_vm_firewall(), filters)]
+        return ClientPagedResultList(self.provider, fws)
+
+    def delete(self, group_id):
+        self.provider.azure_client.delete_vm_firewall(group_id)
+
+
+class AzureKeyPairService(BaseKeyPairService):
+    PARTITION_KEY = '00000000-0000-0000-0000-000000000000'
+
+    def __init__(self, provider):
+        super(AzureKeyPairService, self).__init__(provider)
+
+    def get(self, key_pair_id):
+        try:
+            key_pair = self.provider.azure_client.\
+                get_public_key(key_pair_id)
+
+            if key_pair:
+                return AzureKeyPair(self.provider, key_pair)
+            return None
+        except AzureException as error:
+            log.exception(error)
+            return None
+
+    def list(self, limit=None, marker=None):
+        key_pairs, resume_marker = self.provider.azure_client.list_public_keys(
+            AzureKeyPairService.PARTITION_KEY,  marker=marker,
+            limit=limit or self.provider.config.default_result_limit)
+        results = [AzureKeyPair(self.provider, key_pair)
+                   for key_pair in key_pairs]
+        return ServerPagedResultList(is_truncated=resume_marker,
+                                     marker=resume_marker,
+                                     supports_total=False,
+                                     data=results)
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        key_pair = self.get(name)
+        return ClientPagedResultList(self.provider,
+                                     [key_pair] if key_pair else [])
+
+    def create(self, name, public_key_material=None):
+        AzureKeyPair.assert_valid_resource_name(name)
+
+        key_pair = self.get(name)
+
+        if key_pair:
+            raise DuplicateResourceException(
+                'Keypair already exists with name {0}'.format(name))
+
+        private_key = None
+        if not public_key_material:
+            public_key_material, private_key = cb_helpers.generate_key_pair()
+
+        entity = {
+                  'PartitionKey': AzureKeyPairService.PARTITION_KEY,
+                  'RowKey': str(uuid.uuid4()),
+                  'Name': name,
+                  'Key': public_key_material
+                 }
+
+        self.provider.azure_client.create_public_key(entity)
+        key_pair = self.get(name)
+        key_pair.material = private_key
+        return key_pair
+
+
+class AzureBucketService(BaseBucketService):
+    def __init__(self, provider):
+        super(AzureBucketService, self).__init__(provider)
+
+    def get(self, bucket_id):
+        """
+        Returns a bucket given its ID. Returns ``None`` if the bucket
+        does not exist.
+        """
+        try:
+            bucket = self.provider.azure_client.get_container(bucket_id)
+            return AzureBucket(self.provider, bucket)
+        except AzureException as error:
+            log.exception(error)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        buckets = [AzureBucket(self.provider, bucket)
+                   for bucket in
+                   self.provider.azure_client.list_containers(prefix=name)]
+        return ClientPagedResultList(self.provider, buckets)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all containers.
+        """
+        buckets = [AzureBucket(self.provider, bucket)
+                   for bucket in self.provider.azure_client.list_containers()]
+        return ClientPagedResultList(self.provider, buckets,
+                                     limit=limit, marker=marker)
+
+    def create(self, name, location=None):
+        """
+        Create a new bucket.
+        """
+        AzureBucket.assert_valid_resource_name(name)
+        bucket = self.provider.azure_client.create_container(name.lower())
+        return AzureBucket(self.provider, bucket)
+
+
+class AzureStorageService(BaseStorageService):
+    def __init__(self, provider):
+        super(AzureStorageService, self).__init__(provider)
+
+        # Initialize provider services
+        self._volume_svc = AzureVolumeService(self.provider)
+        self._snapshot_svc = AzureSnapshotService(self.provider)
+        self._bucket_svc = AzureBucketService(self.provider)
+
+    @property
+    def volumes(self):
+        return self._volume_svc
+
+    @property
+    def snapshots(self):
+        return self._snapshot_svc
+
+    @property
+    def buckets(self):
+        return self._bucket_svc
+
+
+class AzureVolumeService(BaseVolumeService):
+    def __init__(self, provider):
+        super(AzureVolumeService, self).__init__(provider)
+
+    def get(self, volume_id):
+        """
+        Returns a volume given its id.
+        """
+        try:
+            volume = self.provider.azure_client.get_disk(volume_id)
+            return AzureVolume(self.provider, volume)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        cb_vols = [AzureVolume(self.provider, volume)
+                   for volume in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_disks(), filters)]
+        return ClientPagedResultList(self.provider, cb_vols)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all volumes.
+        """
+        azure_vols = self.provider.azure_client.list_disks()
+        cb_vols = [AzureVolume(self.provider, vol) for vol in azure_vols]
+        return ClientPagedResultList(self.provider, cb_vols,
+                                     limit=limit, marker=marker)
+
+    def create(self, name, size, zone=None, snapshot=None, description=None):
+        """
+        Creates a new volume.
+        """
+        AzureVolume.assert_valid_resource_name(name)
+        zone_id = zone.id if isinstance(zone, PlacementZone) else zone
+        snapshot = (self.provider.storage.snapshots.get(snapshot)
+                    if snapshot and isinstance(snapshot, str) else snapshot)
+        disk_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
+        tags = {'Name': name}
+        if description:
+            tags.update(Description=description)
+        if snapshot:
+            params = {
+                'location':
+                    zone_id or self.provider.azure_client.region_name,
+                'creation_data': {
+                    'create_option': DiskCreateOption.copy,
+                    'source_uri': snapshot.resource_id
+                },
+                'tags': tags
+            }
+
+            disk = self.provider.azure_client.create_snapshot_disk(disk_name,
+                                                                   params)
+
+        else:
+            params = {
+                'location':
+                    zone_id or self.provider.region_name,
+                'disk_size_gb': size,
+                'creation_data': {
+                    'create_option': DiskCreateOption.empty
+                },
+                'tags': tags}
+
+            disk = self.provider.azure_client.create_empty_disk(disk_name,
+                                                                params)
+
+        azure_vol = self.provider.azure_client.get_disk(disk.id)
+        cb_vol = AzureVolume(self.provider, azure_vol)
+
+        return cb_vol
+
+
+class AzureSnapshotService(BaseSnapshotService):
+    def __init__(self, provider):
+        super(AzureSnapshotService, self).__init__(provider)
+
+    def get(self, ss_id):
+        """
+        Returns a snapshot given its id.
+        """
+        try:
+            snapshot = self.provider.azure_client.get_snapshot(ss_id)
+            return AzureSnapshot(self.provider, snapshot)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        cb_snapshots = [AzureSnapshot(self.provider, snapshot)
+                        for snapshot in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_snapshots(), filters)]
+        return ClientPagedResultList(self.provider, cb_snapshots)
+
+    def list(self, limit=None, marker=None):
+        """
+               List all snapshots.
+        """
+        snaps = [AzureSnapshot(self.provider, obj)
+                 for obj in
+                 self.provider.azure_client.list_snapshots()]
+        return ClientPagedResultList(self.provider, snaps, limit, marker)
+
+    def create(self, name, volume, description=None):
+        """
+        Creates a new snapshot of a given volume.
+        """
+        AzureSnapshot.assert_valid_resource_name(name)
+        volume = (self.provider.storage.volumes.get(volume)
+                  if isinstance(volume, str) else volume)
+
+        tags = {'Name': name}
+        snapshot_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
+
+        if description:
+            tags.update(Description=description)
+
+        params = {
+            'location': self.provider.azure_client.region_name,
+            'creation_data': {
+                'create_option': DiskCreateOption.copy,
+                'source_uri': volume.resource_id
+            },
+            'disk_size_gb': volume.size,
+            'tags': tags
+        }
+
+        azure_snap = self.provider.azure_client.create_snapshot(snapshot_name,
+                                                                params)
+        return AzureSnapshot(self.provider, azure_snap)
+
+
+class AzureComputeService(BaseComputeService):
+    def __init__(self, provider):
+        super(AzureComputeService, self).__init__(provider)
+        self._vm_type_svc = AzureVMTypeService(self.provider)
+        self._instance_svc = AzureInstanceService(self.provider)
+        self._region_svc = AzureRegionService(self.provider)
+        self._images_svc = AzureImageService(self.provider)
+
+    @property
+    def images(self):
+        return self._images_svc
+
+    @property
+    def vm_types(self):
+        return self._vm_type_svc
+
+    @property
+    def instances(self):
+        return self._instance_svc
+
+    @property
+    def regions(self):
+        return self._region_svc
+
+
+class AzureInstanceService(BaseInstanceService):
+    def __init__(self, provider):
+        super(AzureInstanceService, self).__init__(provider)
+
+    def create(self, name, image, vm_type, subnet=None, zone=None,
+               key_pair=None, vm_firewalls=None, user_data=None,
+               launch_config=None, **kwargs):
+
+        instance_name = name.replace("_", "-") if name \
+            else "{0} - {1}".format("cb", uuid.uuid4())
+
+        AzureInstance.assert_valid_resource_name(instance_name)
+
+        # Key_pair is mandatory in azure and it should not be None.
+        temp_key_pair = None
+        if key_pair:
+            key_pair = (self.provider.security.key_pairs.get(key_pair)
+                        if isinstance(key_pair, str) else key_pair)
+        else:
+            # Create a temporary keypair if none is provided to keep Azure
+            # happy, but the private key will be discarded, so it'll be all
+            # but useless. However, this will allow an instance to be launched
+            # without specifying a keypair, so users may still be able to login
+            # if they have a preinstalled keypair/password baked into the image
+            default_kp_name = "cb_default_key_pair"
+            default_kp = self.provider.security.key_pairs.find(
+                name=default_kp_name)
+            if default_kp:
+                key_pair = default_kp[0]
+            else:
+                key_pair = self.provider.security.key_pairs.create(
+                    name=default_kp_name)
+                temp_key_pair = key_pair
+
+        image = (image if isinstance(image, AzureMachineImage) else
+                 self.provider.compute.images.get(image))
+        if not isinstance(image, AzureMachineImage):
+            raise Exception("Provided image %s is not a valid azure image"
+                            % image)
+
+        instance_size = vm_type.id if \
+            isinstance(vm_type, VMType) else vm_type
+
+        if not subnet:
+            subnet = self.provider.networking.subnets.get_or_create_default()
+        else:
+            subnet = (self.provider.networking.subnets.get(subnet)
+                      if isinstance(subnet, str) else subnet)
+
+        zone_id = zone.id if isinstance(zone, PlacementZone) else zone
+
+        subnet_id, zone_id, vm_firewall_id = \
+            self._resolve_launch_options(instance_name,
+                                         subnet, zone_id, vm_firewalls)
+
+        storage_profile = self._create_storage_profile(image, launch_config,
+                                                       instance_name, zone_id)
+
+        nic_params = {
+                'location': self._provider.region_name,
+                'ip_configurations': [{
+                    'name': instance_name + '_ip_config',
+                    'private_ip_allocation_method': 'Dynamic',
+                    'subnet': {
+                        'id': subnet_id
+                    }
+                }]
+            }
+
+        if vm_firewall_id:
+            nic_params['network_security_group'] = {
+                'id': vm_firewall_id
+            }
+        nic_info = self.provider.azure_client.create_nic(
+            instance_name + '_nic',
+            nic_params
+        )
+        # #! indicates shell script
+        ud = '#cloud-config\n' + user_data \
+            if user_data and not user_data.startswith('#!')\
+            and not user_data.startswith('#cloud-config') else user_data
+
+        params = {
+            'location': zone_id or self._provider.region_name,
+            'os_profile': {
+                'admin_username': self.provider.vm_default_user_name,
+                'computer_name': instance_name,
+                'linux_configuration': {
+                             "disable_password_authentication": True,
+                             "ssh": {
+                                 "public_keys": [{
+                                      "path":
+                                      "/home/{}/.ssh/authorized_keys".format(
+                                          self.provider.vm_default_user_name),
+                                      "key_data": key_pair._key_pair.Key
+                                     }]
+                                   }
+                           }
+            },
+            'hardware_profile': {
+                'vm_size': instance_size
+            },
+            'network_profile': {
+                'network_interfaces': [{
+                    'id': nic_info.id
+                }]
+            },
+            'storage_profile': storage_profile,
+            'tags': {'Name': name}
+        }
+
+        if key_pair:
+            params['tags'].update(Key_Pair=key_pair.name)
+
+        if user_data:
+            custom_data = base64.b64encode(bytes(ud, 'utf-8'))
+            params['os_profile']['custom_data'] = str(custom_data, 'utf-8')
+
+        try:
+            vm = self.provider.azure_client.create_vm(instance_name, params)
+        except Exception as e:
+            # If VM creation fails, attempt to clean up intermediary resources
+            self.provider.azure_client.delete_nic(nic_info.id)
+            for disk_def in storage_profile.get('data_disks', []):
+                if disk_def.get('tags', {}).get('delete_on_terminate'):
+                    disk_id = disk_def.get('managed_disk', {}).get('id')
+                    if disk_id:
+                        self.provider.storage.volumes.delete(disk_id)
+            raise e
+        finally:
+            if temp_key_pair:
+                temp_key_pair.delete()
+        return AzureInstance(self.provider, vm)
+
+    def _resolve_launch_options(self, name, subnet=None, zone_id=None,
+                                vm_firewalls=None):
+        if subnet:
+            # subnet's zone takes precedence
+            zone_id = subnet.zone.id
+        vm_firewall_id = None
+
+        if isinstance(vm_firewalls, list) and len(vm_firewalls) > 0:
+
+            if isinstance(vm_firewalls[0], VMFirewall):
+                vm_firewalls_ids = [fw.id for fw in vm_firewalls]
+                vm_firewall_id = vm_firewalls[0].resource_id
+            else:
+                vm_firewalls_ids = vm_firewalls
+                vm_firewall = self.provider.security.\
+                    vm_firewalls.get(vm_firewalls[0])
+                vm_firewall_id = vm_firewall.resource_id
+
+            if len(vm_firewalls) > 1:
+                new_fw = self.provider.security.vm_firewalls.\
+                    create('{0}-fw'.format(name), 'Merge vm firewall {0}'.
+                           format(','.join(vm_firewalls_ids)))
+
+                for fw in vm_firewalls:
+                    new_fw.add_rule(src_dest_fw=fw)
+
+                vm_firewall_id = new_fw.resource_id
+
+        return subnet.resource_id, zone_id, vm_firewall_id
+
+    def _create_storage_profile(self, image, launch_config, instance_name,
+                                zone_id):
+
+        storage_profile = {
+            'image_reference': {
+                'id': image.resource_id
+            },
+            "os_disk": {
+                "name": instance_name + '_os_disk',
+                "create_option": DiskCreateOption.from_image
+            },
+        }
+
+        if launch_config:
+            data_disks, root_disk_size = self._process_block_device_mappings(
+                launch_config, instance_name, zone_id)
+            if data_disks:
+                storage_profile['data_disks'] = data_disks
+            if root_disk_size:
+                storage_profile['os_disk']['disk_size_gb'] = root_disk_size
+
+        return storage_profile
+
+    def _process_block_device_mappings(self, launch_config,
+                                       vm_name, zone=None):
+        """
+        Processes block device mapping information
+        and returns a Data disk dictionary list. If new volumes
+        are requested (source is None and destination is VOLUME), they will be
+        created and the relevant volume ids included in the mapping.
+        """
+        data_disks = []
+        root_disk_size = None
+
+        def append_disk(disk_def, device_no, delete_on_terminate):
+            # In azure, there is no option to specify terminate disks
+            # (similar to AWS delete_on_terminate) on VM delete.
+            # This method uses the azure tags functionality to store
+            # the  delete_on_terminate option when the virtual machine
+            # is deleted, we parse the tags and delete accordingly
+            disk_def['lun'] = device_no
+            disk_def['tags'] = {
+                'delete_on_terminate': delete_on_terminate
+            }
+            data_disks.append(disk_def)
+
+        for device_no, device in enumerate(launch_config.block_devices):
+            if device.is_volume:
+                if device.is_root:
+                    root_disk_size = device.size
+                else:
+                    # In azure, os disk automatically created,
+                    # we are ignoring the root disk, if specified
+                    if isinstance(device.source, Snapshot):
+                        snapshot_vol = device.source.create_volume()
+                        disk_def = {
+                            # pylint:disable=protected-access
+                            'name': snapshot_vol._volume.name,
+                            'create_option': DiskCreateOption.attach,
+                            'managed_disk': {
+                                'id': snapshot_vol.id
+                            }
+                        }
+                    elif isinstance(device.source, Volume):
+                        disk_def = {
+                            # pylint:disable=protected-access
+                            'name': device.source._volume.name,
+                            'create_option': DiskCreateOption.attach,
+                            'managed_disk': {
+                                'id': device.source.id
+                            }
+                        }
+                    elif isinstance(device.source, MachineImage):
+                        disk_def = {
+                            # pylint:disable=protected-access
+                            'name': device.source._volume.name,
+                            'create_option': DiskCreateOption.from_image,
+                            'source_resource_id': device.source.id
+                        }
+                    else:
+                        disk_def = {
+                            # pylint:disable=protected-access
+                            'create_option': DiskCreateOption.empty,
+                            'disk_size_gb': device.size
+                        }
+                    append_disk(disk_def, device_no,
+                                device.delete_on_terminate)
+            else:  # device is ephemeral
+                # in azure we cannot add the ephemeral disks explicitly
+                pass
+
+        return data_disks, root_disk_size
+
+    def create_launch_config(self):
+        return AzureLaunchConfig(self.provider)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all instances.
+        """
+        instances = [AzureInstance(self.provider, inst)
+                     for inst in self.provider.azure_client.list_vm()]
+        return ClientPagedResultList(self.provider, instances,
+                                     limit=limit, marker=marker)
+
+    def get(self, instance_id):
+        """
+        Returns an instance given its id. Returns None
+        if the object does not exist.
+        """
+        try:
+            vm = self.provider.azure_client.get_vm(instance_id)
+            return AzureInstance(self.provider, vm)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filtr = {'Name': name}
+        instances = [AzureInstance(self.provider, inst)
+                     for inst in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_vm(), filtr)]
+        return ClientPagedResultList(self.provider, instances)
+
+
+class AzureImageService(BaseImageService):
+    def __init__(self, provider):
+        super(AzureImageService, self).__init__(provider)
+
+    def get(self, image_id):
+        """
+        Returns an Image given its id
+        """
+        try:
+            image = self.provider.azure_client.get_image(image_id)
+            return AzureMachineImage(self.provider, image)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        cb_images = [AzureMachineImage(self.provider, image)
+                     for image in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_images(), filters)]
+        return ClientPagedResultList(self.provider, cb_images)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all images.
+        """
+        azure_images = self.provider.azure_client.list_images()
+        cb_images = [AzureMachineImage(self.provider, img)
+                     for img in azure_images]
+        return ClientPagedResultList(self.provider, cb_images,
+                                     limit=limit, marker=marker)
+
+
+class AzureVMTypeService(BaseVMTypeService):
+
+    def __init__(self, provider):
+        super(AzureVMTypeService, self).__init__(provider)
+
+    @property
+    def instance_data(self):
+        """
+        Fetch info about the available instances.
+        """
+        r = self.provider.azure_client.list_vm_types()
+        return r
+
+    def list(self, limit=None, marker=None):
+        vm_types = [AzureVMType(self.provider, vm_type)
+                    for vm_type in self.instance_data]
+        return ClientPagedResultList(self.provider, vm_types,
+                                     limit=limit, marker=marker)
+
+
+class AzureNetworkingService(BaseNetworkingService):
+    def __init__(self, provider):
+        super(AzureNetworkingService, self).__init__(provider)
+        self._network_service = AzureNetworkService(self.provider)
+        self._subnet_service = AzureSubnetService(self.provider)
+        self._router_service = AzureRouterService(self.provider)
+
+    @property
+    def networks(self):
+        return self._network_service
+
+    @property
+    def subnets(self):
+        return self._subnet_service
+
+    @property
+    def routers(self):
+        return self._router_service
+
+
+class AzureNetworkService(BaseNetworkService):
+    def __init__(self, provider):
+        super(AzureNetworkService, self).__init__(provider)
+
+    def get(self, network_id):
+        try:
+            network = self.provider.azure_client.get_network(network_id)
+            return AzureNetwork(self.provider, network)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def list(self, limit=None, marker=None):
+        """
+        List all networks.
+        """
+        networks = [AzureNetwork(self.provider, network)
+                    for network in self.provider.azure_client.list_networks()]
+        return ClientPagedResultList(self.provider, networks,
+                                     limit=limit, marker=marker)
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        networks = [AzureNetwork(self.provider, network)
+                    for network in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_networks(), filters)]
+        return ClientPagedResultList(self.provider, networks)
+
+    def create(self, name, cidr_block):
+        # Azure requires CIDR block to be specified when creating a network
+        # so set a default one and use the largest allowed netmask.
+        network_name = AzureNetwork.CB_DEFAULT_NETWORK_NAME
+        if name:
+            network_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
+
+        AzureNetwork.assert_valid_resource_name(network_name)
+
+        params = {
+            'location': self.provider.azure_client.region_name,
+            'address_space': {
+                'address_prefixes': [cidr_block]
+            },
+            'tags': {'Name': name or AzureNetwork.CB_DEFAULT_NETWORK_NAME}
+        }
+        az_network = self.provider.azure_client.create_network(network_name,
+                                                               params)
+        cb_network = AzureNetwork(self.provider, az_network)
+        return cb_network
+
+    def delete(self, network_id):
+        """
+        Delete an existing network.
+        """
+        self.provider.azure_client.delete_network(network_id)
+
+
+class AzureRegionService(BaseRegionService):
+    def __init__(self, provider):
+        super(AzureRegionService, self).__init__(provider)
+
+    def get(self, region_id):
+        region = None
+        for azureRegion in self.provider.azure_client.list_locations():
+            if azureRegion.name == region_id:
+                region = AzureRegion(self.provider, azureRegion)
+                break
+        return region
+
+    def list(self, limit=None, marker=None):
+        regions = [AzureRegion(self.provider, region)
+                   for region in self.provider.azure_client.list_locations()]
+        return ClientPagedResultList(self.provider, regions,
+                                     limit=limit, marker=marker)
+
+    @property
+    def current(self):
+        return self.get(self.provider.region_name)
+
+
+class AzureSubnetService(BaseSubnetService):
+
+    def __init__(self, provider):
+        super(AzureSubnetService, self).__init__(provider)
+
+    def get(self, subnet_id):
+        """
+         Azure does not provide an api to get the subnet directly by id.
+         It also requires the network id.
+         To make it consistent across the providers the following code
+         gets the specific code from the subnet list.
+
+        :param subnet_id:
+        :return:
+        """
+        try:
+            azure_subnet = self.provider.azure_client.get_subnet(subnet_id)
+            return AzureSubnet(self.provider,
+                               azure_subnet) if azure_subnet else None
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def list(self, network=None, limit=None, marker=None):
+        """
+        List subnets
+        """
+        return ClientPagedResultList(self.provider,
+                                     self._list_subnets(network),
+                                     limit=limit, marker=marker)
+
+    def _list_subnets(self, network=None):
+        result_list = []
+        if network:
+            network_id = network.id \
+                if isinstance(network, Network) else network
+            result_list = self.provider.azure_client.list_subnets(network_id)
+        else:
+            for net in self.provider.azure_client.list_networks():
+                result_list.extend(self.provider.azure_client.list_subnets(
+                    net.id
+                ))
+        subnets = [AzureSubnet(self.provider, subnet)
+                   for subnet in result_list]
+
+        return subnets
+
+    def create(self, network, cidr_block, name=None, **kwargs):
+        """
+        Create subnet
+        """
+        AzureSubnet.assert_valid_resource_name(name)
+        network_id = network.id \
+            if isinstance(network, Network) else network
+
+        if not name:
+            subnet_name = AzureSubnet.CB_DEFAULT_SUBNET_NAME
+        else:
+            subnet_name = name
+
+        subnet_info = self.provider.azure_client\
+            .create_subnet(
+                            network_id,
+                            subnet_name,
+                            {
+                                'address_prefix': cidr_block
+                            }
+                          )
+
+        return AzureSubnet(self.provider, subnet_info)
+
+    def get_or_create_default(self, zone=None):
+        default_cidr = '10.0.1.0/24'
+
+        # No provider-default Subnet exists, look for a library-default one
+        matches = self.find(name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
+        if matches:
+            return matches[0]
+
+        # No provider-default Subnet exists, try to create it (net + subnets)
+        networks = self.provider.networking.networks.find(
+            name=AzureNetwork.CB_DEFAULT_NETWORK_NAME)
+
+        if networks:
+            network = networks[0]
+        else:
+            network = self.provider.networking.networks.create(
+                AzureNetwork.CB_DEFAULT_NETWORK_NAME, '10.0.0.0/16')
+
+        subnet = self.create(network, default_cidr,
+                             name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
+        return subnet
+
+    def delete(self, subnet):
+        subnet_id = subnet.id if isinstance(subnet, Subnet) else subnet
+        self.provider.azure_client.delete_subnet(subnet_id)
+
+
+class AzureRouterService(BaseRouterService):
+    def __init__(self, provider):
+        super(AzureRouterService, self).__init__(provider)
+
+    def get(self, router_id):
+        try:
+            route = self.provider.azure_client.get_route_table(router_id)
+            return AzureRouter(self.provider, route)
+        except (CloudError, InvalidValueException) as cloudError:
+            # Azure raises the cloud error if the resource not available
+            log.exception(cloudError)
+            return None
+
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
+        filters = {'Name': name}
+        routes = [AzureRouter(self.provider, route)
+                  for route in azure_helpers.filter_by_tag(
+                self.provider.azure_client.list_route_tables(), filters)]
+
+        return ClientPagedResultList(self.provider, routes)
+
+    def list(self, limit=None, marker=None):
+        routes = [AzureRouter(self.provider, route)
+                  for route in
+                  self.provider.azure_client.list_route_tables()]
+        return ClientPagedResultList(self.provider,
+                                     routes,
+                                     limit=limit, marker=marker)
+
+    def create(self, name, network):
+        AzureRouter.assert_valid_resource_name(name)
+        parameters = {"location": self.provider.region_name,
+                      'tags': {'Name': name}}
+        route = self.provider.azure_client. \
+            create_route_table(name, parameters)
+        return AzureRouter(self.provider, route)

+ 3 - 3
cloudbridge/cloud/providers/openstack/helpers.py

@@ -7,7 +7,7 @@ import logging as log
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 
 
-def os_result_limit(provider, requested_limit):
+def os_result_limit(provider, requested_limit=None):
     """
     Calculates the limit for OpenStack.
     """
@@ -21,7 +21,7 @@ def os_result_limit(provider, requested_limit):
     return limit + 1
 
 
-def to_server_paged_list(provider, objects, limit):
+def to_server_paged_list(provider, objects, limit=None):
     """
     A convenience function for wrapping a list of OpenStack native objects in
     a ServerPagedResultList. OpenStack
@@ -31,7 +31,7 @@ def to_server_paged_list(provider, objects, limit):
     """
     limit = limit or provider.config.default_result_limit
     is_truncated = len(objects) > limit
-    next_token = objects[limit].id if is_truncated else None
+    next_token = objects[limit-1].id if is_truncated else None
     results = ServerPagedResultList(is_truncated,
                                     next_token,
                                     False)

+ 1 - 6
cloudbridge/cloud/providers/openstack/provider.py

@@ -1,7 +1,6 @@
 """Provider implementation based on OpenStack Python clients for OpenStack."""
 
 import inspect
-
 import os
 
 from cinderclient import client as cinder_client
@@ -18,7 +17,6 @@ from novaclient import client as nova_client
 from novaclient import shell as nova_shell
 
 from openstack import connection
-from openstack import profile
 
 from swiftclient import client as swift_client
 
@@ -126,11 +124,8 @@ class OpenStackCloudProvider(BaseCloudProvider):
         return self._cached_keystone_session
 
     def _connect_openstack(self):
-        prof = profile.Profile()
-        prof.set_region(profile.Profile.ALL, self.region_name)
-
         return connection.Connection(
-            profile=prof,
+            region_name=self.region_name,
             user_agent='cloudbridge',
             auth_url=self.auth_url,
             project_name=self.project_name,

+ 147 - 22
cloudbridge/cloud/providers/openstack/resources.py

@@ -3,15 +3,17 @@ DataTypes used by this provider
 """
 import inspect
 import ipaddress
-
 import logging
 import os
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketContainer
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseFloatingIP
+from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
+from cloudbridge.cloud.base.resources import BaseGatewayContainer
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInternetGateway
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -47,9 +49,9 @@ from neutronclient.common.exceptions import PortNotFoundClient
 import novaclient.exceptions as novaex
 
 from openstack.exceptions import HttpException
+from openstack.exceptions import ResourceNotFound
 
 import swiftclient
-
 from swiftclient.service import SwiftService, SwiftUploadObject
 
 
@@ -209,7 +211,7 @@ class OpenStackVMType(BaseVMType):
 
     @property
     def ram(self):
-        return self._os_flavor.ram
+        return int(self._os_flavor.ram) / 1024
 
     @property
     def size_root_disk(self):
@@ -364,6 +366,35 @@ class OpenStackInstance(BaseInstance):
         """
         return getattr(self._os_instance, 'OS-EXT-AZ:availability_zone', None)
 
+    @property
+    def subnet_id(self):
+        """
+        Extract (one) subnet id associated with this instance.
+
+        In OpenStack, instances are associated with ports instead of
+        instances so we need to dig through several connections to retrieve
+        the subnet_id. Further, there can potentially be several ports
+        connected to to different subnets. This implementation retrieves one
+        subnet, the one corresponding to port associated with the first
+        private IP associated with the instance.
+        """
+        # MAC address can be used to identify a port so extract the MAC
+        # address corresponding to the (first) private IP associated with the
+        # instance.
+        for net in self._os_instance.to_dict().get('addresses').keys():
+            for iface in self._os_instance.to_dict().get('addresses')[net]:
+                if iface.get('OS-EXT-IPS:type') == 'fixed':
+                    port = iface.get('OS-EXT-IPS-MAC:mac_addr')
+                    addr = iface.get('addr')
+                    break
+        # Now get a handle to a port with the given MAC address and get the
+        # subnet to which the private IP is connected as the desired id.
+        for prt in self._provider.neutron.list_ports().get('ports'):
+            if prt.get('mac_address') == port:
+                for ip in prt.get('fixed_ips'):
+                    if ip.get('ip_address') == addr:
+                        return ip.get('subnet_id')
+
     @property
     def vm_firewalls(self):
         return [
@@ -396,19 +427,31 @@ class OpenStackInstance(BaseInstance):
         return OpenStackMachineImage(
             self._provider, self._provider.compute.images.get(image_id))
 
+    def _get_fip(self, floating_ip):
+        """Get a floating IP object based on the supplied ID."""
+        return OpenStackFloatingIP(
+            self._provider,
+            self._provider.os_conn.network.get_ip(floating_ip))
+
     def add_floating_ip(self, floating_ip):
         """
         Add a floating IP address to this instance.
         """
         log.debug("Adding floating IP adress: %s", floating_ip)
-        self._os_instance.add_floating_ip(floating_ip.public_ip)
+        fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
+               else self._get_fip(floating_ip))
+        self._provider.os_conn.compute.add_floating_ip_to_server(
+            self.id, fip.public_ip)
 
     def remove_floating_ip(self, floating_ip):
         """
         Remove a floating IP address from this instance.
         """
         log.debug("Removing floating IP adress: %s", floating_ip)
-        self._os_instance.remove_floating_ip(floating_ip.public_ip)
+        fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
+               else self._get_fip(floating_ip))
+        self._provider.os_conn.compute.remove_floating_ip_from_server(
+            self.id, fip.public_ip)
 
     def add_vm_firewall(self, firewall):
         """
@@ -710,6 +753,53 @@ class OpenStackSnapshot(BaseSnapshot):
         return cb_vol
 
 
+class OpenStackGatewayContainer(BaseGatewayContainer):
+    """For OpenStack, an internet gateway is a just an 'external' network."""
+
+    def __init__(self, provider, network):
+        super(OpenStackGatewayContainer, self).__init__(provider, network)
+
+    def _check_fip_connectivity(self, external_net):
+        # Due to current limitations in OpenStack:
+        # https://bugs.launchpad.net/neutron/+bug/1743480, it's not
+        # possible to differentiate between floating ip networks and provider
+        # external networks. Therefore, we systematically step through
+        # all available networks and perform an assignment test to infer valid
+        # floating ip nets.
+        dummy_router = self._provider.networking.routers.create(
+            network=self._network, name='cb_conn_test_router')
+        with cb_helpers.cleanup_action(lambda: dummy_router.delete()):
+            try:
+                dummy_router.attach_gateway(external_net)
+                return True
+            except Exception:
+                return False
+
+    def get_or_create_inet_gateway(self, name=None):
+        """For OS, inet gtw is any net that has `external` property set."""
+        if name:
+            OpenStackInternetGateway.assert_valid_resource_name(name)
+
+        external_nets = (n for n in self._provider.networking.networks
+                         if n.external)
+        for net in external_nets:
+            if self._check_fip_connectivity(net):
+                return OpenStackInternetGateway(self._provider, net)
+        return None
+
+    def delete(self, gateway):
+        log.debug("Deleting OpenStack Gateway: %s", gateway)
+        gateway.delete()
+
+    def list(self, limit=None, marker=None):
+        log.debug("OpenStack listing of all current internet gateways")
+        igl = [OpenStackInternetGateway(self._provider, n)
+               for n in self._provider.networking.networks
+               if n.external and self._check_fip_connectivity(n)]
+        return ClientPagedResultList(self._provider, igl, limit=limit,
+                                     marker=marker)
+
+
 class OpenStackNetwork(BaseNetwork):
 
     # Ref: https://github.com/openstack/neutron/blob/master/neutron/plugins/
@@ -728,6 +818,7 @@ class OpenStackNetwork(BaseNetwork):
     def __init__(self, provider, network):
         super(OpenStackNetwork, self).__init__(provider)
         self._network = network
+        self._gateway_service = OpenStackGatewayContainer(provider, self)
 
     @property
     def id(self):
@@ -764,7 +855,8 @@ class OpenStackNetwork(BaseNetwork):
         return ''
 
     def delete(self):
-        if self.id in str(self._provider.neutron.list_networks()):
+        if not self.external and self.id in str(
+                self._provider.neutron.list_networks()):
             # If there are ports associated with the network, it won't delete
             ports = self._provider.neutron.list_ports(
                 network_id=self.id).get('ports', [])
@@ -793,6 +885,10 @@ class OpenStackNetwork(BaseNetwork):
             # subnet no longer exists
             self._network.state = NetworkState.UNKNOWN
 
+    @property
+    def gateways(self):
+        return self._gateway_service
+
 
 class OpenStackSubnet(BaseSubnet):
 
@@ -856,6 +952,32 @@ class OpenStackSubnet(BaseSubnet):
             self._state = SubnetState.UNKNOWN
 
 
+class OpenStackFloatingIPContainer(BaseFloatingIPContainer):
+
+    def __init__(self, provider, gateway):
+        super(OpenStackFloatingIPContainer, self).__init__(provider, gateway)
+
+    def get(self, fip_id):
+        try:
+            return OpenStackFloatingIP(
+                self._provider, self._provider.os_conn.network.get_ip(fip_id))
+        except ResourceNotFound:
+            return None
+
+    def list(self, limit=None, marker=None):
+        fips = [OpenStackFloatingIP(self._provider, fip)
+                for fip in self._provider.os_conn.network.ips(
+                    floating_network_id=self.gateway.id
+                )]
+        return ClientPagedResultList(self._provider, fips,
+                                     limit=limit, marker=marker)
+
+    def create(self):
+        return OpenStackFloatingIP(
+            self._provider, self._provider.os_conn.network.create_ip(
+                floating_network_id=self.gateway.id))
+
+
 class OpenStackFloatingIP(BaseFloatingIP):
 
     def __init__(self, provider, floating_ip):
@@ -881,6 +1003,14 @@ class OpenStackFloatingIP(BaseFloatingIP):
     def delete(self):
         self._ip.delete(self._provider.os_conn.session)
 
+    def refresh(self):
+        net = self._provider.networking.networks.get(
+            self._ip.floating_network_id)
+        gw = net.gateways.get_or_create_inet_gateway()
+        fip = gw.floating_ips.get(self.id)
+        # pylint:disable=protected-access
+        self._ip = fip._ip
+
 
 class OpenStackRouter(BaseRouter):
 
@@ -966,6 +1096,7 @@ class OpenStackInternetGateway(BaseInternetGateway):
             # pylint:disable=protected-access
             gateway_net = gateway_net._network
         self._gateway_net = gateway_net
+        self._fips_container = OpenStackFloatingIPContainer(provider, self)
 
     @property
     def id(self):
@@ -985,7 +1116,7 @@ class OpenStackInternetGateway(BaseInternetGateway):
 
     @property
     def network_id(self):
-        return self._gateway_net.id
+        return self._gateway_net.get('id')
 
     def refresh(self):
         """Refresh the state of this network by re-querying the provider."""
@@ -1006,23 +1137,16 @@ class OpenStackInternetGateway(BaseInternetGateway):
         """Do nothing on openstack"""
         pass
 
+    @property
+    def floating_ips(self):
+        return self._fips_container
+
 
 class OpenStackKeyPair(BaseKeyPair):
 
     def __init__(self, provider, key_pair):
         super(OpenStackKeyPair, self).__init__(provider, key_pair)
 
-    @property
-    def material(self):
-        """
-        Unencrypted private key.
-
-        :rtype: str
-        :return: Unencrypted private key or ``None`` if not available.
-
-        """
-        return getattr(self._key_pair, 'private_key', None)
-
 
 class OpenStackVMFirewall(BaseVMFirewall):
 
@@ -1337,10 +1461,11 @@ class OpenStackBucketContainer(BaseBucketContainer):
             cb_objects,
             limit)
 
-    def find(self, name, limit=None, marker=None):
-        objects = [obj for obj in self if obj.name == name]
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
     def create(self, object_name):
         self._provider.swift.put_object(self.bucket.name, object_name, None)

+ 108 - 136
cloudbridge/cloud/providers/openstack/services.py

@@ -7,12 +7,11 @@ import re
 
 from cinderclient.exceptions import NotFound as CinderNotFound
 
+import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.services import BaseBucketService
 from cloudbridge.cloud.base.services import BaseComputeService
-from cloudbridge.cloud.base.services import BaseFloatingIPService
-from cloudbridge.cloud.base.services import BaseGatewayService
 from cloudbridge.cloud.base.services import BaseImageService
 from cloudbridge.cloud.base.services import BaseInstanceService
 from cloudbridge.cloud.base.services import BaseKeyPairService
@@ -27,7 +26,8 @@ from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVMFirewallService
 from cloudbridge.cloud.base.services import BaseVMTypeService
 from cloudbridge.cloud.base.services import BaseVolumeService
-from cloudbridge.cloud.interfaces.exceptions import ProviderInternalException
+from cloudbridge.cloud.interfaces.exceptions \
+    import DuplicateResourceException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import PlacementZone
@@ -45,7 +45,6 @@ from novaclient.exceptions import NotFound as NovaNotFound
 from openstack.exceptions import ResourceNotFound
 
 from .resources import OpenStackBucket
-from .resources import OpenStackFloatingIP
 from .resources import OpenStackInstance
 from .resources import OpenStackInternetGateway
 from .resources import OpenStackKeyPair
@@ -154,35 +153,38 @@ class OpenStackKeyPairService(BaseKeyPairService):
         return ClientPagedResultList(self.provider, results,
                                      limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for a key pair by a given list of attributes.
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         keypairs = self.provider.nova.keypairs.findall(name=name)
         results = [OpenStackKeyPair(self.provider, kp)
                    for kp in keypairs]
         log.debug("Searching for %s in: %s", name, keypairs)
-        return ClientPagedResultList(self.provider, results,
-                                     limit=limit, marker=marker)
-
-    def create(self, name):
-        """
-        Create a new key pair or raise an exception if one already exists.
+        return ClientPagedResultList(self.provider, results)
 
-        :type name: str
-        :param name: The name of the key pair to be created.
-
-        :rtype: ``object`` of :class:`.KeyPair`
-        :return:  A key pair instance or ``None`` if one was not be created.
-        """
+    def create(self, name, public_key_material=None):
         log.debug("Creating a new key pair with the name: %s", name)
         OpenStackKeyPair.assert_valid_resource_name(name)
 
-        kp = self.provider.nova.keypairs.create(name)
-        if kp:
-            return OpenStackKeyPair(self.provider, kp)
-        log.debug("Key Pair with the name %s already exists", name)
-        return None
+        existing_kp = self.find(name=name)
+        if existing_kp:
+            raise DuplicateResourceException(
+                'Keypair already exists with name {0}'.format(name))
+
+        private_key = None
+        if not public_key_material:
+            public_key_material, private_key = cb_helpers.generate_key_pair()
+
+        kp = self.provider.nova.keypairs.create(name,
+                                                public_key=public_key_material)
+        cb_kp = OpenStackKeyPair(self.provider, kp)
+        cb_kp.material = private_key
+        return cb_kp
 
 
 class OpenStackVMFirewallService(BaseVMFirewallService):
@@ -219,13 +221,19 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
             return OpenStackVMFirewall(self.provider, sg)
         return None
 
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         log.debug("Searching for %s", name)
         sgs = [self.provider.os_conn.network.find_security_group(name)]
         results = [OpenStackVMFirewall(self.provider, sg)
                    for sg in sgs if sg]
-        return ClientPagedResultList(self.provider, results,
-                                     limit=limit, marker=marker)
+        return ClientPagedResultList(self.provider, results)
 
     def delete(self, group_id):
         log.debug("Deleting OpenStack Firewall with the id: %s", group_id)
@@ -253,10 +261,14 @@ class OpenStackImageService(BaseImageService):
                       image_id)
             return None
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for an image by a given list of attributes
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         log.debug("Searching for the OpenStack image with the name: %s", name)
         regex = fnmatch.translate(name)
         cb_images = [
@@ -264,7 +276,7 @@ class OpenStackImageService(BaseImageService):
             for img in self
             if img.name and re.search(regex, img.name)]
 
-        return oshelpers.to_server_paged_list(self.provider, cb_images, limit)
+        return oshelpers.to_server_paged_list(self.provider, cb_images)
 
     def list(self, filter_by_owner=True, limit=None, marker=None):
         """
@@ -339,20 +351,24 @@ class OpenStackVolumeService(BaseVolumeService):
             log.debug("Volume %s was not found.", volume_id)
             return None
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for a volume by a given list of attributes.
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         log.debug("Searching for an OpenStack Volume with the name %s", name)
         search_opts = {'name': name}
         cb_vols = [
             OpenStackVolume(self.provider, vol)
             for vol in self.provider.cinder.volumes.list(
                 search_opts=search_opts,
-                limit=oshelpers.os_result_limit(self.provider, limit),
-                marker=marker)]
+                limit=oshelpers.os_result_limit(self.provider),
+                marker=None)]
 
-        return oshelpers.to_server_paged_list(self.provider, cb_vols, limit)
+        return oshelpers.to_server_paged_list(self.provider, cb_vols)
 
     def list(self, limit=None, marker=None):
         """
@@ -403,14 +419,17 @@ class OpenStackSnapshotService(BaseSnapshotService):
             log.debug("Snapshot %s was not found.", snapshot_id)
             return None
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for a volume by a given list of attributes.
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         search_opts = {'name': name,  # TODO: Cinder is ignoring name
-                       'limit': oshelpers.os_result_limit(self.provider,
-                                                          limit),
-                       'marker': marker}
+                       'limit': oshelpers.os_result_limit(self.provider),
+                       'marker': None}
         log.debug("Searching for an OpenStack volume with the following "
                   "params: %s", search_opts)
         cb_snaps = [
@@ -418,7 +437,7 @@ class OpenStackSnapshotService(BaseSnapshotService):
             snap in self.provider.cinder.volume_snapshots.list(search_opts)
             if snap.name == name]
 
-        return oshelpers.to_server_paged_list(self.provider, cb_snaps, limit)
+        return oshelpers.to_server_paged_list(self.provider, cb_snaps)
 
     def list(self, limit=None, marker=None):
         """
@@ -469,18 +488,22 @@ class OpenStackBucketService(BaseBucketService):
             log.debug("Bucket %s was not found.", bucket_id)
             return None
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for a bucket by a given list of attributes.
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         log.debug("Searching for the OpenStack Bucket with the name: %s", name)
         _, container_list = self.provider.swift.get_account(
-            limit=oshelpers.os_result_limit(self.provider, limit),
-            marker=marker)
+            limit=oshelpers.os_result_limit(self.provider),
+            marker=None)
         cb_buckets = [OpenStackBucket(self.provider, c)
                       for c in container_list
                       if name in c.get("name")]
-        return oshelpers.to_server_paged_list(self.provider, cb_buckets, limit)
+        return oshelpers.to_server_paged_list(self.provider, cb_buckets)
 
     def list(self, limit=None, marker=None):
         """
@@ -511,7 +534,7 @@ class OpenStackRegionService(BaseRegionService):
 
     def get(self, region_id):
         log.debug("Getting OpenStack Region with the id: %s", region_id)
-        region = (r for r in self.list() if r.id == region_id)
+        region = (r for r in self if r.id == region_id)
         return next(region, None)
 
     def list(self, limit=None, marker=None):
@@ -707,18 +730,22 @@ class OpenStackInstanceService(BaseInstanceService):
     def create_launch_config(self):
         return BaseLaunchConfig(self.provider)
 
-    def find(self, name, limit=None, marker=None):
-        """
-        Searches for an instance by a given list of attributes.
-        """
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         search_opts = {'name': name}
         cb_insts = [
             OpenStackInstance(self.provider, inst)
             for inst in self.provider.nova.servers.list(
                 search_opts=search_opts,
-                limit=oshelpers.os_result_limit(self.provider, limit),
-                marker=marker)]
-        return oshelpers.to_server_paged_list(self.provider, cb_insts, limit)
+                limit=oshelpers.os_result_limit(self.provider),
+                marker=None)]
+        return oshelpers.to_server_paged_list(self.provider, cb_insts)
 
     def list(self, limit=None, marker=None):
         """
@@ -749,9 +776,7 @@ class OpenStackNetworkingService(BaseNetworkingService):
         super(OpenStackNetworkingService, self).__init__(provider)
         self._network_service = OpenStackNetworkService(self.provider)
         self._subnet_service = OpenStackSubnetService(self.provider)
-        self._fip_service = OpenStackFloatingIPService(self.provider)
         self._router_service = OpenStackRouterService(self.provider)
-        self._gateway_service = OpenStackGatewayService(self.provider)
 
     @property
     def networks(self):
@@ -761,18 +786,10 @@ class OpenStackNetworkingService(BaseNetworkingService):
     def subnets(self):
         return self._subnet_service
 
-    @property
-    def floating_ips(self):
-        return self._fip_service
-
     @property
     def routers(self):
         return self._router_service
 
-    @property
-    def gateways(self):
-        return self._gateway_service
-
 
 class OpenStackNetworkService(BaseNetworkService):
 
@@ -791,15 +808,21 @@ class OpenStackNetworkService(BaseNetworkService):
         return ClientPagedResultList(self.provider, networks,
                                      limit=limit, marker=marker)
 
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'name'))
+
         log.debug("Searching for the OpenStack Network with the "
                   "name: %s", name)
         networks = [OpenStackNetwork(self.provider, network)
                     for network in self.provider.neutron.list_networks(
                         name=name)
                     .get('networks') if network]
-        return ClientPagedResultList(self.provider, networks,
-                                     limit=limit, marker=marker)
+        return ClientPagedResultList(self.provider, networks)
 
     def create(self, name, cidr_block):
         log.debug("Creating OpenStack Network with the params: "
@@ -825,7 +848,7 @@ class OpenStackSubnetService(BaseSubnetService):
         if network:
             network_id = (network.id if isinstance(network, OpenStackNetwork)
                           else network)
-            subnets = [subnet for subnet in self.list() if network_id ==
+            subnets = [subnet for subnet in self if network_id ==
                        subnet.network_id]
         else:
             subnets = [OpenStackSubnet(self.provider, subnet) for subnet in
@@ -865,10 +888,8 @@ class OpenStackSubnetService(BaseSubnetService):
             router = self.provider.networking.routers.create(
                 network=net, name=OpenStackRouter.CB_DEFAULT_ROUTER_NAME)
             router.attach_subnet(sn)
-            gteway = (self.provider.networking.gateways
-                      .get_or_create_inet_gateway(
-                          OpenStackInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME
-                          ))
+            gteway = net.gateways.get_or_create_inet_gateway(
+                        OpenStackInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME)
             router.attach_gateway(gteway)
             return sn
         except NeutronClientException:
@@ -880,41 +901,11 @@ class OpenStackSubnetService(BaseSubnetService):
                      else subnet)
         self.provider.neutron.delete_subnet(subnet_id)
         # Adhere to the interface docs
-        if subnet_id not in self.list():
+        if subnet_id not in self:
             return True
         return False
 
 
-class OpenStackFloatingIPService(BaseFloatingIPService):
-
-    def __init__(self, provider):
-        super(OpenStackFloatingIPService, self).__init__(provider)
-
-    def get(self, fip_id):
-        try:
-            return OpenStackFloatingIP(
-                self.provider, self.provider.os_conn.network.get_ip(fip_id))
-        except ResourceNotFound:
-            return None
-
-    def list(self, limit=None, marker=None):
-        fips = [OpenStackFloatingIP(self.provider, fip)
-                for fip in self.provider.os_conn.network.ips()]
-        return ClientPagedResultList(self.provider, fips,
-                                     limit=limit, marker=marker)
-
-    def create(self):
-        # OpenStack requires a floating IP to be associated with an external,
-        # network, so choose the first external network found
-        for n in self.provider.networking.networks:
-            if n.external:
-                return OpenStackFloatingIP(
-                    self.provider, self.provider.os_conn.network.create_ip(
-                        floating_network_id=n.id))
-        raise ProviderInternalException(
-            "This OpenStack cloud has no designated external network")
-
-
 class OpenStackRouterService(BaseRouterService):
 
     def __init__(self, provider):
@@ -931,12 +922,11 @@ class OpenStackRouterService(BaseRouterService):
         return ClientPagedResultList(self.provider, os_routers, limit=limit,
                                      marker=marker)
 
-    def find(self, name, limit=None, marker=None):
-        log.debug("Searching for OpenStack Router with the params: "
-                  "[name: %s, limit: %s, marker: %s]", name, limit, marker)
-        aws_routers = [r for r in self if r.name == name]
-        return ClientPagedResultList(self.provider, aws_routers, limit=limit,
-                                     marker=marker)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
     def create(self, name, network):
         """
@@ -952,21 +942,3 @@ class OpenStackRouterService(BaseRouterService):
         body = {'router': {'name': name}} if name else None
         router = self.provider.neutron.create_router(body)
         return OpenStackRouter(self.provider, router.get('router'))
-
-
-class OpenStackGatewayService(BaseGatewayService):
-
-    def __init__(self, provider):
-        super(OpenStackGatewayService, self).__init__(provider)
-
-    def get_or_create_inet_gateway(self, name):
-        OpenStackInternetGateway.assert_valid_resource_name(name)
-
-        for n in self.provider.networking.networks:
-            if n.external:
-                return OpenStackInternetGateway(self.provider, n)
-        return None
-
-    def delete(self, gateway):
-        log.debug("Deleting OpenStack Gateway: %s", gateway)
-        gateway.delete()

+ 177 - 162
docs/extras/_images/object_relationships_detailed.svg

@@ -15,7 +15,7 @@
    id="svg3515"
    version="1.1"
    inkscape:version="0.92.2 5c3e80d, 2017-08-06"
-   sodipodi:docname="object_relationships_detailed_orig.svg">
+   sodipodi:docname="object_relationships_detailed.svg">
   <metadata
      id="metadata3654">
     <rdf:RDF>
@@ -39,8 +39,8 @@
      guidetolerance="10"
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="1151"
+     inkscape:window-width="1680"
+     inkscape:window-height="1005"
      id="namedview3650"
      showgrid="false"
      inkscape:snap-bbox="true"
@@ -50,13 +50,13 @@
      inkscape:snap-bbox-midpoints="true"
      inkscape:snap-nodes="false"
      inkscape:snap-others="false"
-     inkscape:zoom="2.0646416"
-     inkscape:cx="178.79878"
-     inkscape:cy="250.65042"
+     inkscape:zoom="2.3597163"
+     inkscape:cx="207.32094"
+     inkscape:cy="151.06602"
      inkscape:window-x="0"
      inkscape:window-y="1"
      inkscape:window-maximized="1"
-     inkscape:current-layer="svg_18" />
+     inkscape:current-layer="a228" />
   <clipPath
      id="p.0">
     <path
@@ -210,50 +210,58 @@
      id="svg_130"
      d="m 552.35504,624.71287 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 z" />
   <path
-     style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
+     style="fill:#000000;fill-opacity:0;fill-rule:nonzero;stroke-width:1.13679719"
      inkscape:connector-curvature="0"
      id="svg_147"
-     d="m 723.07745,265.8829 56.97638,0" />
+     d="m 723.07745,265.8829 h 73.63102" />
   <path
-     style="fill-rule:evenodd;stroke:#000000;stroke-linejoin:round"
+     style="fill-rule:evenodd;stroke:#000000;stroke-width:1.13679719;stroke-linejoin:round"
      inkscape:connector-curvature="0"
      id="svg_148"
-     d="m 723.07745,265.8829 50.97638,0" />
+     d="m 723.07745,265.8829 h 65.87717" />
   <path
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1.13679719"
      inkscape:connector-curvature="0"
      id="svg_149"
-     d="m 774.05383,267.53464 4.53809,-1.65174 -4.53809,-1.65173 0,3.30347 z" />
-  <path
-     style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
-     inkscape:connector-curvature="0"
-     id="svg_150"
-     d="m 723.07745,420.94882 56.97638,0" />
-  <path
-     style="fill-rule:evenodd;stroke:#000000;stroke-linejoin:round"
-     inkscape:connector-curvature="0"
-     id="svg_151"
-     d="m 723.07745,420.94882 50.97638,0" />
-  <path
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000"
-     inkscape:connector-curvature="0"
-     id="svg_152"
-     d="m 774.05383,422.60055 4.53809,-1.65173 -4.53809,-1.65173 0,3.30347 z" />
+     d="m 788.95462,267.53464 5.86461,-1.65174 -5.86461,-1.65173 z" />
+  <g
+     id="g3985"
+     transform="matrix(1.1990483,0,0,1,-143.92734,0)">
+    <path
+       d="m 723.07745,420.94882 h 56.97638"
+       id="svg_150"
+       inkscape:connector-curvature="0"
+       style="fill:#000000;fill-opacity:0;fill-rule:nonzero" />
+    <path
+       d="m 723.07745,420.94882 h 50.97638"
+       id="svg_151"
+       inkscape:connector-curvature="0"
+       style="fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
+    <path
+       d="m 774.05383,422.60055 4.53809,-1.65173 -4.53809,-1.65173 v 3.30347 z"
+       id="svg_152"
+       inkscape:connector-curvature="0"
+       style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
+  </g>
   <path
      style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
      inkscape:connector-curvature="0"
      id="svg_153"
      d="m 823.08923,675.71287 h 56.97638" />
-  <path
-     inkscape:connector-curvature="0"
-     id="svg_154"
-     d="M 724.99042,624.71287 H 775.9668"
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
-  <path
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000"
-     inkscape:connector-curvature="0"
-     id="svg_155"
-     d="m 775.06561,626.36461 4.53809,-1.65174 -4.53809,-1.65173 z" />
+  <g
+     id="g3989"
+     transform="matrix(1.1980338,0,0,0.96655058,-143.66068,20.896282)">
+    <path
+       style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round"
+       d="M 724.99042,624.71287 H 775.9668"
+       id="svg_154"
+       inkscape:connector-curvature="0" />
+    <path
+       d="m 775.06561,626.36461 4.53809,-1.65174 -4.53809,-1.65173 z"
+       id="svg_155"
+       inkscape:connector-curvature="0"
+       style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
+  </g>
   <path
      style="fill:#b6d7a8;fill-rule:nonzero"
      inkscape:connector-curvature="0"
@@ -271,16 +279,36 @@
      id="e19_texte"
      y="417.07101"
      x="733.17401">.rules</text>
-  <text
-     style="font-size:11.47889996px;line-height:0%;font-family:Arial;fill:#000000"
-     font-size="11.4789px"
-     id="e20_texte"
-     y="620.40527"
-     x="727.85992">.objects</text>
+  <g
+     id="g3963">
+    <g
+       id="g3966">
+      <text
+         style="font-size:11.47889996px;line-height:0%;font-family:Arial;fill:#000000"
+         font-size="11.4789px"
+         id="e20_texte"
+         y="620.40527"
+         x="727.85992">.objects</text>
+    </g>
+    <g
+       id="g3970"
+       transform="translate(0,57.14286)">
+      <text
+         x="727.85992"
+         y="811.8327"
+         id="text3968"
+         font-size="11.4789px"
+         style="font-size:11.47885731px;line-height:0%;font-family:Arial;fill:#000000;-inkscape-font-specification:'Arial, Normal';font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;">
+        <tspan
+           sodipodi:role="line"
+           id="tspan3980">.floating_ips</tspan>
+      </text>
+    </g>
+  </g>
   <a
      xlink:href="../api_docs/cloud/providers.html#cloudprovider"
-     target="_parent"
-     id="svg_14">
+     id="svg_14"
+     target="_parent">
     <path
        style="fill:#a4c2f4;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -298,8 +326,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#computeservice"
-     target="_parent"
-     id="svg_15">
+     id="svg_15"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -317,8 +345,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#securityservice"
-     target="_parent"
-     id="svg_17">
+     id="svg_17"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -333,8 +361,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#storageservice"
-     target="_parent"
-     id="svg_18">
+     id="svg_18"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -349,8 +377,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#imageservice"
-     target="_parent"
-     id="svg_21">
+     id="svg_21"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -368,8 +396,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#vmtypeservice"
-     target="_parent"
-     id="svg_23">
+     id="svg_23"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -387,8 +415,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#instanceservice"
-     target="_parent"
-     id="svg_24">
+     id="svg_24"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -406,8 +434,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#regionservice"
-     target="_parent"
-     id="svg_26">
+     id="svg_26"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -425,8 +453,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#keypairservice"
-     target="_parent"
-     id="svg_27">
+     id="svg_27"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -441,8 +469,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#vmfirewallservice"
-     target="_parent"
-     id="svg_29">
+     id="svg_29"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -457,8 +485,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#volumeservice"
-     target="_parent"
-     id="svg_30">
+     id="svg_30"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -473,8 +501,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#snapshotservice"
-     target="_parent"
-     id="svg_32">
+     id="svg_32"
+     target="_parent">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -489,9 +517,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#bucket"
-     target="_parent"
+     transform="translate(99.999997,-17.142858)"
      id="svg_25"
-     transform="translate(99.999997,-17.142858)">
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -509,9 +537,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#bucketobject"
-     target="_parent"
+     transform="translate(111.42856,-17.142858)"
      id="svg_33"
-     transform="translate(99.999997,-17.142858)">
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -529,13 +557,14 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#vmfirewallrule"
-     target="_parent"
-     id="svg_35">
+     transform="translate(11.428572)"
+     id="svg_35"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
        id="svg_12"
-       d="m 780.46631,408.83292 0,0 c 0,-3.11655 2.52649,-5.64304 5.64306,-5.64304 l 156.99732,0 c 1.49658,0 2.93201,0.59454 3.99023,1.6528 1.05835,1.05826 1.65283,2.49362 1.65283,3.99024 l 0,22.57217 c 0,3.11658 -2.52648,5.64304 -5.64306,5.64304 l -156.99732,0 0,0 c -3.11657,0 -5.64306,-2.52646 -5.64306,-5.64304 l 0,-22.57217 z" />
+       d="m 780.46631,408.83292 v 0 c 0,-3.11655 2.52649,-5.64304 5.64306,-5.64304 h 156.99732 c 1.49658,0 2.93201,0.59454 3.99023,1.6528 1.05835,1.05826 1.65283,2.49362 1.65283,3.99024 v 22.57217 c 0,3.11658 -2.52648,5.64304 -5.64306,5.64304 H 786.10937 v 0 c -3.11657,0 -5.64306,-2.52646 -5.64306,-5.64304 z" />
     <text
        style="font-size:18.36630058px;line-height:0%;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
        stroke-linejoin="null"
@@ -548,8 +577,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#vmfirewall"
-     target="_parent"
-     id="svg_36">
+     id="svg_36"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -567,8 +596,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#snapshot"
-     target="_parent"
-     id="svg_38">
+     id="svg_38"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -586,8 +615,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#volume"
-     target="_parent"
-     id="svg_39">
+     id="svg_39"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -605,8 +634,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#keypair"
-     target="_parent"
-     id="svg_41">
+     id="svg_41"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -624,13 +653,14 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#placementzone"
-     target="_parent"
-     id="svg_42">
+     transform="translate(17.142858)"
+     id="svg_42"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
        id="svg_87"
-       d="m 780.06561,254.5968 0,0 c 0,-3.11656 2.52649,-5.64303 5.64307,-5.64303 l 156.99737,0 c 1.49659,0 2.93195,0.59452 3.99024,1.6528 1.05829,1.05827 1.65277,2.4936 1.65277,3.99023 l 0,22.57218 c 0,3.11657 -2.52643,5.64303 -5.64301,5.64303 l -156.99737,0 0,0 c -3.11658,0 -5.64307,-2.52646 -5.64307,-5.64303 l 0,-22.57218 z" />
+       d="m 780.06561,254.5968 v 0 c 0,-3.11656 2.52649,-5.64303 5.64307,-5.64303 h 156.99737 c 1.49659,0 2.93195,0.59452 3.99024,1.6528 1.05829,1.05827 1.65277,2.4936 1.65277,3.99023 v 22.57218 c 0,3.11657 -2.52643,5.64303 -5.64301,5.64303 H 785.70868 v 0 c -3.11658,0 -5.64307,-2.52646 -5.64307,-5.64303 z" />
     <text
        style="font-size:18.36630058px;line-height:0%;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
        stroke-linejoin="null"
@@ -643,8 +673,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#region"
-     target="_parent"
-     id="svg_44">
+     id="svg_44"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -662,8 +692,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#instance"
-     target="_parent"
-     id="svg_45">
+     id="svg_45"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -681,8 +711,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#vmtype"
-     target="_parent"
-     id="svg_47">
+     id="svg_47"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -700,8 +730,8 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#machineimage"
-     target="_parent"
-     id="svg_49">
+     id="svg_49"
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -797,9 +827,9 @@
      inkscape:connector-curvature="0" />
   <a
      xlink:href="../api_docs/cloud/services.html#networkingservice"
-     transform="translate(0,203.43349)"
+     id="svg_18-0"
      target="_parent"
-     id="svg_18-0">
+     transform="translate(0,203.43349)">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -841,14 +871,14 @@
      width="51.428574" />
   <a
      xlink:href="../api_docs/cloud/services.html#snapshotservice"
-     id="a277"
+     transform="translate(0,17.42911)"
      target="_parent"
-     transform="translate(0,17.42911)" />
+     id="a277" />
   <a
      xlink:href="../api_docs/cloud/services.html#bucketservice"
-     id="a316"
+     transform="translate(-1,438.85381)"
      target="_parent"
-     transform="translate(-1,438.85381)">
+     id="a316">
     <path
        d="m 286.4042,175.07298 v 0 c 0,-3.11658 2.52646,-5.64305 5.64304,-5.64305 h 156.99738 c 1.49664,0 2.93197,0.59452 3.99023,1.65281 1.05829,1.05827 1.6528,2.4936 1.6528,3.99024 v 22.57217 c 0,3.11656 -2.52646,5.64305 -5.64303,5.64305 H 292.04724 v 0 c -3.11658,0 -5.64304,-2.52649 -5.64304,-5.64305 z"
        id="path312"
@@ -912,9 +942,9 @@
      style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
   <a
      xlink:href="../api_docs/cloud/resources.html#router"
-     transform="translate(99.999997,185.71424)"
+     target="_parent"
      id="a352"
-     target="_parent">
+     transform="translate(99.999997,185.71424)">
     <path
        d="m 455.79394,630.56958 v 0 c 0,-3.11652 2.52649,-5.64301 5.64307,-5.64301 h 156.99738 c 1.49664,0 2.93194,0.59455 3.99023,1.65278 1.05829,1.05828 1.65283,2.49365 1.65283,3.99023 v 22.57221 c 0,3.11657 -2.52649,5.64306 -5.64306,5.64306 H 461.43701 v 0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64306 z"
        id="path348"
@@ -932,9 +962,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#subnet"
-     id="a358"
+     transform="translate(0,202.8571)"
      target="_parent"
-     transform="translate(0,202.8571)">
+     id="a358">
     <path
        d="m 554.79394,571.91199 v 0 c 0,-3.11658 2.52649,-5.64301 5.64307,-5.64301 h 156.99738 c 1.49664,0 2.93194,0.59448 3.99023,1.65277 1.05829,1.05829 1.65283,2.49359 1.65283,3.99024 v 22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 H 560.43701 v 0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 z"
        id="path354"
@@ -952,9 +982,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#network"
-     id="a364"
+     transform="translate(0,202.8571)"
      target="_parent"
-     transform="translate(0,202.8571)">
+     id="a364">
     <path
        d="m 554.79394,529.47467 v 0 c 0,-3.11658 2.52649,-5.64307 5.64307,-5.64307 h 156.99738 c 1.49664,0 2.93194,0.59455 3.99023,1.65284 1.05829,1.05828 1.65283,2.49359 1.65283,3.99023 v 22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 H 560.43701 v 0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 z"
        id="path360"
@@ -990,9 +1020,9 @@
      inkscape:connector-curvature="0" />
   <a
      xlink:href="../api_docs/cloud/services.html#networkservice"
-     id="a396"
+     transform="translate(0,517.14261)"
      target="_parent"
-     transform="translate(0,517.14261)">
+     id="a396">
     <path
        d="m 286.4042,214.83531 v 0 c 0,-3.11656 2.52646,-5.64305 5.64304,-5.64305 h 156.99738 c 1.49664,0 2.93197,0.59454 3.99023,1.65282 1.05829,1.05827 1.6528,2.4936 1.6528,3.99023 v 22.57218 c 0,3.11657 -2.52646,5.64305 -5.64303,5.64305 H 292.04724 v 0 c -3.11658,0 -5.64304,-2.52648 -5.64304,-5.64305 z"
        id="path392"
@@ -1010,9 +1040,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#subnetservice"
-     transform="translate(0,559.99971)"
+     id="a406"
      target="_parent"
-     id="a406">
+     transform="translate(0,559.99971)">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -1030,9 +1060,9 @@
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#routerservice"
-     transform="translate(0,602.85684)"
+     id="a422"
      target="_parent"
-     id="a422">
+     transform="translate(0,602.85684)">
     <path
        style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -1060,9 +1090,9 @@
      d="m 552.35504,870.42705 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 z" />
   <a
      xlink:href="../api_docs/cloud/resources.html#floatingip"
-     target="_parent"
+     transform="translate(99.999997,228.57131)"
      id="a432"
-     transform="translate(99.999997,228.57131)">
+     target="_parent">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
@@ -1076,7 +1106,7 @@
        y="648.35571"
        id="text430"
        font-size="18.3663px"
-       xml:space="preserve">FloatingIP</text>
+       xml:space="preserve">InternetGateway</text>
   </a>
   <path
      inkscape:connector-curvature="0"
@@ -1086,9 +1116,9 @@
      style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.73930079px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
   <a
      xlink:href="../api_docs/cloud/services.html#floatingipservice"
-     id="a440"
+     transform="translate(0,645.71391)"
      target="_parent"
-     transform="translate(0,645.71391)">
+     id="a440">
     <path
        d="m 286.4042,214.83531 v 0 c 0,-3.11656 2.52646,-5.64305 5.64304,-5.64305 h 156.99738 c 1.49664,0 2.93197,0.59454 3.99023,1.65282 1.05829,1.05827 1.6528,2.4936 1.6528,3.99023 v 22.57218 c 0,3.11657 -2.52646,5.64305 -5.64303,5.64305 H 292.04724 v 0 c -3.11658,0 -5.64304,-2.52648 -5.64304,-5.64305 z"
        id="path436"
@@ -1102,68 +1132,53 @@
        x="370.54593"
        stroke-linecap="null"
        stroke-linejoin="null"
-       style="font-size:18.36630058px;line-height:0%;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none">floating_ips</text>
+       style="font-size:18.36630058px;line-height:0%;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none">gateways</text>
   </a>
-  <path
-     d="m 454.18765,913.28415 h 96.66739"
-     id="path442"
-     inkscape:connector-curvature="0"
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
   <path
      d="m 552.35504,913.28415 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 z"
      id="path444"
      inkscape:connector-curvature="0"
      style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
+  <path
+     inkscape:connector-curvature="0"
+     inkscape:connector-type="polyline"
+     id="path460"
+     d="m 256.82176,713.3759 0.16488,158.39059"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.44018424px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
   <a
-     xlink:href="../api_docs/cloud/resources.html#internetgateway"
-     transform="translate(99.999997,271.42841)"
-     id="a450"
-     target="_parent">
+     xlink:href="../api_docs/cloud/resources.html#bucketobject"
+     target="_parent"
+     id="a228"
+     transform="translate(111.42856,228.81925)">
     <path
-       d="m 455.79394,630.56958 v 0 c 0,-3.11652 2.52649,-5.64301 5.64307,-5.64301 h 156.99738 c 1.49664,0 2.93194,0.59455 3.99023,1.65278 1.05829,1.05828 1.65283,2.49365 1.65283,3.99023 v 22.57221 c 0,3.11657 -2.52649,5.64306 -5.64306,5.64306 H 461.43701 v 0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64306 z"
-       id="path446"
+       d="m 681.06561,630.56958 v 0 c 0,-3.11652 2.52649,-5.64301 5.64307,-5.64301 h 156.99737 c 1.49659,0 2.93195,0.59455 3.99024,1.65278 1.05829,1.05828 1.65277,2.49365 1.65277,3.99023 v 22.57221 c 0,3.11657 -2.52643,5.64306 -5.64301,5.64306 H 686.70868 v 0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64306 z"
+       id="path224"
        inkscape:connector-curvature="0"
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000" />
     <text
        xml:space="preserve"
        font-size="18.3663px"
-       id="text448"
+       id="text226"
        y="648.35571"
-       x="539.93567"
+       x="765.20734"
        stroke-linecap="null"
        stroke-linejoin="null"
-       style="font-size:18.36630058px;line-height:0%;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none">InternetGateway</text>
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"><tspan
+         sodipodi:role="line"
+         id="tspan3997">FloatingIP</tspan></text>
   </a>
-  <path
-     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.73930079px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-     d="m 256.33097,915.59063 29.5658,-0.59455"
-     id="path452"
-     inkscape:connector-type="polyline"
-     inkscape:connector-curvature="0" />
-  <a
-     xlink:href="../api_docs/cloud/services.html#gatewayservice"
-     transform="translate(0,688.57101)"
-     target="_parent"
-     id="a458">
+  <g
+     id="g3993"
+     transform="matrix(1.1980338,0,0,0.96655058,-143.66068,29.210846)">
     <path
-       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
-       id="path454"
-       d="m 286.4042,214.83531 v 0 c 0,-3.11656 2.52646,-5.64305 5.64304,-5.64305 h 156.99738 c 1.49664,0 2.93197,0.59454 3.99023,1.65282 1.05829,1.05827 1.6528,2.4936 1.6528,3.99023 v 22.57218 c 0,3.11657 -2.52646,5.64305 -5.64303,5.64305 H 292.04724 v 0 c -3.11658,0 -5.64304,-2.52648 -5.64304,-5.64305 z" />
-    <text
-       style="font-size:18.36630058px;line-height:0%;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
-       stroke-linejoin="null"
-       stroke-linecap="null"
-       x="370.54593"
-       y="231.1214"
-       id="text456"
-       font-size="18.3663px"
-       xml:space="preserve">gateways</text>
-  </a>
-  <path
-     inkscape:connector-curvature="0"
-     inkscape:connector-type="polyline"
-     id="path460"
-     d="m 256.82201,713.37364 0.16438,202.59496"
-     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.6263299px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+       id="path3976"
+       d="M 724.99042,873.28414 H 775.9668"
+       style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
+    <path
+       style="fill:#000000;fill-rule:evenodd;stroke:#000000"
+       inkscape:connector-curvature="0"
+       id="path3978"
+       d="m 775.06561,874.93588 4.53809,-1.65174 -4.53809,-1.65173 z" />
+  </g>
 </svg>

+ 24 - 8
docs/getting_started.rst

@@ -66,6 +66,20 @@ OpenStack (with Keystone authentication v3):
                                                       config)
     image_id = '97755049-ee4f-4515-b92f-ca00991ee99a'  # Ubuntu 14.04 @ Jetstream
 
+Azure:
+
+.. code-block:: python
+
+    from cloudbridge.cloud.factory import CloudProviderFactory, ProviderList
+
+    config = {'azure_subscription_id': 'REPLACE WITH ACTUAL VALUE',
+              'azure_client_id': 'REPLACE WITH ACTUAL VALUE',
+              'azure_secret': 'REPLACE WITH ACTUAL VALUE',
+              'azure_tenant': ' REPLACE WITH ACTUAL VALUE'}
+    provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
+    image_id = 'ami-2d39803a'  # Ubuntu 14.04 (HVM)
+
+
 List some resources
 -------------------
 Once you have a reference to a provider, explore the cloud platform:
@@ -92,7 +106,7 @@ on disk as a read-only file.
     with open('cloudbridge_intro.pem', 'w') as f:
         f.write(kp.material)
     import os
-    os.chmod('cloudbridge_intro.pem', 0400)
+    os.chmod('cloudbridge_intro.pem', 0o400)
 
 Create a network
 ----------------
@@ -107,12 +121,12 @@ attaching an internet gateway to the subnet via a router.
     sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28')
     router = self.provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = self.provider.networking.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway(name)
     router.attach_gateway(gateway)
 
 
 Create a VM firewall
------------------------
+--------------------
 Next, we need to create a VM firewall (also commonly known as a security group)
 and add a rule to allow ssh access. A VM firewall needs to be associated with
 a private network.
@@ -138,7 +152,7 @@ also add the network interface as a launch argument.
                       key=lambda x: x.vcpus*x.ram)[0]
     inst = provider.compute.instances.create(
         name='cloudbridge-intro', image=img, vm_type=vm_type,
-        subnet=subnet, key_pair=kp, vm_firewalls=[fw])
+        subnet=sn, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
     # Show instance state
@@ -158,11 +172,13 @@ 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.
+and then associate it with the instance. Note that floating IPs are associated
+with an Internet Gateway so we allocate the IP under the gateway we dealt with
+earlier.
 
 .. code-block:: python
 
-    fip = provider.networking.floating_ips.create()
+    fip = gateway.floating_ips.create()
     inst.add_floating_ip(fip)
     inst.refresh()
     inst.public_ips
@@ -177,7 +193,7 @@ To wrap things up, let's clean up all the resources we have created
 
 .. code-block:: python
 
-    inst.terminate()
+    inst.delete()
     from cloudbridge.cloud.interfaces import InstanceState
     inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
                    terminal_states=[InstanceState.ERROR])  # Blocking call
@@ -186,7 +202,7 @@ To wrap things up, let's clean up all the resources we have created
     kp.delete()
     os.remove('cloudbridge_intro.pem')
     router.detach_gateway(gateway)
-    router.detach_subnet(subnet)
+    router.detach_subnet(sn)
     gateway.delete()
     router.delete()
     sn.delete()

+ 1 - 1
docs/topics/install.rst

@@ -34,7 +34,7 @@ To install additional libraries required by CloudBridge contributors, such as
 `tox <https://tox.readthedocs.org/en/latest/>`_, clone the source code
 repository and run the following command from the repository root directory::
 
-    pip install -e .[dev]
+    pip install -U -e .[dev]
 
 ----------
 

+ 1 - 1
docs/topics/launch.rst

@@ -67,7 +67,7 @@ that subnet.
     # make sure subnet has internet access
     router = self.provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = self.provider.networking.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway(name)
     router.attach_gateway(gateway)
 
     inst = provider.compute.instances.create(

+ 7 - 7
docs/topics/networking.rst

@@ -71,18 +71,18 @@ of the block and allow up to 16 IP addresses within a subnet (``/28``).
 
 .. code-block:: python
 
-    net = self.provider.networking.networks.create(
+    net = provider.networking.networks.create(
         name='my-network', cidr_block='10.0.0.0/16')
     sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28', zone=zone)
-    router = self.provider.networking.routers.create(network=net, name='my-router')
+    router = provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = self.provider.networking.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway(name)
     router.attach_gateway(gateway)
 
 
 2. Allowing internet access to a launched VM
-----------------------------------------------
-The additional step that's require here is to assign a floating ip to the VM.
+--------------------------------------------
+The additional step that's required here is to assign a floating IP to the VM:
 
 .. code-block:: python
 
@@ -94,10 +94,10 @@ The additional step that's require here is to assign a floating ip to the VM.
 
     router = provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = provider.networking.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway(net, name)
     router.attach_gateway(gateway)
 
-    fip = provider.networking.networks.create_floating_ip()
+    fip = provider.networking.floating_ips.create()
     vm.add_floating_ip(fip)
 
 

+ 3 - 3
docs/topics/object_storage.rst

@@ -34,8 +34,8 @@ To locate and download this uploaded file again, you can do the following:
 
 .. code-block:: python
 
-    bucket = provider.storage.buckets.find('my-bucket')[0]
-    obj = bucket.objects.find('my-data.txt')[0]
+    bucket = provider.storage.buckets.find(name='my-bucket')[0]
+    obj = bucket.objects.find(name='my-data.txt')[0]
     print("Size: {0}, Modified: {1}".format(obj.size, obj.last_modified))
     with open('/tmp/myfile.txt', 'wb') as f:
         obj.save_content(f)
@@ -65,6 +65,6 @@ Once a provider is obtained, you can access the container as usual:
 
 .. code-block:: python
 
-    bucket = provider.object_store.get(container)
+    bucket = provider.storage.buckets.get(container)
     obj = bucket.create_object('my_object.txt')
     obj.upload_from_file(source)

+ 0 - 1
docs/topics/overview.rst

@@ -14,4 +14,3 @@ Introductions to all the key parts of CloudBridge you'll need to know:
     Using block storage <block_storage.rst>
     Using object storage <object_storage.rst>
     Troubleshooting <troubleshooting.rst>
-

+ 24 - 1
docs/topics/setup.rst

@@ -33,6 +33,17 @@ OS_PROJECT_NAME      OS_STORAGE_URL
 OS_REGION_NAME       OS_AUTH_TOKEN
 ===================  ==================
 
+**Azure**
+
+======================  ==================
+Mandatory variables     Optional Variables
+======================  ==================
+AZURE_SUBSCRIPTION_ID   AZURE_REGION_NAME
+AZURE_CLIENT_ID         AZURE_RESOURCE_GROUP
+AZURE_SECRET            AZURE_STORAGE_ACCOUNT_NAME
+AZURE_TENANT            AZURE_VM_DEFAULT_USER_NAME
+                        AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
+======================  ==================
 
 Once the environment variables are set, you can create a connection as follows:
 
@@ -57,6 +68,18 @@ will override environment values.
               'aws_secret_key' : '<your_secret_key>'}
     provider = CloudProviderFactory().create_provider(ProviderList.AWS, config)
 
+
+    ## For Azure
+    config = {'azure_subscription_id': '<your_subscription_id>',
+              'azure_client_id': '<your_client_id>',
+              'azure_secret': '<your_secret>',
+              'azure_tenant': '<your_tenant>',
+              'azure_resource_group': '<your resource group>'}
+    provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
+
+For Azure, Create service principle credentials from the following link : 
+https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
+
 Some optional configuration values can only be provided through the config
 dictionary. These are listed below for each provider.
 
@@ -100,7 +123,7 @@ Providing access credentials in a file
 CloudBridge can also read credentials from a file on your local file system.
 The file should be placed in one of two locations: ``/etc/cloudbridge.ini`` or
 ``~/.cloudbridge``. Each set of credentials should be delineated with the
-provider ID (e.g., ``openstack``, ``aws``) with the necessary credentials
+provider ID (e.g., ``openstack``, ``aws``, ``azure``) with the necessary credentials
 being supplied in YAML format. Note that only one set of credentials per
 cloud provider type can be supplied (i.e., via this method, it is not possible
 to provide credentials for two different OpenStack clouds).

+ 1 - 1
docs/topics/testing.rst

@@ -43,7 +43,7 @@ Specific environment and infrastructure
 If you’d like to run the tests on a specific environment only, say Python 2.7,
 against a specific infrastructure, say aws, use a command like this:
 ``tox -e py27-aws``. The available provider names are listed in the
-`ProviderList`_ class (e.g., ``aws`` or ``openstack``).  
+`ProviderList`_ class (e.g., ``aws`` or ``openstack``).
 
 Specific test cases
 ~~~~~~~~~~~~~~~~~~~~

+ 18 - 0
setup.cfg

@@ -0,0 +1,18 @@
+[coverage:run]
+branch = True
+source = cloudbridge
+omit =
+  cloudbridge/cloud/interfaces/*
+
+[nosetests]
+with-coverage=1
+cover-branches=1
+cover-package=cloudbridge
+processes=5
+process-timeout=2700
+match=^[Tt]est 
+# Don't capture stdout - print immediately
+nocapture=1
+
+[bdist_wheel]
+universal = 1

+ 16 - 6
setup.py

@@ -24,8 +24,17 @@ REQS_BASE = [
     'retrying>=1.3.3'
 ]
 REQS_AWS = ['boto3']
+REQS_AZURE = ['msrest>=0.4.7',
+              'msrestazure>=0.4.7',
+              'azure-common>=1.1.5',
+              'azure-mgmt-resource>=1.0.0rc1',
+              'azure-mgmt-compute>=1.0.0rc1',
+              'azure-mgmt-network>=1.0.0rc1',
+              'azure-mgmt-storage>=1.0.0rc1',
+              'azure-storage>=0.34.0',
+              'pysftp>=0.2.9']
 REQS_OPENSTACK = [
-    'openstacksdk',
+    'openstacksdk>=0.12.0',
     'python-novaclient>=7.0.0',
     'python-glanceclient>=2.5.0',
     'python-cinderclient>=1.9.0',
@@ -33,12 +42,14 @@ REQS_OPENSTACK = [
     'python-neutronclient>=6.0.0',
     'python-keystoneclient>=3.13.0'
 ]
-REQS_FULL = REQS_BASE + REQS_AWS + REQS_OPENSTACK
+REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_OPENSTACK
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
 REQS_DEV = ([
     'tox>=2.1.1',
-    'moto>=1.1.11',
+    'nose',
+    # 'moto>=1.1.11',  # until https://github.com/spulec/moto/issues/1396
     'sphinx>=1.3.1',
+    'pydevd',
     'flake8>=3.3.0',
     'flake8-import-order>=0.12'] + REQS_FULL
 )
@@ -49,11 +60,10 @@ setup(
     description='A simple layer of abstraction over multiple cloud providers.',
     author='Galaxy and GVL Projects',
     author_email='help@genome.edu.au',
-    url='http://cloudbridge.readthedocs.org/',
+    url='http://cloudbridge.cloudve.org/',
     install_requires=REQS_FULL,
     extras_require={
-        ':python_version=="2.7"': ['py2-ipaddress'],
-        ':python_version=="3"': ['py2-ipaddress'],
+        ':python_version<"3.3"': ['ipaddress'],
         'full': REQS_FULL,
         'dev': REQS_DEV
     },

+ 19 - 0
test/fixtures/custom_amis.json

@@ -0,0 +1,19 @@
+[
+  {
+    "ami_id": "ami-aa2ea6d0",
+    "state": "available",
+    "public": true,
+    "owner_id": "099720109477",
+    "image_location": "amazon/getting-started",
+    "sriov": "simple",
+    "root_device_type": "ebs",
+    "root_device_name": "/dev/sda1",
+    "description": "Canonical, Ubuntu, 16.04 LTS, amd64 xenial image build on 2017-11-21",
+    "image_type": "machine",
+    "platform": null,
+    "architecture": "x86_64",
+    "name": "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-20171121.1",
+    "virtualization_type": "hvm",
+    "hypervisor": "xen"
+  }
+]

+ 40 - 3
test/helpers/__init__.py

@@ -4,7 +4,6 @@ import sys
 import traceback
 import unittest
 import uuid
-
 from contextlib import contextmanager
 
 from cloudbridge.cloud.factory import CloudProviderFactory
@@ -80,7 +79,8 @@ def skipIfNoService(services):
 
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
-        "image": os.environ.get('CB_IMAGE_AWS', 'ami-5ac2cd4d'),
+        # Match the ami value with entry in custom_amis.json for use with moto
+        "image": os.environ.get('CB_IMAGE_AWS', 'ami-aa2ea6d0'),
         "vm_type": os.environ.get('CB_VM_TYPE_AWS', 't2.nano'),
         "placement": os.environ.get('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
@@ -89,6 +89,17 @@ TEST_DATA_CONFIG = {
                                 '842b949c-ea76-48df-998d-8a41f2626243'),
         "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'zone-r1'),
+    },
+    "AzureCloudProvider": {
+        "placement":
+            os.environ.get('CB_PLACEMENT_AZURE', 'eastus'),
+        "image":
+            os.environ.get('CB_IMAGE_AZURE',
+                           '/subscriptions/7904d702-e01c-4826-8519-f5a25c866a9'
+                           '6/resourceGroups/cloudbridge/providers/Microsoft.C'
+                           'ompute/images/cb-test-image'),
+        "vm_type":
+            os.environ.get('CB_VM_TYPE_AZURE', 'Basic_A2'),
     }
 }
 
@@ -98,6 +109,8 @@ def get_provider_test_data(provider, key):
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
     elif "OpenStackCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
+    elif "AzureCloudProvider" in provider.name:
+        return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
     return None
 
 
@@ -123,10 +136,32 @@ def delete_test_network(network):
                 pass
 
 
+def get_test_gateway(provider, name):
+    """
+    Get an internet gateway for testing.
+
+    This includes creating a network for the gateway, which is also returned.
+    """
+    net_name = 'cb_testgwnet-{0}'.format(get_uuid())
+    net = provider.networking.networks.create(
+        name=net_name, cidr_block='10.0.0.0/16')
+    return net, net.gateways.get_or_create_inet_gateway(name)
+
+
+def delete_test_gateway(network, gateway):
+    """
+    Delete the supplied network and gateway.
+    """
+    with cleanup_action(lambda: network.delete()):
+        with cleanup_action(lambda: gateway.delete()):
+            pass
+
+
 def create_test_instance(
         provider, instance_name, subnet, launch_config=None,
         key_pair=None, vm_firewalls=None, user_data=None):
-    return provider.compute.instances.create(
+
+    instance = provider.compute.instances.create(
         instance_name,
         get_provider_test_data(provider, 'image'),
         get_provider_test_data(provider, 'vm_type'),
@@ -137,6 +172,8 @@ def create_test_instance(
         launch_config=launch_config,
         user_data=user_data)
 
+    return instance
+
 
 def get_test_instance(provider, name, key_pair=None, vm_firewalls=None,
                       subnet=None, user_data=None):

+ 8 - 16
test/helpers/standard_interface_tests.py

@@ -70,7 +70,7 @@ def check_find(test, service, obj):
         len(find_objs) == 1,
         "Find objects for %s does not return the expected object: %s. Got %s"
         % (type(obj).__name__, obj.name, find_objs))
-    test.assertEqual(find_objs[0], obj)
+    test.assertEqual(find_objs[0].id, obj.id)
     return find_objs
 
 
@@ -85,7 +85,7 @@ def check_find_non_existent(test, service):
 
 def check_get(test, service, obj):
     get_obj = service.get(obj.id)
-    test.assertEqual(get_obj, obj)
+    test.assertEqual(get_obj.id, obj.id)
     test.assertIsInstance(get_obj, type(obj))
     return get_obj
 
@@ -161,33 +161,25 @@ def check_standard_behaviour(test, service, obj):
     obj_get = check_get(test, service, obj)
     check_get_non_existent(test, service)
 
-    test.assertTrue(
-        obj == objs_list[0] == objs_iter[0] == objs_find[0] == obj_get,
-        "Objects returned by list: {0}, iter: {1}, find: {2} and get: {3} "
-        " are not as expected: {4}" .format(objs_list[0].id, objs_iter[0].id,
-                                            objs_find[0].id, obj_get.id,
-                                            obj.id))
-
     test.assertTrue(
         obj.id == objs_list[0].id == objs_iter[0].id ==
         objs_find[0].id == obj_get.id,
         "Object Ids returned by list: {0}, iter: {1}, find: {2} and get: {3} "
-        " are not as expected: {4}" .format(objs_list[0].id, objs_iter[0].id,
-                                            objs_find[0].id, obj_get.id,
-                                            obj.id))
+        " are not as expected: {4}".format(objs_list[0].id, objs_iter[0].id,
+                                           objs_find[0].id, obj_get.id,
+                                           obj.id))
 
     test.assertTrue(
         obj.name == objs_list[0].name == objs_iter[0].name ==
         objs_find[0].name == obj_get.name,
         "Names returned by list: {0}, iter: {1}, find: {2} and get: {3} "
-        " are not as expected: {4}" .format(objs_list[0].id, objs_iter[0].id,
-                                            objs_find[0].id, obj_get.id,
-                                            obj.id))
+        " are not as expected: {4}".format(objs_list[0].id, objs_iter[0].id,
+                                           objs_find[0].id, obj_get.id,
+                                           obj.id))
 
 
 def check_create(test, service, iface, name_prefix,
                  create_func, cleanup_func):
-
     # check create with invalid name
     with test.assertRaises(InvalidNameException):
         # spaces should raise an exception

+ 5 - 3
test/test_block_store_service.py

@@ -1,6 +1,5 @@
 import time
 import uuid
-
 from test import helpers
 from test.helpers import ProviderTestBase
 from test.helpers import standard_interface_tests as sit
@@ -18,6 +17,8 @@ import six
 
 class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['storage.volumes'])
     def test_crud_volume(self):
         """
@@ -114,8 +115,9 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                 self.assertEqual(test_vol.attachments.volume, test_vol)
                 self.assertEqual(test_vol.attachments.instance_id,
                                  test_instance.id)
-                self.assertEqual(test_vol.attachments.device,
-                                 "/dev/sda2")
+                if not self.provider.PROVIDER_ID == 'azure':
+                    self.assertEqual(test_vol.attachments.device,
+                                     "/dev/sda2")
                 test_vol.detach()
                 test_vol.name = 'newvolname1'
                 test_vol.wait_for(

+ 2 - 1
test/test_cloud_factory.py

@@ -1,5 +1,4 @@
 import unittest
-
 from test import helpers
 
 from cloudbridge.cloud import factory
@@ -13,6 +12,8 @@ from cloudbridge.cloud.providers.aws.provider import MockAWSCloudProvider
 
 class CloudFactoryTestCase(unittest.TestCase):
 
+    _multiprocess_can_split_ = True
+
     def test_create_provider_valid(self):
         """
         Creating a provider with a known name should return

+ 2 - 1
test/test_cloud_helpers.py

@@ -1,5 +1,4 @@
 import itertools
-
 from test.helpers import ProviderTestBase
 
 from cloudbridge.cloud.base.resources import ClientPagedResultList
@@ -18,6 +17,8 @@ class DummyResult(object):
 
 class CloudHelpersTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     def setUp(self):
         super(CloudHelpersTestCase, self).setUp()
         self.objects = [DummyResult(1, "One"),

+ 20 - 7
test/test_compute_service.py

@@ -1,5 +1,4 @@
 import ipaddress
-
 from test import helpers
 from test.helpers import ProviderTestBase
 from test.helpers import standard_interface_tests as sit
@@ -17,6 +16,8 @@ import six
 
 class CloudComputeServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
     def test_crud_instance(self):
         name = "cb_instcrud-{0}".format(helpers.get_uuid())
@@ -96,7 +97,13 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 test_instance.image_id,
                 helpers.get_provider_test_data(self.provider, "image"))
             self.assertIsInstance(test_instance.public_ips, list)
+            if test_instance.public_ips:
+                self.assertTrue(
+                    test_instance.public_ips[0], "public ip should contain a"
+                    " valid value if a list of public_ips exist")
             self.assertIsInstance(test_instance.private_ips, list)
+            self.assertTrue(test_instance.private_ips[0], "private ip should"
+                            " contain a valid value")
             self.assertEqual(
                 test_instance.key_pair_name,
                 kp.name)
@@ -176,7 +183,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         lc.add_volume_device(
             is_root=True,
             source=img,
-            size=img.min_disk if img and img.min_disk else 2,
+            size=img.min_disk if img and img.min_disk else 30,
             delete_on_terminate=True)
 
         # Attempting to add more than one root volume should raise an
@@ -256,7 +263,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 lc.add_volume_device(
                     is_root=True,
                     source=img,
-                    size=img.min_disk if img and img.min_disk else 2,
+                    size=img.min_disk if img and img.min_disk else 30,
                     delete_on_terminate=True)
 
                 # Add all available ephemeral devices
@@ -291,7 +298,6 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         # correspond to requested mappings
 
     @helpers.skipIfNoService(['compute.instances', 'networking.networks',
-                              'networking.floating_ips',
                               'security.vm_firewalls'])
     def test_instance_methods(self):
         name = "cb_instmethods-{0}".format(helpers.get_uuid())
@@ -338,11 +344,14 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             with helpers.cleanup_action(lambda: cleanup_router(router,
                                                                gateway)):
                 router.attach_subnet(subnet)
-                gateway = (self.provider.networking.gateways
-                           .get_or_create_inet_gateway(name))
+                gateway = net.gateways.get_or_create_inet_gateway(name)
                 router.attach_gateway(gateway)
                 # check whether adding an elastic ip works
-                fip = self.provider.networking.floating_ips.create()
+                fip = gateway.floating_ips.create()
+                self.assertFalse(
+                    fip.in_use,
+                    "Newly created floating IP address should not be in use.")
+
                 with helpers.cleanup_action(lambda: fip.delete()):
                     with helpers.cleanup_action(
                             lambda: test_inst.remove_floating_ip(fip)):
@@ -351,6 +360,10 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         # On Devstack, FloatingIP is listed under private_ips.
                         self.assertIn(fip.public_ip, test_inst.public_ips +
                                       test_inst.private_ips)
+                        fip.refresh()
+                        self.assertTrue(
+                            fip.in_use,
+                            "Attached floating IP address should be in use.")
                     test_inst.refresh()
                     self.assertNotIn(
                         fip.public_ip,

+ 2 - 0
test/test_image_service.py

@@ -8,6 +8,8 @@ from cloudbridge.cloud.interfaces.resources import MachineImage
 
 class CloudImageServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['compute.images', 'networking.networks',
                               'compute.instances'])
     def test_create_and_list_image(self):

+ 4 - 2
test/test_interface.py

@@ -1,9 +1,7 @@
 import unittest
-
 from test.helpers import ProviderTestBase
 
 import cloudbridge
-
 from cloudbridge.cloud import interfaces
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
@@ -12,6 +10,8 @@ from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 
 class CloudInterfaceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     def test_name_property(self):
         """
         Name should always return a value and should not raise an exception
@@ -59,6 +59,8 @@ class CloudInterfaceTestCase(ProviderTestBase):
         elif self.provider.PROVIDER_ID == 'openstack':
             cloned_config['os_username'] = "cb_dummy"
             cloned_config['os_password'] = "cb_dummy"
+        elif self.provider.PROVIDER_ID == 'azure':
+            cloned_config['azure_subscription_id'] = "cb_dummy"
 
         with self.assertRaises(ProviderConnectionException):
             cloned_provider = CloudProviderFactory().create_provider(

+ 44 - 29
test/test_network_service.py

@@ -1,5 +1,4 @@
 import test.helpers as helpers
-
 from test.helpers import ProviderTestBase
 from test.helpers import get_provider_test_data
 from test.helpers import standard_interface_tests as sit
@@ -12,6 +11,8 @@ from cloudbridge.cloud.interfaces.resources import Subnet
 
 class CloudNetworkServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['networking.networks'])
     def test_crud_network(self):
 
@@ -47,15 +48,22 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 % net.cidr_block)
 
             cidr = '10.0.1.0/24'
-            sn = net.create_subnet(name=subnet_name, cidr_block=cidr,
-                                   zone=helpers.get_provider_test_data(
-                                       self.provider, 'placement'))
+            sn = net.create_subnet(
+                name=subnet_name, cidr_block=cidr,
+                zone=helpers.get_provider_test_data(self.provider,
+                                                    'placement'))
             with helpers.cleanup_action(lambda: sn.delete()):
                 self.assertTrue(
                     sn in net.subnets,
                     "Subnet ID %s should be listed in network subnets %s."
                     % (sn.id, net.subnets))
 
+                self.assertTrue(
+                    sn in self.provider.networking.subnets.list(network=net),
+                    "Subnet ID %s should be included in the subnets list %s."
+                    % (sn.id, self.provider.networking.subnets.list(net))
+                )
+
                 self.assertListEqual(
                     net.subnets, [sn],
                     "Network should have exactly one subnet: %s." % sn.id)
@@ -94,38 +102,46 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             sit.check_crud(self, self.provider.networking.subnets, Subnet,
                            "cb_crudsubnet", create_subnet, cleanup_subnet)
 
-    @helpers.skipIfNoService(['networking.floating_ips'])
     def test_crud_floating_ip(self):
+        net, gw = helpers.get_test_gateway(
+            self.provider, 'cb_crudfipgw-{0}'.format(helpers.get_uuid()))
 
         def create_fip(name):
-            return self.provider.networking.floating_ips.create()
+            fip = gw.floating_ips.create()
+            return fip
 
         def cleanup_fip(fip):
-            self.provider.networking.floating_ips.delete(fip.id)
+            gw.floating_ips.delete(fip.id)
 
-        sit.check_crud(self, self.provider.networking.floating_ips, FloatingIP,
-                       "cb_crudfip", create_fip, cleanup_fip,
-                       skip_name_check=True)
+        with helpers.cleanup_action(
+                lambda: helpers.delete_test_gateway(net, gw)):
+            sit.check_crud(self, gw.floating_ips, FloatingIP,
+                           "cb_crudfip", create_fip, cleanup_fip,
+                           skip_name_check=True)
 
     def test_floating_ip_properties(self):
         # Check floating IP address
-        fip = self.provider.networking.floating_ips.create()
-        with helpers.cleanup_action(lambda: fip.delete()):
-            fipl = list(self.provider.networking.floating_ips)
-            self.assertIn(fip, fipl)
-            # 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.assertFalse(
-                fip.private_ip,
-                "Floating IP should not have a private IP value ({0})."
-                .format(fip.private_ip))
-            self.assertFalse(
-                fip.in_use,
-                "Newly created floating IP address should not be in use.")
+        net, gw = helpers.get_test_gateway(
+            self.provider, 'cb_crudfipgw-{0}'.format(helpers.get_uuid()))
+        fip = gw.floating_ips.create()
+        with helpers.cleanup_action(
+                lambda: helpers.delete_test_gateway(net, gw)):
+            with helpers.cleanup_action(lambda: fip.delete()):
+                fipl = list(gw.floating_ips)
+                self.assertIn(fip, fipl)
+                # 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.assertFalse(
+                    fip.private_ip,
+                    "Floating IP should not have a private IP value ({0})."
+                    .format(fip.private_ip))
+                self.assertFalse(
+                    fip.in_use,
+                    "Newly created floating IP address should not be in use.")
 
     @helpers.skipIfNoService(['networking.routers'])
     def test_crud_router(self):
@@ -169,8 +185,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 #                     router.id, router.network_id))
 
             router.attach_subnet(sn)
-            gteway = (self.provider.networking.gateways
-                      .get_or_create_inet_gateway(name))
+            gteway = net.gateways.get_or_create_inet_gateway(name)
             router.attach_gateway(gteway)
             # TODO: add a check for routes after that's been implemented
 

+ 2 - 0
test/test_object_life_cycle.py

@@ -7,6 +7,8 @@ from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
 class CloudObjectLifeCycleTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['storage.volumes'])
     def test_object_life_cycle(self):
         """

+ 3 - 1
test/test_object_store_service.py

@@ -2,7 +2,6 @@ import filecmp
 import os
 import tempfile
 import uuid
-
 from datetime import datetime
 from io import BytesIO
 from test import helpers
@@ -21,12 +20,15 @@ import requests
 
 class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket(self):
         """
         Create a new bucket, check whether the expected values are set,
         and delete it.
         """
+
         def create_bucket(name):
             return self.provider.storage.buckets.create(name)
 

+ 2 - 0
test/test_region_service.py

@@ -9,6 +9,8 @@ import six
 
 class CloudRegionServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['compute.regions'])
     def test_get_and_list_regions(self):
         """

+ 16 - 1
test/test_security_service.py

@@ -3,6 +3,8 @@ from test import helpers
 from test.helpers import ProviderTestBase
 from test.helpers import standard_interface_tests as sit
 
+import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VMFirewall
@@ -11,6 +13,8 @@ from cloudbridge.cloud.interfaces.resources import VMFirewallRule
 
 class CloudSecurityServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_crud_key_pair_service(self):
 
@@ -22,7 +26,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
         def extra_tests(kp):
             # Recreating existing keypair should raise an exception
-            with self.assertRaises(Exception):
+            with self.assertRaises(DuplicateResourceException):
                 self.provider.security.key_pairs.create(name=kp.name)
 
         sit.check_crud(self, self.provider.security.key_pairs, KeyPair,
@@ -42,6 +46,17 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             self.assertIsNone(kp.material,
                               "Keypair material should now be empty")
 
+    @helpers.skipIfNoService(['security.key_pairs'])
+    def test_import_key_pair(self):
+        name = 'cb_kpimport-{0}'.format(helpers.get_uuid())
+
+        public_key, _ = cb_helpers.generate_key_pair()
+        kp = self.provider.security.key_pairs.create(
+            name=name, public_key_material=public_key)
+        with helpers.cleanup_action(lambda: kp.delete()):
+            self.assertIsNone(kp.material, "Private KeyPair material should"
+                              " be None when key is imported.")
+
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_crud_vm_firewall(self):
         name = 'cb_crudfw-{0}'.format(helpers.get_uuid())

+ 2 - 1
test/test_vm_types_service.py

@@ -1,5 +1,4 @@
 from test import helpers
-
 from test.helpers import ProviderTestBase
 from test.helpers import standard_interface_tests as sit
 
@@ -8,6 +7,8 @@ import six
 
 class CloudVMTypeServiceTestCase(ProviderTestBase):
 
+    _multiprocess_can_split_ = True
+
     @helpers.skipIfNoService(['compute.vm_types'])
     def test_vm_type_properties(self):
 

+ 8 - 4
tox.ini

@@ -8,21 +8,25 @@
 # Alternatively, to run mock tests only, run tox as follows:
 # CB_USE_MOCK_PROVIDERS=True tox -e py27-aws
 #
-# Simply running tox -e py27-aws also works, because the default is to use
+# Simply running tox -e py27-aws also works because the default is to use
 # mock providers.
 
 [tox]
-envlist = {py27,py36,pypy}-{aws,openstack}
+envlist = {py27,py36,pypy}-{aws,azure,openstack}
 
 [testenv]
-commands = flake8 cloudbridge test setup.py 
-    {envpython} -m coverage run --branch --source=cloudbridge --omit=cloudbridge/cloud/interfaces/* setup.py test {posargs}
+commands = flake8 cloudbridge test setup.py
+           # see setup.cfg for options sent to nosetests and coverage
+           {envpython} setup.py nosetests {posargs}
 setenv =
+    MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
     aws: CB_TEST_PROVIDER=aws
+    azure: CB_TEST_PROVIDER=azure
     openstack: CB_TEST_PROVIDER=openstack
 passenv =
     CB_USE_MOCK_PROVIDERS
     aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
+    azure: AZURE_SUBSCRIPTION_ID AZURE_CLIENT_ID AZURE_SECRET AZURE_TENANT AZURE_REGION_NAME AZURE_RESOURCE_GROUP AZURE_STORAGE_ACCOUNT AZURE_VM_DEFAULT_USER_NAME AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
     openstack:  CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS OS_AUTH_URL OS_PASSWORD OS_PROJECT_NAME OS_TENANT_NAME OS_USERNAME OS_REGION_NAME OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME NOVA_SERVICE_NAME
 deps =
     -rrequirements.txt