Quellcode durchsuchen

Merge branch 'master' into gce

Nuwan Goonasekera vor 7 Jahren
Ursprung
Commit
2fcd798e2b
76 geänderte Dateien mit 4517 neuen und 2581 gelöschten Zeilen
  1. 9 0
      .gitignore
  2. 56 52
      .travis.yml
  3. 69 28
      CHANGELOG.rst
  4. 1 1
      LICENSE
  5. 46 51
      README.rst
  6. 1 1
      cloudbridge/__init__.py
  7. 75 5
      cloudbridge/cloud/base/helpers.py
  8. 12 5
      cloudbridge/cloud/base/provider.py
  9. 73 243
      cloudbridge/cloud/base/resources.py
  10. 86 45
      cloudbridge/cloud/base/services.py
  11. 7 8
      cloudbridge/cloud/factory.py
  12. 15 1
      cloudbridge/cloud/interfaces/exceptions.py
  13. 6 6
      cloudbridge/cloud/interfaces/provider.py
  14. 320 184
      cloudbridge/cloud/interfaces/resources.py
  15. 60 63
      cloudbridge/cloud/interfaces/services.py
  16. 20 26
      cloudbridge/cloud/providers/aws/helpers.py
  17. 7 6
      cloudbridge/cloud/providers/aws/provider.py
  18. 183 80
      cloudbridge/cloud/providers/aws/resources.py
  19. 268 146
      cloudbridge/cloud/providers/aws/services.py
  20. 301 69
      cloudbridge/cloud/providers/azure/azure_client.py
  21. 47 22
      cloudbridge/cloud/providers/azure/helpers.py
  22. 84 39
      cloudbridge/cloud/providers/azure/provider.py
  23. 285 185
      cloudbridge/cloud/providers/azure/resources.py
  24. 334 307
      cloudbridge/cloud/providers/azure/services.py
  25. 18 21
      cloudbridge/cloud/providers/openstack/provider.py
  26. 235 110
      cloudbridge/cloud/providers/openstack/resources.py
  27. 194 194
      cloudbridge/cloud/providers/openstack/services.py
  28. 0 0
      codeclimate.yml
  29. 2 2
      docs/api_docs/cloud/exceptions.rst
  30. 34 0
      docs/concepts.rst
  31. 177 162
      docs/extras/_images/object_relationships_detailed.svg
  32. 87 21
      docs/getting_started.rst
  33. 169 0
      docs/topics/aws_mapping.rst
  34. 223 0
      docs/topics/azure_mapping.rst
  35. BIN
      docs/topics/captures/aws-ami-dash.png
  36. BIN
      docs/topics/captures/aws-bucket.png
  37. BIN
      docs/topics/captures/aws-instance-dash.png
  38. BIN
      docs/topics/captures/aws-services-dash.png
  39. BIN
      docs/topics/captures/az-label-dash.png
  40. BIN
      docs/topics/captures/az-net-id.png
  41. BIN
      docs/topics/captures/az-net-label.png
  42. BIN
      docs/topics/captures/az-storacc.png
  43. BIN
      docs/topics/captures/az-subnet-label.png
  44. BIN
      docs/topics/captures/az-subnet-name.png
  45. BIN
      docs/topics/captures/os-instance-dash.png
  46. BIN
      docs/topics/captures/os-kp-dash.png
  47. 1 1
      docs/topics/contributor_guide.rst
  48. 0 20
      docs/topics/design-decisions.rst
  49. 126 0
      docs/topics/design_decisions.rst
  50. 13 12
      docs/topics/install.rst
  51. 13 11
      docs/topics/launch.rst
  52. 6 6
      docs/topics/networking.rst
  53. 110 0
      docs/topics/os_mapping.rst
  54. 5 5
      docs/topics/provider_development.rst
  55. 33 8
      docs/topics/release_process.rst
  56. 65 0
      docs/topics/resource_types_and_mapping.rst
  57. 38 21
      docs/topics/setup.rst
  58. 6 4
      docs/topics/testing.rst
  59. 0 1
      requirements.txt
  60. 11 4
      setup.cfg
  61. 31 23
      setup.py
  62. 33 41
      test/helpers/__init__.py
  63. 115 38
      test/helpers/standard_interface_tests.py
  64. 61 56
      test/test_block_store_service.py
  65. 2 1
      test/test_cloud_factory.py
  66. 29 1
      test/test_cloud_helpers.py
  67. 76 67
      test/test_compute_service.py
  68. 56 16
      test/test_image_service.py
  69. 2 1
      test/test_interface.py
  70. 76 49
      test/test_network_service.py
  71. 5 6
      test/test_object_life_cycle.py
  72. 29 34
      test/test_object_store_service.py
  73. 4 4
      test/test_region_service.py
  74. 60 62
      test/test_security_service.py
  75. 5 4
      test/test_vm_types_service.py
  76. 2 2
      tox.ini

+ 9 - 0
.gitignore

@@ -60,3 +60,12 @@ target/
 /venv/
 
 credentials.tar.gz
+bootstrap.py
+ISB-*
+launch.json
+settings.json
+run_nose.py
+*ipynb*
+
+# PyCharm
+.idea/

+ 56 - 52
.travis.yml

@@ -35,59 +35,63 @@ matrix:
     env: TOX_ENV=pypy-gce
   - python: pypy-5.3.1
     env: TOX_ENV=pypy-openstack
+env:
+  global:
+    - PYTHONUNBUFFERED=True
 before_install:
-- openssl aes-256-cbc -K $encrypted_b3fcf6d0737c_key -iv $encrypted_b3fcf6d0737c_iv
-  -in credentials.tar.gz.enc -out credentials.tar.gz -d
-- |
-  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
+	- openssl aes-256-cbc -K $encrypted_b3fcf6d0737c_key -iv $encrypted_b3fcf6d0737c_iv
+  		-in credentials.tar.gz.enc -out credentials.tar.gz -d
+    - |
+      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 -U setuptools
-- pip install tox
-- pip install coveralls
-- pip install codecov
+    - pip install -U pip
+    - pip install -U setuptools
+    - pip install tox
+    - pip install coveralls
+    - pip install codecov
 script:
-- tox -e $TOX_ENV
+    - tox -r -e $TOX_ENV
 after_script:
-- |
-  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"
-       coveralls & codecov & wait
-       ;;
-  esac
+    - |
+      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"
+           coveralls & codecov & wait
+           ;;
+      esac

+ 69 - 28
CHANGELOG.rst

@@ -1,18 +1,59 @@
-0.3.3 - Aug 7, 2017. (sha 348e1e88935f61f53a83ed8d6a0e012a46621e25)
+1.0.2 - September 25, 2018 (sha 621aeed1a8d7c5ad270649f8ee960e9682e57dae)
 -------
+* Added AWS instance types caching for better performance
+* Added ``router.subnets`` property
+* Ensure the default network for CloudBridge on AWS has subnets
 
-* Remove explicit versioning of requests and Babel
+1.0.1 - September 7, 2018. (sha 3130492008c5e0e115b8dfec880d32a4ac90b761)
+-------
+* Fixed minor bug when retrieving buckets with only limited access.
+* Relaxed some library version dependencies (e.g. six).
+
+1.0.0 - September 6, 2018. (sha 11bccd822f21a598fc753995440cf1a409984889)
+-------
+
+* Added Microsoft Azure as a provider.
+* Restructured the interface to make it more comprehensible and uniform across
+  all supported providers. See `issue #69 <https://github.com/CloudVE/cloudbridge/issues/69>`_
+  for more details as well as the library layout image for an easy visual
+  reference: https://github.com/CloudVE/cloudbridge#quick-reference.
+* Migrated AWS implementation to use the boto3 library (thanks @01000101)
+* Cleaned up use of ``name`` property for resources. Resources now have ``id``,
+  ``name``, and ``label`` properties to represent respectively: a unique
+  identifier supplied by the provider; a descriptive, unchangeable name; and a
+  user-supplied label that can be modified during the existence of a resource.
+* Added enforcement of name and label value: names must be at least 3 characters
+  in length at minimum, and 64 characters at maximum, consisting of only lower
+  case letters and dashes. Should not start or end with a dash.
+* Refactored tests and extracted standard interface tests where all resources
+  are being tested using the same code structure. Also, tests will run only
+  for providers that implement a given service.
+* Moved the repository from github.com/gvlproject to github.com/cloudve org.
+* When deleting an OpenStack network, clear any ports.
+* Added support for launching OpenStack instances into a specific subnet
+* Update image list interface to allow filtering by owner.
+* When listing images on AWS, filter only the ones by current account owner.
+* Retrieve AWS instance types from a public service to include latest values.
+* Instance state uses ``DELETED`` state instead of ``TERMINATED``.
+* Return VM type RAM in GB.
+* Add implementation for ``generate_url`` on OpenStack.
+* General documentation updates.
+
+0.3.3 - August 7, 2017. (sha 348e1e88935f61f53a83ed8d6a0e012a46621e25)
+-------
+
+* Remove explicit versioning of requests and Babel.
 
 0.3.2 - June 10, 2017. (sha f07f3cbd758a0872b847b5537d9073c90f87c24d)
 -------
 
-* Patch release to support files>5GB with OpenStack (thanks @MartinPaulo)
-* Misc bug fixes
+* Patch release to support files>5GB with OpenStack (thanks @MartinPaulo).
+* Misc bug fixes.
 
 0.3.1 - April 18, 2017. (sha f36a462e886d8444cb2818f6573677ecf0565315)
 -------
 
-* Patch for binary file handling in openstack
+* Patch for binary file handling in OpenStack.
 
 0.3.0 - April 11, 2017. (sha 13539ccda9e4809082796574d18b1b9bb3f2c624)
 -------
@@ -23,9 +64,9 @@
 * Added supports for accessing EC2 containers with restricted permissions.
 * Removed exists() method from object store interface. Use get()==None check
   instead.
-* New method (img.min_disk) for geting size of machine image.
-* Test improvements (flake8 during build, more tests)
-* Misc bug fixes and improvements
+* New method (img.min_disk) for getting size of machine image.
+* Test improvements (flake8 during build, more tests).
+* Misc bug fixes and improvements.
 * Changed library to beta state
 * General documentation updates (testing, release process)
 
@@ -36,35 +77,35 @@
   removed the option of adding network interface to a launch config object.
 * Added object store methods: upload from file path, list objects with a
   prefix, check if an object exists, (AWS only) get an accessible URL for an
-  object (thanks @VJalili)
+  object (thanks @VJalili).
 * Modified `get_ec2_credentials()` method to `get_or_create_ec2_credentials()`
 * Added an option to read provider config values from a file
-  (`~/.cloudbridge` or `/etc/cloudbridge`)
-* Replaced py35 with py36 for running tests
-* Added logging configuration for the library
-* General documentation updates
+  (`~/.cloudbridge` or `/etc/cloudbridge`).
+* Replaced py35 with py36 for running tests.
+* Added logging configuration for the library.
+* General documentation updates.
 
 
 0.1.1 - Aug 10, 2016. (sha 0122fb1173c88ae64e40140ffd35ff3797e9e4ad)
 -------
 
-* For AWS, always launch instances into private networking (i.e., VPC)
-* Support for using OpenStack Keystone v3
-* Add functionality to manipulate routers and routes
-* Add FloatingIP resource type and integrate with Network service
-* Numerous documentation updates
-* For an OpenStack provider, add method to get the ec2 credentials for a user
+* For AWS, always launch instances into private networking (i.e., VPC).
+* Support for using OpenStack Keystone v3.
+* Add functionality to manipulate routers and routes.
+* Add FloatingIP resource type and integrate with Network service.
+* Numerous documentation updates.
+* For an OpenStack provider, add method to get the ec2 credentials for a user.
 
 
 0.1.0 - Jan 30, 2016.
 -------
 
-* Initial release of CloudBridge
-* Support for Bucket, Instance, Instance type, Key pair, Machine image
-  Region, Security group, Snapshot, Volume, Network and Subnet services
-* Support for paging results, block device mapping and launching into VPCs
-* Support for AWS and OpenStack clouds
-* Basic usage docs and complete API docs
-* 95% test coverage
-* Support for AWS mock test provder (via
-  `moto <https://github.com/spulec/moto>`_)
+* Initial release of CloudBridge.
+* Support for Bucket, Instance, Instance type, Key pair, Machine image.
+  Region, Security group, Snapshot, Volume, Network and Subnet services.
+* Support for paging results, block device mapping and launching into VPCs.
+* Support for AWS and OpenStack clouds.
+* Basic usage docs and complete API docs.
+* 95% test coverage.
+* Support for AWS mock test provider (via
+  `moto <https://github.com/spulec/moto>`_).

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015 gvlproject
+Copyright (c) 2015 CloudVE
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 46 - 51
README.rst

@@ -1,19 +1,11 @@
-CloudBridge aims to provide a simple layer of abstraction over
-different cloud providers, reducing or eliminating the need to write
-conditional code for each cloud.
+CloudBridge provides a simple layer of abstraction over different
+Infrastructure-as-a-Service cloud providers, reducing or eliminating the need
+to write conditional code for each cloud.
 
-.. image:: https://landscape.io/github/gvlproject/cloudbridge/master/landscape.svg?style=flat
-   :target: https://landscape.io/github/gvlproject/cloudbridge/master
-   :alt: Landscape Code Health
-
-.. image:: https://coveralls.io/repos/gvlproject/cloudbridge/badge.svg?branch=master&service=github
-   :target: https://coveralls.io/github/gvlproject/cloudbridge?branch=master
+.. image:: https://coveralls.io/repos/CloudVE/cloudbridge/badge.svg?branch=master&service=github
+   :target: https://coveralls.io/github/CloudVE/cloudbridge?branch=master
    :alt: Code Coverage
 
-.. image:: https://codeclimate.com/github/gvlproject/cloudbridge/badges/gpa.svg
-   :target: https://codeclimate.com/github/gvlproject/cloudbridge
-   :alt: Code Climate
-
 .. image:: https://img.shields.io/pypi/v/cloudbridge.svg
    :target: https://pypi.python.org/pypi/cloudbridge/
    :alt: latest version available on PyPI
@@ -22,52 +14,52 @@ 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
+.. image:: https://badge.waffle.io/CloudVE/cloudbridge.png?label=in%20progress&title=In%20Progress 
+   :target: https://waffle.io/CloudVE/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/4
-              :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/7
-              :target: https://travis-ci.org/gvlproject/cloudbridge
-
-.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/3
-             :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/6
-             :target: https://travis-ci.org/gvlproject/cloudbridge
-.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/9
-             :target: https://travis-ci.org/gvlproject/cloudbridge
-
-.. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/2
-                :target: https://travis-ci.org/gvlproject/cloudbridge/branches
-.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/5
-                :target: https://travis-ci.org/gvlproject/cloudbridge/branches
-.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/master/8
-                :target: https://travis-ci.org/gvlproject/cloudbridge/branches
-
-.. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/gvlproject/cloudbridge/branches/gce/3
-              :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
-~~~~~~~~~~~~
+.. |aws-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/1
+              :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/4
+              :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/7
+              :target: https://travis-ci.org/CloudVE/cloudbridge
+
+.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/3
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/6
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/9
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+
+.. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/2
+                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/5
+                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/8
+                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+
+.. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/3
+              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+.. |gce-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/6
+              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+.. |gce-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/9
+              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+
+
+Build Status Tests
+~~~~~~~~~~~~~~~~~~
 
 +--------------------------+--------------+--------------+--------------+
 | **Provider/Environment** | py27         | py36         | pypy         |
 +--------------------------+--------------+--------------+--------------+
-| **aws**                  | |aws-py27|   | |aws-py36|   | |aws-pypy|   |
+| **AWS**                  | |aws-py27|   | |aws-py36|   | |aws-pypy|   |
 +--------------------------+--------------+--------------+--------------+
-| **openstack**            | |os-py27|    | |os-py36|    | |os-pypy|    |
+| **OpenStack**            | |os-py27|    | |os-py36|    | |os-pypy|    |
 +--------------------------+--------------+--------------+--------------+
-| **azure (beta)**         | |azure-py27| | |azure-py36| | |azure-py36| |
+| **Azure**                | |azure-py27| | |azure-py36| | |azure-py36| |
 +--------------------------+--------------+--------------+--------------+
-| **gce (alpha)**          | |gce-py27|   | |gce-py36|   | |gce-pypy|   |
+| **GCE (alpha)**          | |gce-py27|   | |gce-py36|   | |gce-pypy|   |
 +--------------------------+--------------+--------------+--------------+
 
 Installation
@@ -96,6 +88,9 @@ exploring the API:
   provider = CloudProviderFactory().create_provider(ProviderList.AWS, {})
   print(provider.security.key_pairs.list())
 
+The exact same command (as well as any other CloudBridge method) will run with
+any of the supported providers: ``ProviderList.[AWS | AZURE | OPENSTACK]``!
+
 
 Citation
 ~~~~~~~~
@@ -154,7 +149,7 @@ Community contributions for any part of the project are welcome. If you have
 a completely new idea or would like to bounce your idea before moving forward
 with the implementation, feel free to create an issue to start a discussion.
 
-Contributions should come in the form or a pull request. We strive for 100% test
+Contributions should come in the form of a pull request. We strive for 100% test
 coverage so code will only be accepted if it comes with appropriate tests and it
 does not break existing functionality. Further, the code needs to be well
 documented and all methods have docstrings. We are largely adhering to the

+ 1 - 1
cloudbridge/__init__.py

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

+ 75 - 5
cloudbridge/cloud/base/helpers.py

@@ -1,3 +1,7 @@
+import fnmatch
+import functools
+import os
+import re
 import sys
 import traceback
 from contextlib import contextmanager
@@ -6,7 +10,11 @@ from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization as crypt_serialization
 from cryptography.hazmat.primitives.asymmetric import rsa
 
-from six import reraise
+from deprecation import deprecated
+
+import six
+
+import cloudbridge
 
 
 def generate_key_pair():
@@ -38,9 +46,17 @@ def filter_by(prop_name, kwargs, objs):
     """
     prop_val = kwargs.pop(prop_name, None)
     if prop_val:
-        match = (o for o in objs if getattr(o, prop_name) == prop_val)
-        return match
-    return objs
+        if isinstance(prop_val, six.string_types):
+            regex = fnmatch.translate(prop_val)
+            results = [o for o in objs
+                       if getattr(o, prop_name)
+                       and re.search(regex, getattr(o, prop_name))]
+        else:
+            results = [o for o in objs
+                       if getattr(o, prop_name) == prop_val]
+        return results
+    else:
+        return objs
 
 
 def generic_find(filter_names, kwargs, objs):
@@ -87,9 +103,63 @@ def cleanup_action(cleanup_func):
         except Exception as e:
             print("Error during exception cleanup: {0}".format(e))
             traceback.print_exc()
-        reraise(ex_class, ex_val, ex_traceback)
+        six.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 get_env(varname, default_value=None):
+    """
+    Return the value of the environment variable or default_value.
+
+    This is a helper method that wraps ``os.environ.get`` to ensure type
+    compatibility across py2 and py3. For py2, any value obtained from an
+    environment variable, ensure ``unicode`` type and ``str`` for py3. The
+    casting is done only for string variables.
+
+    :type varname: ``str``
+    :param varname: Name of the environment variable for which to check.
+
+    :param default_value: Return this value is the env var is not found.
+                          Defaults to ``None``.
+
+    :return: Value of the supplied environment if found; value of
+             ``default_value`` otherwise.
+    """
+    value = os.environ.get(varname, default_value)
+    if isinstance(value, six.string_types) and not isinstance(
+            value, six.text_type):
+        return six.u(value)
+    return value
+
+
+# Alias deprication decorator, following:
+# https://stackoverflow.com/questions/49802412/
+# how-to-implement-deprecation-in-python-with-argument-alias
+def deprecated_alias(**aliases):
+    def deco(f):
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            rename_kwargs(f.__name__, kwargs, aliases)
+            return f(*args, **kwargs)
+        return wrapper
+    return deco
+
+
+def rename_kwargs(func_name, kwargs, aliases):
+    for alias, new in aliases.items():
+        if alias in kwargs:
+            if new in kwargs:
+                raise TypeError('{} received both {} and {}'.format(
+                    func_name, alias, new))
+            # Manually invoke the deprecated decorator with an empty lambda
+            # to signal deprecation
+            deprecated(deprecated_in='1.1',
+                       removed_in='2.0',
+                       current_version=cloudbridge.__version__,
+                       details='{} is deprecated, use {} instead'.format(
+                           alias, new))(lambda: None)()
+            kwargs[new] = kwargs.pop(alias)

+ 12 - 5
cloudbridge/cloud/base/provider.py

@@ -8,6 +8,8 @@ try:
 except ImportError:  # Python 2
     from ConfigParser import SafeConfigParser as ConfigParser
 
+import six
+
 from cloudbridge.cloud.interfaces import CloudProvider
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 from cloudbridge.cloud.interfaces.resources import Configuration
@@ -148,12 +150,17 @@ class BaseCloudProvider(CloudProvider):
 
         :return: a configuration value for the supplied ``key``
         """
-        log.info("Getting config key: %s with default: %s", key, default_value)
+        log.debug("Getting config key %s, with supplied default value: %s",
+                  key, default_value)
+        value = default_value
         if isinstance(self.config, dict) and self.config.get(key):
-            return self.config.get(key, default_value)
+            value = self.config.get(key, default_value)
         elif hasattr(self.config, key) and getattr(self.config, key):
-            return getattr(self.config, key)
+            value = getattr(self.config, key)
         elif (self._config_parser.has_option(self.PROVIDER_ID, key) and
               self._config_parser.get(self.PROVIDER_ID, key)):
-            return self._config_parser.get(self.PROVIDER_ID, key)
-        return default_value
+            value = self._config_parser.get(self.PROVIDER_ID, key)
+        if isinstance(value, six.string_types) and not isinstance(
+                value, six.text_type):
+            return six.u(value)
+        return value

+ 73 - 243
cloudbridge/cloud/base/resources.py

@@ -8,10 +8,14 @@ import os
 import re
 import shutil
 import time
+import uuid
+
+import six
 
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import InvalidLabelException
 from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
@@ -50,8 +54,6 @@ from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 
-import six
-
 log = logging.getLogger(__name__)
 
 
@@ -59,154 +61,51 @@ 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)
+    # Regular expression for valid cloudbridge resource names/labels.
+    # Can be alphanumeric string that does not start or end with a dash
+    # Must be at least 3 characters in length.
+    # Ref: https://stackoverflow.com/questions/2525327/regex-for-a-za-z0-9
+    # -with-dashes-allowed-in-between-but-not-at-the-start-or-e
+    CB_NAME_PATTERN = re.compile(r"^[a-z][-a-z0-9]{1,61}[a-z0-9]$")
 
     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
+        if not name:
+            return False
+        else:
+            return (True if BaseCloudResource.CB_NAME_PATTERN.match(name)
+                    else False)
+
+    @staticmethod
+    def assert_valid_resource_label(name):
+        if not BaseCloudResource.is_valid_resource_name(name):
+            log.debug("InvalidLabelException raised on %s", name)
+            raise InvalidLabelException(
+                u"Invalid label: %s. Label must be at least 3 characters long"
+                " and at most 63 characters. It must consist of lowercase"
+                " letters, numbers, or dashes. The label must start with a "
+                "letter and not end with a dash." % name)
 
     @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)
+            log.debug("InvalidLabelException raised on %s", name)
             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)
+                u"Invalid name: %s. Name must be at least 3 characters long"
+                " and at most 63 characters. It must consist of lowercase"
+                " letters, numbers, or dashes. The name must not start or"
+                " end with a dash." % name)
+
+    @staticmethod
+    def _generate_name_from_label(label, default):
+        if not label:
+            label = default
+        name = label[:55] + '-' + uuid.uuid4().hex[:6]
+        BaseCloudResource.assert_valid_resource_name(name)
+        return name
 
     @property
     def _provider(self):
@@ -218,6 +117,15 @@ class BaseCloudResource(CloudResource):
         js = {k: v for(k, v) in attr if not k.startswith('_')}
         return js
 
+    def __repr__(self):
+        name_or_label = getattr(self, 'label', self.name)
+        if name_or_label == self.id:
+            return "<CB-{0}: {1}>".format(
+                self.__class__.__name__, self.id)
+        else:
+            return "<CB-{0}: {1} ({2})>".format(
+                self.__class__.__name__, name_or_label, self.id)
+
 
 class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
     """
@@ -229,6 +137,7 @@ class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
     """
 
     def wait_for(self, target_states, terminal_states=None, timeout=None,
+
                  interval=None):
         if timeout is None:
             timeout = self._provider.config.default_wait_timeout
@@ -388,10 +297,6 @@ class BaseVMType(BaseCloudResource, VMType):
     def size_total_disk(self):
         return self.size_root_disk + self.size_ephemeral_disks
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
 
@@ -405,7 +310,7 @@ class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
                 self.id == other.id and
                 # check from most to least likely mutables
                 self.state == other.state and
-                self.name == other.name and
+                self.label == other.label and
                 self.vm_firewalls == other.vm_firewalls and
                 self.public_ips == other.public_ips and
                 self.private_ips == other.private_ips and
@@ -418,10 +323,6 @@ class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
             timeout=timeout,
             interval=interval)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseLaunchConfig(LaunchConfig):
 
@@ -509,7 +410,7 @@ class BaseMachineImage(
                 self.id == other.id and
                 # check from most to least likely mutables
                 self.state == other.state and
-                self.name == other.name and
+                self.label == other.label and
                 self.description == other.description)
 
     def wait_till_ready(self, timeout=None, interval=None):
@@ -519,10 +420,6 @@ class BaseMachineImage(
             timeout=timeout,
             interval=interval)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseAttachmentInfo(AttachmentInfo):
 
@@ -556,7 +453,7 @@ class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
                 self.id == other.id and
                 # check from most to least likely mutables
                 self.state == other.state and
-                self.name == other.name)
+                self.label == other.label)
 
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
@@ -565,10 +462,6 @@ class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
             timeout=timeout,
             interval=interval)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
 
@@ -582,7 +475,7 @@ class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
                 self.id == other.id and
                 # check from most to least likely mutables
                 self.state == other.state and
-                self.name == other.name)
+                self.label == other.label)
 
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
@@ -591,10 +484,6 @@ class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
             timeout=timeout,
             interval=interval)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseKeyPair(BaseCloudResource, KeyPair):
 
@@ -621,7 +510,7 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         """
         Return the name of this key pair.
         """
-        return self._key_pair.name
+        return self.id
 
     @property
     def material(self):
@@ -643,9 +532,6 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         #  multiple providers.
         self._key_pair.delete()
 
-    def __repr__(self):
-        return "<CBKeyPair: {0} ({1})>".format(self.name, self.id)
-
 
 class BaseVMFirewall(BaseCloudResource, VMFirewall):
 
@@ -680,7 +566,7 @@ class BaseVMFirewall(BaseCloudResource, VMFirewall):
         """
         Return the name of this VM firewall.
         """
-        return self._vm_firewall.name
+        return self.id
 
     @property
     def description(self):
@@ -695,10 +581,6 @@ class BaseVMFirewall(BaseCloudResource, VMFirewall):
         """
         return self._vm_firewall.delete()
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.id, self.name)
-
 
 class BaseVMFirewallRuleContainer(BasePageableObjectMixin,
                                   VMFirewallRuleContainer):
@@ -796,10 +678,6 @@ class BasePlacementZone(BaseCloudResource, PlacementZone):
     def __init__(self, provider):
         super(BasePlacementZone, self).__init__(provider)
 
-    def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.id)
-
     def __eq__(self, other):
         return (isinstance(other, PlacementZone) and
                 # pylint:disable=protected-access
@@ -812,10 +690,6 @@ class BaseRegion(BaseCloudResource, Region):
     def __init__(self, provider):
         super(BaseRegion, self).__init__(provider)
 
-    def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.id)
-
     def __eq__(self, other):
         return (isinstance(other, Region) and
                 # pylint:disable=protected-access
@@ -825,7 +699,7 @@ class BaseRegion(BaseCloudResource, Region):
     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['zones'] = [z.name for z in self.zones]
+        js['zones'] = [z.id for z in self.zones]
         return js
 
 
@@ -844,13 +718,15 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
 
     @staticmethod
     def is_valid_resource_name(name):
-        return True if BaseBucketObject.CB_NAME_PATTERN.match(name) else False
+        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(
+            log.debug("InvalidLabelException raised on %s", name,
+                      exc_info=True)
+            raise InvalidLabelException(
                 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)
@@ -866,37 +742,12 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
                 # check from most to least likely mutables
                 self.name == other.name)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 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
@@ -905,10 +756,6 @@ class BaseBucket(BaseCloudResource, Bucket):
                 # check from most to least likely mutables
                 self.name == other.name)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.name, self.id)
-
 
 class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
 
@@ -930,16 +777,12 @@ class BaseGatewayContainer(GatewayContainer, BasePageableObjectMixin):
 
 class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
-    CB_DEFAULT_NETWORK_NAME = os.environ.get('CB_DEFAULT_NETWORK_NAME',
-                                             'cloudbridge-net')
+    CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
+                                              'cloudbridge-net')
 
     def __init__(self, provider):
         super(BaseNetwork, self).__init__(provider)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.id, self.name)
-
     @staticmethod
     def cidr_blocks_overlap(block1, block2):
         common_length = min(int(block1.split('/')[1]),
@@ -960,9 +803,9 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
             timeout=timeout,
             interval=interval)
 
-    def create_subnet(self, name, cidr_block, zone=None):
+    def create_subnet(self, label, cidr_block, zone=None):
         return self._provider.networking.subnets.create(
-            name=name, network=self, cidr_block=cidr_block, zone=zone)
+            label=label, network=self, cidr_block=cidr_block, zone=zone)
 
     def __eq__(self, other):
         return (isinstance(other, Network) and
@@ -973,22 +816,22 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
 class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
-    CB_DEFAULT_SUBNET_NAME = os.environ.get('CB_DEFAULT_SUBNET_NAME',
-                                            'cloudbridge-subnet')
+    CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
+                                             'cloudbridge-subnet')
 
     def __init__(self, provider):
         super(BaseSubnet, 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, Subnet) and
                 # pylint:disable=protected-access
                 self._provider == other._provider and
                 self.id == other.id)
 
+    @property
+    def network(self):
+        return self._provider.networking.networks.get(self.network_id)
+
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
             [SubnetState.AVAILABLE],
@@ -1026,7 +869,6 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
 
     @property
     def name(self):
-        # VM firewall rules don't support names, so pass
         return self.public_ip
 
     @property
@@ -1041,10 +883,6 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
             timeout=timeout,
             interval=interval)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
-                                            self.id, self.public_ip)
-
     def __eq__(self, other):
         return (isinstance(other, FloatingIP) and
                 # pylint:disable=protected-access
@@ -1054,16 +892,12 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
 
 class BaseRouter(BaseCloudResource, Router):
 
-    CB_DEFAULT_ROUTER_NAME = os.environ.get('CB_DEFAULT_ROUTER_NAME',
-                                            'cloudbridge-router')
+    CB_DEFAULT_ROUTER_LABEL = os.environ.get('CB_DEFAULT_ROUTER_LABEL',
+                                             'cloudbridge-router')
 
     def __init__(self, provider):
         super(BaseRouter, self).__init__(provider)
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,
-                                            self.name)
-
     def __eq__(self, other):
         return (isinstance(other, Router) and
                 # pylint:disable=protected-access
@@ -1074,17 +908,13 @@ class BaseRouter(BaseCloudResource, Router):
 class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
                           InternetGateway):
 
-    CB_DEFAULT_INET_GATEWAY_NAME = os.environ.get(
+    CB_DEFAULT_INET_GATEWAY_NAME = cb_helpers.get_env(
         'CB_DEFAULT_INET_GATEWAY_NAME', 'cloudbridge-inetgateway')
 
     def __init__(self, provider):
         super(BaseInternetGateway, self).__init__(provider)
         self.__provider = provider
 
-    def __repr__(self):
-        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,
-                                            self.name)
-
     def __eq__(self, other):
         return (isinstance(other, InternetGateway) and
                 # pylint:disable=protected-access

+ 86 - 45
cloudbridge/cloud/base/services.py

@@ -4,6 +4,10 @@ Base implementation for services available through a provider
 import logging
 
 import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.resources import BaseNetwork
+from cloudbridge.cloud.base.resources import BaseRouter
+from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import Router
 from cloudbridge.cloud.interfaces.services import BucketService
 from cloudbridge.cloud.interfaces.services import CloudService
@@ -39,24 +43,42 @@ class BaseCloudService(CloudService):
         return self._provider
 
 
-class BaseComputeService(ComputeService, BaseCloudService):
+class BaseSecurityService(SecurityService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseComputeService, self).__init__(provider)
+        super(BaseSecurityService, self).__init__(provider)
 
 
-class BaseVolumeService(
-        BasePageableObjectMixin, VolumeService, BaseCloudService):
+class BaseKeyPairService(
+        BasePageableObjectMixin, KeyPairService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseVolumeService, self).__init__(provider)
+        super(BaseKeyPairService, self).__init__(provider)
 
+    def delete(self, key_pair_id):
+        """
+        Delete an existing key pair.
 
-class BaseSnapshotService(
-        BasePageableObjectMixin, SnapshotService, BaseCloudService):
+        :type key_pair_id: str
+        :param key_pair_id: The id of the key pair to be deleted.
+
+        :rtype: ``bool``
+        :return:  ``True`` if the key does not exist. Note that this implies
+                  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 BaseVMFirewallService(
+        BasePageableObjectMixin, VMFirewallService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseSnapshotService, self).__init__(provider)
+        super(BaseVMFirewallService, self).__init__(provider)
 
 
 class BaseStorageService(StorageService, BaseCloudService):
@@ -65,56 +87,45 @@ class BaseStorageService(StorageService, BaseCloudService):
         super(BaseStorageService, self).__init__(provider)
 
 
-class BaseImageService(
-        BasePageableObjectMixin, ImageService, BaseCloudService):
+class BaseVolumeService(
+        BasePageableObjectMixin, VolumeService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseImageService, self).__init__(provider)
+        super(BaseVolumeService, self).__init__(provider)
 
 
-class BaseBucketService(
-        BasePageableObjectMixin, BucketService, BaseCloudService):
+class BaseSnapshotService(
+        BasePageableObjectMixin, SnapshotService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseBucketService, self).__init__(provider)
+        super(BaseSnapshotService, self).__init__(provider)
 
 
-class BaseSecurityService(SecurityService, BaseCloudService):
+class BaseBucketService(
+        BasePageableObjectMixin, BucketService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseSecurityService, self).__init__(provider)
+        super(BaseBucketService, self).__init__(provider)
 
 
-class BaseKeyPairService(
-        BasePageableObjectMixin, KeyPairService, BaseCloudService):
+class BaseComputeService(ComputeService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseKeyPairService, self).__init__(provider)
+        super(BaseComputeService, self).__init__(provider)
 
-    def delete(self, key_pair_id):
-        """
-        Delete an existing key pair.
 
-        :type key_pair_id: str
-        :param key_pair_id: The id of the key pair to be deleted.
+class BaseImageService(
+        BasePageableObjectMixin, ImageService, BaseCloudService):
 
-        :rtype: ``bool``
-        :return:  ``True`` if the key does not exist. Note that this implies
-                  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
+    def __init__(self, provider):
+        super(BaseImageService, self).__init__(provider)
 
 
-class BaseVMFirewallService(
-        BasePageableObjectMixin, VMFirewallService, BaseCloudService):
+class BaseInstanceService(
+        BasePageableObjectMixin, InstanceService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseVMFirewallService, self).__init__(provider)
+        super(BaseInstanceService, self).__init__(provider)
 
 
 class BaseVMTypeService(
@@ -134,13 +145,6 @@ class BaseVMTypeService(
         return ClientPagedResultList(self._provider, list(matches))
 
 
-class BaseInstanceService(
-        BasePageableObjectMixin, InstanceService, BaseCloudService):
-
-    def __init__(self, provider):
-        super(BaseInstanceService, self).__init__(provider)
-
-
 class BaseRegionService(
         BasePageableObjectMixin, RegionService, BaseCloudService):
 
@@ -179,6 +183,18 @@ class BaseNetworkService(
             log.info("Deleting network %s", network_id)
             network.delete()
 
+    def get_or_create_default(self):
+        networks = self.provider.networking.networks.find(
+            label=BaseNetwork.CB_DEFAULT_NETWORK_LABEL)
+
+        if networks:
+            return networks[0]
+        else:
+            log.info("Creating a CloudBridge-default network labeled %s",
+                     BaseNetwork.CB_DEFAULT_NETWORK_LABEL)
+            return self.provider.networking.networks.create(
+                BaseNetwork.CB_DEFAULT_NETWORK_LABEL, '10.0.0.0/16')
+
 
 class BaseSubnetService(
         BasePageableObjectMixin, SubnetService, BaseCloudService):
@@ -188,10 +204,24 @@ class BaseSubnetService(
 
     def find(self, **kwargs):
         obj_list = self
-        filters = ['name']
+        filters = ['label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
         return ClientPagedResultList(self._provider, list(matches))
 
+    def get_or_create_default(self, zone):
+        default_cidr = '10.0.0.0/24'
+
+        # Look for a CB-default subnet
+        matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
+        if matches:
+            return matches[0]
+
+        # No provider-default Subnet exists, try to create it (net + subnets)
+        network = self.provider.networking.networks.get_or_create_default()
+        subnet = self.create(BaseSubnet.CB_DEFAULT_SUBNET_LABEL, network,
+                             default_cidr, zone)
+        return subnet
+
 
 class BaseRouterService(
         BasePageableObjectMixin, RouterService, BaseCloudService):
@@ -209,3 +239,14 @@ class BaseRouterService(
             if router:
                 log.info("Router %s successful deleted.", router)
                 router.delete()
+
+    def get_or_create_default(self, network):
+        net_id = network.id if isinstance(network, Network) else network
+        routers = self.provider.networking.routers.find(
+            label=BaseRouter.CB_DEFAULT_ROUTER_LABEL)
+        for router in routers:
+            if router.network_id == net_id:
+                return router
+        else:
+            return self.provider.networking.routers.create(
+                network=net_id, label=BaseRouter.CB_DEFAULT_ROUTER_LABEL)

+ 7 - 8
cloudbridge/cloud/factory.py

@@ -83,13 +83,13 @@ class CloudProviderFactory(object):
         Imports and registers providers from the given module name.
         Raises an ImportError if the import does not succeed.
         """
