浏览代码

Merge pull request #145 from CloudVE/display_id_and_label

Name and label support
Nuwan Goonasekera 7 年之前
父节点
当前提交
d2cd53dca0
共有 44 个文件被更改,包括 1882 次插入1393 次删除
  1. 3 0
      .travis.yml
  2. 13 3
      cloudbridge/cloud/base/helpers.py
  3. 72 240
      cloudbridge/cloud/base/resources.py
  4. 1 1
      cloudbridge/cloud/base/services.py
  5. 15 1
      cloudbridge/cloud/interfaces/exceptions.py
  6. 2 2
      cloudbridge/cloud/interfaces/provider.py
  7. 150 85
      cloudbridge/cloud/interfaces/resources.py
  8. 54 53
      cloudbridge/cloud/interfaces/services.py
  9. 16 9
      cloudbridge/cloud/providers/aws/helpers.py
  10. 146 73
      cloudbridge/cloud/providers/aws/resources.py
  11. 126 92
      cloudbridge/cloud/providers/aws/services.py
  12. 34 5
      cloudbridge/cloud/providers/azure/azure_client.py
  13. 17 17
      cloudbridge/cloud/providers/azure/helpers.py
  14. 1 1
      cloudbridge/cloud/providers/azure/provider.py
  15. 182 143
      cloudbridge/cloud/providers/azure/resources.py
  16. 145 133
      cloudbridge/cloud/providers/azure/services.py
  17. 124 69
      cloudbridge/cloud/providers/openstack/resources.py
  18. 84 86
      cloudbridge/cloud/providers/openstack/services.py
  19. 2 2
      docs/api_docs/cloud/exceptions.rst
  20. 34 0
      docs/concepts.rst
  21. 20 11
      docs/getting_started.rst
  22. 二进制
      docs/topics/captures/az-label-dash.png
  23. 二进制
      docs/topics/captures/az-net-id.png
  24. 二进制
      docs/topics/captures/az-net-label.png
  25. 1 1
      docs/topics/contributor_guide.rst
  26. 121 0
      docs/topics/dashboard.rst
  27. 0 37
      docs/topics/design-decisions.rst
  28. 118 0
      docs/topics/design_decisions.rst
  29. 1 1
      docs/topics/networking.rst
  30. 19 17
      docs/topics/setup.rst
  31. 4 2
      docs/topics/testing.rst
  32. 1 0
      setup.cfg
  33. 1 1
      setup.py
  34. 23 30
      test/helpers/__init__.py
  35. 106 35
      test/helpers/standard_interface_tests.py
  36. 35 42
      test/test_block_store_service.py
  37. 55 56
      test/test_compute_service.py
  38. 44 11
      test/test_image_service.py
  39. 40 41
      test/test_network_service.py
  40. 2 3
      test/test_object_life_cycle.py
  41. 14 25
      test/test_object_store_service.py
  42. 52 62
      test/test_security_service.py
  43. 3 2
      test/test_vm_types_service.py
  44. 1 1
      tox.ini

+ 3 - 0
.travis.yml

@@ -30,6 +30,9 @@ matrix:
       env: TOX_ENV=pypy-azure
     - python: pypy-5.3.1
       env: TOX_ENV=pypy-openstack
+env:
+  global:
+    - PYTHONUNBUFFERED=True
 before_install:
     - |
       case "$TRAVIS_EVENT_TYPE" in

+ 13 - 3
cloudbridge/cloud/base/helpers.py

@@ -1,4 +1,6 @@
+import fnmatch
 import os
+import re
 import sys
 import traceback
 from contextlib import contextmanager
@@ -39,9 +41,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):

+ 72 - 240
cloudbridge/cloud/base/resources.py

@@ -4,15 +4,18 @@ Base implementation for data objects exposed through a provider or service
 import inspect
 import itertools
 import logging
+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
@@ -58,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/CloudVE/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 not start or"
+                " 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)
+            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):
@@ -217,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):
     """
@@ -228,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
@@ -383,10 +293,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):
 
@@ -400,7 +306,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
@@ -413,10 +319,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):
 
@@ -504,7 +406,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):
@@ -514,10 +416,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):
 
@@ -551,7 +449,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(
@@ -560,10 +458,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):
 
@@ -577,7 +471,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(
@@ -586,10 +480,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):
 
@@ -616,7 +506,7 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         """
         Return the name of this key pair.
         """
-        return self._key_pair.name
+        return self.id
 
     @property
     def material(self):
@@ -638,9 +528,6 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         #  multiple providers.
         self._key_pair.delete()
 
-    def __repr__(self):
-        return "<CBKeyPair: {0}>".format(self.name)
-
 
 class BaseVMFirewall(BaseCloudResource, VMFirewall):
 
@@ -675,7 +562,7 @@ class BaseVMFirewall(BaseCloudResource, VMFirewall):
         """
         Return the name of this VM firewall.
         """
-        return self._vm_firewall.name
+        return self.id
 
     @property
     def description(self):
@@ -690,10 +577,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):
@@ -787,10 +670,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
@@ -803,10 +682,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
@@ -816,7 +691,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
 
 
@@ -835,13 +710,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)
@@ -857,37 +734,12 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
                 # check from most to least likely mutables
                 self.name == other.name)
 
-    def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.name)
-
 
 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
@@ -896,10 +748,6 @@ class BaseBucket(BaseCloudResource, Bucket):
                 # check from most to least likely mutables
                 self.name == other.name)
 
-    def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.name)
-
 
 class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
 
@@ -921,16 +769,12 @@ class BaseGatewayContainer(GatewayContainer, BasePageableObjectMixin):
 
 class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
-    CB_DEFAULT_NETWORK_NAME = cb_helpers.get_env('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)
-
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
             [NetworkState.AVAILABLE],
@@ -938,9 +782,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
@@ -951,22 +795,22 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
 class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
-    CB_DEFAULT_SUBNET_NAME = cb_helpers.get_env('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],
@@ -1004,7 +848,7 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
 
     @property
     def name(self):
-        # VM firewall rules don't support names, so pass
+        # VM firewall rules don't support labels
         return self.public_ip
 
     @property
@@ -1019,10 +863,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
@@ -1032,16 +872,12 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
 
 class BaseRouter(BaseCloudResource, Router):
 
-    CB_DEFAULT_ROUTER_NAME = cb_helpers.get_env('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
@@ -1059,10 +895,6 @@ class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
         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

+ 1 - 1
cloudbridge/cloud/base/services.py

@@ -186,7 +186,7 @@ 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))
 

+ 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

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

@@ -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

+ 150 - 85
cloudbridge/cloud/interfaces/resources.py

@@ -31,9 +31,12 @@ class CloudResource(object):
 
     This interface has a  _provider property that can be used to access the
     provider associated with the resource, which is only intended for use by
-    subclasses. Every cloudbridge resource also has an id and name property.
-    The id property is a unique identifier for the resource. The name property
-    is a display value.
+    subclasses. Every cloudbridge resource also has an id, a name and a
+    label property. The id property is a unique identifier for the resource.
+    The name is a more user-friendly version of an id, suitable for
+    display to an end-user. However, it cannot be used in place of id. See
+    @name documentation. The label property is a user-assignable
+    identifier for the resource.
     """
     __metaclass__ = ABCMeta
 
@@ -66,29 +69,33 @@ class CloudResource(object):
     @abstractproperty
     def name(self):
         """
-        Get the resource name.
-
-        The name property is typically a user-friendly display value for the
-        resource. Some resources may allow the resource name to be set.
-
-        The name property adheres to the following restrictions for most
-        cloudbridge resources:
-        * Names cannot be longer than 63 characters
-        * May only contain lowercase letters, numeric characters, underscores,
-          and dashes. International characters are allowed.
-
-        Some resources may relax/increase these restrictions (e.g. Buckets)
-        depending on their requirements. Consult the resource specific
-        documentation for exact restrictions.
-
-        Some resources may allow an existing resource name to be changed.
-        However, this may lead to cloud-dependent code because not all all
-        providers support this capability. See
-        http://cloudbridge.cloudve.org/en/latest/topics/design-decisions.html
-        for more details and potential implications.
+        Get the name id for the resource.
+
+        The name property is typically a user-friendly id value for the
+        resource. The name is different from the id property in the
+        following ways:
+        1. The name property is often a more user-friendly value to
+           display to the user than the id property.
+        2. The name may sometimes be the same as the id, but should never
+           be used in place of the id.
+        3. The id is what will uniquely identify a resource, and will be used
+           internally by cloudbridge for all get operations etc.
+        4. All resources have a name.
+        5. The name is read-only.
+        6. However, the name may not necessarily be unique, which is the
+           reason why it should not be used for uniquely identifying a
+           resource.
+        Example:
+        The AWS machine image name maps to a cloudbridge name. It is not
+        editable and is a user friendly name such as 'Ubuntu 14.04' and
+        corresponds to the ami-name. It is distinct from the ami-id, which
+        maps to cloudbridge's id property. The ami-name cannot be edited, and
+        is set at creation time. It is not necessarily unique.
+        In Azure, the machine image's name corresponds to cloudbridge's name
+        property. In Azure, it also happens to be the same as the id property.
 
-        :rtype: ``str``
-        :return: Name for this resource as returned by the cloud middleware.
+        The name property and the label property share the same character
+        restrictions. see :py:attr:`~LabeledCloudResource.label`
         """
         pass
 
@@ -100,6 +107,35 @@ class CloudResource(object):
         pass
 
 
