Merge "Add section on release notes to reviewing doc"
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index bc1b158..e6a22b0 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -29,7 +29,7 @@
     @classmethod
     def disable_user(cls, user_name):
         user = cls.get_user_by_name(user_name)
-        cls.users_client.enable_disable_user(user['id'], enabled=False)
+        cls.users_client.update_user_enabled(user['id'], enabled=False)
 
     @classmethod
     def disable_tenant(cls, tenant_name):
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 5902196..3c379f0 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -33,14 +33,14 @@
         cls.creds = cls.os.credentials
 
     @test.idempotent_id('b580fab9-7ae9-46e8-8138-417260cb6f9f')
-    def test_create_ec2_credentials(self):
-        """Create user ec2 credentials."""
-        resp = self.non_admin_users_client.create_user_ec2_credentials(
+    def test_create_ec2_credential(self):
+        """Create user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         access = resp['access']
         self.addCleanup(
-            self.non_admin_users_client.delete_user_ec2_credentials,
+            self.non_admin_users_client.delete_user_ec2_credential,
             self.creds.user_id, access)
         self.assertNotEmpty(resp['access'])
         self.assertNotEmpty(resp['secret'])
@@ -53,21 +53,21 @@
         created_creds = []
         fetched_creds = []
         # create first ec2 credentials
-        creds1 = self.non_admin_users_client.create_user_ec2_credentials(
+        creds1 = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         created_creds.append(creds1['access'])
         # create second ec2 credentials
-        creds2 = self.non_admin_users_client.create_user_ec2_credentials(
+        creds2 = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         created_creds.append(creds2['access'])
         # add credentials to be cleaned up
         self.addCleanup(
-            self.non_admin_users_client.delete_user_ec2_credentials,
+            self.non_admin_users_client.delete_user_ec2_credential,
             self.creds.user_id, creds1['access'])
         self.addCleanup(
-            self.non_admin_users_client.delete_user_ec2_credentials,
+            self.non_admin_users_client.delete_user_ec2_credential,
             self.creds.user_id, creds2['access'])
         # get the list of user ec2 credentials
         resp = self.non_admin_users_client.list_user_ec2_credentials(
@@ -81,32 +81,32 @@
                          ', '.join(cred for cred in missing))
 
     @test.idempotent_id('cb284075-b613-440d-83ca-fe0b33b3c2b8')
-    def test_show_ec2_credentials(self):
-        """Get the definite user ec2 credentials."""
-        resp = self.non_admin_users_client.create_user_ec2_credentials(
+    def test_show_ec2_credential(self):
+        """Get the definite user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         self.addCleanup(
-            self.non_admin_users_client.delete_user_ec2_credentials,
+            self.non_admin_users_client.delete_user_ec2_credential,
             self.creds.user_id, resp['access'])
 
-        ec2_creds = self.non_admin_users_client.show_user_ec2_credentials(
+        ec2_creds = self.non_admin_users_client.show_user_ec2_credential(
             self.creds.user_id, resp['access']
         )["credential"]
         for key in ['access', 'secret', 'user_id', 'tenant_id']:
             self.assertEqual(ec2_creds[key], resp[key])
 
     @test.idempotent_id('6aba0d4c-b76b-4e46-aa42-add79bc1551d')
-    def test_delete_ec2_credentials(self):
-        """Delete user ec2 credentials."""
-        resp = self.non_admin_users_client.create_user_ec2_credentials(
+    def test_delete_ec2_credential(self):
+        """Delete user ec2 credential."""
+        resp = self.non_admin_users_client.create_user_ec2_credential(
             self.creds.user_id,
             tenant_id=self.creds.tenant_id)["credential"]
         access = resp['access']
-        self.non_admin_users_client.delete_user_ec2_credentials(
+        self.non_admin_users_client.delete_user_ec2_credential(
             self.creds.user_id, access)
         self.assertRaises(
             lib_exc.NotFound,
-            self.non_admin_users_client.show_user_ec2_credentials,
+            self.non_admin_users_client.show_user_ec2_credential,
             self.creds.user_id,
             access)
diff --git a/tempest/api/volume/v2/test_image_metadata.py b/tempest/api/volume/v2/test_image_metadata.py
new file mode 100644
index 0000000..1e7bb30
--- /dev/null
+++ b/tempest/api/volume/v2/test_image_metadata.py
@@ -0,0 +1,64 @@
+# Copyright 2016 Red Hat, Inc.
+# 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 testtools import matchers
+
+from tempest.api.volume import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class VolumesV2ImageMetadata(base.BaseVolumeTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(VolumesV2ImageMetadata, cls).resource_setup()
+        # Create a volume from image ID
+        cls.volume = cls.create_volume(imageRef=CONF.compute.image_ref)
+
+    @test.idempotent_id('03efff0b-5c75-4822-8f10-8789ac15b13e')
+    @test.services('image')
+    def test_update_image_metadata(self):
+        # Update image metadata
+        image_metadata = {'image_id': '5137a025-3c5f-43c1-bc64-5f41270040a5',
+                          'image_name': 'image',
+                          'kernel_id': '6ff710d2-942b-4d6b-9168-8c9cc2404ab1',
+                          'ramdisk_id': 'somedisk'}
+        self.volumes_client.update_volume_image_metadata(self.volume['id'],
+                                                         **image_metadata)
+
+        # Fetch image metadata from the volume
+        volume_image_metadata = self.volumes_client.show_volume(
+            self.volume['id'])['volume']['volume_image_metadata']
+
+        # Verify image metadata was updated
+        self.assertThat(volume_image_metadata.items(),
+                        matchers.ContainsAll(image_metadata.items()))
+
+        # Delete one item from image metadata of the volume
+        self.volumes_client.delete_volume_image_metadata(self.volume['id'],
+                                                         'ramdisk_id')
+        del image_metadata['ramdisk_id']
+
+        # Fetch the new image metadata from the volume
+        volume_image_metadata = self.volumes_client.show_volume(
+            self.volume['id'])['volume']['volume_image_metadata']
+
+        # Verify image metadata was updated after item deletion
+        self.assertThat(volume_image_metadata.items(),
+                        matchers.ContainsAll(image_metadata.items()))
+        self.assertNotIn('ramdisk_id', volume_image_metadata)
diff --git a/tempest/clients.py b/tempest/clients.py
index a5a3b19..b7bc4fa 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -23,23 +23,13 @@
 from tempest.lib.services import compute
 from tempest.lib.services import network
 from tempest import manager
-from tempest.services.baremetal.v1.json.baremetal_client import \
-    BaremetalClient
-from tempest.services.data_processing.v1_1.data_processing_client import \
-    DataProcessingClient
-from tempest.services.database.json.flavors_client import \
-    DatabaseFlavorsClient
-from tempest.services.database.json.limits_client import \
-    DatabaseLimitsClient
-from tempest.services.database.json.versions_client import \
-    DatabaseVersionsClient
+from tempest.services import baremetal
+from tempest.services import data_processing
+from tempest.services import database
 from tempest.services import identity
 from tempest.services import image
-from tempest.services.object_storage.account_client import AccountClient
-from tempest.services.object_storage.container_client import ContainerClient
-from tempest.services.object_storage.object_client import ObjectClient
-from tempest.services.orchestration.json.orchestration_client import \
-    OrchestrationClient
+from tempest.services import object_storage
+from tempest.services import orchestration
 from tempest.services import volume
 
 CONF = config.CONF
@@ -81,13 +71,13 @@
         self._set_image_clients()
         self._set_network_clients()
 
-        self.baremetal_client = BaremetalClient(
+        self.baremetal_client = baremetal.BaremetalClient(
             self.auth_provider,
             CONF.baremetal.catalog_type,
             CONF.identity.region,
             endpoint_type=CONF.baremetal.endpoint_type,
             **self.default_params_with_timeout_values)
-        self.orchestration_client = OrchestrationClient(
+        self.orchestration_client = orchestration.OrchestrationClient(
             self.auth_provider,
             CONF.orchestration.catalog_type,
             CONF.orchestration.region or CONF.identity.region,
@@ -95,7 +85,7 @@
             build_interval=CONF.orchestration.build_interval,
             build_timeout=CONF.orchestration.build_timeout,
             **self.default_params)
-        self.data_processing_client = DataProcessingClient(
+        self.data_processing_client = data_processing.DataProcessingClient(
             self.auth_provider,
             CONF.data_processing.catalog_type,
             CONF.identity.region,
@@ -254,17 +244,17 @@
             self.auth_provider, **params_volume)
 
     def _set_database_clients(self):
-        self.database_flavors_client = DatabaseFlavorsClient(
+        self.database_flavors_client = database.DatabaseFlavorsClient(
             self.auth_provider,
             CONF.database.catalog_type,
             CONF.identity.region,
             **self.default_params_with_timeout_values)
-        self.database_limits_client = DatabaseLimitsClient(
+        self.database_limits_client = database.DatabaseLimitsClient(
             self.auth_provider,
             CONF.database.catalog_type,
             CONF.identity.region,
             **self.default_params_with_timeout_values)
-        self.database_versions_client = DatabaseVersionsClient(
+        self.database_versions_client = database.DatabaseVersionsClient(
             self.auth_provider,
             CONF.database.catalog_type,
             CONF.identity.region,
@@ -413,6 +403,9 @@
         }
         params.update(self.default_params_with_timeout_values)
 
-        self.account_client = AccountClient(self.auth_provider, **params)
-        self.container_client = ContainerClient(self.auth_provider, **params)
-        self.object_client = ObjectClient(self.auth_provider, **params)
+        self.account_client = object_storage.AccountClient(self.auth_provider,
+                                                           **params)
+        self.container_client = object_storage.ContainerClient(
+            self.auth_provider, **params)
+        self.object_client = object_storage.ObjectClient(self.auth_provider,
+                                                         **params)
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index e78f6b0..26bd418 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -70,6 +70,7 @@
 
     def take_action(self, parsed_args):
         self._set_env()
+        returncode = 0
         # Local execution mode
         if os.path.isfile('.testr.conf'):
             # If you're running in local execution mode and there is not a
@@ -77,8 +78,8 @@
             if not os.path.isdir('.testrepository'):
                 returncode = run_argv(['testr', 'init'], sys.stdin, sys.stdout,
                                       sys.stderr)
-            if returncode:
-                sys.exit(returncode)
+                if returncode:
+                    sys.exit(returncode)
         else:
             print("No .testr.conf file was found for local execution")
             sys.exit(2)
diff --git a/tempest/lib/services/compute/aggregates_client.py b/tempest/lib/services/compute/aggregates_client.py
index 168126c..ae747d8 100644
--- a/tempest/lib/services/compute/aggregates_client.py
+++ b/tempest/lib/services/compute/aggregates_client.py
@@ -108,7 +108,11 @@
         return rest_client.ResponseBody(resp, body)
 
     def set_metadata(self, aggregate_id, **kwargs):
-        """Replace the aggregate's existing metadata with new metadata."""
+        """Replace the aggregate's existing metadata with new metadata.
+
+        Available params: see http://developer.openstack.org/
+                              api-ref-compute-v2.1.html#addAggregateMetadata
+        """
         post_body = json.dumps({'set_metadata': kwargs})
         resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
                                post_body)
diff --git a/tempest/services/baremetal/__init__.py b/tempest/services/baremetal/__init__.py
index e69de29..390f40a 100644
--- a/tempest/services/baremetal/__init__.py
+++ b/tempest/services/baremetal/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.baremetal.v1.json.baremetal_client import \
+    BaremetalClient
+
+__all__ = ['BaremetalClient']
diff --git a/tempest/services/data_processing/__init__.py b/tempest/services/data_processing/__init__.py
index e69de29..c49bc5c 100644
--- a/tempest/services/data_processing/__init__.py
+++ b/tempest/services/data_processing/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.data_processing.v1_1.data_processing_client import \
+    DataProcessingClient
+
+__all__ = ['DataProcessingClient']
diff --git a/tempest/services/database/__init__.py b/tempest/services/database/__init__.py
index e69de29..9a742d8 100644
--- a/tempest/services/database/__init__.py
+++ b/tempest/services/database/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.database.json.flavors_client import \
+    DatabaseFlavorsClient
+from tempest.services.database.json.limits_client import \
+    DatabaseLimitsClient
+from tempest.services.database.json.versions_client import \
+    DatabaseVersionsClient
+
+__all__ = ['DatabaseFlavorsClient', 'DatabaseLimitsClient',
+           'DatabaseVersionsClient']
diff --git a/tempest/services/identity/v2/json/users_client.py b/tempest/services/identity/v2/json/users_client.py
index 1048840..4ea17f9 100644
--- a/tempest/services/identity/v2/json/users_client.py
+++ b/tempest/services/identity/v2/json/users_client.py
@@ -78,7 +78,7 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
-    def enable_disable_user(self, user_id, **kwargs):
+    def update_user_enabled(self, user_id, **kwargs):
         """Enables or disables a user.
 
         Available params: see http://developer.openstack.org/
@@ -121,7 +121,7 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
-    def create_user_ec2_credentials(self, user_id, **kwargs):
+    def create_user_ec2_credential(self, user_id, **kwargs):
         # TODO(piyush): Current api-site doesn't contain this API description.
         # After fixing the api-site, we need to fix here also for putting the
         # link to api-site.
@@ -132,7 +132,7 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
-    def delete_user_ec2_credentials(self, user_id, access):
+    def delete_user_ec2_credential(self, user_id, access):
         resp, body = self.delete('/users/%s/credentials/OS-EC2/%s' %
                                  (user_id, access))
         self.expected_success(204, resp.status)
@@ -144,7 +144,7 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
-    def show_user_ec2_credentials(self, user_id, access):
+    def show_user_ec2_credential(self, user_id, access):
         resp, body = self.get('/users/%s/credentials/OS-EC2/%s' %
                               (user_id, access))
         self.expected_success(200, resp.status)
diff --git a/tempest/services/object_storage/__init__.py b/tempest/services/object_storage/__init__.py
index e69de29..96fe4a3 100644
--- a/tempest/services/object_storage/__init__.py
+++ b/tempest/services/object_storage/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.object_storage.account_client import AccountClient
+from tempest.services.object_storage.container_client import ContainerClient
+from tempest.services.object_storage.object_client import ObjectClient
+
+__all__ = ['AccountClient', 'ContainerClient', 'ObjectClient']
diff --git a/tempest/services/orchestration/__init__.py b/tempest/services/orchestration/__init__.py
index e69de29..5a1ffcc 100644
--- a/tempest/services/orchestration/__init__.py
+++ b/tempest/services/orchestration/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.services.orchestration.json.orchestration_client import \
+    OrchestrationClient
+
+__all__ = ['OrchestrationClient']
diff --git a/tempest/services/volume/base/base_volumes_client.py b/tempest/services/volume/base/base_volumes_client.py
index 6237745..a3a4eb6 100644
--- a/tempest/services/volume/base/base_volumes_client.py
+++ b/tempest/services/volume/base/base_volumes_client.py
@@ -299,6 +299,23 @@
         self.expected_success(200, resp.status)
         return rest_client.ResponseBody(resp, body)
 
+    def update_volume_image_metadata(self, volume_id, **kwargs):
+        """Update image metadata for the volume."""
+        post_body = json.dumps({'os-set_image_metadata': {'metadata': kwargs}})
+        url = "volumes/%s/action" % (volume_id)
+        resp, body = self.post(url, post_body)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_volume_image_metadata(self, volume_id, key_name):
+        """Delete image metadata item for the volume."""
+        post_body = json.dumps({'os-unset_image_metadata': {'key': key_name}})
+        url = "volumes/%s/action" % (volume_id)
+        resp, body = self.post(url, post_body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
     def retype_volume(self, volume_id, **kwargs):
         """Updates volume with new volume type."""
         post_body = json.dumps({'os-retype': kwargs})
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index 9aa06e5..dcffd21 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -102,6 +102,14 @@
         subprocess.call(['git', 'init'], stderr=DEVNULL)
         self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
 
+    def test_tempest_run_passes_with_testrepository(self):
+        # Git init is required for the pbr testr command. pbr requires a git
+        # version or an sdist to work. so make the test directory a git repo
+        # too.
+        subprocess.call(['git', 'init'], stderr=DEVNULL)
+        subprocess.call(['testr', 'init'])
+        self.assertRunExit(['tempest', 'run', '--regex', 'passing'], 0)
+
     def test_tempest_run_fails(self):
         # Git init is required for the pbr testr command. pbr requires a git
         # version or an sdist to work. so make the test directory a git repo