-        log.info("Importing providers from %s", module_name)
+        log.debug("Importing providers from %s", module_name)
         module = importlib.import_module(
             "{0}.{1}".format(providers.__name__,
                              module_name))
         classes = inspect.getmembers(module, inspect.isclass)
         for _, cls in classes:
-            log.info("Registering the provider: %s", cls)
+            log.debug("Registering the provider: %s", cls)
             self.register_provider_class(cls)
 
     def list_providers(self):
@@ -110,7 +110,7 @@ class CloudProviderFactory(object):
         """
         if not self.provider_list:
             self.discover_providers()
-        log.info("List of available providers: %s", self.provider_list)
+        log.debug("List of available providers: %s", self.provider_list)
         return self.provider_list
 
     def create_provider(self, name, config):
@@ -121,7 +121,8 @@ class CloudProviderFactory(object):
         cloud provider.
 
         :type name: str
-        :param name: Cloud provider name: one of ``aws``, ``openstack``.
+        :param name: Cloud provider name: one of ``aws``, ``openstack``,
+        ``azure``.
 
         :type config: an object with required fields
         :param config: This can be a Bunch or any other object whose fields can
@@ -131,8 +132,7 @@ class CloudProviderFactory(object):
         :return:  a concrete provider instance
         :rtype: ``object`` of :class:`.CloudProvider`
         """
-        log.info("Searching provider with the name %s on %s",
-                 name, config)
+        log.info("Creating '%s' provider", name)
         provider_class = self.get_provider_class(name)
         if provider_class is None:
             log.exception("A provider with the name %s could not "
@@ -140,8 +140,7 @@ class CloudProviderFactory(object):
             raise NotImplementedError(
                 'A provider with name {0} could not be'
                 ' found'.format(name))
-        log.debug("Found provider name: %s with these config "
-                  " details: %s", name, config)
+        log.debug("Created '%s' provider", name)
         return provider_class(config)
 
     def get_provider_class(self, name, get_mock=False):

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

@@ -50,7 +50,7 @@ class ProviderConnectionException(CloudBridgeBaseException):
 class InvalidNameException(CloudBridgeBaseException):
     """
     Marker interface for any attempt to set an invalid name on
-    a CloudBridge resource.An example would be setting uppercase
+    a CloudBridge resource. An example would be setting uppercase
     letters, which are not allowed in a resource name.
     """
 
@@ -58,6 +58,20 @@ class InvalidNameException(CloudBridgeBaseException):
         super(InvalidNameException, self).__init__(msg)
 
 
+class InvalidLabelException(InvalidNameException):
+    """
+    Marker interface for any attempt to set an invalid label on
+    a CloudBridge resource. An example would be setting uppercase
+    letters, which are not allowed in a resource label.
+    InvalidLabelExceptions inherit from, and are a special case
+    of InvalidNameExceptions. At present, these restrictions are
+    identical.
+    """
+
+    def __init__(self, msg):
+        super(InvalidLabelException, self).__init__(msg)
+
+
 class InvalidValueException(CloudBridgeBaseException):
     """
     Marker interface for any attempt to set an invalid value on a CloudBridge

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

@@ -33,9 +33,9 @@ class CloudProvider(object):
         """
         Returns the config object associated with this provider. This object
         is a subclass of :class:`dict` and will contain the properties
-        provided at initialization time. In addition, it also contains extra
-        provider-wide properties such as the default result limit for list()
-        queries.
+        provided at initialization time, grouped under `cloud_properties` and
+        `credentials` keys. In addition, it also contains extra provider-wide
+        properties such as the default result limit for `list()` queries.
 
         Example:
 
@@ -43,7 +43,7 @@ class CloudProvider(object):
 
             config = { 'aws_access_key' : '<my_key>' }
             provider = factory.create_provider(ProviderList.AWS, config)
-            print(provider.config.get('aws_access_key'))
+            print(provider.config['credentials'].get('aws_access_key'))
             print(provider.config.default_result_limit))
             # change provider result limit
             provider.config.default_result_limit = 100
@@ -146,8 +146,8 @@ class CloudProvider(object):
         .. code-block:: python
 
             networks = provider.networking.networks.list()
-            network = provider.networking.networks.create(
-                           name="DevNet", cidr_block='10.0.0.0/16')
+            subnets = provider.networking.subnets.list()
+            routers = provider.networking.routers.list()
 
         :rtype: :class:`.NetworkingService`
         :return:  a NetworkingService object

Datei-Diff unterdrückt, da er zu groß ist
+ 320 - 184
cloudbridge/cloud/interfaces/resources.py


+ 60 - 63
cloudbridge/cloud/interfaces/services.py

@@ -48,15 +48,15 @@ class ComputeService(CloudService):
 
             # print all images
             for image in provider.compute.images:
-                print(image.id, image.name)
+                print(image.id, image.name, image.label)
 
             # print only first 50 images
             for image in provider.compute.images.list(limit=50):
-                print(image.id, image.name)
+                print(image.id, image.name, image.label)
 
             # find image by name
-            image = provider.compute.images.find(name='Ubuntu 14.04')
-            print(image.id, image.name)
+            image = provider.compute.images.find(name='Ubuntu 16.04')[0]
+            print(image.id, image.name, image.label)
 
         :rtype: :class:`.ImageService`
         :return: an ImageService object
@@ -77,7 +77,7 @@ class ComputeService(CloudService):
                 print(vm_type.id, vm_type.name)
 
             # find a specific size by name
-            vm_type = provider.compute.vm_types.find(name='m1.small')
+            vm_type = provider.compute.vm_types.find(name='m1.small')[0]
             print(vm_type.vcpus)
 
         :rtype: :class:`.VMTypeService`
@@ -95,10 +95,10 @@ class ComputeService(CloudService):
         .. code-block:: python
 
             # launch a new instance
-            image = provider.compute.images.find(name='Ubuntu 14.04')[0]
-            size = provider.compute.vm_types.find(name='m1.small')
+            image = provider.compute.images.find(name='Ubuntu 16.04')[0]
+            size = provider.compute.vm_types.find(name='m1.small')[0]
             instance = provider.compute.instances.create('Hello', image, size)
-            print(instance.id, instance.name)
+            print(instance.id, instance.label)
 
         :rtype: :class:`.InstanceService`
         :return: an InstanceService object
@@ -164,11 +164,14 @@ class InstanceService(PageableObjectMixin, CloudService):
         """
         Searches for an instance by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: name, label
 
         :type  name: ``str``
         :param name: The name to search for
 
+        :type  label: ``str``
+        :param label: The label to search for
+
         :rtype: List of ``object`` of :class:`.Instance`
         :return: A list of Instance objects matching the supplied attributes.
         """
@@ -210,15 +213,16 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, image, vm_type, subnet, zone=None,
+    def create(self, label, image, vm_type, subnet, zone=None,
                key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None,
                **kwargs):
         """
         Creates a new virtual machine instance.
 
-        :type  name: ``str``
-        :param name: The name of the virtual machine instance
+        :type  label: ``str``
+        :param label: The label of the virtual machine instance. The instance
+                      name will be derived from this label.
 
         :type  image: ``MachineImage`` or ``str``
         :param image: The MachineImage object or id to boot the virtual machine
@@ -240,7 +244,7 @@ class InstanceService(PageableObjectMixin, CloudService):
                        value but the behaviour is implementation specific.
 
         :type  zone: ``Zone`` or ``str``
-        :param zone: The Zone or its name, where the instance should be placed.
+        :param zone: The Zone or its id, where the instance should be placed.
                      This parameter is provided for legacy compatibility (with
                      classic networks).
 
@@ -248,7 +252,7 @@ class InstanceService(PageableObjectMixin, CloudService):
                      parameter, but in its absence, this value will be used.
 
         :type  key_pair: ``KeyPair`` or ``str``
-        :param key_pair: The KeyPair object or its name, to set for the
+        :param key_pair: The KeyPair object or its id, to set for the
                          instance.
 
         :type  vm_firewalls: A ``list`` of ``VMFirewall`` objects or a
@@ -311,7 +315,7 @@ class VolumeService(PageableObjectMixin, CloudService):
         """
         Searches for a volume by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: label
 
         :rtype: ``object`` of :class:`.Volume`
         :return: a Volume object or ``None`` if not found.
@@ -329,12 +333,12 @@ class VolumeService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, size, zone, snapshot=None, description=None):
+    def create(self, label, size, zone, snapshot=None, description=None):
         """
         Creates a new volume.
 
-        :type  name: ``str``
-        :param name: The name of the volume.
+        :type  label: ``str``
+        :param label: The label for the volume.
 
         :type  size: ``int``
         :param size: The size of the volume (in GB).
@@ -378,7 +382,7 @@ class SnapshotService(PageableObjectMixin, CloudService):
         """
         Searches for a snapshot by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: label
 
         :rtype: list of :class:`.Snapshot`
         :return: a Snapshot object or an empty list if none found.
@@ -396,12 +400,12 @@ class SnapshotService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, volume, description=None):
+    def create(self, label, volume, description=None):
         """
         Creates a new snapshot off a volume.
 
-        :type  name: ``str``
-        :param name: The name of the snapshot
+        :type  label: ``str``
+        :param label: The label for the snapshot.
 
         :type  volume: ``str`` or ``Volume``
         :param volume: The volume to create a snapshot of.
@@ -437,11 +441,11 @@ class StorageService(CloudService):
 
             # print all volumes
             for vol in provider.storage.volumes:
-                print(vol.id, vol.name)
+                print(vol.id, vol.name, vol.label)
 
-            # find volume by name
-            vol = provider.storage.volumes.find(name='my_vol')[0]
-            print(vol.id, vol.name)
+            # find volume by label
+            vol = provider.storage.volumes.find(label='my_vol')[0]
+            print(vol.id, vol.name, vol.label)
 
         :rtype: :class:`.VolumeService`
         :return: a VolumeService object
@@ -459,11 +463,11 @@ class StorageService(CloudService):
 
             # print all snapshots
             for snap in provider.storage.snapshots:
-                print(snap.id, snap.name)
+                print(snap.id, snap.name, snap.label)
 
-            # find snapshot by name
-            snap = provider.storage.snapshots.find(name='my_snap')[0]
-            print(snap.id, snap.name)
+            # find snapshot by label
+            snap = provider.storage.snapshots.find(label='my_snap')[0]
+            print(snap.id, snap.name, snap.label)
 
         :rtype: :class:`.SnapshotService`
         :return: a SnapshotService object
@@ -516,7 +520,7 @@ class ImageService(PageableObjectMixin, CloudService):
         """
         Searches for an image by a given list of attributes
 
-        Supported attributes: name
+        Supported attributes: name, label
 
         :rtype: ``object`` of :class:`.Image`
         :return:  an Image instance
@@ -616,7 +620,7 @@ class NetworkService(PageableObjectMixin, CloudService):
         """
         Searches for a network by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: name, label
 
         :rtype: List of ``object`` of :class:`.Network`
         :return: A list of Network objects matching the supplied attributes.
@@ -624,13 +628,12 @@ class NetworkService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, cidr_block):
+    def create(self, label, cidr_block):
         """
         Create a new network.
 
-        :type name: ``str``
-        :param name: A network name. The name will be set if the
-                     provider supports it.
+        :type label: ``str``
+        :param label: A label for the network.
 
         :type cidr_block: ``str``
         :param cidr_block: The cidr block for this network. Some providers
@@ -668,11 +671,11 @@ class NetworkService(PageableObjectMixin, CloudService):
 
             # Print all subnets
             for s in provider.networking.subnets:
-                print(s.id, s.name)
+                print(s.id, s.name, s.label)
 
             # Get subnet by ID
             s = provider.networking.subnets.get('subnet-id')
-            print(s.id, s.name)
+            print(s.id, s.name, s.label)
 
         :rtype: :class:`.SubnetService`
         :return: a SubnetService object
@@ -719,7 +722,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         """
         Searches for a subnet by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: name, label
 
         :rtype: List of ``object`` of :class:`.Subnet`
         :return: A list of Subnet objects matching the supplied attributes.
@@ -727,13 +730,12 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, network_id, cidr_block, zone=None):
+    def create(self, label, network, cidr_block, zone):
         """
         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 label: ``str``
+        :param label: The subnet label.
 
         :type network: :class:`.Network` object or ``str``
         :param network: Network object or ID under which to create the subnet.
@@ -743,7 +745,7 @@ class SubnetService(PageableObjectMixin, CloudService):
                            subnet.
 
         :type zone: ``str``
-        :param zone: An optional placement zone for the subnet. Some providers
+        :param zone: A placement zone for the subnet. Some providers
                      may not support this, in which case the value is ignored.
 
         :rtype: ``object`` of :class:`.Subnet`
@@ -752,21 +754,17 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def get_or_create_default(self, zone=None):
+    def get_or_create_default(self, zone):
         """
         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).
+        default label used by this library (e.g., cloudbridge-net).
 
-        If this method creates a new subnet, it will create one in each zone
-        available from the provider.
-
-        :type zone: ``str``
-        :param zone: Placement zone where to look for the subnet. If not
-                     supplied, a subnet from random zone will be selected.
+        :type zone: :class:`.PlacementZone` object ``str``
+        :param zone: Placement zone where to look for the subnet.
 
         :rtype: ``object`` of :class:`.Subnet`
         :return: A Subnet object
@@ -818,7 +816,7 @@ class RouterService(PageableObjectMixin, CloudService):
         """
         Searches for a router by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: label
 
         :rtype: List of ``object`` of :class:`.Router`
         :return: A list of Router objects matching the supplied attributes.
@@ -826,13 +824,12 @@ class RouterService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, network):
+    def create(self, label, network):
         """
         Create a new router.
 
-        :type name: ``str``
-        :param name: A router name. The name will be set if the provider
-                     supports it.
+        :type label: ``str``
+        :param label: A router label.
 
         :type network: :class:`.Network` object or ``str``
         :param network: Network object or ID under which to create the router.
@@ -1122,19 +1119,19 @@ class VMFirewallService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, description, network_id):
+    def create(self, label, network, description=None):
         """
         Create a new VMFirewall.
 
-        :type name: str
-        :param name: The name of the new VM firewall.
+        :type label: str
+        :param label: The label for the new VM firewall.
+
+        :type  network: ``str``
+        :param network: Network ID under which to create the VM firewall.
 
         :type description: str
         :param description: The description of the new VM firewall.
 
-        :type  network_id: ``str``
-        :param network_id: Network ID under which to create the VM firewall.
-
         :rtype: ``object`` of :class:`.VMFirewall`
         :return:  A VMFirewall instance or ``None`` if one was not created.
         """

+ 20 - 26
cloudbridge/cloud/providers/aws/helpers.py

@@ -138,21 +138,6 @@ class BotoGenericService(object):
         # 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
@@ -175,15 +160,19 @@ class BotoGenericService(object):
         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))
+        items = pages.build_full_result()
+
+        boto_objs = self._to_boto_resource(collection, args, items)
         resume_token = pages.resume_token
-        return (resume_token, items)
+        return (resume_token, boto_objs)
 
     def _make_query(self, collection, limit, marker):
         """
@@ -196,12 +185,13 @@ class BotoGenericService(object):
         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)
+            res_token, items = self._get_paginated_results(limit, marker,
+                                                           collection)
+            return 'server', res_token, items
         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)
+            return 'client', None, collection
 
     def list(self, limit=None, marker=None, collection=None, **kwargs):
         """
@@ -212,16 +202,20 @@ class BotoGenericService(object):
                            current resource. See http://boto3.readthedocs.io/
                            en/latest/guide/collections.html
         """
+        limit = limit or self.provider.config.default_result_limit
         collection = collection or self.boto_collection.filter(**kwargs)
-        resume_token, boto_objs = self._make_query(collection, limit, marker)
-
+        pag_type, 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,
+        if pag_type == 'server':
+            log.debug("Using server pagination.")
+            return ServerPagedResultList(is_truncated=True if resume_token
+                                         else False,
+                                         marker=resume_token if resume_token
+                                         else None,
                                          supports_total=False,
                                          data=results)
         else:

+ 7 - 6
cloudbridge/cloud/providers/aws/provider.py

@@ -1,17 +1,18 @@
 """Provider implementation based on boto library for AWS-compatible clouds."""
 import logging as log
-import os
 
 import boto3
+
 try:
     # These are installed only for the case of a dev instance
-    from moto.packages.responses import responses
+    import responses
     from moto import mock_ec2
     from moto import mock_s3
 except ImportError:
     log.debug('[aws provider] moto library not available!')
 
 from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.base.helpers import get_env
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 from .services import AWSComputeService
@@ -34,9 +35,9 @@ class AWSCloudProvider(BaseCloudProvider):
                                                   '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_access_key', get_env('AWS_ACCESS_KEY', None)),
             'aws_secret_access_key': self._get_config_value(
-                'aws_secret_key', os.environ.get('AWS_SECRET_KEY', None)),
+                'aws_secret_key', get_env('AWS_SECRET_KEY', None)),
             'aws_session_token': self._get_config_value(
                 'aws_session_token', None)
         }
@@ -105,9 +106,9 @@ class AWSCloudProvider(BaseCloudProvider):
         """
         Get a boto ec2 connection object.
         """
-        return self._conect_ec2_region(region_name=self.region_name)
+        return self._connect_ec2_region(region_name=self.region_name)
 
-    def _conect_ec2_region(self, region_name=None):
+    def _connect_ec2_region(self, region_name=None):
         '''Get an EC2 resource object'''
         return self.session.resource(
             'ec2', region_name=region_name, **self.ec2_cfg)

+ 183 - 80
cloudbridge/cloud/providers/aws/resources.py

@@ -56,7 +56,7 @@ class AWSMachineImage(BaseMachineImage):
         'pending': MachineImageState.PENDING,
         'transient': MachineImageState.PENDING,
         'available': MachineImageState.AVAILABLE,
-        'deregistered': MachineImageState.ERROR,
+        'deregistered': MachineImageState.PENDING,
         'failed': MachineImageState.ERROR,
         'error': MachineImageState.ERROR,
         'invalid': MachineImageState.ERROR
@@ -78,8 +78,23 @@ class AWSMachineImage(BaseMachineImage):
     def name(self):
         try:
             return self._ec2_image.name
-        except AttributeError:
-            return None
+        except (AttributeError, ClientError) as e:
+            log.warn("Cannot get name for image {0}: {1}".format(self.id, e))
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
+        """
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return find_tag_value(self._ec2_image.tags, 'Name')
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._ec2_image.create_tags(Tags=[{'Key': 'Name',
+                                           'Value': value or ""}])
 
     @property
     def description(self):
@@ -141,7 +156,7 @@ class AWSPlacementZone(BasePlacementZone):
 
     @property
     def name(self):
-        return self._aws_zone
+        return self.id
 
     @property
     def region_name(self):
@@ -160,7 +175,7 @@ class AWSVMType(BaseVMType):
 
     @property
     def name(self):
-        return self._inst_dict['instance_type']
+        return self.id
 
     @property
     def family(self):
@@ -168,7 +183,10 @@ class AWSVMType(BaseVMType):
 
     @property
     def vcpus(self):
-        return self._inst_dict.get('vCPU')
+        vcpus = self._inst_dict.get('vCPU')
+        if vcpus == 'N/A':
+            return None
+        return vcpus
 
     @property
     def ram(self):
@@ -216,24 +234,30 @@ class AWSInstance(BaseInstance):
     def __init__(self, provider, ec2_instance):
         super(AWSInstance, self).__init__(provider)
         self._ec2_instance = ec2_instance
+        self._unknown_state = False
 
     @property
     def id(self):
         return self._ec2_instance.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
+        return self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
         """
         .. note:: an instance must have a (case sensitive) tag ``Name``
         """
         return find_tag_value(self._ec2_instance.tags, 'Name')
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._ec2_instance.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._ec2_instance.create_tags(Tags=[{'Key': 'Name',
+                                              'Value': value or ""}])
 
     @property
     def public_ips(self):
@@ -268,6 +292,10 @@ class AWSInstance(BaseInstance):
     def zone_id(self):
         return self._ec2_instance.placement.get('AvailabilityZone')
 
+    @property
+    def subnet_id(self):
+        return self._ec2_instance.subnet_id
+
     @property
     def vm_firewalls(self):
         return [
@@ -283,17 +311,20 @@ class AWSInstance(BaseInstance):
         ]))
 
     @property
-    def key_pair_name(self):
+    def key_pair_id(self):
         return self._ec2_instance.key_name
 
-    def create_image(self, name):
-        self.assert_valid_resource_name(name)
+    def create_image(self, label):
+        self.assert_valid_resource_label(label)
+        name = self._generate_name_from_label(label, 'cb-img')
 
         image = AWSMachineImage(self._provider,
                                 self._ec2_instance.create_image(Name=name))
         # Wait for the image to exist
         self._provider.ec2_conn.meta.client.get_waiter('image_exists').wait(
             ImageIds=[image.id])
+        # Add image label
+        image.label = label
         # Return the image
         image.refresh()
         return image
@@ -338,6 +369,8 @@ class AWSInstance(BaseInstance):
 
     @property
     def state(self):
+        if self._unknown_state:
+            return InstanceState.UNKNOWN
         try:
             return AWSInstance.INSTANCE_STATE_MAP.get(
                 self._ec2_instance.state['Name'], InstanceState.UNKNOWN)
@@ -348,10 +381,11 @@ class AWSInstance(BaseInstance):
     def refresh(self):
         try:
             self._ec2_instance.reload()
+            self._unknown_state = False
         except ClientError:
             # The instance no longer exists and cannot be refreshed.
             # set the state to unknown
-            self._ec2_instance.state = {'Name': InstanceState.UNKNOWN}
+            self._unknown_state = True
 
     # pylint:disable=unused-argument
     def _wait_till_exists(self, timeout=None, interval=None):
@@ -375,21 +409,29 @@ class AWSVolume(BaseVolume):
     def __init__(self, provider, volume):
         super(AWSVolume, self).__init__(provider)
         self._volume = volume
+        self._unknown_state = False
 
     @property
     def id(self):
         return self._volume.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
-        return find_tag_value(self._volume.tags, 'Name')
+        return self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
+        try:
+            return find_tag_value(self._volume.tags, 'Name')
+        except ClientError as e:
+            log.warn("Cannot get label for volume {0}: {1}".format(self.id, e))
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._volume.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._volume.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
 
     @property
     def description(self):
@@ -397,7 +439,8 @@ class AWSVolume(BaseVolume):
 
     @description.setter
     def description(self, value):
-        self._volume.create_tags(Tags=[{'Key': 'Description', 'Value': value}])
+        self._volume.create_tags(Tags=[{'Key': 'Description',
+                                        'Value': value or ""}])
 
     @property
     def size(self):
@@ -442,12 +485,16 @@ class AWSVolume(BaseVolume):
                 Device=a.device,
                 Force=force)
 
-    def create_snapshot(self, name, description=None):
+    def create_snapshot(self, label, description=None):
+        self.assert_valid_resource_label(label)
         snap = AWSSnapshot(
             self._provider,
             self._volume.create_snapshot(
-                Description=description))
-        snap.name = name
+                TagSpecifications=[{'ResourceType': 'snapshot',
+                                    'Tags': [{'Key': 'Name',
+                                              'Value': label}]}],
+                Description=description or ""))
+        snap.wait_till_ready()
         return snap
 
     def delete(self):
@@ -455,6 +502,8 @@ class AWSVolume(BaseVolume):
 
     @property
     def state(self):
+        if self._unknown_state:
+            return VolumeState.UNKNOWN
         try:
             return AWSVolume.VOLUME_STATE_MAP.get(
                 self._volume.state, VolumeState.UNKNOWN)
@@ -465,10 +514,11 @@ class AWSVolume(BaseVolume):
     def refresh(self):
         try:
             self._volume.reload()
+            self._unknown_state = False
         except ClientError:
             # The volume no longer exists and cannot be refreshed.
             # set the status to unknown
-            self._volume.state = VolumeState.UNKNOWN
+            self._unknown_state = True
 
 
 class AWSSnapshot(BaseSnapshot):
@@ -485,21 +535,30 @@ class AWSSnapshot(BaseSnapshot):
     def __init__(self, provider, snapshot):
         super(AWSSnapshot, self).__init__(provider)
         self._snapshot = snapshot
+        self._unknown_state = False
 
     @property
     def id(self):
         return self._snapshot.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
-        return find_tag_value(self._snapshot.tags, 'Name')
+        return self.id
 
-    @name.setter
+    @property
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._snapshot.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self):
+        try:
+            return find_tag_value(self._snapshot.tags, 'Name')
+        except ClientError as e:
+            log.warn("Cannot get label for snap {0}: {1}".format(self.id, e))
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._snapshot.create_tags(Tags=[{'Key': 'Name',
+                                          'Value': value or ""}])
 
     @property
     def description(self):
@@ -508,7 +567,7 @@ class AWSSnapshot(BaseSnapshot):
     @description.setter
     def description(self, value):
         self._snapshot.create_tags(Tags=[{
-            'Key': 'Description', 'Value': value}])
+            'Key': 'Description', 'Value': value or ""}])
 
     @property
     def size(self):
@@ -524,6 +583,8 @@ class AWSSnapshot(BaseSnapshot):
 
     @property
     def state(self):
+        if self._unknown_state:
+            return SnapshotState.UNKNOWN
         try:
             return AWSSnapshot.SNAPSHOT_STATE_MAP.get(
                 self._snapshot.state, SnapshotState.UNKNOWN)
@@ -534,22 +595,23 @@ class AWSSnapshot(BaseSnapshot):
     def refresh(self):
         try:
             self._snapshot.reload()
+            self._unknown_state = False
         except ClientError:
             # The snapshot no longer exists and cannot be refreshed.
             # set the status to unknown
-            self._snapshot.state = SnapshotState.UNKNOWN
+            self._unknown_state = True
 
     def delete(self):
         self._snapshot.delete()
 
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
+        label = "from-snap-{0}".format(self.label or self.id)
         cb_vol = self._provider.storage.volumes.create(
-            name=self.name,
+            label=label,
             size=size,
             zone=placement,
             snapshot=self.id)
         cb_vol.wait_till_ready()
-        cb_vol.name = "from_snap_{0}".format(self.name or self.id)
         return cb_vol
 
 
@@ -567,8 +629,25 @@ class AWSVMFirewall(BaseVMFirewall):
 
     @property
     def name(self):
+        """
+        Return the name of this VM firewall.
+        """
         return self._vm_firewall.group_name
 
+    @property
+    def label(self):
+        try:
+            return find_tag_value(self._vm_firewall.tags, 'Name')
+        except ClientError:
+            return None
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._vm_firewall.create_tags(Tags=[{'Key': 'Name',
+                                             'Value': value or ""}])
+
     @property
     def network_id(self):
         return self._vm_firewall.vpc_id
@@ -760,7 +839,10 @@ class AWSBucketObject(BaseBucketObject):
 
     @property
     def size(self):
-        return self._obj.size
+        try:
+            return self._obj.content_length
+        except AttributeError:  # we're dealing with s3.ObjectSummary
+            return self._obj.size
 
     @property
     def last_modified(self):
@@ -778,12 +860,15 @@ class AWSBucketObject(BaseBucketObject):
     def delete(self):
         self._obj.delete()
 
-    def generate_url(self, expires_in=0):
+    def generate_url(self, expires_in):
         return self._provider.s3_conn.meta.client.generate_presigned_url(
             'get_object',
             Params={'Bucket': self._obj.bucket_name, 'Key': self.id},
             ExpiresIn=expires_in)
 
+    def refresh(self):
+        self._obj.load()
+
 
 class AWSBucket(BaseBucket):
 
@@ -798,7 +883,7 @@ class AWSBucket(BaseBucket):
 
     @property
     def name(self):
-        return self._bucket.name
+        return self.id
 
     @property
     def objects(self):
@@ -830,9 +915,7 @@ class AWSBucketContainer(BaseBucketContainer):
         else:
             # pylint:disable=protected-access
             boto_objs = self.bucket._bucket.objects.all()
-        objects = [AWSBucketObject(self._provider, obj)
-                   for obj in boto_objs]
-
+        objects = [AWSBucketObject(self._provider, obj) for obj in boto_objs]
         return ClientPagedResultList(self._provider, objects,
                                      limit=limit, marker=marker)
 
@@ -869,7 +952,7 @@ class AWSRegion(BaseRegion):
             conn = self._provider.ec2_conn
         else:
             # pylint:disable=protected-access
-            conn = self._provider._conect_ec2_region(region_name=self.id)
+            conn = self._provider._connect_ec2_region(region_name=self.id)
 
         zones = (conn.meta.client.describe_availability_zones()
                  .get('AvailabilityZones', []))
@@ -891,6 +974,7 @@ class AWSNetwork(BaseNetwork):
         super(AWSNetwork, self).__init__(provider)
         self._vpc = network
         self._gtw_container = AWSGatewayContainer(provider, self)
+        self._unknown_state = False
 
     @property
     def id(self):
@@ -898,13 +982,17 @@ class AWSNetwork(BaseNetwork):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return find_tag_value(self._vpc.tags, 'Name')
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._vpc.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._vpc.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
 
     @property
     def external(self):
@@ -916,6 +1004,8 @@ class AWSNetwork(BaseNetwork):
 
     @property
     def state(self):
+        if self._unknown_state:
+            return NetworkState.UNKNOWN
         try:
             return AWSNetwork._NETWORK_STATE_MAP.get(
                 self._vpc.state, NetworkState.UNKNOWN)
@@ -937,10 +1027,11 @@ class AWSNetwork(BaseNetwork):
     def refresh(self):
         try:
             self._vpc.reload()
+            self._unknown_state = False
         except ClientError:
             # The network no longer exists and cannot be refreshed.
             # set the status to unknown
-            self._vpc.state = NetworkState.UNKNOWN
+            self._unknown_state = True
 
     def wait_till_ready(self, timeout=None, interval=None):
         self._provider.ec2_conn.meta.client.get_waiter('vpc_available').wait(
@@ -963,6 +1054,7 @@ class AWSSubnet(BaseSubnet):
     def __init__(self, provider, subnet):
         super(AWSSubnet, self).__init__(provider)
         self._subnet = subnet
+        self._unknown_state = False
 
     @property
     def id(self):
@@ -970,13 +1062,17 @@ class AWSSubnet(BaseSubnet):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return find_tag_value(self._subnet.tags, 'Name')
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._subnet.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._subnet.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
 
     @property
     def cidr_block(self):
@@ -996,6 +1092,8 @@ class AWSSubnet(BaseSubnet):
 
     @property
     def state(self):
+        if self._unknown_state:
+            return SubnetState.UNKNOWN
         try:
             return self._SUBNET_STATE_MAP.get(
                 self._subnet.state, SubnetState.UNKNOWN)
@@ -1004,13 +1102,12 @@ class AWSSubnet(BaseSubnet):
             return SubnetState.UNKNOWN
 
     def refresh(self):
-        subnet = self._provider.networking.subnets.get(self.id)
-        if subnet:
-            # pylint:disable=protected-access
-            self._subnet = subnet._subnet
-        else:
+        try:
+            self._subnet.reload()
+            self._unknown_state = False
+        except ClientError:
             # subnet no longer exists
-            self._subnet.state = SubnetState.UNKNOWN
+            self._unknown_state = True
 
 
 class AWSFloatingIPContainer(BaseFloatingIPContainer):
@@ -1058,7 +1155,7 @@ class AWSFloatingIP(BaseFloatingIP):
 
     @property
     def in_use(self):
-        return True if self._ip.instance_id else False
+        return True if self._ip.association_id else False
 
     def delete(self):
         self._ip.release()
@@ -1079,13 +1176,18 @@ class AWSRouter(BaseRouter):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return find_tag_value(self._route_table.tags, 'Name')
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._route_table.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._route_table.create_tags(Tags=[{'Key': 'Name',
+                                             'Value': value or ""}])
 
     def refresh(self):
         try:
@@ -1119,6 +1221,11 @@ class AWSRouter(BaseRouter):
             a.delete()
         self.refresh()
 
+    @property
+    def subnets(self):
+        return [AWSSubnet(self._provider, rta.subnet)
+                for rta in self._route_table.associations if rta.subnet]
+
     def attach_gateway(self, gateway):
         gw_id = (gateway.id if isinstance(gateway, AWSInternetGateway)
                  else gateway)
@@ -1142,15 +1249,12 @@ class AWSGatewayContainer(BaseGatewayContainer):
                                   cb_resource=AWSInternetGateway,
                                   boto_collection_name='internet_gateways')
 
-    def get_or_create_inet_gateway(self, name=None):
-        log.debug("Get or create inet gateway %s on net %s", name,
+    def get_or_create_inet_gateway(self):
+        log.debug("Get or create inet gateway on net %s",
                   self._network)
-        if name:
-            AWSInternetGateway.assert_valid_resource_name(name)
-
         network_id = self._network.id if isinstance(
             self._network, AWSNetwork) else self._network
-        # Don't filter by name because it may conflict with at least the
+        # Don't filter by label because it may conflict with at least the
         # default VPC that most accounts have but that network is typically
         # without a name.
         gtw = self.svc.find(filter_name='attachment.vpc-id',
@@ -1159,8 +1263,10 @@ class AWSGatewayContainer(BaseGatewayContainer):
             return gtw[0]  # There can be only one gtw attached to a VPC
         # Gateway does not exist so create one and attach to the supplied net
         cb_gateway = self.svc.create('create_internet_gateway')
-        if name:
-            cb_gateway.name = name
+        cb_gateway._gateway.create_tags(
+            Tags=[{'Key': 'Name',
+                   'Value': AWSInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME
+                   }])
         cb_gateway._gateway.attach_to_vpc(VpcId=network_id)
         return cb_gateway
 
@@ -1195,12 +1301,6 @@ class AWSInternetGateway(BaseInternetGateway):
     def name(self):
         return find_tag_value(self._gateway.tags, 'Name')
 
-    @name.setter
-    # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._gateway.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
-
     def refresh(self):
         try:
             self._gateway.reload()
@@ -1221,9 +1321,12 @@ class AWSInternetGateway(BaseInternetGateway):
         return None
 
     def delete(self):
-        if self.network_id:
-            self._gateway.detach_from_vpc(VpcId=self.network_id)
-        self._gateway.delete()
+        try:
+            if self.network_id:
+                self._gateway.detach_from_vpc(VpcId=self.network_id)
+            self._gateway.delete()
+        except ClientError as e:
+            log.warn("Error deleting gateway {0}: {1}".format(self.id, e))
 
     @property
     def floating_ips(self):

+ 268 - 146
cloudbridge/cloud/providers/aws/services.py

@@ -4,6 +4,10 @@ import string
 
 from botocore.exceptions import ClientError
 
+import cachetools
+
+import requests
+
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.services import BaseBucketService
@@ -26,14 +30,13 @@ from cloudbridge.cloud.interfaces.exceptions \
     import DuplicateResourceException, InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
+from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 
-import requests
-
 from .helpers import BotoEC2Service
 from .helpers import BotoS3Service
 from .resources import AWSBucket
@@ -42,6 +45,7 @@ from .resources import AWSKeyPair
 from .resources import AWSLaunchConfig
 from .resources import AWSMachineImage
 from .resources import AWSNetwork
+from .resources import AWSPlacementZone
 from .resources import AWSRegion
 from .resources import AWSRouter
 from .resources import AWSSnapshot
@@ -131,24 +135,30 @@ class AWSVMFirewallService(BaseVMFirewallService):
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
-    def create(self, name, description, network_id):
+    @cb_helpers.deprecated_alias(network_id='network')
+    def create(self, label, network, description=None):
         log.debug("Creating Firewall Service with the parameters "
-                  "[name: %s id: %s description: %s]", name, network_id,
+                  "[label: %s id: %s description: %s]", label, network,
                   description)
-        AWSVMFirewall.assert_valid_resource_name(name)
-        return self.svc.create('create_security_group', GroupName=name,
-                               Description=description, VpcId=network_id)
+        AWSVMFirewall.assert_valid_resource_label(label)
+        name = AWSVMFirewall._generate_name_from_label(label, 'cb-fw')
+        network_id = network.id if isinstance(network, Network) else network
+        obj = self.svc.create('create_security_group', GroupName=name,
+                              Description=description or name,
+                              VpcId=network_id)
+        obj.label = label
+        return obj
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
-
+        # Filter by name or label
+        label = kwargs.pop('label', None)
+        log.debug("Searching for Firewall Service %s", label)
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        log.debug("Searching for Firewall Service %s", name)
-        return self.svc.find(filter_name='group-name', filter_value=name)
+                            " Supported attributes: %s" % (kwargs, 'label'))
+        return self.svc.find(filter_name='tag:Name',
+                             filter_value=label)
 
     def delete(self, firewall_id):
         log.info("Deleting Firewall Service with the id %s", firewall_id)
@@ -194,25 +204,25 @@ class AWSVolumeService(BaseVolumeService):
         return self.svc.get(volume_id)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for AWS Volume Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        log.debug("Searching for AWS Volume Service %s", label)
+        return self.svc.find(filter_name='tag:Name', filter_value=label)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
-    def create(self, name, size, zone, snapshot=None, description=None):
+    def create(self, label, size, zone, snapshot=None, description=None):
         log.debug("Creating AWS Volume Service with the parameters "
-                  "[name: %s size: %s zone: %s snapshot: %s "
-                  "description: %s]", name, size, zone, snapshot,
+                  "[label: %s size: %s zone: %s snapshot: %s "
+                  "description: %s]", label, size, zone, snapshot,
                   description)
-        AWSVolume.assert_valid_resource_name(name)
+        AWSVolume.assert_valid_resource_label(label)
 
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         snapshot_id = snapshot.id if isinstance(
@@ -223,7 +233,7 @@ class AWSVolumeService(BaseVolumeService):
                                  SnapshotId=snapshot_id)
         # Wait until ready to tag instance
         cb_vol.wait_till_ready()
-        cb_vol.name = name
+        cb_vol.label = label
         if description:
             cb_vol.description = description
         return cb_vol
@@ -243,34 +253,39 @@ class AWSSnapshotService(BaseSnapshotService):
         return self.svc.get(snapshot_id)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
-
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        log.debug("Searching for AWS Snapshot Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        # Filter by description or label
+        label = kwargs.get('label', None)
+
+        obj_list = []
+        if label:
+            log.debug("Searching for AWS Snapshot with label %s", label)
+            obj_list.extend(self.svc.find(filter_name='tag:Name',
+                                          filter_value=label,
+                                          OwnerIds=['self']))
+        else:
+            obj_list = list(self)
+        filters = ['label']
+        return cb_helpers.generic_find(filters, kwargs, obj_list)
 
     def list(self, limit=None, marker=None):
-        return self.svc.list(limit=limit, marker=marker)
+        return self.svc.list(limit=limit, marker=marker,
+                             OwnerIds=['self'])
 
-    def create(self, name, volume, description=None):
+    def create(self, label, volume, description=None):
         """
         Creates a new snapshot of a given volume.
         """
         log.debug("Creating a new AWS snapshot Service with the "
-                  "parameters [name: %s volume: %s description: %s]",
-                  name, volume, description)
-        AWSSnapshot.assert_valid_resource_name(name)
+                  "parameters [label: %s volume: %s description: %s]",
+                  label, volume, description)
+        AWSSnapshot.assert_valid_resource_label(label)
 
         volume_id = volume.id if isinstance(volume, AWSVolume) else volume
 
         cb_snap = self.svc.create('create_snapshot', VolumeId=volume_id)
         # Wait until ready to tag instance
         cb_snap.wait_till_ready()
-        cb_snap.name = name
+        cb_snap.label = label
         if cb_snap.description:
             cb_snap.description = description
         return cb_snap
@@ -304,13 +319,12 @@ class AWSBucketService(BaseBucketService):
             # Bucket instance to allow further operations.
             # http://stackoverflow.com/questions/32331456/using-boto-upload-file-to-s3-
             # sub-folder-when-i-have-no-permissions-on-listing-fo
-            if e.response['Error']['Code'] == 403:
+            if e.response['Error']['Code'] == "403":
                 log.warning("AWS Bucket %s already exists but user doesn't "
                             "have enough permissions to access the bucket",
                             bucket_id)
-                bucket = self.provider.s3_conn.get_bucket(bucket_id,
-                                                          validate=False)
-                return AWSBucket(self.provider, bucket)
+                return AWSBucket(self.provider,
+                                 self.provider.s3_conn.Bucket(bucket_id))
         # For all other responses, it's assumed that the bucket does not exist.
         return None
 
@@ -333,41 +347,31 @@ class AWSBucketService(BaseBucketService):
         # LocationConstraint results in an InvalidLocationConstraint.
         # Therefore, it must be special-cased and omitted altogether.
         # See: https://github.com/boto/boto3/issues/125
+        # In addition, us-east-1 also behaves differently when it comes
+        # to raising duplicate resource exceptions, so perform a manual
+        # check
         if loc_constraint == 'us-east-1':
-            return self.svc.create('create_bucket', Bucket=name)
+            try:
+                # check whether bucket already exists
+                self.provider.s3_conn.meta.client.head_bucket(Bucket=name)
+            except ClientError as e:
+                if e.response['Error']['Code'] == "404":
+                    # bucket doesn't exist, go ahead and create it
+                    return self.svc.create('create_bucket', Bucket=name)
+            raise DuplicateResourceException(
+                    'Bucket already exists with name {0}'.format(name))
         else:
-            return self.svc.create('create_bucket', Bucket=name,
-                                   CreateBucketConfiguration={
-                                       'LocationConstraint': loc_constraint
-                                   })
-
-
-class AWSImageService(BaseImageService):
-
-    def __init__(self, provider):
-        super(AWSImageService, self).__init__(provider)
-        self.svc = BotoEC2Service(provider=self.provider,
-                                  cb_resource=AWSMachineImage,
-                                  boto_collection_name='images')
-
-    def get(self, image_id):
-        log.debug("Getting AWS Image Service with the id: %s", image_id)
-        return self.svc.get(image_id)
-
-    def find(self, **kwargs):
-        name = kwargs.pop('name', None)
-
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        log.debug("Searching for AWS Image Service %s", name)
-        return self.svc.find(filter_name='name', filter_value=name)
-
-    def list(self, filter_by_owner=True, limit=None, marker=None):
-        return self.svc.list(Owners=['self'] if filter_by_owner else [],
-                             limit=limit, marker=marker)
+            try:
+                return self.svc.create('create_bucket', Bucket=name,
+                                       CreateBucketConfiguration={
+                                           'LocationConstraint': loc_constraint
+                                        })
+            except ClientError as e:
+                if e.response['Error']['Code'] == "BucketAlreadyOwnedByYou":
+                    raise DuplicateResourceException(
+                        'Bucket already exists with name {0}'.format(name))
+                else:
+                    raise
 
 
 class AWSComputeService(BaseComputeService):
@@ -396,6 +400,54 @@ class AWSComputeService(BaseComputeService):
         return self._region_svc
 
 
+class AWSImageService(BaseImageService):
+
+    def __init__(self, provider):
+        super(AWSImageService, self).__init__(provider)
+        self.svc = BotoEC2Service(provider=self.provider,
+                                  cb_resource=AWSMachineImage,
+                                  boto_collection_name='images')
+
+    def get(self, image_id):
+        log.debug("Getting AWS Image Service with the id: %s", image_id)
+        return self.svc.get(image_id)
+
+    def find(self, **kwargs):
+        # Filter by name or label
+        label = kwargs.get('label', None)
+        # Popped here, not used in the generic find
+        owner = kwargs.pop('owners', None)
+        extra_args = {}
+        if owner:
+            extra_args.update(Owners=owner)
+
+        obj_list = []
+
+        # The original list is made by combining both searches by "tag:Name"
+        # and "AMI name" to allow for searches of public images
+        if label:
+            log.debug("Searching for AWS Image Service %s", label)
+            obj_list.extend(self.svc.find(filter_name='name',
+                                          filter_value=label, **extra_args))
+            obj_list.extend(self.svc.find(filter_name='tag:Name',
+                                          filter_value=label, **extra_args))
+
+        if not label:
+            obj_list = self
+
+        # Add name filter for the generic find method, to allow searching
+        # through AMI names for a match (public images will likely have an
+        # AMI name and no tag:Name)
+        kwargs.update({'name': label})
+        filters = ['label', 'name']
+        return cb_helpers.generic_find(filters, kwargs, obj_list)
+
+    def list(self, filter_by_owner=True, limit=None, marker=None):
+        return self.svc.list(Owners=['self'] if filter_by_owner else
+                             ['amazon', 'self'],
+                             limit=limit, marker=marker)
+
+
 class AWSInstanceService(BaseInstanceService):
 
     def __init__(self, provider):
@@ -404,15 +456,15 @@ class AWSInstanceService(BaseInstanceService):
                                   cb_resource=AWSInstance,
                                   boto_collection_name='instances')
 
-    def create(self, name, image, vm_type, subnet, zone=None,
+    def create(self, label, image, vm_type, subnet, zone,
                key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None, **kwargs):
         log.debug("Creating AWS Instance Service with the params "
-                  "[name: %s image: %s type: %s subnet: %s zone: %s "
+                  "[label: %s image: %s type: %s subnet: %s zone: %s "
                   "key pair: %s firewalls: %s user data: %s config %s "
-                  "others: %s]", name, image, vm_type, subnet, zone,
+                  "others: %s]", label, image, vm_type, subnet, zone,
                   key_pair, vm_firewalls, user_data, launch_config, kwargs)
-        AWSInstance.assert_valid_resource_name(name)
+        AWSInstance.assert_valid_resource_label(label)
 
         image_id = image.id if isinstance(image, MachineImage) else image
         vm_size = vm_type.id if \
@@ -449,7 +501,7 @@ class AWSInstanceService(BaseInstanceService):
             # pylint:disable=protected-access
             inst[0]._wait_till_exists()
             # Tag the instance w/ the name
-            inst[0].name = name
+            inst[0].label = label
             return inst[0]
         raise ValueError(
             'Expected a single object response, got a list: %s' % inst)
@@ -523,7 +575,8 @@ class AWSInstanceService(BaseInstanceService):
                         raise InvalidConfigurationException(
                             "The source is none and the destination is a"
                             " volume. Therefore, you must specify a size.")
-                ebs_def['DeleteOnTermination'] = device.delete_on_terminate
+                ebs_def['DeleteOnTermination'] = device.delete_on_terminate \
+                    or True
                 if device.size:
                     ebs_def['VolumeSize'] = device.size
                 if ebs_def:
@@ -542,14 +595,14 @@ class AWSInstanceService(BaseInstanceService):
         return self.svc.get(instance_id)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        return self.svc.find(filter_name='tag:Name', filter_value=label)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
@@ -561,6 +614,7 @@ class AWSVMTypeService(BaseVMTypeService):
         super(AWSVMTypeService, self).__init__(provider)
 
     @property
+    @cachetools.cached(cachetools.TTLCache(maxsize=1, ttl=24*3600))
     def instance_data(self):
         """
         Fetch info about the available instances.