+class LabeledCloudResource(CloudResource):
+
+    @abstractproperty
+    def label(self):
+        """
+        Get the resource label.
+
+        The label property is a user-defined, editable identifier for a
+        resource. It will often correspond to a user editable resource label
+        in the underlying cloud provider, or be simulated through tags/labels.
+
+        The label property adheres to the following restrictions:
+        * Must be at least 3 characters in length.
+        * Cannot be longer than 63 characters.
+        * May only contain ASCII characters comprising of lowercase letters,
+          numeric characters, and dashes.
+        * Must begin with an alphanumeric character and end with one
+          (i.e. cannot begin or end with a dash)
+
+        Some resources may not support labels, in which case, a
+        NotImplementedError will be thrown.
+
+        :rtype: ``str``
+        :return: Label for this resource as returned by the cloud middleware.
+        :throws NotImplementedError if this resource does not support labels
+        """
+        pass
+
+
 class Configuration(dict):
     """
     Represents a cloudbridge configuration object
@@ -460,19 +496,15 @@ class InstanceState(object):
     ERROR = "error"
 
 
-class Instance(ObjectLifeCycleMixin, CloudResource):
+class Instance(ObjectLifeCycleMixin, LabeledCloudResource):
 
     __metaclass__ = ABCMeta
 
-    @CloudResource.name.setter
+    @LabeledCloudResource.label.setter
     @abstractmethod
-    def name(self, value):
+    def label(self, value):
         """
-        Set the instance name.
-
-        Note that the changing the name of an existing resource may result in
-        cloud-dependent code. See the following page for more details:
-        http://cloudbridge.cloudve.org/en/latest/topics/design-decisions.html
+        Set the instance label.
         """
         pass
 
@@ -506,7 +538,7 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         object, you can use the ``instance.vm_type`` property instead.
 
         :rtype: ``str``
-        :return: VM type name for this instance (e.g., ``m1.large``)
+        :return: VM type id for this instance (e.g., ``m1.large``)
         """
         pass
 
@@ -598,17 +630,17 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         pass
 
     @abstractproperty
-    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.
 
         :rtype: ``str``
-        :return: Name of the ssh key pair associated with this instance.
+        :return: Id of the ssh key pair associated with this instance.
         """
         pass
 
     @abstractmethod
-    def create_image(self, name):
+    def create_image(self, label):
         """
         Create a new image based on this instance.
 
@@ -698,8 +730,8 @@ class LaunchConfig(object):
         lc = provider.compute.instances.create_launch_config()
         lc.add_block_device(...)
 
-        inst = provider.compute.instances.create(name, image, vm_type,
-                                                 network, launch_config=lc)
+        inst = provider.compute.instances.create(
+            'MyVM', image, vm_type, subnet, launch_config=lc)
     """
 
     @abstractmethod
@@ -794,7 +826,7 @@ class LaunchConfig(object):
         pass
 
 
-class MachineImage(ObjectLifeCycleMixin, CloudResource):
+class MachineImage(ObjectLifeCycleMixin, LabeledCloudResource):
 
     __metaclass__ = ABCMeta
 
@@ -850,12 +882,20 @@ class NetworkState(object):
     ERROR = "error"
 
 
-class Network(ObjectLifeCycleMixin, CloudResource):
+class Network(ObjectLifeCycleMixin, LabeledCloudResource):
     """
     Represents a software-defined network, like the Virtual Private Cloud.
     """
     __metaclass__ = ABCMeta
 
+    @LabeledCloudResource.label.setter
+    @abstractmethod
+    def label(self, value):
+        """
+        Set the resource label.
+        """
+        pass
+
     @abstractproperty
     def external(self):
         """
@@ -910,13 +950,13 @@ class Network(ObjectLifeCycleMixin, CloudResource):
         pass
 
     @abstractmethod
-    def create_subnet(self, name, cidr_block, zone=None):
+    def create_subnet(self, label, cidr_block, zone=None):
         """
         Create a new network subnet and associate it with this 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. The subnet name will be derived from
+                      this label.
 
         :type cidr_block: ``str``
         :param cidr_block: CIDR block within this Network to assign to the
@@ -960,12 +1000,20 @@ class SubnetState(object):
     ERROR = "error"
 
 
-class Subnet(ObjectLifeCycleMixin, CloudResource):
+class Subnet(ObjectLifeCycleMixin, LabeledCloudResource):
     """
     Represents a subnet, as part of a Network.
     """
     __metaclass__ = ABCMeta
 
+    @LabeledCloudResource.label.setter
+    @abstractmethod
+    def label(self, value):
+        """
+        Set the resource label.
+        """
+        pass
+
     @abstractproperty
     def cidr_block(self):
         """
@@ -986,6 +1034,16 @@ class Subnet(ObjectLifeCycleMixin, CloudResource):
         """
         pass
 
+    @abstractproperty
+    def network(self):
+        """
+        The parent network object associated with this this subnet.
+
+        :rtype: ``Network``
+        :return: `Network` object
+        """
+        pass
+
     @abstractproperty
     def zone(self):
         """
@@ -1043,7 +1101,7 @@ class FloatingIPContainer(PageableObjectMixin):
         """
         Searches for a FloatingIP by a given list of attributes.
 
-        Supported attributes: name, public_ip
+        Supported attributes: label, public_ip
 
         Example:
 
@@ -1156,12 +1214,25 @@ class RouterState(object):
     DETACHED = "detached"
 
 
-class Router(CloudResource):
+class Router(LabeledCloudResource):
     """
     Represents a private network router.
+
+    This logical router is meant to roughly mimic the properties of a physical
+    router. Therefore, attaching a subnet can be thought of as plugging in a
+    network cable to enable routing to/from that subnet. Attaching a gateway
+    can be thought of as plugging in an upstream link.
     """
     __metaclass__ = ABCMeta
 
+    @LabeledCloudResource.label.setter
+    @abstractmethod
+    def label(self, value):
+        """
+        Set the resource label.
+        """
+        pass
+
     @abstractproperty
     def state(self):
         """
@@ -1273,8 +1344,7 @@ class GatewayContainer(PageableObjectMixin):
         provide internet routing to a network.
 
         :type  name: ``str``
-        :param name: The gateway name. This applies only if creating a gateway
-                     and if the provider supports it.
+        :param name: The gateway label.
 
         :rtype: ``object``  of :class:`.InternetGateway` or ``None``
         :return: an InternetGateway object of ``None`` if not found.
@@ -1402,22 +1472,18 @@ class VolumeState(object):
     ERROR = "error"
 
 
-class Volume(ObjectLifeCycleMixin, CloudResource):
+class Volume(ObjectLifeCycleMixin, LabeledCloudResource):
     """
     Represents a block storage device (aka volume).
     """
 
     __metaclass__ = ABCMeta
 
-    @CloudResource.name.setter
+    @LabeledCloudResource.label.setter
     @abstractmethod
-    def name(self, value):
+    def label(self, value):
         """
-        Set the volume name.
-
-        Note that the changing the name of an existing resource may result in
-        cloud-dependent code. See the following page for more details:
-        http://cloudbridge.cloudve.org/en/latest/topics/design-decisions.html
+        Set the volume label.
         """
         pass
 
@@ -1427,7 +1493,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         Get the volume description.
 
         Some cloud providers may not support this property, and will return the
-        volume name instead.
+        volume label instead.
 
         :rtype: ``str``
         :return: Description for this volume as returned by the cloud
@@ -1443,7 +1509,7 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
 
         Some cloud providers may not support this property, and setting the
         description may have no effect (providers that do not support this
-        property will always return the volume name as the description).
+        property will always return the volume label as the description).
         """
         pass
 
@@ -1542,12 +1608,12 @@ class Volume(ObjectLifeCycleMixin, CloudResource):
         pass
 
     @abstractmethod
-    def create_snapshot(self, name, description=None):
+    def create_snapshot(self, label, description=None):
         """
         Create a snapshot of this Volume.
 
-        :type name: ``str``
-        :param name: The name of this snapshot.
+        :type label: ``str``
+        :param label: The label for this snapshot.
 
         :type description: ``str``
         :param description: A description of the snapshot.
@@ -1587,22 +1653,18 @@ class SnapshotState(object):
     ERROR = "error"
 
 
-class Snapshot(ObjectLifeCycleMixin, CloudResource):
+class Snapshot(ObjectLifeCycleMixin, LabeledCloudResource):
     """
     Represents a snapshot of a block storage device.
     """
 
     __metaclass__ = ABCMeta
 
-    @CloudResource.name.setter
+    @LabeledCloudResource.label.setter
     @abstractmethod
-    def name(self, value):
+    def label(self, value):
         """
-        Set the snapshot name.
-
-        Note that the changing the name of an existing resource may result in
-        cloud-dependent code. See the following page for more details:
-        http://cloudbridge.cloudve.org/en/latest/topics/design-decisions.html
+        Set the snapshot label.
         """
         pass
 
@@ -1612,7 +1674,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
         Get the snapshot description.
 
         Some cloud providers may not support this property, and will return the
-        snapshot name instead.
+        snapshot label instead.
 
         :rtype: ``str``
         :return: Description for this snapshot as returned by the cloud
@@ -1628,7 +1690,7 @@ class Snapshot(ObjectLifeCycleMixin, CloudResource):
 
         Some cloud providers may not support this property, and setting the
         description may have no effect (providers that do not support this
-        property will always return the snapshot name as the description).
+        property will always return the snapshot label as the description).
 
         :type value: ``str``
         :param value: The value for the snapshot description.
@@ -1795,7 +1857,7 @@ class PlacementZone(CloudResource):
         A region this placement zone is associated with.
 
         :rtype: ``str``
-        :return: The name of the region the zone is associated with.
+        :return: The id of the region the zone is associated with.
         """
         pass
 
@@ -1892,7 +1954,7 @@ class VMType(CloudResource):
         pass
 
 
-class VMFirewall(CloudResource):
+class VMFirewall(LabeledCloudResource):
     """
     Represents a firewall resource applied to virtual machines.
 
@@ -1901,6 +1963,14 @@ class VMFirewall(CloudResource):
 
     __metaclass__ = ABCMeta
 
