Ehsan Chiniforooshan 8 лет назад
Родитель
Сommit
f23c77a37f
50 измененных файлов с 5014 добавлено и 3694 удалено
  1. 4 46
      .codeclimate.yml
  2. 65 17
      .travis.yml
  3. 5 0
      CHANGELOG.rst
  4. 47 4
      README.rst
  5. 1 1
      cloudbridge/__init__.py
  6. 19 3
      cloudbridge/cloud/base/provider.py
  7. 388 59
      cloudbridge/cloud/base/resources.py
  8. 109 20
      cloudbridge/cloud/base/services.py
  9. 19 0
      cloudbridge/cloud/factory.py
  10. 33 1
      cloudbridge/cloud/interfaces/exceptions.py
  11. 19 33
      cloudbridge/cloud/interfaces/provider.py
  12. 248 352
      cloudbridge/cloud/interfaces/resources.py
  13. 408 166
      cloudbridge/cloud/interfaces/services.py
  14. 326 0
      cloudbridge/cloud/providers/aws/helpers.py
  15. 56 96
      cloudbridge/cloud/providers/aws/provider.py
  16. 339 425
      cloudbridge/cloud/providers/aws/resources.py
  17. 356 452
      cloudbridge/cloud/providers/aws/services.py
  18. 3 0
      cloudbridge/cloud/providers/openstack/helpers.py
  19. 37 19
      cloudbridge/cloud/providers/openstack/provider.py
  20. 357 232
      cloudbridge/cloud/providers/openstack/resources.py
  21. 351 192
      cloudbridge/cloud/providers/openstack/services.py
  22. 15 0
      docs/api_docs/cloud/exceptions.rst
  23. 62 6
      docs/api_docs/cloud/resources.rst
  24. 35 10
      docs/api_docs/cloud/services.rst
  25. 3 3
      docs/concepts.rst
  26. 382 262
      docs/extras/_images/object_relationships_detailed.svg
  27. 52 23
      docs/getting_started.rst
  28. 5 5
      docs/topics/block_storage.rst
  29. 39 22
      docs/topics/launch.rst
  30. 86 25
      docs/topics/networking.rst
  31. 3 3
      docs/topics/object_lifecycles.rst
  32. 70 0
      docs/topics/object_storage.rst
  33. 2 0
      docs/topics/overview.rst
  34. 2 7
      docs/topics/provider_development.rst
  35. 22 0
      docs/topics/troubleshooting.rst
  36. 61 49
      setup.py
  37. 34 25
      test/helpers/__init__.py
  38. 276 0
      test/helpers/standard_interface_tests.py
  39. 62 183
      test/test_block_store_service.py
  40. 1 1
      test/test_cloud_factory.py
  41. 155 204
      test/test_compute_service.py
  42. 22 90
      test/test_image_service.py
  43. 0 110
      test/test_instance_types_service.py
  44. 10 9
      test/test_interface.py
  45. 120 189
      test/test_network_service.py
  46. 3 5
      test/test_object_life_cycle.py
  47. 85 96
      test/test_object_store_service.py
  48. 8 30
      test/test_region_service.py
  49. 133 219
      test/test_security_service.py
  50. 76 0
      test/test_vm_types_service.py

+ 4 - 46
.codeclimate.yml

@@ -1,52 +1,7 @@
 ---
 engines:
   duplication:
-    enabled: true
-    exclude_fingerprints:
-    - 3f76576c813e5e592f6ea1bc65ef9291
-    - f9ae8e1021c766dc0c2f353576fbcff7
-    - 49fc4b591037aff996ec0c998e402c5f
-    - 928d8aa915cd115ffe71cd78347390db
-    - 93bd0fc00f922563410152af13ef94cb
-    - 5a84f0e7b122136393d7895d4186568a
-    - 82e3773e63540ee70c4e5d2223b22ba5
-    - e334c778bcb258f7ea5ee7b4842f8439
-    - 4db6cfe08a809f77603390e57959bca4
-    - 12359446d14e41bd0b8056bc0ea3f500
-    - 7e1d5dbed1c3450da4b715a15a1712aa
-    - 6e2064d77fe1bad0f7ba4b4436fb6f58
-    - 8b984e10ad6b1c856eaf5b1c5f0cc771
-    - adff4c0fe5487d389ebad12f7fbf735b
-    - 395e33c8f74b14d7ed50826dc6f7e9ea
-    - c43753e3d76728eb587cc90d367e78e2
-    - 4a8cb608af3d98686a8b52677abe98f1
-    - 1d0b2558b44cd3afcaf0842eb01ac126
-    - 5b09dfb9597a0d5c5dcdbd7354575868
-    - 57c95f150892fb14ada5fdd65aace243
-    - 71ec0d54f2735b6ddf6fb7fc4246d7e8
-    - 3171604c43bcdcff6c7c1808ebe19a7c
-    - 05e9998892578171b0ccc5eb52fad64c
-    - a3476df9f580f36515bb6df4cf6d761c
-    - 4338aeca204b4be34e12a248184cf68c
-    - 00018ee13f3965f34e1266c25d9abf46
-    - e05fd70fb1e31f2eac66eb72fcb34e65
-    - ba8c9e2a035e48a9d8ba63a1dec05938
-    - 60e2bc835f7d865555e91012552d9651
-    - 636058deef4b937602ae181298d53407
-    - 81359193076ff9bc7c3c43c5b9f08dc3
-    - c69f9fc31902518cd9401afbaf6e8054
-    - b96edfe9656ced6f89d40efff1ac517f
-    - 267902c446edd49bcb0b0b91989bf513
-    - b0af10855cfdd282c4475a4f90f9f1e9
-    - afbc94256ebb699c261fcd849314004b
-    - 2cda35091e395ff1e3d4f2a2a22f6351
-    - 4bd9fc7270af515e4b6a8887ad97916f
-    - 39ecd11c13fc7eaead1ba87449b43e00
-    - 315c7e088d37fe4ce0e06836bf5ac0fc
-    - 34f0d22f0a660fa3d387db57c93ae142
-    - cbea9efa808e14c1dfa325d40cef496d
-    - f49f7714146a22266988d48fe07b7768
-    - 77f9e287b9d3fec73d1f36d81df46942
+    enabled: false
     config:
       languages:
       - ruby
@@ -55,6 +10,8 @@ engines:
       - php
   fixme:
     enabled: true
+  pep8:
+    enabled: true
   radon:
     enabled: true
     config: 
@@ -70,3 +27,4 @@ ratings:
   - "**.rb"
 exclude_paths:
 - test/**/*
+- docs/**/*

+ 65 - 17
.travis.yml

@@ -1,28 +1,76 @@
+dist: trusty
 language: python
-python: 3.6
 os:
   - linux
 #  - osx
-env:
-  - TOX_ENV=py27-aws
-  - TOX_ENV=py27-openstack
-  - TOX_ENV=py27-gce
-  - TOX_ENV=py36-aws
-  - TOX_ENV=py36-openstack
-  - TOX_ENV=py36-gce
-  - TOX_ENV=pypy-aws
-  - TOX_ENV=pypy-openstack
-  - TOX_ENV=pypy-gce
 matrix:
   fast_finish: true
   allow_failures:
     - os: osx
+  include:
+    - python: 2.7
+      env: TOX_ENV=py27-aws
+    - python: 2.7
+      env: TOX_ENV=py27-openstack
+    - python: 3.6
+      env: TOX_ENV=py36-aws
+    - 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-openstack
+before_install:
+    - |
+      case "$TRAVIS_EVENT_TYPE" in
+        push|pull_request)
+           # Check whether we need to run a test for this provider
+           DOCS_REGEX='(\.rst$)|(^(docs))/'
+           FILES_IN_CHANGESET="`git diff --name-only $TRAVIS_COMMIT_RANGE`"
+           echo "$FILES_IN_CHANGESET" | grep -qvE "$DOCS_REGEX" || {
+              echo "Only docs were updated. Stopping build process."
+              exit
+           }
+           echo "$FILES_IN_CHANGESET" | grep -qvE "$DOCS_REGEX|(^(cloudbridge/cloud/providers))" || {
+              echo "Only docs and providers were updated. Checking whether this provider was changed."
+              # Extract env and provider from $TOXENV into $PYENV and $PROVIDER respectively
+              IFS=- read PYENV PROVIDER <<< "$TOX_ENV"
+              echo "$FILES_IN_CHANGESET" | grep -qE "^(cloudbridge/cloud/providers/$PROVIDER)" && {
+                 echo "This provider was affected by this changeset. Running tests."
+              } || {
+                 echo "This provider was not affected by this changeset. Stopping build process."
+                 exit
+              }
+           }
+           ;;
+        *)
+           echo "Build triggered through API or CRON job. Running regardless of changes"
+           ;;
+      esac
 install:
-  - pip install tox
-  - pip install coveralls
-  - pip install codecov
+    - pip install -U setuptools
+    - pip install tox
+    - pip install coveralls
+    - pip install codecov
 script:
-  - tox -e $TOX_ENV
+    - tox -e $TOX_ENV
 after_success:
-  - coveralls
-  - codecov
+    - |
+      case "$TRAVIS_EVENT_TYPE" in
+        push|pull_request)
+           # Don't run coverage if tests or cloudbridge interface was not affected
+           DOCS_REGEX='(\.rst$)|(^(docs))/'
+           FILES_IN_CHANGESET="`git diff --name-only $TRAVIS_COMMIT_RANGE`"
+           echo "$FILES_IN_CHANGESET" | grep -qvE "$DOCS_REGEX|(^(cloudbridge/cloud/providers))" && {
+              coveralls &
+              codecov &
+              wait
+           } || {
+              echo "Only docs and providers were updated. Not running coverage."
+           }
+           ;;
+        *)
+           echo "Build triggered through API or CRON job. Running regardless of changes"
+           ;;
+      esac
+

+ 5 - 0
CHANGELOG.rst

@@ -1,3 +1,8 @@
+0.3.3 - Aug 7, 2017. (sha 348e1e88935f61f53a83ed8d6a0e012a46621e25)
+-------
+
+* Remove explicit versioning of requests and Babel
+
 0.3.2 - June 10, 2017. (sha f07f3cbd758a0872b847b5537d9073c90f87c24d)
 -------
 

+ 47 - 4
README.rst

@@ -10,10 +10,6 @@ conditional code for each cloud.
    :target: https://coveralls.io/github/gvlproject/cloudbridge?branch=master
    :alt: Code Coverage
 
-.. image:: https://travis-ci.org/gvlproject/cloudbridge.svg?branch=master
-   :target: https://travis-ci.org/gvlproject/cloudbridge
-   :alt: Travis Build Status
-
 .. image:: https://codeclimate.com/github/gvlproject/cloudbridge/badges/gpa.svg
    :target: https://codeclimate.com/github/gvlproject/cloudbridge
    :alt: Code Climate
@@ -26,6 +22,53 @@ conditional code for each cloud.
    :target: http://cloudbridge.readthedocs.org/en/latest/?badge=latest
    :alt: Documentation Status
 
+.. image:: https://badge.waffle.io/gvlproject/cloudbridge.png?label=in%20progress&title=In%20Progress 
+   :target: https://waffle.io/gvlproject/cloudbridge?utm_source=badge
+   :alt: 'Waffle.io - Issues in progress'
+
+.. |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
+              :target: https://travis-ci.org/gvlproject/cloudbridge
+.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/5
+              :target: https://travis-ci.org/gvlproject/cloudbridge
+
+.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/2
+             :target: https://travis-ci.org/gvlproject/cloudbridge
+.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/4
+             :target: https://travis-ci.org/gvlproject/cloudbridge
+.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/6
+             :target: https://travis-ci.org/gvlproject/cloudbridge
+
+.. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/azure_dev/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
+                :target: https://travis-ci.org/gvlproject/cloudbridge/branches
+.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/azure_dev/8
+                :target: https://travis-ci.org/gvlproject/cloudbridge/branches
+
+.. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/gce/3
+              :target: https://travis-ci.org/gvlproject/cloudbridge/branches
+.. |gce-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/gce/6
+              :target: https://travis-ci.org/gvlproject/cloudbridge/branches
+.. |gce-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/gce/9
+              :target: https://travis-ci.org/gvlproject/cloudbridge/branches
+
+
+Build Status
+~~~~~~~~~~~~
+
++--------------------------+--------------+--------------+--------------+
+| **Provider/Environment** | py27         | py36         | pypy         |
++--------------------------+--------------+--------------+--------------+
+| **aws**                  | |aws-py27|   | |aws-py36|   | |aws-pypy|   |
++--------------------------+--------------+--------------+--------------+
+| **openstack**            | |os-py27|    | |os-py36|    | |os-pypy|    |
++--------------------------+--------------+--------------+--------------+
+| **azure (alpha)**        | |azure-py27| | |azure-py36| | |azure-py36| |
++--------------------------+--------------+--------------+--------------+
+| **gce (alpha)**          | |gce-py27|   | |gce-py36|   | |gce-pypy|   |
++--------------------------+--------------+--------------+--------------+
 
 Installation
 ~~~~~~~~~~~~

+ 1 - 1
cloudbridge/__init__.py

@@ -2,7 +2,7 @@
 import logging
 
 # Current version of the library
-__version__ = '0.3.2'
+__version__ = '0.3.3'
 
 
 def get_version():

+ 19 - 3
cloudbridge/cloud/base/provider.py

@@ -1,16 +1,18 @@
 """Base implementation of a provider interface."""
 import functools
+import logging
 import os
 from os.path import expanduser
 try:
-    from configparser import SafeConfigParser
+    from configparser import ConfigParser
 except ImportError:  # Python 2
-    from ConfigParser import SafeConfigParser
+    from ConfigParser import SafeConfigParser as ConfigParser
 
 from cloudbridge.cloud.interfaces import CloudProvider
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 from cloudbridge.cloud.interfaces.resources import Configuration
 
+log = logging.getLogger(__name__)
 
 DEFAULT_RESULT_LIMIT = 50
 DEFAULT_WAIT_TIMEOUT = 600
@@ -37,6 +39,8 @@ class BaseConfiguration(Configuration):
         :rtype: ``int``
         :return: The maximum number of results to return
         """
+        log.debug("Maximum number of results for list methods %s",
+                  DEFAULT_RESULT_LIMIT)
         return self.get('default_result_limit', DEFAULT_RESULT_LIMIT)
 
     @property
@@ -44,6 +48,8 @@ class BaseConfiguration(Configuration):
         """
         Gets the default wait timeout for LifeCycleObjects.
         """
+        log.debug("Default wait timeout for LifeCycleObjects %s",
+                  DEFAULT_WAIT_TIMEOUT)
         return self.get('default_wait_timeout', DEFAULT_WAIT_TIMEOUT)
 
     @property
@@ -51,6 +57,8 @@ class BaseConfiguration(Configuration):
         """
         Gets the default wait interval for LifeCycleObjects.
         """
+        log.debug("Default wait interfal for LifeCycleObjects %s",
+                  DEFAULT_WAIT_INTERVAL)
         return self.get('default_wait_interval', DEFAULT_WAIT_INTERVAL)
 
     @property
@@ -73,7 +81,7 @@ class BaseCloudProvider(CloudProvider):
 
     def __init__(self, config):
         self._config = BaseConfiguration(config)
-        self._config_parser = SafeConfigParser()
+        self._config_parser = ConfigParser()
         self._config_parser.read(CloudBridgeConfigLocations)
 
     @property
@@ -90,10 +98,12 @@ class BaseCloudProvider(CloudProvider):
         check whether cloud credentials work. Providers should override with
         more efficient implementations.
         """
+        log.debug("Checking if cloud credential works...")
         try:
             self.security.key_pairs.list()
             return True
         except Exception as e:
+            log.exception("ProviderConnectionException occurred")
             raise ProviderConnectionException(
                 "Authentication with cloud provider failed: %s" % (e,))
 
@@ -111,13 +121,18 @@ class BaseCloudProvider(CloudProvider):
         :rtype: bool
         :return: ``True`` if the service type is supported.
         """
+        log.info("Checking if provider supports %s", service_type)
         try:
             if self._deepgetattr(self, service_type):
+                log.info("This provider supports %s",
+                         service_type)
                 return True
         except AttributeError:
             pass  # Undefined service type
         except NotImplementedError:
             pass  # service not implemented
+        log.info("This provider doesn't support %s",
+                 service_type)
         return False
 
     def _get_config_value(self, key, default_value):
@@ -133,6 +148,7 @@ class BaseCloudProvider(CloudProvider):
 
         :return: a configuration value for the supplied ``key``
         """
+        log.info("Getting config key: %s with default: %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):

+ 388 - 59
cloudbridge/cloud/base/resources.py

@@ -3,23 +3,26 @@ Base implementation for data objects exposed through a provider or service
 """
 import inspect
 import itertools
-import json
 import logging
 import os
+import re
 import shutil
 import time
 
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import Bucket
+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 GatewayState
 from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import InstanceState
-from cloudbridge.cloud.interfaces.resources import InstanceType
+from cloudbridge.cloud.interfaces.resources import InternetGateway
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import LaunchConfig
 from cloudbridge.cloud.interfaces.resources import MachineImage
@@ -32,11 +35,14 @@ from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import Region
 from cloudbridge.cloud.interfaces.resources import ResultList
 from cloudbridge.cloud.interfaces.resources import Router
-from cloudbridge.cloud.interfaces.resources import SecurityGroup
-from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import Subnet
+from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.interfaces.resources import VMFirewallRule
+from cloudbridge.cloud.interfaces.resources import VMFirewallRuleContainer
+from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 
@@ -46,10 +52,158 @@ log = logging.getLogger(__name__)
 
 
 class BaseCloudResource(CloudResource):
+    """
+    Base implementation of a CloudBridge Resource.
+    """
+
+    # Regular expression for valid cloudbridge resource names.
+    # They, must match the same criteria as GCE labels.
+    # as discussed here: https://github.com/gvlproject/cloudbridge/issues/55
+    #
+    # NOTE: The following regex is based on GCEs internal validation logic,
+    # and is significantly complex to allow for international characters.
+    CB_NAME_PATTERN = re.compile(six.u(
+        r"^[\u0061-\u007A\u00B5\u00DF-\u00F6\u00F8-\u00FF\u0101\u0103\u0105"
+        "\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B"
+        "\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131"
+        "\u0133\u0135\u0137\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146"
+        "\u0148\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B"
+        "\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171"
+        "\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C"
+        "\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA"
+        "\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9\u01BA\u01BD-\u01BF\u01C6\u01C9"
+        "\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC\u01DD\u01DF"
+        "\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF\u01F0\u01F3\u01F5"
+        "\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D"
+        "\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223"
+        "\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F"
+        "\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371"
+        "\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0\u03D1\u03D5-"
+        "\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB"
+        "\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB\u03FC\u0430-\u045F\u0461\u0463"
+        "\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479"
+        "\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497"
+        "\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD"
+        "\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4"
+        "\u04C6\u04C8\u04CA\u04CC\u04CE\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9"
+        "\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF"
+        "\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505"
+        "\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B"
+        "\u051D\u051F\u0521\u0523\u0525\u0527\u0561-\u0587\u1D00-\u1D2B"
+        "\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D"
+        "\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23"
+        "\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39"
+        "\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F"
+        "\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65"
+        "\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B"
+        "\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91"
+        "\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD"
+        "\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3"
+        "\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9"
+        "\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF"
+        "\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15"
+        "\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67"
+        "\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4"
+        "\u1FB6\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6\u1FC7\u1FD0-\u1FD3\u1FD6\u1FD7"
+        "\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6\u1FF7\u210A\u210E\u210F\u2113\u212F"
+        "\u2134\u2139\u213C\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61"
+        "\u2C65\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73\u2C74\u2C76-\u2C7B\u2C81"
+        "\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97"
+        "\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD"
+        "\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3"
+        "\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9"
+        "\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25"
+        "\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651"
+        "\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667"
+        "\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F"
+        "\uA691\uA693\uA695\uA697\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-"
+        "\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745"
+        "\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B"
+        "\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-"
+        "\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791"
+        "\uA793\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7FA\uFB00-\uFB06\uFB13-\uFB17"
+        "\uFF41-\uFF5A\u00AA\u00BA\u01BB\u01C0-\u01C3\u0294\u05D0-\u05EA"
+        "\u05F0-\u05F2\u0620-\u063F\u0641-\u064A\u066E\u066F\u0671-\u06D3"
+        "\u06D5\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-"
+        "\u07A5\u07B1\u07CA-\u07EA\u0800-\u0815\u0840-\u0858\u08A0\u08A2-"
+        "\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0972-\u0977\u0979-"
+        "\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2"
+        "\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1"
+        "\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33"
+        "\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-"
+        "\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-"
+        "\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-"
+        "\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D"
+        "\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95"
+        "\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-"
+        "\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33"
+        "\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-"
+        "\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0"
+        "\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D"
+        "\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-"
+        "\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E45"
+        "\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-"
+        "\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2"
+        "\u0EB3\u0EBD\u0EC0-\u0EC4\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-"
+        "\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D"
+        "\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10D0-\u10FA"
+        "\u10FD-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-"
+        "\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0"
+        "\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A"
+        "\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A"
+        "\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751"
+        "\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17DC\u1820-\u1842\u1844-"
+        "\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D"
+        "\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54"
+        "\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5"
+        "\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C77\u1CE9-\u1CEC\u1CEE-\u1CF1"
+        "\u1CF5\u1CF6\u2135-\u2138\u2D30-\u2D67\u2D80-\u2D96\u2DA0-\u2DA6"
+        "\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE"
+        "\u2DD0-\u2DD6\u2DD8-\u2DDE\u3006\u303C\u3041-\u3096\u309F\u30A1-"
+        "\u30FA\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF"
+        "\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA014\uA016-\uA48C\uA4D0-\uA4F7"
+        "\uA500-\uA60B\uA610-\uA61F\uA62A\uA62B\uA66E\uA6A0-\uA6E5\uA7FB-"
+        "\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-"
+        "\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C"
+        "\uA984-\uA9B2\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA6F"
+        "\uAA71-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD"
+        "\uAAC0\uAAC2\uAADB\uAADC\uAAE0-\uAAEA\uAAF2\uAB01-\uAB06\uAB09-"
+        "\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-"
+        "\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB1D"
+        "\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43"
+        "\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-"
+        "\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF66-\uFF6F\uFF71-\uFF9D\uFFA0-"
+        "\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\u0030-"
+        "\u0039\u00B2\u00B3\u00B9\u00BC-\u00BE\u0660-\u0669\u06F0-\u06F9"
+        "\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F"
+        "\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F"
+        "\u0C78-\u0C7E\u0CE6-\u0CEF\u0D66-\u0D75\u0E50-\u0E59\u0ED0-\u0ED9"
+        "\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0"
+        "\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA"
+        "\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49"
+        "\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-"
+        "\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-"
+        "\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-"
+        "\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-"
+        "\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uAA50-\uAA59\uABF0-"
+        "\uABF9\uFF10-\uFF19_-]{0,63}$"), re.UNICODE)
 
     def __init__(self, provider):
         self.__provider = provider
 
+    @staticmethod
+    def is_valid_resource_name(name):
+        return True if BaseCloudResource.CB_NAME_PATTERN.match(name) else False
+
+    @staticmethod
+    def assert_valid_resource_name(name):
+        if not BaseCloudResource.is_valid_resource_name(name):
+            log.debug("InvalidNameException raised on %s", name, exc_info=True)
+            raise InvalidNameException(
+                u"Invalid name: %s. Name must be at most 63 characters "
+                "long and consist of lowercase letters, numbers, "
+                "underscores, dashes or international characters" % name)
+
     @property
     def _provider(self):
         return self.__provider
@@ -58,7 +212,7 @@ class BaseCloudResource(CloudResource):
         # Get all attributes but filter methods and private/magic ones
         attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
         js = {k: v for(k, v) in attr if not k.startswith('_')}
-        return json.dumps(js, sort_keys=True)
+        return js
 
 
 class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
@@ -198,29 +352,26 @@ class BasePageableObjectMixin(PageableObjectMixin):
     """
 
     def __iter__(self):
-        marker = None
-
-        result_list = self.list(marker=marker)
+        result_list = self.list()
         if result_list.supports_server_paging:
             for result in result_list:
                 yield result
             while result_list.is_truncated:
-                result_list = self.list(marker=marker)
+                result_list = self.list(marker=result_list.marker)
                 for result in result_list:
                     yield result
-                marker = result_list.marker
         else:
             for result in result_list.data:
                 yield result
 
 
-class BaseInstanceType(InstanceType, BaseCloudResource):
+class BaseVMType(BaseCloudResource, VMType):
 
     def __init__(self, provider):
-        super(BaseInstanceType, self).__init__(provider)
+        super(BaseVMType, self).__init__(provider)
 
     def __eq__(self, other):
-        return (isinstance(other, InstanceType) and
+        return (isinstance(other, VMType) and
                 # pylint:disable=protected-access
                 self._provider == other._provider and
                 self.id == other.id)
@@ -247,7 +398,7 @@ class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
                 # check from most to least likely mutables
                 self.state == other.state and
                 self.name == other.name and
-                self.security_groups == other.security_groups and
+                self.vm_firewalls == other.vm_firewalls and
                 self.public_ips == other.public_ips and
                 self.private_ips == other.private_ips and
                 self.image_id == other.image_id)
@@ -255,7 +406,7 @@ class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
             [InstanceState.RUNNING],
-            terminal_states=[InstanceState.TERMINATED, InstanceState.ERROR],
+            terminal_states=[InstanceState.DELETED, InstanceState.ERROR],
             timeout=timeout,
             interval=interval)
 
@@ -292,6 +443,8 @@ class BaseLaunchConfig(LaunchConfig):
         block_device = self._validate_volume_device(
             source=source, is_root=is_root, size=size,
             delete_on_terminate=delete_on_terminate)
+        log.debug("Appending %s to the block_devices list",
+                  block_device)
         self.block_devices.append(block_device)
 
     def _validate_volume_device(self, source=None, is_root=None,
@@ -301,21 +454,29 @@ class BaseLaunchConfig(LaunchConfig):
         InvalidConfigurationException if the configuration is incorrect.
         """
         if source is None and not size:
+            log.exception("Raised InvalidConfigurationException, no"
+                          " size argument specified.")
             raise InvalidConfigurationException(
                 "A size must be specified for a blank new volume")
 
         if source and \
                 not isinstance(source, (Snapshot, Volume, MachineImage)):
+            log.exception("InvalidConfigurationException raised, "
+                          "source argument not specified correctly.")
             raise InvalidConfigurationException(
                 "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.")
                 raise InvalidConfigurationException(
                     "The size must be None or a number 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)
                     raise InvalidConfigurationException(
                         "An existing block device: {0} has already been"
                         " marked as root. There can only be one root device.")
@@ -425,7 +586,7 @@ class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
                                             self.name, self.id)
 
 
-class BaseKeyPair(KeyPair, BaseCloudResource):
+class BaseKeyPair(BaseCloudResource, KeyPair):
 
     def __init__(self, provider, key_pair):
         super(BaseKeyPair, self).__init__(provider)
@@ -466,20 +627,19 @@ class BaseKeyPair(KeyPair, BaseCloudResource):
         return "<CBKeyPair: {0} ({1})>".format(self.name, self.id)
 
 
-class BaseSecurityGroup(SecurityGroup, BaseCloudResource):
+class BaseVMFirewall(BaseCloudResource, VMFirewall):
 
-    def __init__(self, provider, security_group):
-        super(BaseSecurityGroup, self).__init__(provider)
-        self._security_group = security_group
+    def __init__(self, provider, vm_firewall):
+        super(BaseVMFirewall, self).__init__(provider)
+        self._vm_firewall = vm_firewall
 
     def __eq__(self, other):
         """
-        Check if all the defined rules match across both security groups.
+        Check if all the defined rules match across both VM firewalls.
         """
-        return (isinstance(other, SecurityGroup) and
+        return (isinstance(other, VMFirewall) and
                 # pylint:disable=protected-access
                 self._provider == other._provider and
-                len(self.rules) == len(other.rules) and  # Shortcut
                 set(self.rules) == set(other.rules))
 
     def __ne__(self, other):
@@ -488,55 +648,119 @@ class BaseSecurityGroup(SecurityGroup, BaseCloudResource):
     @property
     def id(self):
         """
-        Get the ID of this security group.
+        Get the ID of this VM firewall.
 
         :rtype: str
-        :return: Security group ID
+        :return: VM firewall ID
         """
-        return self._security_group.id
+        return self._vm_firewall.id
 
     @property
     def name(self):
         """
-        Return the name of this security group.
+        Return the name of this VM firewall.
         """
-        return self._security_group.name
+        return self._vm_firewall.name
 
     @property
     def description(self):
         """
-        Return the description of this security group.
+        Return the description of this VM firewall.
         """
-        return self._security_group.description
+        return self._vm_firewall.description
 
     def delete(self):
         """
-        Delete this security group.
+        Delete this VM firewall.
         """
-        return self._security_group.delete()
+        return self._vm_firewall.delete()
 
     def __repr__(self):
         return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
                                             self.id, self.name)
 
 
-class BaseSecurityGroupRule(SecurityGroupRule, BaseCloudResource):
+class BaseVMFirewallRuleContainer(BasePageableObjectMixin,
+                                  VMFirewallRuleContainer):
+
+    def __init__(self, provider, firewall):
+        self.__provider = provider
+        self.firewall = firewall
+
+    @property
+    def _provider(self):
+        return self.__provider
 
-    def __init__(self, provider, rule, parent):
-        super(BaseSecurityGroupRule, self).__init__(provider)
+    def get(self, rule_id):
+        matches = [rule for rule in self if rule.id == rule_id]
+        if matches:
+            return matches[0]
+        else:
+            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)
+
+    def delete(self, rule_id):
+        rule = self.get(rule_id)
+        if rule:
+            rule.delete()
+
+
+class BaseVMFirewallRule(BaseCloudResource, VMFirewallRule):
+
+    def __init__(self, parent_fw, rule):
+        # pylint:disable=protected-access
+        super(BaseVMFirewallRule, self).__init__(
+            parent_fw._provider)
+        self.firewall = parent_fw
         self._rule = rule
-        self._parent = parent
+
+        # Cache name
+        self._name = "{0}-{1}-{2}-{3}-{4}-{5}".format(
+            self.direction, self.protocol, self.from_port, self.to_port,
+            self.cidr, self.src_dest_fw_id).lower()
+
+    @property
+    def name(self):
+        return self._name
 
     def __repr__(self):
-        return ("<CBSecurityGroupRule: IP: {0}; from: {1}; to: {2}; grp: {3}>"
-                .format(self.ip_protocol, self.from_port, self.to_port,
-                        self.group))
+        return ("<{0}: id: {1}; direction: {2}; protocol: {3};  from: {4};"
+                " to: {5}; cidr: {6}, src_dest_fw: {7}>"
+                .format(self.__class__.__name__, self.id, self.direction,
+                        self.protocol, self.from_port, self.to_port, self.cidr,
+                        self.src_dest_fw_id))
 
     def __eq__(self, other):
-        return self.ip_protocol == other.ip_protocol and \
-            self.from_port == other.from_port and \
-            self.to_port == other.to_port and \
-            self.cidr_ip == other.cidr_ip
+        return (isinstance(other, VMFirewallRule) and
+                self.direction == other.direction and
+                self.protocol == other.protocol and
+                self.from_port == other.from_port and
+                self.to_port == other.to_port and
+                self.cidr == other.cidr and
+                self.src_dest_fw_id == other.src_dest_fw_id)
 
     def __ne__(self, other):
         return not self.__eq__(other)
@@ -545,19 +769,26 @@ class BaseSecurityGroupRule(SecurityGroupRule, BaseCloudResource):
         """
         Return a hash-based interpretation of all of the object's field values.
 
-        This is requried for operations on hashed collections including
+        This is requeried for operations on hashed collections including
         ``set``, ``frozenset``, and ``dict``.
         """
-        return hash("{0}{1}{2}{3}{4}".format(self.ip_protocol, self.from_port,
-                                             self.to_port, self.cidr_ip,
-                                             self.group))
+        return hash("{0}{1}{2}{3}{4}{5}".format(
+            self.direction, self.protocol, self.from_port, self.to_port,
+            self.cidr, self.src_dest_fw_id))
+
+    def to_json(self):
+        attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
+        js = {k: v for (k, v) in attr if not k.startswith('_')}
+        js['src_dest_fw'] = self.src_dest_fw_id
+        js['firewall'] = self.firewall.id
+        return js
 
     @property
     def parent(self):
         return self._parent
 
 
-class BasePlacementZone(PlacementZone, BaseCloudResource):
+class BasePlacementZone(BaseCloudResource, PlacementZone):
 
     def __init__(self, provider):
         super(BasePlacementZone, self).__init__(provider)
@@ -573,7 +804,7 @@ class BasePlacementZone(PlacementZone, BaseCloudResource):
                 self.id == other.id)
 
 
-class BaseRegion(Region, BaseCloudResource):
+class BaseRegion(BaseCloudResource, Region):
 
     def __init__(self, provider):
         super(BaseRegion, self).__init__(provider)
@@ -592,14 +823,35 @@ class BaseRegion(Region, BaseCloudResource):
         attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
         js = {k: v for(k, v) in attr if not k.startswith('_')}
         js['zones'] = [z.name for z in self.zones]
-        return json.dumps(js, sort_keys=True)
+        return js
+
 
+class BaseBucketObject(BaseCloudResource, BucketObject):
 
-class BaseBucketObject(BucketObject, BaseCloudResource):
+    # Regular expression for valid bucket keys.
+    # They, must match the following criteria: http://docs.aws.amazon.com/"
+    # AmazonS3/latest/dev/UsingMetadata.html#object-key-guidelines
+    #
+    # Note: The following regex is based on: https://stackoverflow.com/question
+    # s/537772/what-is-the-most-correct-regular-expression-for-a-unix-file-path
+    CB_NAME_PATTERN = re.compile(r"[^\0]+")
 
     def __init__(self, provider):
         super(BaseBucketObject, self).__init__(provider)
 
+    @staticmethod
+    def is_valid_resource_name(name):
+        return True if BaseBucketObject.CB_NAME_PATTERN.match(name) else False
+
+    @staticmethod
+    def assert_valid_resource_name(name):
+        if not BaseBucketObject.is_valid_resource_name(name):
+            log.debug("InvalidNameException raised on %s", name, exc_info=True)
+            raise InvalidNameException(
+                u"Invalid object name: %s. Name must match criteria defined "
+                "in: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMeta"
+                "data.html#object-key-guidelines" % name)
+
     def save_content(self, target_stream):
         """
         Download this object and write its
@@ -620,11 +872,32 @@ class BaseBucketObject(BucketObject, BaseCloudResource):
                                       self.name)
 
 
-class BaseBucket(BasePageableObjectMixin, Bucket, BaseCloudResource):
+class BaseBucket(BaseCloudResource, Bucket):
+
+    # Regular expression for valid bucket names.
+    # They, must match the following criteria: http://docs.aws.amazon.com/aws
+    # cloudtrail/latest/userguide/cloudtrail-s3-bucket-naming-requirements.html
+    #
+    # NOTE: The following regex is based on: https://stackoverflow.com/questio
+    # ns/2063213/regular-expression-for-validating-dns-label-host-name
+    CB_NAME_PATTERN = re.compile(r"^(?![0-9]+$)(?!-)[a-z0-9-]{3,63}(?<!-)$")
 
     def __init__(self, provider):
         super(BaseBucket, self).__init__(provider)
 
+    @staticmethod
+    def is_valid_resource_name(name):
+        return True if BaseBucket.CB_NAME_PATTERN.match(name) else False
+
+    @staticmethod
+    def assert_valid_resource_name(name):
+        if not BaseBucket.is_valid_resource_name(name):
+            log.debug("Invalid resource name %s", name, exc_info=True)
+            raise InvalidNameException(
+                u"Invalid bucket name: %s. Name must match criteria defined "
+                "in: http://docs.aws.amazon.com/awscloudtrail/latest/userguide"
+                "/cloudtrail-s3-bucket-naming-requirements.html" % name)
+
     def __eq__(self, other):
         return (isinstance(other, Bucket) and
                 # pylint:disable=protected-access
@@ -638,10 +911,21 @@ class BaseBucket(BasePageableObjectMixin, Bucket, BaseCloudResource):
                                       self.name)
 
 
-class BaseNetwork(BaseCloudResource, Network, BaseObjectLifeCycleMixin):
+class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
+
+    def __init__(self, provider, bucket):
+        self.__provider = provider
+        self.bucket = bucket
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+
+class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
     CB_DEFAULT_NETWORK_NAME = os.environ.get('CB_DEFAULT_NETWORK_NAME',
-                                             'CloudBridgeNet')
+                                             'cloudbridge-net')
 
     def __init__(self, provider):
         super(BaseNetwork, self).__init__(provider)
@@ -657,6 +941,10 @@ class BaseNetwork(BaseCloudResource, Network, BaseObjectLifeCycleMixin):
             timeout=timeout,
             interval=interval)
 
+    def create_subnet(self, name, cidr_block, zone=None):
+        return self._provider.networking.subnets.create(
+            name=name, network=self, cidr_block=cidr_block, zone=zone)
+
     def __eq__(self, other):
         return (isinstance(other, Network) and
                 # pylint:disable=protected-access
@@ -664,10 +952,10 @@ class BaseNetwork(BaseCloudResource, Network, BaseObjectLifeCycleMixin):
                 self.id == other.id)
 
 
-class BaseSubnet(Subnet, BaseCloudResource):
+class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
     CB_DEFAULT_SUBNET_NAME = os.environ.get('CB_DEFAULT_SUBNET_NAME',
-                                            'CloudBridgeSubnet')
+                                            'cloudbridge-subnet')
 
     def __init__(self, provider):
         super(BaseSubnet, self).__init__(provider)
@@ -682,12 +970,26 @@ class BaseSubnet(Subnet, BaseCloudResource):
                 self._provider == other._provider and
                 self.id == other.id)
 
+    def wait_till_ready(self, timeout=None, interval=None):
+        self.wait_for(
+            [SubnetState.AVAILABLE],
+            terminal_states=[SubnetState.ERROR],
+            timeout=timeout,
+            interval=interval)
+
 
-class BaseFloatingIP(FloatingIP, BaseCloudResource):
+class BaseFloatingIP(BaseCloudResource, FloatingIP):
 
     def __init__(self, provider):
         super(BaseFloatingIP, self).__init__(provider)
 
+    @property
+    def name(self):
+        """
+        VM firewall rules don't support names, so pass
+        """
+        return self.public_ip
+
     def __repr__(self):
         return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
                                             self.id, self.public_ip)
@@ -699,10 +1001,10 @@ class BaseFloatingIP(FloatingIP, BaseCloudResource):
                 self.id == other.id)
 
 
-class BaseRouter(Router, BaseCloudResource):
+class BaseRouter(BaseCloudResource, Router):
 
     CB_DEFAULT_ROUTER_NAME = os.environ.get('CB_DEFAULT_ROUTER_NAME',
-                                            'CloudBridgeRouter')
+                                            'cloudbridge-router')
 
     def __init__(self, provider):
         super(BaseRouter, self).__init__(provider)
@@ -716,3 +1018,30 @@ class BaseRouter(Router, BaseCloudResource):
                 # pylint:disable=protected-access
                 self._provider == other._provider and
                 self.id == other.id)
+
+
+class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
+                          InternetGateway):
+
+    CB_DEFAULT_INET_GATEWAY_NAME = os.environ.get(
+        'CB_DEFAULT_INET_GATEWAY_NAME', 'cloudbridge-inetgateway')
+
+    def __init__(self, provider):
+        super(BaseInternetGateway, self).__init__(provider)
+
+    def __repr__(self):
+        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,
+                                            self.name)
+
+    def __eq__(self, other):
+        return (isinstance(other, InternetGateway) and
+                # pylint:disable=protected-access
+                self._provider == other._provider and
+                self.id == other.id)
+
+    def wait_till_ready(self, timeout=None, interval=None):
+        self.wait_for(
+            [GatewayState.AVAILABLE],
+            terminal_states=[GatewayState.ERROR, GatewayState.UNKNOWN],
+            timeout=timeout,
+            interval=interval)

+ 109 - 20
cloudbridge/cloud/base/services.py

@@ -1,24 +1,34 @@
 """
 Base implementation for services available through a provider
 """
-from cloudbridge.cloud.interfaces.services import BlockStoreService
+import logging
+
+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 InstanceTypesService
 from cloudbridge.cloud.interfaces.services import KeyPairService
 from cloudbridge.cloud.interfaces.services import NetworkService
-from cloudbridge.cloud.interfaces.services import ObjectStoreService
+from cloudbridge.cloud.interfaces.services import NetworkingService
 from cloudbridge.cloud.interfaces.services import RegionService
-from cloudbridge.cloud.interfaces.services import SecurityGroupService
+from cloudbridge.cloud.interfaces.services import RouterService
 from cloudbridge.cloud.interfaces.services import SecurityService
 from cloudbridge.cloud.interfaces.services import SnapshotService
+from cloudbridge.cloud.interfaces.services import StorageService
 from cloudbridge.cloud.interfaces.services import SubnetService
+from cloudbridge.cloud.interfaces.services import VMFirewallService
+from cloudbridge.cloud.interfaces.services import VMTypeService
 from cloudbridge.cloud.interfaces.services import VolumeService
 
 from .resources import BasePageableObjectMixin
 
+log = logging.getLogger(__name__)
+
 
 class BaseCloudService(CloudService):
 
@@ -50,10 +60,10 @@ class BaseSnapshotService(
         super(BaseSnapshotService, self).__init__(provider)
 
 
-class BaseBlockStoreService(BlockStoreService, BaseCloudService):
+class BaseStorageService(StorageService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseBlockStoreService, self).__init__(provider)
+        super(BaseStorageService, self).__init__(provider)
 
 
 class BaseImageService(
@@ -63,11 +73,11 @@ class BaseImageService(
         super(BaseImageService, self).__init__(provider)
 
 
-class BaseObjectStoreService(
-        BasePageableObjectMixin, ObjectStoreService, BaseCloudService):
+class BaseBucketService(
+        BasePageableObjectMixin, BucketService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseObjectStoreService, self).__init__(provider)
+        super(BaseBucketService, self).__init__(provider)
 
 
 class BaseSecurityService(SecurityService, BaseCloudService):
@@ -94,34 +104,38 @@ class BaseKeyPairService(
                   that the key may not have been deleted by this method but
                   instead has not existed in the first place.
         """
+        log.info("Deleting the existing key pair %s", key_pair_id)
         kp = self.get(key_pair_id)
         if kp:
             kp.delete()
         return True
 
 
-class BaseSecurityGroupService(
-        BasePageableObjectMixin, SecurityGroupService, BaseCloudService):
+class BaseVMFirewallService(
+        BasePageableObjectMixin, VMFirewallService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseSecurityGroupService, self).__init__(provider)
+        super(BaseVMFirewallService, self).__init__(provider)
 
 
-class BaseInstanceTypesService(
-        BasePageableObjectMixin, InstanceTypesService, BaseCloudService):
+class BaseVMTypeService(
+        BasePageableObjectMixin, VMTypeService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseInstanceTypesService, self).__init__(provider)
+        super(BaseVMTypeService, self).__init__(provider)
 
-    def get(self, instance_type_id):
-        itype = (t for t in self.list() if t.id == instance_type_id)
-        return next(itype, None)
+    def get(self, vm_type_id):
+        vm_type = (t for t in self if t.id == vm_type_id)
+        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.list() if itype.name == 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}")
 
@@ -139,6 +153,15 @@ 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]
+
+
+class BaseNetworkingService(NetworkingService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseNetworkingService, self).__init__(provider)
+
 
 class BaseNetworkService(
         BasePageableObjectMixin, NetworkService, BaseCloudService):
@@ -146,13 +169,18 @@ class BaseNetworkService(
     def __init__(self, provider):
         super(BaseNetworkService, self).__init__(provider)
 
+    @property
+    def subnets(self):
+        return [subnet for subnet in self.provider.subnets
+                if subnet.network_id == self.id]
+
     def delete(self, network_id):
         if network_id is None:
             return True
         network = self.get(network_id)
         if network:
+            log.info("Deleting network %s", network_id)
             network.delete()
-        return True
 
 
 class BaseSubnetService(
@@ -160,3 +188,64 @@ class BaseSubnetService(
 
     def __init__(self, provider):
         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()
+
+
+class BaseRouterService(
+        BasePageableObjectMixin, RouterService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseRouterService, self).__init__(provider)
+
+    def delete(self, router):
+        if isinstance(router, Router):
+            log.info("Router %s successful deleted.", router)
+            router.delete()
+        else:
+            log.info("Getting router %s", router)
+            router = self.get(router)
+            if router:
+                log.info("Router %s successful deleted.", router)
+                router.delete()
+
+
+class BaseGatewayService(
+        GatewayService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseGatewayService, self).__init__(provider)

+ 19 - 0
cloudbridge/cloud/factory.py

@@ -26,6 +26,7 @@ class CloudProviderFactory(object):
 
     def __init__(self):
         self.provider_list = defaultdict(dict)
+        log.debug("Providers List: %s", self.provider_list)
 
     def register_provider_class(self, cls):
         """
@@ -73,6 +74,7 @@ class CloudProviderFactory(object):
         Note that this methods does not guard against a failed import.
         """
         for _, modname, _ in pkgutil.iter_modules(providers.__path__):
+            log.debug("Importing provider: %s", modname)
             self._import_provider(modname)
 
     def _import_provider(self, module_name):
@@ -80,11 +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)
         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)
             self.register_provider_class(cls)
 
     def list_providers(self):
@@ -105,6 +109,7 @@ class CloudProviderFactory(object):
         """
         if not self.provider_list:
             self.discover_providers()
+        log.info("List of available providers: %s", self.provider_list)
         return self.provider_list
 
     def create_provider(self, name, config):
@@ -125,11 +130,17 @@ 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)
         provider_class = self.get_provider_class(name)
         if provider_class is None:
+            log.exception("A provider with the name %s could not "
+                          "be found", name)
             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)
         return provider_class(config)
 
     def get_provider_class(self, name, get_mock=False):
@@ -144,13 +155,18 @@ class CloudProviderFactory(object):
         :return: A class corresponding to the requested provider or ``None``
                  if the provider was not found.
         """
+        log.debug("Returning a class for the %s provider", name)
         impl = self.list_providers().get(name)
         if impl:
             if get_mock and impl.get("mock_class"):
+                log.debug("param get_mock set to True, returning "
+                          "a mock version of the provider %s", name)
                 return impl["mock_class"]
             else:
+                log.debug("Returning the real version of %s", name)
                 return impl["class"]
         else:
+            log.debug("Provider with the name: %s not found", name)
             return None
 
     def get_all_provider_classes(self, get_mock=False):
@@ -168,7 +184,10 @@ class CloudProviderFactory(object):
         all_providers = []
         for impl in self.list_providers().values():
             if get_mock and impl.get("mock_class"):
+                log.debug("param get_mock set to True, appending "
+                          "a mock version of the provider %s", impl)
                 all_providers.append(impl["mock_class"])
             else:
                 all_providers.append(impl["class"])
+        log.info("List of provider classes: %s", all_providers)
         return all_providers

+ 33 - 1
cloudbridge/cloud/interfaces/exceptions.py

@@ -28,11 +28,43 @@ class InvalidConfigurationException(CloudBridgeBaseException):
     pass
 
 
+class ProviderInternalException(CloudBridgeBaseException):
+    """
+    Marker interface for provider specific errors.
+    Thrown when CloudBridge encounters an error internal to a
+    provider.
+    """
+    pass
+
+
 class ProviderConnectionException(CloudBridgeBaseException):
     """
     Marker interface for connection errors to a cloud provider.
-    Thrown when cloudbridge is unable to connect with a provider,
+    Thrown when CloudBridge is unable to connect with a provider,
     for example, when credentials are incorrect, or connection
     settings are invalid.
     """
     pass
+
+
+class InvalidNameException(CloudBridgeBaseException):
+    """
+    Marker interface for any attempt to set an invalid name on
+    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)
+
+
+class InvalidValueException(CloudBridgeBaseException):
+    """
+    Marker interface for any attempt to set an invalid value on a CloudBridge
+    resource.An example would be setting an unrecognised value for the
+    direction of a firewall rule other than TrafficDirection.INBOUND or
+    TrafficDirection.OUTBOUND.
+    """
+    def __init__(self, param, value):
+        super(InvalidValueException, self).__init__(
+            "Param %s has been given an unrecognised value %s" %
+            (param, value))

+ 19 - 33
cloudbridge/cloud/interfaces/provider.py

@@ -89,9 +89,9 @@ class CloudProvider(object):
 
         .. code-block:: python
 
-            if provider.has_service(CloudServiceType.OBJECT_STORE):
+            if provider.has_service(CloudServiceType.BUCKET):
                print("Provider supports object store services")
-               provider.object_store.list()
+               provider.storage.buckets.list()
 
 
         :type service_type: :class:`.CloudServiceType`
@@ -123,7 +123,7 @@ class CloudProvider(object):
         .. code-block:: python
 
             regions = provider.compute.regions.list()
-            instance_types = provider.compute.instance_types.list()
+            vm_types = provider.compute.vm_types.list()
             instances = provider.compute.instances.list()
             images = provider.compute.images.list()
 
@@ -137,7 +137,7 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def network(self):
+    def networking(self):
         """
         Provide access to all network related services in this provider.
 
@@ -145,11 +145,12 @@ class CloudProvider(object):
 
         .. code-block:: python
 
-            networks = provider.network.list()
-            network = provider.network.create(name="DevNet")
+            networks = provider.networking.networks.list()
+            network = provider.networking.networks.create(
+                           name="DevNet", cidr_block='10.0.0.0/16')
 
-        :rtype: :class:`.NetworkService`
-        :return:  a NetworkService object
+        :rtype: :class:`.NetworkingService`
+        :return:  a NetworkingService object
         """
 
     @abstractproperty
@@ -162,7 +163,7 @@ class CloudProvider(object):
         .. code-block:: python
 
             keypairs = provider.security.keypairs.list()
-            security_groups = provider.security.security_groups.list()
+            vm_firewalls = provider.security.vm_firewalls.list()
 
 
         :rtype: ``object`` of :class:`.SecurityService`
@@ -171,38 +172,23 @@ class CloudProvider(object):
         pass
 
     @abstractproperty
-    def block_store(self):
+    def storage(self):
         """
-        Provides access to the volume and snapshot services in this
-        provider.
+        Provides access to storage related services in this provider.
+        This includes the volume, snapshot and bucket services,
 
         Example:
 
         .. code-block:: python
 
-            volumes = provider.block_store.volumes.list()
-            snapshots = provider.block_store.snapshots.list()
-
-        :rtype: :class:`.BlockStoreService`
-        :return: a BlockStoreService object
-        """
-        pass
-
-    @abstractproperty
-    def object_store(self):
-        """
-        Provides access to object storage services in this provider.
-
-        Example:
-
-        .. code-block:: python
-
-            if provider.has_service(CloudServiceType.OBJECT_STORE):
+            volumes = provider.storage.volumes.list()
+            snapshots = provider.storage.snapshots.list()
+            if provider.has_service(CloudServiceType.BUCKET):
                print("Provider supports object store services")
-               print(provider.object_store.list())
+               print(provider.storage.buckets.list())
 
-        :rtype: ``object`` of :class:`.ObjectStoreService`
-        :return: an ObjectStoreService object
+        :rtype: :class:`.StorageService`
+        :return: a StorageService object
         """
         pass
 

Разница между файлами не показана из-за своего большого размера
+ 248 - 352
cloudbridge/cloud/interfaces/resources.py


+ 408 - 166
cloudbridge/cloud/interfaces/services.py

@@ -64,24 +64,24 @@ class ComputeService(CloudService):
         pass
 
     @abstractproperty
-    def instance_types(self):
+    def vm_types(self):
         """
-        Provides access to all Instance type related services in this provider.
+        Provides access to all VM type related services in this provider.
 
         Example:
 
         .. code-block:: python
 
-            # list all instance sizes
-            for inst_type in provider.compute.instance_types:
-                print(inst_type.id, inst_type.name)
+            # list all VM sizes
+            for vm_type in provider.compute.vm_types:
+                print(vm_type.id, vm_type.name)
 
             # find a specific size by name
-            inst_type = provider.compute.instance_types.find(name='m1.small')
-            print(inst_type.vcpus)
+            vm_type = provider.compute.vm_types.find(name='m1.small')
+            print(vm_type.vcpus)
 
-        :rtype: :class:`.InstanceTypeService`
-        :return: an InstanceTypeService object
+        :rtype: :class:`.VMTypeService`
+        :return: an VMTypeService object
         """
         pass
 
@@ -96,7 +96,7 @@ class ComputeService(CloudService):
 
             # launch a new instance
             image = provider.compute.images.find(name='Ubuntu 14.04')[0]
-            size = provider.compute.instance_types.find(name='m1.small')
+            size = provider.compute.vm_types.find(name='m1.small')
             instance = provider.compute.instances.create('Hello', image, size)
             print(instance.id, instance.name)
 
@@ -160,10 +160,26 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name):
+    def find(self, name, limit=None, marker=None):
         """
         Searches for an instance by a given list of attributes.
 
+        :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.
         """
@@ -205,8 +221,8 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, image, instance_type, subnet, zone=None,
-               key_pair=None, security_groups=None, user_data=None,
+    def create(self, name, image, vm_type, subnet, zone=None,
+               key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None,
                **kwargs):
         """
@@ -219,8 +235,8 @@ class InstanceService(PageableObjectMixin, CloudService):
         :param image: The MachineImage object or id to boot the virtual machine
                       with
 
-        :type  instance_type: ``InstanceType`` or ``str``
-        :param instance_type: The InstanceType or name, specifying the size of
+        :type  vm_type: ``VMType`` or ``str``
+        :param vm_type: The VMType or name, specifying the size of
                               the instance to boot into
 
         :type  subnet:  ``Subnet`` or ``str``
@@ -246,16 +262,16 @@ class InstanceService(PageableObjectMixin, CloudService):
         :param key_pair: The KeyPair object or its name, to set for the
                          instance.
 
-        :type  security_groups: A ``list`` of ``SecurityGroup`` objects or a
-                                list of ``str`` object IDs
-        :param security_groups: A list of ``SecurityGroup`` objects or a list
-                                of ``SecurityGroup`` IDs, which should be
-                                assigned to this instance.
+        :type  vm_firewalls: A ``list`` of ``VMFirewall`` objects or a
+                             list of ``str`` object IDs
+        :param vm_firewalls: A list of ``VMFirewall`` objects or a list
+                             of ``VMFirewall`` IDs, which should be
+                             assigned to this instance.
 
-                                The security groups must be associated with the
-                                same network as the supplied subnet. Use
-                                ``network.security_groups`` to retrieve a list
-                                of security groups belonging to a network.
+                             The VM firewalls must be associated with the
+                             same network as the supplied subnet. Use
+                             ``network.vm_firewalls`` to retrieve a list
+                             of firewalls belonging to a network.
 
         :type  user_data: ``str``
         :param user_data: An extra userdata object which is compatible with
@@ -408,11 +424,12 @@ class SnapshotService(PageableObjectMixin, CloudService):
         pass
 
 
-class BlockStoreService(CloudService):
+class StorageService(CloudService):
 
     """
-    The Block Store Service interface provides access to block device services,
-    such as volume and snapshot services in the provider.
+    The Storage Service interface provides access to block device services,
+    such as volume and snapshot services, as well as object store services,
+    such as buckets, in the provider.
     """
     __metaclass__ = ABCMeta
 
@@ -426,11 +443,11 @@ class BlockStoreService(CloudService):
         .. code-block:: python
 
             # print all volumes
-            for vol in provider.block_store.volumes:
+            for vol in provider.storage.volumes:
                 print(vol.id, vol.name)
 
             # find volume by name
-            vol = provider.block_store.volumes.find(name='my_vol')[0]
+            vol = provider.storage.volumes.find(name='my_vol')[0]
             print(vol.id, vol.name)
 
         :rtype: :class:`.VolumeService`
@@ -448,15 +465,37 @@ class BlockStoreService(CloudService):
         .. code-block:: python
 
             # print all snapshots
-            for snap in provider.block_store.snapshots:
+            for snap in provider.storage.snapshots:
                 print(snap.id, snap.name)
 
             # find snapshot by name
-            snap = provider.block_store.snapshots.find(name='my_snap')[0]
+            snap = provider.storage.snapshots.find(name='my_snap')[0]
             print(snap.id, snap.name)
 
         :rtype: :class:`.SnapshotService`
-        :return: an SnapshotService object
+        :return: a SnapshotService object
+        """
+        pass
+
+    @abstractproperty
+    def buckets(self):
+        """
+        Provides access to object storage services in this provider.
+
+        Example:
+
+        .. code-block:: python
+
+            # print all buckets
+            for bucket in provider.storage.buckets:
+                print(bucket.id, bucket.name)
+
+            # find bucket by name
+            bucket = provider.storage.buckets.find(name='my_bucket')[0]
+            print(bucket.id, bucket.name)
+
+        :rtype: :class:`.BucketService`
+        :return: a BucketService object
         """
         pass
 
@@ -490,16 +529,75 @@ class ImageService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, filter_by_owner=True, limit=None, marker=None):
         """
         List all images.
 
+        :type  filter_by_owner: ``bool``
+        :param filter_by_owner: If ``True``, return only images owned
+                                by the current user. Else, return all
+                                public images available from the provider.
+                                Note that fetching all images may take a
+                                long time.
+
         :rtype: ``list`` of :class:`.Image`
         :return:  list of image objects
         """
         pass
 
 
+class NetworkingService(CloudService):
+
+    """
+    Base service interface for networking.
+
+    This service offers a collection of networking services that in turn
+    provide access to networking resources.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractproperty
+    def networks(self):
+        """
+        Provides access to all Network related services.
+
+        :rtype: :class:`.NetworkService`
+        :return: a Network service object
+        """
+        pass
+
+    @abstractproperty
+    def subnets(self):
+        """
+        Provides access to all Subnet related services.
+
+        :rtype: :class:`.SubnetService`
+        :return: a Subnet service object
+        """
+        pass
+
+    @abstractproperty
+    def routers(self):
+        """
+        Provides access to all Router related services.
+
+        :rtype: :class:`.RouterService`
+        :return: a Router service object
+        """
+        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):
 
     """
@@ -531,14 +629,34 @@ class NetworkService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name=None):
+    def find(self, name, limit=None, marker=None):
+        """
+        Searches for a network by a given list of attributes.
+
+        :rtype: List of ``object`` of :class:`.Network`
+        :return: A list of Network objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, name, cidr_block):
         """
         Create a new network.
 
         :type name: ``str``
-        :param name: An optional network name. The name will be set if the
+        :param name: A network name. The name will be set if the
                      provider supports it.
 
+        :type cidr_block: ``str``
+        :param cidr_block: The cidr block for this network. Some providers
+                           will respect this at the network level, while others
+                           will only respect it at subnet level. However, to
+                           write portable code, you should make sure that any
+                           subnets you create fall within this initially
+                           specified range. Note that the block size should be
+                           between a /16 netmask (65,536 IP addresses) and /28
+                           netmask (16 IP addresses). e.g. 10.0.0.0/16
+
         :rtype: ``object`` of :class:`.Network`
         :return:  A Network object
         """
@@ -551,11 +669,6 @@ class NetworkService(PageableObjectMixin, CloudService):
 
         :type network_id: ``str``
         :param network_id: The ID of the network to be deleted.
-
-        :rtype: ``bool``
-        :return:  ``True`` if the network does not exist, ``False`` otherwise.
-                  Note that this implies that the network may not have been
-                  deleted by this method but instead has not existed at all.
         """
         pass
 
@@ -569,11 +682,11 @@ class NetworkService(PageableObjectMixin, CloudService):
         .. code-block:: python
 
             # Print all subnets
-            for s in provider.network.subnets:
+            for s in provider.networking.subnets:
                 print(s.id, s.name)
 
             # Get subnet by ID
-            s = provider.network.subnets.get('subnet-id')
+            s = provider.networking.subnets.get('subnet-id')
             print(s.id, s.name)
 
         :rtype: :class:`.SubnetService`
@@ -581,57 +694,6 @@ class NetworkService(PageableObjectMixin, CloudService):
         """
         pass
 
-    @abstractmethod
-    def floating_ips(self, network_id=None):
-        """
-        List floating (i.e., static) IP addresses.
-
-        :type network_id: ``str``
-        :param network_id: The ID of the network by which to filter the IPs.
-
-        :rtype: ``list`` of :class:`FloatingIP`
-        :return: list of floating IP objects
-        """
-        pass
-
-    @abstractmethod
-    def create_floating_ip(self):
-        """
-        Allocate a new floating (i.e., static) IP address.
-
-        :type network_id: ``str``
-        :param network_id: The ID of the network with which to associate the
-                           new IP address.
-
-        :rtype: :class:`FloatingIP`
-        :return: floating IP object
-        """
-        pass
-
-    @abstractmethod
-    def routers(self):
-        """
-        Get a list of available routers.
-
-        :rtype: ``list`` of :class: `Router`
-        :return: list of routers
-        """
-        pass
-
-    @abstractmethod
-    def create_router(self, name=None):
-        """
-        Create a new router/gateway.
-
-        :type name: ``str``
-        :param name: An optional router name. The name will be set if the
-                     provider supports it.
-
-        :rtype: :class:`Router`
-        :return: a newly created router object
-        """
-        pass
-
 
 class SubnetService(PageableObjectMixin, CloudService):
 
@@ -645,8 +707,8 @@ class SubnetService(PageableObjectMixin, CloudService):
         """
         Returns a Subnet given its ID or ``None`` if not found.
 
-        :type network_id: :class:`.Network` object or ``str``
-        :param network_id: The ID of the subnet to retrieve.
+        :type subnet_id: :class:`.Network` object or ``str``
+        :param subnet_id: The ID of the subnet to retrieve.
 
         :rtype: ``object`` of :class:`.Subnet`
         return: a Subnet object
@@ -654,6 +716,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
+    # pylint:disable=arguments-differ
     def list(self, network=None, limit=None, marker=None):
         """
         List all subnets or filter them by the supplied network ID.
@@ -667,10 +730,24 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, network_id, cidr_block, name=None, zone=None):
+    def find(self, name, limit=None, marker=None):
+        """
+        Searches for a subnet by a given list of attributes.
+
+        :rtype: List of ``object`` of :class:`.Subnet`
+        :return: A list of Subnet objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, name, network_id, cidr_block, zone=None):
         """
         Create a new subnet within the supplied network.
 
+        :type name: ``str``
+        :param name: The subnet name. The name will be set if the
+                     provider supports it.
+
         :type network: :class:`.Network` object or ``str``
         :param network: Network object or ID under which to create the subnet.
 
@@ -678,10 +755,6 @@ class SubnetService(PageableObjectMixin, CloudService):
         :param cidr_block: CIDR block within the Network to assign to the
                            subnet.
 
-        :type name: ``str``
-        :param name: An optional subnet name. The name will be set if the
-                     provider supports it.
-
         :type zone: ``str``
         :param zone: An optional placement zone for the subnet. Some providers
                      may not support this, in which case the value is ignored.
@@ -695,6 +768,8 @@ class SubnetService(PageableObjectMixin, CloudService):
     def get_or_create_default(self, zone=None):
         """
         Return a default subnet for the account or create one if not found.
+        This provides a convenience method for obtaining a network if you
+        are not particularly concerned with how the network is structured.
 
         A default network is one marked as such by the provider or matches the
         default name used by this library (e.g., CloudBridgeNet).
@@ -718,20 +793,183 @@ class SubnetService(PageableObjectMixin, CloudService):
 
         :type subnet: :class:`.Subnet` object or ``str``
         :param subnet: Subnet object or ID of the subnet to delete.
+        """
+        pass
 
-        :rtype: ``bool``
-        :return:  ``True`` if the subnet does not exist, ``False`` otherwise.
-                  Note that this implies that the subnet may not have been
-                  deleted by this method but instead has not existed at all.
+
+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.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, router_id):
+        """
+        Returns a Router object given its ID.
+
+        :type router_id: ``str``
+        :param router_id: The ID of the router to retrieve.
+
+        :rtype: ``object``  of :class:`.Router` or ``None``
+        :return: a Router object of ``None`` if not found.
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List all routers.
+
+        :rtype: ``list`` of :class:`.Router`
+        :return: list of Router objects
+        """
+        pass
+
+    @abstractmethod
+    def find(self, name, limit=None, marker=None):
+        """
+        Searches for a router by a given list of attributes.
+
+        :rtype: List of ``object`` of :class:`.Router`
+        :return: A list of Router objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, name, network):
+        """
+        Create a new router.
+
+        :type name: ``str``
+        :param name: A router name. The name will be set if the provider
+                     supports it.
+
+        :type network: :class:`.Network` object or ``str``
+        :param network: Network object or ID under which to create the router.
+
+        :rtype: ``object`` of :class:`.Router`
+        :return:  A Router object
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, router):
+        """
+        Delete an existing Router.
+
+        :type router: :class:`.Router` object or ``str``
+        :param router: Router object or ID of the router to delete.
+        """
+        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 ObjectStoreService(PageableObjectMixin, CloudService):
+class BucketService(PageableObjectMixin, CloudService):
 
     """
-    The Object Storage Service interface provides access to the underlying
-    object store capabilities of this provider. This service is optional and
+    The Bucket Service interface provides access to the underlying
+    object storage capabilities of this provider. This service is optional and
     the :func:`CloudProvider.has_service()` method should be used to verify its
     availability before using the service.
     """
@@ -748,7 +986,7 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
 
         .. code-block:: python
 
-            bucket = provider.object_store.get('my_bucket_id')
+            bucket = provider.storage.buckets.get('my_bucket_id')
             print(bucket.id, bucket.name)
 
         :rtype: :class:`.Bucket`
@@ -757,7 +995,7 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def find(self, name):
+    def find(self, name, limit=None, marker=None):
         """
         Searches for a bucket by a given list of attributes.
 
@@ -765,7 +1003,7 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
 
         .. code-block:: python
 
-            buckets = provider.object_store.find(name='my_bucket_name')
+            buckets = provider.storage.buckets.find(name='my_bucket_name')
             for bucket in buckets:
                 print(bucket.id, bucket.name)
 
@@ -783,7 +1021,7 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
 
         .. code-block:: python
 
-            buckets = provider.object_store.find(name='my_bucket_name')
+            buckets = provider.storage.buckets.find(name='my_bucket_name')
             for bucket in buckets:
                 print(bucket.id, bucket.name)
 
@@ -804,7 +1042,7 @@ class ObjectStoreService(PageableObjectMixin, CloudService):
 
         .. code-block:: python
 
-            bucket = provider.object_store.create('my_bucket_name')
+            bucket = provider.storage.buckets.create('my_bucket_name')
             print(bucket.name)
 
 
@@ -851,24 +1089,24 @@ class SecurityService(CloudService):
         pass
 
     @abstractproperty
-    def security_groups(self):
+    def vm_firewalls(self):
         """
-        Provides access to security groups for this provider.
+        Provides access to firewalls (security groups) for this provider.
 
         Example:
 
         .. code-block:: python
 
-            # print all security groups
-            for sg in provider.security.security_groups:
-                print(sg.id, sg.name)
+            # print all VM firewalls
+            for fw in provider.security.vm_firewalls:
+                print(fw.id, fw.name)
 
-            # find security group by name
-            sg = provider.security.security_groups.find(name='my_sg')[0]
-            print(sg.id, sg.name)
+            # find firewall by name
+            fw = provider.security.vm_firewalls.find(name='my_vm_fw')[0]
+            print(fw.id, fw.name)
 
-        :rtype: :class:`.SecurityGroupService`
-        :return: a SecurityGroupService object
+        :rtype: :class:`.VMFirewallService`
+        :return: a VMFirewallService object
         """
         pass
 
@@ -936,7 +1174,7 @@ class KeyPairService(PageableObjectMixin, CloudService):
     @abstractmethod
     def delete(self, key_pair_id):
         """
-        Delete an existing SecurityGroup.
+        Delete an existing VMFirewall.
 
         :type key_pair_id: str
         :param key_pair_id: The id of the key pair to be deleted.
@@ -949,70 +1187,70 @@ class KeyPairService(PageableObjectMixin, CloudService):
         pass
 
 
-class SecurityGroupService(PageableObjectMixin, CloudService):
+class VMFirewallService(PageableObjectMixin, CloudService):
 
     """
-    Base interface for security groups.
+    Base interface for VM firewalls.
     """
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, security_group_id):
+    def get(self, vm_firewall_id):
         """
-        Returns a SecurityGroup given its ID. Returns ``None`` if the
-        SecurityGroup does not exist.
+        Returns a VMFirewall given its ID. Returns ``None`` if the
+        VMFirewall does not exist.
 
         Example:
 
         .. code-block:: python
 
-            sg = provider.security.security_groups.get('my_sg_id')
-            print(sg.id, sg.name)
+            fw = provider.security.vm_firewalls.get('my_fw_id')
+            print(fw.id, fw.name)
 
-        :rtype: :class:`.SecurityGroup`
-        :return:  a SecurityGroup instance
+        :rtype: :class:`.VMFirewall`
+        :return:  a VMFirewall instance
         """
         pass
 
     @abstractmethod
     def list(self, limit=None, marker=None):
         """
-        List all security groups associated with this account.
+        List all VM firewalls associated with this account.
 
-        :rtype: ``list`` of :class:`.SecurityGroup`
-        :return:  list of SecurityGroup objects
+        :rtype: ``list`` of :class:`.VMFirewall`
+        :return:  list of VMFirewall objects
         """
         pass
 
     @abstractmethod
     def create(self, name, description, network_id):
         """
-        Create a new SecurityGroup.
+        Create a new VMFirewall.
 
         :type name: str
-        :param name: The name of the new security group.
+        :param name: The name of the new VM firewall.
 
         :type description: str
-        :param description: The description of the new security group.
+        :param description: The description of the new VM firewall.
 
         :type  network_id: ``str``
-        :param network_id: Network ID under which to create the security group.
+        :param network_id: Network ID under which to create the VM firewall.
 
-        :rtype: ``object`` of :class:`.SecurityGroup`
-        :return:  A SecurityGroup instance or ``None`` if one was not created.
+        :rtype: ``object`` of :class:`.VMFirewall`
+        :return:  A VMFirewall instance or ``None`` if one was not created.
         """
         pass
 
     @abstractmethod
     def find(self, name, limit=None, marker=None):
         """
-        Get security groups associated with your account filtered by name.
+        Get VM firewalls associated with your account filtered by name.
 
         :type name: str
-        :param name: The name of the security group to retrieve.
+        :param name: The name of the VM firewall to retrieve.
 
-        :rtype: list of :class:`SecurityGroup`
-        :return: A list of SecurityGroup objects or an empty list if none
+        :rtype: list of :class:`VMFirewall`
+        :return: A list of VMFirewall objects or an empty list if none
                  found.
         """
         pass
@@ -1020,48 +1258,42 @@ class SecurityGroupService(PageableObjectMixin, CloudService):
     @abstractmethod
     def delete(self, group_id):
         """
-        Delete an existing SecurityGroup.
+        Delete an existing VMFirewall.
 
         :type group_id: str
-        :param group_id: The security group ID to be deleted.
-
-        :rtype: ``bool``
-        :return:  ``True`` if the security group does not exist, ``False``
-                  otherwise. Note that this implies that the group may not have
-                  been deleted by this method but instead has not existed in
-                  the first place.
+        :param group_id: The VM firewall ID to be deleted.
         """
         pass
 
 
-class InstanceTypesService(PageableObjectMixin, CloudService):
+class VMTypeService(PageableObjectMixin, CloudService):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, instance_type_id):
+    def get(self, vm_type_id):
         """
-        Returns an InstanceType given its ID. Returns ``None`` if the
-        InstanceType does not exist.
+        Returns an VMType given its ID. Returns ``None`` if the
+        VMType does not exist.
 
         Example:
 
         .. code-block:: python
 
-            itype = provider.compute.instance_types.get('my_itype_id')
-            print(itype.id, itype.name)
+            vm_type = provider.compute.vm_types.get('my_vm_type_id')
+            print(vm_type.id, vm_type.name)
 
-        :rtype: :class:`.InstanceType`
-        :return:  an InstanceType instance
+        :rtype: :class:`.VMType`
+        :return:  an VMType instance
         """
         pass
 
     @abstractmethod
     def list(self, limit=None, marker=None):
         """
-        List all instance types.
+        List all VM types.
 
-        :rtype: ``list`` of :class:`.InstanceType`
-        :return: list of InstanceType objects
+        :rtype: ``list`` of :class:`.VMType`
+        :return: list of VMType objects
         """
         pass
 
@@ -1070,8 +1302,8 @@ class InstanceTypesService(PageableObjectMixin, CloudService):
         """
         Searches for instances by a given list of attributes.
 
-        :rtype: ``list`` of :class:`.InstanceType`
-        :return: list of InstanceType objects
+        :rtype: ``object`` of :class:`.VMType`
+        :return: an Instance object
         """
         pass
 
@@ -1115,3 +1347,13 @@ class RegionService(PageableObjectMixin, CloudService):
         :return:  list of region objects
         """
         pass
+
+    @abstractmethod
+    def find(self, name):
+        """
+        Searches for a region by a given list of attributes.
+
+        :rtype: ``object`` of :class:`.Region`
+        :return: a Region object
+        """
+        pass

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

@@ -0,0 +1,326 @@
+"""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
+from botocore.exceptions import ClientError
+from botocore.utils import merge_dicts
+
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
+
+
+def trim_empty_params(params_dict):
+    """
+    Given a dict containing potentially null values, trims out
+    all the null values. This is to please Boto, which throws
+    a parameter validation exception for NoneType arguments.
+    e.g. Given
+        {
+            'GroupName': 'abc',
+            'Description': None,
+            'VpcId': 'xyz',
+        }
+    returns:
+        {
+            'GroupName': 'abc',
+            'VpcId': 'xyz'
+        }
+    """
+    log.debug("Removing null values from %s", params_dict)
+    return {k: v for k, v in params_dict.items() if v is not None}
+
+
+def find_tag_value(tags, key):
+    """
+    Finds the value associated with a given key from a list of AWS tags.
+
+    :type tags: list of ``dict``
+    :param tags: The AWS tag list to search through
+
+    :type key: ``str``
+    :param key: Name of the tag to search for
+    """
+    log.info("Searching for %s in %s", key, tags)
+    for tag in tags or []:
+        if tag.get('Key') == key:
+            log.info("Found %s, returning %s", key, tag.get('Value'))
+            return tag.get('Value')
+    return None
+
+
+class BotoGenericService(object):
+    """
+    Generic implementation of a Boto3 AWS service. Uses Boto3
+    resource, collection and paging support to implement
+    basic cloudbridge methods.
+    """
+    def __init__(self, provider, cb_resource, boto_conn, boto_collection_name):
+        """
+        :type provider: :class:`AWSCloudProvider`
+        :param provider: CloudBridge AWS provider to use
+
+        :type cb_resource: :class:`CloudResource`
+        :param cb_resource: CloudBridge Resource class to wrap results in
+
+        :type boto_conn: :class:`Boto3.Resource`
+        :param boto_conn: Boto top level service resource (e.g. EC2, S3)
+                          connection.
+
+        :type boto_collection_name: ``str``
+        :param boto_collection_name: Boto collection name that corresponds
+                                    to the CloudBridge resource (e.g. key_pair)
+        """
+        self.provider = provider
+        self.cb_resource = cb_resource
+        self.boto_conn = boto_conn
+        self.boto_collection_model = self._infer_collection_model(
+            boto_conn, boto_collection_name)
+        # Perform an empty filter to convert to a ResourceCollection
+        self.boto_collection = (getattr(self.boto_conn, boto_collection_name)
+                                .filter())
+        self.boto_resource = self._infer_boto_resource(
+            boto_conn, self.boto_collection_model)
+
+    def _infer_collection_model(self, conn, collection_name):
+        log.debug("Retrieving boto model for collection: %s", collection_name)
+        return next(col for col in conn.meta.resource_model.collections
+                    if col.name == collection_name)
+
+    def _infer_boto_resource(self, conn, collection_model):
+        log.debug("Retrieving resource model for collection: %s",
+                  collection_model.name)
+        resource_model = next(
+            sr for sr in conn.meta.resource_model.subresources
+            if sr.resource.model.name == collection_model.resource.model.name)
+        return getattr(self.boto_conn, resource_model.name)
+
+    def get(self, resource_id):
+        """
+        Returns a single resource.
+
+        :type resource_id: ``str``
+        :param resource_id: ID of the boto resource to fetch
+        """
+        try:
+            log.debug("Retrieving resource: %s with id: %s",
+                      self.boto_collection_model.name, resource_id)
+            obj = self.boto_resource(resource_id)
+            obj.load()
+            log.debug("Successfully Retrieved: %s", obj)
+            return self.cb_resource(self.provider, obj)
+        except ClientError as exc:
+            error_code = exc.response['Error']['Code']
+            if any(status in error_code for status in
+                   ('NotFound', 'InvalidParameterValue', 'Malformed', '404')):
+                log.debug("Object not found: %s", resource_id)
+                return None
+            else:
+                raise exc
+
+    def _get_list_operation(self):
+        """
+        This function discovers the list operation for a particular resource
+        collection. For example, given the resource collection model for
+        KeyPair, it returns the list operation for it, as describe_key_pairs.
+        """
+        return xform_name(self.boto_collection_model.request.operation)
+
+    def _to_boto_resource(self, collection, params, page):
+        """
+        This function duplicates some of the logic of the pages() method in
+        boto.resources.collection.ResourceCollection. It will convert a raw
+        json response to the corresponding Boto resource. It's necessary
+        because paginators() return json responses, and there's no direct way
+        to convert a paginated json response to a Boto Resource.
+        """
+        # pylint:disable=protected-access
+        return collection._handler(collection._parent, params, page)
+
+    def _resource_iterator(self, collection, params, pages, limit):
+        """
+        Iterates through the pages of a paginated result, converting the
+        objects to BotoResources as necessary. This duplicates the logic in
+        boto's ResourceCollection(). pending issue:
+        https://github.com/boto/boto3/issues/1268
+        """
+        count = 0
+        for page in pages:
+            for item in self._to_boto_resource(collection, params, page):
+                count += 1
+                if limit is not None and count > limit:
+                    return
+                yield item
+
+    def _get_paginated_results(self, limit, marker, collection):
+        """
+        If a Boto Paginator is available, use it. The results
+        are converted back into BotoResources by directly accessing
+        protected members of ResourceCollection. This logic can be removed
+        depending on issue: https://github.com/boto/boto3/issues/1268.
+        """
+        # pylint:disable=protected-access
+        cleaned_params = collection._params.copy()
+        cleaned_params.pop('limit', None)
+        cleaned_params.pop('page_size', None)
+        # pylint:disable=protected-access
+        params = create_request_parameters(
+            collection._parent, collection._model.request)
+        merge_dicts(params, cleaned_params, append_lists=True)
+
+        client = self.boto_conn.meta.client
+        list_op = self._get_list_operation()
+        paginator = client.get_paginator(list_op)
+        PaginationConfig = {}
+        if limit:
+            PaginationConfig = {'MaxItems': limit, 'PageSize': limit}
+        if marker:
+            PaginationConfig.update({'StartingToken': marker})
+        params.update({'PaginationConfig': PaginationConfig})
+        args = trim_empty_params(params)
+        pages = paginator.paginate(**args)
+        # resume_token is not populated unless the iterator is used
+        items = list(self._resource_iterator(collection, params, pages, limit))
+        resume_token = pages.resume_token
+        return (resume_token, items)
+
+    def _make_query(self, collection, limit, marker):
+        """
+        Decide between server or client pagination,
+        depending on the availability of a Boto Paginator.
+        See issue: https://github.com/boto/boto3/issues/1268
+        """
+        client = self.boto_conn.meta.client
+        list_op = self._get_list_operation()
+        if client.can_paginate(list_op):
+            log.debug("Supports server side pagination. Server will"
+                      " limit and page results.")
+            return self._get_paginated_results(limit, marker, collection)
+        else:
+            log.debug("Does not support server side pagination. Client will"
+                      " limit and page results.")
+            # Do not limit, let the ClientPagedResultList enforce limit
+            return (None, collection)
+
+    def list(self, limit=None, marker=None, collection=None, **kwargs):
+        """
+        List a set of resources.
+
+        :type  collection: ``ResourceCollection``
+        :param collection: Boto resource collection object corresponding to the
+                           current resource. See http://boto3.readthedocs.io/
+                           en/latest/guide/collections.html
+        """
+        collection = collection or self.boto_collection.filter(**kwargs)
+        resume_token, boto_objs = self._make_query(collection, limit, marker)
+
+        # Wrap in CB objects.
+        results = [self.cb_resource(self.provider, obj) for obj in boto_objs]
+
+        if resume_token:
+            log.debug("Received a resume token, using server pagination.")
+            return ServerPagedResultList(is_truncated=True,
+                                         marker=resume_token,
+                                         supports_total=False,
+                                         data=results)
+        else:
+            log.debug("Did not received a resume token, will page in client"
+                      " if necessary.")
+            return ClientPagedResultList(self.provider, results,
+                                         limit=limit, marker=marker)
+
+    def find(self, filter_name, filter_value, limit=None, marker=None,
+             **kwargs):
+        """
+        Return a list of resources by filter.
+
+        :type filter_name: ``str``
+        :param filter_name: Name of the filter to use
+
+        :type filter_value: ``str``
+        :param filter_value: Value to filter with
+        """
+        collection = self.boto_collection
+        collection = collection.filter(Filters=[{
+            'Name': filter_name,
+            'Values': [filter_value]
+            }])
+        if kwargs:
+            collection = collection.filter(**kwargs)
+        return self.list(limit=limit, marker=marker, collection=collection)
+
+    def create(self, boto_method, **kwargs):
+        """
+        Creates a resource
+
+        :type boto_method: ``str``
+        :param boto_method: AWS Service method to invoke
+
+        :type kwargs: ``dict``
+        :param kwargs: Arguments to be passed as-is to the service method
+        """
+        log.debug("Creating a resource by invoking %s on these arguments: %s",
+                  boto_method, kwargs)
+        trimmed_args = trim_empty_params(kwargs)
+        result = getattr(self.boto_conn, boto_method)(**trimmed_args)
+        if isinstance(result, list):
+            return [self.cb_resource(self.provider, obj)
+                    for obj in result if obj]
+        else:
+            return self.cb_resource(self.provider, result) if result else None
+
+    def delete(self, resource_id):
+        """
+        Deletes a resource by id
+
+        :type resource_id: ``str``
+        :param resource_id: ID of the resource
+        """
+        log.info("Delete the resource with the id %s", resource_id)
+        res = self.get(resource_id)
+        if res:
+            res.delete()
+
+
+class BotoEC2Service(BotoGenericService):
+    """
+    Boto EC2 service implementation
+    """
+    def __init__(self, provider, cb_resource,
+                 boto_collection_name):
+        """
+        :type provider: :class:`AWSCloudProvider`
+        :param provider: CloudBridge AWS provider to use
+
+        :type cb_resource: :class:`CloudResource`
+        :param cb_resource: CloudBridge Resource class to wrap results in
+
+        :type boto_collection_name: ``str``
+        :param boto_collection_name: Boto collection name that corresponds
+                                    to the CloudBridge resource (e.g. key_pair)
+        """
+        super(BotoEC2Service, self).__init__(
+            provider, cb_resource, provider.ec2_conn,
+            boto_collection_name)
+
+
+class BotoS3Service(BotoGenericService):
+    """
+    Boto S3 service implementation.
+    """
+    def __init__(self, provider, cb_resource,
+                 boto_collection_name):
+        """
+        :type provider: :class:`AWSCloudProvider`
+        :param provider: CloudBridge AWS provider to use
+
+        :type cb_resource: :class:`CloudResource`
+        :param cb_resource: CloudBridge Resource class to wrap results in
+
+        :type boto_collection_name: ``str``
+        :param boto_collection_name: Boto collection name that corresponds
+                                    to the CloudBridge resource (e.g. key_pair)
+        """
+        super(BotoS3Service, self).__init__(
+            provider, cb_resource, provider.s3_conn,
+            boto_collection_name)

+ 56 - 96
cloudbridge/cloud/providers/aws/provider.py

@@ -1,73 +1,78 @@
 """Provider implementation based on boto library for AWS-compatible clouds."""
-
+import logging as log
 import os
 
-import boto
-from boto.ec2.regioninfo import RegionInfo
+import boto3
 try:
     # These are installed only for the case of a dev instance
-    from httpretty import HTTPretty
+    from moto.packages.responses import responses
     from moto import mock_ec2
     from moto import mock_s3
 except ImportError:
-    # TODO: Once library logging is configured, change this
-    print("[aws provider] moto library not available!")
+    log.debug('[aws provider] moto library not available!')
 
 from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
-from .services import AWSBlockStoreService
 from .services import AWSComputeService
-from .services import AWSNetworkService
-from .services import AWSObjectStoreService
+from .services import AWSNetworkingService
 from .services import AWSSecurityService
+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"
 
     def __init__(self, config):
         super(AWSCloudProvider, self).__init__(config)
-        self.cloud_type = 'aws'
 
         # Initialize cloud connection fields
-        self.a_key = self._get_config_value(
-            'aws_access_key', os.environ.get('AWS_ACCESS_KEY', None))
-        self.s_key = self._get_config_value(
-            'aws_secret_key', os.environ.get('AWS_SECRET_KEY', None))
-        self.session_token = self._get_config_value('aws_session_token', None)
-        # EC2 connection fields
-        self.ec2_is_secure = self._get_config_value('ec2_is_secure', True)
-        self.region_name = self._get_config_value(
-            'ec2_region_name', 'us-east-1')
-        self.region_endpoint = self._get_config_value(
-            'ec2_region_endpoint', 'ec2.us-east-1.amazonaws.com')
-        self.ec2_port = self._get_config_value('ec2_port', None)
-        self.ec2_conn_path = self._get_config_value('ec2_conn_path', '/')
-        self.ec2_validate_certs = self._get_config_value(
-            'ec2_validate_certs', False)
-        # S3 connection fields
-        self.s3_is_secure = self._get_config_value('s3_is_secure', True)
-        self.s3_host = self._get_config_value('s3_host', 's3.amazonaws.com')
-        self.s3_port = self._get_config_value('s3_port', None)
-        self.s3_conn_path = self._get_config_value('s3_conn_path', '/')
-        self.s3_validate_certs = self._get_config_value(
-            's3_validate_certs', False)
+        # These are passed as-is to Boto
+        self.region_name = self._get_config_value('aws_region_name',
+                                                  'us-east-1')
+        self.session_cfg = {
+            'aws_access_key_id': self._get_config_value(
+                'aws_access_key', os.environ.get('AWS_ACCESS_KEY', None)),
+            'aws_secret_access_key': self._get_config_value(
+                'aws_secret_key', os.environ.get('AWS_SECRET_KEY', None)),
+            'aws_session_token': self._get_config_value(
+                'aws_session_token', None)
+        }
+        self.ec2_cfg = {
+            'use_ssl': self._get_config_value('ec2_is_secure', True),
+            'verify': self._get_config_value('ec2_validate_certs', True),
+            'endpoint_url': self._get_config_value('ec2_endpoint_url', None)
+        }
+        self.s3_cfg = {
+            'use_ssl': self._get_config_value('s3_is_secure', True),
+            'verify': self._get_config_value('s3_validate_certs', True),
+            'endpoint_url': self._get_config_value('s3_endpoint_url', None)
+        }
 
         # service connections, lazily initialized
+        self._session = None
         self._ec2_conn = None
         self._vpc_conn = None
         self._s3_conn = None
 
         # Initialize provider services
         self._compute = AWSComputeService(self)
-        self._network = AWSNetworkService(self)
+        self._networking = AWSNetworkingService(self)
         self._security = AWSSecurityService(self)
-        self._block_store = AWSBlockStoreService(self)
-        self._object_store = AWSObjectStoreService(self)
+        self._storage = AWSStorageService(self)
+
+    @property
+    def session(self):
+        '''Get a low-level session object or create one if needed'''
+        if not self._session:
+            if self.config.debug_mode:
+                boto3.set_stream_logger(level=log.DEBUG)
+            self._session = boto3.session.Session(
+                region_name=self.region_name, **self.session_cfg)
+        return self._session
 
     @property
     def ec2_conn(self):
@@ -75,12 +80,6 @@ class AWSCloudProvider(BaseCloudProvider):
             self._ec2_conn = self._connect_ec2()
         return self._ec2_conn
 
-    @property
-    def vpc_conn(self):
-        if not self._vpc_conn:
-            self._vpc_conn = self._connect_vpc()
-        return self._vpc_conn
-
     @property
     def s3_conn(self):
         if not self._s3_conn:
@@ -92,71 +91,32 @@ class AWSCloudProvider(BaseCloudProvider):
         return self._compute
 
     @property
-    def network(self):
-        return self._network
+    def networking(self):
+        return self._networking
 
     @property
     def security(self):
         return self._security
 
     @property
-    def block_store(self):
-        return self._block_store
-
-    @property
-    def object_store(self):
-        return self._object_store
+    def storage(self):
+        return self._storage
 
     def _connect_ec2(self):
         """
         Get a boto ec2 connection object.
         """
-        r = RegionInfo(name=self.region_name, endpoint=self.region_endpoint)
-        return self._conect_ec2_region(r)
-
-    def _conect_ec2_region(self, region):
-        ec2_conn = boto.connect_ec2(
-            aws_access_key_id=self.a_key,
-            aws_secret_access_key=self.s_key,
-            is_secure=self.ec2_is_secure,
-            region=region,
-            port=self.ec2_port,
-            path=self.ec2_conn_path,
-            validate_certs=self.ec2_validate_certs,
-            debug=2 if self.config.debug_mode else 0)
-        return ec2_conn
-
-    def _connect_vpc(self):
-        """
-        Get a boto VPC connection object.
-        """
-        r = RegionInfo(name=self.region_name, endpoint=self.region_endpoint)
-        vpc_conn = boto.connect_vpc(
-            aws_access_key_id=self.a_key,
-            aws_secret_access_key=self.s_key,
-            security_token=self.session_token,
-            is_secure=self.ec2_is_secure,
-            region=r,
-            port=self.ec2_port,
-            path=self.ec2_conn_path,
-            validate_certs=self.ec2_validate_certs,
-            debug=2 if self.config.debug_mode else 0)
-        return vpc_conn
+        return self._conect_ec2_region(region_name=self.region_name)
+
+    def _conect_ec2_region(self, region_name=None):
+        '''Get an EC2 resource object'''
+        return self.session.resource(
+            'ec2', region_name=region_name, **self.ec2_cfg)
 
     def _connect_s3(self):
-        """
-        Get a boto S3 connection object.
-        """
-        s3_conn = boto.connect_s3(aws_access_key_id=self.a_key,
-                                  aws_secret_access_key=self.s_key,
-                                  security_token=self.session_token,
-                                  is_secure=self.s3_is_secure,
-                                  port=self.s3_port,
-                                  host=self.s3_host,
-                                  path=self.s3_conn_path,
-                                  validate_certs=self.s3_validate_certs,
-                                  debug=2 if self.config.debug_mode else 0)
-        return s3_conn
+        '''Get an S3 resource object'''
+        return self.session.resource(
+            's3', region_name=self.region_name, **self.s3_cfg)
 
 
 class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
@@ -172,8 +132,8 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
         self.ec2mock.start()
         self.s3mock = mock_s3()
         self.s3mock.start()
-        HTTPretty.register_uri(
-            HTTPretty.GET,
+        responses.add(
+            responses.GET,
             self.AWS_INSTANCE_DATA_DEFAULT_URL,
             body=u"""
 [

Разница между файлами не показана из-за своего большого размера
+ 339 - 425
cloudbridge/cloud/providers/aws/resources.py


Разница между файлами не показана из-за своего большого размера
+ 356 - 452
cloudbridge/cloud/providers/aws/services.py


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

@@ -2,6 +2,7 @@
 Helper functions
 """
 import itertools
+import logging as log
 
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 
@@ -15,6 +16,8 @@ def os_result_limit(provider, requested_limit):
     # i.e. if length(objects) is one more than the limit,
     # we know that the object has another page of results,
     # so we always request one extra record.
+    log.debug("Limit of OpenStack: %s Requested Limit: %s",
+              limit, requested_limit)
     return limit + 1
 
 

+ 37 - 19
cloudbridge/cloud/providers/openstack/provider.py

@@ -17,13 +17,15 @@ from neutronclient.v2_0 import client as neutron_client
 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
 
-from .services import OpenStackBlockStoreService
 from .services import OpenStackComputeService
-from .services import OpenStackNetworkService
-from .services import OpenStackObjectStoreService
+from .services import OpenStackNetworkingService
 from .services import OpenStackSecurityService
+from .services import OpenStackStorageService
 
 
 class OpenStackCloudProvider(BaseCloudProvider):
@@ -33,7 +35,6 @@ class OpenStackCloudProvider(BaseCloudProvider):
 
     def __init__(self, config):
         super(OpenStackCloudProvider, self).__init__(config)
-        self.cloud_type = 'openstack'
 
         # Initialize cloud connection fields
         self.username = self._get_config_value(
@@ -60,16 +61,16 @@ class OpenStackCloudProvider(BaseCloudProvider):
         self._cinder = None
         self._swift = None
         self._neutron = None
+        self._os_conn = None
 
         # Additional cached variables
         self._cached_keystone_session = None
 
         # Initialize provider services
         self._compute = OpenStackComputeService(self)
-        self._network = OpenStackNetworkService(self)
+        self._networking = OpenStackNetworkingService(self)
         self._security = OpenStackSecurityService(self)
-        self._block_store = OpenStackBlockStoreService(self)
-        self._object_store = OpenStackObjectStoreService(self)
+        self._storage = OpenStackStorageService(self)
 
     @property
     def nova(self):
@@ -108,8 +109,8 @@ class OpenStackCloudProvider(BaseCloudProvider):
             return self._cached_keystone_session
 
         if self._keystone_version == 3:
-            from keystoneauth1.identity.v3 import Password as Password_v3
-            auth = Password_v3(auth_url=self.auth_url,
+            from keystoneauth1.identity import v3
+            auth = v3.Password(auth_url=self.auth_url,
                                username=self.username,
                                password=self.password,
                                user_domain_name=self.user_domain_name,
@@ -117,13 +118,28 @@ class OpenStackCloudProvider(BaseCloudProvider):
                                project_name=self.project_name)
             self._cached_keystone_session = session.Session(auth=auth)
         else:
-            from keystoneauth1.identity.v2 import Password as Password_v2
-            auth = Password_v2(self.auth_url, username=self.username,
+            from keystoneauth1.identity import v2
+            auth = v2.Password(self.auth_url, username=self.username,
                                password=self.password,
                                tenant_name=self.project_name)
             self._cached_keystone_session = session.Session(auth=auth)
         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,
+            user_agent='cloudbridge',
+            auth_url=self.auth_url,
+            project_name=self.project_name,
+            username=self.username,
+            password=self.password,
+            user_domain_name=self.user_domain_name,
+            project_domain_name=self.project_domain_name
+        )
+
 #     @property
 #     def glance(self):
 #         if not self._glance:
@@ -148,25 +164,27 @@ class OpenStackCloudProvider(BaseCloudProvider):
             self._neutron = self._connect_neutron()
         return self._neutron
 
+    @property
+    def os_conn(self):
+        if not self._os_conn:
+            self._os_conn = self._connect_openstack()
+        return self._os_conn
+
     @property
     def compute(self):
         return self._compute
 
     @property
-    def network(self):
-        return self._network
+    def networking(self):
+        return self._networking
 
     @property
     def security(self):
         return self._security
 
     @property
-    def block_store(self):
-        return self._block_store
-
-    @property
-    def object_store(self):
-        return self._object_store
+    def storage(self):
+        return self._storage
 
     def _connect_nova(self):
         return self._connect_nova_region(self.region_name)

+ 357 - 232
cloudbridge/cloud/providers/openstack/resources.py

@@ -3,58 +3,73 @@ DataTypes used by this provider
 """
 import inspect
 import ipaddress
-import json
 
+import logging
 import os
 
 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 BaseInstance
-from cloudbridge.cloud.base.resources import BaseInstanceType
+from cloudbridge.cloud.base.resources import BaseInternetGateway
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseMachineImage
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BaseRegion
 from cloudbridge.cloud.base.resources import BaseRouter
-from cloudbridge.cloud.base.resources import BaseSecurityGroup
-from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseVMFirewall
+from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
+from cloudbridge.cloud.base.resources import BaseVMType
 from cloudbridge.cloud.base.resources import BaseVolume
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
+from cloudbridge.cloud.interfaces.resources import GatewayState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
-from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 from keystoneclient.v3.regions import Region
 
+from neutronclient.common.exceptions import PortNotFoundClient
+
 import novaclient.exceptions as novaex
 
+from openstack.exceptions import HttpException
+
 import swiftclient
 
 from swiftclient.service import SwiftService, SwiftUploadObject
 
+
 ONE_GIG = 1048576000  # in bytes
 FIVE_GIG = ONE_GIG * 5  # in bytes
 
+log = logging.getLogger(__name__)
+
 
 class OpenStackMachineImage(BaseMachineImage):
 
     # ref: http://docs.openstack.org/developer/glance/statuses.html
     IMAGE_STATE_MAP = {
-        'QUEUED': MachineImageState.PENDING,
-        'SAVING': MachineImageState.PENDING,
-        'ACTIVE': MachineImageState.AVAILABLE,
-        'KILLED': MachineImageState.ERROR,
-        'DELETED': MachineImageState.ERROR,
-        'PENDING_DELETE': MachineImageState.ERROR
+        'queued': MachineImageState.PENDING,
+        'saving': MachineImageState.PENDING,
+        'active': MachineImageState.AVAILABLE,
+        'killed': MachineImageState.ERROR,
+        'deleted': MachineImageState.ERROR,
+        'pending_delete': MachineImageState.ERROR,
+        'deactivated': MachineImageState.ERROR
     }
 
     def __init__(self, provider, os_image):
@@ -95,13 +110,13 @@ class OpenStackMachineImage(BaseMachineImage):
         :rtype: ``int``
         :return: The minimum disk size needed by this image
         """
-        return self._os_image.minDisk
+        return self._os_image.min_disk
 
     def delete(self):
         """
         Delete this image
         """
-        self._os_image.delete()
+        self._os_image.delete(self._provider.os_conn.session)
 
     @property
     def state(self):
@@ -113,6 +128,7 @@ class OpenStackMachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
+        log.debug("Refreshing OpenStack Machine Image")
         image = self._provider.compute.images.get(self.id)
         if image:
             self._os_image = image._os_image  # pylint:disable=protected-access
@@ -127,7 +143,9 @@ class OpenStackPlacementZone(BasePlacementZone):
     def __init__(self, provider, zone, region):
         super(OpenStackPlacementZone, self).__init__(provider)
         if isinstance(zone, OpenStackPlacementZone):
-            self._os_zone = zone._os_zone  # pylint:disable=protected-access
+            # pylint:disable=protected-access
+            self._os_zone = zone._os_zone
+            # pylint:disable=protected-access
             self._os_region = zone._os_region
         else:
             self._os_zone = zone
@@ -165,10 +183,10 @@ class OpenStackPlacementZone(BasePlacementZone):
         return self._os_region
 
 
-class OpenStackInstanceType(BaseInstanceType):
+class OpenStackVMType(BaseVMType):
 
     def __init__(self, provider, os_flavor):
-        super(OpenStackInstanceType, self).__init__(provider)
+        super(OpenStackVMType, self).__init__(provider)
         self._os_flavor = os_flavor
 
     @property
@@ -223,7 +241,7 @@ class OpenStackInstance(BaseInstance):
     INSTANCE_STATE_MAP = {
         'ACTIVE': InstanceState.RUNNING,
         'BUILD': InstanceState.PENDING,
-        'DELETED': InstanceState.TERMINATED,
+        'DELETED': InstanceState.DELETED,
         'ERROR': InstanceState.ERROR,
         'HARD_REBOOT': InstanceState.REBOOTING,
         'PASSWORD': InstanceState.PENDING,
@@ -253,6 +271,7 @@ class OpenStackInstance(BaseInstance):
         return self._os_instance.id
 
     @property
+    # pylint:disable=arguments-differ
     def name(self):
         """
         Get the instance name.
@@ -265,8 +284,10 @@ class OpenStackInstance(BaseInstance):
         """
         Set the instance name.
         """
+        self.assert_valid_resource_name(value)
+
         self._os_instance.name = value
-        self._os_instance.update()
+        self._os_instance.update(name=value)
 
     @property
     def public_ips(self):
@@ -293,20 +314,20 @@ class OpenStackInstance(BaseInstance):
                 if ipaddress.ip_address(address).is_private]
 
     @property
-    def instance_type_id(self):
+    def vm_type_id(self):
         """
-        Get the instance type name.
+        Get the VM type name.
         """
         return self._os_instance.flavor.get('id')
 
     @property
-    def instance_type(self):
+    def vm_type(self):
         """
-        Get the instance type object.
+        Get the VM type object.
         """
         flavor = self._provider.nova.flavors.get(
             self._os_instance.flavor.get('id'))
-        return OpenStackInstanceType(self._provider, flavor)
+        return OpenStackVMType(self._provider, flavor)
 
     def reboot(self):
         """
@@ -314,10 +335,15 @@ class OpenStackInstance(BaseInstance):
         """
         self._os_instance.reboot()
 
-    def terminate(self):
+    def delete(self):
         """
-        Permanently terminate this instance.
+        Permanently delete this instance.
         """
+        # delete the port we created when launching
+        # Assumption: it's the first interface in the list
+        iface_list = self._os_instance.interface_list()
+        if iface_list:
+            self._provider.neutron.delete_port(iface_list[0].port_id)
         self._os_instance.delete()
 
     @property
@@ -339,19 +365,18 @@ class OpenStackInstance(BaseInstance):
         return getattr(self._os_instance, 'OS-EXT-AZ:availability_zone', None)
 
     @property
-    def security_groups(self):
-        """
-        Get the security groups associated with this instance.
-        """
-        return [self._provider.security.security_groups.find(group['name'])[0]
-                for group in self._os_instance.security_groups]
+    def vm_firewalls(self):
+        return [
+            self._provider.security.vm_firewalls.get(group.id)
+            for group in self._os_instance.list_security_group()
+        ]
 
     @property
-    def security_group_ids(self):
+    def vm_firewall_ids(self):
         """
-        Get the security groups IDs associated with this instance.
+        Get the VM firewall IDs associated with this instance.
         """
-        return [group.id for group in self.security_groups]
+        return [fw.id for fw in self.vm_firewalls]
 
     @property
     def key_pair_name(self):
@@ -364,33 +389,40 @@ class OpenStackInstance(BaseInstance):
         """
         Create a new image based on this instance.
         """
+        log.debug("Creating OpenStack Image with the name %s", name)
+        self.assert_valid_resource_name(name)
+
         image_id = self._os_instance.create_image(name)
         return OpenStackMachineImage(
             self._provider, self._provider.compute.images.get(image_id))
 
-    def add_floating_ip(self, ip_address):
+    def add_floating_ip(self, floating_ip):
         """
         Add a floating IP address to this instance.
         """
-        self._os_instance.add_floating_ip(ip_address)
+        log.debug("Adding floating IP adress: %s", floating_ip)
+        self._os_instance.add_floating_ip(floating_ip.public_ip)
 
-    def remove_floating_ip(self, ip_address):
+    def remove_floating_ip(self, floating_ip):
         """
         Remove a floating IP address from this instance.
         """
-        self._os_instance.remove_floating_ip(ip_address)
+        log.debug("Removing floating IP adress: %s", floating_ip)
+        self._os_instance.remove_floating_ip(floating_ip.public_ip)
 
-    def add_security_group(self, sg):
+    def add_vm_firewall(self, firewall):
         """
-        Add a security group to this instance
+        Add a VM firewall to this instance
         """
-        self._os_instance.add_security_group(sg.id)
+        log.debug("Adding firewall: %s", firewall)
+        self._os_instance.add_security_group(firewall.id)
 
-    def remove_security_group(self, sg):
+    def remove_vm_firewall(self, firewall):
         """
-        Remove a security group from this instance
+        Remove a VM firewall from this instance
         """
-        self._os_instance.remove_security_group(sg.id)
+        log.debug("Removing firewall: %s", firewall)
+        self._os_instance.remove_security_group(firewall.id)
 
     @property
     def state(self):
@@ -437,6 +469,7 @@ class OpenStackRegion(BaseRegion):
             zones = self._provider.nova.availability_zones.list(detailed=False)
         else:
             try:
+                # pylint:disable=protected-access
                 region_nova = self._provider._connect_nova_region(self.name)
                 zones = region_nova.availability_zones.list(detailed=False)
             except novaex.EndpointNotFound:
@@ -474,6 +507,7 @@ class OpenStackVolume(BaseVolume):
         return self._volume.id
 
     @property
+    # pylint:disable=arguments-differ
     def name(self):
         """
         Get the volume name.
@@ -481,10 +515,12 @@ class OpenStackVolume(BaseVolume):
         return self._volume.name
 
     @name.setter
-    def name(self, value):  # pylint:disable=arguments-differ
+    # pylint:disable=arguments-differ
+    def name(self, value):
         """
         Set the volume name.
         """
+        self.assert_valid_resource_name(value)
         self._volume.name = value
         self._volume.update(name=value)
 
@@ -512,7 +548,7 @@ class OpenStackVolume(BaseVolume):
     @property
     def source(self):
         if self._volume.snapshot_id:
-            return self._provider.block_store.snapshots.get(
+            return self._provider.storage.snapshots.get(
                 self._volume.snapshot_id)
         return None
 
@@ -530,6 +566,7 @@ class OpenStackVolume(BaseVolume):
         """
         Attach this volume to an instance.
         """
+        log.debug("Attaching %s to %s instance", device, instance)
         instance_id = instance.id if isinstance(
             instance,
             OpenStackInstance) else instance
@@ -545,7 +582,9 @@ class OpenStackVolume(BaseVolume):
         """
         Create a snapshot of this Volume.
         """
-        return self._provider.block_store.snapshots.create(
+        log.debug("Creating snapchat of volume: %s with the "
+                  "description: %s", name, description)
+        return self._provider.storage.snapshots.create(
             name, self, description=description)
 
     def delete(self):
@@ -564,7 +603,7 @@ class OpenStackVolume(BaseVolume):
         Refreshes the state of this volume by re-querying the cloud provider
         for its latest state.
         """
-        vol = self._provider.block_store.volumes.get(
+        vol = self._provider.storage.volumes.get(
             self.id)
         if vol:
             self._volume = vol._volume  # pylint:disable=protected-access
@@ -594,6 +633,7 @@ class OpenStackSnapshot(BaseSnapshot):
         return self._snapshot.id
 
     @property
+    # pylint:disable=arguments-differ
     def name(self):
         """
         Get the snapshot name.
@@ -601,10 +641,12 @@ class OpenStackSnapshot(BaseSnapshot):
         return self._snapshot.name
 
     @name.setter
-    def name(self, value):  # pylint:disable=arguments-differ
+    # pylint:disable=arguments-differ
+    def name(self, value):
         """
         Set the snapshot name.
         """
+        self.assert_valid_resource_name(value)
         self._snapshot.name = value
         self._snapshot.update(name=value)
 
@@ -639,7 +681,7 @@ class OpenStackSnapshot(BaseSnapshot):
         Refreshes the state of this snapshot by re-querying the cloud provider
         for its latest state.
         """
-        snap = self._provider.block_store.snapshots.get(
+        snap = self._provider.storage.snapshots.get(
             self.id)
         if snap:
             self._snapshot = snap._snapshot  # pylint:disable=protected-access
@@ -658,7 +700,7 @@ class OpenStackSnapshot(BaseSnapshot):
         """
         Create a new Volume from this Snapshot.
         """
-        vol_name = "Created from {0} ({1})".format(self.id, self.name)
+        vol_name = "from_snap_{0}".format(self.id or self.name)
         size = size if size else self._snapshot.size
         os_vol = self._provider.cinder.volumes.create(
             size, name=vol_name, availability_zone=placement,
@@ -695,6 +737,16 @@ class OpenStackNetwork(BaseNetwork):
     def name(self):
         return self._network.get('name', None)
 
+    @name.setter
+    def name(self, value):  # pylint:disable=arguments-differ
+        """
+        Set the network name.
+        """
+        self.assert_valid_resource_name(value)
+        self._provider.neutron.update_network(self.id,
+                                              {'network': {'name': value}})
+        self.refresh()
+
     @property
     def external(self):
         return self._network.get('router:external', False)
@@ -713,28 +765,33 @@ class OpenStackNetwork(BaseNetwork):
 
     def delete(self):
         if 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', [])
+            for port in ports:
+                try:
+                    self._provider.neutron.delete_port(port.get('id'))
+                except PortNotFoundClient:
+                    # Ports could have already been deleted if instances
+                    # are terminated etc. so exceptions can be safely ignored
+                    pass
             self._provider.neutron.delete_network(self.id)
-        # Adhere to the interface docs
-        if self.id not in str(self._provider.neutron.list_networks()):
-            return True
 
+    @property
     def subnets(self):
         subnets = (self._provider.neutron.list_subnets(network_id=self.id)
                    .get('subnets', []))
         return [OpenStackSubnet(self._provider, subnet) for subnet in subnets]
 
-    def create_subnet(self, cidr_block, name='', zone=None):
-        """OpenStack has no support for subnet zones so the value is ignored"""
-        subnet_info = {'name': name, 'network_id': self.id,
-                       'cidr': cidr_block, 'ip_version': 4}
-        subnet = (self._provider.neutron.create_subnet({'subnet': subnet_info})
-                  .get('subnet'))
-        return OpenStackSubnet(self._provider, subnet)
-
     def refresh(self):
         """Refresh the state of this network by re-querying the provider."""
-        net = self._provider.neutron.list_networks(id=self.id).get('networks')
-        self._network = net[0] if net else {}
+        network = self._provider.networking.networks.get(self.id)
+        if network:
+            # pylint:disable=protected-access
+            self._network = network._network
+        else:
+            # subnet no longer exists
+            self._network.state = NetworkState.UNKNOWN
 
 
 class OpenStackSubnet(BaseSubnet):
@@ -742,6 +799,7 @@ class OpenStackSubnet(BaseSubnet):
     def __init__(self, provider, subnet):
         super(OpenStackSubnet, self).__init__(provider)
         self._subnet = subnet
+        self._state = None
 
     @property
     def id(self):
@@ -751,6 +809,16 @@ class OpenStackSubnet(BaseSubnet):
     def name(self):
         return self._subnet.get('name', None)
 
+    @name.setter
+    def name(self, value):  # pylint:disable=arguments-differ
+        """
+        Set the subnet name.
+        """
+        self.assert_valid_resource_name(value)
+        self._provider.neutron.update_subnet(
+            self.id, {'subnet': {'name': value}})
+        self._subnet['name'] = value
+
     @property
     def cidr_block(self):
         return self._subnet.get('cidr', None)
@@ -771,9 +839,21 @@ class OpenStackSubnet(BaseSubnet):
     def delete(self):
         if self.id in str(self._provider.neutron.list_subnets()):
             self._provider.neutron.delete_subnet(self.id)
-        # Adhere to the interface docs
-        if self.id not in str(self._provider.neutron.list_subnets()):
-            return True
+
+    @property
+    def state(self):
+        return SubnetState.UNKNOWN if self._state == SubnetState.UNKNOWN \
+             else SubnetState.AVAILABLE
+
+    def refresh(self):
+        subnet = self._provider.networking.subnets.get(self.id)
+        if subnet:
+            # pylint:disable=protected-access
+            self._subnet = subnet._subnet
+            self._state = SubnetState.AVAILABLE
+        else:
+            # subnet no longer exists
+            self._state = SubnetState.UNKNOWN
 
 
 class OpenStackFloatingIP(BaseFloatingIP):
@@ -784,24 +864,22 @@ class OpenStackFloatingIP(BaseFloatingIP):
 
     @property
     def id(self):
-        return self._ip.get('id', None)
+        return self._ip.id
 
     @property
     def public_ip(self):
-        return self._ip.get('floating_ip_address', None)
+        return self._ip.floating_ip_address
 
     @property
     def private_ip(self):
-        return self._ip.get('fixed_ip_address', None)
+        return self._ip.fixed_ip_address
 
+    @property
     def in_use(self):
-        return bool(self._ip.get('port_id', None))
+        return bool(self._ip.port_id)
 
     def delete(self):
-        self._provider.neutron.delete_floatingip(self.id)
-        # Adhere to the interface docs
-        if self.id not in str(self._provider.neutron.list_floatingips()):
-            return True
+        self._ip.delete(self._provider.os_conn.session)
 
 
 class OpenStackRouter(BaseRouter):
@@ -818,6 +896,16 @@ class OpenStackRouter(BaseRouter):
     def name(self):
         return self._router.get('name', None)
 
+    @name.setter
+    def name(self, value):  # pylint:disable=arguments-differ
+        """
+        Set the router name.
+        """
+        self.assert_valid_resource_name(value)
+        self._provider.neutron.update_router(
+            self.id, {'router': {'name': value}})
+        self.refresh()
+
     def refresh(self):
         self._router = self._provider.neutron.show_router(self.id)['router']
 
@@ -836,40 +924,88 @@ class OpenStackRouter(BaseRouter):
 
     def delete(self):
         self._provider.neutron.delete_router(self.id)
-        # Adhere to the interface docs
-        if self.id not in str(self._provider.neutron.list_routers()):
-            return True
-
-    def attach_network(self, network_id):
-        self._router = self._provider.neutron.add_gateway_router(
-            self.id, {'network_id': network_id}).get('router', self._router)
-        if self.network_id and self.network_id == network_id:
-            return True
-        return False
 
-    def detach_network(self):
-        self._router = self._provider.neutron.remove_gateway_router(
-            self.id).get('router', self._router)
-        if not self.network_id:
-            return True
-        return False
-
-    def add_route(self, subnet_id):
-        router_interface = {'subnet_id': subnet_id}
+    def attach_subnet(self, subnet):
+        router_interface = {'subnet_id': subnet.id}
         ret = self._provider.neutron.add_interface_router(
             self.id, router_interface)
-        if subnet_id in ret.get('subnet_ids', ""):
+        if subnet.id in ret.get('subnet_ids', ""):
             return True
         return False
 
-    def remove_route(self, subnet_id):
-        router_interface = {'subnet_id': subnet_id}
+    def detach_subnet(self, subnet):
+        router_interface = {'subnet_id': subnet.id}
         ret = self._provider.neutron.remove_interface_router(
             self.id, router_interface)
-        if subnet_id in ret.get('subnet_ids', ""):
+        if subnet.id in ret.get('subnet_ids', ""):
             return True
         return False
 
+    def attach_gateway(self, gateway):
+        self._provider.neutron.add_gateway_router(
+            self.id, {'network_id': gateway.id})
+
+    def detach_gateway(self, gateway):
+        self._provider.neutron.remove_gateway_router(
+            self.id).get('router', self._router)
+
+
+class OpenStackInternetGateway(BaseInternetGateway):
+
+    GATEWAY_STATE_MAP = {
+        NetworkState.AVAILABLE: GatewayState.AVAILABLE,
+        NetworkState.DOWN: GatewayState.ERROR,
+        NetworkState.ERROR: GatewayState.ERROR,
+        NetworkState.PENDING: GatewayState.CONFIGURING,
+        NetworkState.UNKNOWN: GatewayState.UNKNOWN
+    }
+
+    def __init__(self, provider, gateway_net):
+        super(OpenStackInternetGateway, self).__init__(provider)
+        if isinstance(gateway_net, OpenStackNetwork):
+            # pylint:disable=protected-access
+            gateway_net = gateway_net._network
+        self._gateway_net = gateway_net
+
+    @property
+    def id(self):
+        return self._gateway_net.get('id', None)
+
+    @property
+    def name(self):
+        return self._gateway_net.get('name', None)
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        self.assert_valid_resource_name(value)
+        self._provider.neutron.update_network(self.id,
+                                              {'network': {'name': value}})
+        self.refresh()
+
+    @property
+    def network_id(self):
+        return self._gateway_net.id
+
+    def refresh(self):
+        """Refresh the state of this network by re-querying the provider."""
+        network = self._provider.networking.networks.get(self.id)
+        if network:
+            # pylint:disable=protected-access
+            self._gateway_net = network._network
+        else:
+            # subnet no longer exists
+            self._gateway_net.state = NetworkState.UNKNOWN
+
+    @property
+    def state(self):
+        return self.GATEWAY_STATE_MAP.get(
+            self._gateway_net.state, GatewayState.UNKNOWN)
+
+    def delete(self):
+        """Do nothing on openstack"""
+        pass
+
 
 class OpenStackKeyPair(BaseKeyPair):
 
@@ -888,10 +1024,11 @@ class OpenStackKeyPair(BaseKeyPair):
         return getattr(self._key_pair, 'private_key', None)
 
 
-class OpenStackSecurityGroup(BaseSecurityGroup):
+class OpenStackVMFirewall(BaseVMFirewall):
 
-    def __init__(self, provider, security_group):
-        super(OpenStackSecurityGroup, self).__init__(provider, security_group)
+    def __init__(self, provider, vm_firewall):
+        super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
+        self._rule_svc = OpenStackVMFirewallRuleContainer(provider, self)
 
     @property
     def network_id(self):
@@ -904,149 +1041,124 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
 
     @property
     def rules(self):
-        # Update SG object; otherwise, recently added rules do now show
-        self._security_group = self._provider.nova.security_groups.get(
-            self._security_group)
-        return [OpenStackSecurityGroupRule(self._provider, r, self)
-                for r in self._security_group.rules]
-
-    def add_rule(self, ip_protocol=None, from_port=None, to_port=None,
-                 cidr_ip=None, src_group=None):
-        """
-        Create a security group rule.
-
-        You need to pass in either ``src_group`` OR ``ip_protocol`` AND
-        ``from_port``, ``to_port``, ``cidr_ip``.  In other words, either
-        you are authorizing another group or you are authorizing some
-        ip-based rule.
-
-        :type ip_protocol: str
-        :param ip_protocol: Either ``tcp`` | ``udp`` | ``icmp``
-
-        :type from_port: int
-        :param from_port: The beginning port number you are enabling
-
-        :type to_port: int
-        :param to_port: The ending port number you are enabling
-
-        :type cidr_ip: str or list of strings
-        :param cidr_ip: The CIDR block you are providing access to.
-
-        :type src_group: ``object`` of :class:`.SecurityGroup`
-        :param src_group: The Security Group you are granting access to.
-
-        :rtype: :class:``.SecurityGroupRule``
-        :return: Rule object if successful or ``None``.
-        """
-        if src_group:
-            if not isinstance(src_group, SecurityGroup):
-                src_group = self._provider.security.security_groups.get(
-                    src_group)
-            existing_rule = self.get_rule(ip_protocol=ip_protocol,
-                                          from_port=from_port,
-                                          to_port=to_port,
-                                          src_group=src_group)
-            if existing_rule:
-                return existing_rule
-
-            rule = self._provider.nova.security_group_rules.create(
-                parent_group_id=self._security_group.id,
-                ip_protocol=ip_protocol,
-                from_port=from_port,
-                to_port=to_port,
-                group_id=src_group.id)
-            if rule:
-                # We can only return one Rule so default to TCP (ie, last in
-                # the for loop above).
-                return OpenStackSecurityGroupRule(self._provider,
-                                                  rule.to_dict(), self)
-        else:
-            existing_rule = self.get_rule(ip_protocol=ip_protocol,
-                                          from_port=from_port,
-                                          to_port=to_port,
-                                          cidr_ip=cidr_ip)
-            if existing_rule:
-                return existing_rule
-
-            rule = self._provider.nova.security_group_rules.create(
-                parent_group_id=self._security_group.id,
-                ip_protocol=ip_protocol,
-                from_port=from_port,
-                to_port=to_port,
-                cidr=cidr_ip)
-            if rule:
-                return OpenStackSecurityGroupRule(self._provider,
-                                                  rule.to_dict(), self)
-        return None
+        return self._rule_svc
 
-    def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
-                 cidr_ip=None, src_group=None):
-        # Update SG object; otherwise, recently added rules do not show
-        self._security_group = self._provider.nova.security_groups.get(
-            self._security_group)
-        for rule in self._security_group.rules:
-            if (rule['ip_protocol'] == ip_protocol and
-                rule['from_port'] == from_port and
-                rule['to_port'] == to_port and
-                (rule['ip_range'].get('cidr') == cidr_ip or
-                 (rule['group'].get('name') == src_group.name if src_group
-                  else False))):
-                return OpenStackSecurityGroupRule(self._provider, rule, self)
-        return None
+    def delete(self):
+        return self._vm_firewall.delete(self._provider.os_conn.session)
+
+    def refresh(self):
+        self._vm_firewall = self._provider.os_conn.network.get_security_group(
+            self.id)
 
     def to_json(self):
         attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
         js = {k: v for(k, v) in attr if not k.startswith('_')}
         json_rules = [r.to_json() for r in self.rules]
-        js['rules'] = [json.loads(r) for r in json_rules]
-        return json.dumps(js, sort_keys=True)
-
-
-class OpenStackSecurityGroupRule(BaseSecurityGroupRule):
-
-    def __init__(self, provider, rule, parent):
-        super(OpenStackSecurityGroupRule, self).__init__(
-            provider, rule, parent)
+        js['rules'] = json_rules
+        return js
+
+
+class OpenStackVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
+
+    def __init__(self, provider, firewall):
+        super(OpenStackVMFirewallRuleContainer, self).__init__(
+            provider, firewall)
+
+    def list(self, limit=None, marker=None):
+        # pylint:disable=protected-access
+        rules = [OpenStackVMFirewallRule(self.firewall, r)
+                 for r in self.firewall._vm_firewall.security_group_rules]
+        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):
+        src_dest_fw_id = (src_dest_fw.id if isinstance(src_dest_fw,
+                                                       OpenStackVMFirewall)
+                          else src_dest_fw)
+
+        try:
+            if direction == TrafficDirection.INBOUND:
+                os_direction = 'ingress'
+            elif direction == TrafficDirection.OUTBOUND:
+                os_direction = 'egress'
+            else:
+                raise InvalidValueException("direction", direction)
+            # pylint:disable=protected-access
+            rule = self._provider.os_conn.network.create_security_group_rule(
+                security_group_id=self.firewall.id,
+                direction=os_direction,
+                port_range_max=to_port,
+                port_range_min=from_port,
+                protocol=protocol,
+                remote_ip_prefix=cidr,
+                remote_group_id=src_dest_fw_id)
+            self.firewall.refresh()
+            return OpenStackVMFirewallRule(self.firewall, rule.to_dict())
+        except HttpException as e:
+            self.firewall.refresh()
+            # 409=Conflict, raised for duplicate rule
+            if e.http_status == 409:
+                existing = self.find(direction=direction, protocol=protocol,
+                                     from_port=from_port, to_port=to_port,
+                                     cidr=cidr, src_dest_fw_id=src_dest_fw_id)
+                return existing[0]
+            else:
+                raise e
+
+
+class OpenStackVMFirewallRule(BaseVMFirewallRule):
+
+    def __init__(self, parent_fw, rule):
+        super(OpenStackVMFirewallRule, self).__init__(parent_fw, rule)
 
     @property
     def id(self):
         return self._rule.get('id')
 
     @property
-    def ip_protocol(self):
-        return self._rule.get('ip_protocol')
+    def direction(self):
+        direction = self._rule.get('direction')
+        if direction == 'ingress':
+            return TrafficDirection.INBOUND
+        elif direction == 'egress':
+            return TrafficDirection.OUTBOUND
+        else:
+            return None
+
+    @property
+    def protocol(self):
+        return self._rule.get('protocol')
 
     @property
     def from_port(self):
-        return int(self._rule.get('from_port') or 0)
+        return self._rule.get('port_range_min')
 
     @property
     def to_port(self):
-        return int(self._rule.get('to_port') or 0)
+        return self._rule.get('port_range_max')
 
     @property
-    def cidr_ip(self):
-        return self._rule.get('ip_range', {}).get('cidr')
+    def cidr(self):
+        return self._rule.get('remote_ip_prefix')
 
     @property
-    def group(self):
-        cg = self._rule.get('group', {}).get('name')
-        if cg:
-            security_groups = self._provider.nova.security_groups.list()
-            for sg in security_groups:
-                if sg.name == cg:
-                    return OpenStackSecurityGroup(self._provider, sg)
+    def src_dest_fw_id(self):
+        fw = self.src_dest_fw
+        if fw:
+            return fw.id
         return None
 
-    def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
-        js['group'] = self.group.id if self.group else ''
-        js['parent'] = self.parent.id if self.parent else ''
-        return json.dumps(js, sort_keys=True)
+    @property
+    def src_dest_fw(self):
+        fw_id = self._rule.get('remote_group_id')
+        if fw_id:
+            return self._provider.security.vm_firewalls.get(fw_id)
+        return None
 
     def delete(self):
-        return self._provider.nova.security_group_rules.delete(self.id)
+        self._provider.os_conn.network.delete_security_group_rule(self.id)
+        self.firewall.refresh()
 
 
 class OpenStackBucketObject(BaseBucketObject):
@@ -1114,6 +1226,7 @@ class OpenStackBucketObject(BaseBucketObject):
                 upload_options['segment_size'] = FIVE_GIG
 
         # remap the swift service's connection factory method
+        # pylint:disable=protected-access
         swiftclient.service.get_conn = self._provider._connect_swift
 
         result = True
@@ -1138,6 +1251,7 @@ class OpenStackBucketObject(BaseBucketObject):
         """
 
         # remap the swift service's connection factory method
+        # pylint:disable=protected-access
         swiftclient.service.get_conn = self._provider._connect_swift
 
         result = True
@@ -1165,6 +1279,7 @@ class OpenStackBucket(BaseBucket):
     def __init__(self, provider, bucket):
         super(OpenStackBucket, self).__init__(provider)
         self._bucket = bucket
+        self._object_container = OpenStackBucketContainer(provider, self)
 
     @property
     def id(self):
@@ -1172,11 +1287,21 @@ class OpenStackBucket(BaseBucket):
 
     @property
     def name(self):
-        """
-        Get this bucket's name.
-        """
         return self._bucket.get("name")
 
+    @property
+    def objects(self):
+        return self._object_container
+
+    def delete(self, delete_contents=False):
+        self._provider.swift.delete_container(self.name)
+
+
+class OpenStackBucketContainer(BaseBucketContainer):
+
+    def __init__(self, provider, bucket):
+        super(OpenStackBucketContainer, self).__init__(provider, bucket)
+
     def get(self, name):
         """
         Retrieve a given object from this bucket.
@@ -1186,9 +1311,9 @@ class OpenStackBucket(BaseBucket):
         return the first element.
         """
         _, object_list = self._provider.swift.get_container(
-            self.name, prefix=name)
+            self.bucket.name, prefix=name)
         if object_list:
-            return OpenStackBucketObject(self._provider, self,
+            return OpenStackBucketObject(self._provider, self.bucket,
                                          object_list[0])
         else:
             return None
@@ -1201,22 +1326,22 @@ class OpenStackBucket(BaseBucket):
         :return: List of all available BucketObjects within this bucket.
         """
         _, object_list = self._provider.swift.get_container(
-            self.name, limit=oshelpers.os_result_limit(self._provider, limit),
+            self.bucket.name,
+            limit=oshelpers.os_result_limit(self._provider, limit),
             marker=marker, prefix=prefix)
         cb_objects = [OpenStackBucketObject(
-            self._provider, self, obj) for obj in object_list]
+            self._provider, self.bucket, obj) for obj in object_list]
 
         return oshelpers.to_server_paged_list(
             self._provider,
             cb_objects,
             limit)
 
-    def delete(self, delete_contents=False):
-        """
-        Delete this bucket.
-        """
-        self._provider.swift.delete_container(self.name)
+    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 create_object(self, object_name):
-        self._provider.swift.put_object(self.name, object_name, None)
+    def create(self, object_name):
+        self._provider.swift.put_object(self.bucket.name, object_name, None)
         return self.get(object_name)

+ 351 - 192
cloudbridge/cloud/providers/openstack/services.py

@@ -9,27 +9,32 @@ from cinderclient.exceptions import NotFound as CinderNotFound
 
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
-from cloudbridge.cloud.base.services import BaseBlockStoreService
+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 BaseInstanceTypesService
 from cloudbridge.cloud.base.services import BaseKeyPairService
 from cloudbridge.cloud.base.services import BaseNetworkService
-from cloudbridge.cloud.base.services import BaseObjectStoreService
+from cloudbridge.cloud.base.services import BaseNetworkingService
 from cloudbridge.cloud.base.services import BaseRegionService
-from cloudbridge.cloud.base.services import BaseSecurityGroupService
+from cloudbridge.cloud.base.services import BaseRouterService
 from cloudbridge.cloud.base.services import BaseSecurityService
 from cloudbridge.cloud.base.services import BaseSnapshotService
+from cloudbridge.cloud.base.services import BaseStorageService
 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.resources import InstanceType
+from cloudbridge.cloud.interfaces.exceptions import ProviderInternalException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import PlacementZone
-from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Subnet
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
@@ -37,18 +42,21 @@ from neutronclient.common.exceptions import NeutronClientException
 
 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 OpenStackInstanceType
+from .resources import OpenStackInternetGateway
 from .resources import OpenStackKeyPair
 from .resources import OpenStackMachineImage
 from .resources import OpenStackNetwork
 from .resources import OpenStackRegion
 from .resources import OpenStackRouter
-from .resources import OpenStackSecurityGroup
 from .resources import OpenStackSnapshot
 from .resources import OpenStackSubnet
+from .resources import OpenStackVMFirewall
+from .resources import OpenStackVMType
 from .resources import OpenStackVolume
 
 log = logging.getLogger(__name__)
@@ -61,7 +69,7 @@ class OpenStackSecurityService(BaseSecurityService):
 
         # Initialize provider services
         self._key_pairs = OpenStackKeyPairService(provider)
-        self._security_groups = OpenStackSecurityGroupService(provider)
+        self._vm_firewalls = OpenStackVMFirewallService(provider)
 
     @property
     def key_pairs(self):
@@ -74,14 +82,14 @@ class OpenStackSecurityService(BaseSecurityService):
         return self._key_pairs
 
     @property
-    def security_groups(self):
+    def vm_firewalls(self):
         """
-        Provides access to security groups for this provider.
+        Provides access to VM firewalls for this provider.
 
-        :rtype: ``object`` of :class:`.SecurityGroupService`
-        :return: a SecurityGroupService object
+        :rtype: ``object`` of :class:`.VMFirewallService`
+        :return: a VMFirewallService object
         """
-        return self._security_groups
+        return self._vm_firewalls
 
     def get_or_create_ec2_credentials(self):
         """
@@ -90,13 +98,14 @@ class OpenStackSecurityService(BaseSecurityService):
         """
         keystone = self.provider.keystone
         if hasattr(keystone, 'ec2'):
-            user_creds = [cred for cred in keystone.ec2.list(keystone.user_id)
-                          if cred.tenant_id == keystone.tenant_id]
+            user_id = keystone.session.get_user_id()
+            user_creds = [cred for cred in keystone.ec2.list(user_id) if
+                          cred.tenant_id == keystone.session.get_project_id()]
             if user_creds:
                 return user_creds[0]
             else:
-                return keystone.ec2.create(keystone.user_id,
-                                           keystone.tenant_id)
+                return keystone.ec2.create(
+                    user_id, keystone.session.get_project_id())
 
         return None
 
@@ -105,21 +114,12 @@ class OpenStackSecurityService(BaseSecurityService):
         A provider specific method than returns the ec2 endpoints if
         available.
         """
-        service_catalog = self.provider.keystone.service_catalog.get_data()
-        current_region = self.provider.compute.regions.current.id
-        ec2_url = [endpoint.get('publicURL')
-                   for svc in service_catalog
-                   for endpoint in svc.get('endpoints', [])
-                   if endpoint.get('region', None) ==
-                   current_region and svc.get('type', None) == 'ec2']
-        s3_url = [endpoint.get('publicURL')
-                  for svc in service_catalog
-                  for endpoint in svc.get('endpoints', [])
-                  if endpoint.get('region', None) ==
-                  current_region and svc.get('type', None) == 's3']
+        keystone = self.provider.keystone
+        ec2_url = keystone.session.get_endpoint(service_type='ec2')
+        s3_url = keystone.session.get_endpoint(service_type='s3')
 
-        return {'ec2_endpoint': ec2_url[0] if ec2_url else None,
-                's3_endpoint': s3_url[0] if s3_url else None}
+        return {'ec2_endpoint': ec2_url,
+                's3_endpoint': s3_url}
 
 
 class OpenStackKeyPairService(BaseKeyPairService):
@@ -131,10 +131,12 @@ class OpenStackKeyPairService(BaseKeyPairService):
         """
         Returns a KeyPair given its id.
         """
+        log.debug("Returning KeyPair with the id %s", key_pair_id)
         try:
             return OpenStackKeyPair(
                 self.provider, self.provider.nova.keypairs.get(key_pair_id))
         except NovaNotFound:
+            log.debug("KeyPair %s was not found.", key_pair_id)
             return None
 
     def list(self, limit=None, marker=None):
@@ -144,10 +146,11 @@ class OpenStackKeyPairService(BaseKeyPairService):
         :rtype: ``list`` of :class:`.KeyPair`
         :return:  list of KeyPair objects
         """
-
         keypairs = self.provider.nova.keypairs.list()
         results = [OpenStackKeyPair(self.provider, kp)
                    for kp in keypairs]
+        log.debug("Listing all key pairs associated with OpenStack "
+                  "Account: %s", results)
         return ClientPagedResultList(self.provider, results,
                                      limit=limit, marker=marker)
 
@@ -158,6 +161,7 @@ class OpenStackKeyPairService(BaseKeyPairService):
         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)
 
@@ -171,89 +175,63 @@ class OpenStackKeyPairService(BaseKeyPairService):
         :rtype: ``object`` of :class:`.KeyPair`
         :return:  A key pair instance or ``None`` if one was not be created.
         """
+        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
 
 
-class OpenStackSecurityGroupService(BaseSecurityGroupService):
+class OpenStackVMFirewallService(BaseVMFirewallService):
 
     def __init__(self, provider):
-        super(OpenStackSecurityGroupService, self).__init__(provider)
+        super(OpenStackVMFirewallService, self).__init__(provider)
 
-    def get(self, sg_id):
-        """
-        Returns a SecurityGroup given its id.
-        """
+    def get(self, firewall_id):
+        log.debug("Getting OpenStack VM Firewall with the id: %s", firewall_id)
         try:
-            return OpenStackSecurityGroup(
-                self.provider, self.provider.nova.security_groups.get(sg_id))
-        except NovaNotFound:
+            return OpenStackVMFirewall(
+                self.provider,
+                self.provider.os_conn.network.get_security_group(firewall_id))
+        except ResourceNotFound:
+            log.debug("Firewall %s not found.", firewall_id)
             return None
 
     def list(self, limit=None, marker=None):
-        """
-        List all security groups associated with this account.
-
-        :rtype: ``list`` of :class:`.SecurityGroup`
-        :return:  list of SecurityGroup objects
-        """
-
-        sgs = [OpenStackSecurityGroup(self.provider, sg)
-               for sg in self.provider.nova.security_groups.list()]
+        firewalls = [
+            OpenStackVMFirewall(self.provider, fw)
+            for fw in self.provider.os_conn.network.security_groups()]
 
-        return ClientPagedResultList(self.provider, sgs,
+        return ClientPagedResultList(self.provider, firewalls,
                                      limit=limit, marker=marker)
 
     def create(self, name, description, network_id):
-        """
-        Create a new security group under the current account.
-
-        :type name: str
-        :param name: The name of the new security group.
-
-        :type description: str
-        :param description: The description of the new security group.
-
-        :type  network_id: ``None``
-        :param network_id: Not applicable for OpenStack (yet) so any value is
-                           ignored.
-
-        :rtype: ``object`` of :class:`.SecurityGroup`
-        :return: a SecurityGroup object
-        """
-        sg = self.provider.nova.security_groups.create(name, description)
+        OpenStackVMFirewall.assert_valid_resource_name(name)
+        log.debug("Creating OpenStack VM Firewall with the params: "
+                  "[name: %s network id: %s description: %s]", name,
+                  network_id, description)
+        sg = self.provider.os_conn.network.create_security_group(
+            name=name, description=description)
         if sg:
-            return OpenStackSecurityGroup(self.provider, sg)
+            return OpenStackVMFirewall(self.provider, sg)
         return None
 
     def find(self, name, limit=None, marker=None):
-        """
-        Get all security groups associated with your account.
-        """
-        sgs = self.provider.nova.security_groups.findall(name=name)
-        results = [OpenStackSecurityGroup(self.provider, sg)
-                   for sg in sgs]
+        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)
 
     def delete(self, group_id):
-        """
-        Delete an existing SecurityGroup.
-
-        :type group_id: str
-        :param group_id: The security group ID to be deleted.
-
-        :rtype: ``bool``
-        :return:  ``True`` if the security group does not exist, ``False``
-                  otherwise. Note that this implies that the group may not have
-                  been deleted by this method but instead has not existed in
-                  the first place.
-        """
-        sg = self.get(group_id)
-        if sg:
-            sg.delete()
+        log.debug("Deleting OpenStack Firewall with the id: %s", group_id)
+        firewall = self.get(group_id)
+        if firewall:
+            firewall.delete()
         return True
 
 
@@ -266,35 +244,39 @@ class OpenStackImageService(BaseImageService):
         """
         Returns an Image given its id
         """
+        log.debug("Getting OpenStack Image with the id: %s", image_id)
         try:
             return OpenStackMachineImage(
-                self.provider, self.provider.nova.images.get(image_id))
-        except NovaNotFound:
+                self.provider, self.provider.os_conn.image.get_image(image_id))
+        except ResourceNotFound:
+            log.debug("ResourceNotFound exception raised, %s not found",
+                      image_id)
             return None
 
     def find(self, name, limit=None, marker=None):
         """
         Searches for an image by a given list of attributes
         """
+        log.debug("Searching for the OpenStack image with the name: %s", name)
         regex = fnmatch.translate(name)
         cb_images = [
-            OpenStackMachineImage(self.provider, img)
+            img
             for img in self
             if img.name and re.search(regex, img.name)]
 
         return oshelpers.to_server_paged_list(self.provider, cb_images, limit)
 
-    def list(self, limit=None, marker=None):
+    def list(self, filter_by_owner=True, limit=None, marker=None):
         """
         List all images.
         """
-        if marker is None:
-            os_images = self.provider.nova.images.list(
-                limit=oshelpers.os_result_limit(self.provider, limit))
-        else:
-            os_images = self.provider.nova.images.list(
-                limit=oshelpers.os_result_limit(self.provider, limit),
-                marker=marker)
+        project_id = None
+        if filter_by_owner:
+            project_id = self.provider.os_conn.session.get_project_id()
+        os_images = self.provider.os_conn.image.images(
+            owner=project_id,
+            limit=oshelpers.os_result_limit(self.provider, limit),
+            marker=marker)
 
         cb_images = [
             OpenStackMachineImage(self.provider, img)
@@ -302,14 +284,14 @@ class OpenStackImageService(BaseImageService):
         return oshelpers.to_server_paged_list(self.provider, cb_images, limit)
 
 
-class OpenStackInstanceTypesService(BaseInstanceTypesService):
+class OpenStackVMTypeService(BaseVMTypeService):
 
     def __init__(self, provider):
-        super(OpenStackInstanceTypesService, self).__init__(provider)
+        super(OpenStackVMTypeService, self).__init__(provider)
 
     def list(self, limit=None, marker=None):
         cb_itypes = [
-            OpenStackInstanceType(self.provider, obj)
+            OpenStackVMType(self.provider, obj)
             for obj in self.provider.nova.flavors.list(
                 limit=oshelpers.os_result_limit(self.provider, limit),
                 marker=marker)]
@@ -317,14 +299,15 @@ class OpenStackInstanceTypesService(BaseInstanceTypesService):
         return oshelpers.to_server_paged_list(self.provider, cb_itypes, limit)
 
 
-class OpenStackBlockStoreService(BaseBlockStoreService):
+class OpenStackStorageService(BaseStorageService):
 
     def __init__(self, provider):
-        super(OpenStackBlockStoreService, self).__init__(provider)
+        super(OpenStackStorageService, self).__init__(provider)
 
         # Initialize provider services
         self._volume_svc = OpenStackVolumeService(self.provider)
         self._snapshot_svc = OpenStackSnapshotService(self.provider)
+        self._bucket_svc = OpenStackBucketService(self.provider)
 
     @property
     def volumes(self):
@@ -334,6 +317,10 @@ class OpenStackBlockStoreService(BaseBlockStoreService):
     def snapshots(self):
         return self._snapshot_svc
 
+    @property
+    def buckets(self):
+        return self._bucket_svc
+
 
 class OpenStackVolumeService(BaseVolumeService):
 
@@ -344,16 +331,19 @@ class OpenStackVolumeService(BaseVolumeService):
         """
         Returns a volume given its id.
         """
+        log.debug("Getting OpenStack Volume with the id: %s", volume_id)
         try:
             return OpenStackVolume(
                 self.provider, self.provider.cinder.volumes.get(volume_id))
         except CinderNotFound:
+            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.
         """
+        log.debug("Searching for an OpenStack Volume with the name %s", name)
         search_opts = {'name': name}
         cb_vols = [
             OpenStackVolume(self.provider, vol)
@@ -380,6 +370,11 @@ class OpenStackVolumeService(BaseVolumeService):
         """
         Creates a new volume.
         """
+        log.debug("Creating a new volume with the params: "
+                  "[name: %s size: %s zone: %s snapshot: %s description: %s]",
+                  name, size, zone, snapshot, description)
+        OpenStackVolume.assert_valid_resource_name(name)
+
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         snapshot_id = snapshot.id if isinstance(
             snapshot, OpenStackSnapshot) and snapshot else snapshot
@@ -399,11 +394,13 @@ class OpenStackSnapshotService(BaseSnapshotService):
         """
         Returns a snapshot given its id.
         """
+        log.debug("Getting OpenStack snapshot with the id: %s", snapshot_id)
         try:
             return OpenStackSnapshot(
                 self.provider,
                 self.provider.cinder.volume_snapshots.get(snapshot_id))
         except CinderNotFound:
+            log.debug("Snapshot %s was not found.", snapshot_id)
             return None
 
     def find(self, name, limit=None, marker=None):
@@ -414,6 +411,8 @@ class OpenStackSnapshotService(BaseSnapshotService):
                        'limit': oshelpers.os_result_limit(self.provider,
                                                           limit),
                        'marker': marker}
+        log.debug("Searching for an OpenStack volume with the following "
+                  "params: %s", search_opts)
         cb_snaps = [
             OpenStackSnapshot(self.provider, snap) for
             snap in self.provider.cinder.volume_snapshots.list(search_opts)
@@ -437,6 +436,9 @@ class OpenStackSnapshotService(BaseSnapshotService):
         """
         Creates a new snapshot of a given volume.
         """
+        log.debug("Creating a new snapshot of the %s volume.", name)
+        OpenStackSnapshot.assert_valid_resource_name(name)
+
         volume_id = (volume.id if isinstance(volume, OpenStackVolume)
                      else volume)
 
@@ -446,16 +448,17 @@ class OpenStackSnapshotService(BaseSnapshotService):
         return OpenStackSnapshot(self.provider, os_snap)
 
 
-class OpenStackObjectStoreService(BaseObjectStoreService):
+class OpenStackBucketService(BaseBucketService):
 
     def __init__(self, provider):
-        super(OpenStackObjectStoreService, self).__init__(provider)
+        super(OpenStackBucketService, self).__init__(provider)
 
     def get(self, bucket_id):
         """
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist.
         """
+        log.debug("Getting OpenStack bucket with the id: %s", bucket_id)
         _, container_list = self.provider.swift.get_account(
             prefix=bucket_id)
         if container_list:
@@ -463,12 +466,14 @@ class OpenStackObjectStoreService(BaseObjectStoreService):
                                    next((c for c in container_list
                                          if c['name'] == bucket_id), None))
         else:
+            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.
         """
+        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)
@@ -492,6 +497,9 @@ class OpenStackObjectStoreService(BaseObjectStoreService):
         """
         Create a new bucket.
         """
+        log.debug("Creating a new OpenStack Bucket with the name: %s", name)
+        OpenStackBucket.assert_valid_resource_name(name)
+
         self.provider.swift.put_container(name)
         return self.get(name)
 
@@ -502,6 +510,7 @@ class OpenStackRegionService(BaseRegionService):
         super(OpenStackRegionService, self).__init__(provider)
 
     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)
         return next(region, None)
 
@@ -537,7 +546,7 @@ class OpenStackComputeService(BaseComputeService):
 
     def __init__(self, provider):
         super(OpenStackComputeService, self).__init__(provider)
-        self._instance_type_svc = OpenStackInstanceTypesService(self.provider)
+        self._vm_type_svc = OpenStackVMTypeService(self.provider)
         self._instance_svc = OpenStackInstanceService(self.provider)
         self._region_svc = OpenStackRegionService(self.provider)
         self._images_svc = OpenStackImageService(self.provider)
@@ -547,8 +556,8 @@ class OpenStackComputeService(BaseComputeService):
         return self._images_svc
 
     @property
-    def instance_types(self):
-        return self._instance_type_svc
+    def vm_types(self):
+        return self._vm_type_svc
 
     @property
     def instances(self):
@@ -564,48 +573,85 @@ class OpenStackInstanceService(BaseInstanceService):
     def __init__(self, provider):
         super(OpenStackInstanceService, self).__init__(provider)
 
-    def create(self, name, image, instance_type, subnet, zone=None,
-               key_pair=None, security_groups=None, user_data=None,
+    def create(self, name, image, vm_type, subnet, zone=None,
+               key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None,
                **kwargs):
         """Create a new virtual machine instance."""
+        OpenStackInstance.assert_valid_resource_name(name)
+
         image_id = image.id if isinstance(image, MachineImage) else image
-        instance_size = instance_type.id if \
-            isinstance(instance_type, InstanceType) else \
-            self.provider.compute.instance_types.find(
-                name=instance_type)[0].id
-        network_id = subnet.network_id if isinstance(subnet, Subnet) else None
-        if not network_id and subnet:
-            network_id = (self.provider.network.subnets.get(subnet).network_id
-                          if isinstance(subnet, str) else None)
+        vm_size = vm_type.id if \
+            isinstance(vm_type, VMType) else \
+            self.provider.compute.vm_types.find(
+                name=vm_type)[0].id
+        if isinstance(subnet, Subnet):
+            subnet_id = subnet.id
+            net_id = subnet.network_id
+        else:
+            subnet_id = subnet
+            net_id = (self.provider.networking.subnets
+                      .get(subnet_id).network_id
+                      if subnet_id else None)
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         key_pair_name = key_pair.name if \
             isinstance(key_pair, KeyPair) else key_pair
-        if security_groups:
-            if isinstance(security_groups, list) and \
-                    isinstance(security_groups[0], SecurityGroup):
-                security_groups_list = [sg.name for sg in security_groups]
-            else:
-                security_groups_list = security_groups
-        else:
-            security_groups_list = None
         bdm = None
         if launch_config:
             bdm = self._to_block_device_mapping(launch_config)
 
-        log.debug("Launching in network %s" % network_id)
+        # Security groups must be passed in as a list of IDs and attached to a
+        # port if a port is being created. Otherwise, the security groups must
+        # be passed in as a list of names to the servers.create() call.
+        # OpenStack will respect the port's security groups first and then
+        # fall-back to the named security groups.
+        sg_name_list = []
+        nics = None
+        if subnet_id:
+            log.debug("Creating network port for %s in subnet: %s",
+                      name, subnet_id)
+            sg_list = []
+            if vm_firewalls:
+                if isinstance(vm_firewalls, list) and \
+                        isinstance(vm_firewalls[0], VMFirewall):
+                    sg_list = vm_firewalls
+                else:
+                    sg_list = (self.provider.security.vm_firewalls
+                               .find(name=sg) for sg in vm_firewalls)
+                    sg_list = (sg[0] for sg in sg_list if sg)
+            sg_id_list = [sg.id for sg in sg_list]
+            port_def = {
+                "port": {
+                    "admin_state_up": True,
+                    "name": name,
+                    "network_id": net_id,
+                    "fixed_ips": [{"subnet_id": subnet_id}],
+                    "security_groups": sg_id_list
+                }
+            }
+            port_id = self.provider.neutron.create_port(port_def)['port']['id']
+            nics = [{'net-id': net_id, 'port-id': port_id}]
+        else:
+            if vm_firewalls:
+                if isinstance(vm_firewalls, list) and \
+                        isinstance(vm_firewalls[0], VMFirewall):
+                    sg_name_list = [sg.name for sg in vm_firewalls]
+                else:
+                    sg_name_list = vm_firewalls
+
+        log.debug("Launching in subnet %s", subnet_id)
         os_instance = self.provider.nova.servers.create(
             name,
             None if self._has_root_device(launch_config) else image_id,
-            instance_size,
+            vm_size,
             min_count=1,
             max_count=1,
             availability_zone=zone_id,
             key_name=key_pair_name,
-            security_groups=security_groups_list,
-            userdata=user_data,
+            security_groups=sg_name_list,
+            userdata=str(user_data) or None,
             block_device_mapping_v2=bdm,
-            nics=[{'net-id': network_id}] if network_id else None)
+            nics=nics)
         return OpenStackInstance(self.provider, os_instance)
 
     def _to_block_device_mapping(self, launch_config):
@@ -693,61 +739,77 @@ class OpenStackInstanceService(BaseInstanceService):
             os_instance = self.provider.nova.servers.get(instance_id)
             return OpenStackInstance(self.provider, os_instance)
         except NovaNotFound:
+            log.debug("Instance %s was not found.", instance_id)
             return None
 
 
+class OpenStackNetworkingService(BaseNetworkingService):
+
+    def __init__(self, provider):
+        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):
+        return self._network_service
+
+    @property
+    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):
 
     def __init__(self, provider):
         super(OpenStackNetworkService, self).__init__(provider)
-        self._subnet_svc = OpenStackSubnetService(self.provider)
 
     def get(self, network_id):
-        network = (n for n in self.list() if n.id == network_id)
+        log.debug("Getting OpenStack Network with the id: %s", network_id)
+        network = (n for n in self if n.id == network_id)
         return next(network, None)
 
     def list(self, limit=None, marker=None):
         networks = [OpenStackNetwork(self.provider, network)
                     for network in self.provider.neutron.list_networks()
-                    .get('networks', [])]
+                    .get('networks') if network]
         return ClientPagedResultList(self.provider, networks,
                                      limit=limit, marker=marker)
 
-    def create(self, name=''):
+    def find(self, name, limit=None, marker=None):
+        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)
+
+    def create(self, name, cidr_block):
+        log.debug("Creating OpenStack Network with the params: "
+                  "[name: %s Cinder Block: %s]", name, cidr_block)
+        OpenStackNetwork.assert_valid_resource_name(name)
+
         net_info = {'name': name}
         network = self.provider.neutron.create_network({'network': net_info})
         return OpenStackNetwork(self.provider, network.get('network'))
 
-    @property
-    def subnets(self):
-        return self._subnet_svc
-
-    def floating_ips(self, network_id=None):
-        if network_id:
-            al = self.provider.neutron.list_floatingips(
-                floating_network_id=network_id)['floatingips']
-        else:
-            al = self.provider.neutron.list_floatingips()['floatingips']
-        return [OpenStackFloatingIP(self.provider, a) for a in al]
-
-    def create_floating_ip(self):
-        # OpenStack requires a floating IP to be associated with a pool,
-        # so just choose the first one available...
-        ip_pool_name = self.provider.nova.floating_ip_pools.list()[0].name
-        ip = self.provider.nova.floating_ips.create(ip_pool_name)
-        # Nova returns a different object than Neutron so fetch the Neutron one
-        ip = self.provider.neutron.list_floatingips(id=ip.id)['floatingips'][0]
-        return OpenStackFloatingIP(self.provider, ip)
-
-    def routers(self):
-        routers = self.provider.neutron.list_routers().get('routers')
-        return [OpenStackRouter(self.provider, r) for r in routers]
-
-    def create_router(self, name=None):
-        router = self.provider.neutron.create_router(
-            {'router': {'name': name}})
-        return OpenStackRouter(self.provider, router.get('router'))
-
 
 class OpenStackSubnetService(BaseSubnetService):
 
@@ -755,21 +817,29 @@ class OpenStackSubnetService(BaseSubnetService):
         super(OpenStackSubnetService, self).__init__(provider)
 
     def get(self, subnet_id):
-        subnet = (s for s in self.list() if s.id == subnet_id)
+        log.debug("Getting OpenStack Subnet with the id: %s", subnet_id)
+        subnet = (s for s in self if s.id == subnet_id)
         return next(subnet, None)
 
-    def list(self, network=None):
+    def list(self, network=None, limit=None, marker=None):
         if network:
             network_id = (network.id if isinstance(network, OpenStackNetwork)
                           else network)
-            subnets = self.list()
-            return [subnet for subnet in subnets if network_id in
-                    subnet.network_id]
-        subnets = self.provider.neutron.list_subnets().get('subnets', [])
-        return [OpenStackSubnet(self.provider, subnet) for subnet in subnets]
+            subnets = [subnet for subnet in self.list() if network_id ==
+                       subnet.network_id]
+        else:
+            subnets = [OpenStackSubnet(self.provider, subnet) for subnet in
+                       self.provider.neutron.list_subnets().get('subnets', [])]
+        return ClientPagedResultList(self.provider, subnets,
+                                     limit=limit, marker=marker)
 
-    def create(self, network, cidr_block, name='', zone=None):
+    def create(self, name, network, cidr_block, zone=None):
         """zone param is ignored."""
+        log.debug("Creating OpenStack Subnet with the params: "
+                  "[Name: %s Network: %s Cinder Block: %s Zone: -ignored-]",
+                  name, network, cidr_block)
+        OpenStackSubnet.assert_valid_resource_name(name)
+
         network_id = (network.id if isinstance(network, OpenStackNetwork)
                       else network)
         subnet_info = {'name': name, 'network_id': network_id,
@@ -783,27 +853,29 @@ class OpenStackSubnetService(BaseSubnetService):
         Subnet zone is not supported by OpenStack and is thus ignored.
         """
         try:
-            for sn in self.list():
-                if sn.name == OpenStackSubnet.CB_DEFAULT_SUBNET_NAME:
-                    return sn
+            sn = self.find(name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME)
+            if sn:
+                return sn[0]
             # No default; create one
-            net = self.provider.network.create(
-                OpenStackNetwork.CB_DEFAULT_NETWORK_NAME)
-            sn = net.create_subnet(cidr_block='10.0.0.0/24',
-                                   name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME)
-            router = self.provider.network.create_router(
-                OpenStackRouter.CB_DEFAULT_ROUTER_NAME)
-            for n in self.provider.network.list():
-                if n.external:
-                    external_net = n
-                    break
-            router.attach_network(external_net.id)
-            router.add_route(sn.id)
+            net = self.provider.networking.networks.create(
+                name=OpenStackNetwork.CB_DEFAULT_NETWORK_NAME,
+                cidr_block='10.0.0.0/16')
+            sn = net.create_subnet(name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME,
+                                   cidr_block='10.0.0.0/24')
+            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
+                          ))
+            router.attach_gateway(gteway)
             return sn
         except NeutronClientException:
             return None
 
     def delete(self, subnet):
+        log.debug("Deleting subnet: %s", subnet)
         subnet_id = (subnet.id if isinstance(subnet, OpenStackSubnet)
                      else subnet)
         self.provider.neutron.delete_subnet(subnet_id)
@@ -811,3 +883,90 @@ class OpenStackSubnetService(BaseSubnetService):
         if subnet_id not in self.list():
             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):
+        super(OpenStackRouterService, self).__init__(provider)
+
+    def get(self, router_id):
+        log.debug("Getting OpenStack Router with the id: %s", router_id)
+        router = (r for r in self if r.id == router_id)
+        return next(router, None)
+
+    def list(self, limit=None, marker=None):
+        routers = self.provider.neutron.list_routers().get('routers')
+        os_routers = [OpenStackRouter(self.provider, r) for r in routers]
+        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 create(self, name, network):
+        """
+        ``network`` is not used by OpenStack.
+
+        However, the API seems to indicate it is a (required) param?!
+        https://developer.openstack.org/api-ref/networking/v2/
+            ?expanded=delete-router-detail,create-router-detail#create-router
+        """
+        log.debug("Creating OpenStack Router with the name: %s", name)
+        OpenStackRouter.assert_valid_resource_name(name)
+
+        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()

+ 15 - 0
docs/api_docs/cloud/exceptions.rst

@@ -17,3 +17,18 @@ InvalidConfigurationException
 -----------------------------
 .. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidConfigurationException
     :members:
+
+ProviderConnectionException
+-----------------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.ProviderConnectionException
+    :members:
+
+InvalidNameException
+-----------------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidNameException
+    :members:
+
+InvalidValueException
+-----------------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidValueException
+    :members:

+ 62 - 6
docs/api_docs/cloud/resources.rst

@@ -8,11 +8,26 @@ CloudServiceType
 .. autoclass:: cloudbridge.cloud.interfaces.resources.CloudServiceType
     :members:
 
+CloudResource
+-------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.CloudResource
+    :members:
+
+Configuration
+-------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.Configuration
+    :members:
+
 ObjectLifeCycleMixin
 --------------------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.ObjectLifeCycleMixin
     :members:
 
+PageableObjectMixin
+--------------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.PageableObjectMixin
+    :members:
+
 ResultList
 ----------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.ResultList
@@ -43,16 +58,51 @@ MachineImage
 .. autoclass:: cloudbridge.cloud.interfaces.resources.MachineImage
     :members:
 
+NetworkState
+------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.NetworkState
+    :members:
+
 Network
 -------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.Network
     :members:
 
+SubnetState
+------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.SubnetState
+    :members:
+
 Subnet
 ------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.Subnet
     :members:
 
+FloatingIP
+----------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.FloatingIP
+    :members:
+
+RouterState
+------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.RouterState
+    :members:
+
+Router
+------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.Router
+    :members:
+
+Gateway
+--------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.Gateway
+    :members:
+
+InternetGateway
+---------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.InternetGateway
+    :members:
+
 VolumeState
 -----------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.VolumeState
@@ -88,19 +138,25 @@ PlacementZone
 .. autoclass:: cloudbridge.cloud.interfaces.resources.PlacementZone
     :members:
 
-InstanceType
+VMType
 ------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.InstanceType
+.. autoclass:: cloudbridge.cloud.interfaces.resources.VMType
     :members:
 
-SecurityGroup
+VMFirewall
 -------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.SecurityGroup
+.. autoclass:: cloudbridge.cloud.interfaces.resources.VMFirewall
+    :members:
+
+VMFirewallRule
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.resources.VMFirewallRule
     :members:
+    :undoc-members:
 
-SecurityGroupRule
+TrafficDirection
 -----------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.SecurityGroupRule
+.. autoclass:: cloudbridge.cloud.interfaces.resources.TrafficDirection
     :members:
 
 BucketObject

+ 35 - 10
docs/api_docs/cloud/services.rst

@@ -28,9 +28,9 @@ SnapshotService
 .. autoclass:: cloudbridge.cloud.interfaces.services.SnapshotService
     :members:
 
-BlockStoreService
+StorageService
 -----------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.BlockStoreService
+.. autoclass:: cloudbridge.cloud.interfaces.services.StorageService
     :members:
 
 ImageService
@@ -38,14 +38,39 @@ ImageService
 .. autoclass:: cloudbridge.cloud.interfaces.services.ImageService
     :members:
 
+NetworkingService
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.NetworkingService
+    :members:
+
 NetworkService
---------------
+-----------------
 .. autoclass:: cloudbridge.cloud.interfaces.services.NetworkService
     :members:
 
-ObjectStoreService
-------------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.ObjectStoreService
+SubnetService
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.SubnetService
+    :members:
+
+FloatingIPService
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.FloatingIPService
+    :members:
+
+RouterService
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.RouterService
+    :members:
+
+GatewayService
+-----------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.GatewayService
+    :members:
+
+BucketService
+---------------
+.. autoclass:: cloudbridge.cloud.interfaces.services.BucketService
     :members:
 
 SecurityService
@@ -58,14 +83,14 @@ KeyPairService
 .. autoclass:: cloudbridge.cloud.interfaces.services.KeyPairService
     :members:
 
-SecurityGroupService
+VMFirewallService
 --------------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.SecurityGroupService
+.. autoclass:: cloudbridge.cloud.interfaces.services.VMFirewallService
     :members:
 
-InstanceTypesService
+VMTypeService
 --------------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.InstanceTypesService
+.. autoclass:: cloudbridge.cloud.interfaces.services.VMTypeService
     :members:
 
 RegionService

+ 3 - 3
docs/concepts.rst

@@ -7,13 +7,13 @@ Conceptually, CloudBridge consists of the following types of objects.
 the gateway to using its services.
 
 2. Services - Represents a service provided by a cloud provider,
-such as its compute service, block storage service, object storage etc.
+such as its compute service, storage service, networking service etc.
 Services may in turn be divided into smaller services. Smaller services
 tend to have uniform methods, such as create, find and list. For example,
 InstanceService.list(), InstanceService.find() etc. which can be used
 to access cloud resources. Larger services tend to provide organisational
-structure only. For example, the block store service provides access to
-the VolumeService and SnapshotService.
+structure only. For example, the storage service provides access to
+the VolumeService, SnapshotService and BucketService.
 
 3. Resources - resources are objects returned by a service,
 and represent a remote resource. For example, InstanceService.list()

+ 382 - 262
docs/extras/_images/object_relationships_detailed.svg

@@ -10,12 +10,12 @@
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    viewBox="0 0 1000 800"
    width="700"
-   height="600"
+   height="690"
    preserveAspectRatio="xMinYMin meet"
    id="svg3515"
    version="1.1"
-   inkscape:version="0.91 r13725"
-   sodipodi:docname="object_relationships_detailed.svg">
+   inkscape:version="0.92.2 5c3e80d, 2017-08-06"
+   sodipodi:docname="object_relationships_detailed_orig.svg">
   <metadata
      id="metadata3654">
     <rdf:RDF>
@@ -39,8 +39,8 @@
      guidetolerance="10"
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
-     inkscape:window-width="2560"
-     inkscape:window-height="1391"
+     inkscape:window-width="1920"
+     inkscape:window-height="1151"
      id="namedview3650"
      showgrid="false"
      inkscape:snap-bbox="true"
@@ -51,12 +51,12 @@
      inkscape:snap-nodes="false"
      inkscape:snap-others="false"
      inkscape:zoom="2.0646416"
-     inkscape:cx="357.03794"
-     inkscape:cy="309.32363"
+     inkscape:cx="178.79878"
+     inkscape:cy="250.65042"
      inkscape:window-x="0"
      inkscape:window-y="1"
      inkscape:window-maximized="1"
-     inkscape:current-layer="svg3515" />
+     inkscape:current-layer="svg_18" />
   <clipPath
      id="p.0">
     <path
@@ -64,11 +64,6 @@
        clip-rule="nonzero"
        id="svg_1" />
   </clipPath>
-  <path
-     d="m 679.49788,809.4599 -1.12457,1.12463 3.08978,-1.12463 -3.08978,-1.12457 1.12457,1.12457 z"
-     id="path3510"
-     inkscape:connector-curvature="0"
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
   <path
      style="fill:#b6d7a8;fill-rule:nonzero"
      inkscape:connector-curvature="0"
@@ -203,17 +198,17 @@
      style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
      inkscape:connector-curvature="0"
      id="svg_128"
-     d="m 454.68765,681.42715 100.09452,0" />
+     d="M 554.68765,664.28431 H 654.78217" />
   <path
      style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round"
      inkscape:connector-curvature="0"
      id="svg_129"
-     d="m 354.18765,641.85571 96.66739,0" />
+     d="m 454.18765,624.71287 h 96.66739" />
   <path
      style="fill:#000000;fill-rule:evenodd;stroke:#000000"
      inkscape:connector-curvature="0"
      id="svg_130"
-     d="m 452.35504,641.85571 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 1.12457,1.12457 z" />
+     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"
      inkscape:connector-curvature="0"
@@ -248,94 +243,40 @@
      style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
      inkscape:connector-curvature="0"
      id="svg_153"
-     d="m 723.08923,692.85571 56.97638,0" />
+     d="m 823.08923,675.71287 h 56.97638" />
   <path
      inkscape:connector-curvature="0"
      id="svg_154"
-     d="m 624.99042,641.85571 50.97638,0"
+     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 675.06561,643.50745 4.53809,-1.65174 -4.53809,-1.65173 0,3.30347 z" />
+     d="m 775.06561,626.36461 4.53809,-1.65174 -4.53809,-1.65173 z" />
   <path
      style="fill:#b6d7a8;fill-rule:nonzero"
      inkscape:connector-curvature="0"
      id="e22_46"
      d="m 554.79394,175.07382 0,0 c 0,-3.11658 2.52649,-5.64305 5.64307,-5.64305 l 156.99738,0 c 1.49664,0 2.93194,0.59453 3.99023,1.65282 1.05829,1.05827 1.65283,2.4936 1.65283,3.99023 l 0,22.57218 c 0,3.11656 -2.52649,5.64303 -5.64306,5.64303 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52647 -5.64307,-5.64303 l 0,-22.57218 z" />
   <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e9_texte"
-     y="141.577"
-     x="459.401">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e10_texte"
-     y="181.754"
-     x="458.25299">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e11_texte"
-     y="221.356"
-     x="459.401">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e12_texte"
-     y="262.67999"
-     x="458.25299">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e13_texte"
-     y="375.74701"
-     x="455.957">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e14_texte"
-     y="417.07101"
-     x="458.25299">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e15_texte"
-     y="537.026"
-     x="461.69699">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e16_texte"
-     y="579.49799"
-     x="457.10501">.list() returns</text>
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e17_texte"
-     y="638.12201"
-     x="358.10501">.list() returns</text>
-  <text
-     style="font-size:11.47889996px;font-family:Arial;fill:#000000"
+     style="font-size:11.47889996px;line-height:0%;font-family:Arial;fill:#000000"
      font-size="11.4789px"
      id="e18_texte"
      y="262.10501"
      x="733.17297">.zones</text>
   <text
-     style="font-size:11.47889996px;font-family:Arial;fill:#000000"
+     style="font-size:11.47889996px;line-height:0%;font-family:Arial;fill:#000000"
      font-size="11.4789px"
      id="e19_texte"
      y="417.07101"
      x="733.17401">.rules</text>
   <text
-     style="font-size:11.47889996px;font-family:Arial;fill:#000000"
+     style="font-size:11.47889996px;line-height:0%;font-family:Arial;fill:#000000"
      font-size="11.4789px"
      id="e20_texte"
-     y="637.54797"
-     x="627.85999">.objects</text>
+     y="620.40527"
+     x="727.85992">.objects</text>
   <a
      xlink:href="../api_docs/cloud/providers.html#cloudprovider"
      target="_parent"
@@ -346,7 +287,7 @@
        id="svg_4"
        d="m 45.06562,13.08136 0,0 c 0,-4.79226 3.884899,-8.67716 8.677158,-8.67716 l 112.818892,0 c 2.30134,0 4.50841,0.914199 6.13569,2.54148 1.62728,1.627289 2.54149,3.83436 2.54149,6.13568 l 0,34.708672 c 0,4.792259 -3.88491,8.677158 -8.67717,8.677158 l -112.818902,0 0,0 c -4.792259,0 -8.677158,-3.884899 -8.677158,-8.677158 l 0,-34.708672 z" />
     <text
-       style="font-size:24px;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       style="font-size:24px;line-height:0%;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
        xml:space="preserve"
        font-size="24"
        id="svg_162"
@@ -365,7 +306,7 @@
        id="svg_7"
        d="m 159.19948,71.716537 0,0 c 0,-4.792267 3.8849,-8.677166 8.67717,-8.677166 l 176.94488,0 c 2.30133,0 4.50839,0.9142 6.13568,2.541485 1.62729,1.627281 2.54148,3.834351 2.54148,6.135681 l 0,34.708653 c 0,4.79227 -3.88489,8.67717 -8.67716,8.67717 l -176.94488,0 c -4.79227,0 -8.67717,-3.8849 -8.67717,-8.67717 l 0,-34.708653 z" />
     <text
-       style="font-size:20.66209984px;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       style="font-size:20.66209984px;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="256.34909"
@@ -384,14 +325,14 @@
        d="m 159.19948,304.414 0,0 c 0,-4.79227 3.88489,-8.67715 8.67715,-8.67715 l 176.94503,0 c 2.30133,0 4.50836,0.91418 6.13568,2.54147 1.62725,1.62729 2.54144,3.83435 2.54144,6.13568 l 0,34.70871 c 0,4.79227 -3.88489,8.67712 -8.67712,8.67712 l -176.94503,0 c -4.79226,0 -8.67715,-3.88488 -8.67715,-8.67712 l 0,-34.70871 z"
        id="svg_10" />
     <text
-       style="font-size:20.66209984px;font-family:Arial;fill:#000000"
+       style="font-size:20.66209984px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="20.6621px"
        id="e1_texte"
        y="328.76834"
        x="220.7408">security</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/services.html#blockstoreservice"
+     xlink:href="../api_docs/cloud/services.html#storageservice"
      target="_parent"
      id="svg_18">
     <path
@@ -400,27 +341,11 @@
        id="svg_13"
        d="m 159.19948,466.38321 0,0 c 0,-4.79227 3.8849,-8.67719 8.67717,-8.67719 l 176.94488,0 c 2.30133,0 4.50839,0.91422 6.13568,2.54151 1.62729,1.62729 2.54148,3.83435 2.54148,6.13568 l 0,34.70865 c 0,4.79226 -3.88489,8.67718 -8.67716,8.67718 l -176.94488,0 c -4.79227,0 -8.67717,-3.88492 -8.67717,-8.67718 l 0,-34.70865 z" />
     <text
-       style="font-size:20.66209984px;font-family:Arial;fill:#000000"
+       style="font-size:20.66209984px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="20.6621px"
        id="e2_texte"
        y="490.73755"
-       x="203.49075">block_store</text>
-  </a>
-  <a
-     xlink:href="../api_docs/cloud/services.html#objectstoreservice"
-     target="_parent"
-     id="svg_20">
-    <path
-       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
-       inkscape:connector-curvature="0"
-       id="svg_16"
-       d="m 159.19948,619.97375 0,0 c 0,-4.79229 3.8849,-8.67718 8.67717,-8.67718 l 176.94488,0 c 2.30133,0 4.50839,0.91425 6.13568,2.5415 1.62729,1.62726 2.54148,3.83435 2.54148,6.13568 l 0,34.70868 c 0,4.79224 -3.88489,8.67713 -8.67716,8.67713 l -176.94488,0 c -4.79227,0 -8.67717,-3.88489 -8.67717,-8.67713 l 0,-34.70868 z" />
-    <text
-       style="font-size:20.66209984px;font-family:Arial;fill:#000000"
-       font-size="20.6621px"
-       id="e5_texte"
-       y="644.32806"
-       x="200.03242">object_store</text>
+       x="220.63364">storage</text>
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#imageservice"
@@ -432,7 +357,7 @@
        id="svg_31"
        d="m 286.4042,135.31232 0,0 c 0,-3.11656 2.52646,-5.64304 5.64304,-5.64304 l 156.99738,0 c 1.49664,0 2.93197,0.59453 3.99023,1.6528 1.05829,1.05829 1.6528,2.49361 1.6528,3.99024 l 0,22.57219 c 0,3.11656 -2.52646,5.64303 -5.64303,5.64303 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52647 -5.64304,-5.64303 l 0,-22.57219 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="370.54593"
@@ -442,7 +367,7 @@
        xml:space="preserve">images</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/services.html#instancetypesservice"
+     xlink:href="../api_docs/cloud/services.html#vmtypeservice"
      target="_parent"
      id="svg_23">
     <path
@@ -451,14 +376,14 @@
        id="svg_34"
        d="m 286.4042,175.07297 0,0 c 0,-3.11658 2.52646,-5.64305 5.64304,-5.64305 l 156.99738,0 c 1.49664,0 2.93197,0.59452 3.99023,1.65281 1.05829,1.05828 1.6528,2.49361 1.6528,3.99024 l 0,22.57217 c 0,3.11656 -2.52646,5.64305 -5.64303,5.64305 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52649 -5.64304,-5.64305 l 0,-22.57217 z" />
     <text
-       style="font-size:18.36630058px;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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="191.35905"
        id="svg_6"
        font-size="18.3663px"
-       xml:space="preserve">instance_types</text>
+       xml:space="preserve">vm_types</text>
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#instanceservice"
@@ -470,7 +395,7 @@
        id="svg_37"
        d="m 286.4042,214.83531 0,0 c 0,-3.11656 2.52646,-5.64305 5.64304,-5.64305 l 156.99738,0 c 1.49664,0 2.93197,0.59454 3.99023,1.65282 1.05829,1.05827 1.6528,2.4936 1.6528,3.99023 l 0,22.57218 c 0,3.11657 -2.52646,5.64305 -5.64303,5.64305 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52648 -5.64304,-5.64305 l 0,-22.57218 z" />
     <text
-       style="font-size:18.36630058px;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
@@ -489,7 +414,7 @@
        id="svg_40"
        d="m 286.4042,254.5968 0,0 c 0,-3.11656 2.52646,-5.64303 5.64304,-5.64303 l 156.99738,0 c 1.49664,0 2.93197,0.59452 3.99023,1.6528 1.05829,1.05827 1.6528,2.4936 1.6528,3.99023 l 0,22.57218 c 0,3.11657 -2.52646,5.64303 -5.64303,5.64303 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52646 -5.64304,-5.64303 l 0,-22.57218 z" />
     <text
-       style="font-size:18.36630058px;font-family:Sans-serif;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
@@ -508,14 +433,14 @@
        id="svg_28"
        d="m 286.4042,368.90079 0,0 c 0,-3.11655 2.52646,-5.64304 5.64304,-5.64304 l 156.99738,0 c 1.49664,0 2.93197,0.59454 3.99023,1.6528 1.05829,1.05829 1.6528,2.49362 1.6528,3.99024 l 0,22.5722 c 0,3.11655 -2.52646,5.64304 -5.64303,5.64304 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52649 -5.64304,-5.64304 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;fill:#000000"
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="18.3663px"
        id="e3_texte"
        y="386.68689"
        x="336.34592">keypairs</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/services.html#securitygroupservice"
+     xlink:href="../api_docs/cloud/services.html#vmfirewallservice"
      target="_parent"
      id="svg_29">
     <path
@@ -524,11 +449,11 @@
        id="svg_43"
        d="m 286.4042,409.66275 0,0 c 0,-3.11658 2.52646,-5.64307 5.64304,-5.64307 l 156.99738,0 c 1.49664,0 2.93197,0.59455 3.99023,1.65284 1.05829,1.05825 1.6528,2.49359 1.6528,3.99023 l 0,22.57217 c 0,3.11655 -2.52646,5.64304 -5.64303,5.64304 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52649 -5.64304,-5.64304 l 0,-22.57217 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;fill:#000000"
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="18.3663px"
        id="e4_texte"
        y="427.44882"
-       x="305.71259">security_groups</text>
+       x="319.99832">vm_firewalls</text>
   </a>
   <a
      xlink:href="../api_docs/cloud/services.html#volumeservice"
@@ -540,7 +465,7 @@
        id="svg_19"
        d="m 286.4042,529.47467 0,0 c 0,-3.11658 2.52646,-5.64307 5.64304,-5.64307 l 156.99738,0 c 1.49664,0 2.93197,0.59455 3.99023,1.65284 1.05829,1.05828 1.6528,2.49359 1.6528,3.99023 l 0,22.5722 c 0,3.11658 -2.52646,5.64301 -5.64303,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52643 -5.64304,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;fill:#000000"
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="18.3663px"
        id="e6_texte"
        y="547.26074"
@@ -556,7 +481,7 @@
        id="svg_22"
        d="m 286.4042,571.91199 0,0 c 0,-3.11658 2.52646,-5.64301 5.64304,-5.64301 l 156.99738,0 c 1.49664,0 2.93197,0.59448 3.99023,1.65277 1.05829,1.05829 1.6528,2.49359 1.6528,3.99024 l 0,22.5722 c 0,3.11658 -2.52646,5.64301 -5.64303,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64304,-2.52643 -5.64304,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;fill:#000000"
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;fill:#000000"
        font-size="18.3663px"
        id="e7_texte"
        y="589.69806"
@@ -565,14 +490,15 @@
   <a
      xlink:href="../api_docs/cloud/resources.html#bucket"
      target="_parent"
-     id="svg_25">
+     id="svg_25"
+     transform="translate(99.999997,-17.142858)">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
        id="svg_93"
-       d="m 455.79394,630.56958 0,0 c 0,-3.11652 2.52649,-5.64301 5.64307,-5.64301 l 156.99738,0 c 1.49664,0 2.93194,0.59455 3.99023,1.65278 1.05829,1.05828 1.65283,2.49365 1.65283,3.99023 l 0,22.57221 c 0,3.11657 -2.52649,5.64306 -5.64306,5.64306 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64306 l 0,-22.57221 z" />
+       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" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="539.93567"
@@ -584,14 +510,15 @@
   <a
      xlink:href="../api_docs/cloud/resources.html#bucketobject"
      target="_parent"
-     id="svg_33">
+     id="svg_33"
+     transform="translate(99.999997,-17.142858)">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
        id="svg_96"
-       d="m 681.06561,630.56958 0,0 c 0,-3.11652 2.52649,-5.64301 5.64307,-5.64301 l 156.99737,0 c 1.49659,0 2.93195,0.59455 3.99024,1.65278 1.05829,1.05828 1.65277,2.49365 1.65277,3.99023 l 0,22.57221 c 0,3.11657 -2.52643,5.64306 -5.64301,5.64306 l -156.99737,0 0,0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64306 l 0,-22.57221 z" />
+       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" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="765.20734"
@@ -601,7 +528,7 @@
        xml:space="preserve">BucketObject</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/resources.html#securitygrouprule"
+     xlink:href="../api_docs/cloud/resources.html#vmfirewallrule"
      target="_parent"
      id="svg_35">
     <path
@@ -610,17 +537,17 @@
        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" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="864.60803"
        y="426.61902"
        id="e36_5"
        font-size="18.3663px"
-       xml:space="preserve">SecurityGroupRule</text>
+       xml:space="preserve">VMFirewallRule</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/resources.html#securitygroup"
+     xlink:href="../api_docs/cloud/resources.html#vmfirewall"
      target="_parent"
      id="svg_36">
     <path
@@ -629,14 +556,14 @@
        id="svg_61"
        d="m 554.79394,409.66275 0,0 c 0,-3.11658 2.52649,-5.64307 5.64307,-5.64307 l 156.99738,0 c 1.49664,0 2.93194,0.59455 3.99023,1.65284 1.05829,1.05825 1.65283,2.49359 1.65283,3.99023 l 0,22.57217 c 0,3.11655 -2.52649,5.64304 -5.64306,5.64304 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64304 l 0,-22.57217 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93573"
        y="427.44882"
        id="e30_5"
        font-size="18.3663px"
-       xml:space="preserve">SecurityGroup</text>
+       xml:space="preserve">VMFirewall</text>
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#snapshot"
@@ -648,7 +575,7 @@
        id="svg_58"
        d="m 554.79394,571.91199 0,0 c 0,-3.11658 2.52649,-5.64301 5.64307,-5.64301 l 156.99738,0 c 1.49664,0 2.93194,0.59448 3.99023,1.65277 1.05829,1.05829 1.65283,2.49359 1.65283,3.99024 l 0,22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93573"
@@ -667,7 +594,7 @@
        id="svg_55"
        d="m 554.79394,529.47467 0,0 c 0,-3.11658 2.52649,-5.64307 5.64307,-5.64307 l 156.99738,0 c 1.49664,0 2.93194,0.59455 3.99023,1.65284 1.05829,1.05828 1.65283,2.49359 1.65283,3.99023 l 0,22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="631.646"
@@ -686,7 +613,7 @@
        id="svg_99"
        d="m 554.79394,368.90079 0,0 c 0,-3.11655 2.52649,-5.64304 5.64307,-5.64304 l 156.99738,0 c 1.49664,0 2.93194,0.59454 3.99023,1.6528 1.05829,1.05829 1.65283,2.49362 1.65283,3.99024 l 0,22.5722 c 0,3.11655 -2.52649,5.64304 -5.64306,5.64304 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64304 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93567"
@@ -705,7 +632,7 @@
        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" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="864.2074"
@@ -724,7 +651,7 @@
        id="svg_64"
        d="m 554.79394,254.5968 0,0 c 0,-3.11656 2.52649,-5.64303 5.64307,-5.64303 l 156.99738,0 c 1.49664,0 2.93194,0.59452 3.99023,1.6528 1.05829,1.05827 1.65283,2.4936 1.65283,3.99023 l 0,22.57218 c 0,3.11657 -2.52649,5.64303 -5.64306,5.64303 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52646 -5.64307,-5.64303 l 0,-22.57218 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="641.70209"
@@ -743,7 +670,7 @@
        id="svg_11"
        d="m 554.79394,213.71393 0,0 c 0,-3.11658 2.52662,-5.64307 5.64307,-5.64307 l 156.99756,0 c 1.49658,0 2.93188,0.59454 3.99023,1.65283 1.05811,1.05829 1.65284,2.49359 1.65284,3.99024 l 0,22.5722 c 0,3.11652 -2.52662,5.64301 -5.64307,5.64301 l -156.99756,0 0,0 c -3.11645,0 -5.64307,-2.52649 -5.64307,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93573"
@@ -753,7 +680,7 @@
        xml:space="preserve">Instance</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/resources.html#instancetype"
+     xlink:href="../api_docs/cloud/resources.html#vmtype"
      target="_parent"
      id="svg_47">
     <path
@@ -762,14 +689,14 @@
        id="e24_46"
        d="m 554.79394,175.07382 0,0 c 0,-3.11658 2.52649,-5.64305 5.64307,-5.64305 l 156.99738,0 c 1.49664,0 2.93194,0.59453 3.99023,1.65282 1.05829,1.05827 1.65283,2.4936 1.65283,3.99023 l 0,22.57218 c 0,3.11656 -2.52649,5.64303 -5.64306,5.64303 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52647 -5.64307,-5.64303 l 0,-22.57218 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93573"
        y="192.85989"
        id="e26_5"
        font-size="18.3663px"
-       xml:space="preserve">InstanceType</text>
+       xml:space="preserve">VMType</text>
   </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#machineimage"
@@ -781,7 +708,7 @@
        id="svg_2"
        d="m 554.79394,134.21393 0,0 c 0,-3.11658 2.52649,-5.64307 5.64307,-5.64307 l 156.99744,0 c 1.49658,0 2.93188,0.59454 3.99023,1.65283 1.05823,1.05829 1.65283,2.49359 1.65283,3.99024 l 0,22.5722 c 0,3.11652 -2.52661,5.64301 -5.64306,5.64301 l -156.99744,0 0,0 c -3.11658,0 -5.64307,-2.52649 -5.64307,-5.64301 l 0,-22.5722 z" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
        x="638.93567"
@@ -791,8 +718,8 @@
        xml:space="preserve">MachineImage</text>
   </a>
   <path
-     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.13655138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-     d="m 109.41699,55.714308 0.0249,654.285682"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.11912274px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 109.41699,55.714361 0.0249,634.313959"
      id="path3922"
      inkscape:connector-type="polyline"
      inkscape:connector-curvature="0" />
@@ -863,187 +790,380 @@
      inkscape:connector-type="polyline"
      inkscape:connector-curvature="0" />
   <path
-     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.99340224px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-     d="m 256.82024,510.53287 0.16792,73.9978"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.24181509px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 256.82113,510.52584 0.16614,116.86232"
      id="path3922-6-4-3"
      inkscape:connector-type="polyline"
      inkscape:connector-curvature="0" />
+  <a
+     xlink:href="../api_docs/cloud/services.html#networkingservice"
+     transform="translate(0,203.43349)"
+     target="_parent"
+     id="svg_18-0">
+    <path
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
+       inkscape:connector-curvature="0"
+       id="svg_13-4"
+       d="m 159.19948,466.38321 v 0 c 0,-4.79227 3.8849,-8.67719 8.67717,-8.67719 h 176.94488 c 2.30133,0 4.50839,0.91422 6.13568,2.54151 1.62729,1.62729 2.54148,3.83435 2.54148,6.13568 v 34.70865 c 0,4.79226 -3.88489,8.67718 -8.67716,8.67718 H 167.87665 c -4.79227,0 -8.67717,-3.88492 -8.67717,-8.67718 z" />
+    <text
+       style="font-size:20.66209984px;line-height:0%;font-family:Arial;fill:#000000"
+       font-size="20.6621px"
+       id="e2_texte-0"
+       y="490.73755"
+       x="203.49075">networking</text>
+  </a>
+  <image
+     y="321.76834"
+     x="109.18641"
+     id="image3516"
+     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII= "
+     style="image-rendering:optimizeSpeed"
+     preserveAspectRatio="none"
+     height="2.8571429"
+     width="51.428574" />
+  <image
+     y="483.08792"
+     x="109.42226"
+     id="image3527"
+     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII= "
+     style="image-rendering:optimizeSpeed"
+     preserveAspectRatio="none"
+     height="2.8571429"
+     width="51.428574" />
+  <image
+     y="687.1712"
+     x="109.42944"
+     id="image3538"
+     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII= "
+     style="image-rendering:optimizeSpeed"
+     preserveAspectRatio="none"
+     height="2.8571429"
+     width="51.428574" />
+  <a
+     xlink:href="../api_docs/cloud/services.html#snapshotservice"
+     id="a277"
+     target="_parent"
+     transform="translate(0,17.42911)" />
+  <a
+     xlink:href="../api_docs/cloud/services.html#bucketservice"
+     id="a316"
+     target="_parent"
+     transform="translate(-1,438.85381)">
+    <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"
+       inkscape:connector-curvature="0"
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000" />
+    <text
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text314"
+       y="191.35905"
+       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">buckets</text>
+  </a>
   <path
-     style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
      inkscape:connector-curvature="0"
-     id="svg_122-9"
-     d="m 259.6368,767.05135 100.09451,0" />
+     inkscape:connector-type="polyline"
+     id="path318"
+     d="m 256.33097,627.01935 29.5658,-0.59455"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.73930079px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
   <path
-     style="fill-rule:evenodd;stroke:#000000;stroke-width:1.2603476;stroke-linejoin:round"
+     d="M 454.68765,743.61785 H 554.78217"
+     id="path320"
      inkscape:connector-curvature="0"
-     id="svg_123-8"
-     d="m 259.6368,767.05141 153.55383,0" />
+     style="fill:#000000;fill-opacity:0;fill-rule:nonzero" />
   <path
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1"
+     d="m 454.68765,743.61791 h 96.66739"
+     id="path322"
      inkscape:connector-curvature="0"
-     id="svg_124-2"
-     d="m 413.80937,767.05141 -1.12457,1.12458 3.08979,-1.12458 -3.08979,-1.12457 1.12457,1.12457 z" />
+     style="fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
   <path
-     style="fill:#000000;fill-opacity:0;fill-rule:nonzero"
+     d="m 551.35504,743.61791 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 z"
+     id="path324"
      inkscape:connector-curvature="0"
-     id="svg_125-7"
-     d="m 259.6368,809.48867 100.09451,0" />
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
   <path
-     style="fill-rule:evenodd;stroke:#000000;stroke-width:1.27151763;stroke-linejoin:round"
+     d="M 454.68765,786.05517 H 554.78217"
+     id="path326"
      inkscape:connector-curvature="0"
-     id="svg_126-3"
-     d="m 259.6368,809.48867 156.28767,0" />
+     style="fill:#000000;fill-opacity:0;fill-rule:nonzero" />
   <path
-     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1"
+     d="m 454.68765,786.05517 h 96.66739"
+     id="path328"
      inkscape:connector-curvature="0"
-     id="svg_127-3"
-     d="m 414.94998,809.48864 -1.12457,1.12463 3.08979,-1.12463 -3.08979,-1.12457 1.12457,1.12457 z" />
-  <text
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000"
-     font-size="16.0705px"
-     id="e15_texte-1"
-     y="763.03931"
-     x="291.34485">.list() returns</text>
+     style="fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
+  <path
+     d="m 551.35504,786.05517 -1.12457,1.12463 3.08978,-1.12463 -3.08978,-1.12457 z"
+     id="path330"
+     inkscape:connector-curvature="0"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
+  <path
+     d="m 454.18765,827.56998 h 96.66739"
+     id="path332"
+     inkscape:connector-curvature="0"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round" />
+  <path
+     d="m 552.35504,827.56998 -1.12457,1.12458 3.08978,-1.12458 -3.08978,-1.12457 z"
+     id="path334"
+     inkscape:connector-curvature="0"
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000" />
+  <a
+     xlink:href="../api_docs/cloud/resources.html#router"
+     transform="translate(99.999997,185.71424)"
+     id="a352"
+     target="_parent">
+    <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"
+       inkscape:connector-curvature="0"
+       style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000" />
+    <text
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text350"
+       y="648.35571"
+       x="539.93567"
+       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">Router</text>
+  </a>
+  <a
+     xlink:href="../api_docs/cloud/resources.html#subnet"
+     id="a358"
+     target="_parent"
+     transform="translate(0,202.8571)">
+    <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"
+       inkscape:connector-curvature="0"
+       style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000" />
+    <text
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text356"
+       y="589.69812"
+       x="638.93573"
+       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">Subnet</text>
+  </a>
   <a
      xlink:href="../api_docs/cloud/resources.html#network"
-     transform="translate(0,226.29061)"
+     id="a364"
      target="_parent"
-     id="svg_18-0">
+     transform="translate(0,202.8571)">
     <path
-       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
+       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"
        inkscape:connector-curvature="0"
-       id="svg_13-4"
-       d="m 159.19948,466.38321 0,0 c 0,-4.79227 3.8849,-8.67719 8.67717,-8.67719 l 176.94488,0 c 2.30133,0 4.50839,0.91422 6.13568,2.54151 1.62729,1.62729 2.54148,3.83435 2.54148,6.13568 l 0,34.70865 c 0,4.79226 -3.88489,8.67718 -8.67716,8.67718 l -176.94488,0 c -4.79227,0 -8.67717,-3.88492 -8.67717,-8.67718 l 0,-34.70865 z" />
+       style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000" />
     <text
-       style="font-size:20.66209984px;font-family:Arial;fill:#000000"
-       font-size="20.6621px"
-       id="e2_texte-0"
-       y="490.73755"
-       x="203.49075">network</text>
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text362"
+       y="547.26074"
+       x="631.646"
+       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">Network</text>
   </a>
+  <path
+     inkscape:connector-curvature="0"
+     inkscape:connector-type="polyline"
+     id="path366"
+     d="m 256.91071,744.92595 28.9859,-0.59907"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.73478723px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  <path
+     inkscape:connector-curvature="0"
+     inkscape:connector-type="polyline"
+     id="path368"
+     d="m 256.33097,787.01933 29.5658,-0.59455"
+     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.73930079px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  <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,829.87646 29.5658,-0.59455"
+     id="path378"
+     inkscape:connector-type="polyline"
+     inkscape:connector-curvature="0" />
   <a
-     xlink:href="../api_docs/cloud/services.html#cloudbridge.cloud.interfaces.services.NetworkService.subnets"
-     transform="translate(-135.51234,225.97254)"
+     xlink:href="../api_docs/cloud/services.html#networkservice"
+     id="a396"
      target="_parent"
-     id="svg_38-3">
+     transform="translate(0,517.14261)">
     <path
-       style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
+       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"
        inkscape:connector-curvature="0"
-       id="svg_58-7"
-       d="m 554.79394,571.91199 0,0 c 0,-3.11658 2.52649,-5.64301 5.64307,-5.64301 l 156.99738,0 c 1.49664,0 2.93194,0.59448 3.99023,1.65277 1.05829,1.05829 1.65283,2.49359 1.65283,3.99024 l 0,22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 l 0,-22.5722 z" />
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text394"
+       y="231.1214"
+       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">networks</text>
+  </a>
+  <a
+     xlink:href="../api_docs/cloud/services.html#subnetservice"
+     transform="translate(0,559.99971)"
+     target="_parent"
+     id="a406">
+    <path
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
+       inkscape:connector-curvature="0"
+       id="path402"
+       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="638.93573"
-       y="589.69812"
-       id="e32_5-8"
+       x="370.54593"
+       y="231.1214"
+       id="text404"
        font-size="18.3663px"
        xml:space="preserve">subnets</text>
   </a>
   <a
-     xlink:href="../api_docs/cloud/services.html#networkservice"
-     transform="translate(-137.05743,225.46801)"
+     xlink:href="../api_docs/cloud/services.html#routerservice"
+     transform="translate(0,602.85684)"
      target="_parent"
-     id="svg_39-0">
+     id="a422">
+    <path
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000"
+       inkscape:connector-curvature="0"
+       id="path418"
+       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="text420"
+       font-size="18.3663px"
+       xml:space="preserve">routers</text>
+  </a>
+  <path
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-linejoin:round"
+     inkscape:connector-curvature="0"
+     id="path424"
+     d="m 454.18765,870.42705 h 96.66739" />
+  <path
+     style="fill:#000000;fill-rule:evenodd;stroke:#000000"
+     inkscape:connector-curvature="0"
+     id="path426"
+     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"
+     id="a432"
+     transform="translate(99.999997,228.57131)">
     <path
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000"
        inkscape:connector-curvature="0"
-       id="svg_55-9"
-       d="m 554.79394,529.47467 0,0 c 0,-3.11658 2.52649,-5.64307 5.64307,-5.64307 l 156.99738,0 c 1.49664,0 2.93194,0.59455 3.99023,1.65284 1.05829,1.05828 1.65283,2.49359 1.65283,3.99023 l 0,22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 l 0,-22.5722 z" />
+       id="path428"
+       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" />
     <text
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none"
+       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"
        stroke-linecap="null"
-       x="631.646"
-       y="547.26074"
-       id="e31_5-1"
+       x="539.93567"
+       y="648.35571"
+       id="text430"
        font-size="18.3663px"
-       xml:space="preserve">Network</text>
+       xml:space="preserve">FloatingIP</text>
   </a>
   <path
-     style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.9934023px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-     d="m 258.91223,736.82348 0.16792,73.9978"
-     id="path3922-6-4-3-7"
+     inkscape:connector-curvature="0"
      inkscape:connector-type="polyline"
-     inkscape:connector-curvature="0" />
+     id="path434"
+     d="m 256.33097,872.73353 29.5658,-0.59455"
+     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/resources.html#subnet"
-     id="a3500"
+     xlink:href="../api_docs/cloud/services.html#floatingipservice"
+     id="a440"
      target="_parent"
-     transform="translate(128.63104,225.79059)">
+     transform="translate(0,645.71391)">
+    <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"
+       inkscape:connector-curvature="0"
+       style="fill:#ffe599;fill-rule:nonzero;stroke:#000000" />
+    <text
+       xml:space="preserve"
+       font-size="18.3663px"
+       id="text438"
+       y="231.1214"
+       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>
+  </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" />
+  <a
+     xlink:href="../api_docs/cloud/resources.html#internetgateway"
+     transform="translate(99.999997,271.42841)"
+     id="a450"
+     target="_parent">
     <path
-       d="m 554.79394,571.91199 0,0 c 0,-3.11658 2.52649,-5.64301 5.64307,-5.64301 l 156.99738,0 c 1.49664,0 2.93194,0.59448 3.99023,1.65277 1.05829,1.05829 1.65283,2.49359 1.65283,3.99024 l 0,22.5722 c 0,3.11658 -2.52649,5.64301 -5.64306,5.64301 l -156.99738,0 0,0 c -3.11658,0 -5.64307,-2.52643 -5.64307,-5.64301 l 0,-22.5722 z"
-       id="path3502"
+       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"
        inkscape:connector-curvature="0"
        style="fill:#b6d7a8;fill-rule:nonzero;stroke:#000000" />
     <text
        xml:space="preserve"
        font-size="18.3663px"
-       id="text3504"
-       y="589.69812"
-       x="638.93573"
+       id="text448"
+       y="648.35571"
+       x="539.93567"
        stroke-linecap="null"
        stroke-linejoin="null"
-       style="font-size:18.36630058px;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none">Subnet</text>
+       style="font-size:18.36630058px;line-height:0%;font-family:Arial;text-anchor:middle;fill:#000000;stroke-width:0;stroke-dasharray:none">InternetGateway</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">
+    <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
-     d="m 586.25759,809.48868 95.04957,0"
-     id="path3506"
      inkscape:connector-curvature="0"
-     style="fill-rule:evenodd;stroke:#000000;stroke-width:0.9915967;stroke-linejoin:round" />
-  <text
-     x="588.67493"
-     y="805.78864"
-     id="text3508"
-     font-size="16.0705px"
-     style="font-size:16.07049942px;font-family:Arial;fill:#000000">.list() returns</text>
-  <image
-     y="321.76834"
-     x="109.18641"
-     id="image3516"
-     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU
-GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz
-y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII=
-"
-     style="image-rendering:optimizeSpeed"
-     preserveAspectRatio="none"
-     height="2.8571429"
-     width="51.428574" />
-  <image
-     y="483.08792"
-     x="109.42226"
-     id="image3527"
-     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU
-GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz
-y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII=
-"
-     style="image-rendering:optimizeSpeed"
-     preserveAspectRatio="none"
-     height="2.8571429"
-     width="51.428574" />
-  <image
-     y="710.02814"
-     x="109.36291"
-     id="image3538"
-     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU
-GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz
-y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII=
-"
-     style="image-rendering:optimizeSpeed"
-     preserveAspectRatio="none"
-     height="2.8571429"
-     width="51.428574" />
-  <image
-     y="637.32806"
-     x="110.12805"
-     id="image3549"
-     xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAACCAYAAAA96w70AAAABHNCSVQICAgIfAhkiAAAAExJREFU
-GJVjTE9P/////3+G////M3z48IEBBhgZGRkEBAQYvn37xvDz50+4OBcXFwMbGxtV1drZ2TmzMDMz
-y5mYmPxITEz8wTBw4CsAF8MozPDMVGsAAAAASUVORK5CYII=
-"
-     style="image-rendering:optimizeSpeed"
-     preserveAspectRatio="none"
-     height="2.8571429"
-     width="51.428574" />
+     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" />
 </svg>

+ 52 - 23
docs/getting_started.rst

@@ -11,6 +11,9 @@ CloudBridge is available on PyPI so to install the latest available version,
 run::
 
     pip install --upgrade cloudbridge
+    
+For common issues during setup, check the following section:
+`Common Setup Issues <topics/troubleshooting.html>`
 
 Create a provider
 -----------------
@@ -69,10 +72,10 @@ Once you have a reference to a provider, explore the cloud platform:
 
 .. code-block:: python
 
-    provider.compute.images.list()
     provider.security.security_groups.list()
-    provider.block_store.snapshots.list()
-    provider.object_store.list()
+    provider.compute.vm_types.list()
+    provider.storage.snapshots.list()
+    provider.storage.buckets.list()
 
 This will demonstrate the fact that the library was properly installed and your
 provider object is setup correctly but it is not very interesting. Therefore,
@@ -91,19 +94,35 @@ on disk as a read-only file.
     import os
     os.chmod('cloudbridge_intro.pem', 0400)
 
-Create a security group
+Create a network
+----------------
+A cloudbridge instance should be launched into a private subnet. We'll create
+a private network and subnet, and make sure it has internet connectivity, by
+attaching an internet gateway to the subnet via a router.
+
+.. code-block:: python
+
+    net = self.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')
+    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)
+    router.attach_gateway(gateway)
+
+
+Create a VM firewall
 -----------------------
-Next, we need to create a security group and add a rule to allow ssh access.
-A security group needs to be associated with a private network, so we'll also
-need to fetch it.
+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.
 
 .. code-block:: python
 
-    provider.network.list()  # Find a desired network ID
-    net = provider.network.get('desired network ID')
-    sg = provider.security.security_groups.create(
-        'cloudbridge_intro', 'A security group used by CloudBridge', net.id)
-    sg.add_rule('tcp', 22, 22, '0.0.0.0/0')
+    net = provider.networking.networks.get('desired network ID')
+    fw = provider.security.vm_firewalls.create(
+        'cloudbridge-intro', 'A VM firewall used by CloudBridge', net.id)
+    fw.rules.create(TrafficDirection.INBOUND, 'tcp', 22, 22, '0.0.0.0/0')
 
 Launch an instance
 ------------------
@@ -114,18 +133,27 @@ also add the network interface as a launch argument.
 .. code-block:: python
 
     img = provider.compute.images.get(image_id)
-    inst_type = sorted([t for t in provider.compute.instance_types.list()
-                        if t.vcpus >= 2 and t.ram >= 4],
-                       key=lambda x: x.vcpus*x.ram)[0]
+    vm_type = sorted([t for t in provider.compute.vm_types
+                      if t.vcpus >= 2 and t.ram >= 4],
+                      key=lambda x: x.vcpus*x.ram)[0]
     inst = provider.compute.instances.create(
-        name='CloudBridge-intro', image=img, instance_type=inst_type,
-        network=net, key_pair=kp, security_groups=[sg])
+        name='cloudbridge-intro', image=img, vm_type=vm_type,
+        subnet=subnet, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
     # Show instance state
     inst.state
     # 'running'
 
+.. note ::
+
+   Note that we iterated through provider.compute.vm_types directly
+   instead of calling provider.compute.vm_types.list(). This is
+   because we need to iterate through all records in this case. The list()
+   method may not always return all records, depending on the global limit
+   for records, necessitating that additional records be paged in. See
+   :doc:`topics/paging_and_iteration`.
+
 Assign a public IP address
 --------------------------
 To access the instance, let's assign a public IP address to the instance. For
@@ -134,8 +162,8 @@ and then associate it with the instance.
 
 .. code-block:: python
 
-    fip = provider.network.create_floating_ip()
-    inst.add_floating_ip(fip.public_ip)
+    fip = provider.networking.floating_ips.create()
+    inst.add_floating_ip(fip)
     inst.refresh()
     inst.public_ips
     # [u'54.166.125.219']
@@ -151,14 +179,15 @@ To wrap things up, let's clean up all the resources we have created
 
     inst.terminate()
     from cloudbridge.cloud.interfaces import InstanceState
-    inst.wait_for([InstanceState.TERMINATED, InstanceState.UNKNOWN],
+    inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
                    terminal_states=[InstanceState.ERROR])  # Blocking call
     fip.delete()
-    sg.delete()
+    fw.delete()
     kp.delete()
     os.remove('cloudbridge_intro.pem')
-    router.remove_route(sn.id)
-    router.detach_network()
+    router.detach_gateway(gateway)
+    router.detach_subnet(subnet)
+    gateway.delete()
     router.delete()
     sn.delete()
     net.delete()

+ 5 - 5
docs/topics/block_storage.rst

@@ -14,11 +14,11 @@ performed via the :class:`.VolumeService`. To start, let's create a 1GB volume.
 
 .. code-block:: python
 
-    vol = provider.block_store.volumes.create('CloudBridge-vol', 1, 'us-east-1e')
+    vol = provider.storage.volumes.create('cloudbridge-vol', 1, 'us-east-1e')
     vol.wait_till_ready()
-    provider.block_store.volumes.list()
+    provider.storage.volumes.list()
 
-Next, let's attach the volume to a running instance as device ``/dev/sdh``::
+Next, let's attach the volume to a running instance as device ``/dev/sdh``:
 
     vol.attach('i-dbf37022', '/dev/sdh')
     vol.refresh()
@@ -54,8 +54,8 @@ long time for a snapshot to become ready, particularly on AWS.
 
 In order to make use of a snapshot, it is necessary to create a volume from it::
 
-    vol = provider.block_store.volumes.create(
-        'CloudBridge-snap-vol', 1, 'us-east-1e', snapshot=snap)
+    vol = provider.storage.volumes.create(
+        'cloudbridge-snap-vol', 1, 'us-east-1e', snapshot=snap)
 
 The newly created volume behaves just like any other volume and can be attached
 to an instance for use.

+ 39 - 22
docs/topics/launch.rst

@@ -15,21 +15,31 @@ and 4 GB RAM.
 .. code-block:: python
 
     img = provider.compute.images.get('ami-f4cc1de2')  # Ubuntu 16.04 on AWS
-    inst_type = sorted([t for t in provider.compute.instance_types.list()
-                        if t.vcpus >= 2 and t.ram >= 4],
-                       key=lambda x: x.vcpus*x.ram)[0]
+    vm_type = sorted([t for t in provider.compute.vm_types
+                      if t.vcpus >= 2 and t.ram >= 4],
+                      key=lambda x: x.vcpus*x.ram)[0]
+
+In addition, CloudBridge instances must be launched into a private subnet.
+While it is possible to create complex network configurations as shown in the
+`Private networking`_ section, if you don't particularly care where the
+instance is launched, CloudBridge provides a convenience function to quickly
+obtain a default subnet for use.
+
+.. code-block:: python
+
+    subnet = provider.networking.subnets.get_or_create_default()
 
 When launching an instance, you can also specify several optional arguments
-such as the security group, a key pair, or instance user data. To allow you to
-connect to the launched instances, we will also supply those parameters (note
-that we're making an assumption here these resources exist; if you don't have
-those resources under your account, take a look at the
+such as the firewall (a.k.a security group), a key pair, or instance user data.
+To allow you to connect to the launched instances, we will also supply those
+parameters (note that we're making an assumption here these resources exist;
+if you don't have those resources under your account, take a look at the
 `Getting Started <../getting_started.html>`_ guide).
 
 .. code-block:: python
 
     kp = provider.security.key_pairs.find(name='cloudbridge_intro')[0]
-    sg = provider.security.security_groups.list()[0]
+    fw = provider.security.vm_firewalls.list()[0]
 
 Launch an instance
 ------------------
@@ -38,24 +48,31 @@ Once we have all the desired pieces, we'll use them to launch an instance:
 .. code-block:: python
 
     inst = provider.compute.instances.create(
-        name='CloudBridge-VPC', image=img, instance_type=inst_type,
-        key_pair=kp, security_groups=[sg])
+        name='cloudbridge-vpc', image=img, vm_type=vm_type,
+        subnet=subnet, key_pair=kp, vm_firewalls=[fw])
 
 Private networking
 ~~~~~~~~~~~~~~~~~~
 Private networking gives you control over the networking setup for your
 instance(s) and is considered the preferred method for launching instances. To
-launch an instance with an explicit private network, supply a subnet within
-a network as an additional argument to the ``create`` method:
+launch an instance with an explicit private network, you can create a custom
+network and make sure it has internet connectivity. You can then launch into
+that subnet.
 
 .. code-block:: python
 
-    provider.network.list()  # Find a desired network ID
-    net = provider.network.get('desired network ID')
-    sn = net.subnets()[0]  # Get a handle on the desired subnet to launch with
+    net = self.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')
+    # 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)
+    router.attach_gateway(gateway)
+
     inst = provider.compute.instances.create(
-        name='CloudBridge-VPC', image=img, instance_type=inst_type,
-        subnet=sn, key_pair=kp, security_groups=[sg])
+        name='cloudbridge-vpc', image=img, vm_type=vm_type,
+        subnet=sn, key_pair=kp, vm_firewalls=[fw])
 
 For more information on how to create and setup a private network, take a look
 at `Networking <./networking.html>`_.
@@ -76,8 +93,8 @@ refer to :class:`.LaunchConfig`.
     lc = provider.compute.instances.create_launch_config()
     lc.add_volume_device(source=img, size=11, is_root=True)
     inst = provider.compute.instances.create(
-        name='CloudBridge-BDM', image=img,  instance_type=inst_type,
-        launch_config=lc, key_pair=kp, security_groups=[sg])
+        name='cloudbridge-bdm', image=img,  vm_type=vm_type,
+        launch_config=lc, key_pair=kp, vm_firewalls=[fw])
 
 where ``img`` is the :class:`.Image` object to use for the root volume.
 
@@ -97,10 +114,10 @@ assign a floating IP address to your instance. This can be done as follows:
 
 .. code-block:: python
 
-    # List all the IP addresses and find the desired one
-    provider.network.floating_ips()
+    # Create a new floating IP address
+    fip = provider.networking.floating_ips.create()
     # Assign the desired IP to the instance
-    inst.add_floating_ip('149.165.168.143')
+    inst.add_floating_ip(fip)
     inst.refresh()
     inst.public_ips
     # [u'149.165.168.143']

+ 86 - 25
docs/topics/networking.rst

@@ -3,6 +3,7 @@ Private networking
 Private networking gives you control over the networking setup for your
 instance(s) and is considered the preferred method for launching instances.
 Also, providers these days are increasingly requiring use of private networks.
+All CloudBridge deployed VMs must be deployed into a particular subnet.
 
 If you do not explicitly specify a private network to use when launching an
 instance, CloudBridge will attempt to use a default one. A 'default' network is
@@ -11,40 +12,100 @@ exist, CloudBridge will look for one with a predefined name (by default, called
 'CloudBridgeNet', which can be overridden with environment variable
 ``CB_DEFAULT_NETWORK_NAME``).
 
-Create a new private network
-----------------------------
+Once a VM is deployed, cloudbridge's networking capabilities must address
+several common scenarios.
+
+1. Allowing internet access from a launched VM
+
+   In the simplest scenario, a user may simply want to launch an instance and
+   allow the instance to access the internet.
+
+
+2. Allowing internet access to a launched VM
+
+   Alternatively, the user may want to allow the instance to be contactable
+   from the internet. In a more complex scenario, a user may want to deploy
+   VMS into several subnets, and deploy a gateway, jump host or bastion host
+   to access other VMs which are not directly connected to the internet. In
+   the latter scenario, the gateway/jump host/bastion host will need to be
+   contactable over the internet.
+
+
+3. Secure access between subnets for n-tier applications
+
+   In this third scenario, a multi-tier app may be deployed into several
+   subnets depending on their tier. For example, consider the following
+   scenario:
+
+   - Tier 1/Subnet 1 - Web Server Needs to be externally accessible over the
+     internet. However, in this particular scenario, the web server itself does
+     not need access to the internet.
+
+   - Tier 2/Subnet 2 - Application Server The Application server must only be
+     able to communicate with the database server in Subnet 3, and receive
+     communication from the Web Server in Subnet 1. However, we assume a
+     special case here where the application server needs to access the
+     internet.
+
+   - Tier 3/Subnet 3 - Database Server The database server must only be able to
+     receive incoming traffic from Tier 2, but must not be able to make
+     outgoing traffic outside of its subnet.
+
+    At present, CloudBridge does not provide support for this scenario,
+    primarily because OpenStack's FwaaS (Firewall-as-a-Service) is not widely
+    available.
+
+1. Allowing internet access from a launched VM
+----------------------------------------------
 Creating a private network is a simple, one-line command but appropriately
-connecting it so it has Internet access is a multi-step process:
+connecting it so that it has uniform Internet access across all providers
+is a multi-step process:
 (1) create a network; (2) create a subnet within this network; (3) create a
-router; (4) attach the router to an external network; and (5) add a route to
-the router that links with a subnet. For some providers, any network can
-be external (ie, connected to the Internet) while for others it's a specific,
-pre-defined one that exists in the an account by default. In order to properly
-connect the router, we need to ensure we're using an external network.
+router; (4) attach the router to the subnet and (5) attach the router to the
+internet gateway.
 
-When creating the subnet, we need to set an address pool. We can obtain the
-private network address space via network object's ``cidr_block`` field (e.g.,
-``10.0.0.0/16``). Below, we'll create a subnet starting from the beginning of
-the block and allow up to 16 IP addresses into the subnet (``/28``).
+When creating a network, we need to set an address pool. Any subsequent
+subnets you create must have a CIDR block that falls within the parent
+network's CIDR block. Below, we'll create a subnet starting from the beginning
+of the block and allow up to 16 IP addresses within a subnet (``/28``).
 
 .. code-block:: python
 
-    net = provider.network.create('cloudbridge_intro')
-    sn = net.create_subnet('10.0.0.0/28', 'cloudbridge-intro')
-    router = provider.network.create_router('cloudbridge-intro')
-    if not net.external:
-        for n in self.provider.network.list():
-            if n.external:
-                net = n
-                break
-    router.attach_network(net.id)
-    router.add_route(sn.id)
+    net = self.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.attach_subnet(sn)
+    gateway = self.provider.networking.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.
+
+.. code-block:: python
+
+    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)
+
+    vm = provider.compute.instances.create('my-inst', subnet=sn, ...)
+
+    router = provider.networking.routers.create(network=net, name='my-router')
+    router.attach_subnet(sn)
+    gateway = provider.networking.gateways.get_or_create_inet_gateway(name)
+    router.attach_gateway(gateway)
+
+    fip = provider.networking.networks.create_floating_ip()
+    vm.add_floating_ip(fip)
+
 
 Retrieve an existing private network
 ------------------------------------
-If you already have existing networks, we can query for those:
+If you already have existing networks, we can query for it:
 
 .. code-block:: python
 
-    provider.network.list()  # Find a desired network ID
-    net = provider.network.get('desired network ID')
+    provider.networking.networks.list()  # Find a desired network ID
+    net = provider.networking.networks.get('desired network ID')

+ 3 - 3
docs/topics/object_lifecycles.rst

@@ -31,14 +31,14 @@ follows:
 
     self.wait_for(
         [InstanceState.RUNNING],
-        terminal_states=[InstanceState.TERMINATED, InstanceState.ERROR],
+        terminal_states=[InstanceState.DELETED, InstanceState.ERROR],
         timeout=timeout,
         interval=interval)
 
 This would cause the wait_for method to repeatedly call refresh() till the
 object's state reaches RUNNING. It will raise a :class:`WaitStateException`
 if the timeout expires, or the object reaches a terminal state, such as
-TERMINATED or ERROR, in which case it is no longer reasonable to wait for the
+DELETED or ERROR, in which case it is no longer reasonable to wait for the
 object to reach a running state.
 
 Informational states and actionable states
@@ -76,7 +76,7 @@ CONFIGURING           informational   Instance is being reconfigured in some
                                       way and may not be usable.
 RUNNING               actionable      Instance is running.
 REBOOTING             informational   Instance is rebooting.
-TERMINATED            actionable      Instance is terminated. No further
+DELETED               actionable      Instance is deleted. No further
                                       operations possible.
 STOPPED               actionable      Instance is stopped. Instance can be
                                       resumed.

+ 70 - 0
docs/topics/object_storage.rst

@@ -0,0 +1,70 @@
+Working with object storage
+==========================
+Object storage provides a simple way to store and retrieve large amounts of
+unstructured data over HTTP. Object Storage is also referred to as Blob (Binary
+Large OBject) Storage by Azure, and Simple Storage Service (S3) by Amazon.
+
+Typically, you would store your objects within a Bucket, as it is known in
+AWS and GCE. A Bucket is also called a Container in OpenStack and Azure. In
+CloudBridge, we use the term Bucket.
+
+Storing objects in a bucket
+---------------------------
+To store an object within a bucket, we need to first create a bucket or
+retrieve an existing bucket.
+
+.. code-block:: python
+
+    bucket = provider.storage.buckets.create('my-bucket')
+    bucket.objects.list()
+
+Next, let's upload some data to this bucket. To efficiently upload a file,
+simple use the upload_from_file method.
+
+.. code-block:: python
+
+    obj = bucket.objects.create('my-data.txt')
+    obj.upload_from_file('/path/to/myfile.txt')
+
+You can also use the upload() function to upload from an in memory stream.
+Note that, an object you create with objects.create() doesn't actually get
+persisted until you upload some content.
+
+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]
+    print("Size: {0}, Modified: {1}".format(obj.size, obj.last_modified))
+    with open('/tmp/myfile.txt', 'wb') as f:
+        obj.save_content(f)
+ 
+
+Using tokens for authentication
+-------------------------------
+Some providers may support using temporary credentials with a session token,
+in which case you will be able to access a particular bucket by using that
+session token.
+
+.. code-block:: python
+
+    provider = CloudProviderFactory().create_provider(
+        ProviderList.AWS,
+        {'aws_access_key': 'ACCESS_KEY',
+         'aws_secret_key': 'SECRET_KEY',
+         'aws_session_token': 'MY_SESSION_TOKEN'})
+.. code-block:: python
+
+    provider = CloudProviderFactory().create_provider(
+        ProviderList.OPENSTACK,
+        {'os_storage_url': 'SWIFT_STORAGE_URL',
+         'os_auth_token': 'MY_SESSION_TOKEN'})
+
+Once a provider is obtained, you can access the container as usual:
+
+.. code-block:: python
+
+    bucket = provider.object_store.get(container)
+    obj = bucket.create_object('my_object.txt')
+    obj.upload_from_file(source)

+ 2 - 0
docs/topics/overview.rst

@@ -12,4 +12,6 @@ Introductions to all the key parts of CloudBridge you'll need to know:
     Object states and lifecycles <object_lifecycles.rst>
     Paging and iteration <paging_and_iteration.rst>
     Using block storage <block_storage.rst>
+    Using object storage <object_storage.rst>
+    Troubleshooting <troubleshooting.rst>
 

+ 2 - 7
docs/topics/provider_development.rst

@@ -57,7 +57,7 @@ You should see the tests fail with the following message:
 .. code-block:: bash
 
     TypeError: Can't instantiate abstract class GCECloudProvider with abstract
-    methods block_store, compute, object_store, security, network
+    methods storage, compute, security, network
 
 6. Therefore, our next step is to implement these methods. We can start off by
 implementing these methods in ``provider.py`` and raising a
@@ -81,12 +81,7 @@ implementing these methods in ``provider.py`` and raising a
             "GCECloudProvider does not implement this service")
 
     @property
-    def block_store(self):
-        raise NotImplementedError(
-            "GCECloudProvider does not implement this service")
-
-    @property
-    def object_store(self):
+    def storage(self):
         raise NotImplementedError(
             "GCECloudProvider does not implement this service")
 

+ 22 - 0
docs/topics/troubleshooting.rst

@@ -0,0 +1,22 @@
+Common Setup Issues
+===================
+
+macOS Issues
+------------
+
+* If you are getting an error message like so: ``Authentication with cloud provider failed: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749)``
+  then this indicates that you are probably using a newer version of Python on
+  macOS. Starting with Python 3.6, the Python installer includes its own version
+  of OpenSSL and it no longer uses the system trusted certificate keychains.
+
+  Python 3.6 includes a script that can install a bundle of root certificates
+  from ``certifi``.  To install this bundle execute the following:
+
+  .. code-block:: bash
+
+    cd /Applications/Python\ 3.6/
+    sudo ./Install\ Certificates.command
+
+  For more information see `this StackOverflow
+  answer <https://stackoverflow.com/a/42583411/1419499>`_ and the `Python 3.6
+  Release Notes <https://www.python.org/downloads/release/python-360/>`_.

+ 61 - 49
setup.py

@@ -1,4 +1,7 @@
-"""Library install script for setuptools."""
+"""
+Package install information
+"""
+
 import ast
 import os
 import re
@@ -15,53 +18,62 @@ with open(os.path.join('cloudbridge', '__init__.py')) as f:
             version = ast.literal_eval(m.group(1))
             break
 
-base_reqs = ['bunch>=1.0.1', 'six>=1.10.0', 'retrying>=1.3.3']
-openstack_reqs = ['requests',
-                  'Babel',
-                  'python-novaclient==7.0.0',
-                  'python-glanceclient>=2.5.0,<=2.6.0',
-                  'python-cinderclient>=1.9.0,<=2.0.1',
-                  'python-swiftclient>=3.2.0,<=3.3.0',
-                  'python-neutronclient>=6.0.0,<=6.1.0',
-                  'python-keystoneclient>=3.8.0,<=3.10.0']
-aws_reqs = ['boto>=2.38.0,<=2.46.1']
-gce_reqs = ['google-api-python-client>=1.4.2', "cryptography>=1.4"]
-full_reqs = base_reqs + aws_reqs + openstack_reqs + gce_reqs
+REQS_BASE = [
+    'bunch>=1.0.1',
+    'six>=1.10.0',
+    'retrying>=1.3.3'
+]
+REQS_AWS = ['boto3']
+REQS_OPENSTACK = [
+    'openstacksdk',
+    'python-novaclient>=7.0.0',
+    'python-glanceclient>=2.5.0',
+    'python-cinderclient>=1.9.0',
+    'python-swiftclient>=3.2.0',
+    'python-neutronclient>=6.0.0',
+    'python-keystoneclient>=3.13.0'
+]
+REQS_FULL = REQS_BASE + REQS_AWS + REQS_OPENSTACK
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
-dev_reqs = (full_reqs + ['tox>=2.1.1', 'moto<1.0.0', 'sphinx>=1.3.1',
-            'flake8>=3.3.0', 'flake8-import-order>=0.12', 'httpretty==0.8.10'])
+REQS_DEV = ([
+    'tox>=2.1.1',
+    'moto>=1.1.11',
+    'sphinx>=1.3.1',
+    'flake8>=3.3.0',
+    'flake8-import-order>=0.12'] + REQS_FULL
+)
 
-setup(name='cloudbridge',
-      version=version,
-      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/',
-      install_requires=full_reqs,
-      extras_require={
-          ':python_version=="2.7"': ['py2-ipaddress'],
-          ':python_version=="3"': ['py2-ipaddress'],
-          'full': full_reqs,
-          'dev': dev_reqs
-      },
-      packages=find_packages(),
-      license='MIT',
-      classifiers=[
-          'Development Status :: 4 - Beta',
-          'Environment :: Console',
-          'Intended Audience :: Developers',
-          'Intended Audience :: System Administrators',
-          'License :: OSI Approved :: MIT License',
-          'Operating System :: OS Independent',
-          'Programming Language :: Python',
-          'Topic :: Software Development :: Libraries :: Python Modules',
-          'Programming Language :: Python :: 2.7',
-          'Programming Language :: Python :: 3',
-          'Programming Language :: Python :: 3.4',
-          'Programming Language :: Python :: 3.5',
-          'Programming Language :: Python :: 3.6',
-          'Programming Language :: Python :: Implementation :: CPython',
-          'Programming Language :: Python :: Implementation :: PyPy'],
-      test_suite="test"
-      )
+setup(
+    name='cloudbridge',
+    version=version,
+    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/',
+    install_requires=REQS_FULL,
+    extras_require={
+        ':python_version=="2.7"': ['py2-ipaddress'],
+        ':python_version=="3"': ['py2-ipaddress'],
+        'full': REQS_FULL,
+        'dev': REQS_DEV
+    },
+    packages=find_packages(),
+    license='MIT',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Console',
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
+        'Programming Language :: Python :: Implementation :: PyPy'],
+    test_suite="test"
+)

+ 34 - 25
test/helpers.py → test/helpers/__init__.py

@@ -1,7 +1,9 @@
 import functools
 import os
 import sys
+import traceback
 import unittest
+import uuid
 
 from contextlib import contextmanager
 
@@ -45,11 +47,13 @@ def cleanup_action(cleanup_func):
             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()
 
 
 def skipIfNoService(services):
@@ -77,18 +81,14 @@ def skipIfNoService(services):
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
         "image": os.environ.get('CB_IMAGE_AWS', 'ami-5ac2cd4d'),
-        "instance_type": os.environ.get('CB_INSTANCE_TYPE_AWS', 't2.nano'),
+        "vm_type": os.environ.get('CB_VM_TYPE_AWS', 't2.nano'),
         "placement": os.environ.get('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
     "OpenStackCloudProvider": {
         "image": os.environ.get('CB_IMAGE_OS',
                                 '842b949c-ea76-48df-998d-8a41f2626243'),
-        "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'm1.tiny'),
-        "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
-    },
-    "GCECloudProvider": {
-        "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'f1-micro'),
-        "placement": os.environ.get('CB_PLACEMENT_GCE', 'us-central1-a'),
+        "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
+        "placement": os.environ.get('CB_PLACEMENT_OS', 'zone-r1'),
     }
 }
 
@@ -107,7 +107,8 @@ def create_test_network(provider, name):
     """
     Create a network with one subnet, returning the network and subnet objects.
     """
-    net = provider.network.create(name=name)
+    net = provider.networking.networks.create(name=name,
+                                              cidr_block='10.0.0.0/16')
     cidr_block = (net.cidr_block).split('/')[0] or '10.0.0.1'
     sn = net.create_subnet(cidr_block='{0}/28'.format(cidr_block), name=name,
                            zone=get_provider_test_data(provider, 'placement'))
@@ -119,60 +120,67 @@ def delete_test_network(network):
     Delete the supplied network, first deleting any contained subnets.
     """
     with cleanup_action(lambda: network.delete()):
-        for sn in network.subnets():
-            sn.delete()
+        for sn in network.subnets:
+            with cleanup_action(lambda: sn.delete()):
+                pass
 
 
 def create_test_instance(
         provider, instance_name, subnet, launch_config=None,
-        key_pair=None, security_groups=None):
+        key_pair=None, vm_firewalls=None, user_data=None):
     return provider.compute.instances.create(
         instance_name,
         get_provider_test_data(provider, 'image'),
-        get_provider_test_data(provider, 'instance_type'),
+        get_provider_test_data(provider, 'vm_type'),
         subnet=subnet,
         zone=get_provider_test_data(provider, 'placement'),
         key_pair=key_pair,
-        security_groups=security_groups,
-        launch_config=launch_config)
+        vm_firewalls=vm_firewalls,
+        launch_config=launch_config,
+        user_data=user_data)
 
 
-def get_test_instance(provider, name, key_pair=None, security_groups=None,
-                      subnet=None):
+def get_test_instance(provider, name, key_pair=None, vm_firewalls=None,
+                      subnet=None, user_data=None):
     launch_config = None
     instance = create_test_instance(
         provider,
         name,
         subnet=subnet,
         key_pair=key_pair,
-        security_groups=security_groups,
-        launch_config=launch_config)
+        vm_firewalls=vm_firewalls,
+        launch_config=launch_config,
+        user_data=user_data)
     instance.wait_till_ready()
     return instance
 
 
 def get_test_fixtures_folder():
-    return os.path.join(os.path.dirname(__file__), 'fixtures/')
+    return os.path.join(os.path.dirname(__file__), '../fixtures/')
 
 
 def delete_test_instance(instance):
     if instance:
-        instance.terminate()
-        instance.wait_for([InstanceState.TERMINATED, InstanceState.UNKNOWN],
+        instance.delete()
+        instance.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
                           terminal_states=[InstanceState.ERROR])
 
 
-def cleanup_test_resources(instance=None, network=None, security_group=None,
+def cleanup_test_resources(instance=None, network=None, vm_firewall=None,
                            key_pair=None):
     """Clean up any combination of supplied resources."""
     with cleanup_action(lambda: delete_test_network(network)
                         if network else None):
         with cleanup_action(lambda: key_pair.delete() if key_pair else None):
-            with cleanup_action(lambda: security_group.delete()
-                                if security_group else None):
+            with cleanup_action(lambda: vm_firewall.delete()
+                                if vm_firewall else None):
                 delete_test_instance(instance)
 
 
+def get_uuid():
+    return str(uuid.uuid4())
+
+
 class ProviderTestBase(unittest.TestCase):
 
     _provider = None
@@ -200,7 +208,8 @@ class ProviderTestBase(unittest.TestCase):
         provider_class = factory.get_provider_class(provider_name,
                                                     get_mock=use_mock_drivers)
         config = {'default_wait_interval':
-                  self.get_provider_wait_interval(provider_class)}
+                  self.get_provider_wait_interval(provider_class),
+                  'default_result_limit': 1}
         return provider_class(config)
 
     @property

+ 276 - 0
test/helpers/standard_interface_tests.py

@@ -0,0 +1,276 @@
+"""
+Standard tests for behaviour common across the whole of cloudbridge.
+This includes:
+   1. Checking that every resource has an id property
+   2. Checking for object equality and repr
+   3. Checking standard behaviour for list, iter, find, get, delete
+"""
+import test.helpers as helpers
+import uuid
+
+from cloudbridge.cloud.interfaces.exceptions \
+    import InvalidNameException
+from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
+from cloudbridge.cloud.interfaces.resources import ResultList
+
+
+def check_repr(test, obj):
+    test.assertTrue(
+        obj.id in repr(obj),
+        "repr(obj) for %s contain the object id so that the object"
+        " can be reconstructed, but does not. eval(repr(obj)) == obj"
+        % (type(obj).__name__,))
+
+
+def check_json(test, obj):
+    val = obj.to_json()
+    test.assertEqual(val.get('id'), obj.id)
+    test.assertEqual(val.get('name'), obj.name)
+
+
+def check_obj_properties(test, obj):
+    test.assertEqual(obj, obj, "Object should be equal to itself")
+    test.assertFalse(obj != obj, "Object inequality should be false")
+    check_obj_name(test, obj)
+
+
+def check_list(test, service, obj):
+    list_objs = service.list()
+    test.assertIsInstance(list_objs, ResultList)
+    all_records = list_objs
+    while list_objs.is_truncated:
+        list_objs = service.list(marker=list_objs.marker)
+        all_records += list_objs
+    match_objs = [o for o in all_records if o.id == obj.id]
+    test.assertTrue(
+        len(match_objs) == 1,
+        "List objects for %s does not return the expected object id %s. Got %s"
+        % (type(obj).__name__, obj.id, match_objs))
+    return match_objs
+
+
+def check_iter(test, service, obj):
+    # check iteration
+    iter_objs = list(service)
+    iter_ids = [o.id for o in service]
+    test.assertEqual(len(set(iter_ids)), len(iter_ids),
+                     "Iteration should not return duplicates")
+    match_objs = [o for o in iter_objs if o.id == obj.id]
+    test.assertTrue(
+        len(match_objs) == 1,
+        "Iter objects for %s does not return the expected object id %s. Got %s"
+        % (type(obj).__name__, obj.id, match_objs))
+    return match_objs
+
+
+def check_find(test, service, obj):
+    # check find
+    find_objs = service.find(name=obj.name)
+    test.assertTrue(
+        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)
+    return find_objs
+
+
+def check_find_non_existent(test, service):
+    # check find
+    find_objs = service.find(name="random_imagined_obj_name")
+    test.assertTrue(
+        len(find_objs) == 0,
+        "Find non-existent object for %s returned unexpected objects: %s"
+        % (type(service).__name__, find_objs))
+
+
+def check_get(test, service, obj):
+    get_obj = service.get(obj.id)
+    test.assertEqual(get_obj, obj)
+    test.assertIsInstance(get_obj, type(obj))
+    return get_obj
+
+
+def check_get_non_existent(test, service):
+    # check get
+    get_objs = service.get(str(uuid.uuid4()))
+    test.assertIsNone(
+        get_objs,
+        "Get non-existent object for %s returned unexpected objects: %s"
+        % (type(service).__name__, get_objs))
+
+
+def check_delete(test, service, obj, perform_delete=False):
+    if perform_delete:
+        obj.delete()
+
+    objs = service.list()
+    found_objs = [o for o in objs if o.id == obj.id]
+    test.assertTrue(
+        len(found_objs) == 0,
+        "Object %s in service %s should have been deleted but still exists."
+        % (found_objs, type(service).__name__))
+
+
+def check_obj_name(test, obj):
+    """
+    Cloudbridge identifiers must be 1-63 characters long, and comply with
+    RFC1035. In addition, identifiers should contain only lowercase letters,
+    numeric characters, underscores, and dashes. International
+    characters are allowed.
+    """
+
+    # if name has a setter, make sure invalid values cannot be set
+    name_property = getattr(type(obj), 'name', None)
+    if isinstance(name_property, property) and name_property.fset:
+        # setting letters, numbers and international characters should succeed
+        # TODO: Unicode characters trip up Moto. Add following: \u0D85\u0200
+        VALID_NAME = u"hello_world-123"
+        original_name = obj.name
+        obj.name = VALID_NAME
+        # setting spaces should raise an exception
+        with test.assertRaises(InvalidNameException):
+            obj.name = "hello world"
+        # setting upper case characters should raise an exception
+        with test.assertRaises(InvalidNameException):
+            obj.name = "helloWorld"
+        # setting special characters should raise an exception
+        with test.assertRaises(InvalidNameException):
+            obj.name = "hello.world:how_goes_it"
+        # setting a length > 63 should result in an exception
+        with test.assertRaises(InvalidNameException,
+                               msg="Name of length > 64 should be disallowed"):
+            obj.name = "a" * 64
+        # refreshing should yield the last successfully set name
+        obj.refresh()
+        test.assertEqual(obj.name, VALID_NAME)
+        obj.name = original_name
+
+
+def check_standard_behaviour(test, service, obj):
+    """
+    Checks standard behaviour in a given cloudbridge resource
+    of a given service.
+    """
+    check_repr(test, obj)
+    check_json(test, obj)
+    check_obj_properties(test, obj)
+    objs_list = check_list(test, service, obj)
+    objs_iter = check_iter(test, service, obj)
+    objs_find = check_find(test, service, obj)
+    check_find_non_existent(test, service)
+    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))
+
+    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))
+
+
+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
+        create_func("hello world")
+    # check create with invalid name
+    with test.assertRaises(InvalidNameException):
+        # uppercase characters should raise an exception
+        create_func("helloWorld")
+    # setting special characters should raise an exception
+    with test.assertRaises(InvalidNameException):
+        create_func("hello.world:how_goes_it")
+    # setting a length > 63 should result in an exception
+    with test.assertRaises(InvalidNameException,
+                           msg="Name of length > 64 should be disallowed"):
+        create_func("a" * 64)
+
+
+def check_crud(test, service, iface, name_prefix,
+               create_func, cleanup_func, extra_test_func=None,
+               custom_check_delete=None, skip_name_check=False):
+    """
+    Checks crud behaviour of a given cloudbridge service. The create_func will
+    be used as a factory function to create a service object and the
+    cleanup_func will be used to destroy the object. Once an object is created
+    using the create_func, all other standard behavioural tests can be run
+    against that object.
+
+    :type  test: ``TestCase``
+    :param test: The TestCase object to use
+
+    :type  service: ``CloudService``
+    :param service: The CloudService object under test. For example,
+                    a VolumeService object.
+
+    :type  iface: ``type``
+    :param iface: The type to test behaviour against. This type must be a
+                  subclass of ``CloudResource``.
+
+    :type  name_prefix: ``str``
+    :param name_prefix: The name to prefix all created objects with. This
+                        function will generated a new name with the
+                        specified name_prefix for each test object created
+                        and pass that name into the create_func
+
+    :type  create_func: ``func``
+    :param create_func: The create_func must accept the name of the object to
+                        create as a parameter and return the constructed
+                        object.
+
+    :type  cleanup_func: ``func``
+    :param cleanup_func: The cleanup_func must accept the created object
+                         and perform all cleanup tasks required to delete the
+                         object.
+
+    :type  extra_test_func: ``func``
+    :param extra_test_func: This function will be called to perform additional
+                            tests after object construction and initialization,
+                            but before object cleanup. It will receive the
+                            created object as a parameter.
+
+    :type  custom_check_delete: ``func``
+    :param custom_check_delete: If provided, this function will be called
+                                instead of the standard check_delete function
+                                to make sure that the object has been deleted.
+
+    :type  skip_name_check: ``boolean``
+    :param skip_name_check:  If True, the invalid name checking will be
+                             skipped.
+    """
+
+    obj = None
+    with helpers.cleanup_action(lambda: cleanup_func(obj)):
+        if not skip_name_check:
+            check_create(test, service, iface, name_prefix,
+                         create_func, cleanup_func)
+        name = "{0}-{1}".format(name_prefix, helpers.get_uuid())
+        obj = create_func(name)
+        if issubclass(iface, ObjectLifeCycleMixin):
+            obj.wait_till_ready()
+        check_standard_behaviour(test, service, obj)
+        if extra_test_func:
+            extra_test_func(obj)
+    if custom_check_delete:
+        custom_check_delete(obj)
+    else:
+        check_delete(test, service, obj)

+ 62 - 183
test/test_block_store_service.py

@@ -3,101 +3,47 @@ import uuid
 
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
+from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import SnapshotState
 from cloudbridge.cloud.interfaces import VolumeState
+from cloudbridge.cloud.interfaces.provider import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
+from cloudbridge.cloud.interfaces.resources import Snapshot
+from cloudbridge.cloud.interfaces.resources import Volume
 
 import six
 
 
 class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['block_store.volumes'])
+    @helpers.skipIfNoService(['storage.volumes'])
     def test_crud_volume(self):
         """
         Create a new volume, check whether the expected values are set,
         and delete it
         """
-        name = "CBUnitTestCreateVol-{0}".format(uuid.uuid4())
-        test_vol = self.provider.block_store.volumes.create(
-            name,
-            1,
-            helpers.get_provider_test_data(self.provider, "placement"))
+        def create_vol(name):
+            return self.provider.storage.volumes.create(
+                name,
+                1,
+                helpers.get_provider_test_data(self.provider, "placement"))
 
         def cleanup_vol(vol):
             vol.delete()
             vol.wait_for([VolumeState.DELETED, VolumeState.UNKNOWN],
                          terminal_states=[VolumeState.ERROR])
 
-        with helpers.cleanup_action(lambda: cleanup_vol(test_vol)):
-            test_vol.wait_till_ready()
-            self.assertTrue(
-                test_vol.id in repr(test_vol),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
-            volumes = self.provider.block_store.volumes.list()
-            list_volumes = [vol for vol in volumes if vol.name == name]
-            self.assertTrue(
-                len(list_volumes) == 1,
-                "List volumes does not return the expected volume %s" %
-                name)
-
-            # check iteration
-            iter_volumes = [vol for vol in self.provider.block_store.volumes
-                            if vol.name == name]
-            self.assertTrue(
-                len(iter_volumes) == 1,
-                "Iter volumes does not return the expected volume %s" %
-                name)
-
-            # check find
-            find_vols = self.provider.block_store.volumes.find(name=name)
-            self.assertTrue(
-                len(find_vols) == 1,
-                "Find volumes does not return the expected volume %s" %
-                name)
-
-            # check non-existent find
-            # TODO: Moto has a bug with filters causing the following test
-            # to fail. Need to add tag based filtering support for volumes
-#             find_vols = self.provider.block_store.volumes.find(
-#                 name="non_existent_vol")
-#             self.assertTrue(
-#                 len(find_vols) == 0,
-#                 "Find() for a non-existent volume returned %s" % find_vols)
+        sit.check_crud(self, self.provider.storage.volumes, Volume,
+                       "cb_createvol", create_vol, cleanup_vol)
 
-            get_vol = self.provider.block_store.volumes.get(
-                test_vol.id)
-            self.assertTrue(
-                list_volumes[0] ==
-                get_vol == test_vol,
-                "Ids returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(list_volumes[0].id,
-                                         get_vol.id,
-                                         test_vol.id))
-            self.assertTrue(
-                list_volumes[0].name ==
-                get_vol.name == test_vol.name,
-                "Names returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(list_volumes[0].name,
-                                         get_vol.name,
-                                         test_vol.name))
-        volumes = self.provider.block_store.volumes.list()
-        found_volumes = [vol for vol in volumes if vol.name == name]
-        self.assertTrue(
-            len(found_volumes) == 0,
-            "Volume %s should have been deleted but still exists." %
-            name)
-
-    @helpers.skipIfNoService(['block_store.volumes'])
+    @helpers.skipIfNoService(['storage.volumes'])
     def test_attach_detach_volume(self):
         """
         Create a new volume, and attempt to attach it to an instance
         """
-        instance_name = "CBVolOps-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        name = "cb_attachvol-{0}".format(helpers.get_uuid())
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
@@ -105,11 +51,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
             net, subnet = helpers.create_test_network(
-                self.provider, instance_name)
+                self.provider, name)
             test_instance = helpers.get_test_instance(
-                self.provider, instance_name, subnet=subnet)
-            name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
-            test_vol = self.provider.block_store.volumes.create(
+                self.provider, name, subnet=subnet)
+
+            test_vol = self.provider.storage.volumes.create(
                 name, 1, test_instance.zone_id)
             with helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
@@ -122,14 +68,12 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     [VolumeState.AVAILABLE],
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
 
-    @helpers.skipIfNoService(['block_store.volumes'])
+    @helpers.skipIfNoService(['storage.volumes'])
     def test_volume_properties(self):
         """
         Test volume properties
         """
-        instance_name = "CBVolProps-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        name = "cb_volprops-{0}".format(helpers.get_uuid())
         vol_desc = 'newvoldesc1'
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
@@ -138,12 +82,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
             net, subnet = helpers.create_test_network(
-                self.provider, instance_name)
+                self.provider, name)
             test_instance = helpers.get_test_instance(
-                self.provider, instance_name, subnet=subnet)
+                self.provider, name, subnet=subnet)
 
-            name = "CBUnitTestVolProps-{0}".format(uuid.uuid4())
-            test_vol = self.provider.block_store.volumes.create(
+            test_vol = self.provider.storage.volumes.create(
                 name, 1, test_instance.zone_id, description=vol_desc)
             with helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
@@ -185,23 +128,24 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     [VolumeState.AVAILABLE],
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
 
-    @helpers.skipIfNoService(['block_store.snapshots'])
+    @helpers.skipIfNoService(['storage.snapshots'])
     def test_crud_snapshot(self):
         """
         Create a new volume, create a snapshot of the volume, and check
         whether list_snapshots properly detects the new snapshot.
         Delete everything afterwards.
         """
-        name = "CBUnitTestCreateSnap-{0}".format(uuid.uuid4())
-        test_vol = self.provider.block_store.volumes.create(
+        name = "cb_crudsnap-{0}".format(helpers.get_uuid())
+        test_vol = self.provider.storage.volumes.create(
             name,
             1,
             helpers.get_provider_test_data(self.provider, "placement"))
         with helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
-            snap_name = "CBSnapshot-{0}".format(name)
-            test_snap = test_vol.create_snapshot(name=snap_name,
-                                                 description=snap_name)
+
+            def create_snap(name):
+                return test_vol.create_snapshot(name=name,
+                                                description=name)
 
             def cleanup_snap(snap):
                 snap.delete()
@@ -209,114 +153,33 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     [SnapshotState.UNKNOWN],
                     terminal_states=[SnapshotState.ERROR])
 
-            with helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
-                test_snap.wait_till_ready()
-                self.assertTrue(
-                    test_snap.id in repr(test_snap),
-                    "repr(obj) should contain the object id so that the object"
-                    " can be reconstructed, but does not.")
-
-                snaps = self.provider.block_store.snapshots.list()
-                list_snaps = [snap for snap in snaps
-                              if snap.name == snap_name]
-                self.assertTrue(
-                    len(list_snaps) == 1,
-                    "List snapshots does not return the expected volume %s" %
-                    name)
-
-                # check iteration
-                iter_snaps = [
-                    snap for snap in self.provider.block_store.snapshots
-                    if snap.name == snap_name]
-                self.assertTrue(
-                    len(iter_snaps) == 1,
-                    "Iter snapshots does not return the expected volume %s" %
-                    name)
-
-                # check find
-                find_snap = self.provider.block_store.snapshots.find(
-                    name=snap_name)
-                self.assertTrue(
-                    len(find_snap) == 1,
-                    "Find snaps does not return the expected snapshot %s" %
-                    name)
-
-                # check non-existent find
-                # TODO: Moto has a bug with filters causing the following test
-                # to fail. Need to add tag based filtering support for snaps
-#                 find_snap = self.provider.block_store.snapshots.find(
-#                     name="non_existent_snap")
-#                 self.assertTrue(
-#                     len(find_snap) == 0,
-#                     "Find() for a non-existent snap returned %s" %
-#                     find_snap)
-
-                get_snap = self.provider.block_store.snapshots.get(
-                    test_snap.id)
-                self.assertTrue(
-                    list_snaps[0] ==
-                    get_snap == test_snap,
-                    "Ids returned by list: {0} and get: {1} are not as "
-                    " expected: {2}" .format(list_snaps[0].id,
-                                             get_snap.id,
-                                             test_snap.id))
-                self.assertTrue(
-                    list_snaps[0].name ==
-                    get_snap.name == test_snap.name,
-                    "Names returned by list: {0} and get: {1} are not as "
-                    " expected: {2}" .format(list_snaps[0].name,
-                                             get_snap.name,
-                                             test_snap.name))
-
-                # Test volume creation from a snapshot (via VolumeService)
-                sv_name = "CBUnitTestSnapVol-{0}".format(name)
-                snap_vol = self.provider.block_store.volumes.create(
-                    sv_name,
-                    1,
-                    helpers.get_provider_test_data(self.provider, "placement"),
-                    snapshot=test_snap)
-                with helpers.cleanup_action(lambda: snap_vol.delete()):
-                    snap_vol.wait_till_ready()
-
-                # Test volume creation from a snapshot (via Snapshot)
-                snap_vol2 = test_snap.create_volume(
-                    helpers.get_provider_test_data(self.provider, "placement"))
-                with helpers.cleanup_action(lambda: snap_vol2.delete()):
-                    snap_vol2.wait_till_ready()
-
-            snaps = self.provider.block_store.snapshots.list()
-            found_snaps = [snap for snap in snaps
-                           if snap.name == snap_name]
-            self.assertTrue(
-                len(found_snaps) == 0,
-                "Snapshot %s should have been deleted but still exists." %
-                snap_name)
+            sit.check_crud(self, self.provider.storage.snapshots, Snapshot,
+                           "cb_snap", create_snap, cleanup_snap)
 
             # Test creation of a snap via SnapshotService
-            snap_too_name = "CBSnapToo-{0}".format(name)
-            time.sleep(15)  # Or get SnapshotCreationPerVolumeRateExceeded
-            test_snap_too = self.provider.block_store.snapshots.create(
-                name=snap_too_name, volume=test_vol, description=snap_too_name)
-            with helpers.cleanup_action(lambda: cleanup_snap(test_snap_too)):
-                test_snap_too.wait_till_ready()
-                self.assertTrue(
-                    test_snap_too.id in repr(test_snap_too),
-                    "repr(obj) should contain the object id so that the object"
-                    " can be reconstructed, but does not.")
+            def create_snap2(name):
+                return self.provider.storage.snapshots.create(
+                    name=name, volume=test_vol, description=name)
+
+            if (self.provider.PROVIDER_ID == ProviderList.AWS and
+                    not isinstance(self.provider, TestMockHelperMixin)):
+                time.sleep(15)  # Or get SnapshotCreationPerVolumeRateExceeded
+            sit.check_crud(self, self.provider.storage.snapshots, Snapshot,
+                           "cb_snaptwo", create_snap2, cleanup_snap)
 
-    @helpers.skipIfNoService(['block_store.snapshots'])
+    @helpers.skipIfNoService(['storage.snapshots'])
     def test_snapshot_properties(self):
         """
         Test snapshot properties
         """
-        name = "CBTestSnapProp-{0}".format(uuid.uuid4())
-        test_vol = self.provider.block_store.volumes.create(
+        name = "cb_snapprop-{0}".format(uuid.uuid4())
+        test_vol = self.provider.storage.volumes.create(
             name,
             1,
             helpers.get_provider_test_data(self.provider, "placement"))
         with helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
-            snap_name = "CBSnapProp-{0}".format(name)
+            snap_name = "cb_snap-{0}".format(name)
             test_snap = test_vol.create_snapshot(name=snap_name,
                                                  description=snap_name)
 
@@ -345,3 +208,19 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                 test_snap.refresh()
                 self.assertEqual(test_snap.name, 'snapnewname1')
                 self.assertEqual(test_snap.description, 'snapnewdescription1')
+
+                # Test volume creation from a snapshot (via VolumeService)
+                sv_name = "cb_snapvol_{0}".format(test_snap.name)
+                snap_vol = self.provider.storage.volumes.create(
+                    sv_name,
+                    1,
+                    helpers.get_provider_test_data(self.provider, "placement"),
+                    snapshot=test_snap)
+                with helpers.cleanup_action(lambda: snap_vol.delete()):
+                    snap_vol.wait_till_ready()
+
+                # Test volume creation from a snapshot (via Snapshot)
+                snap_vol2 = test_snap.create_volume(
+                    helpers.get_provider_test_data(self.provider, "placement"))
+                with helpers.cleanup_action(lambda: snap_vol2.delete()):
+                    snap_vol2.wait_till_ready()

+ 1 - 1
test/test_cloud_factory.py

@@ -21,7 +21,7 @@ class CloudFactoryTestCase(unittest.TestCase):
         self.assertIsInstance(CloudProviderFactory().create_provider(
             factory.ProviderList.AWS, {}),
             interfaces.CloudProvider,
-            "create_provider did not return a valid instance type")
+            "create_provider did not return a valid VM type")
 
     def test_create_provider_invalid(self):
         """

+ 155 - 204
test/test_compute_service.py

@@ -1,90 +1,57 @@
 import ipaddress
-import uuid
 
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
+from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InvalidConfigurationException
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
-from cloudbridge.cloud.interfaces.resources import InstanceType
-# from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import Instance
+from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import VMType
 
 import six
 
 
 class CloudComputeServiceTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['compute.instances', 'network'])
+    @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
     def test_crud_instance(self):
-        name = "CBInstCrud-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        name = "cb_instcrud-{0}".format(helpers.get_uuid())
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
-        inst = None
         net = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                inst, net)):
-            net, subnet = helpers.create_test_network(self.provider, name)
-            inst = helpers.get_test_instance(self.provider, name,
-                                             subnet=subnet)
+        subnet = None
 
-            all_instances = self.provider.compute.instances.list()
+        def create_inst(name):
+            # Also test whether sending in an empty_dict for user_data
+            # results in an automatic conversion to string.
+            return helpers.get_test_instance(self.provider, name,
+                                             subnet=subnet, user_data={})
 
-            list_instances = [i for i in all_instances if i.name == name]
-            self.assertTrue(
-                len(list_instances) == 1,
-                "List instances does not return the expected instance %s" %
-                name)
+        def cleanup_inst(inst):
+            inst.delete()
+            inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN])
 
-            # check iteration
-            iter_instances = [i for i in self.provider.compute.instances
-                              if i.name == name]
-            self.assertTrue(
-                len(iter_instances) == 1,
-                "Iter instances does not return the expected instance %s" %
-                name)
-
-            # check find
-            find_instances = self.provider.compute.instances.find(name=name)
+        def check_deleted(inst):
+            deleted_inst = self.provider.compute.instances.get(
+                inst.id)
             self.assertTrue(
-                len(find_instances) == 1,
-                "Find instances does not return the expected instance %s" %
+                deleted_inst is None or deleted_inst.state in (
+                    InstanceState.DELETED,
+                    InstanceState.UNKNOWN),
+                "Instance %s should have been deleted but still exists." %
                 name)
 
-            # check non-existent find
-            find_instances = self.provider.compute.instances.find(
-                name="non_existent")
-            self.assertTrue(
-                len(find_instances) == 0,
-                "Find() for a non-existent image returned %s" % find_instances)
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                                               network=net)):
+            net, subnet = helpers.create_test_network(self.provider, name)
 
-            get_inst = self.provider.compute.instances.get(
-                inst.id)
-            self.assertTrue(
-                list_instances[0] ==
-                get_inst == inst,
-                "Objects returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(list_instances[0].id,
-                                         get_inst.id,
-                                         inst.id))
-            self.assertTrue(
-                list_instances[0].name ==
-                get_inst.name == inst.name,
-                "Names returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(list_instances[0].name,
-                                         get_inst.name,
-                                         inst.name))
-        deleted_inst = self.provider.compute.instances.get(
-            inst.id)
-        self.assertTrue(
-            deleted_inst is None or deleted_inst.state in (
-                InstanceState.TERMINATED,
-                InstanceState.UNKNOWN),
-            "Instance %s should have been deleted but still exists." %
-            name)
+            sit.check_crud(self, self.provider.compute.instances, Instance,
+                           "cb_instcrud", create_inst, cleanup_inst,
+                           custom_check_delete=check_deleted)
 
     def _is_valid_ip(self, address):
         try:
@@ -93,35 +60,28 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             return False
         return True
 
-    @helpers.skipIfNoService(['compute.instances', 'network',
-                              'security.security_groups',
+    @helpers.skipIfNoService(['compute.instances', 'networking.networks',
+                              'security.vm_firewalls',
                               'security.key_pairs'])
     def test_instance_properties(self):
-        name = "CBInstProps-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        name = "cb_inst_props-{0}".format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         test_instance = None
         net = None
-        sg = None
+        fw = None
         kp = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_instance, net, sg, kp)):
+                test_instance, net, fw, kp)):
             net, subnet = helpers.create_test_network(self.provider, name)
             kp = self.provider.security.key_pairs.create(name=name)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
             test_instance = helpers.get_test_instance(self.provider,
                                                       name, key_pair=kp,
-                                                      security_groups=[sg],
+                                                      vm_firewalls=[fw],
                                                       subnet=subnet)
-
-            self.assertTrue(
-                test_instance.id in repr(test_instance),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
             self.assertEqual(
                 test_instance.name, name,
                 "Instance name {0} is not equal to the expected name"
@@ -132,13 +92,6 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                              " {1}".format(test_instance.image_id, image_id))
             self.assertIsInstance(test_instance.zone_id,
                                   six.string_types)
-            # FIXME: Moto is not returning the instance's placement zone
-#             find_zone = [zone for zone in
-#                          self.provider.compute.regions.current.zones
-#                          if zone.id == test_instance.zone_id]
-#             self.assertEqual(len(find_zone), 1,
-#                              "Instance's placement zone could not be "
-#                              " found in zones list")
             self.assertEqual(
                 test_instance.image_id,
                 helpers.get_provider_test_data(self.provider, "image"))
@@ -147,44 +100,52 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             self.assertEqual(
                 test_instance.key_pair_name,
                 kp.name)
-            self.assertIsInstance(test_instance.security_groups, list)
+            self.assertIsInstance(test_instance.vm_firewalls, list)
             self.assertEqual(
-                test_instance.security_groups[0],
-                sg)
-            self.assertIsInstance(test_instance.security_group_ids, list)
+                test_instance.vm_firewalls[0],
+                fw)
+            self.assertIsInstance(test_instance.vm_firewall_ids, list)
             self.assertEqual(
-                test_instance.security_group_ids[0],
-                sg.id)
+                test_instance.vm_firewall_ids[0],
+                fw.id)
             # Must have either a public or a private ip
             ip_private = test_instance.private_ips[0] \
                 if test_instance.private_ips else None
             ip_address = test_instance.public_ips[0] \
                 if test_instance.public_ips and test_instance.public_ips[0] \
                 else ip_private
+            # Convert to unicode for py27 compatibility with ipaddress()
+            ip_address = u"{}".format(ip_address)
             self.assertIsNotNone(
                 ip_address,
                 "Instance must have either a public IP or a private IP")
             self.assertTrue(
                 self._is_valid_ip(ip_address),
-                "Instance must have a valid IP address")
-            self.assertIsInstance(test_instance.instance_type_id,
+                "Instance must have a valid IP address. Got: %s" % ip_address)
+            self.assertIsInstance(test_instance.vm_type_id,
                                   six.string_types)
-            itype = self.provider.compute.instance_types.get(
-                test_instance.instance_type_id)
+            vm_type = self.provider.compute.vm_types.get(
+                test_instance.vm_type_id)
             self.assertEqual(
-                itype, test_instance.instance_type,
-                "Instance type {0} does not match expected type {1}".format(
-                    itype.name, test_instance.instance_type))
-            self.assertIsInstance(itype, InstanceType)
+                vm_type, test_instance.vm_type,
+                "VM type {0} does not match expected type {1}".format(
+                    vm_type.name, test_instance.vm_type))
+            self.assertIsInstance(vm_type, VMType)
             expected_type = helpers.get_provider_test_data(self.provider,
-                                                           'instance_type')
+                                                           'vm_type')
             self.assertEqual(
-                itype.name, expected_type,
-                "Instance type {0} does not match expected type {1}".format(
-                    itype.name, expected_type))
+                vm_type.name, expected_type,
+                "VM type {0} does not match expected type {1}".format(
+                    vm_type.name, expected_type))
+            find_zone = [zone for zone in
+                         self.provider.compute.regions.current.zones
+                         if zone.id == test_instance.zone_id]
+            self.assertEqual(len(find_zone), 1,
+                             "Instance's placement zone could not be "
+                             " found in zones list")
 
     @helpers.skipIfNoService(['compute.instances', 'compute.images',
-                              'compute.instance_types'])
+                              'compute.vm_types'])
     def test_block_device_mapping_launch_config(self):
         lc = self.provider.compute.instances.create_launch_config()
 
@@ -230,63 +191,60 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 delete_on_terminate=True)
 
         # Add all available ephemeral devices
-        instance_type_name = helpers.get_provider_test_data(
+        vm_type_name = helpers.get_provider_test_data(
             self.provider,
-            "instance_type")
-        inst_type = self.provider.compute.instance_types.find(
-            name=instance_type_name)[0]
-        for _ in range(inst_type.num_ephemeral_disks):
+            "vm_type")
+        vm_type = self.provider.compute.vm_types.find(
+            name=vm_type_name)[0]
+        for _ in range(vm_type.num_ephemeral_disks):
             lc.add_ephemeral_device()
 
         # block_devices should be populated
         self.assertTrue(
-            len(lc.block_devices) == 2 + inst_type.num_ephemeral_disks,
+            len(lc.block_devices) == 2 + vm_type.num_ephemeral_disks,
             "Expected %d total block devices bit found %d" %
-            (2 + inst_type.num_ephemeral_disks, len(lc.block_devices)))
+            (2 + vm_type.num_ephemeral_disks, len(lc.block_devices)))
 
     @helpers.skipIfNoService(['compute.instances', 'compute.images',
-                              'compute.instance_types', 'block_store.volumes'])
+                              'compute.vm_types', 'storage.volumes'])
     def test_block_device_mapping_attachments(self):
-        name = "CBInstBlkAttch-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
-
-        # Comment out BDM tests because OpenStack is not stable enough yet
-        if True:
-            if True:
-
-                # test_vol = self.provider.block_store.volumes.create(
-                #    name,
-                #    1,
-                #    helpers.get_provider_test_data(self.provider,
-                #                                   "placement"))
-                # with helpers.cleanup_action(lambda: test_vol.delete()):
-                #    test_vol.wait_till_ready()
-                #    test_snap = test_vol.create_snapshot(name=name,
-                #                                         description=name)
-                #
-                #    def cleanup_snap(snap):
-                #        snap.delete()
-                #        snap.wait_for(
-                #            [SnapshotState.UNKNOWN],
-                #            terminal_states=[SnapshotState.ERROR])
-                #
-                #    with helpers.cleanup_action(lambda:
-                #                                cleanup_snap(test_snap)):
-                #         test_snap.wait_till_ready()
+        name = "cb_blkattch-{0}".format(helpers.get_uuid())
+
+        if self.provider.PROVIDER_ID == ProviderList.OPENSTACK:
+            raise self.skipTest("Not running BDM tests because OpenStack is"
+                                " not stable enough yet")
+
+        test_vol = self.provider.storage.volumes.create(
+           name,
+           1,
+           helpers.get_provider_test_data(self.provider,
+                                          "placement"))
+        with helpers.cleanup_action(lambda: test_vol.delete()):
+            test_vol.wait_till_ready()
+            test_snap = test_vol.create_snapshot(name=name,
+                                                 description=name)
+
+            def cleanup_snap(snap):
+                snap.delete()
+                snap.wait_for([SnapshotState.UNKNOWN],
+                              terminal_states=[SnapshotState.ERROR])
+
+            with helpers.cleanup_action(lambda:
+                                        cleanup_snap(test_snap)):
+                test_snap.wait_till_ready()
 
                 lc = self.provider.compute.instances.create_launch_config()
 
-#                 # Add a new blank volume
-#                 lc.add_volume_device(size=1, delete_on_terminate=True)
-#
-#                 # Attach an existing volume
-#                 lc.add_volume_device(size=1, source=test_vol,
-#                                      delete_on_terminate=True)
-#
-#                 # Add a new volume based on a snapshot
-#                 lc.add_volume_device(size=1, source=test_snap,
-#                                      delete_on_terminate=True)
+                # Add a new blank volume
+                lc.add_volume_device(size=1, delete_on_terminate=True)
+
+                # Attach an existing volume
+                lc.add_volume_device(size=1, source=test_vol,
+                                     delete_on_terminate=True)
+
+                # Add a new volume based on a snapshot
+                lc.add_volume_device(size=1, source=test_snap,
+                                     delete_on_terminate=True)
 
                 # Override root volume size
                 image_id = helpers.get_provider_test_data(
@@ -302,12 +260,12 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     delete_on_terminate=True)
 
                 # Add all available ephemeral devices
-                instance_type_name = helpers.get_provider_test_data(
+                vm_type_name = helpers.get_provider_test_data(
                     self.provider,
-                    "instance_type")
-                inst_type = self.provider.compute.instance_types.find(
-                    name=instance_type_name)[0]
-                for _ in range(inst_type.num_ephemeral_disks):
+                    "vm_type")
+                vm_type = self.provider.compute.vm_types.find(
+                    name=vm_type_name)[0]
+                for _ in range(vm_type.num_ephemeral_disks):
                     lc.add_ephemeral_device()
 
                 net, subnet = helpers.create_test_network(self.provider, name)
@@ -332,75 +290,68 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         # TODO: Check instance attachments and make sure they
                         # correspond to requested mappings
 
-    @helpers.skipIfNoService(['compute.instances', 'network',
-                              'security.security_groups'])
+    @helpers.skipIfNoService(['compute.instances', 'networking.networks',
+                              'networking.floating_ips',
+                              'security.vm_firewalls'])
     def test_instance_methods(self):
-        name = "CBInstProps-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        name = "cb_instmethods-{0}".format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         test_inst = None
         net = None
-        sg = None
+        fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_inst, net, sg)):
+                test_inst, net, fw)):
             net, subnet = helpers.create_test_network(self.provider, name)
             test_inst = helpers.get_test_instance(self.provider, name,
                                                   subnet=subnet)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
 
-            # Check adding a security group to a running instance
-            test_inst.add_security_group(sg)
+            # Check adding a VM firewall to a running instance
+            test_inst.add_vm_firewall(fw)
             test_inst.refresh()
             self.assertTrue(
-                sg in test_inst.security_groups, "Expected security group '%s'"
-                " to be among instance security_groups: [%s]" %
-                (sg, test_inst.security_groups))
+                fw in test_inst.vm_firewalls, "Expected VM firewall '%s'"
+                " to be among instance vm_firewalls: [%s]" %
+                (fw, test_inst.vm_firewalls))
 
-            # Check removing a security group from a running instance
-            test_inst.remove_security_group(sg)
+            # Check removing a VM firewall from a running instance
+            test_inst.remove_vm_firewall(fw)
             test_inst.refresh()
             self.assertTrue(
-                sg not in test_inst.security_groups, "Expected security group"
-                " '%s' to be removed from instance security_groups: [%s]" %
-                (sg, test_inst.security_groups))
+                fw not in test_inst.vm_firewalls, "Expected VM firewall"
+                " '%s' to be removed from instance vm_firewalls: [%s]" %
+                (fw, test_inst.vm_firewalls))
 
             # check floating ips
-            router = self.provider.network.create_router(name=name)
-
-            with helpers.cleanup_action(lambda: router.delete()):
-
-                # TODO: Cloud specific code, needs fixing
-                if self.provider.PROVIDER_ID == 'openstack':
-                    for n in self.provider.network.list():
-                        if n.external:
-                            external_net = n
-                            break
-                else:
-                    external_net = net
-                router.attach_network(external_net.id)
-                router.add_route(subnet.id)
-
-                def cleanup_router():
-                    router.remove_route(subnet.id)
-                    router.detach_network()
-
-                with helpers.cleanup_action(lambda: cleanup_router()):
-                    # check whether adding an elastic ip works
-                    fip = self.provider.network.create_floating_ip()
-                    with helpers.cleanup_action(lambda: fip.delete()):
-                        test_inst.add_floating_ip(fip.public_ip)
-                        test_inst.refresh()
-                        self.assertIn(fip.public_ip, test_inst.public_ips)
-
-                        if isinstance(self.provider, TestMockHelperMixin):
-                            # TODO: Moto bug does not refresh removed public ip
-                            return
-
-                        # check whether removing an elastic ip works
-                        test_inst.remove_floating_ip(fip.public_ip)
+            router = self.provider.networking.routers.create(name, net)
+            gateway = None
+
+            def cleanup_router(router, gateway):
+                with helpers.cleanup_action(lambda: router.delete()):
+                    with helpers.cleanup_action(lambda: gateway.delete()):
+                        router.detach_subnet(subnet)
+                        router.detach_gateway(gateway)
+
+            with helpers.cleanup_action(lambda: cleanup_router(router,
+                                                               gateway)):
+                router.attach_subnet(subnet)
+                gateway = (self.provider.networking.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()
+                with helpers.cleanup_action(lambda: fip.delete()):
+                    with helpers.cleanup_action(
+                            lambda: test_inst.remove_floating_ip(fip)):
+                        test_inst.add_floating_ip(fip)
                         test_inst.refresh()
-                        self.assertNotIn(fip.public_ip, test_inst.public_ips)
+                        # On Devstack, FloatingIP is listed under private_ips.
+                        self.assertIn(fip.public_ip, test_inst.public_ips +
+                                      test_inst.private_ips)
+                    test_inst.refresh()
+                    self.assertNotIn(
+                        fip.public_ip,
+                        test_inst.public_ips + test_inst.private_ips)

+ 22 - 90
test/test_image_service.py

@@ -1,17 +1,14 @@
-import uuid
-
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 from cloudbridge.cloud.interfaces import MachineImageState
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
-
-import six
+from cloudbridge.cloud.interfaces.resources import MachineImage
 
 
 class CloudImageServiceTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['compute.images', 'network',
+    @helpers.skipIfNoService(['compute.images', 'networking.networks',
                               'compute.instances'])
     def test_create_and_list_image(self):
         """
@@ -19,14 +16,27 @@ class CloudImageServiceTestCase(ProviderTestBase):
         This covers waiting till the image is ready, checking that the image
         name is the expected one and whether list_images is functional.
         """
-        instance_name = "CBImageTest-{0}-{1}".format(
-            self.provider.name,
-            uuid.uuid4())
+        instance_name = "cb_crudimage-{0}".format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         test_instance = None
         net = None
+
+        def create_img(name):
+            return test_instance.create_image(name)
+
+        def cleanup_img(img):
+            img.delete()
+            img.wait_for(
+                [MachineImageState.UNKNOWN, MachineImageState.ERROR])
+
+        def extra_tests(img):
+            # check image size
+            img.refresh()
+            self.assertGreater(img.min_disk, 0, "Minimum disk"
+                               " size required by image is invalid")
+
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
             net, subnet = helpers.create_test_network(
@@ -34,84 +44,6 @@ class CloudImageServiceTestCase(ProviderTestBase):
             test_instance = helpers.get_test_instance(
                 self.provider, instance_name, subnet=subnet)
 
-            name = "CBUnitTestListImg-{0}".format(uuid.uuid4())
-            test_image = test_instance.create_image(name)
-
-            def cleanup_img(img):
-                img.delete()
-                img.wait_for(
-                    [MachineImageState.UNKNOWN, MachineImageState.ERROR])
-
-            with helpers.cleanup_action(lambda: cleanup_img(test_image)):
-                test_image.wait_till_ready()
-
-                self.assertTrue(
-                    test_instance.id in repr(test_instance),
-                    "repr(obj) should contain the object id so that the object"
-                    " can be reconstructed, but does not.")
-
-                self.assertTrue(
-                    test_image.description is None or isinstance(
-                        test_image.description, six.string_types),
-                    "Image description must be None or a string")
-
-                # This check won't work when >50 images are available
-                # images = self.provider.compute.images.list()
-                # list_images = [image for image in images
-                #                if image.name == name]
-                # self.assertTrue(
-                #     len(list_images) == 1,
-                #     "List images does not return the expected image %s" %
-                #     name)
-
-                # check iteration
-                iter_images = [image for image in self.provider.compute.images
-                               if image.name == name]
-                self.assertTrue(
-                    name in [ii.name for ii in iter_images],
-                    "Iter images (%s) does not contain the expected image %s" %
-                    (iter_images, name))
-
-                # find image
-                found_images = self.provider.compute.images.find(name=name)
-                self.assertTrue(
-                    name in [fi.name for fi in found_images],
-                    "Find images error: expected image %s but found: %s" %
-                    (name, found_images))
-
-                # check non-existent find
-                ne_images = self.provider.compute.images.find(
-                    name="non_existent")
-                self.assertTrue(
-                    len(ne_images) == 0,
-                    "Find() for a non-existent image returned %s" %
-                    ne_images)
-
-                get_img = self.provider.compute.images.get(
-                    test_image.id)
-                self.assertTrue(
-                    found_images[0] == get_img == test_image,
-                    "Objects returned by list: {0} and get: {1} are not as "
-                    " expected: {2}" .format(found_images[0].id,
-                                             get_img.id,
-                                             test_image.id))
-                self.assertTrue(
-                    found_images[0].name == get_img.name == test_image.name,
-                    "Names returned by find: {0} and get: {1} are"
-                    " not as expected: {2}" .format(found_images[0].name,
-                                                    get_img.name,
-                                                    test_image.name))
-                # TODO: Fix moto so that the BDM is populated correctly
-                if not isinstance(self.provider, TestMockHelperMixin):
-                    # check image size
-                    self.assertGreater(get_img.min_disk, 0, "Minimum disk size"
-                                       " required by image is invalid")
-            # TODO: Images take a long time to deregister on EC2. Needs
-            # investigation
-            images = self.provider.compute.images.list()
-            found_images = [image for image in images
-                            if image.name == name]
-            self.assertTrue(
-                len(found_images) == 0,
-                "Image %s should have been deleted but still exists." %
-                name)
+            sit.check_crud(self, self.provider.compute.images, MachineImage,
+                           "cb_listimg", create_img, cleanup_img,
+                           extra_test_func=extra_tests)

+ 0 - 110
test/test_instance_types_service.py

@@ -1,110 +0,0 @@
-from test import helpers
-
-from test.helpers import ProviderTestBase
-
-from cloudbridge.cloud.interfaces.resources import InstanceType
-
-import six
-
-
-class CloudInstanceTypesServiceTestCase(ProviderTestBase):
-
-    @helpers.skipIfNoService(['compute.instance_types'])
-    def test_instance_types(self):
-        instance_types = self.provider.compute.instance_types.list()
-        # Check iteration, keeping the first 50 entries (the .list() default)
-        iter_instance_types = list(self.provider.compute.instance_types)[:50]
-        self.assertListEqual(iter_instance_types, instance_types)
-
-        for inst_type in instance_types:
-            self.assertTrue(
-                inst_type.id in repr(inst_type),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
-            self.assertIsNotNone(
-                inst_type.id,
-                "InstanceType id must have a value")
-            self.assertIsNotNone(
-                inst_type.name,
-                "InstanceType name must have a value")
-            self.assertTrue(
-                inst_type.family is None or isinstance(
-                    inst_type.family,
-                    six.string_types),
-                "InstanceType family family be None or a"
-                " string but is: {0}".format(inst_type.family))
-            self.assertTrue(
-                inst_type.vcpus is None or (
-                    isinstance(inst_type.vcpus, six.integer_types) and
-                    inst_type.vcpus >= 0),
-                "InstanceType vcpus family be None or a positive integer")
-            self.assertTrue(
-                inst_type.ram is None or inst_type.ram >= 0,
-                "InstanceType ram must be None or a positive number")
-            self.assertTrue(
-                inst_type.size_root_disk is None or
-                inst_type.size_root_disk >= 0,
-                "InstanceType size_root_disk must be None or a positive number"
-                " but is: {0}".format(inst_type.size_root_disk))
-            self.assertTrue(
-                inst_type.size_ephemeral_disks is None or
-                inst_type.size_ephemeral_disks >= 0,
-                "InstanceType size_ephemeral_disk must be None or a positive"
-                " number")
-            self.assertTrue(
-                isinstance(inst_type.num_ephemeral_disks,
-                           six.integer_types) and
-                inst_type.num_ephemeral_disks >= 0,
-                "InstanceType num_ephemeral_disks must be None or a positive"
-                " number")
-            self.assertTrue(
-                inst_type.size_total_disk is None or
-                inst_type.size_total_disk >= 0,
-                "InstanceType size_total_disk must be None or a positive"
-                " number")
-            self.assertTrue(
-                inst_type.extra_data is None or isinstance(
-                    inst_type.extra_data, dict),
-                "InstanceType extra_data must be None or a dict")
-
-    @helpers.skipIfNoService(['compute.instance_types'])
-    def test_instance_types_find(self):
-        """
-        Searching for an instance by name should return an
-        InstanceType object and searching for a non-existent
-        object should return an empty iterator
-        """
-        instance_type_name = helpers.get_provider_test_data(
-            self.provider,
-            "instance_type")
-        inst_type = self.provider.compute.instance_types.find(
-            name=instance_type_name)[0]
-        self.assertTrue(isinstance(inst_type, InstanceType),
-                        "Find must return an InstanceType object")
-
-        self.assertFalse(self.provider.compute.instance_types.find(
-            name="non_existent_instance_type"), "Searching for a non-existent"
-            " instance type must return an empty list")
-
-        with self.assertRaises(TypeError):
-            self.provider.compute.instance_types.find(
-                non_existent_param="random_value")
-
-    @helpers.skipIfNoService(['compute.instance_types'])
-    def test_instance_types_get(self):
-        """
-        Searching for an instance by id should return an
-        InstanceType object and searching for a non-existent
-        object should return None
-        """
-        compute_svc = self.provider.compute
-        instance_type_name = helpers.get_provider_test_data(
-            self.provider,
-            "instance_type")
-        inst_type = self.provider.compute.instance_types.find(
-            name=instance_type_name)[0]
-        self.assertEqual(inst_type,
-                         compute_svc.instance_types.get(inst_type.id))
-        self.assertIsNone(compute_svc.instance_types.get("non_existent_id"),
-                          "Searching for a non-existent instance id must"
-                          " return None")

+ 10 - 9
test/test_interface.py

@@ -51,15 +51,16 @@ class CloudInterfaceTestCase(ProviderTestBase):
                 "Mock providers are not expected to"
                 " authenticate correctly")
 
-        cloned_provider = CloudProviderFactory().create_provider(
-            self.provider.PROVIDER_ID, self.provider.config)
+        # Mock up test by clearing credentials on a per provider basis
+        cloned_config = self.provider.config.copy()
+        if self.provider.PROVIDER_ID == 'aws':
+            cloned_config['aws_access_key'] = "dummy_a_key"
+            cloned_config['aws_secret_key'] = "dummy_s_key"
+        elif self.provider.PROVIDER_ID == 'openstack':
+            cloned_config['os_username'] = "cb_dummy"
+            cloned_config['os_password'] = "cb_dummy"
 
         with self.assertRaises(ProviderConnectionException):
-            # Mock up test by clearing credentials on a per provider basis
-            if cloned_provider.PROVIDER_ID == 'aws':
-                cloned_provider.a_key = "dummy_a_key"
-                cloned_provider.s_key = "dummy_s_key"
-            elif cloned_provider.PROVIDER_ID == 'openstack':
-                cloned_provider.username = "cb_dummy"
-                cloned_provider.password = "cb_dummy"
+            cloned_provider = CloudProviderFactory().create_provider(
+                self.provider.PROVIDER_ID, cloned_config)
             cloned_provider.authenticate()

+ 120 - 189
test/test_network_service.py

@@ -1,141 +1,35 @@
 import test.helpers as helpers
-import uuid
+
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
+from cloudbridge.cloud.interfaces.resources import FloatingIP
+from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import Subnet
 
 
 class CloudNetworkServiceTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['network'])
-    def test_crud_network_service(self):
-        name = 'cbtestnetworkservice-{0}'.format(uuid.uuid4())
-        subnet_name = 'cbtestsubnetservice-{0}'.format(uuid.uuid4())
-        net = self.provider.network.create(name=name)
-        with helpers.cleanup_action(
-            lambda:
-                self.provider.network.delete(network_id=net.id)
-        ):
-            # test list method
-            netl = self.provider.network.list()
-            list_netl = [n for n in netl if n.name == name]
-            self.assertTrue(
-                len(list_netl) == 1,
-                "List networks does not return the expected network %s" %
-                name)
-
-            # check get
-            get_net = self.provider.network.get(network_id=net.id)
-            self.assertTrue(
-                get_net == net,
-                "Get network did not return the expected network {0}."
-                .format(name))
-
-            # check subnet
-            subnet = self.provider.network.subnets.create(
-                network=net, cidr_block="10.0.0.1/24", name=subnet_name)
-            with helpers.cleanup_action(
-                lambda:
-                    self.provider.network.subnets.delete(subnet=subnet)
-            ):
-                # test list method
-                subnetl = self.provider.network.subnets.list(network=net)
-                list_subnetl = [n for n in subnetl if n.name == subnet_name]
-                self.assertTrue(
-                    len(list_subnetl) == 1,
-                    "List subnets does not return the expected subnet %s" %
-                    subnet_name)
-                # test get method
-                sn = self.provider.network.subnets.get(subnet.id)
-                self.assertTrue(
-                    subnet.id == sn.id,
-                    "GETting subnet should return the same subnet")
-
-            subnetl = self.provider.network.subnets.list()
-            found_subnet = [n for n in subnetl if n.name == subnet_name]
-            self.assertTrue(
-                len(found_subnet) == 0,
-                "Subnet {0} should have been deleted but still exists."
-                .format(subnet_name))
-
-            # Check floating IP address
-            ip = self.provider.network.create_floating_ip()
-            ip_id = ip.id
-            with helpers.cleanup_action(lambda: ip.delete()):
-                ipl = self.provider.network.floating_ips()
-                self.assertTrue(
-                    ip in ipl,
-                    "Floating IP address {0} should exist in the list {1}"
-                    .format(ip.id, ipl))
-                # 2016-08: address filtering not implemented in moto
-                # empty_ipl = self.provider.network.floating_ips('dummy-net')
-                # self.assertFalse(
-                #     empty_ipl,
-                #     "Bogus network should not have any floating IPs: {0}"
-                #     .format(empty_ipl))
-                self.assertIn(
-                    ip.public_ip, repr(ip),
-                    "repr(obj) should contain the address public IP value.")
-                self.assertFalse(
-                    ip.private_ip,
-                    "Floating IP should not have a private IP value ({0})."
-                    .format(ip.private_ip))
-                self.assertFalse(
-                    ip.in_use(),
-                    "Newly created floating IP address should not be in use.")
-            ipl = self.provider.network.floating_ips()
-            found_ip = [a for a in ipl if a.id == ip_id]
-            self.assertTrue(
-                len(found_ip) == 0,
-                "Floating IP {0} should have been deleted but still exists."
-                .format(ip_id))
-
-        netl = self.provider.network.list()
-        found_net = [n for n in netl if n.name == name]
-        self.assertEqual(
-            len(found_net), 0,
-            "Network {0} should have been deleted but still exists."
-            .format(name))
-
-    def test_crud_ip(self):
-        ip = self.provider.network.create_floating_ip()
-        ip_id = ip.id
-        with helpers.cleanup_action(lambda: ip.delete()):
-            ipl = self.provider.network.floating_ips()
-            self.assertTrue(
-                ip in ipl,
-                "Floating IP address {0} should exist in the list {1}"
-                .format(ip.id, ipl))
-            # 2016-08: address filtering not implemented in moto
-            # empty_ipl = self.provider.network.floating_ips('dummy-net')
-            # self.assertFalse(
-            #     empty_ipl,
-            #     "Bogus network should not have any floating IPs: {0}"
-            #     .format(empty_ipl))
-            self.assertIn(
-                ip.public_ip, repr(ip),
-                "repr(obj) should contain the address public IP value.")
-            self.assertFalse(
-                ip.private_ip,
-                "Floating IP should not have a private IP value ({0})."
-                .format(ip.private_ip))
-            self.assertFalse(
-                ip.in_use(),
-                "Newly created floating IP address should not be in use.")
-        ipl = self.provider.network.floating_ips()
-        found_ip = [a for a in ipl if a.id == ip_id]
-        self.assertTrue(
-            len(found_ip) == 0,
-            "Floating IP {0} should have been deleted but still exists."
-            .format(ip_id))
-
-    @helpers.skipIfNoService(['network'])
-    @helpers.skipIfNoService(['network'])
-    @helpers.skipIfNoService(['network'])
+    @helpers.skipIfNoService(['networking.networks'])
     def test_crud_network(self):
-        name = 'cbtestnetwork-{0}'.format(uuid.uuid4())
-        subnet_name = 'cbtestsubnet-{0}'.format(uuid.uuid4())
-        net = self.provider.network.create(name=name)
+
+        def create_net(name):
+            return self.provider.networking.networks.create(
+                name=name, cidr_block='10.0.0.0/16')
+
+        def cleanup_net(net):
+            self.provider.networking.networks.delete(network_id=net.id)
+
+        sit.check_crud(self, self.provider.networking.networks, Network,
+                       "cb_crudnetwork", create_net, cleanup_net)
+
+    @helpers.skipIfNoService(['networking.networks'])
+    def test_network_properties(self):
+        name = 'cb_propnetwork-{0}'.format(helpers.get_uuid())
+        subnet_name = 'cb_propsubnet-{0}'.format(helpers.get_uuid())
+        net = self.provider.networking.networks.create(
+            name=name, cidr_block='10.0.0.0/16')
         with helpers.cleanup_action(
             lambda: net.delete()
         ):
@@ -144,10 +38,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 net.state, 'available',
                 "Network in state '%s', yet should be 'available'" % net.state)
 
-            self.assertIn(
-                net.id, repr(net),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not.")
+            sit.check_repr(self, net)
 
             self.assertIn(
                 net.cidr_block, ['', '10.0.0.0/16'],
@@ -155,15 +46,18 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 % net.cidr_block)
 
             cidr = '10.0.1.0/24'
-            sn = net.create_subnet(
-                cidr_block=cidr, name=subnet_name,
-                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.id in [s.id for s in net.subnets()],
+                    sn in net.subnets,
                     "Subnet ID %s should be listed in network subnets %s."
-                    % (sn.id, net.subnets()))
+                    % (sn.id, net.subnets))
+
+                self.assertListEqual(
+                    net.subnets, [sn],
+                    "Network should have exactly one subnet: %s." % sn.id)
 
                 self.assertIn(
                     net.id, sn.network_id,
@@ -175,74 +69,111 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     "Subnet's CIDR %s should match the specified one %s." % (
                         sn.cidr_block, cidr))
 
-    @helpers.skipIfNoService(['network.routers'])
+    def test_crud_subnet(self):
+        # Late binding will make sure that create_subnet gets the
+        # correct value
+        net = None
+
+        def create_subnet(name):
+            return self.provider.networking.subnets.create(
+                network=net, cidr_block="10.0.0.1/24", name=name)
+
+        def cleanup_subnet(subnet):
+            self.provider.networking.subnets.delete(subnet=subnet)
+
+        net_name = 'cb_crudsubnet-{0}'.format(helpers.get_uuid())
+        net = self.provider.networking.networks.create(
+            name=net_name, cidr_block='10.0.0.0/16')
+        with helpers.cleanup_action(
+            lambda:
+                self.provider.networking.networks.delete(network_id=net.id)
+        ):
+            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):
+
+        def create_fip(name):
+            return self.provider.networking.floating_ips.create()
+
+        def cleanup_fip(fip):
+            self.provider.networking.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)
+
+    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.")
+
+    @helpers.skipIfNoService(['networking.routers'])
     def test_crud_router(self):
 
-        def _cleanup(net, subnet, router):
+        def _cleanup(net, subnet, router, gateway):
             with helpers.cleanup_action(lambda: net.delete()):
                 with helpers.cleanup_action(lambda: subnet.delete()):
-                    with helpers.cleanup_action(lambda: router.delete()):
-                        router.remove_route(subnet.id)
-                        router.detach_network()
+                    with helpers.cleanup_action(lambda: gateway.delete()):
+                        with helpers.cleanup_action(lambda: router.delete()):
+                            router.detach_subnet(subnet)
+                            router.detach_gateway(gateway)
 
-        name = 'cbtestrouter-{0}'.format(uuid.uuid4())
+        name = 'cb_crudrouter-{0}'.format(helpers.get_uuid())
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
         sn = None
         router = None
-        with helpers.cleanup_action(lambda: _cleanup(net, sn, router)):
-            router = self.provider.network.create_router(name=name)
-            net = self.provider.network.create(name=name)
+        gteway = None
+        with helpers.cleanup_action(lambda: _cleanup(net, sn, router, gteway)):
+            net = self.provider.networking.networks.create(
+                name=name, cidr_block='10.0.0.0/16')
+            router = self.provider.networking.routers.create(network=net,
+                                                             name=name)
             cidr = '10.0.1.0/24'
-            sn = net.create_subnet(cidr_block=cidr, name=name,
+            sn = net.create_subnet(name=name, cidr_block=cidr,
                                    zone=helpers.get_provider_test_data(
                                        self.provider, 'placement'))
 
             # Check basic router properties
-            self.assertIn(
-                router, self.provider.network.routers(),
-                "Router {0} should exist in the router list {1}.".format(
-                    router.id, self.provider.network.routers()))
-            self.assertIn(
-                router.id, repr(router),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not.")
-            self.assertEqual(
-                router.name, name,
-                "Router {0} name should be {1}.".format(router.name, name))
+            sit.check_standard_behaviour(
+                self, self.provider.networking.routers, router)
             self.assertEqual(
                 router.state, RouterState.DETACHED,
                 "Router {0} state {1} should be {2}.".format(
                     router.id, router.state, RouterState.DETACHED))
-            self.assertFalse(
-                router.network_id,
-                "Router {0} should not be assoc. with a network {1}".format(
-                    router.id, router.network_id))
-
-            # TODO: Cloud specific code, needs fixing
-            # Check router connectivity
-            # On OpenStack only one network is external and on AWS every
-            # network is external, yet we need to use the one we've created?!
-            if self.provider.PROVIDER_ID == 'openstack':
-                for n in self.provider.network.list():
-                    if n.external:
-                        external_net = n
-                        break
-            else:
-                external_net = net
-            router.attach_network(external_net.id)
-            router.refresh()
-            self.assertEqual(
-                router.network_id, external_net.id,
-                "Router should be attached to network {0}, not {1}".format(
-                    external_net.id, router.network_id))
-            router.add_route(sn.id)
+
+#             self.assertFalse(
+#                 router.network_id,
+#                 "Router {0} should not be assoc. with a network {1}".format(
+#                     router.id, router.network_id))
+
+            router.attach_subnet(sn)
+            gteway = (self.provider.networking.gateways
+                      .get_or_create_inet_gateway(name))
+            router.attach_gateway(gteway)
             # TODO: add a check for routes after that's been implemented
 
-        routerl = self.provider.network.routers()
-        found_router = [r for r in routerl if r.name == name]
-        self.assertEqual(
-            len(found_router), 0,
-            "Router {0} should have been deleted but still exists."
-            .format(name))
+        sit.check_delete(self, self.provider.networking.routers, router)
+
+    @helpers.skipIfNoService(['networking.networks'])
+    def test_default_network(self):
+        subnet = self.provider.networking.subnets.get_or_create_default()
+        self.assertIsInstance(subnet, Subnet)

+ 3 - 5
test/test_object_life_cycle.py

@@ -1,5 +1,3 @@
-import uuid
-
 from test import helpers
 from test.helpers import ProviderTestBase
 
@@ -9,13 +7,13 @@ from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
 class CloudObjectLifeCycleTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['block_store.volumes'])
+    @helpers.skipIfNoService(['storage.volumes'])
     def test_object_life_cycle(self):
         """
         Test object life cycle methods by using a volume.
         """
-        name = "CBUnitTestLifeCycle-{0}".format(uuid.uuid4())
-        test_vol = self.provider.block_store.volumes.create(
+        name = "cb_objlifecycle-{0}".format(helpers.get_uuid())
+        test_vol = self.provider.storage.volumes.create(
             name,
             1,
             helpers.get_provider_test_data(self.provider, "placement"))

+ 85 - 96
test/test_object_store_service.py

@@ -7,8 +7,13 @@ from datetime import datetime
 from io import BytesIO
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 from unittest import skip
 
+from cloudbridge.cloud.factory import ProviderList
+from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
+from cloudbridge.cloud.interfaces.provider import TestMockHelperMixin
+from cloudbridge.cloud.interfaces.resources import Bucket
 from cloudbridge.cloud.interfaces.resources import BucketObject
 
 import requests
@@ -16,83 +21,80 @@ import requests
 
 class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
-    @helpers.skipIfNoService(['object_store'])
+    @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket(self):
         """
         Create a new bucket, check whether the expected values are set,
         and delete it.
         """
-        name = "cbtestcreatebucket-{0}".format(uuid.uuid4())
-        test_bucket = self.provider.object_store.create(name)
+        def create_bucket(name):
+            return self.provider.storage.buckets.create(name)
+
+        def cleanup_bucket(bucket):
+            bucket.delete()
+
+        with self.assertRaises(InvalidNameException):
+            # underscores are not allowed in bucket names
+            create_bucket("cb_bucket")
+
+        with self.assertRaises(InvalidNameException):
+            # names of length less than 3 should raise an exception
+            create_bucket("cb")
+
+        with self.assertRaises(InvalidNameException):
+            # names of length greater than 63 should raise an exception
+            create_bucket("a" * 64)
+
+        with self.assertRaises(InvalidNameException):
+            # bucket name cannot be an IP address
+            create_bucket("197.10.100.42")
+
+        sit.check_crud(self, self.provider.storage.buckets, Bucket,
+                       "cb-crudbucket", create_bucket, cleanup_bucket,
+                       skip_name_check=True)
+
+    @helpers.skipIfNoService(['storage.buckets'])
+    def test_crud_bucket_object(self):
+        test_bucket = None
+
+        def create_bucket_obj(name):
+            obj = test_bucket.objects.create(name)
+            # TODO: This is wrong. We shouldn't have to have a separate
+            # call to upload some content before being able to delete
+            # the content. Maybe the create_object method should accept
+            # the file content as a parameter.
+            obj.upload("dummy content")
+            return obj
+
+        def cleanup_bucket_obj(bucket_obj):
+            bucket_obj.delete()
+
         with helpers.cleanup_action(lambda: test_bucket.delete()):
-            self.assertTrue(
-                test_bucket.id in repr(test_bucket),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
-
-            buckets = self.provider.object_store.list()
-
-            list_buckets = [c for c in buckets if c.name == name]
-            self.assertTrue(
-                len(list_buckets) == 1,
-                "List buckets does not return the expected bucket %s" %
-                name)
-
-            # check iteration
-            iter_buckets = [c for c in self.provider.object_store
-                            if c.name == name]
-            self.assertTrue(
-                len(iter_buckets) == 1,
-                "Iter buckets does not return the expected bucket %s" %
-                name)
-
-            # check find
-            find_buckets = self.provider.object_store.find(name=name)
-            self.assertTrue(
-                len(find_buckets) == 1,
-                "Find buckets does not return the expected bucket %s" %
-                name)
-
-            get_bucket = self.provider.object_store.get(
-                test_bucket.id)
-            self.assertTrue(
-                list_buckets[0] ==
-                get_bucket == test_bucket,
-                "Objects returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(list_buckets[0].id,
-                                         get_bucket.id,
-                                         test_bucket.name))
-
-        buckets = self.provider.object_store.list()
-        found_buckets = [c for c in buckets if c.name == name]
-        self.assertTrue(
-            len(found_buckets) == 0,
-            "Bucket %s should have been deleted but still exists." %
-            name)
-
-    @helpers.skipIfNoService(['object_store'])
-    def test_crud_bucket_objects(self):
+            name = "cb-crudbucketobj-{0}".format(uuid.uuid4())
+            test_bucket = self.provider.storage.buckets.create(name)
+
+            sit.check_crud(self, test_bucket.objects, BucketObject,
+                           "cb_bucketobj", create_bucket_obj,
+                           cleanup_bucket_obj, skip_name_check=True)
+
+    @helpers.skipIfNoService(['storage.buckets'])
+    def test_crud_bucket_object_properties(self):
         """
         Create a new bucket, upload some contents into the bucket, and
         check whether list properly detects the new content.
         Delete everything afterwards.
         """
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
-        test_bucket = self.provider.object_store.create(name)
+        test_bucket = self.provider.storage.buckets.create(name)
 
         # ensure that the bucket is empty
-        objects = test_bucket.list()
+        objects = test_bucket.objects.list()
         self.assertEqual([], objects)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name_prefix = "hello"
             obj_name = obj_name_prefix + "_world.txt"
-            obj = test_bucket.create_object(obj_name)
-
-            self.assertTrue(
-                obj.id in repr(obj),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
+            obj = test_bucket.objects.create(obj_name)
 
             with helpers.cleanup_action(lambda: obj.delete()):
                 # TODO: This is wrong. We shouldn't have to have a separate
@@ -100,7 +102,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 # the content. Maybe the create_object method should accept
                 # the file content as a parameter.
                 obj.upload("dummy content")
-                objs = test_bucket.list()
+                objs = test_bucket.objects.list()
 
                 self.assertTrue(
                     isinstance(objs[0].size, int),
@@ -113,51 +115,32 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     .format(objs[0].last_modified))
 
                 # check iteration
-                iter_objs = list(test_bucket)
+                iter_objs = list(test_bucket.objects)
                 self.assertListEqual(iter_objs, objs)
 
-                found_objs = [o for o in objs if o.name == obj_name]
-                self.assertTrue(
-                    len(found_objs) == 1,
-                    "List bucket objects does not return the expected"
-                    " object %s" % obj_name)
-
-                get_bucket_obj = test_bucket.get(obj_name)
-                self.assertTrue(
-                    found_objs[0] ==
-                    get_bucket_obj == obj,
-                    "Objects returned by list: {0} and get: {1} are not as "
-                    " expected: {2}" .format(found_objs[0].id,
-                                             get_bucket_obj.id,
-                                             obj.id))
-
-                obj_too = test_bucket.get(obj_name)
+                obj_too = test_bucket.objects.get(obj_name)
                 self.assertTrue(
                     isinstance(obj_too, BucketObject),
                     "Did not get object {0} of expected type.".format(obj_too))
 
-                prefix_filtered_list = test_bucket.list(prefix=obj_name_prefix)
+                prefix_filtered_list = test_bucket.objects.list(
+                    prefix=obj_name_prefix)
                 self.assertTrue(
                     len(objs) == len(prefix_filtered_list) == 1,
                     'The number of objects returned by list function, '
                     'with and without a prefix, are expected to be equal, '
                     'but its detected otherwise.')
 
-            objs = test_bucket.list()
-            found_objs = [o for o in objs if o.name == obj_name]
-            self.assertTrue(
-                len(found_objs) == 0,
-                "Object %s should have been deleted but still exists." %
-                obj_name)
+            sit.check_delete(self, test_bucket.objects, obj)
 
-    @helpers.skipIfNoService(['object_store'])
+    @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content(self):
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
-        test_bucket = self.provider.object_store.create(name)
+        test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
-            obj = test_bucket.create_object(obj_name)
+            obj = test_bucket.objects.create(obj_name)
 
             with helpers.cleanup_action(lambda: obj.delete()):
                 content = b"Hello World. Here's some content."
@@ -173,15 +156,17 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     target_stream2.write(data)
                 self.assertEqual(target_stream2.getvalue(), content)
 
-    @skip("Skip until OpenStack implementation is provided")
-    @helpers.skipIfNoService(['object_store'])
+    @helpers.skipIfNoService(['storage.buckets'])
     def test_generate_url(self):
+        if self.provider.PROVIDER_ID == ProviderList.OPENSTACK:
+            raise self.skipTest("Skip until OpenStack impl is provided")
+
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
-        test_bucket = self.provider.object_store.create(name)
+        test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
-            obj = test_bucket.create_object(obj_name)
+            obj = test_bucket.objects.create(obj_name)
 
             with helpers.cleanup_action(lambda: obj.delete()):
                 content = b"Hello World. Generate a url."
@@ -190,16 +175,20 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 obj.save_content(target_stream)
 
                 url = obj.generate_url(100)
+                if isinstance(self.provider, TestMockHelperMixin):
+                    raise self.skipTest(
+                        "Skipping rest of test - mock providers can't"
+                        " access generated url")
                 self.assertEqual(requests.get(url).content, content)
 
-    @helpers.skipIfNoService(['object_store'])
+    @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content_from_file(self):
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
-        test_bucket = self.provider.object_store.create(name)
+        test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
-            obj = test_bucket.create_object(obj_name)
+            obj = test_bucket.objects.create(obj_name)
 
             with helpers.cleanup_action(lambda: obj.delete()):
                 test_file = os.path.join(
@@ -211,7 +200,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     self.assertEqual(target_stream.getvalue(), f.read())
 
     @skip("Skip unless you want to test swift objects bigger than 5 Gig")
-    @helpers.skipIfNoService(['object_store'])
+    @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content_with_large_file(self):
         """
         Creates a 6 Gig file in the temp directory, then uploads it to
@@ -226,9 +215,9 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         with helpers.cleanup_action(lambda: os.remove(six_gig_file)):
             download_file = "{0}/cbtestfile-{1}".format(temp_dir, file_name)
             bucket_name = "cbtestbucketlargeobjs-{0}".format(uuid.uuid4())
-            test_bucket = self.provider.object_store.create(bucket_name)
+            test_bucket = self.provider.storage.buckets.create(bucket_name)
             with helpers.cleanup_action(lambda: test_bucket.delete()):
-                test_obj = test_bucket.create_object(file_name)
+                test_obj = test_bucket.objects.create(file_name)
                 with helpers.cleanup_action(lambda: test_obj.delete()):
                     file_uploaded = test_obj.upload_from_file(six_gig_file)
                     self.assertTrue(file_uploaded, "Could not upload object?")

+ 8 - 30
test/test_region_service.py

@@ -1,5 +1,6 @@
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 from cloudbridge.cloud.interfaces import Region
 
@@ -14,11 +15,9 @@ class CloudRegionServiceTestCase(ProviderTestBase):
         Test whether the region listing methods work,
         and whether zones are returned appropriately.
         """
-        regions = self.provider.compute.regions.list()
-
-        # check iteration
-        iter_regions = list(self.provider.compute.regions)
-        self.assertListEqual(iter_regions, regions)
+        regions = list(self.provider.compute.regions)
+        sit.check_standard_behaviour(
+            self, self.provider.compute.regions, regions[-1])
 
         for region in regions:
             self.assertIsInstance(
@@ -29,22 +28,6 @@ class CloudRegionServiceTestCase(ProviderTestBase):
                 region.name,
                 "Region name should be a non-empty string")
 
-        region = self.provider.compute.regions.get(regions[0].id)
-        self.assertEqual(
-            region,
-            regions[0],
-            "List and get methods should return the same regions")
-
-        self.assertTrue(
-            region.id in repr(region),
-            "repr(obj) should contain the object id so that the object"
-            " can be reconstructed, but does not.")
-
-        self.assertTrue(
-            region.name in region.to_json(),
-            "Region name {0} not in JSON representation {1}".format(
-                region.name, region.to_json()))
-
     @helpers.skipIfNoService(['compute.regions'])
     def test_regions_unique(self):
         """
@@ -61,7 +44,7 @@ class CloudRegionServiceTestCase(ProviderTestBase):
         """
         current_region = self.provider.compute.regions.current
         self.assertIsInstance(current_region, Region)
-        self.assertTrue(current_region in self.provider.compute.regions.list())
+        self.assertTrue(current_region in self.provider.compute.regions)
 
     @helpers.skipIfNoService(['compute.regions'])
     def test_zones(self):
@@ -70,8 +53,7 @@ class CloudRegionServiceTestCase(ProviderTestBase):
         """
         zone_find_count = 0
         test_zone = helpers.get_provider_test_data(self.provider, "placement")
-        regions = self.provider.compute.regions.list()
-        for region in regions:
+        for region in self.provider.compute.regions:
             self.assertTrue(region.name)
             for zone in region.zones:
                 self.assertTrue(zone.id)
@@ -81,9 +63,5 @@ class CloudRegionServiceTestCase(ProviderTestBase):
                                            six.string_types))
                 if test_zone == zone.name:
                     zone_find_count += 1
-        # TODO: Can't do a check for zone_find_count == 1 because Moto
-        # always returns the same zone for any region
-        self.assertTrue(zone_find_count > 0,
-                        "The test zone: {0} should appear exactly"
-                        " once in the list of regions, but was not found"
-                        .format(test_zone, zone_find_count))
+        # zone info cannot be repeated between regions
+        self.assertEqual(zone_find_count, 1)

+ 133 - 219
test/test_security_service.py

@@ -1,286 +1,200 @@
 """Test cloudbridge.security modules."""
-import json
-import unittest
-import uuid
-
 from test import helpers
 from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
+from cloudbridge.cloud.interfaces.resources import KeyPair
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.interfaces.resources import VMFirewallRule
 
 
 class CloudSecurityServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_crud_key_pair_service(self):
-        name = 'cbtestkeypairA-{0}'.format(uuid.uuid4())
-        kp = self.provider.security.key_pairs.create(name=name)
-        with helpers.cleanup_action(
-            lambda:
-                self.provider.security.key_pairs.delete(key_pair_id=kp.id)
-        ):
-            # test list method
-            kpl = self.provider.security.key_pairs.list()
-            list_kpl = [i for i in kpl if i.id == kp.id]
-            self.assertTrue(
-                len(list_kpl) == 1,
-                "List key pairs does not return the expected key pair %s" %
-                name)
 
-            # check iteration
-            iter_kpl = [i for i in self.provider.security.key_pairs
-                        if i.id == kp.id]
-            self.assertTrue(
-                len(iter_kpl) == 1,
-                "Iter key pairs does not return the expected key pair %s" %
-                name)
+        def create_kp(name):
+            return self.provider.security.key_pairs.create(name=name)
 
-            # check find
-            find_kp = self.provider.security.key_pairs.find(name=kp.name)[0]
-            self.assertTrue(
-                find_kp == kp,
-                "Find key pair did not return the expected key {0}."
-                .format(name))
-
-            # check get
-            get_kp = self.provider.security.key_pairs.get(kp.id)
-            self.assertTrue(
-                get_kp == kp,
-                "Get key pair did not return the expected key {0}."
-                .format(name))
+        def cleanup_kp(kp):
+            self.provider.security.key_pairs.delete(key_pair_id=kp.id)
 
+        def extra_tests(kp):
             # Recreating existing keypair should raise an exception
             with self.assertRaises(Exception):
-                self.provider.security.key_pairs.create(name=name)
-        kpl = self.provider.security.key_pairs.list()
-        found_kp = [k for k in kpl if k.id == kp.id]
-        self.assertTrue(
-            len(found_kp) == 0,
-            "Key pair {0} should have been deleted but still exists."
-            .format(name))
-        no_kp = self.provider.security.key_pairs.find(name='bogus_kp')
-        self.assertFalse(
-            no_kp,
-            "Found a key pair {0} that should not exist?".format(no_kp))
+                self.provider.security.key_pairs.create(name=kp.name)
+
+        sit.check_crud(self, self.provider.security.key_pairs, KeyPair,
+                       "cb_crudkp", create_kp, cleanup_kp,
+                       extra_test_func=extra_tests)
 
     @helpers.skipIfNoService(['security.key_pairs'])
-    def test_key_pair(self):
-        name = 'cbtestkeypairB-{0}'.format(uuid.uuid4())
+    def test_key_pair_properties(self):
+        name = 'cb_kpprops-{0}'.format(helpers.get_uuid())
         kp = self.provider.security.key_pairs.create(name=name)
         with helpers.cleanup_action(lambda: kp.delete()):
-            kpl = self.provider.security.key_pairs.list()
-            found_kp = [k for k in kpl if k.id == kp.id]
-            self.assertTrue(
-                len(found_kp) == 1,
-                "List key pairs did not return the expected key {0}."
-                .format(name))
-            self.assertTrue(
-                kp.id in repr(kp),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
             self.assertIsNotNone(
                 kp.material,
                 "KeyPair material is empty but it should not be.")
-            self.assertTrue(
-                kp == kp,
-                "The same key pair should be equal to self.")
-            # check json deserialization
-            self.assertTrue(json.loads(kp.to_json()),
-                            "to_json must yield a valid json string: {0}"
-                            .format(kp.to_json()))
-        kpl = self.provider.security.key_pairs.list()
-        found_kp = [k for k in kpl if k.id == kp.id]
-        self.assertTrue(
-            len(found_kp) == 0,
-            "Key pair {0} should have been deleted but still exists."
-            .format(name))
+            # get the keypair again - keypair material should now be empty
+            kp = self.provider.security.key_pairs.get(kp.id)
+            self.assertIsNone(kp.material,
+                              "Keypair material should now be empty")
+
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_crud_vm_firewall(self):
+        name = 'cb_crudfw-{0}'.format(helpers.get_uuid())
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+
+        def create_fw(name):
+            return self.provider.security.vm_firewalls.create(
+                name=name, description=name, network_id=net.id)
+
+        def cleanup_fw(fw):
+            fw.delete()
+
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net)):
+            net, _ = helpers.create_test_network(self.provider, name)
 
-    def cleanup_sg(self, sg, net):
-        with helpers.cleanup_action(
-                lambda: self.provider.network.delete(network_id=net.id)):
-            self.provider.security.security_groups.delete(group_id=sg.id)
+            sit.check_crud(self, self.provider.security.vm_firewalls,
+                           VMFirewall, "cb_crudfw", create_fw, cleanup_fw)
 
-    @helpers.skipIfNoService(['security.security_groups'])
-    def test_crud_security_group_service(self):
-        name = 'CBTestSecurityGroupA-{0}'.format(uuid.uuid4())
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_vm_firewall_properties(self):
+        name = 'cb_propfw-{0}'.format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
-        sg = None
+        fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, security_group=sg)):
+                network=net, vm_firewall=fw)):
             net, _ = helpers.create_test_network(self.provider, name)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
-            self.assertEqual(name, sg.description)
 
-            # test list method
-            sgl = self.provider.security.security_groups.list()
-            found_sgl = [i for i in sgl if i.name == name]
-            self.assertTrue(
-                len(found_sgl) == 1,
-                "List security groups does not return the expected group %s" %
-                name)
+            self.assertEqual(name, fw.description)
 
-            # check iteration
-            found_sgl = [i for i in self.provider.security.security_groups
-                         if i.name == name]
-            self.assertTrue(
-                len(found_sgl) == 1,
-                "Iter security groups does not return the expected group %s" %
-                name)
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_crud_vm_firewall_rules(self):
+        name = 'cb_crudfw_rules-{0}'.format(helpers.get_uuid())
 
-            # check find
-            find_sg = self.provider.security.security_groups.find(name=sg.name)
-            self.assertTrue(
-                len(find_sg) == 1,
-                "List security groups returned {0} when expected was: {1}."
-                .format(find_sg, sg.name))
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net)):
+            net, _ = helpers.create_test_network(self.provider, name)
 
-            # check get
-            get_sg = self.provider.security.security_groups.get(sg.id)
-            self.assertTrue(
-                get_sg == sg,
-                "Get SecurityGroup did not return the expected key {0}."
-                .format(name))
+            fw = None
+            with helpers.cleanup_action(lambda: fw.delete()):
+                fw = self.provider.security.vm_firewalls.create(
+                    name=name, description=name, network_id=net.id)
 
-            self.assertTrue(
-                sg.id in repr(sg),
-                "repr(obj) should contain the object id so that the object"
-                " can be reconstructed, but does not. eval(repr(obj)) == obj")
-        sgl = self.provider.security.security_groups.list()
-        found_sg = [g for g in sgl if g.name == name]
-        self.assertTrue(
-            len(found_sg) == 0,
-            "Security group {0} should have been deleted but still exists."
-            .format(name))
-        no_sg = self.provider.security.security_groups.find(name='bogus_sg')
-        self.assertTrue(
-            len(no_sg) == 0,
-            "Found a bogus security group?!?".format(no_sg))
+                def create_fw_rule(name):
+                    return fw.rules.create(
+                        direction=TrafficDirection.INBOUND, protocol='tcp',
+                        from_port=1111, to_port=1111, cidr='0.0.0.0/0')
+
+                def cleanup_fw_rule(rule):
+                    rule.delete()
+
+                sit.check_crud(self, fw.rules, VMFirewallRule, "cb_crudfwrule",
+                               create_fw_rule, cleanup_fw_rule,
+                               skip_name_check=True)
 
-    @helpers.skipIfNoService(['security.security_groups'])
-    def test_security_group(self):
-        """Test for proper creation of a security group."""
-        name = 'CBTestSecurityGroupB-{0}'.format(uuid.uuid4())
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_vm_firewall_rule_properties(self):
+        name = 'cb_propfwrule-{0}'.format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
-        sg = None
+        fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, security_group=sg)):
+                network=net, vm_firewall=fw)):
             net, _ = helpers.create_test_network(self.provider, name)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
-            rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
-                               cidr_ip='0.0.0.0/0')
-            found_rule = sg.get_rule(ip_protocol='tcp', from_port=1111,
-                                     to_port=1111, cidr_ip='0.0.0.0/0')
-            self.assertTrue(
-                rule == found_rule,
-                "Expected rule {0} not found in security group: {1}".format(
-                    rule, sg.rules))
-
-            object_keys = (
-                sg.rules[0].ip_protocol,
-                sg.rules[0].from_port,
-                sg.rules[0].to_port)
-            self.assertTrue(
-                all(str(key) in repr(sg.rules[0]) for key in object_keys),
-                "repr(obj) should contain ip_protocol, form_port, and to_port"
-                " so that the object can be reconstructed, but does not:"
-                " {0}; {1}".format(sg.rules[0], object_keys))
-            self.assertTrue(
-                sg == sg,
-                "The same security groups should be equal?")
-            self.assertFalse(
-                sg != sg,
-                "The same security groups should still be equal?")
-#             json_repr = json.dumps(
-#                 {"description": name, "name": name, "id": sg.id,
-#                  "rules":
-#                     [{"from_port": 1111, "group": "", "cidr_ip": "0.0.0.0/0",
-#                       "parent": sg.id, "to_port": 1111, "ip_protocol": "tcp",
-#                       "id": sg.rules[0].id}]},
-#                 sort_keys=True)
-#             self.assertTrue(
-#                 sg.to_json() == json_repr,
-#                 "JSON SG representation {0} does not match expected {1}"
-#                 .format(sg.to_json(), json_repr))
-
-        sgl = self.provider.security.security_groups.list()
-        found_sg = [g for g in sgl if g.name == name]
-        self.assertTrue(
-            len(found_sg) == 0,
-            "Security group {0} should have been deleted but still exists."
-            .format(name))
 
-    @helpers.skipIfNoService(['security.security_groups'])
-    def test_security_group_rule_add_twice(self):
-        """Test whether adding the same rule twice succeeds."""
-        if isinstance(self.provider, TestMockHelperMixin):
-            raise unittest.SkipTest(
-                "Mock provider returns InvalidParameterValue: "
-                "Value security_group is invalid for parameter.")
+            rule = fw.rules.create(
+                direction=TrafficDirection.INBOUND, protocol='tcp',
+                from_port=1111, to_port=1111, cidr='0.0.0.0/0')
+            self.assertEqual(rule.direction, TrafficDirection.INBOUND)
+            self.assertEqual(rule.protocol, 'tcp')
+            self.assertEqual(rule.from_port, 1111)
+            self.assertEqual(rule.to_port, 1111)
+            self.assertEqual(rule.cidr, '0.0.0.0/0')
 
-        name = 'CBTestSecurityGroupC-{0}'.format(uuid.uuid4())
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_vm_firewall_rule_add_twice(self):
+        name = 'cb_fwruletwice-{0}'.format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
-        sg = None
+        fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, security_group=sg)):
+                network=net, vm_firewall=fw)):
 
             net, _ = helpers.create_test_network(self.provider, name)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
-            rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
-                               cidr_ip='0.0.0.0/0')
+
+            rule = fw.rules.create(
+                direction=TrafficDirection.INBOUND, protocol='tcp',
+                from_port=1111, to_port=1111, cidr='0.0.0.0/0')
             # attempting to add the same rule twice should succeed
-            same_rule = sg.add_rule(ip_protocol='tcp', from_port=1111,
-                                    to_port=1111, cidr_ip='0.0.0.0/0')
-            self.assertTrue(
-                rule == same_rule,
-                "Expected rule {0} not found in security group: {1}".format(
-                    same_rule, sg.rules))
+            same_rule = fw.rules.create(
+                direction=TrafficDirection.INBOUND, protocol='tcp',
+                from_port=1111, to_port=1111, cidr='0.0.0.0/0')
+            self.assertEqual(rule, same_rule)
 
-    @helpers.skipIfNoService(['security.security_groups'])
-    def test_security_group_group_rule(self):
-        """Test for proper creation of a security group rule."""
-        name = 'CBTestSecurityGroupD-{0}'.format(uuid.uuid4())
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_vm_firewall_group_rule(self):
+        name = 'cb_fwrule-{0}'.format(helpers.get_uuid())
 
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         net = None
-        sg = None
+        fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, security_group=sg)):
+                network=net, vm_firewall=fw)):
             net, _ = helpers.create_test_network(self.provider, name)
-            sg = self.provider.security.security_groups.create(
+            fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
-            self.assertTrue(
-                len(sg.rules) == 0,
-                "Expected no security group group rule. Got {0}."
-                .format(sg.rules))
-            rule = sg.add_rule(src_group=sg, ip_protocol='tcp', from_port=1,
-                               to_port=65535)
-            self.assertTrue(
-                rule.group.name == name,
-                "Expected security group rule name {0}. Got {1}."
-                .format(name, rule.group.name))
-            for r in sg.rules:
+            rules = list(fw.rules)
+            self.assertTrue(
+                # TODO: This should be made consistent across all providers.
+                # Currently, OpenStack creates two rules, one for IPV6 and
+                # another for IPV4
+                len(rules) >= 1, "Expected a single VM firewall rule allowing"
+                " all outbound traffic. Got {0}.".format(rules))
+            self.assertEqual(
+                rules[0].direction, TrafficDirection.OUTBOUND,
+                "Expected rule to be outbound. Got {0}.".format(rules))
+            rule = fw.rules.create(
+                direction=TrafficDirection.INBOUND, src_dest_fw=fw,
+                protocol='tcp', from_port=1, to_port=65535)
+            self.assertTrue(
+                rule.src_dest_fw.name == name,
+                "Expected VM firewall rule name {0}. Got {1}."
+                .format(name, rule.src_dest_fw.name))
+            for r in fw.rules:
                 r.delete()
-            sg = self.provider.security.security_groups.get(sg.id)  # update
+            fw = self.provider.security.vm_firewalls.get(fw.id)  # update
             self.assertTrue(
-                sg is None or len(sg.rules) == 0,
-                "Deleting SecurityGroupRule should delete it: {0}".format(
-                    [] if sg is None else sg.rules))
-        sgl = self.provider.security.security_groups.list()
-        found_sg = [g for g in sgl if g.name == name]
+                len(list(fw.rules)) == 0,
+                "Deleting VMFirewallRule should delete it: {0}".format(
+                    fw.rules))
+        fwl = self.provider.security.vm_firewalls.list()
+        found_fw = [f for f in fwl if f.name == name]
         self.assertTrue(
-            len(found_sg) == 0,
-            "Security group {0} should have been deleted but still exists."
+            len(found_fw) == 0,
+            "VM firewall {0} should have been deleted but still exists."
             .format(name))

+ 76 - 0
test/test_vm_types_service.py

@@ -0,0 +1,76 @@
+from test import helpers
+
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
+
+import six
+
+
+class CloudVMTypeServiceTestCase(ProviderTestBase):
+
+    @helpers.skipIfNoService(['compute.vm_types'])
+    def test_vm_type_properties(self):
+
+        for vm_type in self.provider.compute.vm_types:
+            sit.check_repr(self, vm_type)
+            self.assertIsNotNone(
+                vm_type.id,
+                "VMType id must have a value")
+            self.assertIsNotNone(
+                vm_type.name,
+                "VMType name must have a value")
+            self.assertTrue(
+                vm_type.family is None or isinstance(
+                    vm_type.family,
+                    six.string_types),
+                "VMType family family be None or a"
+                " string but is: {0}".format(vm_type.family))
+            self.assertTrue(
+                vm_type.vcpus is None or (
+                    isinstance(vm_type.vcpus, six.integer_types) and
+                    vm_type.vcpus >= 0),
+                "VMType vcpus family be None or a positive integer")
+            self.assertTrue(
+                vm_type.ram is None or vm_type.ram >= 0,
+                "VMType ram must be None or a positive number")
+            self.assertTrue(
+                vm_type.size_root_disk is None or
+                vm_type.size_root_disk >= 0,
+                "VMType size_root_disk must be None or a positive number"
+                " but is: {0}".format(vm_type.size_root_disk))
+            self.assertTrue(
+                vm_type.size_ephemeral_disks is None or
+                vm_type.size_ephemeral_disks >= 0,
+                "VMType size_ephemeral_disk must be None or a positive"
+                " number")
+            self.assertTrue(
+                isinstance(vm_type.num_ephemeral_disks,
+                           six.integer_types) and
+                vm_type.num_ephemeral_disks >= 0,
+                "VMType num_ephemeral_disks must be None or a positive"
+                " number")
+            self.assertTrue(
+                vm_type.size_total_disk is None or
+                vm_type.size_total_disk >= 0,
+                "VMType size_total_disk must be None or a positive"
+                " number")
+            self.assertTrue(
+                vm_type.extra_data is None or isinstance(
+                    vm_type.extra_data, dict),
+                "VMType extra_data must be None or a dict")
+
+    @helpers.skipIfNoService(['compute.vm_types'])
+    def test_vm_types_standard(self):
+        """
+        Searching for an instance by name should return an
+        VMType object and searching for a non-existent
+        object should return an empty iterator
+        """
+        vm_type_name = helpers.get_provider_test_data(
+            self.provider,
+            "vm_type")
+        vm_type = self.provider.compute.vm_types.find(
+            name=vm_type_name)[0]
+
+        sit.check_standard_behaviour(
+                self, self.provider.compute.vm_types, vm_type)

Некоторые файлы не были показаны из-за большого количества измененных файлов