@@ -571,13 +625,16 @@ class AWSVMTypeService(BaseVMTypeService):
         https://github.com/powdahound/ec2instances.info (in particular, this
         file: https://raw.githubusercontent.com/powdahound/ec2instances.info/
         master/www/instances.json).
-
-        TODO: Needs a caching function with timeout
         """
         r = requests.get(self.provider.config.get(
             "aws_instance_info_url",
             self.provider.AWS_INSTANCE_DATA_DEFAULT_URL))
-        return r.json()
+        # Some instances are only available in certain regions. Use pricing
+        # info to determine and filter out instance types that are not
+        # available in the current region
+        vm_types_list = r.json()
+        return [vm_type for vm_type in vm_types_list
+                if vm_type.get('pricing', {}).get(self.provider.region_name)]
 
     def list(self, limit=None, marker=None):
         vm_types = [AWSVMType(self.provider, vm_type)
@@ -651,28 +708,47 @@ class AWSNetworkService(BaseNetworkService):
         return self.svc.list(limit=limit, marker=marker)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for AWS Network Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        log.debug("Searching for AWS Network Service %s", label)
+        return self.svc.find(filter_name='tag:Name', filter_value=label)
 
-    def create(self, name, cidr_block):
+    def create(self, label, cidr_block):
         log.debug("Creating AWS Network Service with the params "
-                  "[name: %s block: %s]", name, cidr_block)
-        AWSNetwork.assert_valid_resource_name(name)
+                  "[label: %s block: %s]", label, cidr_block)
+        AWSNetwork.assert_valid_resource_label(label)
 
         cb_net = self.svc.create('create_vpc', CidrBlock=cidr_block)
         # Wait until ready to tag instance
         cb_net.wait_till_ready()
-        if name:
-            cb_net.name = name
+        if label:
+            cb_net.label = label
         return cb_net
 
+    def get_or_create_default(self):
+        # # Look for provided default network
+        # for net in self.provider.networking.networks:
+        #     if net._vpc.is_default:
+        #         return net
+
+        # No provider-default, try CB-default instead
+        default_nets = self.provider.networking.networks.find(
+            label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
+        if default_nets:
+            return default_nets[0]
+
+        else:
+            log.info("Creating a CloudBridge-default network labeled %s",
+                     AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
+            return self.provider.networking.networks.create(
+                label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
+                cidr_block='10.0.0.0/16')
+
 
 class AWSSubnetService(BaseSubnetService):
 
@@ -696,68 +772,114 @@ class AWSSubnetService(BaseSubnetService):
             return self.svc.list(limit=limit, marker=marker)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for AWS Subnet Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        log.debug("Searching for AWS Subnet Service %s", label)
+        return self.svc.find(filter_name='tag:Name', filter_value=label)
 
-    def create(self, name, network, cidr_block, zone=None):
+    def create(self, label, network, cidr_block, zone):
         log.debug("Creating AWS Subnet Service with the params "
-                  "[name: %s network: %s block: %s zone: %s]",
-                  name, network, cidr_block, zone)
-        AWSSubnet.assert_valid_resource_name(name)
+                  "[label: %s network: %s block: %s zone: %s]",
+                  label, network, cidr_block, zone)
+        AWSSubnet.assert_valid_resource_label(label)
+
+        zone_name = zone.name if isinstance(
+            zone, AWSPlacementZone) else zone
 
         network_id = network.id if isinstance(network, AWSNetwork) else network
 
         subnet = self.svc.create('create_subnet',
                                  VpcId=network_id,
                                  CidrBlock=cidr_block,
-                                 AvailabilityZone=zone)
-        if name:
-            subnet.name = name
+                                 AvailabilityZone=zone_name)
+        if label:
+            subnet.label = label
         return subnet
 
-    def get_or_create_default(self, zone=None):
-        if zone:
-            snl = self.svc.find('availabilityZone', zone)
-        else:
-            snl = self.svc.list()
-
-        # Find first available default subnet by sorted order
-        # of availability zone. (e.g. prefer us-east-1a over 1e,
-        # This is because newer zones tend to have less compatibility
-        # with different instance types. (e.g. c5.large not available
-        # on us-east-1e as of 14 Dec. 2017
-        # pylint:disable=protected-access
-        snl.sort(key=lambda sn: sn._subnet.availability_zone)
-        for sn in snl:
-            # pylint:disable=protected-access
-            if sn._subnet.default_for_az:
-                return sn
-        # No provider-default Subnet exists, look for a library-default one
-        for sn in snl:
-            # pylint:disable=protected-access
-            for tag in sn._subnet.tags or {}:
-                if (tag.get('Key') == 'Name' and
-                        tag.get('Value') == AWSSubnet.CB_DEFAULT_SUBNET_NAME):
-                    return sn
-        # No provider-default Subnet exists, try to create it (net + subnets)
-        default_net = self.provider.networking.networks.create(
-            name=AWSNetwork.CB_DEFAULT_NETWORK_NAME, cidr_block='10.0.0.0/16')
+    def get_or_create_default(self, zone):
+        zone_name = zone.name if isinstance(zone, AWSPlacementZone) else zone
+
+        # # Look for provider default subnet in current zone
+        # if zone_name:
+        #     snl = self.svc.find('availabilityZone', zone_name)
+        #
+        # else:
+        #     snl = self.svc.list()
+        #     # Find first available default subnet by sorted order
+        #     # of availability zone. Prefer zone us-east-1a over 1e,
+        #     # because newer zones tend to have less compatibility
+        #     # with different instance types (e.g. c5.large not available
+        #     # on us-east-1e as of 14 Dec. 2017).
+        #     # pylint:disable=protected-access
+        #     snl.sort(key=lambda sn: sn._subnet.availability_zone)
+        #
+        # for sn in snl:
+        #     # pylint:disable=protected-access
+        #     if sn._subnet.default_for_az:
+        #         return sn
+
+        # If no provider-default subnet has been found, look for
+        # cloudbridge-default by label. We suffix labels by availability zone,
+        # thus we add the wildcard for the regular expression to find the
+        # subnet
+        snl = self.find(label=AWSSubnet.CB_DEFAULT_SUBNET_LABEL + "*")
+
+        if snl:
+            snl.sort(key=lambda sn: sn._subnet.availability_zone)
+            if not zone_name:
+                return snl[0]
+            for subnet in snl:
+                if subnet.zone.name == zone_name:
+                    return subnet
+
+        # No default Subnet exists, try to create a CloudBridge-specific
+        # subnet. This involves creating the network, subnets, internet
+        # gateway, and connecting it all together so that the network has
+        # Internet connectivity.
+
+        # Check if a default net already exists and get it or create on
+        default_net = self.provider.networking.networks.get_or_create_default()
+
+        # Get/create an internet gateway for the default network and a
+        # corresponding router if it does not already exist.
+        # NOTE: Comment this out because the docs instruct users to setup
+        # network connectivity manually. There's a bit of discrepancy here
+        # though because the provider-default network will have Internet
+        # connectivity (unlike the CloudBridge-default network with this
+        # being commented) and is hence left in the codebase.
+        # default_gtw = default_net.gateways.get_or_create_inet_gateway()
+        # router_label = "{0}-router".format(
+        #   AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
+        # default_routers = self.provider.networking.routers.find(
+        #     label=router_label)
+        # if len(default_routers) == 0:
+        #     default_router = self.provider.networking.routers.create(
+        #         router_label, default_net)
+        #     default_router.attach_gateway(default_gtw)
+        # else:
+        #     default_router = default_routers[0]
+
         # Create a subnet in each of the region's zones
         region = self.provider.compute.regions.get(self.provider.region_name)
         default_sn = None
-        for i, z in enumerate(region.zones):
-            sn = self.create(AWSSubnet.CB_DEFAULT_SUBNET_NAME, default_net,
-                             '10.0.{0}.0/24'.format(i), z.name)
-            if zone and zone == z.name:
+        for i, z in reversed(list(enumerate(region.zones))):
+            sn_label = "{0}-{1}".format(AWSSubnet.CB_DEFAULT_SUBNET_LABEL,
+                                        z.id[-1])
+            log.info("Creating default CloudBridge subnet %s", sn_label)
+            sn = self.create(
+                sn_label, default_net, '10.0.{0}.0/24'.format(i), z)
+            # Create a route table entry between the SN and the inet gateway
+            # See note above about why this is commented
+            # default_router.attach_subnet(sn)
+            if zone and zone_name == z.name:
                 default_sn = sn
         # No specific zone was supplied; return the last created subnet
+        # The list was originally reversed to have the last subnet be in zone a
         if not default_sn:
             default_sn = sn
         return default_sn
@@ -782,27 +904,27 @@ class AWSRouterService(BaseRouterService):
         return self.svc.get(router_id)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for AWS Router Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        log.debug("Searching for AWS Router Service %s", label)
+        return self.svc.find(filter_name='tag:Name', filter_value=label)
 
     def list(self, limit=None, marker=None):
         return self.svc.list(limit=limit, marker=marker)
 
-    def create(self, name, network):
+    def create(self, label, network):
         log.debug("Creating AWS Router Service with the params "
-                  "[name: %s network: %s]", name, network)
-        AWSRouter.assert_valid_resource_name(name)
+                  "[label: %s network: %s]", label, network)
+        AWSRouter.assert_valid_resource_label(label)
 
         network_id = network.id if isinstance(network, AWSNetwork) else network
 
         cb_router = self.svc.create('create_route_table', VpcId=network_id)
-        if name:
-            cb_router.name = name
+        if label:
+            cb_router.label = label
         return cb_router

+ 301 - 69
cloudbridge/cloud/providers/azure/azure_client.py

@@ -2,62 +2,89 @@ import datetime
 import logging
 from io import BytesIO
 
+from azure.common import AzureConflictHttpError
 from azure.common.credentials import ServicePrincipalCredentials
+from azure.cosmosdb.table.tableservice import TableService
 from azure.mgmt.compute import ComputeManagementClient
+from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.network import NetworkManagementClient
 from azure.mgmt.resource import ResourceManagementClient
 from azure.mgmt.resource.subscriptions import SubscriptionClient
 from azure.mgmt.storage import StorageManagementClient
 from azure.storage.blob import BlobPermissions
 from azure.storage.blob import BlockBlobService
-from azure.storage.table import TableService
+from azure.storage.common import TokenCredential
+
+from msrestazure.azure_exceptions import CloudError
+
+import tenacity
+
+from cloudbridge.cloud.interfaces.exceptions import \
+    DuplicateResourceException, InvalidLabelException, \
+    ProviderConnectionException, WaitStateException
 
 from . import helpers as azure_helpers
 
 log = logging.getLogger(__name__)
 
-IMAGE_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                    '{resourceGroupName}/providers/Microsoft.Compute/' \
-                    'images/{imageName}'
-NETWORK_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                     '{resourceGroupName}/providers/Microsoft.Network' \
-                     '/virtualNetworks/{virtualNetworkName}'
-NETWORK_INTERFACE_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
-                                'resourceGroups/{resourceGroupName}' \
-                                '/providers/Microsoft.Network/' \
-                                'networkInterfaces/{networkInterfaceName}'
-PUBLIC_IP_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups' \
-                        '/{resourceGroupName}/providers/Microsoft.Network' \
-                        '/publicIPAddresses/{publicIpAddressName}'
-SNAPSHOT_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                       '{resourceGroupName}/providers/Microsoft.Compute/' \
-                       'snapshots/{snapshotName}'
-SUBNET_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                     '{resourceGroupName}/providers/Microsoft.Network' \
-                     '/virtualNetworks/{virtualNetworkName}/subnets' \
-                     '/{subnetName}'
-VM_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                       '{resourceGroupName}/providers/Microsoft.Compute/' \
-                       'virtualMachines/{vmName}'
-VM_FIREWALL_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
-                             'resourceGroups/{resourceGroupName}/' \
-                             'providers/Microsoft.Network/' \
-                             'networkSecurityGroups/' \
-                             '{networkSecurityGroupName}'
-VM_FIREWALL_RULE_RESOURCE_ID = '/subscriptions/{subscriptionId}/' \
-                             'resourceGroups/{resourceGroupName}/' \
-                             'providers/Microsoft.Network/' \
-                             'networkSecurityGroups/' \
-                             '{networkSecurityGroupName}/' \
-                             'securityRules/{securityRuleName}'
-VOLUME_RESOURCE_ID = '/subscriptions/{subscriptionId}/resourceGroups/' \
-                     '{resourceGroupName}/providers/Microsoft.Compute/' \
-                     'disks/{diskName}'
+IMAGE_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                     '{resourceGroupName}/providers/Microsoft.Compute/'
+                     'images/{imageName}',
+                     '{imageName}',
+                     '{publisher}:{offer}:{sku}:{version}']
+NETWORK_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                       '{resourceGroupName}/providers/Microsoft.Network'
+                       '/virtualNetworks/{virtualNetworkName}',
+                       '{virtualNetworkName}']
+NETWORK_INTERFACE_RESOURCE_ID = ['/subscriptions/{subscriptionId}/'
+                                 'resourceGroups/{resourceGroupName}'
+                                 '/providers/Microsoft.Network/'
+                                 'networkInterfaces/{networkInterfaceName}',
+                                 '{networkInterfaceName}']
+PUBLIC_IP_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups'
+                         '/{resourceGroupName}/providers/Microsoft.Network'
+                         '/publicIPAddresses/{publicIpAddressName}',
+                         '{publicIpAddressName}']
+ROUTER_RESOURCE_ID = ['/subscriptions/{subscriptionId}'
+                      '/resourceGroups/{resourceGroupName}'
+                      '/providers/Microsoft.Network/routeTables/{routerName}',
+                      '{routerName}']
+SNAPSHOT_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                        '{resourceGroupName}/providers/Microsoft.Compute/'
+                        'snapshots/{snapshotName}',
+                        '{snapshotName}']
+SUBNET_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                      '{resourceGroupName}/providers/Microsoft.Network'
+                      '/virtualNetworks/{virtualNetworkName}/subnets'
+                      '/{subnetName}',
+                      '{virtualNetworkName}/{subnetName}']
+VM_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                  '{resourceGroupName}/providers/Microsoft.Compute/'
+                  'virtualMachines/{vmName}',
+                  '{vmName}']
+VM_FIREWALL_RESOURCE_ID = ['/subscriptions/{subscriptionId}/'
+                           'resourceGroups/{resourceGroupName}/'
+                           'providers/Microsoft.Network/'
+                           'networkSecurityGroups/'
+                           '{networkSecurityGroupName}',
+                           '{networkSecurityGroupName}']
+VM_FIREWALL_RULE_RESOURCE_ID = ['/subscriptions/{subscriptionId}/'
+                                'resourceGroups/{resourceGroupName}/'
+                                'providers/Microsoft.Network/'
+                                'networkSecurityGroups/'
+                                '{networkSecurityGroupName}/'
+                                'securityRules/{securityRuleName}',
+                                '{securityRuleName}']
+VOLUME_RESOURCE_ID = ['/subscriptions/{subscriptionId}/resourceGroups/'
+                      '{resourceGroupName}/providers/Microsoft.Compute/'
+                      'disks/{diskName}',
+                      '{diskName}']
 
 IMAGE_NAME = 'imageName'
 NETWORK_NAME = 'virtualNetworkName'
 NETWORK_INTERFACE_NAME = 'networkInterfaceName'
 PUBLIC_IP_NAME = 'publicIpAddressName'
+ROUTER_NAME = 'routerName'
 SNAPSHOT_NAME = 'snapshotName'
 SUBNET_NAME = 'subnetName'
 VM_NAME = 'vmName'
@@ -65,6 +92,66 @@ VM_FIREWALL_NAME = 'networkSecurityGroupName'
 VM_FIREWALL_RULE_NAME = 'securityRuleName'
 VOLUME_NAME = 'diskName'
 
+# Listing possible somewhat through:
+# azure.mgmt.devtestlabs.operations.GalleryImageOperations
+gallery_image_references = \
+    [GalleryImageReference(publisher='Canonical',
+                           offer='UbuntuServer',
+                           sku='16.04.0-LTS',
+                           version='latest'),
+     GalleryImageReference(publisher='Canonical',
+                           offer='UbuntuServer',
+                           sku='14.04.5-LTS',
+                           version='latest'),
+     GalleryImageReference(publisher='OpenLogic',
+                           offer='CentOS',
+                           sku='7.5',
+                           version='latest'),
+     GalleryImageReference(publisher='OpenLogic',
+                           offer='CentOS',
+                           sku='6.9',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftWindowsServer',
+                           offer='WindowsServer',
+                           sku='2016-Nano-Server',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftWindowsServer',
+                           offer='WindowsServer',
+                           sku='2016-Datacenter',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftWindowsDesktop',
+                           offer='Windows-10',
+                           sku='rs4-pron',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftVisualStudio',
+                           offer='Windows',
+                           sku='Windows-10-N-x64',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftVisualStudio',
+                           offer='VisualStudio',
+                           sku='VS-2017-Ent-WS2016',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftSQLServer',
+                           offer='SQL2017-WS2016',
+                           sku='Web',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftSQLServer',
+                           offer='SQL2017-WS2016',
+                           sku='Standard',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftSQLServer',
+                           offer='SQL2017-WS2016',
+                           sku='SQLDEV',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftSQLServer',
+                           offer='SQL2017-WS2016',
+                           sku='Express',
+                           version='latest'),
+     GalleryImageReference(publisher='MicrosoftSQLServer',
+                           offer='SQL2017-WS2016',
+                           sku='Enterprise',
+                           version='latest')]
+
 
 class AzureClient(object):
     """
@@ -72,13 +159,14 @@ class AzureClient(object):
     """
     def __init__(self, config):
         self._config = config
-        self.subscription_id = config.get('azure_subscription_id')
+        self.subscription_id = str(config.get('azure_subscription_id'))
         self._credentials = ServicePrincipalCredentials(
             client_id=config.get('azure_client_id'),
             secret=config.get('azure_secret'),
             tenant=config.get('azure_tenant')
         )
 
+        self._access_token = config.get('azure_access_token')
         self._resource_client = None
         self._storage_client = None
         self._network_management_client = None
@@ -87,14 +175,30 @@ class AzureClient(object):
         self._access_key_result = None
         self._block_blob_service = None
         self._table_service = None
+        self._storage_account = None
 
         log.debug("azure subscription : %s", self.subscription_id)
 
     @property
+    @tenacity.retry(stop=tenacity.stop_after_attempt(5), reraise=True)
     def access_key_result(self):
         if not self._access_key_result:
+            storage_account = self.storage_account
+
+            if self.get_storage_account(storage_account).\
+                    provisioning_state.value != 'Succeeded':
+                log.debug(
+                    "Storage account %s is not in Succeeded state yet. ",
+                    storage_account)
+                raise WaitStateException(
+                    "Waited too long for storage account: {0} to "
+                    "become ready.".format(
+                        storage_account,
+                        self.get_storage_account(storage_account).
+                        provisioning_state))
+
             self._access_key_result = self.storage_client.storage_accounts. \
-                list_keys(self.resource_group, self.storage_account)
+                list_keys(self.resource_group, storage_account)
         return self._access_key_result
 
     @property
@@ -152,14 +256,22 @@ class AzureClient(object):
 
     @property
     def blob_service(self):
+        self._get_or_create_storage_account()
         if not self._block_blob_service:
-            self._block_blob_service = BlockBlobService(
-                self.storage_account,
-                self.access_key_result.keys[0].value)
+            if self._access_token:
+                token_credential = TokenCredential(self._access_token)
+                self._block_blob_service = BlockBlobService(
+                    account_name=self.storage_account,
+                    token_credential=token_credential)
+            else:
+                self._block_blob_service = BlockBlobService(
+                    account_name=self.storage_account,
+                    account_key=self.access_key_result.keys[0].value)
         return self._block_blob_service
 
     @property
     def table_service(self):
+        self._get_or_create_storage_account()
         if not self._table_service:
             self._table_service = TableService(
                 self.storage_account,
@@ -185,6 +297,65 @@ class AzureClient(object):
         return self.storage_client.storage_accounts. \
             create(self.resource_group, name.lower(), params).result()
 
+    # Create a storage account. To prevent a race condition, try
+    # to get or create at least twice
+    @tenacity.retry(stop=tenacity.stop_after_attempt(2),
+                    retry=tenacity.retry_if_exception_type(CloudError),
+                    reraise=True)
+    def _get_or_create_storage_account(self):
+        if self._storage_account:
+            return self._storage_account
+        else:
+            try:
+                self._storage_account = \
+                    self.get_storage_account(self.storage_account)
+            except CloudError as cloud_error:
+                if cloud_error.error.error == "ResourceNotFound":
+                    storage_account_params = {
+                        'sku': {
+                            'name': 'Standard_LRS'
+                        },
+                        'kind': 'storage',
+                        'location': self.region_name,
+                    }
+                    try:
+                        self._storage_account = \
+                            self.create_storage_account(self.storage_account,
+                                                        storage_account_params)
+                    except CloudError as cloud_error2:  # pragma: no cover
+                        if cloud_error2.error.error == "AuthorizationFailed":
+                            mess = 'The following error was returned by ' \
+                                   'Azure:\n%s\n\nThis is likely because the' \
+                                   ' Role associated with the provided ' \
+                                   'credentials does not allow for Storage ' \
+                                   'Account creation.\nA Storage Account is ' \
+                                   'necessary in order to perform the ' \
+                                   'desired operation. You must either ' \
+                                   'provide an existing Storage Account name' \
+                                   ' as part of the configuration, or ' \
+                                   'elevate the associated Role.\nFor more ' \
+                                   'information on roles, see: https://docs.' \
+                                   'microsoft.com/en-us/azure/role-based-' \
+                                   'access-control/overview\n' % cloud_error2
+                            raise ProviderConnectionException(mess)
+
+                        elif cloud_error2.error.error == \
+                                "StorageAccountAlreadyTaken":
+                            mess = 'The following error was ' \
+                                   'returned by Azure:\n%s\n\n' \
+                                   'Note that Storage Account names must be ' \
+                                   'unique across Azure (not just in your ' \
+                                   'subscription).\nFor more information ' \
+                                   'see https://docs.microsoft.com/en-us/' \
+                                   'azure/azure-resource-manager/resource-' \
+                                   'manager-storage-account-name-errors\n' \
+                                   % cloud_error2
+                            raise InvalidLabelException(mess)
+                        else:
+                            raise cloud_error2
+                else:
+                    raise cloud_error
+
     def list_locations(self):
         return self.subscription_client.subscriptions. \
             list_locations(self.subscription_id)
@@ -237,11 +408,25 @@ class AzureClient(object):
         return self.network_management_client.security_rules. \
             delete(self.resource_group, vm_firewall, name).result()
 
-    def list_containers(self, prefix=None):
-        return self.blob_service.list_containers(prefix=prefix)
+    def list_containers(self, prefix=None, limit=None, marker=None):
+        results = self.blob_service.list_containers(prefix=prefix,
+                                                    num_results=limit,
+                                                    marker=marker)
+        return (results.items, results.next_marker)
 
     def create_container(self, container_name):
-        self.blob_service.create_container(container_name)
+        try:
+            self.blob_service.create_container(container_name,
+                                               fail_on_exist=True)
+        except AzureConflictHttpError as cloud_error:
+            if cloud_error.error_code == "ContainerAlreadyExists":
+                msg = "The given Bucket name '%s' already exists. Please " \
+                      "use the `get` or `find` method to get a reference to " \
+                      "an existing Bucket, or specify a new Bucket name to " \
+                      "create.\nNote that in Azure, Buckets are contained " \
+                      "in Storage Accounts." % container_name
+                raise DuplicateResourceException(msg)
+
         return self.blob_service.get_container_properties(container_name)
 
     def get_container(self, container_name):
@@ -268,7 +453,7 @@ class AzureClient(object):
         self.blob_service.delete_blob(container_name, blob_name)
 
     def get_blob_url(self, container_name, blob_name, expiry_time):
-        expiry_date = datetime.datetime.now() + datetime.timedelta(
+        expiry_date = datetime.datetime.utcnow() + datetime.timedelta(
             seconds=expiry_time)
         sas = self.blob_service.generate_blob_shared_access_signature(
             container_name, blob_name, permission=BlobPermissions.READ,
@@ -359,6 +544,12 @@ class AzureClient(object):
             raw=True
         )
 
+    def is_gallery_image(self, image_id):
+        url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
+                                             image_id)
+        # If it is a gallery image, it will always have an offer
+        return 'offer' in url_params
+
     def create_image(self, name, params):
         return self.compute_client.images. \
             create_or_update(self.resource_group, name,
@@ -367,29 +558,43 @@ class AzureClient(object):
     def delete_image(self, image_id):
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
                                              image_id)
-        name = url_params.get(IMAGE_NAME)
-        self.compute_client.images.delete(self.resource_group, name).wait()
+        if not self.is_gallery_image(image_id):
+            name = url_params.get(IMAGE_NAME)
+            self.compute_client.images.delete(self.resource_group, name).wait()
 
     def list_images(self):
-        return self.compute_client.images. \
-            list_by_resource_group(self.resource_group)
+        azure_images = list(self.compute_client.images.
+                            list_by_resource_group(self.resource_group))
+        return azure_images
+
+    def list_gallery_refs(self):
+        return gallery_image_references
 
     def get_image(self, image_id):
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
                                              image_id)
-        name = url_params.get(IMAGE_NAME)
-        return self.compute_client.images.get(self.resource_group, name)
+        if self.is_gallery_image(image_id):
+            return GalleryImageReference(publisher=url_params['publisher'],
+                                         offer=url_params['offer'],
+                                         sku=url_params['sku'],
+                                         version=url_params['version'])
+        else:
+            name = url_params.get(IMAGE_NAME)
+            return self.compute_client.images.get(self.resource_group, name)
 
     def update_image_tags(self, image_id, tags):
         url_params = azure_helpers.parse_url(IMAGE_RESOURCE_ID,
                                              image_id)
-        name = url_params.get(IMAGE_NAME)
-        return self.compute_client.images. \
-            create_or_update(self.resource_group, name,
-                             {
-                                 'tags': tags,
-                                 'location': self.region_name
-                             }).result()
+        if self.is_gallery_image(image_id):
+            return True
+        else:
+            name = url_params.get(IMAGE_NAME)
+            return self.compute_client.images. \
+                create_or_update(self.resource_group, name,
+                                 {
+                                     'tags': tags,
+                                     'location': self.region_name
+                                 }).result()
 
     def list_vm_types(self):
         return self.compute_client.virtual_machine_sizes. \
@@ -427,7 +632,7 @@ class AzureClient(object):
 
     def get_network_id_for_subnet(self, subnet_id):
         url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID, subnet_id)
-        network_id = NETWORK_RESOURCE_ID
+        network_id = NETWORK_RESOURCE_ID[0]
         for key, val in url_params.items():
             network_id = network_id.replace("{" + key + "}", val)
         return network_id
@@ -460,18 +665,34 @@ class AzureClient(object):
 
         return subnet_info
 
+    def __if_subnet_in_use(e):
+        # return True if the CloudError exception is due to subnet being in use
+        if isinstance(e, CloudError):
+            if e.error.error == "InUseSubnetCannotBeDeleted":
+                return True
+        return False
+
+    @tenacity.retry(stop=tenacity.stop_after_attempt(5),
+                    retry=tenacity.retry_if_exception(__if_subnet_in_use),
+                    wait=tenacity.wait_fixed(5),
+                    reraise=True)
     def delete_subnet(self, subnet_id):
         url_params = azure_helpers.parse_url(SUBNET_RESOURCE_ID,
                                              subnet_id)
         network_name = url_params.get(NETWORK_NAME)
         subnet_name = url_params.get(SUBNET_NAME)
-        result_delete = self.network_management_client \
-            .subnets.delete(
-                self.resource_group,
-                network_name,
-                subnet_name
-            )
-        result_delete.wait()
+
+        try:
+            result_delete = self.network_management_client \
+                .subnets.delete(
+                    self.resource_group,
+                    network_name,
+                    subnet_name
+                )
+            result_delete.wait()
+        except CloudError as cloud_error:
+            log.exception(cloud_error.message)
+            raise cloud_error
 
     def create_floating_ip(self, public_ip_name, public_ip_parameters):
         return self.network_management_client.public_ip_addresses. \
@@ -494,6 +715,14 @@ class AzureClient(object):
             public_ip_addresses.delete(self.resource_group,
                                        public_ip_name).wait()
 
+    def update_fip_tags(self, fip_id, tags):
+        url_params = azure_helpers.parse_url(PUBLIC_IP_RESOURCE_ID,
+                                             fip_id)
+        fip_name = url_params.get(PUBLIC_IP_NAME)
+        self.network_management_client.public_ip_addresses. \
+            create_or_update(self.resource_group,
+                             fip_name, tags).result()
+
     def list_floating_ips(self):
         return self.network_management_client.public_ip_addresses.list(
             self.resource_group)
@@ -691,8 +920,11 @@ class AzureClient(object):
             route_tables.list(self.resource_group)
 
     def get_route_table(self, router_id):
+        url_params = azure_helpers.parse_url(ROUTER_RESOURCE_ID,
+                                             router_id)
+        router_name = url_params.get(ROUTER_NAME)
         return self.network_management_client. \
-            route_tables.get(self.resource_group, router_id)
+            route_tables.get(self.resource_group, router_name)
 
     def create_route_table(self, route_table_name, params):
         return self.network_management_client. \

+ 47 - 22
cloudbridge/cloud/providers/azure/helpers.py

@@ -1,41 +1,66 @@
 from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
 
 
-def filter_by_tag(list_items, filters):
-    """
-    This function filter items on the tags
-    :param list_items:
-    :param filters:
-    :return:
-    """
-    filtered_list = []
-    if filters:
-        for obj in list_items:
-            for key in filters:
-                if obj.tags and filters[key] in obj.tags.get(key, ''):
-                    filtered_list.append(obj)
-
-        return filtered_list
-    else:
-        return list_items
+# def filter_by_tag(list_items, filters):
+#     """
+#     This function filter items on the tags
+#     :param list_items:
+#     :param filters:
+#     :return:
+#     """
+#     filtered_list = []
+#     if filters:
+#         for obj in list_items:
+#             for key in filters:
+#                 if obj.tags and filters[key] in obj.tags.get(key, ''):
+#                     filtered_list.append(obj)
+#
+#         return filtered_list
+#     else:
+#         return list_items
 
 
-def parse_url(template_url, original_url):
+def parse_url(template_urls, original_url):
     """
     In Azure all the resource IDs are returned as URIs.
     ex: '/subscriptions/{subscriptionId}/resourceGroups/' \
        '{resourceGroupName}/providers/Microsoft.Compute/' \
        'virtualMachines/{vmName}'
-    This function splits the resource ID based on the template url passed
+    This function splits the resource ID based on the template urls passed
     and returning the dictionary.
+
+    The only exception to that format are image URN's which are used for
+    public gallery references:
+    https://docs.microsoft.com/en-us/azure/virtual-machines/linux/cli-ps-findimage
     """