+    @LabeledCloudResource.label.setter
+    @abstractmethod
+    def label(self, value):
+        """
+        Set the resource label.
+        """
+        pass
+
     @abstractproperty
     def description(self):
         """
@@ -1954,7 +2024,7 @@ class VMFirewallRuleContainer(PageableObjectMixin):
 
             fw = provider.security.vm_firewalls.get('my_fw_id')
             rule = fw.rules.get('rule_id')
-            print(rule.id, rule.name)
+            print(rule.id, rule.label)
 
         :rtype: :class:`.FirewallRule`
         :return:  a FirewallRule instance
@@ -2026,8 +2096,8 @@ class VMFirewallRuleContainer(PageableObjectMixin):
         """
         Find a firewall rule filtered by the given parameters.
 
-        :type name: str
-        :param name: The name of the VM firewall to retrieve.
+        :type label: str
+        :param label: The label of the VM firewall to retrieve.
 
         :type protocol: ``str``
         :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
@@ -2289,11 +2359,6 @@ class Bucket(CloudResource):
         """
         Retrieve the name of the current bucket.
 
-        The bucket name adheres to a naming requirement that is more
-        relaxed than the naming requirement enforced across CloudBridge. More
-        details are available here: http://docs.aws.amazon.com/awscloudtrail/
-        latest/userguide/cloudtrail-s3-bucket-naming-requirements.html
-
         :rtype: ``str``
         :return: Name for this instance as returned by the cloud middleware.
         """
@@ -2384,7 +2449,7 @@ class BucketContainer(PageableObjectMixin):
     @abstractmethod
     def find(self, **kwargs):
         """
-        Searche for an object by a given list of attributes.
+        Search for an object by a given list of attributes.
 
         Supported attributes: ``name``
 

+ 54 - 53
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 16.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`
@@ -98,7 +98,7 @@ class ComputeService(CloudService):
             image = provider.compute.images.find(name='Ubuntu 16.04')[0]
             size = provider.compute.vm_types.find(name='m1.small')
             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):
+    def create(self, label, network_id, 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.
@@ -759,7 +761,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         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., cloudbridge-net).
+        default label used by this library (e.g., cloudbridge-net).
 
         :type zone: :class:`.PlacementZone` object ``str``
         :param zone: Placement zone where to look for the subnet.
@@ -814,7 +816,7 @@ class RouterService(PageableObjectMixin, CloudService):
         """
         Searches for a router by a given list of attributes.
 
-        Supported attributes: name
+        Supported attributes: name, label
 
         :rtype: List of ``object`` of :class:`.Router`
         :return: A list of Router objects matching the supplied attributes.
@@ -822,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.
@@ -1118,19 +1119,19 @@ class VMFirewallService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, name, description, network_id):
+    def create(self, label, network_id, description=None):
         """
         Create a new VMFirewall.
 
-        :type name: str
-        :param name: The name of the new VM firewall.
-
-        :type description: str
-        :param description: The description of the new VM firewall.
+        :type label: str
+        :param label: The label for the new VM firewall.
 
         :type  network_id: ``str``
         :param network_id: Network ID under which to create the VM firewall.
 
+        :type description: str
+        :param description: The description of the new VM firewall.
+
         :rtype: ``object`` of :class:`.VMFirewall`
         :return:  A VMFirewall instance or ``None`` if one was not created.
         """

+ 16 - 9
cloudbridge/cloud/providers/aws/helpers.py

@@ -175,8 +175,10 @@ 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)
@@ -196,12 +198,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 +215,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:

+ 146 - 73
cloudbridge/cloud/providers/aws/resources.py

@@ -81,6 +81,21 @@ class AWSMachineImage(BaseMachineImage):
         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):
         try:
@@ -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):
@@ -287,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
@@ -342,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)
@@ -352,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):
@@ -379,24 +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 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 name for volume {0}: {1}".format(self.id, 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):
@@ -404,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):
@@ -449,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):
@@ -462,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)
@@ -472,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):
@@ -492,24 +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 self.id
+
+    @property
+    # pylint:disable=arguments-differ
+    def label(self):
         try:
             return find_tag_value(self._snapshot.tags, 'Name')
         except ClientError as e:
-            log.warn("Cannot get name for snap {0}: {1}".format(self.id, e))
+            log.warn("Cannot get label for snap {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._snapshot.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._snapshot.create_tags(Tags=[{'Key': 'Name',
+                                          'Value': value or ""}])
 
     @property
     def description(self):
@@ -518,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):
@@ -534,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)
@@ -544,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
 
 
@@ -577,19 +629,24 @@ 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:
-            name = find_tag_value(self._vm_firewall.tags, 'Name')
-            if not name:  # Return group_name (which cannot be changed)
-                name = self._vm_firewall.group_name
-            return name
+            return find_tag_value(self._vm_firewall.tags, 'Name')
         except ClientError:
-            return self._vm_firewall.group_name
+            return None
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._vm_firewall.create_tags(Tags=[{'Key': 'Name', 'Value': value}])
+    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):
@@ -826,7 +883,7 @@ class AWSBucket(BaseBucket):
 
     @property
     def name(self):
-        return self._bucket.name
+        return self.id
 
     @property
     def objects(self):
@@ -917,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):
@@ -924,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):
@@ -942,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)
@@ -966,7 +1030,7 @@ class AWSNetwork(BaseNetwork):
         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(
@@ -989,6 +1053,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):
@@ -996,13 +1061,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):
@@ -1022,6 +1091,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)
@@ -1030,13 +1101,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):
@@ -1105,13 +1175,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:
@@ -1171,12 +1246,9 @@ class AWSGatewayContainer(BaseGatewayContainer):
     def get_or_create_inet_gateway(self, name=None):
         log.debug("Get or create inet gateway %s on net %s", name,
                   self._network)
-        if name:
-            AWSInternetGateway.assert_valid_resource_name(name)
-
         network_id = self._network.id if isinstance(
             self._network, AWSNetwork) else self._network
-        # Don't filter by name because it may conflict with at least the
+        # 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',
@@ -1186,7 +1258,14 @@ class AWSGatewayContainer(BaseGatewayContainer):
         # 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
+            AWSInternetGateway.assert_valid_resource_name(name)
+            cb_gateway._gateway.create_tags(
+                Tags=[{'Key': 'Name', 'Value': name}])
+        else:
+            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
 
@@ -1221,12 +1300,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()

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

@@ -132,24 +132,28 @@ 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):
+    def create(self, label, network_id, 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_id,
                   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')
+        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)
@@ -195,25 +199,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(
@@ -224,7 +228,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
@@ -244,34 +248,36 @@ class AWSSnapshotService(BaseSnapshotService):
         return self.svc.get(snapshot_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 Snapshot Service %s", name)
-        return self.svc.find(filter_name='tag:Name', filter_value=name)
+        log.debug("Searching for AWS Snapshot Service %s", label)
+        return self.svc.find(filter_name='tag:Name', filter_value=label,
+                             OwnerIds=['self'])
 
     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
@@ -305,7 +311,7 @@ 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)
@@ -334,13 +340,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
-                                   })
+            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 AWSImageService(BaseImageService):
@@ -356,15 +380,15 @@ class AWSImageService(BaseImageService):
         return self.svc.get(image_id)
 
     def find(self, **kwargs):
+        # Filter by name or label
         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)
+        if name:
+            log.debug("Searching for AWS Image Service %s", name)
+            obj_list = self.svc.find(filter_name='name', filter_value=name)
+        else:
+            obj_list = self
+        filters = ['label']
+        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 [],
@@ -405,15 +429,15 @@ class AWSInstanceService(BaseInstanceService):
                                   cb_resource=AWSInstance,
                                   boto_collection_name='instances')
 
-    def create(self, name, image, vm_type, subnet, zone,
+    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 \
@@ -450,7 +474,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)
@@ -544,14 +568,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)
@@ -653,26 +677,26 @@ 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
 
 
@@ -698,30 +722,33 @@ 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):
+    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):
@@ -739,22 +766,29 @@ class AWSSubnetService(BaseSubnetService):
             # 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
+
+        # Refresh the list for the default label
+        snl = self.find(label=AWSSubnet.CB_DEFAULT_SUBNET_LABEL)
+
+        if len(snl) > 0:
+            return snl[0]
+
         # 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')
+        # Check if default net exists
+        default_nets = self.provider.networking.networks.find(
+            label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
+        if len(default_nets) > 0:
+            default_net = default_nets[0]
+        else:
+            default_net = self.provider.networking.networks.create(
+                label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
+                cidr_block='10.0.0.0/16')
         # 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)
+            sn = self.create(AWSSubnet.CB_DEFAULT_SUBNET_LABEL, default_net,
+                             '10.0.{0}.0/24'.format(i), z)
             if zone and zone == z.name:
                 default_sn = sn
         # No specific zone was supplied; return the last created subnet
@@ -782,27 +816,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

+ 34 - 5
cloudbridge/cloud/providers/azure/azure_client.py

@@ -2,6 +2,7 @@ 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
@@ -19,7 +20,8 @@ from msrestazure.azure_exceptions import CloudError
 import tenacity
 
 from cloudbridge.cloud.interfaces.exceptions import \
-    InvalidNameException, ProviderConnectionException, WaitStateException
+    DuplicateResourceException, InvalidLabelException, \
+    ProviderConnectionException, WaitStateException
 
 from . import helpers as azure_helpers
 
@@ -43,6 +45,10 @@ 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}',
@@ -78,6 +84,7 @@ 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'
@@ -315,7 +322,7 @@ class AzureClient(object):
                         self._storage_account = \
                             self.create_storage_account(self.storage_account,
                                                         storage_account_params)
-                    except CloudError as cloud_error2:
+                    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' \
@@ -343,7 +350,7 @@ class AzureClient(object):
                                    'azure/azure-resource-manager/resource-' \
                                    'manager-storage-account-name-errors\n' \
                                    % cloud_error2
-                            raise InvalidNameException(mess)
+                            raise InvalidLabelException(mess)
                         else:
                             raise cloud_error2
                 else:
