Merge "Replace assertLessEqual - is not in py26 testtools"
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index e8acec5..4ff6b07 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -85,7 +85,7 @@
                           aggregates))
 
     @attr(type='gate')
-    def test_aggregate_create_get_details(self):
+    def test_aggregate_create_update_metadata_get_details(self):
         # Create an aggregate and ensure its details are returned.
         aggregate_name = rand_name(self.aggregate_name_prefix)
         resp, aggregate = self.client.create_aggregate(aggregate_name)
@@ -96,6 +96,18 @@
         self.assertEqual(aggregate['name'], body['name'])
         self.assertEqual(aggregate['availability_zone'],
                          body['availability_zone'])
+        self.assertEqual({}, body["metadata"])
+
+        # set the metadata of the aggregate
+        meta = {"key": "value"}
+        resp, body = self.client.set_metadata(aggregate['id'], meta)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
+
+        # verify the metadata has been set
+        resp, body = self.client.get_aggregate(aggregate['id'])
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
 
     @attr(type='gate')
     def test_aggregate_create_update_with_az(self):
diff --git a/tempest/api/image/v2/test_images_tags.py b/tempest/api/image/v2/test_images_tags.py
new file mode 100644
index 0000000..7e3bde4
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags.py
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.api.image import base
+from tempest.common.utils import data_utils
+from tempest.test import attr
+
+
+class ImagesTagsTest(base.BaseV2ImageTest):
+
+    @attr(type='gate')
+    def test_update_delete_tags_for_image(self):
+        resp, body = self.create_image(container_format='bare',
+                                       disk_format='raw',
+                                       visibility='public')
+        image_id = body['id']
+        tag = data_utils.rand_name('tag-')
+        self.addCleanup(self.client.delete_image, image_id)
+
+        # Creating image tag and verify it.
+        resp, body = self.client.add_image_tag(image_id, tag)
+        self.assertEqual(resp.status, 204)
+        resp, body = self.client.get_image_metadata(image_id)
+        self.assertEqual(resp.status, 200)
+        self.assertIn(tag, body['tags'])
+
+        # Deleting image tag and verify it.
+        resp = self.client.delete_image_tag(image_id, tag)
+        self.assertEqual(resp.status, 204)
+        resp, body = self.client.get_image_metadata(image_id)
+        self.assertEqual(resp.status, 200)
+        self.assertNotIn(tag, body['tags'])
diff --git a/tempest/api/image/v2/test_images_tags_negative.py b/tempest/api/image/v2/test_images_tags_negative.py
new file mode 100644
index 0000000..e0d84de
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags_negative.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+from tempest.api.image import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ImagesTagsNegativeTest(base.BaseV2ImageTest):
+
+    @attr(type=['negative', 'gate'])
+    def test_update_tags_for_non_existing_image(self):
+        # Update tag with non existing image.
+        tag = data_utils.rand_name('tag-')
+        non_exist_image = str(uuid.uuid4())
+        self.assertRaises(exceptions.NotFound, self.client.add_image_tag,
+                          non_exist_image, tag)
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_non_existing_tag(self):
+        # Delete non existing tag.
+        resp, body = self.create_image(container_format='bare',
+                                       disk_format='raw',
+                                       is_public=True,
+                                       )
+        image_id = body['id']
+        tag = data_utils.rand_name('non-exist-tag-')
+        self.addCleanup(self.client.delete_image, image_id)
+        self.assertRaises(exceptions.NotFound, self.client.delete_image_tag,
+                          image_id, tag)
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index 75ce9ff..b7c6bf1 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -97,3 +97,14 @@
                                post_body, self.headers)
         body = json.loads(body)
         return resp, body['aggregate']
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = {
+            'metadata': meta,
+        }
+        post_body = json.dumps({'set_metadata': post_body})
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['aggregate']
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
index 8ef0af6..5faaff5 100644
--- a/tempest/services/compute/xml/aggregates_client.py
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -21,6 +21,7 @@
 from tempest import exceptions
 from tempest.services.compute.xml.common import Document
 from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
 from tempest.services.compute.xml.common import xml_to_json
 
 
@@ -112,3 +113,18 @@
                                self.headers)
         aggregate = self._format_aggregate(etree.fromstring(body))
         return resp, aggregate
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = Element("set_metadata")
+        metadata = Element("metadata")
+        post_body.append(metadata)
+        for k, v in meta.items():
+            meta = Element(k)
+            meta.append(Text(v))
+            metadata.append(meta)
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
diff --git a/tempest/services/image/v2/json/image_client.py b/tempest/services/image/v2/json/image_client.py
index f0531ec..62b8ff6 100644
--- a/tempest/services/image/v2/json/image_client.py
+++ b/tempest/services/image/v2/json/image_client.py
@@ -124,3 +124,13 @@
         url = 'v2/images/%s/file' % image_id
         resp, body = self.get(url)
         return resp, body
+
+    def add_image_tag(self, image_id, tag):
+        url = 'v2/images/%s/tags/%s' % (image_id, tag)
+        resp, body = self.put(url, body=None, headers=self.headers)
+        return resp, body
+
+    def delete_image_tag(self, image_id, tag):
+        url = 'v2/images/%s/tags/%s' % (image_id, tag)
+        resp, _ = self.delete(url)
+        return resp
diff --git a/tools/skip_tracker.py b/tools/skip_tracker.py
index ffaf134..0ae3323 100755
--- a/tools/skip_tracker.py
+++ b/tools/skip_tracker.py
@@ -46,14 +46,24 @@
     test methods that have been decorated to skip because of
     a particular bug.
     """
-    results = []
+    results = {}
     debug("Searching in %s", start)
     for root, _dirs, files in os.walk(start):
         for name in files:
             if name.startswith('test_') and name.endswith('py'):
                 path = os.path.join(root, name)
                 debug("Searching in %s", path)
-                results += find_skips_in_file(path)
+                temp_result = find_skips_in_file(path)
+                for method_name, bug_no in temp_result:
+                    if results.get(bug_no):
+                        result_dict = results.get(bug_no)
+                        if result_dict.get(name):
+                            result_dict[name].append(method_name)
+                        else:
+                            result_dict[name] = [method_name]
+                        results[bug_no] = result_dict
+                    else:
+                        results[bug_no] = {name: [method_name]}
     return results
 
 
@@ -83,11 +93,19 @@
     return results
 
 
+def get_results(result_dict):
+    results = []
+    for bug_no in result_dict.keys():
+        for method in result_dict[bug_no]:
+            results.append((method, bug_no))
+    return results
+
+
 if __name__ == '__main__':
     logging.basicConfig(format='%(levelname)s: %(message)s',
                         level=logging.INFO)
     results = find_skips()
-    unique_bugs = sorted(set([bug for (method, bug) in results]))
+    unique_bugs = sorted(set([bug for (method, bug) in get_results(results)]))
     unskips = []
     duplicates = []
     info("Total bug skips found: %d", len(results))
@@ -122,4 +140,7 @@
         print("should be removed from the test cases:")
         print()
         for bug in unskips:
-            print("  %7s" % bug)
+            message = "  %7s in " % bug
+            locations = ["%s" % x for x in results[bug].keys()]
+            message += " and ".join(locations)
+            print(message)