-    template_url_parts = template_url.split('/')
+    if not original_url:
+        raise InvalidValueException(template_urls, original_url)
     original_url_parts = original_url.split('/')
+    if len(original_url_parts) == 1:
+        original_url_parts = original_url.split(':')
+    for each_template in template_urls:
+        template_url_parts = each_template.split('/')
+        if len(template_url_parts) == 1:
+            template_url_parts = each_template.split(':')
+        if len(template_url_parts) == len(original_url_parts):
+            break
     if len(template_url_parts) != len(original_url_parts):
-        raise InvalidValueException(template_url, original_url)
+        raise InvalidValueException(template_urls, original_url)
     resource_param = {}
     for key, value in zip(template_url_parts, original_url_parts):
         if key.startswith('{') and key.endswith('}'):
             resource_param.update({key[1:-1]: value})
-
     return resource_param
+
+
+def generate_urn(gallery_image):
+    """
+    This function takes an azure gallery image and outputs a corresponding URN
+    :param gallery_image: a GalleryImageReference object
+    :return: URN as string
+    """
+    reference_dict = gallery_image.as_dict()
+    return ':'.join([reference_dict['publisher'],
+                     reference_dict['offer'],
+                     reference_dict['sku'],
+                     reference_dict['version']])

+ 84 - 39
cloudbridge/cloud/providers/azure/provider.py

@@ -1,14 +1,21 @@
 import logging
-import os
+import uuid
 
+from deprecation import deprecated
+
+from msrestazure.azure_exceptions import CloudError
+
+import tenacity
+
+import cloudbridge
 from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.base.helpers import get_env
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 from cloudbridge.cloud.providers.azure.azure_client import AzureClient
 from cloudbridge.cloud.providers.azure.services \
     import AzureComputeService, AzureNetworkingService, \
     AzureSecurityService, AzureStorageService
 
-from msrestazure.azure_exceptions import CloudError
-
 log = logging.getLogger(__name__)
 
 
@@ -19,37 +26,44 @@ class AzureCloudProvider(BaseCloudProvider):
         super(AzureCloudProvider, self).__init__(config)
 
         # mandatory config values
-        self.subscription_id = self. \
-            _get_config_value('azure_subscription_id',
-                              os.environ.get('AZURE_SUBSCRIPTION_ID', None))
+        self.subscription_id = self._get_config_value(
+            'azure_subscription_id', get_env('AZURE_SUBSCRIPTION_ID'))
         self.client_id = self._get_config_value(
-            'azure_client_id', os.environ.get('AZURE_CLIENT_ID', None))
+            'azure_client_id', get_env('AZURE_CLIENT_ID', None))
         self.secret = self._get_config_value(
-            'azure_secret', os.environ.get('AZURE_SECRET', None))
+            'azure_secret', get_env('AZURE_SECRET', None))
         self.tenant = self._get_config_value(
-            'azure_tenant', os.environ.get('AZURE_TENANT', None))
+            'azure_tenant', get_env('AZURE_TENANT', None))
 
         # optional config values
+        self.access_token = self._get_config_value(
+            'azure_access_token', get_env('AZURE_ACCESS_TOKEN', None))
         self.region_name = self._get_config_value(
-            'azure_region_name', os.environ.get('AZURE_REGION_NAME',
-                                                'eastus'))
+            'azure_region_name', get_env('AZURE_REGION_NAME', 'eastus'))
         self.resource_group = self._get_config_value(
-            'azure_resource_group', os.environ.get('AZURE_RESOURCE_GROUP',
-                                                   'cloudbridge'))
-        # Storage account name is limited to a max length of 24 characters
-        # so take part of the client id to keep it unique
+            'azure_resource_group', get_env('AZURE_RESOURCE_GROUP',
+                                            'cloudbridge'))
+        # Storage account name is limited to a max length of 24 alphanum chars
+        # and unique across all of Azure. Thus, a uuid is used to generate a
+        # unique name for the Storage Account based on the resource group,
+        # while also using the subscription ID to ensure that different users
+        # having the same resource group name do not have the same SA name.
         self.storage_account = self._get_config_value(
             'azure_storage_account',
-            os.environ.get('AZURE_STORAGE_ACCOUNT',
-                           'storageacc' + self.client_id[-12:]))
+            get_env(
+                'AZURE_STORAGE_ACCOUNT',
+                'storacc' + self.subscription_id[-6:] +
+                str(uuid.uuid5(uuid.NAMESPACE_OID,
+                               str(self.resource_group)))[-6:]))
 
         self.vm_default_user_name = self._get_config_value(
-            'azure_vm_default_user_name', os.environ.get
-            ('AZURE_VM_DEFAULT_USER_NAME', 'cbuser'))
+                'azure_vm_default_username', get_env(
+                    'AZURE_VM_DEFAULT_USERNAME', None)) \
+            or self.__get_deprecated_username('cbuser')
 
         self.public_key_storage_table_name = self._get_config_value(
-            'azure_public_key_storage_table_name', os.environ.get
-            ('AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME', 'cbcerts'))
+            'azure_public_key_storage_table_name', get_env(
+                'AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME', 'cbcerts'))
 
         self._azure_client = None
 
@@ -58,6 +72,23 @@ class AzureCloudProvider(BaseCloudProvider):
         self._compute = AzureComputeService(self)
         self._networking = AzureNetworkingService(self)
 
+    def __get_deprecated_username(self, default):
+        username = self._get_config_value(
+            'azure_vm_default_user_name', get_env(
+                'AZURE_VM_DEFAULT_USER_NAME', None))
+        if username:
+            return self.__wrap_deprecated_username(username)
+        else:
+            return default
+
+    @deprecated(deprecated_in='1.1',
+                removed_in='2.0',
+                current_version=cloudbridge.__version__,
+                details='AZURE_VM_DEFAULT_USER_NAME was deprecated in favor '
+                        'of AZURE_VM_DEFAULT_USERNAME')
+    def __wrap_deprecated_username(self, username):
+        return username
+
     @property
     def compute(self):
         return self._compute
@@ -91,13 +122,17 @@ class AzureCloudProvider(BaseCloudProvider):
                 'azure_resource_group': self.resource_group,
                 'azure_storage_account': self.storage_account,
                 'azure_public_key_storage_table_name':
-                    self.public_key_storage_table_name
+                    self.public_key_storage_table_name,
+                'azure_access_token': self.access_token
             }
 
             self._azure_client = AzureClient(provider_config)
             self._initialize()
         return self._azure_client
 
+    @tenacity.retry(stop=tenacity.stop_after_attempt(2),
+                    retry=tenacity.retry_if_exception_type(CloudError),
+                    reraise=True)
     def _initialize(self):
         """
         Verifying that resource group and storage account exists
@@ -106,21 +141,31 @@ class AzureCloudProvider(BaseCloudProvider):
         """
         try:
             self._azure_client.get_resource_group(self.resource_group)
-        except CloudError:
-            resource_group_params = {'location': self.region_name}
-            self._azure_client.create_resource_group(self.resource_group,
-                                                     resource_group_params)
 
-        try:
-            self._azure_client.get_storage_account(self.storage_account)
-        except CloudError:
-            storage_account_params = {
-                'sku': {
-                    'name': 'Standard_LRS'
-                },
-                'kind': 'storage',
-                'location': self.region_name,
-            }
-            self._azure_client. \
-                create_storage_account(self.storage_account,
-                                       storage_account_params)
+        except CloudError as cloud_error:
+            if cloud_error.error.error == "ResourceGroupNotFound":
+                resource_group_params = {'location': self.region_name}
+                try:
+                    self._azure_client.\
+                        create_resource_group(self.resource_group,
+                                              resource_group_params)
+                except CloudError as cloud_error2:  # pragma: no cover
+                    if cloud_error2.error.error == "AuthorizationFailed":
+                        mess = 'The following error was returned by Azure:\n' \
+                               '%s\n\nThis is likely because the Role' \
+                               'associated with the given credentials does ' \
+                               'not allow for Resource Group creation.\nA ' \
+                               'Resource Group is necessary to manage ' \
+                               'resources in Azure. You must either ' \
+                               'provide an existing Resource Group as part ' \
+                               'of the configuration, or elevate the ' \
+                               'associated role.\nFor more information on ' \
+                               'roles, see: https://docs.microsoft.com/' \
+                               'en-us/azure/role-based-access-control/' \
+                               'overview\n' % cloud_error2
+                        raise ProviderConnectionException(mess)
+                    else:
+                        raise cloud_error2
+
+            else:
+                raise cloud_error

+ 285 - 185
cloudbridge/cloud/providers/azure/resources.py

@@ -3,11 +3,16 @@ DataTypes used by this provider
 """
 import collections
 import logging
-import uuid
+from uuid import uuid4
 
 from azure.common import AzureException
+from azure.mgmt.devtestlabs.models import GalleryImageReference
 from azure.mgmt.network.models import NetworkSecurityGroup
 
+from msrestazure.azure_exceptions import CloudError
+
+import pysftp
+
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo, \
     BaseBucket, BaseBucketContainer, BaseBucketObject, BaseFloatingIP, \
@@ -21,9 +26,7 @@ from cloudbridge.cloud.interfaces.resources import Instance, \
     MachineImageState, NetworkState, RouterState, \
     SnapshotState, SubnetState, TrafficDirection
 
-from msrestazure.azure_exceptions import CloudError
-
-import pysftp
+from . import helpers as azure_helpers
 
 log = logging.getLogger(__name__)
 
@@ -37,7 +40,7 @@ class AzureVMFirewall(BaseVMFirewall):
 
     @property
     def network_id(self):
-        return None
+        return self._vm_firewall.tags.get('network_id', None)
 
     @property
     def resource_id(self):
@@ -49,12 +52,16 @@ class AzureVMFirewall(BaseVMFirewall):
 
     @property
     def name(self):
-        return self._vm_firewall.tags.get('Name', self._vm_firewall.name)
+        return self._vm_firewall.name
 
-    @name.setter
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._vm_firewall.tags.update(Name=value)
+    @property
+    def label(self):
+        return self._vm_firewall.tags.get('Label', None)
+
+    @label.setter
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._vm_firewall.tags.update(Label=value or "")
         self._provider.azure_client.update_vm_firewall_tags(
             self.id, self._vm_firewall.tags)
 
@@ -64,7 +71,7 @@ class AzureVMFirewall(BaseVMFirewall):
 
     @description.setter
     def description(self, value):
-        self._vm_firewall.tags.update(Description=value)
+        self._vm_firewall.tags.update(Description=value or "")
         self._provider.azure_client.\
             update_vm_firewall_tags(self.id,
                                     self._vm_firewall.tags)
@@ -85,8 +92,8 @@ class AzureVMFirewall(BaseVMFirewall):
                 get_vm_firewall(self.id)
             if not self._vm_firewall.tags:
                 self._vm_firewall.tags = {}
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The security group no longer exists and cannot be refreshed.
 
     def to_json(self):
@@ -138,7 +145,7 @@ class AzureVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
             cidr = '0.0.0.0/0'
 
         count = len(self.firewall._vm_firewall.security_rules) + 1
-        rule_name = "Rule - " + str(count)
+        rule_name = "cb-rule-" + str(count)
         priority = 1000 + count
         destination_port_range = str(from_port) + "-" + str(to_port)
         source_port_range = '*'
@@ -174,15 +181,15 @@ class AzureVMFirewallRule(BaseVMFirewallRule):
     def id(self):
         return self._rule.id
 
+    @property
+    def name(self):
+        return self._rule.name
+
     @property
     def direction(self):
         return (TrafficDirection.INBOUND if self._rule.direction == "Inbound"
                 else TrafficDirection.OUTBOUND)
 
-    @property
-    def name(self):
-        return self._rule.name
-
     @property
     def protocol(self):
         return self._rule.protocol
@@ -237,9 +244,6 @@ class AzureBucketObject(BaseBucketObject):
 
     @property
     def name(self):
-        """
-        Get this object's name.
-        """
         return self._key.name
 
     @property
@@ -264,7 +268,7 @@ class AzureBucketObject(BaseBucketObject):
         iterable.
         """
         content_stream = self._provider.azure_client. \
-            get_blob_content(self._container.name, self._key.name)
+            get_blob_content(self._container.id, self._key.name)
         if content_stream:
             content_stream.seek(0)
         return content_stream
@@ -276,7 +280,7 @@ class AzureBucketObject(BaseBucketObject):
         """
         try:
             self._provider.azure_client.create_blob_from_text(
-                self._container.name, self.name, data)
+                self._container.id, self.id, data)
             return True
         except AzureException as azureEx:
             log.exception(azureEx)
@@ -288,7 +292,7 @@ class AzureBucketObject(BaseBucketObject):
         """
         try:
             self._provider.azure_client.create_blob_from_file(
-                self._container.name, self.name, path)
+                self._container.id, self.id, path)
             return True
         except AzureException as azureEx:
             log.exception(azureEx)
@@ -301,15 +305,19 @@ class AzureBucketObject(BaseBucketObject):
         :rtype: bool
         :return: True if successful
         """
-        self._provider.azure_client.delete_blob(self._container.name,
-                                                self.name)
+        self._provider.azure_client.delete_blob(self._container.id,
+                                                self.id)
 
-    def generate_url(self, expires_in=0):
+    def generate_url(self, expires_in):
         """
         Generate a URL to this object.
         """
         return self._provider.azure_client.get_blob_url(
-            self._container.name, self.name, expires_in)
+            self._container.id, self.id, expires_in)
+
+    def refresh(self):
+        self._key = self._provider.azure_client.get_blob(
+            self._container.id, self._key.id)
 
 
 class AzureBucket(BaseBucket):
@@ -356,7 +364,8 @@ class AzureBucketContainer(BaseBucketContainer):
         Retrieve a given object from this bucket.
         """
         try:
-            obj = self._provider.azure_client.get_blob(self.bucket.name, key)
+            obj = self._provider.azure_client.get_blob(self.bucket.name,
+                                                       key)
             return AzureBucketObject(self._provider, self.bucket, obj)
         except AzureException as azureEx:
             log.exception(azureEx)
@@ -426,28 +435,31 @@ class AzureVolume(BaseVolume):
     def resource_id(self):
         return self._volume.id
 
+    @property
+    def name(self):
+        return self._volume.name
+
     @property
     def tags(self):
         return self._volume.tags
 
     @property
-    def name(self):
+    def label(self):
         """
-        Get the volume name.
+        Get the volume label.
 
-        .. note:: an instance must have a (case sensitive) tag ``Name``
+        .. note:: an instance must have a (case sensitive) tag ``Label``
         """
-        return self._volume.tags.get('Name', self._volume.name)
+        return self._volume.tags.get('Label', None)
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the volume name.
+        Set the volume label.
         """
-        # self._volume.name = value
-        self.assert_valid_resource_name(value)
-        self._volume.tags.update(Name=value)
+        self.assert_valid_resource_label(value)
+        self._volume.tags.update(Label=value or "")
         self._provider.azure_client. \
             update_disk_tags(self.id,
                              self._volume.tags)
@@ -458,7 +470,7 @@ class AzureVolume(BaseVolume):
 
     @description.setter
     def description(self, value):
-        self._volume.tags.update(Description=value)
+        self._volume.tags.update(Description=value or "")
         self._provider.azure_client. \
             update_disk_tags(self.id,
                              self._volume.tags)
@@ -520,15 +532,16 @@ class AzureVolume(BaseVolume):
         for vm in self._provider.azure_client.list_vm():
             for item in vm.storage_profile.data_disks:
                 if item.managed_disk and \
-                                item.managed_disk.id == self.resource_id:
+                        item.managed_disk.id == self.resource_id:
                     vm.storage_profile.data_disks.remove(item)
                     self._provider.azure_client.update_vm(vm.id, vm)
 
-    def create_snapshot(self, name, description=None):
+    def create_snapshot(self, label, description=None):
         """
         Create a snapshot of this Volume.
         """
-        return self._provider.storage.snapshots.create(name, self)
+        return self._provider.storage.snapshots.create(label, self,
+                                                       description)
 
     def delete(self):
         """
@@ -550,8 +563,8 @@ class AzureVolume(BaseVolume):
             self._volume = self._provider.azure_client. \
                 get_disk(self.id)
             self._update_state()
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The volume no longer exists and cannot be refreshed.
             # set the state to unknown
             self._state = 'unknown'
@@ -580,27 +593,31 @@ class AzureSnapshot(BaseSnapshot):
     def id(self):
         return self._snapshot.id
 
+    @property
+    def name(self):
+        return self._snapshot.name
+
     @property
     def resource_id(self):
         return self._snapshot.id
 
     @property
-    def name(self):
+    def label(self):
         """
-        Get the snapshot name.
+        Get the snapshot label.
 
-        .. note:: an instance must have a (case sensitive) tag ``Name``
+        .. note:: an instance must have a (case sensitive) tag ``Label``
         """
-        return self._snapshot.tags.get('Name', self._snapshot.name)
+        return self._snapshot.tags.get('Label', None)
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the snapshot name.
+        Set the snapshot label.
         """
-        self.assert_valid_resource_name(value)
-        self._snapshot.tags.update(Name=value)
+        self.assert_valid_resource_label(value)
+        self._snapshot.tags.update(Label=value or "")
         self._provider.azure_client. \
             update_snapshot_tags(self.id,
                                  self._snapshot.tags)
@@ -611,7 +628,7 @@ class AzureSnapshot(BaseSnapshot):
 
     @description.setter
     def description(self, value):
-        self._snapshot.tags.update(Description=value)
+        self._snapshot.tags.update(Description=value or "")
         self._provider.azure_client. \
             update_snapshot_tags(self.id,
                                  self._snapshot.tags)
@@ -642,8 +659,8 @@ class AzureSnapshot(BaseSnapshot):
             self._snapshot = self._provider.azure_client. \
                 get_snapshot(self.id)
             self._state = self._snapshot.provisioning_state
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The snapshot no longer exists and cannot be refreshed.
             # set the state to unknown
             self._state = 'unknown'
@@ -660,8 +677,7 @@ class AzureSnapshot(BaseSnapshot):
         Create a new Volume from this Snapshot.
         """
         return self._provider.storage.volumes. \
-            create(self.name, self.size,
-                   zone=placement, snapshot=self)
+            create(self.name, self.size, zone=placement, snapshot=self)
 
 
 class AzureMachineImage(BaseMachineImage):
@@ -673,11 +689,15 @@ class AzureMachineImage(BaseMachineImage):
 
     def __init__(self, provider, image):
         super(AzureMachineImage, self).__init__(provider)
+        # Image can be either a dict for public image reference
+        # or the Azure iamge object
         self._image = image
-        self._state = self._image.provisioning_state
-
-        if not self._image.tags:
-            self._image.tags = {}
+        if isinstance(self._image, GalleryImageReference):
+            self._state = 'Succeeded'
+        else:
+            self._state = self._image.provisioning_state
+            if not self._image.tags:
+                self._image.tags = {}
 
     @property
     def id(self):
@@ -687,31 +707,42 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``str``
         :return: ID for this instance as returned by the cloud middleware.
         """
-        return self._image.id
+        if self.is_gallery_image:
+            return azure_helpers.generate_urn(self._image)
+        else:
+            return self._image.id
 
     @property
-    def resource_id(self):
-        return self._image.id
+    def name(self):
+        if self.is_gallery_image:
+            return azure_helpers.generate_urn(self._image)
+        else:
+            return self._image.name
 
     @property
-    def name(self):
-        """
-        Get the image name.
+    def resource_id(self):
+        if self.is_gallery_image:
+            return azure_helpers.generate_urn(self._image)
+        else:
+            return self._image.id
 
-        :rtype: ``str``
-        :return: Name for this image as returned by the cloud middleware.
-        """
-        return self._image.tags.get('Name', self._image.name)
+    @property
+    def label(self):
+        if self.is_gallery_image:
+            return azure_helpers.generate_urn(self._image)
+        else:
+            return self._image.tags.get('Label', None)
 
-    @name.setter
-    def name(self, value):
+    @label.setter
+    def label(self, value):
         """
-        Set the image name.
+        Set the image label when it is a private image.
         """
-        self.assert_valid_resource_name(value)
-        self._image.tags.update(Name=value)
-        self._provider.azure_client. \
-            update_image_tags(self.id, self._image.tags)
+        if not self.is_gallery_image:
+            self.assert_valid_resource_label(value)
+            self._image.tags.update(Label=value or "")
+            self._provider.azure_client. \
+                update_image_tags(self.id, self._image.tags)
 
     @property
     def description(self):
@@ -721,16 +752,21 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``str``
         :return: Description for this image as returned by the cloud middleware
         """
-        return self._image.tags.get('Description', None)
+        if self.is_gallery_image:
+            return 'Public gallery image from the Azure Marketplace: '\
+                    + self.name
+        else:
+            return self._image.tags.get('Description', None)
 
     @description.setter
     def description(self, value):
         """
-        Set the image name.
+        Set the image description.
         """
-        self._image.tags.update(Description=value)
-        self._provider.azure_client. \
-            update_image_tags(self.id, self._image.tags)
+        if not self.is_gallery_image:
+            self._image.tags.update(Description=value or "")
+            self._provider.azure_client. \
+                update_image_tags(self.id, self._image.tags)
 
     @property
     def min_disk(self):
@@ -743,31 +779,47 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``int``
         :return: The minimum disk size needed by this image
         """
-        return self._image.storage_profile.os_disk.disk_size_gb or 0
+        if self.is_gallery_image:
+            return 0
+        else:
+            return self._image.storage_profile.os_disk.disk_size_gb or 0
 
     def delete(self):
         """
         Delete this image
         """
-        self._provider.azure_client.delete_image(self.id)
+        if not self.is_gallery_image:
+            self._provider.azure_client.delete_image(self.id)
 
     @property
     def state(self):
-        return AzureMachineImage.IMAGE_STATE_MAP.get(
-            self._state, MachineImageState.UNKNOWN)
+        if self.is_gallery_image:
+            return MachineImageState.AVAILABLE
+        else:
+            return AzureMachineImage.IMAGE_STATE_MAP.get(
+                self._state, MachineImageState.UNKNOWN)
+
+    @property
+    def is_gallery_image(self):
+        """
+        Returns true if the image is a public reference and false if it
+        is a private image in the resource group.
+        """
+        return isinstance(self._image, GalleryImageReference)
 
     def refresh(self):
         """
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        try:
-            self._image = self._provider.azure_client.get_image(self.id)
-            self._state = self._image.provisioning_state
-        except CloudError as cloudError:
-            log.exception(cloudError.message)
-            # image no longer exists
-            self._state = "unknown"
+        if not self.is_gallery_image:
+            try:
+                self._image = self._provider.azure_client.get_image(self.id)
+                self._state = self._image.provisioning_state
+            except CloudError as cloud_error:
+                log.exception(cloud_error.message)
+                # image no longer exists
+                self._state = "unknown"
 
 
 class AzureGatewayContainer(BaseGatewayContainer):
@@ -781,12 +833,8 @@ class AzureGatewayContainer(BaseGatewayContainer):
         self.gateway_singleton = AzureInternetGateway(self._provider, None,
                                                       network)
 
-    def get_or_create_inet_gateway(self, name=None):
-        if name:
-            AzureInternetGateway.assert_valid_resource_name(name)
+    def get_or_create_inet_gateway(self):
         gateway = AzureInternetGateway(self._provider, None, self._network)
-        if name:
-            gateway.name = name
         return gateway
 
     def list(self, limit=None, marker=None):
@@ -814,27 +862,31 @@ class AzureNetwork(BaseNetwork):
     def id(self):
         return self._network.id
 
+    @property
+    def name(self):
+        return self._network.name
+
     @property
     def resource_id(self):
         return self._network.id
 
     @property
-    def name(self):
+    def label(self):
         """
-        Get the network name.
+        Get the network label.
 
-        .. note:: the network must have a (case sensitive) tag ``Name``
+        .. note:: the network must have a (case sensitive) tag ``Label``
         """
-        return self._network.tags.get('Name', self._network.name)
+        return self._network.tags.get('Label', None)
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the network name.
+        Set the network label.
         """
-        self.assert_valid_resource_name(value)
-        self._network.tags.update(Name=value)
+        self.assert_valid_resource_label(value)
+        self._network.tags.update(Label=value or "")
         self._provider.azure_client. \
             update_network_tags(self.id, self._network)
 
@@ -860,8 +912,8 @@ class AzureNetwork(BaseNetwork):
             self._network = self._provider.azure_client.\
                 get_network(self.id)
             self._state = self._network.provisioning_state
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The network no longer exists and cannot be refreshed.
             # set the state to unknown
             self._state = 'unknown'
@@ -888,16 +940,16 @@ class AzureNetwork(BaseNetwork):
         """
         return self._provider.networking.subnets.list(network=self.id)
 
-    def create_subnet(self, cidr_block, name=None, zone=None):
+    def create_subnet(self, label, cidr_block, zone=None):
         """
         Create the subnet with cidr_block
         :param cidr_block:
-        :param name:
+        :param label:
         :param zone:
         :return:
         """
         return self._provider.networking.subnets. \
-            create(network=self.id, cidr_block=cidr_block, name=name)
+            create(label=label, network=self.id, cidr_block=cidr_block)
 
     @property
     def gateways(self):
@@ -925,14 +977,15 @@ class AzureFloatingIPContainer(BaseFloatingIPContainer):
                                      limit=limit, marker=marker)
 
     def create(self):
-        public_ip_address_name = "{0}-{1}".format(
-            'public_ip', uuid.uuid4().hex[:6])
         public_ip_parameters = {
             'location': self._provider.azure_client.region_name,
             'public_ip_allocation_method': 'Static'
         }
+
+        public_ip_name = 'cb-fip-' + uuid4().hex[:6]
+
         floating_ip = self._provider.azure_client.\
-            create_floating_ip(public_ip_address_name, public_ip_parameters)
+            create_floating_ip(public_ip_name, public_ip_parameters)
         return AzureFloatingIP(self._provider, floating_ip, self._network_id)
 
 
@@ -947,6 +1000,10 @@ class AzureFloatingIP(BaseFloatingIP):
     def id(self):
         return self._ip.id
 
+    @property
+    def name(self):
+        return self._ip.ip_address
+
     @property
     def resource_id(self):
         return self._ip.id
@@ -1051,29 +1108,52 @@ class AzureSubnet(BaseSubnet):
         super(AzureSubnet, self).__init__(provider)
         self._subnet = subnet
         self._state = self._subnet.provisioning_state
+        self._tag_name = None
 
     @property
     def id(self):
         return self._subnet.id
 
     @property
-    def resource_id(self):
-        return self._subnet.id
+    def name(self):
+        net_name = self.network_id.split('/')[-1]
+        sn_name = self._subnet.name
+        return '{0}/{1}'.format(net_name, sn_name)
 
     @property
-    def name(self):
-        """
-        Get the subnet name.
+    def label(self):
+        # Although Subnet doesn't support labels, we use the parent Network's
+        # tags to track the subnet's labels
+        network = self.network
+        az_network = network._network
+        return az_network.tags.get(self.tag_name, None)
 
-        .. note:: the subnet must have a (case sensitive) tag ``Name``
-        """
-        return self._subnet.name
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        network = self.network
+        az_network = network._network
+        kwargs = {self.tag_name: value or ""}
+        az_network.tags.update(**kwargs)
+        self._provider.azure_client.update_network_tags(
+            az_network.id, az_network)
+
+    @property
+    def tag_name(self):
+        if not self._tag_name:
+            self._tag_name = 'SubnetLabel_{0}'.format(self._subnet.name)
+        return self._tag_name
+
+    @property
+    def resource_id(self):
+        return self._subnet.id
 
     @property
     def zone(self):
         # pylint:disable=protected-access
         region = self._provider.compute.regions.get(
-            self._network._network.location)
+            self.network._network.location)
         return region.zones[0]
 
     @property
@@ -1084,10 +1164,6 @@ class AzureSubnet(BaseSubnet):
     def network_id(self):
         return self._provider.azure_client.get_network_id_for_subnet(self.id)
 
-    @property
-    def _network(self):
-        return self._provider.networking.networks.get(self.network_id)
-
     def delete(self):
         self._provider.azure_client.delete_subnet(self.id)
 
@@ -1104,8 +1180,8 @@ class AzureSubnet(BaseSubnet):
             self._subnet = self._provider.azure_client. \
                 get_subnet(self.id)
             self._state = self._subnet.provisioning_state
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The subnet no longer exists and cannot be refreshed.
             # set the state to unknown
             self._state = 'unknown'
@@ -1161,27 +1237,34 @@ class AzureInstance(BaseInstance):
         """
         return self._vm.id
 
+    @property
+    def name(self):
+        """
+        Get the instance name.
+        """
+        return self._vm.name
+
     @property
     def resource_id(self):
         return self._vm.id
 
     @property
-    def name(self):
+    def label(self):
         """
-        Get the instance name.
+        Get the instance label.
 
-        .. note:: an instance must have a (case sensitive) tag ``Name``
+        .. note:: an instance must have a (case sensitive) tag ``Label``
         """
-        return self._vm.tags.get('Name', self._vm.name)
+        return self._vm.tags.get('Label', None)
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the instance name.
+        Set the instance label.
         """
-        self.assert_valid_resource_name(value)
-        self._vm.tags.update(Name=value)
+        self.assert_valid_resource_label(value)
+        self._vm.tags.update(Label=value or "")
         self._provider.azure_client. \
             update_vm_tags(self.id, self._vm)
 
@@ -1234,17 +1317,14 @@ class AzureInstance(BaseInstance):
         """
         self._provider.azure_client.deallocate_vm(self.id)
         self._provider.azure_client.delete_vm(self.id)
-        for public_ip_id in self._public_ip_ids:
-            self._provider.azure_client.delete_floating_ip(public_ip_id)
         for nic_id in self._nic_ids:
             self._provider.azure_client.delete_nic(nic_id)
+        for public_ip_id in self._public_ip_ids:
+            self._provider.azure_client.delete_floating_ip(public_ip_id)
         for data_disk in self._vm.storage_profile.data_disks:
             if data_disk.managed_disk:
-                disk = self._provider.azure_client.\
-                    get_disk(data_disk.managed_disk.id)
-                if disk and disk.tags \
-                        and disk.tags.get('delete_on_terminate',
-                                          'False') == 'True':
+                if self._vm.tags.get('delete_on_terminate',
+                                     'False') == 'True':
                     self._provider.azure_client.\
                         delete_disk(data_disk.managed_disk.id)
         if self._vm.storage_profile.os_disk.managed_disk:
@@ -1254,9 +1334,17 @@ class AzureInstance(BaseInstance):
     @property
     def image_id(self):
         """
-        Get the image ID for this insance.
+        Get the image ID for this instance.
         """
-        return self._vm.storage_profile.image_reference.id
+        # Not tested for resource group images
+        reference_dict = self._vm.storage_profile.image_reference.as_dict()
+        if reference_dict.get('publisher'):
+            return ':'.join([reference_dict['publisher'],
+                             reference_dict['offer'],
+                             reference_dict['sku'],
+                             reference_dict['version']])
+        else:
+            return reference_dict['id']
 
     @property
     def zone_id(self):
@@ -1265,6 +1353,20 @@ class AzureInstance(BaseInstance):
         """
         return self._vm.location
 
+    @property
+    def subnet_id(self):
+        """
+        Return the first subnet id associated with the first network iface.
+
+        An Azure instance can have multiple network interfaces attached with
+        each interface having at most one subnet. This method will return only
+        the subnet of the first attached network interface.
+        """
+        for nic_id in self._nic_ids:
+            nic = self._provider.azure_client.get_nic(nic_id)
+            for ipc in nic.ip_configurations:
+                return ipc.subnet.id
+
     @property
     def vm_firewalls(self):
         return [self._provider.security.vm_firewalls.get(group_id)
@@ -1277,13 +1379,13 @@ class AzureInstance(BaseInstance):
                 if nic.network_security_group]
 
     @property
-    def key_pair_name(self):
+    def key_pair_id(self):
         """
         Get the name of the key pair associated with this instance.
         """
         return self._vm.tags.get('Key_Pair')
 
-    def create_image(self, name, private_key_path=None):
+    def create_image(self, label, private_key_path=None):
         """
         Create a new image based on this instance. Documentation for create
         image available at https://docs.microsoft.com/en-us/azure/virtual-ma
@@ -1296,7 +1398,8 @@ class AzureInstance(BaseInstance):
         CloudBridge interface to pass the private key file path
         """
 
-        self.assert_valid_resource_name(name)
+        self.assert_valid_resource_label(label)
+        name = self._generate_name_from_label(label, 'cb-img')
 
         if not self._state == 'VM generalized':
             if not self._state == 'VM running':
@@ -1312,10 +1415,11 @@ class AzureInstance(BaseInstance):
             'source_virtual_machine': {
                 'id': self.resource_id
             },
-            'tags': {'Name': name}
+            'tags': {'Label': label}
         }
 
-        image = self._provider.azure_client.create_image(name, create_params)
+        image = self._provider.azure_client.create_image(name,
+                                                         create_params)
         return AzureMachineImage(self._provider, image)
 
     def _deprovision(self, private_key_path):
@@ -1440,8 +1544,8 @@ class AzureInstance(BaseInstance):
             if not self._vm.tags:
                 self._vm.tags = {}
             self._update_state()
-        except (CloudError, ValueError) as cloudError:
-            log.exception(cloudError.message)
+        except (CloudError, ValueError) as cloud_error:
+            log.exception(cloud_error.message)
             # The volume no longer exists and cannot be refreshed.
             # set the state to unknown
             self._state = 'unknown'
@@ -1481,7 +1585,7 @@ class AzureVMType(BaseVMType):
 
     @property
     def ram(self):
-        return self._vm_type.memory_in_mb
+        return int(self._vm_type.memory_in_mb) / 1024
 
     @property
     def size_root_disk(self):
@@ -1534,6 +1638,10 @@ class AzureRouter(BaseRouter):
 
     @property
     def id(self):
+        return self._route_table.id
+
+    @property
+    def name(self):
         return self._route_table.name
 
     @property
@@ -1541,22 +1649,22 @@ class AzureRouter(BaseRouter):
         return self._route_table.id
 
     @property
-    def name(self):
+    def label(self):
         """
-        Get the router name.
+        Get the router label.
 
-        .. note:: the router must have a (case sensitive) tag ``Name``
+        .. note:: the router must have a (case sensitive) tag ``Label``
         """