@@ -405,7 +412,18 @@ class AzureClient(object):
         return self.blob_service.list_containers(prefix=prefix)
 
     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):
@@ -694,6 +712,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)
@@ -891,8 +917,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. \

+ 17 - 17
cloudbridge/cloud/providers/azure/helpers.py

@@ -1,23 +1,23 @@
 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_urls, original_url):

+ 1 - 1
cloudbridge/cloud/providers/azure/provider.py

@@ -128,7 +128,7 @@ class AzureCloudProvider(BaseCloudProvider):
                     self._azure_client.\
                         create_resource_group(self.resource_group,
                                               resource_group_params)
-                except CloudError as cloud_error2:
+                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' \

+ 182 - 143
cloudbridge/cloud/providers/azure/resources.py

@@ -3,7 +3,7 @@ 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
@@ -52,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)
 
@@ -67,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)
@@ -141,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 = '*'
@@ -177,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
@@ -240,9 +244,6 @@ class AzureBucketObject(BaseBucketObject):
 
     @property
     def name(self):
-        """
-        Get this object's name.
-        """
         return self._key.name
 
     @property
@@ -267,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
@@ -279,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)
@@ -291,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)
@@ -304,19 +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):
         """
         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.name, self._key.name)
+            self._container.id, self._key.id)
 
 
 class AzureBucket(BaseBucket):
@@ -363,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)
@@ -433,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)
@@ -465,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)
@@ -531,11 +536,12 @@ class AzureVolume(BaseVolume):
                     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):
         """
@@ -587,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)
@@ -618,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)
@@ -667,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):
@@ -698,39 +707,40 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``str``
         :return: ID for this instance as returned by the cloud middleware.
         """
-        if isinstance(self._image, GalleryImageReference):
+        if self.is_gallery_image:
             return azure_helpers.generate_urn(self._image)
         else:
             return self._image.id
 
+    @property
+    def name(self):
+        if self.is_gallery_image:
+            return azure_helpers.generate_urn(self._image)
+        else:
+            return self._image.name
+
     @property
     def resource_id(self):
-        if isinstance(self._image, GalleryImageReference):
+        if self.is_gallery_image:
             return azure_helpers.generate_urn(self._image)
         else:
             return self._image.id
 
     @property
-    def name(self):
-        """
-        Get the image name.
-
-        :rtype: ``str``
-        :return: Name for this image as returned by the cloud middleware.
-        """
-        if isinstance(self._image, GalleryImageReference):
+    def label(self):
+        if self.is_gallery_image:
             return azure_helpers.generate_urn(self._image)
         else:
-            return self._image.tags.get('Name', self._image.name)
+            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.
         """
-        if not isinstance(self._image, GalleryImageReference):
-            self.assert_valid_resource_name(value)
-            self._image.tags.update(Name=value)
+        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)
 
@@ -742,7 +752,7 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``str``
         :return: Description for this image as returned by the cloud middleware
         """
-        if isinstance(self._image, GalleryImageReference):
+        if self.is_gallery_image:
             return 'Public gallery image from the Azure Marketplace: '\
                     + self.name
         else:
@@ -753,8 +763,8 @@ class AzureMachineImage(BaseMachineImage):
         """
         Set the image description.
         """
-        if not isinstance(self._image, GalleryImageReference):
-            self._image.tags.update(Description=value)
+        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)
 
@@ -769,7 +779,7 @@ class AzureMachineImage(BaseMachineImage):
         :rtype: ``int``
         :return: The minimum disk size needed by this image
         """
-        if isinstance(self._image, GalleryImageReference):
+        if self.is_gallery_image:
             return 0
         else:
             return self._image.storage_profile.os_disk.disk_size_gb or 0
@@ -778,12 +788,12 @@ class AzureMachineImage(BaseMachineImage):
         """
         Delete this image
         """
-        if not isinstance(self._image, GalleryImageReference):
+        if not self.is_gallery_image:
             self._provider.azure_client.delete_image(self.id)
 
     @property
     def state(self):
-        if isinstance(self._image, GalleryImageReference):
+        if self.is_gallery_image:
             return MachineImageState.AVAILABLE
         else:
             return AzureMachineImage.IMAGE_STATE_MAP.get(
@@ -802,7 +812,7 @@ class AzureMachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        if not isinstance(self._image, dict):
+        if not self.is_gallery_image:
             try:
                 self._image = self._provider.azure_client.get_image(self.id)
                 self._state = self._image.provisioning_state
@@ -824,11 +834,7 @@ class AzureGatewayContainer(BaseGatewayContainer):
                                                       network)
 
     def get_or_create_inet_gateway(self, name=None):
-        if name:
-            AzureInternetGateway.assert_valid_resource_name(name)
         gateway = AzureInternetGateway(self._provider, None, self._network)
-        if name:
-            gateway.name = name
         return gateway
 
     def list(self, limit=None, marker=None):
@@ -856,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)
 
@@ -930,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):
@@ -967,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)
 
 
@@ -989,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
@@ -1093,23 +1108,46 @@ 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_' + self._subnet.name
+        return self._tag_name
+
+    @property
+    def resource_id(self):
+        return self._subnet.id
 
     @property
     def zone(self):
@@ -1203,27 +1241,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)
 
@@ -1297,10 +1342,13 @@ class AzureInstance(BaseInstance):
         """
         # Not tested for resource group images
         reference_dict = self._vm.storage_profile.image_reference.as_dict()
-        return ':'.join([reference_dict['publisher'],
-                         reference_dict['offer'],
-                         reference_dict['sku'],
-                         reference_dict['version']])
+        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):
@@ -1335,13 +1383,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
@@ -1354,7 +1402,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':
@@ -1370,10 +1419,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):
@@ -1592,6 +1642,10 @@ class AzureRouter(BaseRouter):
 
     @property
     def id(self):
+        return self._route_table.id
+
+    @property
+    def name(self):
         return self._route_table.name
 
     @property
@@ -1599,22 +1653,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)
@@ -1660,7 +1714,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 = ''
@@ -1669,25 +1722,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

+ 145 - 133
cloudbridge/cloud/providers/azure/services.py

@@ -22,7 +22,6 @@ from cloudbridge.cloud.interfaces.exceptions import \
 from cloudbridge.cloud.interfaces.resources import MachineImage, \
     Network, PlacementZone, Snapshot, Subnet, VMFirewall, VMType, Volume
 
-from . import helpers as azure_helpers
 from .resources import AzureBucket, \
     AzureInstance, AzureKeyPair, \
     AzureLaunchConfig, AzureMachineImage, AzureNetwork, \
@@ -67,15 +66,17 @@ class AzureVMFirewallService(BaseVMFirewallService):
                for fw in self.provider.azure_client.list_vm_firewall()]
         return ClientPagedResultList(self.provider, fws, limit, marker)
 
-    def create(self, name, description, network_id=None):
-        AzureVMFirewall.assert_valid_resource_name(name)
+    def create(self, label, description=None, network_id=None):
+        AzureVMFirewall.assert_valid_resource_label(label)
+        name = AzureVMFirewall._generate_name_from_label(label, "cb-fw")
         parameters = {"location": self.provider.region_name,
-                      'tags': {'Name': name}}
+                      "tags": {'Label': label}}
 
         if description:
             parameters['tags'].update(Description=description)
 
-        fw = self.provider.azure_client.create_vm_firewall(name, parameters)
+        fw = self.provider.azure_client.create_vm_firewall(name,
+                                                           parameters)
 
         # Add default rules to negate azure default rules.
         # See: https://github.com/CloudVE/cloudbridge/issues/106
@@ -86,7 +87,7 @@ class AzureVMFirewallService(BaseVMFirewallService):
             # only 0-4096 are allowed for custom rules
             rule.priority = rule.priority - 61440
             rule.access = "Deny"
-            self._provider.azure_client.create_vm_firewall_rule(
+            self.provider.azure_client.create_vm_firewall_rule(
                 fw.id, rule_name, rule)
 
         # Add a new custom rule allowing all outbound traffic to the internet
@@ -98,7 +99,7 @@ class AzureVMFirewallService(BaseVMFirewallService):
                       "destination_address_prefix": "Internet",
                       "access": "Allow",
                       "direction": "Outbound"}
-        result = self._provider.azure_client.create_vm_firewall_rule(
+        result = self.provider.azure_client.create_vm_firewall_rule(
             fw.id, "cb-default-internet-outbound", parameters)
         fw.security_rules.append(result)
 
@@ -106,18 +107,18 @@ class AzureVMFirewallService(BaseVMFirewallService):
         return cb_fw
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['label', 'name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        filters = {'Name': name}
-        fws = [AzureVMFirewall(self.provider, vm_firewall)
-               for vm_firewall in azure_helpers.filter_by_tag(
-               self.provider.azure_client.list_vm_firewall(), filters)]
-        return ClientPagedResultList(self.provider, fws)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
     def delete(self, group_id):
         self.provider.azure_client.delete_vm_firewall(group_id)
@@ -153,16 +154,18 @@ class AzureKeyPairService(BaseKeyPairService):
                                      data=results)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        key_pair = self.get(name)
         return ClientPagedResultList(self.provider,
-                                     [key_pair] if key_pair else [])
+                                     matches if matches else [])
 
     def create(self, name, public_key_material=None):
         AzureKeyPair.assert_valid_resource_name(name)
@@ -207,17 +210,18 @@ class AzureBucketService(BaseBucketService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        buckets = [AzureBucket(self.provider, bucket)
-                   for bucket in
-                   self.provider.azure_client.list_containers(prefix=name)]
-        return ClientPagedResultList(self.provider, buckets)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
     def list(self, limit=None, marker=None):
         """