-        return self._route_table.tags.get('Name', self._route_table.name)
+        return self._route_table.tags.get('Label', None)
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the router name.
+        Set the router label.
         """
-        self.assert_valid_resource_name(value)
-        self._route_table.tags.update(Name=value)
+        self.assert_valid_resource_label(value)
+        self._route_table.tags.update(Label=value or "")
         self._provider.azure_client. \
             update_route_table_tags(self._route_table.name,
                                     self._route_table)
@@ -1585,6 +1693,13 @@ class AzureRouter(BaseRouter):
                                          self.resource_id)
         self.refresh()
 
+    @property
+    def subnets(self):
+        if self._route_table.subnets:
+            return [AzureSubnet(self._provider, sn)
+                    for sn in self._route_table.subnets]
+        return []
+
     def detach_subnet(self, subnet):
         self._provider.azure_client. \
             detach_subnet_to_route_table(subnet.id,
@@ -1602,7 +1717,6 @@ class AzureInternetGateway(BaseInternetGateway):
     def __init__(self, provider, gateway, gateway_net):
         super(AzureInternetGateway, self).__init__(provider)
         self._gateway = gateway
-        self._name = None
         self._network_id = gateway_net.id if isinstance(
             gateway_net, AzureNetwork) else gateway_net
         self._state = ''
@@ -1611,25 +1725,11 @@ class AzureInternetGateway(BaseInternetGateway):
 
     @property
     def id(self):
-        return self._name
+        return "cb-gateway-wrapper"
 
     @property
     def name(self):
-        """
-        Get the gateway name.
-
-        .. note:: the gateway must have a (case sensitive) tag ``Name``
-        """
-        return self._name
-
-    @name.setter
-    # pylint:disable=arguments-differ
-    def name(self, value):
-        """
-        Set the router name.
-        """
-        self.assert_valid_resource_name(value)
-        self._name = value
+        return "cb-gateway-wrapper"
 
     def refresh(self):
         pass

Datei-Diff unterdrückt, da er zu groß ist
+ 334 - 307
cloudbridge/cloud/providers/azure/services.py


+ 18 - 21
cloudbridge/cloud/providers/openstack/provider.py

@@ -1,12 +1,9 @@
 """Provider implementation based on OpenStack Python clients for OpenStack."""
 
 import inspect
-import os
 
 from cinderclient import client as cinder_client
 
-from cloudbridge.cloud.base import BaseCloudProvider
-
 from keystoneauth1 import session
 
 from keystoneclient import client as keystone_client
@@ -17,10 +14,12 @@ 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 cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.base.helpers import get_env
+
 from .services import OpenStackComputeService
 from .services import OpenStackNetworkingService
 from .services import OpenStackSecurityService
@@ -37,21 +36,22 @@ class OpenStackCloudProvider(BaseCloudProvider):
 
         # Initialize cloud connection fields
         self.username = self._get_config_value(
-            'os_username', os.environ.get('OS_USERNAME', None))
+            'os_username', get_env('OS_USERNAME', None))
         self.password = self._get_config_value(
-            'os_password', os.environ.get('OS_PASSWORD', None))
+            'os_password', get_env('OS_PASSWORD', None))
         self.project_name = self._get_config_value(
-            'os_project_name', os.environ.get('OS_PROJECT_NAME', None) or
-            os.environ.get('OS_TENANT_NAME', None))
+            'os_project_name', get_env('OS_PROJECT_NAME', None)
+            or get_env('OS_TENANT_NAME', None))
         self.auth_url = self._get_config_value(
-            'os_auth_url', os.environ.get('OS_AUTH_URL', None))
+            'os_auth_url', get_env('OS_AUTH_URL', None))
         self.region_name = self._get_config_value(
-            'os_region_name', os.environ.get('OS_REGION_NAME', None))
+            'os_region_name', get_env('OS_REGION_NAME', None))
         self.project_domain_name = self._get_config_value(
             'os_project_domain_name',
-            os.environ.get('OS_PROJECT_DOMAIN_NAME', None))
+            get_env('OS_PROJECT_DOMAIN_NAME', None))
         self.user_domain_name = self._get_config_value(
-            'os_user_domain_name', os.environ.get('OS_USER_DOMAIN_NAME', None))
+            'os_user_domain_name',
+            get_env('OS_USER_DOMAIN_NAME', None))
 
         # Service connections, lazily initialized
         self._nova = None
@@ -125,11 +125,8 @@ class OpenStackCloudProvider(BaseCloudProvider):
         return self._cached_keystone_session
 
     def _connect_openstack(self):
-        prof = profile.Profile()
-        prof.set_region(profile.Profile.ALL, self.region_name)
-
         return connection.Connection(
-            profile=prof,
+            region_name=self.region_name,
             user_agent='cloudbridge',
             auth_url=self.auth_url,
             project_name=self.project_name,
@@ -195,10 +192,10 @@ class OpenStackCloudProvider(BaseCloudProvider):
 
         api_version = self._get_config_value(
             'os_compute_api_version',
-            os.environ.get('OS_COMPUTE_API_VERSION', 2))
+            get_env('OS_COMPUTE_API_VERSION', 2))
         service_name = self._get_config_value(
             'nova_service_name',
-            os.environ.get('NOVA_SERVICE_NAME', None))
+            get_env('NOVA_SERVICE_NAME', None))
 
         if self.config.debug_mode:
             nova_shell.OpenStackComputeShell().setup_debugging(True)
@@ -236,7 +233,7 @@ class OpenStackCloudProvider(BaseCloudProvider):
         """Get an OpenStack Cinder (block storage) client object."""
         api_version = self._get_config_value(
             'os_volume_api_version',
-            os.environ.get('OS_VOLUME_API_VERSION', 2))
+            get_env('OS_VOLUME_API_VERSION', 2))
 
         return cinder_client.Client(api_version,
                                     auth_url=self.auth_url,
@@ -305,9 +302,9 @@ class OpenStackCloudProvider(BaseCloudProvider):
         clean_options = self._clean_options(options,
                                             swift_client.Connection.__init__)
         storage_url = self._get_config_value(
-            'os_storage_url', os.environ.get('OS_STORAGE_URL', None))
+            'os_storage_url', get_env('OS_STORAGE_URL', None))
         auth_token = self._get_config_value(
-            'os_auth_token', os.environ.get('OS_AUTH_TOKEN', None))
+            'os_auth_token', get_env('OS_AUTH_TOKEN', None))
         if storage_url and auth_token:
             clean_options['preauthurl'] = storage_url
             clean_options['preauthtoken'] = auth_token

+ 235 - 110
cloudbridge/cloud/providers/openstack/resources.py

@@ -5,6 +5,26 @@ import inspect
 import ipaddress
 import logging
 import os
+try:
+    from urllib.parse import urlparse
+    from urllib.parse import urljoin
+except ImportError:  # python 2
+    from urlparse import urlparse
+    from urlparse import urljoin
+
+from keystoneclient.v3.regions import Region
+
+from neutronclient.common.exceptions import PortNotFoundClient
+
+import novaclient.exceptions as novaex
+
+from openstack.exceptions import HttpException
+from openstack.exceptions import NotFoundException
+from openstack.exceptions import ResourceNotFound
+
+import swiftclient
+from swiftclient.service import SwiftService, SwiftUploadObject
+from swiftclient.utils import generate_temp_url
 
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
@@ -42,19 +62,6 @@ 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
-from openstack.exceptions import ResourceNotFound
-
-import swiftclient
-from swiftclient.service import SwiftService, SwiftUploadObject
-
-
 ONE_GIG = 1048576000  # in bytes
 FIVE_GIG = ONE_GIG * 5  # in bytes
 
@@ -69,8 +76,8 @@ class OpenStackMachineImage(BaseMachineImage):
         'saving': MachineImageState.PENDING,
         'active': MachineImageState.AVAILABLE,
         'killed': MachineImageState.ERROR,
-        'deleted': MachineImageState.ERROR,
-        'pending_delete': MachineImageState.ERROR,
+        'deleted': MachineImageState.UNKNOWN,
+        'pending_delete': MachineImageState.PENDING,
         'deactivated': MachineImageState.ERROR
     }
 
@@ -92,10 +99,27 @@ class OpenStackMachineImage(BaseMachineImage):
     @property
     def name(self):
         """
-        Get the image name.
+        Get the image identifier.
+        """
+        return self._os_image.id
+
+    @property
+    def label(self):
+        """
+        Get the image label.
         """
         return self._os_image.name
 
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        """
+        Set the image label.
+        """
+        self.assert_valid_resource_label(value)
+        self._provider.os_conn.image.update_image(
+            self._os_image, name=value or "")
+
     @property
     def description(self):
         """
@@ -211,7 +235,7 @@ class OpenStackVMType(BaseVMType):
 
     @property
     def ram(self):
-        return self._os_flavor.ram
+        return int(self._os_flavor.ram) / 1024
 
     @property
     def size_root_disk(self):
@@ -273,23 +297,30 @@ class OpenStackInstance(BaseInstance):
         return self._os_instance.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
         """
-        Get the instance name.
+        Get the instance identifier.
+        """
+        return self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
+        """
+        Get the instance label.
         """
         return self._os_instance.name
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the instance name.
+        Set the instance label.
         """
-        self.assert_valid_resource_name(value)
+        self.assert_valid_resource_label(value)
 
         self._os_instance.name = value
-        self._os_instance.update(name=value)
+        self._os_instance.update(name=value or "cb-inst")
 
     @property
     def public_ips(self):
@@ -366,6 +397,35 @@ class OpenStackInstance(BaseInstance):
         """
         return getattr(self._os_instance, 'OS-EXT-AZ:availability_zone', None)
 
+    @property
+    def subnet_id(self):
+        """
+        Extract (one) subnet id associated with this instance.
+
+        In OpenStack, instances are associated with ports instead of
+        subnets so we need to dig through several connections to retrieve
+        the subnet_id. Further, there can potentially be several ports each
+        connected to different subnets. This implementation retrieves one
+        subnet, the one corresponding to port associated with the first
+        private IP associated with the instance.
+        """
+        # MAC address can be used to identify a port so extract the MAC
+        # address corresponding to the (first) private IP associated with the
+        # instance.
+        for net in self._os_instance.to_dict().get('addresses').keys():
+            for iface in self._os_instance.to_dict().get('addresses')[net]:
+                if iface.get('OS-EXT-IPS:type') == 'fixed':
+                    port = iface.get('OS-EXT-IPS-MAC:mac_addr')
+                    addr = iface.get('addr')
+                    break
+        # Now get a handle to a port with the given MAC address and get the
+        # subnet to which the private IP is connected as the desired id.
+        for prt in self._provider.neutron.list_ports().get('ports'):
+            if prt.get('mac_address') == port:
+                for ip in prt.get('fixed_ips'):
+                    if ip.get('ip_address') == addr:
+                        return ip.get('subnet_id')
+
     @property
     def vm_firewalls(self):
         return [
@@ -381,22 +441,23 @@ class OpenStackInstance(BaseInstance):
         return [fw.id for fw in self.vm_firewalls]
 
     @property
-    def key_pair_name(self):
+    def key_pair_id(self):
         """
-        Get the name of the key pair associated with this instance.
+        Get the id of the key pair associated with this instance.
         """
         return self._os_instance.key_name
 
-    def create_image(self, name):
+    def create_image(self, label):
         """
         Create a new image based on this instance.
         """
-        log.debug("Creating OpenStack Image with the name %s", name)
-        self.assert_valid_resource_name(name)
+        log.debug("Creating OpenStack Image with the label %s", label)
+        self.assert_valid_resource_label(label)
 
-        image_id = self._os_instance.create_image(name)
-        return OpenStackMachineImage(
+        image_id = self._os_instance.create_image(label)
+        img = OpenStackMachineImage(
             self._provider, self._provider.compute.images.get(image_id))
+        return img
 
     def _get_fip(self, floating_ip):
         """Get a floating IP object based on the supplied ID."""
@@ -411,7 +472,8 @@ class OpenStackInstance(BaseInstance):
         log.debug("Adding floating IP adress: %s", floating_ip)
         fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
                else self._get_fip(floating_ip))
-        self._os_instance.add_floating_ip(fip.public_ip)
+        self._provider.os_conn.compute.add_floating_ip_to_server(
+            self.id, fip.public_ip)
 
     def remove_floating_ip(self, floating_ip):
         """
@@ -420,7 +482,8 @@ class OpenStackInstance(BaseInstance):
         log.debug("Removing floating IP adress: %s", floating_ip)
         fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
                else self._get_fip(floating_ip))
-        self._os_instance.remove_floating_ip(fip.public_ip)
+        self._provider.os_conn.compute.remove_floating_ip_from_server(
+            self.id, fip.public_ip)
 
     def add_vm_firewall(self, firewall):
         """
@@ -470,8 +533,7 @@ class OpenStackRegion(BaseRegion):
 
     @property
     def name(self):
-        return (self._os_region.id if type(self._os_region) == Region else
-                self._os_region)
+        return self.id
 
     @property
     def zones(self):
@@ -519,22 +581,26 @@ class OpenStackVolume(BaseVolume):
         return self._volume.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
+        return self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
         """
-        Get the volume name.
+        Get the volume label.
         """
         return self._volume.name
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the volume name.
+        Set the volume label.
         """
-        self.assert_valid_resource_name(value)
+        self.assert_valid_resource_label(value)
         self._volume.name = value
-        self._volume.update(name=value)
+        self._volume.update(name=value or "")
 
     @property
     def description(self):
@@ -590,14 +656,14 @@ class OpenStackVolume(BaseVolume):
         """
         self._volume.detach()
 
-    def create_snapshot(self, name, description=None):
+    def create_snapshot(self, label, description=None):
         """
         Create a snapshot of this Volume.
         """
         log.debug("Creating snapchat of volume: %s with the "
-                  "description: %s", name, description)
+                  "description: %s", label, description)
         return self._provider.storage.snapshots.create(
-            name, self, description=description)
+            label, self, description=description)
 
     def delete(self):
         """
@@ -645,22 +711,26 @@ class OpenStackSnapshot(BaseSnapshot):
         return self._snapshot.id
 
     @property
-    # pylint:disable=arguments-differ
     def name(self):
+        return self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
         """
-        Get the snapshot name.
+        Get the snapshot label.
         """
         return self._snapshot.name
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
+    def label(self, value):
         """
-        Set the snapshot name.
+        Set the snapshot label.
         """
-        self.assert_valid_resource_name(value)
+        self.assert_valid_resource_label(value)
         self._snapshot.name = value
-        self._snapshot.update(name=value)
+        self._snapshot.update(name=value or "")
 
     @property
     def description(self):
@@ -712,13 +782,13 @@ class OpenStackSnapshot(BaseSnapshot):
         """
         Create a new Volume from this Snapshot.
         """
-        vol_name = "from_snap_{0}".format(self.id or self.name)
+        vol_label = "from-snap-{0}".format(self.id or self.label)
+        self.assert_valid_resource_label(vol_label)
         size = size if size else self._snapshot.size
         os_vol = self._provider.cinder.volumes.create(
-            size, name=vol_name, availability_zone=placement,
+            size, name=vol_label, availability_zone=placement,
             snapshot_id=self._snapshot.id)
         cb_vol = OpenStackVolume(self._provider, os_vol)
-        cb_vol.name = vol_name
         return cb_vol
 
 
@@ -736,7 +806,7 @@ class OpenStackGatewayContainer(BaseGatewayContainer):
         # all available networks and perform an assignment test to infer valid
         # floating ip nets.
         dummy_router = self._provider.networking.routers.create(
-            network=self._network, name='cb_conn_test_router')
+            label='cb-conn-test-router', network=self._network)
         with cb_helpers.cleanup_action(lambda: dummy_router.delete()):
             try:
                 dummy_router.attach_gateway(external_net)
@@ -744,11 +814,8 @@ class OpenStackGatewayContainer(BaseGatewayContainer):
             except Exception:
                 return False
 
-    def get_or_create_inet_gateway(self, name=None):
+    def get_or_create_inet_gateway(self):
         """For OS, inet gtw is any net that has `external` property set."""
-        if name:
-            OpenStackInternetGateway.assert_valid_resource_name(name)
-
         external_nets = (n for n in self._provider.networking.networks
                          if n.external)
         for net in external_nets:
@@ -795,16 +862,20 @@ class OpenStackNetwork(BaseNetwork):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return self._network.get('name', None)
 
-    @name.setter
-    def name(self, value):  # pylint:disable=arguments-differ
+    @label.setter
+    def label(self, value):  # pylint:disable=arguments-differ
         """
-        Set the network name.
+        Set the network label.
         """
-        self.assert_valid_resource_name(value)
-        self._provider.neutron.update_network(self.id,
-                                              {'network': {'name': value}})
+        self.assert_valid_resource_label(value)
+        self._provider.neutron.update_network(
+            self.id, {'network': {'name': value or ""}})
         self.refresh()
 
     @property
@@ -872,16 +943,20 @@ class OpenStackSubnet(BaseSubnet):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return self._subnet.get('name', None)
 
-    @name.setter
-    def name(self, value):  # pylint:disable=arguments-differ
+    @label.setter
+    def label(self, value):  # pylint:disable=arguments-differ
         """
-        Set the subnet name.
+        Set the subnet label.
         """
-        self.assert_valid_resource_name(value)
+        self.assert_valid_resource_label(value)
         self._provider.neutron.update_subnet(
-            self.id, {'subnet': {'name': value}})
+            self.id, {'subnet': {'name': value or ""}})
         self._subnet['name'] = value
 
     @property
@@ -930,7 +1005,8 @@ class OpenStackFloatingIPContainer(BaseFloatingIPContainer):
         try:
             return OpenStackFloatingIP(
                 self._provider, self._provider.os_conn.network.get_ip(fip_id))
-        except ResourceNotFound:
+        except (ResourceNotFound, NotFoundException):
+            log.debug("Floating IP %s not found.", fip_id)
             return None
 
     def list(self, limit=None, marker=None):
@@ -993,16 +1069,20 @@ class OpenStackRouter(BaseRouter):
 
     @property
     def name(self):
+        return self.id
+
+    @property
+    def label(self):
         return self._router.get('name', None)
 
-    @name.setter
-    def name(self, value):  # pylint:disable=arguments-differ
+    @label.setter
+    def label(self, value):  # pylint:disable=arguments-differ
         """
-        Set the router name.
+        Set the router label.
         """
-        self.assert_valid_resource_name(value)
+        self.assert_valid_resource_label(value)
         self._provider.neutron.update_router(
-            self.id, {'router': {'name': value}})
+            self.id, {'router': {'name': value or ""}})
         self.refresh()
 
     def refresh(self):
@@ -1040,6 +1120,19 @@ class OpenStackRouter(BaseRouter):
             return True
         return False
 
+    @property
+    def subnets(self):
+        # A router and a subnet are linked via a port, so traverse all ports
+        # to find a list of subnets associated with the current router.
+        subnets = []
+        for prt in self._provider.neutron.list_ports().get('ports'):
+            if prt.get('device_id') == self.id and \
+               prt.get('device_owner') == 'network:router_interface':
+                for fixed_ip in prt.get('fixed_ips'):
+                    subnets.append(self._provider.networking.subnets.get(
+                        fixed_ip.get('subnet_id')))
+        return subnets
+
     def attach_gateway(self, gateway):
         self._provider.neutron.add_gateway_router(
             self.id, {'network_id': gateway.id})
@@ -1075,14 +1168,6 @@ class OpenStackInternetGateway(BaseInternetGateway):
     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.get('id')
@@ -1118,6 +1203,7 @@ class OpenStackKeyPair(BaseKeyPair):
 
 
 class OpenStackVMFirewall(BaseVMFirewall):
+    _network_id_tag = "CB-AUTO-associated-network-id: "
 
     def __init__(self, provider, vm_firewall):
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
@@ -1130,7 +1216,45 @@ class OpenStackVMFirewall(BaseVMFirewall):
 
         :return: Always return ``None``.
         """
-        return None
+        # Best way would be to use regex, but using this hacky way to avoid
+        # importing the re package
+        net_id = self._description\
+                     .split(" [{}".format(self._network_id_tag))[-1]\
+                     .split(']')[0]
+        return net_id
+
+    @property
+    def _description(self):
+        return self._vm_firewall.description or ""
+
+    @property
+    def description(self):
+        desc_fragment = " [{}{}]".format(self._network_id_tag,
+                                         self.network_id)
+        desc = self._description
+        if desc:
+            return desc.replace(desc_fragment, "")
+        else:
+            return None
+
+    @property
+    def name(self):
+        """
+        Return the name of this VM firewall.
+        """
+        return self.id
+
+    @property
+    def label(self):
+        return self._vm_firewall.name
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._provider.os_conn.network.update_security_group(
+            self.id, name=value or "")
+        self.refresh()
 
     @property
     def rules(self):
@@ -1191,7 +1315,7 @@ class OpenStackVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
         except HttpException as e:
             self.firewall.refresh()
             # 409=Conflict, raised for duplicate rule
-            if e.http_status == 409:
+            if e.status_code == 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)
@@ -1268,7 +1392,7 @@ class OpenStackBucketObject(BaseBucketObject):
     @property
     def name(self):
         """Get this object's name."""
-        return self._obj.get("name")
+        return self.id
 
     @property
     def size(self):
@@ -1311,7 +1435,7 @@ class OpenStackBucketObject(BaseBucketObject):
               ``swiftclient.service.get_conn`` factory method to
               ``self._provider._connect_swift``
 
-        .. seealso:: https://github.com/gvlproject/cloudbridge/issues/35#issuecomment-297629661 # noqa
+        .. seealso:: https://github.com/CloudVE/cloudbridge/issues/35#issuecomment-297629661 # noqa
         """
         upload_options = {}
         if 'segment_size' not in upload_options:
@@ -1353,18 +1477,19 @@ class OpenStackBucketObject(BaseBucketObject):
                 result = result and del_res['success']
         return result
 
-    def generate_url(self, expires_in=0):
-        """
-        Generates a URL to this object.
-
-        If the object is public, `expires_in` argument is not necessary, but if
-        the object is private, the life time of URL is set using `expires_in`
-        argument.
+    def generate_url(self, expires_in):
+        # Set a temp url key on the object (http://bit.ly/2NBiXGD)
+        temp_url_key = "cloudbridge-tmp-url-key"
+        self._provider.swift.post_account(
+            headers={"x-account-meta-temp-url-key": temp_url_key})
+        base_url = urlparse(self._provider.swift.get_service_auth()[0])
+        access_point = "{0}://{1}".format(base_url.scheme, base_url.netloc)
+        url_path = "/".join([base_url.path, self.cbcontainer.name, self.name])
+        return urljoin(access_point, generate_temp_url(url_path, expires_in,
+                                                       temp_url_key, 'GET'))
 
-        See here for implementation details:
-        http://stackoverflow.com/a/37057172
-        """
-        raise NotImplementedError("This functionality is not implemented yet.")
+    def refresh(self):
+        self._obj = self.cbcontainer.objects.get(self.id)._obj
 
 
 class OpenStackBucket(BaseBucket):
@@ -1380,7 +1505,7 @@ class OpenStackBucket(BaseBucket):
 
     @property
     def name(self):
-        return self._bucket.get("name")
+        return self.id
 
     @property
     def objects(self):
@@ -1398,18 +1523,18 @@ class OpenStackBucketContainer(BaseBucketContainer):
     def get(self, name):
         """
         Retrieve a given object from this bucket.
-
-        FIXME: If multiple objects match the name as their name prefix,
-        all will be returned by the provider but this method will only
-        return the first element.
         """
+        # Swift always returns a reference for the container first,
+        # followed by a list containing references to objects.
         _, object_list = self._provider.swift.get_container(
             self.bucket.name, prefix=name)
-        if object_list:
-            return OpenStackBucketObject(self._provider, self.bucket,
-                                         object_list[0])
-        else:
-            return None
+        # Loop through list of objects looking for an exact name vs. a prefix
+        for obj in object_list:
+            if obj.get('name') == name:
+                return OpenStackBucketObject(self._provider,
+                                             self.bucket,
+                                             obj)
+        return None
 
     def list(self, limit=None, marker=None, prefix=None):
         """

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

@@ -1,12 +1,19 @@
 """
 Services implemented by the OpenStack provider.
 """
-import fnmatch
 import logging
-import re
 
 from cinderclient.exceptions import NotFound as CinderNotFound
 
+from neutronclient.common.exceptions import NeutronClientException
+
+from novaclient.exceptions import NotFound as NovaNotFound
+
+from openstack.exceptions import NotFoundException
+from openstack.exceptions import ResourceNotFound
+
+from swiftclient import ClientException as SwiftClientException
+
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
@@ -30,6 +37,7 @@ from cloudbridge.cloud.interfaces.exceptions \
     import DuplicateResourceException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
+from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Subnet
@@ -38,15 +46,8 @@ from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
-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 OpenStackInstance
-from .resources import OpenStackInternetGateway
 from .resources import OpenStackKeyPair
 from .resources import OpenStackMachineImage
 from .resources import OpenStackNetwork
@@ -198,7 +199,7 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
             return OpenStackVMFirewall(
                 self.provider,
                 self.provider.os_conn.network.get_security_group(firewall_id))
-        except ResourceNotFound:
+        except (ResourceNotFound, NotFoundException):
             log.debug("Firewall %s not found.", firewall_id)
             return None
 
@@ -210,27 +211,33 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
         return ClientPagedResultList(self.provider, firewalls,
                                      limit=limit, marker=marker)
 
-    def create(self, name, description, network_id):
-        OpenStackVMFirewall.assert_valid_resource_name(name)
+    @cb_helpers.deprecated_alias(network_id='network')
+    def create(self, label, network, description=None):
+        OpenStackVMFirewall.assert_valid_resource_label(label)
         log.debug("Creating OpenStack VM Firewall with the params: "
-                  "[name: %s network id: %s description: %s]", name,
-                  network_id, description)
+                  "[label: %s network id: %s description: %s]", label,
+                  network, description)
+        net = network.id if isinstance(network, Network) else network
+        if not description:
+            description = ""
+        description += "[{}{}]".format(OpenStackVMFirewall._network_id_tag,
+                                       net)
         sg = self.provider.os_conn.network.create_security_group(
-            name=name, description=description)
+            name=label, description=description or label)
         if sg:
             return OpenStackVMFirewall(self.provider, sg)
         return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for %s", name)
-        sgs = [self.provider.os_conn.network.find_security_group(name)]
+        log.debug("Searching for %s", label)
+        sgs = [self.provider.os_conn.network.find_security_group(label)]
         results = [OpenStackVMFirewall(self.provider, sg)
                    for sg in sgs if sg]
         return ClientPagedResultList(self.provider, results)
@@ -243,74 +250,6 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
         return True
 
 
-class OpenStackImageService(BaseImageService):
-
-    def __init__(self, provider):
-        super(OpenStackImageService, self).__init__(provider)
-
-    def get(self, image_id):
-        """
-        Returns an Image given its id
-        """
-        log.debug("Getting OpenStack Image with the id: %s", image_id)
-        try:
-            return OpenStackMachineImage(
-                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, **kwargs):
-        name = kwargs.pop('name', None)
-
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        log.debug("Searching for the OpenStack image with the name: %s", name)
-        regex = fnmatch.translate(name)
-        cb_images = [
-            img
-            for img in self
-            if img.name and re.search(regex, img.name)]
-
-        return oshelpers.to_server_paged_list(self.provider, cb_images)
-
-    def list(self, filter_by_owner=True, limit=None, marker=None):
-        """
-        List all images.
-        """
-        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)
-            for img in os_images]
-        return oshelpers.to_server_paged_list(self.provider, cb_images, limit)
-
-
-class OpenStackVMTypeService(BaseVMTypeService):
-
-    def __init__(self, provider):
-        super(OpenStackVMTypeService, self).__init__(provider)
-
-    def list(self, limit=None, marker=None):
-        cb_itypes = [
-            OpenStackVMType(self.provider, obj)
-            for obj in self.provider.nova.flavors.list(
-                limit=oshelpers.os_result_limit(self.provider, limit),
-                marker=marker)]
-
-        return oshelpers.to_server_paged_list(self.provider, cb_itypes, limit)
-
-
 class OpenStackStorageService(BaseStorageService):
 
     def __init__(self, provider):
@@ -352,15 +291,15 @@ class OpenStackVolumeService(BaseVolumeService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for an OpenStack Volume with the name %s", name)
-        search_opts = {'name': name}
+        log.debug("Searching for an OpenStack Volume with the label %s", label)
+        search_opts = {'name': label}
         cb_vols = [
             OpenStackVolume(self.provider, vol)
             for vol in self.provider.cinder.volumes.list(
@@ -382,21 +321,21 @@ class OpenStackVolumeService(BaseVolumeService):
 
         return oshelpers.to_server_paged_list(self.provider, cb_vols, limit)
 
-    def create(self, name, size, zone, snapshot=None, description=None):
+    def create(self, label, size, zone, snapshot=None, description=None):
         """
         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)
+                  "[label: %s size: %s zone: %s snapshot: %s description: %s]",
+                  label, size, zone, snapshot, description)
+        OpenStackVolume.assert_valid_resource_label(label)
 
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         snapshot_id = snapshot.id if isinstance(
             snapshot, OpenStackSnapshot) and snapshot else snapshot
 
         os_vol = self.provider.cinder.volumes.create(
-            size, name=name, description=description,
+            size, name=label, description=description,
             availability_zone=zone_id, snapshot_id=snapshot_id)
         return OpenStackVolume(self.provider, os_vol)
 
@@ -420,22 +359,22 @@ class OpenStackSnapshotService(BaseSnapshotService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        search_opts = {'name': name,  # TODO: Cinder is ignoring name
+        search_opts = {'name': label,  # TODO: Cinder is ignoring name
                        'limit': oshelpers.os_result_limit(self.provider),
                        'marker': None}
-        log.debug("Searching for an OpenStack volume with the following "
+        log.debug("Searching for an OpenStack snapshot with the following "
                   "params: %s", search_opts)
         cb_snaps = [
             OpenStackSnapshot(self.provider, snap) for
             snap in self.provider.cinder.volume_snapshots.list(search_opts)
-            if snap.name == name]
+            if snap.name == label]
 
         return oshelpers.to_server_paged_list(self.provider, cb_snaps)
 
@@ -451,18 +390,18 @@ class OpenStackSnapshotService(BaseSnapshotService):
                              'marker': marker})]
         return oshelpers.to_server_paged_list(self.provider, cb_snaps, limit)
 
-    def create(self, name, volume, description=None):
+    def create(self, label, volume, description=None):
         """
         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)
+        log.debug("Creating a new snapshot of the %s volume.", label)
+        OpenStackSnapshot.assert_valid_resource_label(label)
 
         volume_id = (volume.id if isinstance(volume, OpenStackVolume)
                      else volume)
 
         os_snap = self.provider.cinder.volume_snapshots.create(
-            volume_id, name=name,
+            volume_id, name=label,
             description=description)
         return OpenStackSnapshot(self.provider, os_snap)
 
@@ -522,47 +461,13 @@ class OpenStackBucketService(BaseBucketService):
         """
         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)
-
-
-class OpenStackRegionService(BaseRegionService):
-
-    def __init__(self, provider):
-        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 if r.id == region_id)
-        return next(region, None)
-
-    def list(self, limit=None, marker=None):
-        # pylint:disable=protected-access
-        if self.provider._keystone_version == 3:
-            os_regions = [OpenStackRegion(self.provider, region)
-                          for region in self.provider.keystone.regions.list()]
-            return ClientPagedResultList(self.provider, os_regions,
-                                         limit=limit, marker=marker)
-        else:
-            # Keystone v3 onwards supports directly listing regions
-            # but for v2, this convoluted method is necessary.
-            regions = (
-                endpoint.get('region') or endpoint.get('region_id')
-                for svc in self.provider.keystone.service_catalog.get_data()
-                for endpoint in svc.get('endpoints', [])
-            )
-            regions = set(region for region in regions if region)
-            os_regions = [OpenStackRegion(self.provider, region)
-                          for region in regions]
-
-            return ClientPagedResultList(self.provider, os_regions,
-                                         limit=limit, marker=marker)
-
-    @property
-    def current(self):
-        nova_region = self.provider.nova.client.region_name
-        return self.get(nova_region) if nova_region else None
+        try:
+            self.provider.swift.head_container(name)
+            raise DuplicateResourceException(
+                'Bucket already exists with name {0}'.format(name))
+        except SwiftClientException:
+            self.provider.swift.put_container(name)
+            return self.get(name)
 
 
 class OpenStackComputeService(BaseComputeService):
@@ -591,17 +496,56 @@ class OpenStackComputeService(BaseComputeService):
         return self._region_svc
 
 
+class OpenStackImageService(BaseImageService):
+
+    def __init__(self, provider):
+        super(OpenStackImageService, self).__init__(provider)
+
+    def get(self, image_id):
+        """
+        Returns an Image given its id
+        """
+        log.debug("Getting OpenStack Image with the id: %s", image_id)
+        try:
+            return OpenStackMachineImage(
+                self.provider, self.provider.os_conn.image.get_image(image_id))
+        except (NotFoundException, ResourceNotFound):
+            log.debug("Image %s not found", image_id)
+            return None
+
+    def find(self, **kwargs):
+        filters = ['label']
+        obj_list = self
+        return cb_helpers.generic_find(filters, kwargs, obj_list)
+
+    def list(self, filter_by_owner=True, limit=None, marker=None):
+        """
+        List all images.
+        """
+        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)
+            for img in os_images]
+        return oshelpers.to_server_paged_list(self.provider, cb_images, limit)
+
+
 class OpenStackInstanceService(BaseInstanceService):
 
     def __init__(self, provider):
         super(OpenStackInstanceService, self).__init__(provider)
 
-    def create(self, name, image, vm_type, subnet, zone=None,
+    def create(self, label, image, vm_type, subnet, zone,
                key_pair=None, vm_firewalls=None, user_data=None,
-               launch_config=None,
-               **kwargs):
+               launch_config=None, **kwargs):
         """Create a new virtual machine instance."""
-        OpenStackInstance.assert_valid_resource_name(name)
+        OpenStackInstance.assert_valid_resource_label(label)
 
         image_id = image.id if isinstance(image, MachineImage) else image
         vm_size = vm_type.id if \
@@ -632,7 +576,7 @@ class OpenStackInstanceService(BaseInstanceService):
         nics = None
         if subnet_id:
             log.debug("Creating network port for %s in subnet: %s",
-                      name, subnet_id)
+                      label, subnet_id)
             sg_list = []
             if vm_firewalls:
                 if isinstance(vm_firewalls, list) and \
@@ -640,13 +584,14 @@ class OpenStackInstanceService(BaseInstanceService):
                     sg_list = vm_firewalls
                 else:
                     sg_list = (self.provider.security.vm_firewalls
-                               .find(name=sg) for sg in vm_firewalls)
+                               .find(label=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,
+                    "name": OpenStackInstance._generate_name_from_label(
+                        label, 'cb-port'),
                     "network_id": net_id,
                     "fixed_ips": [{"subnet_id": subnet_id}],
                     "security_groups": sg_id_list
@@ -660,11 +605,13 @@ class OpenStackInstanceService(BaseInstanceService):
                         isinstance(vm_firewalls[0], VMFirewall):
                     sg_name_list = [sg.name for sg in vm_firewalls]
                 else:
-                    sg_name_list = vm_firewalls
+                    sg_list = (self.provider.security.vm_firewalls.get(sg)
+                               for sg in vm_firewalls)
+                    sg_name_list = (sg[0].name for sg in sg_list if sg)
 
         log.debug("Launching in subnet %s", subnet_id)
         os_instance = self.provider.nova.servers.create(
-            name,
+            label,
             None if self._has_root_device(launch_config) else image_id,
             vm_size,
             min_count=1,
@@ -731,14 +678,14 @@ class OpenStackInstanceService(BaseInstanceService):
         return BaseLaunchConfig(self.provider)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        search_opts = {'name': name}
+        search_opts = {'name': label}
         cb_insts = [
             OpenStackInstance(self.provider, inst)
             for inst in self.provider.nova.servers.list(
@@ -770,6 +717,59 @@ class OpenStackInstanceService(BaseInstanceService):
             return None
 
 
+class OpenStackVMTypeService(BaseVMTypeService):
+
+    def __init__(self, provider):
+        super(OpenStackVMTypeService, self).__init__(provider)
+
+    def list(self, limit=None, marker=None):
+        cb_itypes = [
+            OpenStackVMType(self.provider, obj)
+            for obj in self.provider.nova.flavors.list(
+                limit=oshelpers.os_result_limit(self.provider, limit),
+                marker=marker)]
+
+        return oshelpers.to_server_paged_list(self.provider, cb_itypes, limit)
+
+
+class OpenStackRegionService(BaseRegionService):
+
+    def __init__(self, provider):
+        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 if r.id == region_id)
+        return next(region, None)
+
+    def list(self, limit=None, marker=None):
+        # pylint:disable=protected-access
+        if self.provider._keystone_version == 3:
+            os_regions = [OpenStackRegion(self.provider, region)
+                          for region in self.provider.keystone.regions.list()]
+            return ClientPagedResultList(self.provider, os_regions,
+                                         limit=limit, marker=marker)
+        else:
+            # Keystone v3 onwards supports directly listing regions
+            # but for v2, this convoluted method is necessary.
+            regions = (
+                endpoint.get('region') or endpoint.get('region_id')
+                for svc in self.provider.keystone.service_catalog.get_data()
+                for endpoint in svc.get('endpoints', [])
+            )
+            regions = set(region for region in regions if region)
+            os_regions = [OpenStackRegion(self.provider, region)
+                          for region in regions]
+
+            return ClientPagedResultList(self.provider, os_regions,
+                                         limit=limit, marker=marker)
+
+    @property
+    def current(self):
+        nova_region = self.provider.nova.client.region_name
+        return self.get(nova_region) if nova_region else None
+
+
 class OpenStackNetworkingService(BaseNetworkingService):
 
     def __init__(self, provider):
@@ -809,29 +809,30 @@ class OpenStackNetworkService(BaseNetworkService):
                                      limit=limit, marker=marker)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        label = kwargs.pop('label', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
+                            " Supported attributes: %s" % (kwargs, 'label'))
 
-        log.debug("Searching for the OpenStack Network with the "
-                  "name: %s", name)
+        log.debug("Searching for OpenStack Network with label: %s", label)
         networks = [OpenStackNetwork(self.provider, network)
                     for network in self.provider.neutron.list_networks(
-                        name=name)
+                        name=label)
                     .get('networks') if network]
         return ClientPagedResultList(self.provider, networks)
 
-    def create(self, name, cidr_block):
+    def create(self, label, 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}
+                  "[label: %s Cinder Block: %s]", label, cidr_block)
+        OpenStackNetwork.assert_valid_resource_label(label)
+        net_info = {'name': label or ""}
         network = self.provider.neutron.create_network({'network': net_info})
-        return OpenStackNetwork(self.provider, network.get('network'))
+        cb_net = OpenStackNetwork(self.provider, network.get('network'))
+        if label:
+            cb_net.label = label
+        return cb_net
 
 
 class OpenStackSubnetService(BaseSubnetService):
@@ -856,41 +857,40 @@ class OpenStackSubnetService(BaseSubnetService):
         return ClientPagedResultList(self.provider, subnets,
                                      limit=limit, marker=marker)
 
-    def create(self, name, network, cidr_block, zone=None):
+    def create(self, label, network, cidr_block, zone):
         """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)
-
+                  "[Label: %s Network: %s Cinder Block: %s Zone: -ignored-]",
+                  label, network, cidr_block)
+        OpenStackSubnet.assert_valid_resource_label(label)
         network_id = (network.id if isinstance(network, OpenStackNetwork)
                       else network)
-        subnet_info = {'name': name, 'network_id': network_id,
+        subnet_info = {'name': label, 'network_id': network_id,
                        'cidr': cidr_block, 'ip_version': 4}
         subnet = (self.provider.neutron.create_subnet({'subnet': subnet_info})
                   .get('subnet'))
-        return OpenStackSubnet(self.provider, subnet)
+        cb_subnet = OpenStackSubnet(self.provider, subnet)
+        return cb_subnet
 
-    def get_or_create_default(self, zone=None):
+    def get_or_create_default(self, zone):
         """
         Subnet zone is not supported by OpenStack and is thus ignored.
         """
         try:
-            sn = self.find(name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME)
+            sn = self.find(label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL)
             if sn:
                 return sn[0]
-            # No default; create one
-            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)
+            # No default subnet look for default network, then create subnet
+            net = self.provider.networking.networks.get_or_create_default()
+            sn = self.provider.networking.subnets.create(
+                label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
+                cidr_block='10.0.0.0/24',
+                network=net)
+            router = self.provider.networking.routers.get_or_create_default(
+                net)
             router.attach_subnet(sn)
-            gteway = net.gateways.get_or_create_inet_gateway(
-                        OpenStackInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME)
-            router.attach_gateway(gteway)
+            gateway = net.gateways.get_or_create_inet_gateway()
+            router.attach_gateway(gateway)
             return sn
         except NeutronClientException:
             return None
@@ -924,11 +924,11 @@ class OpenStackRouterService(BaseRouterService):
 
     def find(self, **kwargs):
         obj_list = self
-        filters = ['name']
+        filters = ['label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
         return ClientPagedResultList(self._provider, list(matches))
 
-    def create(self, name, network):
+    def create(self, label, network):
         """
         ``network`` is not used by OpenStack.
 
@@ -936,9 +936,9 @@ class OpenStackRouterService(BaseRouterService):
         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)
+        log.debug("Creating OpenStack Router with the label: %s", label)
+        OpenStackRouter.assert_valid_resource_label(label)
 
-        body = {'router': {'name': name}} if name else None
+        body = {'router': {'name': label}} if label else None
         router = self.provider.neutron.create_router(body)
         return OpenStackRouter(self.provider, router.get('router'))

+ 0 - 0
.codeclimate.yml → codeclimate.yml


+ 2 - 2
docs/api_docs/cloud/exceptions.rst

@@ -23,9 +23,9 @@ ProviderConnectionException
 .. autoclass:: cloudbridge.cloud.interfaces.exceptions.ProviderConnectionException
     :members:
 
-InvalidNameException
+InvalidLabelException
 -----------------------------
-.. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidNameException
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidLabelException
     :members:
 
 InvalidValueException

+ 34 - 0
docs/concepts.rst

@@ -1,6 +1,9 @@
 Concepts and Organisation
 =========================
 
+Object types
+------------
+
 Conceptually, CloudBridge consists of the following types of objects.
 
 1. Providers - Represents a connection to a cloud provider, and is
@@ -25,6 +28,37 @@ an instance. Similarly, VolumeService.create() will return a Volume object.
 
 The actual source code structure of CloudBridge also mirrors this organisation.
 
+Object identification and naming
+---------------------------------
+
+In order to function uniformly across cloud providers, object identity
+and naming must be conceptually consistent. In CloudBridge, there are three
+main properties for identifying and naming an object.
+
+1.Id - The `id` corresponds to a unique identifier that can be reliably used to
+reference a resource. All CloudBridge resources have an id. Most methods in
+CloudBridge services, such as `get`, use the `id` property to identify and
+retrieve objects.
+
+2. Name - The `name` property is a more human-readable identifier for
+a particular resource, and is often useful to display to the end user instead
+of the `id`. While it is often unique, it is not guaranteed to be so, and
+therefore, the `id` property must always be used for uniquely identifying
+objects. All CloudBridge resources have a `name` property. The `name` property
+is often assigned during resource creation, and is often derived from the
+`label` property by appending some unique characters to it. Once assigned
+however, it is unchangeable.
+
+3. Label - Most resources also support a `label` property, which is a user
+changeable value that can be used to describe an object. When creating
+resources, cloudbridge often accepts a `label` property as a parameter.
+The `name` property is derived from the `label`, by appending some unique
+characters to it. However, there are some resources which do not support a
+`label` property, such as key pairs and buckets. In the latter case, the
+`name` can be specified during resource creation, but cannot be changed
+thereafter.
+
+
 Detailed class relationships
 ----------------------------
 

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

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

+ 87 - 21
docs/getting_started.rst

@@ -33,7 +33,7 @@ AWS:
     config = {'aws_access_key': 'AKIAJW2XCYO4AF55XFEQ',
               'aws_secret_key': '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'}
     provider = CloudProviderFactory().create_provider(ProviderList.AWS, config)
-    image_id = 'ami-2d39803a'  # Ubuntu 14.04 (HVM)
+    image_id = 'ami-aa2ea6d0'  # Ubuntu 16.04 (HVM)
 
 OpenStack (with Keystone authentication v2):
 
@@ -64,7 +64,7 @@ OpenStack (with Keystone authentication v3):
               'os_user_domain_name': 'domain name'}
     provider = CloudProviderFactory().create_provider(ProviderList.OPENSTACK,
                                                       config)
-    image_id = '97755049-ee4f-4515-b92f-ca00991ee99a'  # Ubuntu 14.04 @ Jetstream
+    image_id = 'acb53109-941f-4593-9bf8-4a53cb9e0739'  # Ubuntu 16.04 @ Jetstream
 
 Azure:
 
@@ -77,7 +77,7 @@ Azure:
               'azure_secret': 'REPLACE WITH ACTUAL VALUE',
               'azure_tenant': ' REPLACE WITH ACTUAL VALUE'}
     provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
-    image_id = 'ami-2d39803a'  # Ubuntu 14.04 (HVM)
+    image_id = 'Canonical:UbuntuServer:16.04.0-LTS:latest'  # Ubuntu 16.04
 
 
 List some resources
@@ -86,7 +86,7 @@ Once you have a reference to a provider, explore the cloud platform:
 
 .. code-block:: python
 
-    provider.security.security_groups.list()
+    provider.security.firewalls.list()
     provider.compute.vm_types.list()
     provider.storage.snapshots.list()
     provider.storage.buckets.list()
@@ -102,11 +102,11 @@ on disk as a read-only file.
 
 .. code-block:: python
 
-    kp = provider.security.key_pairs.create('cloudbridge_intro')
+    import os
+    kp = provider.security.key_pairs.create('cloudbridge-intro')
     with open('cloudbridge_intro.pem', 'w') as f:
         f.write(kp.material)
-    import os
-    os.chmod('cloudbridge_intro.pem', 0400)
+    os.chmod('cloudbridge_intro.pem', 0o400)
 
 Create a network
 ----------------
@@ -116,26 +116,27 @@ 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')
+    net = provider.networking.networks.create(cidr_block='10.0.0.0/16',
+                                              label='my-network')
+    sn = net.create_subnet(cidr_block='10.0.0.0/28', label='my-subnet')
+    router = provider.networking.routers.create(network=net, label='my-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway()
     router.attach_gateway(gateway)
 
 
 Create a VM firewall
------------------------
+--------------------
 Next, we need to create a VM firewall (also commonly known as a security group)
 and add a rule to allow ssh access. A VM firewall needs to be associated with
 a private network.
 
 .. code-block:: python
 
-    net = provider.networking.networks.get('desired network ID')
+    from cloudbridge.cloud.interfaces.resources import TrafficDirection
     fw = provider.security.vm_firewalls.create(
-        'cloudbridge-intro', 'A VM firewall used by CloudBridge', net.id)
+        label='cloudbridge-intro', description='A VM firewall used by
+        CloudBridge', network_id=net.id)
     fw.rules.create(TrafficDirection.INBOUND, 'tcp', 22, 22, '0.0.0.0/0')
 
 Launch an instance
@@ -147,12 +148,13 @@ also add the network interface as a launch argument.
 .. code-block:: python
 
     img = provider.compute.images.get(image_id)
+    zone = provider.compute.regions.get(provider.region_name).zones[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, vm_type=vm_type,
-        subnet=subnet, key_pair=kp, vm_firewalls=[fw])
+        image=img, vm_type=vm_type, label='cloudbridge-intro',
+        subnet=sn, zone=zone, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
     # Show instance state
@@ -172,11 +174,13 @@ Assign a public IP address
 --------------------------
 To access the instance, let's assign a public IP address to the instance. For
 this step, we'll first need to allocate a floating IP address for our account
-and then associate it with the instance.
+and then associate it with the instance. Note that floating IPs are associated
+with an Internet Gateway so we allocate the IP under the gateway we dealt with
+earlier.
 
 .. code-block:: python
 
-    fip = provider.networking.floating_ips.create()
+    fip = gateway.floating_ips.create()
     inst.add_floating_ip(fip)
     inst.refresh()
     inst.public_ips
@@ -185,14 +189,76 @@ and then associate it with the instance.
 From the command prompt, you can now ssh into the instance
 ``ssh -i cloudbridge_intro.pem ubuntu@54.166.125.219``.
 
+Get a resource
+--------------
+When a resource already exists, a reference to it can be retrieved using either
+its ID, name, or label. It is important to note that while IDs and names are
+unique, multiple resources of the same type could use the same label, thus the
+`find` method always returns a list, while the `get` method returns a single
+object. While the methods are similar across resources, they are explicitly
+listed in order to help map each resource with the service that handles it.
+Note that labeled resources allow to find by label, while unlabeled
+resources find by name or their special properties (eg: public_ip for
+floating IPs). For more detailed information on the types of resources and
+their provider mappings, see :doc:`topics/resource_types_and_mappings`.
+
+.. code-block:: python
+
+    # Key Pair
+    kp = provider.security.key_pairs.get('keypair ID')
+    kp_list = provider.security.key_pairs.find(name='cloudbridge-intro')
+    kp = kp_list[0]
+
+    # Floating IPs
+    fip = gateway.floating_ips.get('FloatingIP ID')
+    # Find using public IP address
+    fip_list = gateway.floating_ips.find(public_ip='IP address')
+    # Find using name (the behavior of the `name` property can be 
+    # cloud-dependent). More details can be found `here <topics/resource_types_and_mapping.html>`
+    fip_list = net.gateways.floating_ips.find(name='my-fip')
+    fip = fip_list[0]
+
+    # Network
+    net = provider.networking.networks.get('network ID')
+    net_list = provider.networking.networks.find(label='my-network')
+    net = net_list[0]
+
+    # Subnet
+    sn = provider.networking.subnets.get('subnet ID')
+    # Unknown network
+    sn_list = provider.networking.subnets.find(label='my-subnet')
+    # Known network
+    sn_list = provider.networking.subnets.find(network=net.id,
+                                               label='my-subnet')
+    sn = sn_list(0)
+
+    # Router
+    router = provider.networking.routers.get('router ID')
+    router_list = provider.networking.routers.find(label='my-router')
+    router = router_list[0]
+
+    # Gateway
+    gateway = net.gateways.get_or_create_inet_gateway()
+
+    # Firewall
+    fw = provider.security.vm_firewalls.get('firewall ID')
+    fw_list = provider.security.vm_firewalls.find(label='cloudbridge-intro')
+    fw = fw_list[0]
+
+    # Instance
+    inst = provider.compute.instances.get('instance ID')
+    inst_list = provider.compute.instances.list(label='cloudbridge-intro')
+    inst = inst_list[0]
+
+
 Cleanup
 -------
 To wrap things up, let's clean up all the resources we have created
 
 .. code-block:: python
 
-    inst.terminate()
     from cloudbridge.cloud.interfaces import InstanceState
+    inst.delete()
     inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
                    terminal_states=[InstanceState.ERROR])  # Blocking call
     fip.delete()
@@ -200,7 +266,7 @@ To wrap things up, let's clean up all the resources we have created
     kp.delete()
     os.remove('cloudbridge_intro.pem')
     router.detach_gateway(gateway)
-    router.detach_subnet(subnet)
+    router.detach_subnet(sn)
     gateway.delete()
     router.delete()
     sn.delete()

+ 169 - 0
docs/topics/aws_mapping.rst

@@ -0,0 +1,169 @@
+AWS Dashboard
+-------------
+AWS has a particular dashboard as resources are found within different
+services. The following table lists the dashboard location of each resource,
+and the below screenshot shows how the switch between the various services.
+
++------------------------+-----+
+| Instance               | EC2 |
++------------------------+-----+
+| MachineImage (Private) | EC2 |
++------------------------+-----+
+| Volume                 | EC2 |
++------------------------+-----+
+| Snapshot               | EC2 |
++------------------------+-----+
+| VMFirewall             | EC2 |
++------------------------+-----+
+| FloatingIP             | EC2 |
++------------------------+-----+
+| KeyPair                | EC2 |
++------------------------+-----+
+| VMFirewallRule         | EC2 |
++------------------------+-----+
+| Network                | VPC |
++------------------------+-----+
+| Subnet                 | VPC |
++------------------------+-----+
+| Router                 | VPC |
++------------------------+-----+
+| InternetGateway        | VPC |
++------------------------+-----+
+| Bucket                 | S2  |
++------------------------+-----+
+| BucketObject           | S2  |
++------------------------+-----+
+
+.. figure:: captures/aws-services-dash.png
+   :scale: 50 %
+   :alt: EC2, VPC, and S3
+
+   Resources in AWS are separated into three dashboards depending on the
+   type of service handling the resources
+
+
+AWS - Labeled Resources
+-----------------------
++------------------------+-------------------+----------------+----------------+----------+
+| Labeled Resource       | AWS Resource Type | CB ID          | CB Name        | CB Label |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSInstance            | Instance          | Instance ID    | Instance ID    | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSMachineImage        | AMI               | AMI ID         | AMI Name       | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSNetwork             | VPC               | VPC ID         | VPC ID         | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSSubnet              | Subnet            | Subnet ID      | Subnet ID      | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSRouter              | Route Table       | Route Table ID | Route Table ID | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSVolume              | Volume            | Volume ID      | Volume ID      | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSSnapshot            | Snapshot          | Snapshot ID    | Snapshot ID    | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSVMFirewall          | Security Group    | Group ID       | Group Name     | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. For all labeled resources, the `label` property in AWS
+maps to the tag with `key:Name`. However, unlike in Azure where all resources
+have names, only some AWS resources have an unchangeable name by which to
+identify them. Thus, for most AWS resources, the `name` property maps to the
+ID, in order to preserve the concept of names being a unique identifier,
+even if they are not easily readable in this context. For resources that do
+support naming in AWS, the `name` will be generated from the `label` given at
+creation, consisting of up to 55 characters from the label, followed by a UUID.
+The label property can subsequently be changed, but the name property will
+be set at creation and remain unchanged. Finally, labeled resources support
+a `label` parameter for the `find` method in their corresponding services.
+The below screenshots will help map these properties to AWS objects in the
+web portal.
+
+.. figure:: captures/aws-instance-dash.png
+   :scale: 50 %
+   :alt: name, ID, and label properties for AWS EC2 Instances
+
+   The CloudBridge `name` and `ID` properties map to the unchangeable
+   resource ID in AWS when the resource does not allow for an unchangeable
+   name. The `label` property maps to the tag with key 'Name' for all
+   resources in AWS. By default, this label will appear in the first
+   column.
+
+.. figure:: captures/az-ami-dash.png
+   :scale: 50 %
+   :alt: name, ID, and label properties for AWS EC2 AMIs
+
+   When an AWS resource allows for an unchangeable name, the CloudBridge
+   `ID` property maps to the Resource ID, while the `Name` property maps to
+   the Resource Name. The `label` property maps to the tag with key 'Name'
+   for all resources in AWS. By default, this label will appear in the first
+   column.
+
+
+AWS - Unlabeled Resources
+---------------------------
++-----------------------+--------------------+-------+---------+----------+
+| Unlabeled Resource    | AWS Resource Type  | CB ID | CB Name | CB Label |
++-----------------------+--------------------+-------+---------+----------+
+| AWSKeyPair            | Key Pair           | Name  | Name    | -        |
++-----------------------+--------------------+-------+---------+----------+
+| AWSBucket             | Bucket             | Name  | Name    | -        |
++-----------------------+--------------------+-------+---------+----------+
+| AWSBucketObject       | Bucket Object      | Key   | Key     | -        |
++-----------------------+--------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in AWS, as these resources don't have an `ID` in the
+traditional sense and can be located by name. Finally, unlabeled resources
+support a `name` parameter for the `find` method in their corresponding
+services.
+
+.. figure:: captures/aws-bucket.png
+   :scale: 50 %
+   :alt: list of buckets on AWS dashboard
+
+   Buckets can be found in the Amazon S3 portal. BucketObjects are contained
+   within each Bucket.
+
+
+AWS - Special Unlabeled Resources
+-----------------------------------
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| Unlabeled Resource | AWS Resource Type      | CB ID | CB Name                                                                | CB Label |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| AWSFloatingIP      | Elastic IP             | ID    | [public_ip]                                                            | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| AWSInternetGateway | Internet Gateway       | ID    | tag:Name                                                               | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| AWSVMFirewallRule  | Network Security Rules | ID    | Generated: [direction]-[protocol]-[from_port]-[to_port]-[cidr]-[fw_id] | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed above. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP.
+Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`.
+
+In terms of the gateway, given that gateways are not their own objects in
+other providers, we do not treat them like labeled resources in AWS although
+they could support labels. Thus, the internet gateway create method does not
+take a name parameter, and the `name` property is set automatically to a
+default value. Note that since this value is stored in the tag with key Name,
+the AWS dashboard does allow for its modification, although that is not
+encouraged as the default name is expected for the
+`get_or_create_inet_gateway` method.
+
+Finally, Firewall Rules in AWS differ from traditional unlabeled resources
+by the fact that they do not take a `name` parameter at creation, and the
+`name` property is automatically generated from the rule's properties, as
+shown above. These rules can be found within each Firewall (i.e. Security
+Group) in the AWS EC2 portal, and will not have any name in the AWS dashboard
+

+ 223 - 0
docs/topics/azure_mapping.rst

@@ -0,0 +1,223 @@
+Azure - Labeled Resources
+-------------------------
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| Labeled CloudBridge Resource          | Azure Resource Type    | CB ID | CB Name                | CB Label                           |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureInstance                         | Virtual Machine        | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureMachineImage (Private)           | Image                  | ID    | Name                   | tag:Label                          |
+| AzureMachineImage (Marketplace Image) | VirtualMachineImage    | ID    | URN                    | URN                                |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureNetwork                          | Virtual Network        | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureSubnet                           | Subnet                 | ID    | NetworkName/SubnetName | Network:tag:SubnetLabel_SubnetName |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureRouter                           | Route Table            | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureVolume                           | Disk                   | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureSnapshot                         | Snapshot               | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureVMFirewall                       | Network security group | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. The `label` will then be used to create the `name`,
+which will consist of up to 55 characters from the label, followed by a UUID.
+The label property can subsequently be changed, but the name property will
+remain unchanged, as it is part of the ID. Finally, labeled resources support
+a `label` parameter for the `find` method in their corresponding services.
+The below screenshots will help map these properties to Azure objects in the
+web portal.
+Additionally, although Azure Security Groups are not associated with a
+specific network, such an association is done in CloudBridge, due to its
+necessity in AWS. As such, the VMFirewall creation method requires a
+`network` parameter and the association is accomplished in OpenStack through
+a tag with the key `network_id`.
+
+.. figure:: captures/az-label-dash.png
+   :scale: 50 %
+   :alt: name and label properties in Azure portal
+
+   The CloudBridge `name` property always maps to the unchangeable resource
+   name in Azure. The `label` property maps to the tag with key 'Label' for
+   most resources in Azure. By default, this label will appear in the tags
+   column, but can also be made into its own column, using the feature
+   pointed out in the screenshot above.
+
+.. figure:: captures/az-net-id.png
+   :scale: 50 %
+   :alt: network id in Azure portal
+
+   The CloudBridge `ID` property most often maps to the Resource ID in Azure,
+   which can be found under the properties tab within a resource. The above
+   screenshot shows where to find a resource's ID in Azure's web portal.
+
+.. figure:: captures/az-net-label.png
+   :scale: 50 %
+   :alt: network label in Azure portal
+
+   The CloudBridge `label` property most often maps to the tag with key
+   'Label' in Azure, which can be found under the tags tab within a resource.
+   The above screenshot shows where to find a resource's label in Azure's
+   web portal.
+
+Two labeled resources are exceptions to the general trends presented above,
+namely public images (i.e. Azure Marketplace Images) and subnets.
+
+These public images can be found in the Azure Marketplace, and cannot be
+found on a user's dashboard. A Marketplace Image can be passed either by URN,
+or by public ID, and does not need to be linked to a user. While all
+Marketplace images will not be be listed by the find or list methods at the
+moment, a pre-set list of popular images is built into CloudBridge for that
+purpose. However, one can choose to list all Marketplace Images using the
+`list_marketplace_images` function in the azure client. Specifically,
+this can be done as follows:
+
+.. code-block:: python
+
+    # List all images
+    # Note that in September 2018, around 10 minutes of wall time were required
+    # to fetch the entire list
+    provider.azure_client.list_marketplace_images()
+    # List all images published by Canonical
+    provider.azure_client.list_marketplace_images(publisher='Canonical')
+    # List all Ubuntu images
+    provider.azure_client.list_marketplace_images(publisher='Canonical',
+                                                  offer='UbuntuServer')
+    # List all Ubuntu 16.04 images
+    provider.azure_client.list_marketplace_images(publisher='Canonical',
+                                                  offer='UbuntuServer',
+                                                  sku='16.04.0-LTS')
+    # The ID of the listed object can then be used to retrieve an instance
+    img = provider.compute.images.get
+            ('/Subscriptions/{subscriptionID}/Providers/Microsoft.Compute/\
+            Locations/{regionName}/Publishers/Canonical/ArtifactTypes/VMImage\
+            /Offers/UbuntuServer/Skus/16.04.0-LTS/Versions/16.04.201808140')
+    # The URN can also be used instead if it is already known
+    # When the latest version is desired, it can be retrieved with the
+    # keyword 'latest' in the URN without specifying a version
+    img = provider.compute.images.get(
+          'Canonical:UbuntuServer:16.04.0-LTS:latest')
+
+
+Given that these resources are not owned by the user, they can only be
+referenced and all setters will silently pass. CloudBridge properties `name`
+and `label` will map to the URN, while the `ID` will map to the public `ID`.
+It is also important to note that some of these resources are paid and
+required a plan to use, while others are free but likewise require accepting
+certain terms before being used. These plans and terms are passed and
+accepted silently by CloudBridge in order to keep the code cloud-independent.
+We therefore encourage using the `marketplace website<https://azuremarketplace.microsoft.com/en-us>`_
+to view image and plan details before using them in CloudBridge.
+
+Additionally, Subnets are a particular resource in Azure because they are
+not simply found in the Resource Group like most resources, but are rather
+nested within a network. Moreover, Subnets do not support tags in Azure.
+However, they remain a labeled resource in CloudBridge, which was
+accomplished by creating Network tags holding Subnet labels in Azure. The
+below screenshots will show how to find Subnets and their labels in the
+Azure web portal.
+
+.. figure:: captures/az-subnet-name.png
+   :scale: 50 %
+   :alt: subnet name in Azure portal
+
+   The CloudBridge `name` property for Subnets corresponds to the
+   unchangeable Resource Name in Azure. However, unlike other resources
+   where the Azure Name maps directly to the `name` property alone, a Subnet's
+   `name` property returns the Network's name and the Subnet's name,
+   separated by a slash, thus having the format [networkName]/[subnetName].
+   Subnets are additionally not found in the default resource list, but are
+   rather nested within a Network, in the Subnets tab as shown above.
+
+.. figure:: captures/az-subnet-label.png
+   :scale: 50 %
+   :alt: subnet label in Azure portal
+
+   The CloudBridge `label` property most often maps to the tag with key
+   'Label' in Azure, which can be found under the tags tab within a resource.
+   However, given that Subnets can't hold tags themselves, we set their tags
+   in the Network with which they are associated. The tag name 'Label' thus
+   corresponds to the Network's label, while each contained Subnet will have
+   a corresponding tag with the name 'SubnetLabel_[subnetName]'.
+
+
+Azure - Unlabeled Resources
+---------------------------
++--------------------+----------------------------------------+-------+---------+----------+
+| Unlabeled Resource | Azure Resource Type                    | CB ID | CB Name | CB Label |
++--------------------+----------------------------------------+-------+---------+----------+
+| AzureKeyPair       | StorageAccount:Table                   | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+| AzureBucket        | StorageAccount:BlobContainer           | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+| AzureBucketObject  | StorageAccount:BlobContainer:BlockBlob | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in Azure, as these resources don't have an `ID` in the
+traditional sense and can be located simply by name. Finally, unlabeled
+resources support a `name` parameter for the `find` method in their
+corresponding services.
+
+.. figure:: captures/az-storacc.png
+   :scale: 50 %
+   :alt: storage account in Azure portal
+
+   Bucket and Key Pair objects are different than other resources in Azure,
+   as they are not resources simply residing in a resource group, but are
+   rather found in a storage account. As a result of this difference, these
+   resources do not support labels, and cannot be seen on the default
+   dashboard. In order to find these resources in the Azure web portal, one
+   must head to the storage account containing them, and look in the `Blobs`
+   and `Tables` services respectively for `Buckets` and `KeyPairs`.
+
+
+Azure - Special Unlabeled Resources
+-----------------------------------
++-------------------------+------------------------+--------------------+--------------------+----------+
+| Unlabeled Resource      | Azure Resource Type    | CB ID              | CB Name            | CB Label |
++-------------------------+------------------------+--------------------+--------------------+----------+
+| AzureFloatingIP         | Public IP Address      | ID                 | [public_ip]        | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+| AzureInternetGateway    | None                   | cb-gateway-wrapper | cb-gateway-wrapper | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+| AzureVMFirewallRule     | Network Security Rules | ID                 | name               | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed above. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP. Thus, the name
+seen in the Azure web portal will not map to the CloudBridge name, but will
+rather be auto-generated, while the Azure `IP Address` will map to CloudBridge
+name. Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`, and the get method also accepts a
+'Public IP' instead of an 'ID'.
+
+In terms of the gateway, one of the major discrepancies in Azure is the
+non-existence of an InternetGateway. In fact, Azure resources are exposed
+with no need for an Internet gateway. However, in order to keep resources
+consistent across providers, the CloudBridge Gateway resource exists
+regardless of provider. For Azure, the gateway object created through
+CloudBridge will not appear on the dashboard, but will rather be a cached
+CloudBridge-level wrapper object.
+For a succinct comparison between AWS Gateways and Azure, see `this answer
+<https://social.msdn.microsoft.com/Forums/en-US/
+814ccee0-9fbb-4c04-8135-49d0aaea5f38/
+equivalent-of-aws-internet-gateways-in-azure?
+forum=WAVirtualMachinesVirtualNetwork>`_.
+
+Finally, Firewall Rules in Azure differ from traditional unlabeled
+resources by the fact that they do not take a `name` parameter at creation.
+These rules can be found within each Firewall (i.e. Security Group) in the
+Azure web portal, and will have an automatically generated `name` of the form
+'cb-rule-[int]'.

BIN
docs/topics/captures/aws-ami-dash.png


BIN
docs/topics/captures/aws-bucket.png


BIN
docs/topics/captures/aws-instance-dash.png


BIN
docs/topics/captures/aws-services-dash.png


BIN
docs/topics/captures/az-label-dash.png


BIN
docs/topics/captures/az-net-id.png


BIN
docs/topics/captures/az-net-label.png


BIN
docs/topics/captures/az-storacc.png


BIN
docs/topics/captures/az-subnet-label.png


BIN
docs/topics/captures/az-subnet-name.png


BIN
docs/topics/captures/os-instance-dash.png


BIN
docs/topics/captures/os-kp-dash.png


+ 1 - 1
docs/topics/contributor_guide.rst

@@ -8,7 +8,7 @@ CloudBridge Provider.
    :maxdepth: 1
 
     Design Goals <design_goals.rst>
-    Design Decisions <design-decisions.rst>
+    Design Decisions <design_decisions.rst>
     Testing <testing.rst>
     Provider Development Walkthrough <provider_development.rst>
     Release Process <release_process.rst>

+ 0 - 20
docs/topics/design-decisions.rst

@@ -1,20 +0,0 @@
-Design decisions
-~~~~~~~~~~~~~~~~
-
-This document captures outcomes and, in some cases, the through process behind
-some of the design decisions that took place while architecting CloudBridge.
-It is intended as a reference.
-
-- **Require zone parameter when creating a default subnet.**
-
-  Placement zone is required because it is an explicit application decision,
-  even though ideally *default* would not require input. Before requiring it,
-  the implementations would create a subnet in each availability zone and return
-  the first one in the list. This could potentially return different values over
-  time. Another factor influencing the decision was the example of creating a
-  volume followed by creating an instance with presumably the two needing to be
-  in the same zone. By requiring the zone across the board, it is less likely to
-  lead to a miss match. (Related to 63_.)
-
-
-  .. _63: https://github.com/gvlproject/cloudbridge/issues/63

+ 126 - 0
docs/topics/design_decisions.rst

@@ -0,0 +1,126 @@
+Design decisions
+================
+
+This document captures outcomes and, in some cases, the through process behind
+some of the design decisions that took place while architecting CloudBridge.
+It is intended as a reference.
+
+Require zone parameter when creating a default subnet
+-----------------------------------------------------
+
+  Placement zone is required because it is an explicit application decision,
+  even though ideally *default* would not require input. Before requiring it,
+  the implementations would create a subnet in each availability zone and return
+  the first one in the list. This could potentially return different values over
+  time. Another factor influencing the decision was the example of creating a
+  volume followed by creating an instance with presumably the two needing to be
+  in the same zone. By requiring the zone across the board, it is less likely to
+  lead to a mismatch. (Related to 63_.)
+
+Resource identification, naming, and labeling
+---------------------------------------------
+
+  While it would be reasonable to expect that complex constructs like
+  networking would be the most difficult to abstract away uniformly across
+  providers, in retrospect, simple naming of objects has arguably been the most
+  complex and convoluted to map consistently. CloudBridge has been through
+  several iterations of naming and labeling, before finally settling on the
+  current design. This section captures that history and design rationale.
+
+  **First iteration**
+  In the early days, when CloudBridge supported only AWS and OpenStack, there
+  were only two concepts, `id` and `name`. The `id` was straightforward enough,
+  as it usually mapped to a unique identifier, auto-generated by the provider.
+  The `name` generally mapped to a tag in the case of AWS, and a `name` field
+  in the case of OpenStack. However, even then, there were inconsistencies
+  within individual providers. For example, while AWS generally supported tags,
+  it had a dedicated name field for machine images called ami-name.
+  Furthermore, this name field could only be set at image creation time, and
+  could not be changed thereafter. Similarly, AWS does not allow VM firewall
+  (i.e., security group) names to be changed. Nevertheless, CloudBridge
+  continued to use id and name, with the name being changeable for some
+  resources, and read-only in others.
+
+  As CloudBridge evolved and support was added for Azure and GCE, things only
+  became more complex. Some providers (e.g. Azure and GCE) used a user-provided
+  value instead of an auto-generated value as an `id`, which would also be
+  displayed in their respective dashboards as `Name`. This meant that they were
+  treating their servers as individually named pets, instead of adopting the
+  cattle model, should one be tempted to use that macabre `pets vs cattle`_
+  analogy. These user-provided names could not be changed after a resource had
+  been created. Instead, these providers seemed to be gravitating toward the
+  use of tags (or labels) to support arbitrary naming and name changes. Yet,
+  not all resources support tags so CloudBridge could not rely solely on tags.
+  Further, tags do not need to be unique across multiple resources, while names
+  do (at least for some resources, such as vmfirewalls within a private
+  network). Overall, consistency was challenging to achieve with resource
+  naming. Therefore, it was decided that CloudBridge would continue to support
+  resource renaming to the best extent possible and balance between the
+  use of the resource name property and resource tags. However, because of the
+  inconsistency in rename functionality across providers, using the rename
+  capabilities within CloudBridge would lead to cloud-dependent code (Related to
+  131_.) and therefore, the only option was to continue to stick a caveat emptor
+  to resource renaming.
+  
+  **Second iteration**
+  However, it soon became apparent that this overloaded terminology was
+  continuing to cause confusion. The `id` property in CloudBridge mapped to the
+  unchangeable `name` property in Azure and GCE, and the *name* property in
+  cloudbridge sometimes mapped to a *tag* in certain providers, and a *name* in
+  other providers and they were sometimes read-only, sometimes writable. In an
+  attempt to disambiguate these concepts, it was then decided that perhaps
+  three concepts were needed - `id`, `display_id`, and `label`. The `id` would
+  continue to refer to a unique identifier for a resource and be mapped
+  accordingly to the underlying provider. The `display_id` would be a more
+  user-friendly version of an id, suitable for display to an end-user and be
+  unchangeable, but on rare occasions, not unique. For example, AWS ami-name
+  was a `display_id` while the ami-id was an `id`. Similarly, an Azure resource
+  name mapped to the `display_id`, since it was an unchangeable, user-friendly
+  identifier. Finally, label was a changeable, user-assignable value that would
+  be mapped often to a tag on the resource, or the name of the resource, should
+  the name be changeable. This clearly disambiguated between unique
+  identifiers, user-assignable values and read-only, user-friendly values. At
+  object creation, all services would accept a label as an optional parameter.
+  If provided, the `display_id` would sometimes be derived from the label by
+  appending a uuid to the label, depending on the provider. At other times, it
+  would simply map to an id.
+
+  **Third iteration**
+  It soon became apparent that some resources like keypairs could not have a
+  label at all, yet needed to be named during object creation. However, we
+  could not use `display_id` for this purpose became the display_id, by
+  definition, is unchangeable. It could not be called `label` because the label,
+  in contrast, was changeable. Therefore, it seemed like we were back to
+  calling it `name` instead, introducing yet a fourth term. To simplify this,
+  it was then decided that `display_id` and `name` would be collapsed together
+  into one term and be called `name` instead. All resources would have an `id`
+  and a `name`, and resources that support it would have a `label`. To make
+  things even simpler and consistent, it was also decided that label would be
+  made mandatory for all resources during object creation, and follow the same
+  restrictions as name, which is to have a 3 character minimum. (This was to
+  deal with an exception in OpenStack, where the label mapped to instance name,
+  but could not be empty. Therefore, by making all labels mandatory and adhere
+  to minimum length restrictions, we could make the overall conventions uniform
+  across all resources and therefore easier to remember and enforce.)
+
+  **TL;DR**
+  CloudBridge has three concepts when it comes to naming and identifying
+  objects:
+
+    - `id` is a unique identifier for an object, always
+      auto-generated;
+    - `name` is a read-only, user-friendly value which is
+      suitable for display to the end-user;
+    - `label` is a user-assignable value that can be changed.
+
+  The `name` is often derived from the `label` but not
+  always. Not all resources support `labels`. Some only accept `names`, which
+  can be specified at object creation time (e.g. keypairs). Both `names` and
+  `labels` adhere to the same restrictions - a minimum length of 3 which
+  should be alphanumeric characters or dashes only. Names or labels should
+  not begin or end with a dash, or have consecutive dashes.
+   
+
+  .. _63: https://github.com/CloudVE/cloudbridge/issues/63
+  .. _131: https://github.com/CloudVE/cloudbridge/issues/131
+  .. _pets vs cattle: http://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/

+ 13 - 12
docs/topics/install.rst

@@ -1,7 +1,8 @@
 Installation
 ============
 
-**Prerequisites**: CloudBridge runs on Python 2.7 and higher. Python 3 is recommended.
+**Prerequisites**: CloudBridge runs on Python 2.7 and higher. Python 3 is
+recommended.
 
 We highly recommend installing CloudBridge in a
 `virtualenv <http://virtualenv.readthedocs.org/>`_. Creating a new virtualenv
@@ -13,20 +14,18 @@ is simple:
     virtualenv .venv
     source .venv/bin/activate
 
-Latest release
---------------
-The latest release of cloudbridge can be installed from PyPI::
+Latest stable release
+---------------------
+The latest release of CloudBridge can be installed from PyPI::
 
     pip install cloudbridge
 
 Latest unreleased dev version
 -----------------------------
-The development version of the library can be installed from the
-`Github repo <https://github.com/gvlproject/cloudbridge>`_::
+The development version of the library can be installed directly from the
+`GitHub repo <https://github.com/CloudVE/cloudbridge>`_::
 
-    $ git clone https://github.com/gvlproject/cloudbridge.git
-    $ cd cloudbridge
-    $ python setup.py install
+    $ pip install --upgrade git+https://github.com/CloudVE/cloudbridge.git
 
 Developer installation
 ----------------------
@@ -34,10 +33,12 @@ To install additional libraries required by CloudBridge contributors, such as
 `tox <https://tox.readthedocs.org/en/latest/>`_, clone the source code
 repository and run the following command from the repository root directory::
 
-    pip install -e .[dev]
-
-----------
+    $ git clone https://github.com/CloudVE/cloudbridge.git
+    $ cd cloudbridge
+    $ pip install --upgrade --editable .[dev]
 
+Checking installation
+---------------------
 To check what version of the library you have installed, do the following::
 
     import cloudbridge

+ 13 - 11
docs/topics/launch.rst

@@ -14,23 +14,24 @@ and 4 GB RAM.
 
 .. code-block:: python
 
-    img = provider.compute.images.get('ami-f4cc1de2')  # Ubuntu 16.04 on AWS
+    img = provider.compute.images.get('ami-759bc50a')  # Ubuntu 16.04 on AWS
     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.
+`Private networking`_ section, if you don't particularly care in which subnet
+the instance is launched, CloudBridge provides a convenience function to
+quickly obtain a default subnet for use. We just need to supply a zone to use.
 
 .. code-block:: python
 
-    subnet = provider.networking.subnets.get_or_create_default()
+    zone = provider.compute.regions.get(provider.region_name).zones[0]
+    subnet = provider.networking.subnets.get_or_create_default(zone)
 
 When launching an instance, you can also specify several optional arguments
-such as the firewall (a.k.a security group), a key pair, or instance user data.
+such as the firewall (aka 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
@@ -38,7 +39,7 @@ if you don't have those resources under your account, take a look at the
 
 .. code-block:: python
 
-    kp = provider.security.key_pairs.find(name='cloudbridge_intro')[0]
+    kp = provider.security.key_pairs.find(name='cloudbridge-intro')[0]
     fw = provider.security.vm_firewalls.list()[0]
 
 Launch an instance
@@ -49,7 +50,7 @@ Once we have all the desired pieces, we'll use them to launch an instance:
 
     inst = provider.compute.instances.create(
         name='cloudbridge-vpc', image=img, vm_type=vm_type,
-        subnet=subnet, key_pair=kp, vm_firewalls=[fw])
+        subnet=subnet, zone=zone, key_pair=kp, vm_firewalls=[fw])
 
 Private networking
 ~~~~~~~~~~~~~~~~~~
@@ -67,12 +68,12 @@ that subnet.
     # make sure subnet has internet access
     router = self.provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway()
     router.attach_gateway(gateway)
 
     inst = provider.compute.instances.create(
         name='cloudbridge-vpc', image=img, vm_type=vm_type,
-        subnet=sn, key_pair=kp, vm_firewalls=[fw])
+        subnet=sn, zone=zone, 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>`_.
@@ -94,7 +95,8 @@ refer to :class:`.LaunchConfig`.
     lc.add_volume_device(source=img, size=11, is_root=True)
     inst = provider.compute.instances.create(
         name='cloudbridge-bdm', image=img,  vm_type=vm_type,
-        launch_config=lc, key_pair=kp, vm_firewalls=[fw])
+        launch_config=lc, key_pair=kp, vm_firewalls=[fw],
+        subnet=subnet, zone=zone)
 
 where ``img`` is the :class:`.Image` object to use for the root volume.
 

+ 6 - 6
docs/topics/networking.rst

@@ -51,9 +51,9 @@ several common scenarios.
      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.
+   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
 ----------------------------------------------
@@ -76,7 +76,7 @@ of the block and allow up to 16 IP addresses within a subnet (``/28``).
     sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28', zone=zone)
     router = provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway(name)
+    gateway = net.gateways.get_or_create_inet_gateway()
     router.attach_gateway(gateway)
 
 
@@ -90,11 +90,11 @@ The additional step that's required here is to assign a floating IP to the VM:
         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, ...)
+    vm = provider.compute.instances.create('my-inst', subnet=sn, zone=zone, ...)
 
     router = provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway(net, name)
+    gateway = net.gateways.get_or_create_inet_gateway()
     router.attach_gateway(gateway)
 
     fip = provider.networking.floating_ips.create()

+ 110 - 0
docs/topics/os_mapping.rst

@@ -0,0 +1,110 @@
+OpenStack - Labeled Resources
+-----------------------
++------------------------+------------------------+-----------+----------------+----------+
+| Labeled Resource       | OS Resource Type       | CB ID     | CB Name        | CB Label |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackInstance      | Instance               | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackMachineImage  | Image                  | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackNetwork       | Network                | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackSubnet        | Subnet                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackRouter        | Router                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackVolume        | Volume                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackSnapshot      | Snapshot               | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackVMFirewall    | Security Group         | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. For all labeled resources, the `label` property in OpenStack
+maps to the Name attribute. However, unlike in Azure or AWS, no resource has
+an unchangeable name by which to identify it in our OpenStack implementation.
+The `name` property will therefore map to the ID, preserving its role as an unchangeable 
+identifier even though not easily readable in this context. Finally, labeled resources
+support a `label` parameter for the `find` method in their corresponding services.
+The below screenshots will help map these properties to OpenStack objects in the
+web portal.
+Additionally, although OpenStack Security Groups are not associated with a
+specific network, such an association is done in CloudBridge, due to its
+necessity in AWS. As such, the VMFirewall creation method requires a
+`network` parameter and the association is accomplished in OpenStack through
+the description, by appending the following string to the user-provided description
+(if any) at creation: "[CB-AUTO-associated-network-id: associated_net_id]"
+
+.. figure:: captures/os-instance-dash.png
+   :scale: 50 %
+   :alt: name, ID, and label properties for OS Instances
+
+   The CloudBridge `name` and `ID` properties map to the unchangeable
+   resource ID in OpenStack as resources do not allow for an unchangeable
+   name. The `label` property maps to the 'Name' for all resources in 
+   OpenStack. By default, this label will appear in the first column.
+
+
+OpenStack - Unlabeled Resources
+---------------------------
++-----------------------+------------------------+-------+---------+----------+
+| Unlabeled Resource    | OS Resource Type       | CB ID | CB Name | CB Label |
++-----------------------+------------------------+-------+---------+----------+
+| OpenStackKeyPair      | Key Pair               | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+| OpenStackBucket       | Object Store Container | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+| OpenStackBucketObject | Object                 | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in OpenStack, as these resources don't have an `ID` in the
+traditional sense and can be identified by name. Finally, unlabeled resources
+support a `name` parameter for the `find` method in their corresponding
+services.
+
+.. figure:: captures/os-kp-dash.png
+   :scale: 50 %
+   :alt: KeyPair details on OS dashboard
+
+   KeyPairs and other unlabeled resources in OpenStack have `name` that is
+   unique and unmodifiable. The `ID` will thus map to the `name` property when
+   no other `ID` exists for that OpenStack resource.
+
+
+OpenStack - Special Unlabeled Resources
+-----------------------------------
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| Unlabeled Resource       | OS Resource Type       | CB ID | CB Name                                                                | CB Label |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| OpenStackFloatingIP      | Floating IP            | ID    | [public_ip]                                                            | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| OpenStackInternetGateway | Network `public`       | ID    | 'public'                                                               | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| OpenStackVMFirewallRule  | Security Group Rule    | ID    | Generated: [direction]-[protocol]-[from_port]-[to_port]-[cidr]-[fw_id] | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed before. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP.
+Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`.
+
+In terms of the gateway in OpenStack, it maps to the network named 'public.'
+Thus, the internet gateway create method does not take a name parameter, and
+the `name` property will be 'public'.
+
+Finally, Firewall Rules in OpenStack differ from traditional unlabeled resources
+by the fact that they do not take a `name` parameter at creation, and the
+`name` property is automatically generated from the rule's properties, as
+shown above. These rules can be found within each Firewall (i.e. Security
+Group) in the web portal, and will not have any name in the OpenStack dashboard.

+ 5 - 5
docs/topics/provider_development.rst

@@ -233,8 +233,8 @@ specific manner.
 
 
 
-.. _commit 1: https://github.com/gvlproject/cloudbridge/commit/54c67e93a3cd9d51e7d2b1195ebf4e257d165297
-.. _commit 2: https://github.com/gvlproject/cloudbridge/commit/82c0244aa4229ae0aecfe40d769eb93b06470dc7
-.. _commit 3: https://github.com/gvlproject/cloudbridge/commit/e90a7f6885814a3477cd0b38398d62af64f91093
-.. _commit 4: https://github.com/gvlproject/cloudbridge/commit/2d5c14166a538d320e54eed5bc3fa04997828715
-.. _commit 5: https://github.com/gvlproject/cloudbridge/commit/98c9cf578b672867ee503027295f9d901411e496
+.. _commit 1: https://github.com/CloudVE/cloudbridge/commit/54c67e93a3cd9d51e7d2b1195ebf4e257d165297
+.. _commit 2: https://github.com/CloudVE/cloudbridge/commit/82c0244aa4229ae0aecfe40d769eb93b06470dc7
+.. _commit 3: https://github.com/CloudVE/cloudbridge/commit/e90a7f6885814a3477cd0b38398d62af64f91093
+.. _commit 4: https://github.com/CloudVE/cloudbridge/commit/2d5c14166a538d320e54eed5bc3fa04997828715
+.. _commit 5: https://github.com/CloudVE/cloudbridge/commit/98c9cf578b672867ee503027295f9d901411e496

+ 33 - 8
docs/topics/release_process.rst

@@ -1,21 +1,46 @@
 Release Process
 ~~~~~~~~~~~~~~~
 
-1. Increment version number in cloudbridge/__init__.py as per semver rules.
+1. Make sure `all tests pass <https://travis-ci.org/CloudVE/cloudbridge>`_.
 
-2. Freeze all library dependencies in setup.py. The version numbers can be a range
-   with the upper limit being the latest known working version, and the lowest being
-   the last known working version. 
+2. Increment version number in ``cloudbridge/__init__.py`` as per
+   `semver rules <https://semver.org/>`_.
 
-3. Run all tox tests.
+3. Freeze all library dependencies in ``setup.py`` and commit.
+   The version numbers can be a range with the upper limit being the latest
+   known working version, and the lowest being the last known working version.
 
-4. Add release notes to CHANGELOG.rst. Also add last commit hash to changelog.
+   In general, our strategy is to make provider sdk libraries fixed within
+   relatively known compatibility ranges, so that we reduce the chances of
+   breakage. If someone uses CloudBridge, presumably, they do not use the SDKs
+   directly. For all other libraries, especially, general purpose libraries
+   (e.g. ``six``), our strategy is to make compatibility as broad and
+   unrestricted as possible.
 
-5. Release to PyPi
+4. Add release notes to ``CHANGELOG.rst``. Also add last commit hash to
+   changelog. List of commits can be obtained using
+   ``git shortlog <last release hash>..HEAD``
+
+5. Release to PyPi.
+   (make sure you have run `pip install wheel`)
+   First, test release with PyPI staging server as described in:
+   https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
+
+   Once tested, run:
 
 .. code-block:: bash
 
+   # remove stale files or wheel might package them
+   rm -r build dist
    python setup.py sdist upload
    python setup.py bdist_wheel upload
 
-6. Tag release and make github release.
+6. Tag release and make a GitHub release.
+
+.. code-block:: bash
+
+   git tag -a v1.0.0 -m "Release 1.0.0"
+   git push --tags
+
+7. Increment version number in ``cloudbridge/__init__.py`` to ``version-dev``
+   to indicate the development cycle, commit, and push the changes.

+ 65 - 0
docs/topics/resource_types_and_mapping.rst

@@ -0,0 +1,65 @@
+Resource Types and Dashboard Mapping
+====================================
+
+Cross-Platform Concepts
+-----------------------
+
+Given CloudBridge's goal to work uniformly across cloud providers, some
+compromises were necessary in order to bridge the many differences between
+providers' resources and features. Notably, in order to create a robust and
+conceptually consistent cross-cloud library, resources were separated into
+`labeled` and `unlabeled resources,` and were given three main properties:
+`ID`, `name`, and `label`.
+The `ID` corresponds to a unique identifier that can be reliably used to
+reference a resource. Users can safely use an ID knowing that it will always
+point to the same resource. All resources have an `ID` property, thus making
+it the recommended oproperty for reliably identifying a resource.
+The `label` property, conversely, is a modifiable value that does not need
+to be unique. Unlike the name property, it is not used to identify a
+particular resource, but rather label a resource for easier distinction.
+Only labeled resources have the label property, and these resources require
+a `label` parameter be set at creation time.
+The `name` property corresponds to an unchangeable and unique designation for
+a particular resource. This property is meant to be, in some ways, a more
+human-readable identifier. Thus, when no conceptually comparable property
+exists for a given resource in a particular provider, the ID is returned
+instead, as is the case for all OpenStack and some AWS resources. Given the 
+discrepancy between providers, using the `name` property is not advisable 
+for cross-cloud usage of the library. Labeled resources will use the label
+given at creation as a prefix to the set name, when this property is separable
+from the ID as is the case in Azure and some AWS resources. Finally, unlabeled
+resources will always support a `name`, and some unlabeled resources will require
+a name parameter at creation. Below is a list of all resources classified by
+whether they support a `label` property.
+
++-------------------+---------------------+
+| Labeled Resources | Unlabeled Resources | 
++-------------------+---------------------+
+| Instance          | Key Pair            |
++-------------------+---------------------+
+| MachineImage      | Bucket              |
++-------------------+---------------------+
+| Network           | Bucket Object       |
++-------------------+---------------------+
+| Subnet            | FloatingIP          |
++-------------------+---------------------+
+| Router            | Internet Gateway    |
++-------------------+---------------------+
+| Volume            | VMFirewall Rule     |
++-------------------+---------------------+
+| Snapshot          |                     |
++-------------------+---------------------+
+| VMFirewall        |                     |
++-------------------+---------------------+
+
+
+Properties per Resource per Provider
+------------------------------------
+For each provider, we documented the mapping of CloudBridge resources and
+properties to provider objects, as well as some useful dashboard navigation.
+These sections will thus present summary tables delineating the different types of
+CloudBridge resources, as well as present some design decisions made to
+preserve consistency across providers:
+-`Detailed Azure Mappings <azure_mapping.html>`
+-`Detailed AWS Mappings <aws_mapping.html>`
+-`Detailed OpenStack Mappings <os_mapping.html>`

+ 38 - 21
docs/topics/setup.rst

@@ -8,6 +8,12 @@ be provided in one of following ways:
 2. A dictionary
 3. Configuration file
 
+Procuring access credentials
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For Azure, Create service principle credentials from the following link : 
+https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
+
+
 Providing access credentials through environment variables
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 The following environment variables must be set, depending on the provider in use.
@@ -35,12 +41,24 @@ OS_REGION_NAME       OS_AUTH_TOKEN
 
 **Azure**
 
+Note that managing resources in Azure requires a Resource Group. If a
+Resource Group is not provided as part of the configuration, cloudbridge will
+attempt to create a Resource Group using the given credentials. This
+operation will happen with the client initialization, and requires a
+"contributor" or "owner" role.
+Similarly, a Storage Account is required when managing some resources, such
+as KeyPairs and Buckets. If a Storage Account name is not provided as part
+of the configuration, cloudbridge will attempt to create the Storage Account
+when initializing the relevant services. This operation similarly requires a
+"contributor" or "owner" role.
+For more information on roles, see: https://docs.microsoft.com/en-us/azure/role-based-access-control/overview
+
 ======================  ==================
 Mandatory variables     Optional Variables
 ======================  ==================
 AZURE_SUBSCRIPTION_ID   AZURE_REGION_NAME
 AZURE_CLIENT_ID         AZURE_RESOURCE_GROUP
-AZURE_SECRET            AZURE_STORAGE_ACCOUNT_NAME
+AZURE_SECRET            AZURE_STORAGE_ACCOUNT
 AZURE_TENANT            AZURE_VM_DEFAULT_USER_NAME
                         AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
 ======================  ==================
@@ -77,9 +95,6 @@ will override environment values.
               'azure_resource_group': '<your resource group>'}
     provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
 
-For Azure, Create service principle credentials from the following link : 
-https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
-
 Some optional configuration values can only be provided through the config
 dictionary. These are listed below for each provider.
 
@@ -149,20 +164,22 @@ In addition to the provider specific configuration variables above, there are
 some general configuration environment variables that apply to CloudBridge as
 a whole
 
-======================  ==================
-Variable		            Description
-======================  ==================
-CB_DEBUG                Setting ``CB_DEBUG=True`` will cause detailed debug
-                        output to be printed for each provider (including HTTP
-                        traces).
-CB_USE_MOCK_PROVIDERS   Setting this to ``True`` will cause the CloudBridge test
-                        suite to use mock drivers when available.
-CB_TEST_PROVIDER        Set this value to a valid :class:`.ProviderList` value
-                        such as ``aws``, to limit tests to that provider only.
-CB_DEFAULT_SUBNET_NAME  Name to be used for a subnet that will be considered
-                        the 'default' by the library. This default will be used
-                        only in cases there is no subnet marked as the default by the provider.
-CB_DEFAULT_NETWORK_NAME Name to be used for a network that will be considered
-                        the 'default' by the library. This default will be used
-                        only in cases there is no network marked as the default by the provider.
-======================= ==================
+======================== ======================================================
+Variable		                            Description
+======================== ======================================================
+CB_DEBUG                 Setting ``CB_DEBUG=True`` will cause detailed debug
+                         output to be printed for each provider (including HTTP
+                         traces).
+CB_USE_MOCK_PROVIDERS    Setting this to ``True`` will cause the CloudBridge
+                         test suite to use mock drivers when available.
+CB_TEST_PROVIDER         Set this value to a valid :class:`.ProviderList` value
+                         such as ``aws``, to limit tests to that provider only.
+CB_DEFAULT_SUBNET_LABEL  Name to be used for a subnet that will be considered
+                         the 'default' by the library. This default will be
+                         used only in cases there is no subnet marked as the
+                         default by the provider.
+CB_DEFAULT_NETWORK_LABEL Name to be used for a network that will be considered
+                         the 'default' by the library. This default will be
+                         used only in cases there is no network marked as the
+                         default by the provider.
+======================== ======================================================

+ 6 - 4
docs/topics/testing.rst

@@ -48,10 +48,12 @@ against a specific infrastructure, say aws, use a command like this:
 Specific test cases
 ~~~~~~~~~~~~~~~~~~~~
 You can run a specific test case, as follows:
-``tox -- -s test.test_cloud_factory.CloudFactoryTestCase``
+``tox -- test/test_image_service.py:CloudImageServiceTestCase.test_create_and_list_imag``
 
 It can also be restricted to a particular environment as follows:
-``tox -e "py27-aws" -- -s test.test_cloud_factory.CloudFactoryTestCase``
+``tox -e "py27-aws" -- test/test_cloud_factory.py:CloudFactoryTestCase``
+
+See nosetest documentation for other parameters that can be passed in.
 
 Using unittest directly
 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -74,9 +76,9 @@ You can toggle the use of mock providers by setting an environment variable:
 ``CB_USE_MOCK_PROVIDERS`` to ``Yes`` or ``No``.
 
 
-.. _design goals: https://github.com/gvlproject/cloudbridge/
+.. _design goals: https://github.com/CloudVE/cloudbridge/
    blob/master/README.rst
 .. _tox: https://tox.readthedocs.org/en/latest/
-.. _ProviderList: https://github.com/gvlproject/cloudbridge/blob/master/
+.. _ProviderList: https://github.com/CloudVE/cloudbridge/blob/master/
    cloudbridge/cloud/factory.py#L15
 .. _moto: https://github.com/spulec/moto

+ 0 - 1
requirements.txt

@@ -1,2 +1 @@
-git+git://github.com/cloudve/moto@all_merged#egg=moto
 -e ".[dev]"

+ 11 - 4
setup.cfg

@@ -3,14 +3,21 @@ branch = True
 source = cloudbridge
 omit =
   cloudbridge/cloud/interfaces/*
+  cloudbridge/__init__.py
 
 [nosetests]
 with-coverage=1
 cover-branches=1
 cover-package=cloudbridge
 processes=2
-process-timeout=2700
-match=^[Tt]est 
-# Don't capture stdout - print immediately
-nocapture=1
+process-timeout=3000
+match=^[Tt]est
+verbosity=2
+# When exceptions occur, filter only cloudbridge logs
+logging-filter=cloudbridge
 
+[bdist_wheel]
+universal = 1
+
+[flake8]
+application_import_names = cloudbridge, test

+ 31 - 23
setup.py

@@ -1,5 +1,5 @@
 """
-Package install information
+CloudBridge provides a uniform interface to multiple IaaS cloud providers.
 """
 
 import ast
@@ -20,36 +20,43 @@ with open(os.path.join('cloudbridge', '__init__.py')) as f:
 
 REQS_BASE = [
     'bunch>=1.0.1',
-    'six>=1.10.0',
-    'retrying>=1.3.3',
+    'six>=1.11',
+    'tenacity>=4.12.0,<=5.0',
+    'cachetools>=2.1.0',
+    'deprecated>=1.2.3',
     'oslo.i18n>=3.15.3'
 ]
-REQS_AWS = ['boto3']
-REQS_AZURE = ['msrest>=0.4.7',
-              'msrestazure>=0.4.7',
-              'azure-common>=1.1.5',
-              'azure-mgmt-resource>=1.0.0rc1',
-              'azure-mgmt-compute>=1.0.0rc1',
-              'azure-mgmt-network>=1.0.0rc1',
-              'azure-mgmt-storage>=1.0.0rc1',
-              'azure-storage>=0.34.0',
-              'pysftp>=0.2.9']
+REQS_AWS = ['boto3<1.8.0']
+# Install azure>=3.0.0 package to find which of the azure libraries listed
+# below are compatible with each other. List individual libraries instead
+# of using the azure umbrella package to speed up installation.
+REQS_AZURE = ['msrest>=0.5.4,<0.6',
+              'msrestazure==0.5.0',
+              'azure-common==1.1.14',
+              'azure-mgmt-devtestlabs==2.2.0',
+              'azure-mgmt-resource==2.0.0',
+              'azure-mgmt-compute==4.0.1',
+              'azure-mgmt-network>=2.0.1,<=2.1',
+              'azure-mgmt-storage==2.0.0',
+              'azure-storage-blob==1.3.1',
+              'azure-cosmosdb-table==1.0.4',
+              'pysftp==0.2.9']
 REQS_GCP = ['google-api-python-client', 'oauth2client']
 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'
+    'openstacksdk>=0.12.0,<=0.17',
+    'python-novaclient>=7.0.0,<=11.0',
+    'python-glanceclient>=2.5.0,<=2.12',
+    'python-cinderclient>=1.9.0,<=4.0',
+    'python-swiftclient>=3.2.0,<=3.6',
+    'python-neutronclient>=6.0.0,<=6.9',
+    'python-keystoneclient>=3.13.0,<=3.17'
 ]
 REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_GCP + REQS_OPENSTACK
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
 REQS_DEV = ([
     'tox>=2.1.1',
     'nose',
-    # 'moto>=1.1.11',  # until https://github.com/spulec/moto/issues/1396
+    'moto>=1.3.2',
     'docutils>=0.14',
     'imagesize>=0.7.1',
     'jinja2>=2.9.6',
@@ -63,13 +70,14 @@ setup(
     name='cloudbridge',
     version=version,
     description='A simple layer of abstraction over multiple cloud providers.',
+    long_description=__doc__,
     author='Galaxy and GVL Projects',
     author_email='help@genome.edu.au',
     url='http://cloudbridge.cloudve.org/',
+    setup_requires=['nose>=1.0'],
     install_requires=REQS_FULL,
     extras_require={
-        ':python_version=="2.7"': ['py2-ipaddress'],
-        ':python_version=="3"': ['py2-ipaddress'],
+        ':python_version<"3.3"': ['ipaddress'],
         'full': REQS_FULL,
         'dev': REQS_DEV
     },

+ 33 - 41
test/helpers/__init__.py

@@ -6,12 +6,13 @@ import unittest
 import uuid
 from contextlib import contextmanager
 
+import six
+
+from cloudbridge.cloud.base.helpers import get_env
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
-from six import reraise
-
 
 def parse_bool(val):
     if val:
@@ -47,7 +48,7 @@ def cleanup_action(cleanup_func):
         except Exception as e:
             print("Error during exception cleanup: {0}".format(e))
             traceback.print_exc()
-        reraise(ex_class, ex_val, ex_traceback)
+        six.reraise(ex_class, ex_val, ex_traceback)
     try:
         cleanup_func()
     except Exception as e:
@@ -80,15 +81,15 @@ def skipIfNoService(services):
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
         # Match the ami value with entry in custom_amis.json for use with moto
-        "image": os.environ.get('CB_IMAGE_AWS', 'ami-aa2ea6d0'),
-        "vm_type": os.environ.get('CB_VM_TYPE_AWS', 't2.nano'),
-        "placement": os.environ.get('CB_PLACEMENT_AWS', 'us-east-1a'),
+        "image": get_env('CB_IMAGE_AWS', 'ami-aa2ea6d0'),
+        "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
+        "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
     'OpenStackCloudProvider': {
         'image': os.environ.get('CB_IMAGE_OS',
-                                '842b949c-ea76-48df-998d-8a41f2626243'),
+                                'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
         "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
-        "placement": os.environ.get('CB_PLACEMENT_OS', 'zone-r1'),
+        "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
     },
     'GCECloudProvider': {
         'image': ('https://www.googleapis.com/compute/v1/'
@@ -99,14 +100,12 @@ TEST_DATA_CONFIG = {
     },
     "AzureCloudProvider": {
         "placement":
-            os.environ.get('CB_PLACEMENT_AZURE', 'eastus'),
+            get_env('CB_PLACEMENT_AZURE', 'eastus'),
         "image":
-            os.environ.get('CB_IMAGE_AZURE',
-                           '/subscriptions/7904d702-e01c-4826-8519-f5a25c866a9'
-                           '6/resourceGroups/cloudbridge/providers/Microsoft.C'
-                           'ompute/images/cb-test-image'),
+            get_env('CB_IMAGE_AZURE',
+                    'Canonical:UbuntuServer:16.04.0-LTS:latest'),
         "vm_type":
-            os.environ.get('CB_VM_TYPE_AZURE', 'Basic_A2'),
+            get_env('CB_VM_TYPE_AZURE', 'Basic_A2'),
     }
 }
 
@@ -123,16 +122,12 @@ def get_provider_test_data(provider, key):
     return None
 
 
-def create_test_network(provider, name):
+def get_or_create_default_subnet(provider):
     """
-    Create a network with one subnet, returning the network and subnet objects.
+    Return the default subnet to be used for tests
     """
-    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'))
-    return net, sn
+    return provider.networking.subnets.get_or_create_default(
+        zone=get_provider_test_data(provider, 'placement'))
 
 
 def delete_test_network(network):
@@ -145,34 +140,31 @@ def delete_test_network(network):
                 pass
 
 
-def get_test_gateway(provider, name):
+def get_test_gateway(provider):
     """
     Get an internet gateway for testing.
 
     This includes creating a network for the gateway, which is also returned.
     """
-    net_name = 'cb-testgwnet-{0}'.format(get_uuid())
-    net = provider.networking.networks.create(
-        name=net_name, cidr_block='10.0.0.0/16')
-    return net, net.gateways.get_or_create_inet_gateway(name)
+    sn = get_or_create_default_subnet(provider)
+    net = sn.network
+    return net.gateways.get_or_create_inet_gateway()
 
 
-def delete_test_gateway(network, gateway):
+def delete_test_gateway(gateway):
     """
     Delete the supplied network and gateway.
     """
-    with cleanup_action(lambda: network.delete()):
-        with cleanup_action(lambda: gateway.delete()):
-            pass
+    with cleanup_action(lambda: gateway.delete()):
+        pass
 
 
 def create_test_instance(
-        provider, instance_name, subnet, launch_config=None,
+        provider, instance_label, subnet, launch_config=None,
         key_pair=None, vm_firewalls=None, user_data=None):
 
     instance = provider.compute.instances.create(
-        instance_name,
-        get_provider_test_data(provider, 'image'),
+        instance_label, get_provider_test_data(provider, 'image'),
         get_provider_test_data(provider, 'vm_type'),
         subnet=subnet,
         zone=get_provider_test_data(provider, 'placement'),
@@ -184,12 +176,12 @@ def create_test_instance(
     return instance
 
 
-def get_test_instance(provider, name, key_pair=None, vm_firewalls=None,
+def get_test_instance(provider, label, key_pair=None, vm_firewalls=None,
                       subnet=None, user_data=None):
     launch_config = None
     instance = create_test_instance(
         provider,
-        name,
+        label,
         subnet=subnet,
         key_pair=key_pair,
         vm_firewalls=vm_firewalls,
@@ -210,8 +202,8 @@ def delete_test_instance(instance):
                           terminal_states=[InstanceState.ERROR])
 
 
-def cleanup_test_resources(instance=None, network=None, vm_firewall=None,
-                           key_pair=None):
+def cleanup_test_resources(instance=None, vm_firewall=None,
+                           key_pair=None, network=None):
     """Clean up any combination of supplied resources."""
     with cleanup_action(lambda: delete_test_network(network)
                         if network else None):
@@ -222,7 +214,7 @@ def cleanup_test_resources(instance=None, network=None, vm_firewall=None,
 
 
 def get_uuid():
-    return str(uuid.uuid4())
+    return str(uuid.uuid4())[:6]
 
 
 class ProviderTestBase(unittest.TestCase):
@@ -245,7 +237,7 @@ class ProviderTestBase(unittest.TestCase):
             return 1
 
     def create_provider_instance(self):
-        provider_name = os.environ.get("CB_TEST_PROVIDER", "aws")
+        provider_name = get_env("CB_TEST_PROVIDER", "aws")
         use_mock_drivers = parse_bool(
             os.environ.get("CB_USE_MOCK_PROVIDERS", "True"))
         factory = CloudProviderFactory()
@@ -253,7 +245,7 @@ class ProviderTestBase(unittest.TestCase):
                                                     get_mock=use_mock_drivers)
         config = {'default_wait_interval':
                   self.get_provider_wait_interval(provider_class),
-                  'default_result_limit': 1}
+                  'default_result_limit': 5}
         return provider_class(config)
 
     @property

+ 115 - 38
test/helpers/standard_interface_tests.py

@@ -5,13 +5,18 @@ This includes:
    2. Checking for object equality and repr
    3. Checking standard behaviour for list, iter, find, get, delete
 """
-import test.helpers as helpers
 import uuid
 
+import tenacity
+
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidNameException
+from cloudbridge.cloud.interfaces.resources import LabeledCloudResource
 from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
 from cloudbridge.cloud.interfaces.resources import ResultList
+from cloudbridge.cloud.providers.aws.services import AWSImageService
+
+import test.helpers as helpers
 
 
 def check_repr(test, obj):
@@ -26,14 +31,21 @@ def check_json(test, obj):
     val = obj.to_json()
     test.assertEqual(val.get('id'), obj.id)
     test.assertEqual(val.get('name'), obj.name)
+    if isinstance(obj, LabeledCloudResource):
+        test.assertEqual(val.get('label'), obj.label)
 
 
 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)