@@ -233,7 +237,7 @@ class AzureBucketService(BaseBucketService):
         Create a new bucket.
         """
         AzureBucket.assert_valid_resource_name(name)
-        bucket = self.provider.azure_client.create_container(name.lower())
+        bucket = self.provider.azure_client.create_container(name)
         return AzureBucket(self.provider, bucket)
 
 
@@ -276,18 +280,18 @@ class AzureVolumeService(BaseVolumeService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        filters = {'Name': name}
-        cb_vols = [AzureVolume(self.provider, volume)
-                   for volume in azure_helpers.filter_by_tag(
-                   self.provider.azure_client.list_disks(), filters)]
-        return ClientPagedResultList(self.provider, cb_vols)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
     def list(self, limit=None, marker=None):
         """
@@ -298,18 +302,22 @@ class AzureVolumeService(BaseVolumeService):
         return ClientPagedResultList(self.provider, cb_vols,
                                      limit=limit, marker=marker)
 
-    def create(self, name, size, zone=None, snapshot=None, description=None):
+    def create(self, label, size, zone=None, description=None,
+               snapshot=None):
         """
         Creates a new volume.
         """
-        AzureVolume.assert_valid_resource_name(name)
+        AzureVolume.assert_valid_resource_label(label)
+        disk_name = AzureVolume._generate_name_from_label(label, "cb-vol")
+        tags = {'Label': label}
+
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         snapshot = (self.provider.storage.snapshots.get(snapshot)
                     if snapshot and isinstance(snapshot, str) else snapshot)
-        disk_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
-        tags = {'Name': name}
+
         if description:
             tags.update(Description=description)
+
         if snapshot:
             params = {
                 'location':
@@ -332,7 +340,8 @@ class AzureVolumeService(BaseVolumeService):
                 'creation_data': {
                     'create_option': DiskCreateOption.empty
                 },
-                'tags': tags}
+                'tags': tags
+            }
 
             disk = self.provider.azure_client.create_empty_disk(disk_name,
                                                                 params)
@@ -360,18 +369,18 @@ class AzureSnapshotService(BaseSnapshotService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        filters = {'Name': name}
-        cb_snapshots = [AzureSnapshot(self.provider, snapshot)
-                        for snapshot in azure_helpers.filter_by_tag(
-                        self.provider.azure_client.list_snapshots(), filters)]
-        return ClientPagedResultList(self.provider, cb_snapshots)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
     def list(self, limit=None, marker=None):
         """
@@ -382,20 +391,20 @@ class AzureSnapshotService(BaseSnapshotService):
                  self.provider.azure_client.list_snapshots()]
         return ClientPagedResultList(self.provider, snaps, limit, marker)
 
-    def create(self, name, volume, description=None):
+    def create(self, label, volume, description=None):
         """
         Creates a new snapshot of a given volume.
         """
-        AzureSnapshot.assert_valid_resource_name(name)
-        volume = (self.provider.storage.volumes.get(volume)
-                  if isinstance(volume, str) else volume)
-
-        tags = {'Name': name}
-        snapshot_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
-
+        AzureSnapshot.assert_valid_resource_label(label)
+        snapshot_name = AzureSnapshot._generate_name_from_label(label,
+                                                                "cb-snap")
+        tags = {'Label': label}
         if description:
             tags.update(Description=description)
 
+        volume = (self.provider.storage.volumes.get(volume)
+                  if isinstance(volume, str) else volume)
+
         params = {
             'location': self.provider.azure_client.region_name,
             'creation_data': {
@@ -440,14 +449,14 @@ class AzureInstanceService(BaseInstanceService):
     def __init__(self, provider):
         super(AzureInstanceService, self).__init__(provider)
 
-    def create(self, name, image, vm_type, subnet=None, zone=None,
+    def create(self, label, image, vm_type, subnet=None, zone=None,
                key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None, **kwargs):
 
-        instance_name = name.replace("_", "-") if name \
-            else "{0} - {1}".format("cb", uuid.uuid4())
+        AzureInstance.assert_valid_resource_label(label)
 
-        AzureInstance.assert_valid_resource_name(instance_name)
+        instance_name = AzureInstance._generate_name_from_label(label,
+                                                                "cb-ins")
 
         image = (image if isinstance(image, AzureMachineImage) else
                  self.provider.compute.images.get(image))
@@ -478,7 +487,7 @@ class AzureInstanceService(BaseInstanceService):
                                                        instance_name, zone_id)
 
         nic_params = {
-            'location': self._provider.region_name,
+            'location': self.provider.region_name,
             'ip_configurations': [{
                 'name': instance_name + '_ip_config',
                 'private_ip_allocation_method': 'Dynamic',
@@ -504,15 +513,15 @@ class AzureInstanceService(BaseInstanceService):
         # Key_pair is mandatory in azure and it should not be None.
         temp_key_pair = None
         if key_pair:
-            key_pair = (self.provider.security.key_pairs.get(key_pair)
-                        if isinstance(key_pair, str) else key_pair)
+            key_pair = (key_pair if isinstance(key_pair, AzureKeyPair)
+                        else self.provider.security.key_pairs.get(key_pair))
         else:
             # Create a temporary keypair if none is provided to keep Azure
             # happy, but the private key will be discarded, so it'll be all
             # but useless. However, this will allow an instance to be launched
             # without specifying a keypair, so users may still be able to login
             # if they have a preinstalled keypair/password baked into the image
-            temp_kp_name = "".join(["cb_default_kp_",
+            temp_kp_name = "".join(["cb-default-kp-",
                                    str(uuid.uuid5(uuid.NAMESPACE_OID,
                                                   instance_name))[-6:]])
             key_pair = self.provider.security.key_pairs.create(
@@ -520,7 +529,7 @@ class AzureInstanceService(BaseInstanceService):
             temp_key_pair = key_pair
 
         params = {
-            'location': zone_id or self._provider.region_name,
+            'location': zone_id or self.provider.region_name,
             'os_profile': {
                 'admin_username': self.provider.vm_default_user_name,
                 'computer_name': instance_name,
@@ -545,7 +554,7 @@ class AzureInstanceService(BaseInstanceService):
                 }]
             },
             'storage_profile': storage_profile,
-            'tags': {'Name': name}
+            'tags': {'Label': label}
         }
 
         for disk_def in storage_profile.get('data_disks', []):
@@ -556,7 +565,7 @@ class AzureInstanceService(BaseInstanceService):
             params['os_profile']['custom_data'] = str(custom_data, 'utf-8')
 
         if not temp_key_pair:
-            params['tags'].update(Key_Pair=key_pair.name)
+            params['tags'].update(Key_Pair=key_pair.id)
 
         try:
             vm = self.provider.azure_client.create_vm(instance_name, params)
@@ -575,7 +584,7 @@ class AzureInstanceService(BaseInstanceService):
                 temp_key_pair.delete()
         return AzureInstance(self.provider, vm)
 
-    def _resolve_launch_options(self, name, subnet=None, zone_id=None,
+    def _resolve_launch_options(self, inst_name, subnet=None, zone_id=None,
                                 vm_firewalls=None):
         if subnet:
             # subnet's zone takes precedence
@@ -595,7 +604,8 @@ class AzureInstanceService(BaseInstanceService):
 
             if len(vm_firewalls) > 1:
                 new_fw = self.provider.security.vm_firewalls.\
-                    create('{0}-fw'.format(name), 'Merge vm firewall {0}'.
+                    create(label='{0}-fw'.format(inst_name),
+                           description='Merge vm firewall {0}'.
                            format(','.join(vm_firewalls_ids)))
 
                 for fw in vm_firewalls:
@@ -735,18 +745,18 @@ class AzureInstanceService(BaseInstanceService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # 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,
+                                                           ", ".join(filters)))
 
-        filtr = {'Name': name}
-        instances = [AzureInstance(self.provider, inst)
-                     for inst in azure_helpers.filter_by_tag(
-                     self.provider.azure_client.list_vm(), filtr)]
-        return ClientPagedResultList(self.provider, instances)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
 
 class AzureImageService(BaseImageService):
@@ -766,30 +776,26 @@ class AzureImageService(BaseImageService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        filters = {'Name': name}
-        cb_images = [AzureMachineImage(self.provider, image)
-                     for image in azure_helpers.filter_by_tag(
-                     self.provider.azure_client.list_images(), filters)]
-        # All gallery image properties (id, resource_id, name) are the URN
-        # Improvement: wrap the filters by publisher, offer, etc...
-        cb_images.extend([AzureMachineImage(self.provider, image) for image
-                          in self.provider.azure_client.list_gallery_refs()
-                          if azure_helpers.generate_urn(image) == name])
-        return ClientPagedResultList(self.provider, cb_images)
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
 
-    def list(self, limit=None, marker=None):
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+    def list(self, filter_by_owner=True, limit=None, marker=None):
         """
         List all images.
         """
         azure_images = self.provider.azure_client.list_images()
-        azure_gallery_refs = self.provider.azure_client.list_gallery_refs()
+        azure_gallery_refs = self.provider.azure_client.list_gallery_refs() \
+            if not filter_by_owner else []
         cb_images = [AzureMachineImage(self.provider, img)
                      for img in azure_images + azure_gallery_refs]
         return ClientPagedResultList(self.provider, cb_images,
@@ -859,36 +865,31 @@ class AzureNetworkService(BaseNetworkService):
                                      limit=limit, marker=marker)
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        filters = {'Name': name}
-        networks = [
-            AzureNetwork(self.provider, network) for network
-            in azure_helpers.filter_by_tag(
-                self.provider.azure_client.list_networks(), filters)]
-        return ClientPagedResultList(self.provider, networks)
-
-    def create(self, name, cidr_block):
-        # Azure requires CIDR block to be specified when creating a network
-        # so set a default one and use the largest allowed netmask.
-        network_name = AzureNetwork.CB_DEFAULT_NETWORK_NAME
-        if name:
-            network_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
 
-        AzureNetwork.assert_valid_resource_name(network_name)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
+    def create(self, label, cidr_block):
+        AzureNetwork.assert_valid_resource_label(label)
         params = {
             'location': self.provider.azure_client.region_name,
             'address_space': {
                 'address_prefixes': [cidr_block]
             },
-            'tags': {'Name': name or AzureNetwork.CB_DEFAULT_NETWORK_NAME}
+            'tags': {'Label': label}
         }
+
+        network_name = AzureNetwork._generate_name_from_label(label, 'cb-net')
+
         az_network = self.provider.azure_client.create_network(network_name,
                                                                params)
         cb_network = AzureNetwork(self.provider, az_network)
@@ -969,7 +970,7 @@ class AzureSubnetService(BaseSubnetService):
                         net.id
                     ))
                 except CloudError as cloud_error:
-                    if cloud_error.error.error == "ResourceNotFound":
+                    if "NotFound" in cloud_error.error.error:
                         log.exception(cloud_error)
                     else:
                         raise cloud_error
@@ -980,23 +981,30 @@ class AzureSubnetService(BaseSubnetService):
 
     def find(self, network=None, **kwargs):
         obj_list = self._list_subnets(network)
-        filters = ['name']
+        filters = ['label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
 
-    def create(self, network, cidr_block, name=None, **kwargs):
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+    def create(self, label, network, cidr_block, **kwargs):
         """
         Create subnet
         """
-        AzureSubnet.assert_valid_resource_name(name)
+        # Although Subnet doesn't support labels, we use the parent Network's
+        # tags to track the subnet's labels
+        AzureSubnet.assert_valid_resource_label(label)
+        subnet_name = AzureSubnet._generate_name_from_label(label, "cb-sn")
+
         network_id = network.id \
             if isinstance(network, Network) else network
 
-        if not name:
-            subnet_name = AzureSubnet.CB_DEFAULT_SUBNET_NAME
-        else:
-            subnet_name = name
-
         subnet_info = self.provider.azure_client\
             .create_subnet(
                 network_id,
@@ -1006,28 +1014,30 @@ class AzureSubnetService(BaseSubnetService):
                 }
             )
 
-        return AzureSubnet(self.provider, subnet_info)
+        subnet = AzureSubnet(self.provider, subnet_info)
+        subnet.label = label
+        return subnet
 
     def get_or_create_default(self, zone):
         default_cidr = '10.0.1.0/24'
 
         # No provider-default Subnet exists, look for a library-default one
-        matches = self.find(name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
+        matches = self.find(label=AzureSubnet.CB_DEFAULT_SUBNET_LABEL)
         if matches:
             return matches[0]
 
         # No provider-default Subnet exists, try to create it (net + subnets)
         networks = self.provider.networking.networks.find(
-            name=AzureNetwork.CB_DEFAULT_NETWORK_NAME)
+            label=AzureNetwork.CB_DEFAULT_NETWORK_LABEL)
 
         if networks:
             network = networks[0]
         else:
             network = self.provider.networking.networks.create(
-                AzureNetwork.CB_DEFAULT_NETWORK_NAME, '10.0.0.0/16')
+                AzureNetwork.CB_DEFAULT_NETWORK_LABEL, '10.0.0.0/16')
 
-        subnet = self.create(network, default_cidr,
-                             name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
+        subnet = self.create(AzureSubnet.CB_DEFAULT_SUBNET_LABEL, network,
+                             default_cidr)
         return subnet
 
     def delete(self, subnet):
@@ -1049,19 +1059,18 @@ class AzureRouterService(BaseRouterService):
             return None
 
     def find(self, **kwargs):
-        name = kwargs.pop('name', None)
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'name'))
-
-        filters = {'Name': name}
-        routes = [AzureRouter(self.provider, route)
-                  for route in azure_helpers.filter_by_tag(
-                  self.provider.azure_client.list_route_tables(), filters)]
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
 
-        return ClientPagedResultList(self.provider, routes)
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
     def list(self, limit=None, marker=None):
         routes = [AzureRouter(self.provider, route)
@@ -1071,10 +1080,13 @@ class AzureRouterService(BaseRouterService):
                                      routes,
                                      limit=limit, marker=marker)
 
-    def create(self, name, network):
-        AzureRouter.assert_valid_resource_name(name)
+    def create(self, label, network):
+        AzureRouter.assert_valid_resource_label(label)
+        router_name = AzureRouter._generate_name_from_label(label, "cb-router")
+
         parameters = {"location": self.provider.region_name,
-                      'tags': {'Name': name}}
+                      "tags": {'Label': label}}
+
         route = self.provider.azure_client. \
-            create_route_table(name, parameters)
+            create_route_table(router_name, parameters)
         return AzureRouter(self.provider, route)

+ 124 - 69
cloudbridge/cloud/providers/openstack/resources.py

@@ -99,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):
         """
@@ -280,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):
@@ -417,22 +441,25 @@ 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)
+        name = self._generate_name_from_label(label, 'cb-img')
 
         image_id = self._os_instance.create_image(name)