+    check_obj_label(test, obj)
 
 
+@tenacity.retry(stop=tenacity.stop_after_attempt(10),
+                retry=tenacity.retry_if_exception_type(AssertionError),
+                wait=tenacity.wait_fixed(10),
+                reraise=True)
 def check_list(test, service, obj):
     list_objs = service.list()
     test.assertIsInstance(list_objs, ResultList)
@@ -68,18 +80,31 @@ def check_iter(test, service, obj):
 
 def check_find(test, service, obj):
     # check find
-    find_objs = service.find(name=obj.name)
+    if isinstance(obj, LabeledCloudResource):
+        find_objs = service.find(label=obj.label)
+    else:
+        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))
+        % (type(obj).__name__, getattr(obj, 'label', obj.name), find_objs))
     test.assertEqual(find_objs[0].id, obj.id)
     return find_objs
 
 
-def check_find_non_existent(test, service):
-    # check find
-    find_objs = service.find(name="random_imagined_obj_name")
+def check_find_non_existent(test, service, obj):
+    args = {}
+    # AWSImageService.find looks through all public images by default
+    # In order to get tests to run faster, looking for these non existent
+    # values only in images owned by the current user
+    if isinstance(service, AWSImageService):
+        args = {'owners': ['self']}
+    if isinstance(obj, LabeledCloudResource):
+        find_objs = service.find(label="random_imagined_obj_name", **args)
+    else:
+        find_objs = service.find(name="random_imagined_obj_name")
+    with test.assertRaises(TypeError):
+        service.find(notaparameter="random_imagined_obj_name")
     test.assertTrue(
         len(find_objs) == 0,
         "Find non-existent object for %s returned unexpected objects: %s"
@@ -102,6 +127,10 @@ def check_get_non_existent(test, service):
         % (type(service).__name__, get_objs))
 
 
+@tenacity.retry(stop=tenacity.stop_after_attempt(10),
+                retry=tenacity.retry_if_exception_type(AssertionError),
+                wait=tenacity.wait_fixed(10),
+                reraise=True)
 def check_delete(test, service, obj, perform_delete=False):
     if perform_delete:
         obj.delete()
@@ -115,6 +144,12 @@ def check_delete(test, service, obj, perform_delete=False):
 
 
 def check_obj_name(test, obj):
+    name_property = getattr(type(obj), 'name', None)
+    test.assertIsInstance(name_property, property)
+    test.assertIsNone(name_property.fset, "Name should not have a setter")
+
+
+def check_obj_label(test, obj):
     """
     Cloudbridge identifiers must be 1-63 characters long, and comply with
     RFC1035. In addition, identifiers should contain only lowercase letters,
@@ -122,32 +157,44 @@ def check_obj_name(test, obj):
     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
+    # if label property exists, make sure invalid values cannot be set
+    label_property = getattr(type(obj), 'label', None)
+    if isinstance(label_property, property):
+        test.assertIsInstance(obj, LabeledCloudResource)
+        original_label = obj.label
+        # Three character labels should be allowed
+        obj.label = "abc"
+        VALID_LABEL = u"hello-world-123"
+        obj.label = VALID_LABEL
+        # Two character labels should not be allowed
+        with test.assertRaises(InvalidNameException):
+            obj.label = "ab"
+        # A none value should not be allowed
+        with test.assertRaises(InvalidNameException):
+            obj.label = None
         # setting spaces should raise an exception
         with test.assertRaises(InvalidNameException):
-            obj.name = "hello world"
+            obj.label = "hello world"
         # setting upper case characters should raise an exception
         with test.assertRaises(InvalidNameException):
-            obj.name = "helloWorld"
+            obj.label = "helloWorld"
         # setting special characters should raise an exception
         with test.assertRaises(InvalidNameException):
-            obj.name = "hello.world:how_goes_it"
+            obj.label = "hello.world:how_goes_it"
+        # Starting with a dash should raise an exception
+        with test.assertRaises(InvalidNameException):
+            obj.label = "-hello"
+        # Ending with a dash should raise an exception
+        with test.assertRaises(InvalidNameException):
+            obj.label = "hello-"
         # 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
+                               msg="Label of length > 64 is not allowed"):
+            obj.label = "a" * 64
+        # refreshing should yield the last successfully set label
         obj.refresh()
-        # GCE currently does not support renaming after a resource is created.
-        # test.assertEqual(obj.name, VALID_NAME)
-        obj.name = original_name
+        test.assertEqual(obj.label, VALID_LABEL)
+        obj.label = original_label
 
 
 def check_standard_behaviour(test, service, obj):
@@ -161,7 +208,7 @@ def check_standard_behaviour(test, service, 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)
+    check_find_non_existent(test, service, obj)
     obj_get = check_get(test, service, obj)
     check_get_non_existent(test, service)
 
@@ -181,27 +228,56 @@ def check_standard_behaviour(test, service, obj):
                                            objs_find[0].id, obj_get.id,
                                            obj.id))
 
+    if isinstance(obj, LabeledCloudResource):
+        test.assertTrue(
+            obj.label == objs_list[0].label == objs_iter[0].label ==
+            objs_find[0].label == obj_get.label,
+            "Labels 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
+    # check create with invalid label
     with test.assertRaises(InvalidNameException):
         # spaces should raise an exception
         create_func("hello world")
-    # check create with invalid name
+    # check create with invalid label
     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")
+    # Starting with a dash should raise an exception
+    with test.assertRaises(InvalidNameException):
+        create_func("-hello")
+    # Ending with a dash should raise an exception
+    with test.assertRaises(InvalidNameException):
+        create_func("hello-")
+    # underscores are not allowed
+    with test.assertRaises(InvalidNameException):
+        create_func("hello_bucket")
     # setting a length > 63 should result in an exception
     with test.assertRaises(InvalidNameException,
-                           msg="Name of length > 64 should be disallowed"):
+                           msg="Label of length > 63 should be disallowed"):
         create_func("a" * 64)
+    #  name cannot be an IP address
+    with test.assertRaises(InvalidNameException):
+        create_func("197.10.100.42")
+
+    # empty name are not allowed
+    with test.assertRaises(InvalidNameException):
+        create_func(None)
+    # names of length less than 3 should raise an exception
+    with test.assertRaises(InvalidNameException):
+        create_func("cb")
 
 
-def check_crud(test, service, iface, name_prefix,
+def check_crud(test, service, iface, label_prefix,
                create_func, cleanup_func, extra_test_func=None,
                custom_check_delete=None, skip_name_check=False):
     """
@@ -222,14 +298,14 @@ def check_crud(test, service, iface, name_prefix,
     :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  label_prefix: ``str``
+    :param label_prefix: The label to prefix all created objects with. This
+                        function will generated a new label with the
+                        specified label_prefix for each test object created
+                        and pass that label into the create_func
 
     :type  create_func: ``func``
-    :param create_func: The create_func must accept the name of the object to
+    :param create_func: The create_func must accept the label of the object to
                         create as a parameter and return the constructed
                         object.
 
@@ -250,17 +326,18 @@ def check_crud(test, service, iface, name_prefix,
                                 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
+    :param skip_name_check:  If True, the name related checking will be
                              skipped.
+
     """
 
     obj = None
     with helpers.cleanup_action(lambda: cleanup_func(obj)):
+        label = "{0}-{1}".format(label_prefix, helpers.get_uuid())
         if not skip_name_check:
-            check_create(test, service, iface, name_prefix,
+            check_create(test, service, iface, label_prefix,
                          create_func, cleanup_func)
-        name = "{0}-{1}".format(name_prefix, helpers.get_uuid())
-        obj = create_func(name)
+        obj = create_func(label)
         if issubclass(iface, ObjectLifeCycleMixin):
             obj.wait_till_ready()
         check_standard_behaviour(test, service, obj)

+ 61 - 56
test/test_block_store_service.py

@@ -1,8 +1,6 @@
 import time
-import uuid
-from test import helpers
-from test.helpers import ProviderTestBase
-from test.helpers import standard_interface_tests as sit
+
+import six
 
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import SnapshotState
@@ -12,7 +10,9 @@ from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Volume
 
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 
 class CloudBlockStoreServiceTestCase(ProviderTestBase):
@@ -25,16 +25,22 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         Create a new volume, check whether the expected values are set,
         and delete it
         """
-        def create_vol(name):
+        def create_vol(label):
             return self.provider.storage.volumes.create(
-                name,
-                1,
+                label, 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])
+            if vol:
+                vol.delete()
+                vol.wait_for([VolumeState.DELETED, VolumeState.UNKNOWN],
+                             terminal_states=[VolumeState.ERROR])
+                vol.refresh()
+                self.assertTrue(
+                    vol.state == VolumeState.UNKNOWN,
+                    "Volume.state must be unknown when refreshing after a "
+                    "delete but got %s"
+                    % vol.state)
 
         sit.check_crud(self, self.provider.storage.volumes, Volume,
                        "cb-createvol", create_vol, cleanup_vol)
@@ -44,20 +50,19 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         """
         Create a new volume, and attempt to attach it to an instance
         """
-        name = "cb-attachvol-{0}".format(helpers.get_uuid())
+        label = "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
         test_instance = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_instance, net)):