-        return OpenStackMachineImage(
+        img = OpenStackMachineImage(
             self._provider, self._provider.compute.images.get(image_id))
+        img.label = label
+        return img
 
     def _get_fip(self, floating_ip):
         """Get a floating IP object based on the supplied ID."""
@@ -508,8 +535,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):
@@ -557,22 +583,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):
@@ -628,14 +658,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):
         """
@@ -683,22 +713,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):
@@ -750,13 +784,14 @@ 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)
+        name = self._generate_name_from_label(vol_label, 'cb-vol')
         size = size if size else self._snapshot.size
         os_vol = self._provider.cinder.volumes.create(
-            size, name=vol_name, availability_zone=placement,
+            size, name=name, availability_zone=placement,
             snapshot_id=self._snapshot.id)
         cb_vol = OpenStackVolume(self._provider, os_vol)
-        cb_vol.name = vol_name
+        cb_vol.label = vol_label
         return cb_vol
 
 
@@ -774,7 +809,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)
@@ -784,9 +819,6 @@ class OpenStackGatewayContainer(BaseGatewayContainer):
 
     def get_or_create_inet_gateway(self, name=None):
         """For OS, inet gtw is any net that has `external` property set."""
-        if name:
-            OpenStackInternetGateway.assert_valid_resource_name(name)
-
         external_nets = (n for n in self._provider.networking.networks
                          if n.external)
         for net in external_nets:
@@ -833,16 +865,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
@@ -910,16 +946,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
@@ -1032,16 +1072,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):
@@ -1112,14 +1156,18 @@ class OpenStackInternetGateway(BaseInternetGateway):
 
     @property
     def name(self):
-        return self._gateway_net.get('name', None)
+        return self.id
 
-    @name.setter
+    @property
+    def label(self):
+        return self._gateway_net.tags.get('gateway_name', None)
+
+    @label.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}})
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        self._provider.neutron_client.add_tag(
+            'network', self.id, {'gateway_name': value or ""})
         self.refresh()
 
     @property
@@ -1173,14 +1221,21 @@ class OpenStackVMFirewall(BaseVMFirewall):
 
     @property
     def name(self):
+        """
+        Return the name of this VM firewall.
+        """
+        return self.id
+
+    @property
+    def label(self):
         return self._vm_firewall.name
 
-    @name.setter
+    @label.setter
     # pylint:disable=arguments-differ
-    def name(self, value):
-        self.assert_valid_resource_name(value)
-        self._provider.os_conn.network.update_security_group(self.id,
-                                                             name=value)
+    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
@@ -1319,7 +1374,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):
@@ -1432,7 +1487,7 @@ class OpenStackBucket(BaseBucket):
 
     @property
     def name(self):
-        return self._bucket.get("name")
+        return self.id
 
     @property
     def objects(self):

+ 84 - 86
cloudbridge/cloud/providers/openstack/services.py

@@ -1,9 +1,7 @@
 """
 Services implemented by the OpenStack provider.
 """
-import fnmatch
 import logging
-import re
 
 from cinderclient.exceptions import NotFound as CinderNotFound
 
@@ -14,6 +12,8 @@ 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
@@ -47,7 +47,6 @@ from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 from .resources import OpenStackBucket
 from .resources import OpenStackInstance
-from .resources import OpenStackInternetGateway
 from .resources import OpenStackKeyPair
 from .resources import OpenStackMachineImage
 from .resources import OpenStackNetwork
@@ -211,27 +210,28 @@ 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)
+    def create(self, label, description, network_id):
+        OpenStackVMFirewall.assert_valid_resource_label(label)
+        name = OpenStackVMFirewall._generate_name_from_label(label, 'cb-fw')
         log.debug("Creating OpenStack VM Firewall with the params: "
-                  "[name: %s network id: %s description: %s]", name,
+                  "[label: %s network id: %s description: %s]", label,
                   network_id, description)
         sg = self.provider.os_conn.network.create_security_group(
-            name=name, description=description)
+            name=name, description=description or name)
         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)
@@ -262,21 +262,9 @@ class OpenStackImageService(BaseImageService):
             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)
+        filters = ['name', 'label']
+        obj_list = self
+        return cb_helpers.generic_find(filters, kwargs, obj_list)
 
     def list(self, filter_by_owner=True, limit=None, marker=None):
         """
@@ -352,15 +340,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 +370,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 +408,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 +439,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,9 +510,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)
+        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 OpenStackRegionService(BaseRegionService):
@@ -596,12 +588,12 @@ class OpenStackInstanceService(BaseInstanceService):
     def __init__(self, provider):
         super(OpenStackInstanceService, self).__init__(provider)
 