-            net, subnet = helpers.create_test_network(
-                self.provider, name)
+                test_instance)):
+            subnet = helpers.get_or_create_default_subnet(
+                self.provider)
             test_instance = helpers.get_test_instance(
-                self.provider, name, subnet=subnet)
+                self.provider, label, subnet=subnet)
 
             test_vol = self.provider.storage.volumes.create(
-                name, 1, test_instance.zone_id)
+                label, 1, test_instance.zone_id)
             with helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
                 test_vol.attach(test_instance, '/dev/sda2')
@@ -74,21 +79,20 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         """
         Test volume properties
         """
-        name = "cb-volprops-{0}".format(helpers.get_uuid())
+        label = "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
         test_instance = None
-        net = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_instance, net)):
-            net, subnet = helpers.create_test_network(
-                self.provider, name)
+                test_instance)):
+            subnet = helpers.get_or_create_default_subnet(
+                self.provider)
             test_instance = helpers.get_test_instance(
-                self.provider, name, subnet=subnet)
+                self.provider, label, subnet=subnet)
 
             test_vol = self.provider.storage.volumes.create(
-                name, 1, test_instance.zone_id, description=vol_desc)
+                label, 1, test_instance.zone_id, description=vol_desc)
             with helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
                 self.assertTrue(
@@ -120,12 +124,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                 test_vol.detach()
-                test_vol.name = 'newvolname1'
+                test_vol.label = 'newvolname1'
                 test_vol.wait_for(
                     [VolumeState.AVAILABLE],
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
-                if self.provider.PROVIDER_ID != 'gce':
-                    self.assertEqual(test_vol.name, 'newvolname1')
+                self.assertEqual(test_vol.label, 'newvolname1')
                 self.assertEqual(test_vol.description, vol_desc)
                 self.assertIsNone(test_vol.attachments)
                 test_vol.wait_for(
@@ -139,31 +142,36 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         whether list_snapshots properly detects the new snapshot.
         Delete everything afterwards.
         """
-        name = "cb-crudsnap-{0}".format(helpers.get_uuid())
+        label = "cb-crudsnap-{0}".format(helpers.get_uuid())
         test_vol = self.provider.storage.volumes.create(
-            name,
-            1,
+            label, 1,
             helpers.get_provider_test_data(self.provider, "placement"))
         with helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
 
-            def create_snap(name):
-                return test_vol.create_snapshot(name=name,
-                                                description=name)
+            def create_snap(label):
+                return test_vol.create_snapshot(label=label,
+                                                description=label)
 
             def cleanup_snap(snap):
-                snap.delete()
-                snap.wait_for(
-                    [SnapshotState.UNKNOWN],
-                    terminal_states=[SnapshotState.ERROR])
+                if snap:
+                    snap.delete()
+                    snap.wait_for([SnapshotState.UNKNOWN],
+                                  terminal_states=[SnapshotState.ERROR])
+                    snap.refresh()
+                    self.assertTrue(
+                        snap.state == SnapshotState.UNKNOWN,
+                        "Snapshot.state must be unknown when refreshing after "
+                        "a delete but got %s"
+                        % snap.state)
 
             sit.check_crud(self, self.provider.storage.snapshots, Snapshot,
                            "cb-snap", create_snap, cleanup_snap)
 
             # Test creation of a snap via SnapshotService
-            def create_snap2(name):
+            def create_snap2(label):
                 return self.provider.storage.snapshots.create(
-                    name=name, volume=test_vol, description=name)
+                    label=label, volume=test_vol, description=label)
 
             if (self.provider.PROVIDER_ID == ProviderList.AWS and
                     not isinstance(self.provider, TestMockHelperMixin)):
@@ -176,22 +184,21 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         """
         Test snapshot properties
         """
-        name = "cb-snapprop-{0}".format(uuid.uuid4())
+        label = "cb-snapprop-{0}".format(helpers.get_uuid())
         test_vol = self.provider.storage.volumes.create(
-            name,
-            1,
+            label, 1,
             helpers.get_provider_test_data(self.provider, "placement"))
         with helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
-            snap_name = "cb-snap-{0}".format(name)
-            test_snap = test_vol.create_snapshot(name=snap_name,
-                                                 description=snap_name)
+            snap_label = "cb-snap-{0}".format(label)
+            test_snap = test_vol.create_snapshot(label=snap_label,
+                                                 description=snap_label)
 
             def cleanup_snap(snap):
-                snap.delete()
-                snap.wait_for(
-                    [SnapshotState.UNKNOWN],
-                    terminal_states=[SnapshotState.ERROR])
+                if 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()
@@ -207,18 +214,16 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     % test_vol.description)
                 self.assertEqual(test_vol.id, test_snap.volume_id)
                 self.assertIsNotNone(test_vol.create_time)
-                test_snap.name = 'snapnewname1'
+                test_snap.label = 'snapnewname1'
                 test_snap.description = 'snapnewdescription1'
                 test_snap.refresh()
-                if self.provider.PROVIDER_ID != 'gce':
-                    self.assertEqual(test_snap.name, 'snapnewname1')
+                self.assertEqual(test_snap.label, 'snapnewname1')
                 self.assertEqual(test_snap.description, 'snapnewdescription1')
 
                 # Test volume creation from a snapshot (via VolumeService)
-                sv_name = "cb-snapvol-{0}".format('snapnewname1')
+                sv_label = "cb-snapvol-{0}".format(test_snap.name)
                 snap_vol = self.provider.storage.volumes.create(
-                    sv_name,
-                    1,
+                    sv_label, 1,
                     helpers.get_provider_test_data(self.provider, "placement"),
                     snapshot=test_snap)
                 with helpers.cleanup_action(lambda: snap_vol.delete()):

+ 2 - 1
test/test_cloud_factory.py

@@ -1,5 +1,4 @@
 import unittest
-from test import helpers
 
 from cloudbridge.cloud import factory
 from cloudbridge.cloud import interfaces
@@ -9,6 +8,8 @@ from cloudbridge.cloud.interfaces.provider import CloudProvider
 from cloudbridge.cloud.providers.aws import AWSCloudProvider
 from cloudbridge.cloud.providers.aws.provider import MockAWSCloudProvider
 
+from test import helpers
+
 
 class CloudFactoryTestCase(unittest.TestCase):
 

+ 29 - 1
test/test_cloud_helpers.py

@@ -1,9 +1,13 @@
 import itertools
-from test.helpers import ProviderTestBase
 
+import six
+
+from cloudbridge.cloud.base.helpers import get_env
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 
+from test.helpers import ProviderTestBase
+
 
 class DummyResult(object):
 
@@ -74,3 +78,27 @@ class CloudHelpersTestCase(ProviderTestBase):
                         " lists should return True for server paging.")
         with self.assertRaises(NotImplementedError):
             results.data
+
+    def test_type_validation(self):
+        """
+        Make sure internal type checking implementation properly sets types.
+        """
+        self.provider.config['text_type_check'] = 'test-text'
+        config_value = self.provider._get_config_value('text_type_check', None)
+        self.assertIsInstance(config_value, six.string_types)
+
+        env_value = self.provider._get_config_value(
+            'some_config_value', get_env('MOTO_AMIS_PATH'))
+        self.assertIsInstance(env_value, six.string_types)
+
+        none_value = self.provider._get_config_value(
+            'some_config_value', get_env('MISSING_ENV', None))
+        self.assertIsNone(none_value)
+
+        bool_value = self.provider._get_config_value(
+            'some_config_value', get_env('MISSING_ENV', True))
+        self.assertIsInstance(bool_value, bool)
+
+        int_value = self.provider._get_config_value(
+            'default_result_limit', None)
+        self.assertIsInstance(int_value, int)

+ 76 - 67
test/test_compute_service.py

@@ -1,7 +1,6 @@
 import ipaddress
-from test import helpers
-from test.helpers import ProviderTestBase
-from test.helpers import standard_interface_tests as sit
+
+import six
 
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import InstanceState
@@ -11,7 +10,9 @@ from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import VMType
 
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 
 class CloudComputeServiceTestCase(ProviderTestBase):
@@ -20,21 +21,27 @@ class CloudComputeServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
     def test_crud_instance(self):
-        name = "cb-instcrud-{0}".format(helpers.get_uuid())
+        label = "cb-instcrud-{0}".format(helpers.get_uuid())
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
-        net = None
         subnet = None
 
-        def create_inst(name):
+        def create_inst(label):
             # 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,
+            return helpers.get_test_instance(self.provider, label,
                                              subnet=subnet, user_data={})
 
         def cleanup_inst(inst):
-            inst.delete()
-            inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN])
+            if inst:
+                inst.delete()
+                inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN])
+                inst.refresh()
+                self.assertTrue(
+                    inst.state == InstanceState.UNKNOWN,
+                    "Instance.state must be unknown when refreshing after a "
+                    "delete but got %s"
+                    % inst.state)
 
         def check_deleted(inst):
             deleted_inst = self.provider.compute.instances.get(
@@ -44,15 +51,13 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     InstanceState.DELETED,
                     InstanceState.UNKNOWN),
                 "Instance %s should have been deleted but still exists." %
-                name)
+                label)
 
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                                               network=net)):
-            net, subnet = helpers.create_test_network(self.provider, name)
+        subnet = helpers.get_or_create_default_subnet(self.provider)
 
-            sit.check_crud(self, self.provider.compute.instances, Instance,
-                           "cb-instcrud", create_inst, cleanup_inst,
-                           custom_check_delete=check_deleted)
+        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:
@@ -65,28 +70,28 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                               'security.vm_firewalls',
                               'security.key_pairs'])
     def test_instance_properties(self):
-        name = "cb-inst-props-{0}".format(helpers.get_uuid())
+        label = "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
         fw = None
         kp = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_instance, net, fw, kp)):
-            net, subnet = helpers.create_test_network(self.provider, name)
-            kp = self.provider.security.key_pairs.create(name=name)
+                test_instance, fw, kp)):
+            subnet = helpers.get_or_create_default_subnet(self.provider)
+            net = subnet.network
+            kp = self.provider.security.key_pairs.create(name=label)
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
             test_instance = helpers.get_test_instance(self.provider,
-                                                      name, key_pair=kp,
+                                                      label, key_pair=kp,
                                                       vm_firewalls=[fw],
                                                       subnet=subnet)
             self.assertEqual(
-                test_instance.name, name,
-                "Instance name {0} is not equal to the expected name"
-                " {1}".format(test_instance.name, name))
+                test_instance.label, label,
+                "Instance label {0} is not equal to the expected label"
+                " {1}".format(test_instance.label, label))
             image_id = helpers.get_provider_test_data(self.provider, "image")
             self.assertEqual(test_instance.image_id, image_id,
                              "Image id {0} is not equal to the expected id"
@@ -105,8 +110,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             self.assertTrue(test_instance.private_ips[0], "private ip should"
                             " contain a valid value")
             self.assertEqual(
-                test_instance.key_pair_name,
-                kp.name)
+                test_instance.key_pair_id,
+                kp.id)
             self.assertIsInstance(test_instance.vm_firewalls, list)
             self.assertEqual(
                 test_instance.vm_firewalls[0],
@@ -215,29 +220,28 @@ class CloudComputeServiceTestCase(ProviderTestBase):
     @helpers.skipIfNoService(['compute.instances', 'compute.images',
                               'compute.vm_types', 'storage.volumes'])
     def test_block_device_mapping_attachments(self):
-        name = "cb-blkattch-{0}".format(helpers.get_uuid())
+        label = "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,
+           label, 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)
+            test_snap = test_vol.create_snapshot(label=label,
+                                                 description=label)
 
             def cleanup_snap(snap):
-                snap.delete()
-                snap.wait_for([SnapshotState.UNKNOWN],
-                              terminal_states=[SnapshotState.ERROR])
+                if snap:
+                    snap.delete()
+                    snap.wait_for([SnapshotState.UNKNOWN],
+                                  terminal_states=[SnapshotState.ERROR])
 
-            with helpers.cleanup_action(lambda:
-                                        cleanup_snap(test_snap)):
+            with helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
                 test_snap.wait_till_ready()
 
                 lc = self.provider.compute.instances.create_launch_config()
@@ -280,44 +284,48 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 for _ in range(vm_type.num_ephemeral_disks - 4):
                     lc.add_ephemeral_device()
 
-                net, subnet = helpers.create_test_network(self.provider, name)
+                subnet = helpers.get_or_create_default_subnet(
+                    self.provider)
 
-                with helpers.cleanup_action(lambda:
-                                            helpers.delete_test_network(net)):
-                    inst = helpers.create_test_instance(
-                        self.provider,
-                        name,
-                        subnet=subnet,
-                        launch_config=lc)
-
-                    with helpers.cleanup_action(lambda:
-                                                helpers.delete_test_instance(
-                                                    inst)):
-                        try:
-                            inst.wait_till_ready()
-                        except WaitStateException as e:
-                            self.fail("The block device mapped launch did not "
-                                      " complete successfully: %s" % e)
-                        # TODO: Check instance attachments and make sure they
-                        # correspond to requested mappings
+                inst = helpers.create_test_instance(
+                    self.provider,
+                    label,
+                    subnet=subnet,
+                    launch_config=lc)
 
+                with helpers.cleanup_action(lambda:
+                                            helpers.delete_test_instance(
+                                                inst)):
+                    try:
+                        inst.wait_till_ready()
+                    except WaitStateException as e:
+                        self.fail("The block device mapped launch did not "
+                                  " complete successfully: %s" % e)
+                    # TODO: Check instance attachments and make sure they
+                    # correspond to requested mappings
     @helpers.skipIfNoService(['compute.instances', 'networking.networks',
                               'security.vm_firewalls'])
     def test_instance_methods(self):
-        name = "cb-instmethods-{0}".format(helpers.get_uuid())
+        label = "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
+        test_inst = None
         fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_inst, net, fw)):
-            net, subnet = helpers.create_test_network(self.provider, name)
-            test_inst = helpers.get_test_instance(self.provider, name,
+                instance=test_inst, vm_firewall=fw, network=net)):
+            net = self.provider.networking.networks.create(
+                label=label, cidr_block='10.0.0.0/16')
+            cidr = '10.0.1.0/24'
+            subnet = net.create_subnet(label=label, cidr_block=cidr,
+                                       zone=helpers.get_provider_test_data(
+                                                    self.provider,
+                                                    'placement'))
+            test_inst = helpers.get_test_instance(self.provider, label,
                                                   subnet=subnet)
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
 
             # Check adding a VM firewall to a running instance
             test_inst.add_vm_firewall(fw)
@@ -336,8 +344,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 (fw, test_inst.vm_firewalls))
 
             # check floating ips
-            router = self.provider.networking.routers.create(name, net)
-            gateway = None
+            router = self.provider.networking.routers.create(label, net)
+            gateway = net.gateways.get_or_create_inet_gateway()
 
             def cleanup_router(router, gateway):
                 with helpers.cleanup_action(lambda: router.delete()):
@@ -348,7 +356,6 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             with helpers.cleanup_action(lambda: cleanup_router(router,
                                                                gateway)):
                 router.attach_subnet(subnet)
-                gateway = net.gateways.get_or_create_inet_gateway(name)
                 router.attach_gateway(gateway)
                 fip = None
 
@@ -371,6 +378,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                             fip.in_use,
                             "Attached floating IP address should be in use.")
                     test_inst.refresh()
+                    test_inst.reboot()
+                    test_inst.wait_till_ready()
                     self.assertNotIn(
                         fip.public_ip,
                         test_inst.public_ips + test_inst.private_ips)

+ 56 - 16
test/test_image_service.py

@@ -1,10 +1,10 @@
+from cloudbridge.cloud.interfaces import MachineImageState
+from cloudbridge.cloud.interfaces.resources import Instance, MachineImage
+
 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.resources import MachineImage
-
 
 class CloudImageServiceTestCase(ProviderTestBase):
 
@@ -16,36 +16,76 @@ class CloudImageServiceTestCase(ProviderTestBase):
         """
         Create a new image and check whether that image can be listed.
         This covers waiting till the image is ready, checking that the image
-        name is the expected one and whether list_images is functional.
+        label is the expected one and whether list_images is functional.
         """
-        instance_name = "cb-crudimage-{0}".format(helpers.get_uuid())
+        instance_label = "cb-crudimage-{0}".format(helpers.get_uuid())
+        img_inst_label = "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
+        subnet = None
 
-        def create_img(name):
-            return test_instance.create_image(name)
+        def create_img(label):
+            return test_instance.create_image(label=label)
 
         def cleanup_img(img):
-            img.delete()
-            img.wait_for(
-                [MachineImageState.UNKNOWN, MachineImageState.ERROR])
+            if img:
+                img.delete()
+                img.wait_for(
+                    [MachineImageState.UNKNOWN, MachineImageState.ERROR])
+                img.refresh()
+                self.assertTrue(
+                    img.state == MachineImageState.UNKNOWN,
+                    "MachineImage.state must be unknown when refreshing after "
+                    "a delete but got %s"
+                    % img.state)
 
         def extra_tests(img):
             # check image size
             img.refresh()
             self.assertGreater(img.min_disk, 0, "Minimum disk"
                                " size required by image is invalid")
+            create_instance_from_image(img)
+
+        def create_instance_from_image(img):
+            img_instance = None
+            with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                    img_instance)):
+                img_instance = self.provider.compute.instances.create(
+                    img_inst_label, img,
+                    helpers.get_provider_test_data(self.provider, 'vm_type'),
+                    subnet=subnet,
+                    zone=helpers.get_provider_test_data(
+                        self.provider, 'placement'))
+                img_instance.wait_till_ready()
+                self.assertIsInstance(img_instance, Instance)
+                self.assertEqual(
+                    img_instance.label, img_inst_label,
+                    "Instance label {0} is not equal to the expected label"
+                    " {1}".format(img_instance.label, img_inst_label))
+                image_id = img.id
+                self.assertEqual(img_instance.image_id, image_id,
+                                 "Image id {0} is not equal to the expected id"
+                                 " {1}".format(img_instance.image_id,
+                                               image_id))
+                self.assertIsInstance(img_instance.public_ips, list)
+                if img_instance.public_ips:
+                    self.assertTrue(
+                        img_instance.public_ips[0],
+                        "public ip should contain a"
+                        " valid value if a list of public_ips exist")
+                self.assertIsInstance(img_instance.private_ips, list)
+                self.assertTrue(img_instance.private_ips[0],
+                                "private ip should"
+                                " contain a valid value")
 
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                test_instance, net)):
-            net, subnet = helpers.create_test_network(
-                self.provider, instance_name)
+                test_instance)):
+            subnet = helpers.get_or_create_default_subnet(
+                self.provider)
             test_instance = helpers.get_test_instance(
-                self.provider, instance_name, subnet=subnet)
-
+                self.provider, instance_label, subnet=subnet)
             sit.check_crud(self, self.provider.compute.images, MachineImage,
                            "cb-listimg", create_img, cleanup_img,
                            extra_test_func=extra_tests)

+ 2 - 1
test/test_interface.py

@@ -1,5 +1,4 @@
 import unittest
-from test.helpers import ProviderTestBase
 
 import cloudbridge
 from cloudbridge.cloud import interfaces
@@ -7,6 +6,8 @@ from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 
+from test.helpers import ProviderTestBase
+
 
 class CloudInterfaceTestCase(ProviderTestBase):
 

+ 76 - 49
test/test_network_service.py

@@ -1,14 +1,16 @@
+from cloudbridge.cloud.interfaces.resources import FloatingIP
+from cloudbridge.cloud.interfaces.resources import Network
+from cloudbridge.cloud.interfaces.resources import NetworkState
+from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import Subnet
+from cloudbridge.cloud.interfaces.resources import SubnetState
+
 import test.helpers as helpers
 from test.helpers import ProviderTestBase
 from test.helpers import get_provider_test_data
 from test.helpers import standard_interface_tests as sit
 
 from cloudbridge.cloud.base.resources import BaseNetwork
-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):
 
@@ -17,22 +19,29 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
     @helpers.skipIfNoService(['networking.networks'])
     def test_crud_network(self):
 
-        def create_net(name):
+        def create_net(label):
             return self.provider.networking.networks.create(
-                name=name, cidr_block='10.0.0.0/16')
+                label=label, cidr_block='10.0.0.0/16')
 
         def cleanup_net(net):
-            self.provider.networking.networks.delete(network_id=net.id)
+            if net:
+                net.delete()
+                net.refresh()
+                self.assertTrue(
+                    net.state == NetworkState.UNKNOWN,
+                    "Network.state must be unknown when refreshing after "
+                    "a delete but got %s"
+                    % net.state)
 
         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())
+        label = 'cb-propnetwork-{0}'.format(helpers.get_uuid())
+        subnet_label = 'cb-propsubnet-{0}'.format(helpers.get_uuid())
         net = self.provider.networking.networks.create(
-            name=name, cidr_block='10.0.0.0/16')
+            label=label, cidr_block='10.0.0.0/16')
         with helpers.cleanup_action(lambda: net.delete()):
             net.wait_till_ready()
             self.assertEqual(
@@ -46,10 +55,11 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Network CIDR %s does not contain the expected value."
                 % net.cidr_block)
 
-            cidr = '{0}/24'.format(net.cidr_block.split('/')[0] or '10.0.0.0')
-            sn = net.create_subnet(name=subnet_name, cidr_block=cidr,
-                                   zone=helpers.get_provider_test_data(
-                                       self.provider, 'placement'))
+            cidr = '10.0.20.0/24'
+            sn = net.create_subnet(
+                label=subnet_label, cidr_block=cidr,
+                zone=helpers.get_provider_test_data(self.provider,
+                                                    'placement'))
             with helpers.cleanup_action(lambda: sn.delete()):
                 self.assertTrue(
                     sn in net.subnets,
@@ -66,10 +76,15 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     net.subnets, [sn],
                     "Network should have exactly one subnet: %s." % sn.id)
 
-                self.assertIn(
+                self.assertEqual(
                     net.id, sn.network_id,
-                    "Network ID %s should be specified in the subnet's network"
-                    " id %s." % (net.id, sn.network_id))
+                    "Network ID %s and subnet's network id %s should be"
+                    " equal." % (net.id, sn.network_id))
+
+                self.assertEqual(
+                    net, sn.network,
+                    "Network obj %s and subnet's parent net obj %s"
+                    " should be equal." % (net, sn.network))
 
                 self.assertEqual(
                     cidr, sn.cidr_block,
@@ -83,49 +98,53 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
     def test_crud_subnet(self):
         # Late binding will make sure that create_subnet gets the
         # correct value
-        net = None
+        sn = helpers.get_or_create_default_subnet(self.provider)
+        net = sn.network
 
-        def create_subnet(name):
+        def create_subnet(label):
             return self.provider.networking.subnets.create(
-                network=net, cidr_block="10.0.0.0/24", name=name)
+                label=label, network=net, cidr_block="10.0.10.0/24",
+                zone=helpers.get_provider_test_data(
+                    self.provider, 'placement'))
 
         def cleanup_subnet(subnet):
-            self.provider.networking.subnets.delete(subnet=subnet)
+            if subnet:
+                subnet.delete()
+                subnet.refresh()
+                self.assertTrue(
+                    subnet.state == SubnetState.UNKNOWN,
+                    "Subnet.state must be unknown when refreshing after "
+                    "a delete but got %s"
+                    % subnet.state)
 
-        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)
+        sit.check_crud(self, self.provider.networking.subnets, Subnet,
+                       "cb-crudsubnet", create_subnet, cleanup_subnet)
 
     def test_crud_floating_ip(self):
-        net, gw = helpers.get_test_gateway(
-            self.provider, 'cb-crudfipgw-{0}'.format(helpers.get_uuid()))
+        gw = helpers.get_test_gateway(
+            self.provider)
 
-        def create_fip(name):
+        def create_fip(label):
             fip = gw.floating_ips.create()
             return fip
 
         def cleanup_fip(fip):
-            gw.floating_ips.delete(fip.id)
+            if fip:
+                gw.floating_ips.delete(fip.id)
 
         with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(net, gw)):
+                lambda: helpers.delete_test_gateway(gw)):
             sit.check_crud(self, gw.floating_ips, FloatingIP,
                            "cb-crudfip", create_fip, cleanup_fip,
                            skip_name_check=True)
 
     def test_floating_ip_properties(self):
         # Check floating IP address
-        net, gw = helpers.get_test_gateway(
-            self.provider, 'cb-crudfipgw-{0}'.format(helpers.get_uuid()))
+        gw = helpers.get_test_gateway(
+            self.provider)
         fip = gw.floating_ips.create()
         with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(net, gw)):
+                lambda: helpers.delete_test_gateway(gw)):
             with helpers.cleanup_action(lambda: fip.delete()):
                 fipl = list(gw.floating_ips)
                 self.assertIn(fip, fipl)
@@ -148,13 +167,13 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
         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: gateway.delete()):
-                        with helpers.cleanup_action(lambda: router.delete()):
+                with helpers.cleanup_action(lambda: router.delete()):
+                    with helpers.cleanup_action(lambda: subnet.delete()):
+                        with helpers.cleanup_action(lambda: gateway.delete()):
                             router.detach_subnet(subnet)
                             router.detach_gateway(gateway)
 
-        name = 'cb-crudrouter-{0}'.format(helpers.get_uuid())
+        label = '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
@@ -163,11 +182,11 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         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 = '{0}/24'.format(net.cidr_block.split('/')[0] or '10.0.0.0')
-            sn = net.create_subnet(name=name, cidr_block=cidr,
+                label=label, cidr_block='10.0.0.0/16')
+            router = self.provider.networking.routers.create(label=label,
+                                                             network=net)
+            cidr = '10.0.15.0/24'
+            sn = net.create_subnet(label=label, cidr_block=cidr,
                                    zone=helpers.get_provider_test_data(
                                        self.provider, 'placement'))
 
@@ -185,8 +204,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     "Router {0} should not be assoc. with network {1}".format(
                             router.id, router.network_id))
 
+            self.assertTrue(
+                len(router.subnets) == 0,
+                "No subnet should be attached to router {1}".format(sn, router)
+            )
             router.attach_subnet(sn)
-            gteway = net.gateways.get_or_create_inet_gateway(name)
+            self.assertTrue(
+                len(router.subnets) == 1,
+                "Subnet {0} not attached to router {1}".format(sn, router)
+            )
+            gteway = net.gateways.get_or_create_inet_gateway()
             router.attach_gateway(gteway)
             # TODO: add a check for routes after that's been implemented
 

+ 5 - 6
test/test_object_life_cycle.py

@@ -1,9 +1,9 @@
-from test import helpers
-from test.helpers import ProviderTestBase
-
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
+from test import helpers
+from test.helpers import ProviderTestBase
+
 
 class CloudObjectLifeCycleTestCase(ProviderTestBase):
 
@@ -14,10 +14,9 @@ class CloudObjectLifeCycleTestCase(ProviderTestBase):
         """
         Test object life cycle methods by using a volume.
         """
-        name = "cb-objlifecycle-{0}".format(helpers.get_uuid())
+        label = "cb-objlifecycle-{0}".format(helpers.get_uuid())
         test_vol = self.provider.storage.volumes.create(
-            name,
-            1,
+            label, 1,
             helpers.get_provider_test_data(self.provider, "placement"))
 
         # Waiting for an invalid timeout should raise an exception

+ 29 - 34
test/test_object_store_service.py

@@ -1,21 +1,20 @@
 import filecmp
 import os
 import tempfile
-import uuid
 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
+import requests
+
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.cloud.interfaces.provider import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.resources import Bucket
 from cloudbridge.cloud.interfaces.resources import BucketObject
 
-import requests
+from test import helpers
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 
 class CloudObjectStoreServiceTestCase(ProviderTestBase):
@@ -33,27 +32,17 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             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)
+            if bucket:
+                bucket.delete()
 
-        with self.assertRaises(InvalidNameException):
-            # bucket name cannot be an IP address
-            create_bucket("197.10.100.42")
+        def extra_tests(bucket):
+            # Recreating existing bucket should raise an exception
+            with self.assertRaises(DuplicateResourceException):
+                self.provider.storage.buckets.create(name=bucket.name)
 
         sit.check_crud(self, self.provider.storage.buckets, Bucket,
                        "cb-crudbucket", create_bucket, cleanup_bucket,
-                       skip_name_check=True)
+                       extra_test_func=extra_tests)
 
     @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket_object(self):
@@ -69,10 +58,11 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             return obj
 
         def cleanup_bucket_obj(bucket_obj):
-            bucket_obj.delete()
+            if bucket_obj:
+                bucket_obj.delete()
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
-            name = "cb-crudbucketobj-{0}".format(uuid.uuid4())
+            name = "cb-crudbucketobj-{0}".format(helpers.get_uuid())
             test_bucket = self.provider.storage.buckets.create(name)
 
             sit.check_crud(self, test_bucket.objects, BucketObject,
@@ -86,7 +76,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         check whether list properly detects the new content.
         Delete everything afterwards.
         """
-        name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
+        name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
         # ensure that the bucket is empty
@@ -110,6 +100,13 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     isinstance(objs[0].size, int),
                     "Object size property needs to be a int, not {0}".format(
                         type(objs[0].size)))
+                # GET an object as the size property implementation differs
+                # for objects returned by LIST and GET.
+                obj = test_bucket.objects.get(objs[0].id)
+                self.assertTrue(
+                    isinstance(objs[0].size, int),
+                    "Object size property needs to be an int, not {0}".format(
+                        type(obj.size)))
                 self.assertTrue(
                     datetime.strptime(objs[0].last_modified[:23],
                                       "%Y-%m-%dT%H:%M:%S.%f"),
@@ -137,7 +134,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content(self):
-        name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
+        name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
@@ -160,10 +157,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @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())
+        name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
@@ -185,7 +179,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.buckets'])
     def test_upload_download_bucket_content_from_file(self):
-        name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
+        name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
@@ -216,7 +210,8 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             out.truncate(6 * 1024 * 1024 * 1024)  # 6 Gig...
         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())
+            bucket_name = "cbtestbucketlargeobjs-{0}".format(
+                                                            helpers.get_uuid())
             test_bucket = self.provider.storage.buckets.create(bucket_name)
             with helpers.cleanup_action(lambda: test_bucket.delete()):
                 test_obj = test_bucket.objects.create(file_name)

+ 4 - 4
test/test_region_service.py

@@ -1,10 +1,10 @@
-from test import helpers
-from test.helpers import ProviderTestBase
-from test.helpers import standard_interface_tests as sit
+import six
 
 from cloudbridge.cloud.interfaces import Region
 
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
 
 
 class CloudRegionServiceTestCase(ProviderTestBase):

+ 60 - 62
test/test_security_service.py

@@ -1,8 +1,4 @@
 """Test cloudbridge.security modules."""
-from test import helpers
-from test.helpers import ProviderTestBase
-from test.helpers import standard_interface_tests as sit
-
 import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.cloud.interfaces.resources import KeyPair
@@ -10,6 +6,10 @@ from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.interfaces.resources import VMFirewallRule
 
+from test import helpers
+from test.helpers import ProviderTestBase
+from test.helpers import standard_interface_tests as sit
+
 
 class CloudSecurityServiceTestCase(ProviderTestBase):
 
@@ -22,7 +22,8 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             return self.provider.security.key_pairs.create(name=name)
 
         def cleanup_kp(kp):
-            self.provider.security.key_pairs.delete(key_pair_id=kp.id)
+            if kp:
+                self.provider.security.key_pairs.delete(key_pair_id=kp.id)
 
         def extra_tests(kp):
             # Recreating existing keypair should raise an exception
@@ -59,83 +60,80 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     @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
+        subnet = helpers.get_or_create_default_subnet(self.provider)
+        net = subnet.network
 
-        def create_fw(name):
+        def create_fw(label):
             return self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network=net.id)
 
         def cleanup_fw(fw):
-            fw.delete()
+            if fw:
+                fw.delete()
 
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net)):
-            net, _ = helpers.create_test_network(self.provider, name)
+        def network_id_test(fw):
+            # Checking that the network ID is returned correctly
+            self.assertEqual(fw.network_id, net.id)
 
-            sit.check_crud(self, self.provider.security.vm_firewalls,
-                           VMFirewall, "cb-crudfw", create_fw, cleanup_fw)
+        sit.check_crud(self, self.provider.security.vm_firewalls,
+                       VMFirewall, "cb-crudfw", create_fw, cleanup_fw,
+                       extra_test_func=network_id_test)
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_vm_firewall_properties(self):
-        name = 'cb-propfw-{0}'.format(helpers.get_uuid())
+        label = '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
         fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, vm_firewall=fw)):
-            net, _ = helpers.create_test_network(self.provider, name)
+                vm_firewall=fw)):
+            subnet = helpers.get_or_create_default_subnet(self.provider)
+            net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
 
-            self.assertEqual(name, fw.description)
+            self.assertEqual(label, fw.description)
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_crud_vm_firewall_rules(self):
-        name = 'cb-crudfw-rules-{0}'.format(helpers.get_uuid())
+        label = 'cb-crudfw-rules-{0}'.format(helpers.get_uuid())
 
-        # 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)
+        subnet = helpers.get_or_create_default_subnet(self.provider)
+        net = subnet.network
 
-            fw = None
-            with helpers.cleanup_action(lambda: fw.delete()):
-                fw = self.provider.security.vm_firewalls.create(
-                    name=name, description=name, network_id=net.id)
+        fw = None
+        with helpers.cleanup_action(lambda: fw.delete()):
+            fw = self.provider.security.vm_firewalls.create(
+                label=label, description=label, network_id=net.id)
 
-                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 create_fw_rule(label):
+                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):
+            def cleanup_fw_rule(rule):
+                if rule:
                     rule.delete()
 
-                sit.check_crud(self, fw.rules, VMFirewallRule, "cb-crudfwrule",
-                               create_fw_rule, cleanup_fw_rule,
-                               skip_name_check=True)
+            sit.check_crud(self, fw.rules, VMFirewallRule, "cb-crudfwrule",
+                           create_fw_rule, cleanup_fw_rule,
+                           skip_name_check=True)
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_vm_firewall_rule_properties(self):
-        name = 'cb-propfwrule-{0}'.format(helpers.get_uuid())
+        label = '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
         fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, vm_firewall=fw)):
-            net, _ = helpers.create_test_network(self.provider, name)
+                vm_firewall=fw)):
+            subnet = helpers.get_or_create_default_subnet(self.provider)
+            net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -148,18 +146,18 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_vm_firewall_rule_add_twice(self):
-        name = 'cb-fwruletwice-{0}'.format(helpers.get_uuid())
+        label = '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
         fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, vm_firewall=fw)):
+                vm_firewall=fw)):
 
-            net, _ = helpers.create_test_network(self.provider, name)
+            subnet = helpers.get_or_create_default_subnet(self.provider)
+            net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -172,24 +170,24 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_vm_firewall_group_rule(self):
-        name = 'cb-fwrule-{0}'.format(helpers.get_uuid())
+        label = '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
         fw = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net, vm_firewall=fw)):
-            net, _ = helpers.create_test_network(self.provider, name)
+                vm_firewall=fw)):
+            subnet = helpers.get_or_create_default_subnet(self.provider)
+            net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                name=name, description=name, network_id=net.id)
+                label=label, description=label, network_id=net.id)
             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))
+                rule.src_dest_fw.label == fw.label,
+                "Expected VM firewall rule label {0}. Got {1}."
+                .format(fw.label, rule.src_dest_fw.label))
             for r in fw.rules:
                 r.delete()
             fw = self.provider.security.vm_firewalls.get(fw.id)  # update
@@ -198,8 +196,8 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 "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]
+        found_fw = [f for f in fwl if f.label == label]
         self.assertTrue(
             len(found_fw) == 0,
             "VM firewall {0} should have been deleted but still exists."
-            .format(name))
+            .format(label))

+ 5 - 4
test/test_vm_types_service.py

@@ -1,9 +1,9 @@
+import six
+
 from test import helpers
 from test.helpers import ProviderTestBase
 from test.helpers import standard_interface_tests as sit
 
-import six
-
 
 class CloudVMTypeServiceTestCase(ProviderTestBase):
 
@@ -24,13 +24,14 @@ class CloudVMTypeServiceTestCase(ProviderTestBase):
                 vm_type.family is None or isinstance(
                     vm_type.family,
                     six.string_types),
-                "VMType family family be None or a"
+                "VMType family must 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")
+                "VMType vcpus must be None or a positive integer but is: {0}"
+                .format(vm_type.vcpus))
             self.assertTrue(
                 vm_type.ram is None or vm_type.ram >= 0,
                 "VMType ram must be None or a positive number")

+ 2 - 2
tox.ini

@@ -17,7 +17,7 @@ envlist = {py27,py36,pypy}-{aws,azure,gce,openstack}
 [testenv]
 commands = flake8 cloudbridge test setup.py
            # see setup.cfg for options sent to nosetests and coverage
-           {envpython} setup.py nosetests {posargs}
+           nosetests -v --nocapture --nologcapture --logging-format='%(asctime)s [%(levelname)s] %(name)s: %(message)s' {posargs}
 setenv =
     MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
     aws: CB_TEST_PROVIDER=aws
@@ -25,7 +25,7 @@ setenv =
     gce: CB_TEST_PROVIDER=gce
     openstack: CB_TEST_PROVIDER=openstack
 passenv =
-    CB_USE_MOCK_PROVIDERS
+    CB_USE_MOCK_PROVIDERS PYTHONUNBUFFERED
     aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
     azure: AZURE_SUBSCRIPTION_ID AZURE_CLIENT_ID AZURE_SECRET AZURE_TENANT AZURE_REGION_NAME AZURE_RESOURCE_GROUP AZURE_STORAGE_ACCOUNT AZURE_VM_DEFAULT_USER_NAME AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
     gce: CB_IMAGE_GCE CB_INSTANCE_TYPE_GCE CB_PLACEMENT_GCE GCE_DEFAULT_REGION GCE_DEFAULT_ZONE GCE_PROJECT_NAME GCE_SERVICE_CREDS_FILE GCE_SERVICE_CREDS_DICT

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.