-    def create(self, name, image, vm_type, subnet, zone,
+    def create(self, label, image, vm_type, subnet, zone,
                key_pair=None, vm_firewalls=None, user_data=None,
                launch_config=None,
                **kwargs):
         """Create a new virtual machine instance."""
-        OpenStackInstance.assert_valid_resource_name(name)
+        OpenStackInstance.assert_valid_resource_label(label)
 
         image_id = image.id if isinstance(image, MachineImage) else image
         vm_size = vm_type.id if \
@@ -632,7 +624,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 +632,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 +653,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 +726,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(
@@ -809,29 +804,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,40 +852,42 @@ class OpenStackSubnetService(BaseSubnetService):
         return ClientPagedResultList(self.provider, subnets,
                                      limit=limit, marker=marker)
 
-    def create(self, name, network, cidr_block, zone):
+    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)
+        name = OpenStackSubnet._generate_name_from_label(label, 'cb-subnet')
         network_id = (network.id if isinstance(network, OpenStackNetwork)
                       else network)
         subnet_info = {'name': name, '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)
+        cb_subnet.label = label
+        return cb_subnet
 
     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,
+                label=OpenStackNetwork.CB_DEFAULT_NETWORK_LABEL,
                 cidr_block='10.0.0.0/16')
-            sn = net.create_subnet(name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME,
-                                   cidr_block='10.0.0.0/24')
+            sn = net.create_subnet(
+                label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
+                cidr_block='10.0.0.0/24')
             router = self.provider.networking.routers.create(
-                network=net, name=OpenStackRouter.CB_DEFAULT_ROUTER_NAME)
+                network=net, label=OpenStackRouter.CB_DEFAULT_ROUTER_LABEL)
             router.attach_subnet(sn)
-            gteway = net.gateways.get_or_create_inet_gateway(
-                        OpenStackInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME)
+            gteway = net.gateways.get_or_create_inet_gateway()
             router.attach_gateway(gteway)
             return sn
         except NeutronClientException:
@@ -924,11 +922,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 +934,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'))

+ 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 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
 ----------------------------
 

+ 20 - 11
docs/getting_started.rst

@@ -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()
@@ -116,10 +116,10 @@ attaching an internet gateway to the subnet via a router.
 
 .. code-block:: python
 
-    net = provider.networking.networks.create(
-        name='my-network', cidr_block='10.0.0.0/16')
-    sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28')
-    router = 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='my-gateway')
     router.attach_gateway(gateway)
@@ -135,7 +135,8 @@ a private network.
 
     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=net.id)
     fw.rules.create(TrafficDirection.INBOUND, 'tcp', 22, 22, '0.0.0.0/0')
 
 Launch an instance
@@ -151,7 +152,7 @@ also add the network interface as a launch argument.
                       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,
+        image=img, vm_type=vm_type, label='cloudbridge-intro',
         subnet=sn, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
@@ -190,10 +191,10 @@ From the command prompt, you can now ssh into the instance
 Get a resource
 --------------
 When a resource already exists, a reference to it can be retrieved using either
-its ID or name. It is important to note that while IDs are unique, multiple
-resources of the same type could use the same name on some providers, thus the
+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 explicitely
+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.
 
 .. code-block:: python
@@ -206,19 +207,24 @@ listed in order to help map each resource with the service that handles it.
     # Network
     net = provider.networking.networks.get('network ID')
     net_list = provider.networking.networks.find(name='my-network')
+    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(name='my-subnet')
+    sn_list = provider.networking.subnets.find(label='my-subnet')
     # Known network
     sn_list = provider.networking.subnets.find(network=net.id, name='my-subnet')
+    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(name='my-router')
+    router_list = provider.networking.routers.find(label='my-router')
     router = router_list[0]
 
     # Gateway
@@ -228,18 +234,21 @@ listed in order to help map each resource with the service that handles it.
     fip = gateway.floating_ips.get('FloatingIP ID')
     # Find using public IP address
     fip_list = gateway.floating_ips.find(public_ip='IP address')
-    # Find using tagged name
+    # Find using name or tag
     fip_list = net.gateways.floating_ips.find(name='my-fip')
+    fip_list = net.gateways.floating_ips.find(label='my-fip')
     fip = fip_list[0]
 
     # Firewall
     fw = provider.security.vm_firewalls.get('firewall ID')
     fw_list = provider.security.vm_firewalls.find(name='cloudbridge-intro')
+    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(name='cloudbridge-intro')
+    inst_list = provider.compute.instances.list(label='cloudbridge-intro')
     inst = inst_list[0]
 
 

二进制
docs/topics/captures/az-label-dash.png


二进制
docs/topics/captures/az-net-id.png


二进制
docs/topics/captures/az-net-label.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>

+ 121 - 0
docs/topics/dashboard.rst

@@ -0,0 +1,121 @@
+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 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.
+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. However, when no conceptually comparable property
+exists for a given resource in a particular provider, the ID is returned
+instead, as is the case for OpenStack resources. When the name can be
+determined by a user at resource creation, either the name parameter will be
+used for resources that support it, or the label will be used, when provided
+as a prefix, with an appended uuid to ensure that the name remains unique.
+The `label` property, conversely, is a changeable 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. It
+is however important to note that not all resources support labels. When
+supported, labels given at creation will also be used as a prefix to the name.
+
+Properties per Resource per Provider
+------------------------------------
+The sections below will present a summary table detailing the cloudbridge
+properties implemented for each resource, and their corresponding value in
+the provider's dashboard.
+
+Azure
+-----
++-----------------------------------+-------+---------------+---------------+
+| CloudServiceType                 	| CB_ID	| CB_Name      	| CB_Label  	|
++===================================+=======+===============+===============+
+| Instance                         	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| MachineImage (Private)           	| ID   	| Name       	| Tags:Label 	|
+| MachineImage (Gallery Reference) 	| URN  	| URN        	| URN        	|
++-----------------------------------+-------+---------------+---------------+
+| Network                          	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| Subnet                           	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| FloatingIP                       	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| Router                           	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| InternetGateway                  	| None 	| None       	| -          	|
++-----------------------------------+-------+---------------+---------------+
+| Volume                           	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| Snapshot                         	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| KeyPair                          	| Name 	| Name       	| -          	|
++-----------------------------------+-------+---------------+---------------+
+| VMFirewall                       	| ID   	| Name       	| Tags:Label 	|
++-----------------------------------+-------+---------------+---------------+
+| VMFirewallRule                   	| ID   	| Name       	| -          	|
++-----------------------------------+-------+---------------+---------------+
+| Bucket                           	| Name 	| Name       	| -          	|
++-----------------------------------+-------+---------------+---------------+
+| BucketObject                     	| Name 	| Name       	| -          	|
++-----------------------------------+-------+---------------+---------------+
+
+One of the major discrepancies in Azure is the non-existence of an Internet
+Gateway. In fact, Azure resources are automatically exposed to the internet,
+and thus an internet gateway object is not necessary for this purpose. Thus,
+a gateway object created through cloudbridge in Azure will not appear on the
+dashboard, as a cloudbridge-level wrapper object is returned when trying to
+create or get a gateway, but no object corresponds to that concept in Azure.
+For a succinct comparison between AWS Gateways and Azure, see:
+https://social.msdn.microsoft.com/Forums/en-US/
+814ccee0-9fbb-4c04-8135-49d0aaea5f38/
+equivalent-of-aws-internet-gateways-in-azure?
+forum=WAVirtualMachinesVirtualNetwork
+
+
+.. 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' in
+   Azure. By default, this label will appear in the tags column, but can also
+   be made into its own column, following the button indicated 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 label 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.
+
+.. 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 residing in a resource group, but rather reside
+   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`.
+

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

@@ -1,37 +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_.)
-
-- **Name property updates will result in cloud-dependent code.**
-
-  Some providers (e.g., GCE, Azure) do not allow names of resources to be
-  changed after a resource has been created. Similarly, AWS does not allow VM
-  firewall (i.e., security group) names to be changed. Providers seem to be
-  gravitating toward use of tags (or labels) to support arbitrary naming and
-  name changes. Yet, OpenStack for example, does not have a concept of resource
-  tags so CloudBridge cannot 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
-  is challenging to achieve with resource renaming. With that, CloudBridge will
-  support resource renaming to the best extent possible and balance between the
-  use of resource name property and resource tags. However, because of the
-  inconsistency of rename functionality across the providers, using the rename
-  capabilities within CloudBridge will lead to cloud-dependent code. (Related to
-  131_.)
-
-  .. _63: https://github.com/CloudVE/cloudbridge/issues/63
-  .. _131: https://github.com/CloudVE/cloudbridge/issues/131

+ 118 - 0
docs/topics/design_decisions.rst

@@ -0,0 +1,118 @@
+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_.)
+
+- **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, 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. The `id` is a unique identifier for an object, always
+  auto-generated. The `name` is a read-only, user-friendly value which is
+  suitable for display to the end-user. The `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/

+ 1 - 1
docs/topics/networking.rst

@@ -94,7 +94,7 @@ The additional step that's required here is to assign a floating IP to the VM:
 
     router = provider.networking.routers.create(network=net, name='my-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway(net, name)
+    gateway = net.gateways.get_or_create_inet_gateway(name)
     router.attach_gateway(gateway)
 
     fip = provider.networking.floating_ips.create()

+ 19 - 17
docs/topics/setup.rst

@@ -164,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.
+======================== ======================================================

+ 4 - 2
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
 ~~~~~~~~~~~~~~~~~~~~~~~

+ 1 - 0
setup.cfg

@@ -3,6 +3,7 @@ branch = True
 source = cloudbridge
 omit =
   cloudbridge/cloud/interfaces/*
+  cloudbridge/__init__.py
 
 [nosetests]
 with-coverage=1

+ 1 - 1
setup.py

@@ -23,7 +23,7 @@ REQS_BASE = [
     'six>=1.10.0',
     'tenacity>=4.12.0'
 ]
-REQS_AWS = ['boto3']
+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.

+ 23 - 30
test/helpers/__init__.py

@@ -86,10 +86,10 @@ TEST_DATA_CONFIG = {
         "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
     "OpenStackCloudProvider": {
-        "image": get_env('CB_IMAGE_OS',
-                         'acb53109-941f-4593-9bf8-4a53cb9e0739'),
-        "vm_type": get_env('CB_VM_TYPE_OS', 'm1.tiny'),
-        "placement": get_env('CB_PLACEMENT_OS', 'zone-r1'),
+        "image": os.environ.get('CB_IMAGE_OS',
+                                'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
+        "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
+        "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
     },
     "AzureCloudProvider": {
         "placement":
@@ -113,16 +113,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):
@@ -135,34 +131,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'),
@@ -174,12 +167,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,
@@ -200,8 +193,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):
@@ -212,7 +205,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):
@@ -243,7 +236,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

+ 106 - 35
test/helpers/standard_interface_tests.py

@@ -7,8 +7,11 @@ This includes:
 """
 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
 
@@ -27,14 +30,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)
@@ -66,18 +76,26 @@ 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):
+def check_find_non_existent(test, service, obj):
     # check find
-    find_objs = service.find(name="random_imagined_obj_name")
+    if isinstance(obj, LabeledCloudResource):
+        find_objs = service.find(label="random_imagined_obj_name")
+    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"
@@ -100,6 +118,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()
@@ -113,6 +135,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,
@@ -120,31 +148,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()
-        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):
@@ -158,7 +199,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)
 
@@ -178,27 +219,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):
     """
@@ -219,14 +289,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.
 
@@ -247,17 +317,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)

+ 35 - 42
test/test_block_store_service.py

@@ -1,5 +1,4 @@
 import time
-import uuid
 
 import six
 
@@ -26,10 +25,9 @@ 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):
@@ -39,27 +37,26 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                              terminal_states=[VolumeState.ERROR])
 
         sit.check_crud(self, self.provider.storage.volumes, Volume,
-                       "cb_createvol", create_vol, cleanup_vol)
+                       "cb-createvol", create_vol, cleanup_vol)
 
     @helpers.skipIfNoService(['storage.volumes'])
     def test_attach_detach_volume(self):
         """
         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')
@@ -76,21 +73,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(
@@ -121,11 +117,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])
-                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,17 +135,16 @@ 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):
                 if snap:
@@ -158,34 +153,33 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                                   terminal_states=[SnapshotState.ERROR])
 
             sit.check_crud(self, self.provider.storage.snapshots, Snapshot,
-                           "cb_snap", create_snap, cleanup_snap)
+                           "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)):
                 time.sleep(15)  # Or get SnapshotCreationPerVolumeRateExceeded
             sit.check_crud(self, self.provider.storage.snapshots, Snapshot,
-                           "cb_snaptwo", create_snap2, cleanup_snap)
+                           "cb-snaptwo", create_snap2, cleanup_snap)
 
     @helpers.skipIfNoService(['storage.snapshots'])
     def test_snapshot_properties(self):
         """
         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):
                 if snap:
@@ -207,17 +201,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()
-                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(test_snap.name)
+                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()):

+ 55 - 56
test/test_compute_service.py

@@ -21,16 +21,15 @@ 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):
@@ -46,15 +45,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:
@@ -67,28 +64,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"
@@ -107,8 +104,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],
@@ -217,21 +214,20 @@ 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):
                 if snap:
@@ -277,45 +273,49 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 for _ in range(vm_type.num_ephemeral_disks):
                     lc.add_ephemeral_device()
 
-                net, subnet = helpers.create_test_network(self.provider, name)
+                subnet = helpers.get_or_create_default_subnet(
+                    self.provider)
+
+                inst = helpers.create_test_instance(
+                    self.provider,
+                    label,
+                    subnet=subnet,
+                    launch_config=lc)
 
                 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
+                                            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)
@@ -334,8 +334,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(name=label)
 
             def cleanup_router(router, gateway):
                 with helpers.cleanup_action(lambda: router.delete()):
@@ -346,7 +346,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)
                 # check whether adding an elastic ip works
                 fip = gateway.floating_ips.create()

+ 44 - 11
test/test_image_service.py

@@ -1,5 +1,5 @@
 from cloudbridge.cloud.interfaces import MachineImageState
-from cloudbridge.cloud.interfaces.resources import MachineImage
+from cloudbridge.cloud.interfaces.resources import Instance, MachineImage
 
 from test import helpers
 from test.helpers import ProviderTestBase
@@ -16,18 +16,18 @@ 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):
             if img:
@@ -40,13 +40,46 @@ class CloudImageServiceTestCase(ProviderTestBase):
             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,
+                           "cb-listimg", create_img, cleanup_img,
                            extra_test_func=extra_tests)

+ 40 - 41
test/test_network_service.py

@@ -16,23 +16,23 @@ 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):
             if net:
                 self.provider.networking.networks.delete(network_id=net.id)
 
         sit.check_crud(self, self.provider.networking.networks, Network,
-                       "cb_crudnetwork", create_net, cleanup_net)
+                       "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()
         ):
@@ -48,9 +48,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Network CIDR %s does not contain the expected value."
                 % net.cidr_block)
 
-            cidr = '10.0.1.0/24'
+            cidr = '10.0.20.0/24'
             sn = net.create_subnet(
-                name=subnet_name, cidr_block=cidr,
+                label=subnet_label, cidr_block=cidr,
                 zone=helpers.get_provider_test_data(self.provider,
                                                     'placement'))
             with helpers.cleanup_action(lambda: sn.delete()):
@@ -69,10 +69,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,
@@ -82,11 +87,12 @@ 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'))
 
@@ -94,21 +100,14 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             if subnet:
                 self.provider.networking.subnets.delete(subnet=subnet)
 
-        net_name = 'cb_crudsubnet-{0}'.format(helpers.get_uuid())
-        net = self.provider.networking.networks.create(
-            name=net_name, cidr_block='10.0.0.0/16')
-        with helpers.cleanup_action(
-            lambda:
-                self.provider.networking.networks.delete(network_id=net.id)
-        ):
-            sit.check_crud(self, self.provider.networking.subnets, Subnet,
-                           "cb_crudsubnet", create_subnet, cleanup_subnet)
+        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
 
@@ -117,18 +116,18 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 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,
+                           "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)
@@ -151,13 +150,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
@@ -166,11 +165,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 = '10.0.1.0/24'
-            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'))
 
@@ -188,7 +187,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 #                     router.id, router.network_id))
 
             router.attach_subnet(sn)
-            gteway = net.gateways.get_or_create_inet_gateway(name)
+            gteway = net.gateways.get_or_create_inet_gateway(name=label)
             router.attach_gateway(gteway)
             # TODO: add a check for routes after that's been implemented
 

+ 2 - 3
test/test_object_life_cycle.py

@@ -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

+ 14 - 25
test/test_object_store_service.py

@@ -1,14 +1,13 @@
 import filecmp
 import os
 import tempfile
-import uuid
 from datetime import datetime
 from io import BytesIO
 from unittest import skip
 
 import requests
 
-from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
+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
@@ -36,25 +35,14 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             if bucket:
                 bucket.delete()
 
-        with self.assertRaises(InvalidNameException):
-            # underscores are not allowed in bucket names
-            create_bucket("cb_bucket")
-
-        with self.assertRaises(InvalidNameException):
-            # names of length less than 3 should raise an exception
-            create_bucket("cb")
-
-        with self.assertRaises(InvalidNameException):
-            # names of length greater than 63 should raise an exception
-            create_bucket("a" * 64)
-
-        with self.assertRaises(InvalidNameException):
-            # bucket name cannot be an IP address
-            create_bucket("197.10.100.42")
+        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):
@@ -74,11 +62,11 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 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,
-                           "cb_bucketobj", create_bucket_obj,
+                           "cb-bucketobj", create_bucket_obj,
                            cleanup_bucket_obj, skip_name_check=True)
 
     @helpers.skipIfNoService(['storage.buckets'])
@@ -88,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
@@ -146,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()):
@@ -169,7 +157,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.buckets'])
     def test_generate_url(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()):
@@ -191,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()):
@@ -222,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)

+ 52 - 62
test/test_security_service.py

@@ -31,12 +31,12 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 self.provider.security.key_pairs.create(name=kp.name)
 
         sit.check_crud(self, self.provider.security.key_pairs, KeyPair,
-                       "cb_crudkp", create_kp, cleanup_kp,
+                       "cb-crudkp", create_kp, cleanup_kp,
                        extra_test_func=extra_tests)
 
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_key_pair_properties(self):
-        name = 'cb_kpprops-{0}'.format(helpers.get_uuid())
+        name = 'cb-kpprops-{0}'.format(helpers.get_uuid())
         kp = self.provider.security.key_pairs.create(name=name)
         with helpers.cleanup_action(lambda: kp.delete()):
             self.assertIsNotNone(
@@ -49,7 +49,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_import_key_pair(self):
-        name = 'cb_kpimport-{0}'.format(helpers.get_uuid())
+        name = 'cb-kpimport-{0}'.format(helpers.get_uuid())
 
         public_key, _ = cb_helpers.generate_key_pair()
         kp = self.provider.security.key_pairs.create(
@@ -60,85 +60,75 @@ 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_id=net.id)
 
         def cleanup_fw(fw):
             if fw:
                 fw.delete()
 
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                network=net)):
-            net, _ = helpers.create_test_network(self.provider, name)
-
-            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)
 
     @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):
-                    if rule:
-                        rule.delete()
+            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',
@@ -151,18 +141,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',
@@ -175,17 +165,17 @@ 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)
             rules = list(fw.rules)
             self.assertTrue(
                 # TODO: This should be made consistent across all providers.
@@ -200,9 +190,9 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 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
@@ -211,8 +201,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))

+ 3 - 2
test/test_vm_types_service.py

@@ -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")

+ 1 - 1
tox.ini

@@ -24,7 +24,7 @@ setenv =
     azure: CB_TEST_PROVIDER=azure
     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
     openstack:  CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS OS_AUTH_URL OS_PASSWORD OS_PROJECT_NAME OS_TENANT_NAME OS_USERNAME OS_REGION_NAME OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME NOVA_SERVICE_NAME