port test_server_metadata and test_server_personality into v3 part1

This changeset only copies the v2 files into the appropriate v3
directories unchanged. This is being tried in order to make
reviewing of the porting easier as gerrit will display only what
is actually changed for v3 rather than entirely new files.

Partially implements blueprint nova-v3-api-tests

Change-Id: Ic0029ef8e102755eaa64c01e14f8544f36632376
diff --git a/tempest/api/compute/v3/servers/test_server_metadata.py b/tempest/api/compute/v3/servers/test_server_metadata.py
new file mode 100644
index 0000000..ee0f4a9
--- /dev/null
+++ b/tempest/api/compute/v3/servers/test_server_metadata.py
@@ -0,0 +1,217 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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.compute import base
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ServerMetadataTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ServerMetadataTestJSON, cls).setUpClass()
+        cls.client = cls.servers_client
+        cls.quotas = cls.quotas_client
+        cls.admin_client = cls._get_identity_admin_client()
+        resp, tenants = cls.admin_client.list_tenants()
+        cls.tenant_id = [tnt['id'] for tnt in tenants if tnt['name'] ==
+                         cls.client.tenant_name][0]
+        resp, server = cls.create_test_server(meta={}, wait_until='ACTIVE')
+
+        cls.server_id = server['id']
+
+    def setUp(self):
+        super(ServerMetadataTestJSON, self).setUp()
+        meta = {'key1': 'value1', 'key2': 'value2'}
+        resp, _ = self.client.set_server_metadata(self.server_id, meta)
+        self.assertEqual(resp.status, 200)
+
+    @attr(type='gate')
+    def test_list_server_metadata(self):
+        # All metadata key/value pairs for a server should be returned
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+
+        # Verify the expected metadata items are in the list
+        self.assertEqual(200, resp.status)
+        expected = {'key1': 'value1', 'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_set_server_metadata(self):
+        # The server's metadata should be replaced with the provided values
+        # Create a new set of metadata for the server
+        req_metadata = {'meta2': 'data2', 'meta3': 'data3'}
+        resp, metadata = self.client.set_server_metadata(self.server_id,
+                                                         req_metadata)
+        self.assertEqual(200, resp.status)
+
+        # Verify the expected values are correct, and that the
+        # previous values have been removed
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+        self.assertEqual(resp_metadata, req_metadata)
+
+    @attr(type='gate')
+    def test_server_create_metadata_key_too_long(self):
+        # Attempt to start a server with a meta-data key that is > 255
+        # characters
+
+        # Try a few values
+        for sz in [256, 257, 511, 1023]:
+            key = "k" * sz
+            meta = {key: 'data1'}
+            self.assertRaises(exceptions.OverLimit,
+                              self.create_test_server,
+                              meta=meta)
+
+        # no teardown - all creates should fail
+
+    @attr(type='gate')
+    def test_update_server_metadata(self):
+        # The server's metadata values should be updated to the
+        # provided values
+        meta = {'key1': 'alt1', 'key3': 'value3'}
+        resp, metadata = self.client.update_server_metadata(self.server_id,
+                                                            meta)
+        self.assertEqual(200, resp.status)
+
+        # Verify the values have been updated to the proper values
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+        expected = {'key1': 'alt1', 'key2': 'value2', 'key3': 'value3'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_update_metadata_empty_body(self):
+        # The original metadata should not be lost if empty metadata body is
+        # passed
+        meta = {}
+        _, metadata = self.client.update_server_metadata(self.server_id, meta)
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+        expected = {'key1': 'value1', 'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_get_server_metadata_item(self):
+        # The value for a specific metadata key should be returned
+        resp, meta = self.client.get_server_metadata_item(self.server_id,
+                                                          'key2')
+        self.assertEqual('value2', meta['key2'])
+
+    @attr(type='gate')
+    def test_set_server_metadata_item(self):
+        # The item's value should be updated to the provided value
+        # Update the metadata value
+        meta = {'nova': 'alt'}
+        resp, body = self.client.set_server_metadata_item(self.server_id,
+                                                          'nova', meta)
+        self.assertEqual(200, resp.status)
+
+        # Verify the meta item's value has been updated
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+        expected = {'key1': 'value1', 'key2': 'value2', 'nova': 'alt'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_delete_server_metadata_item(self):
+        # The metadata value/key pair should be deleted from the server
+        resp, meta = self.client.delete_server_metadata_item(self.server_id,
+                                                             'key1')
+        self.assertEqual(204, resp.status)
+
+        # Verify the metadata item has been removed
+        resp, resp_metadata = self.client.list_server_metadata(self.server_id)
+        expected = {'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type=['negative', 'gate'])
+    def test_server_metadata_negative(self):
+        # Blank key should trigger an error.
+        meta = {'': 'data1'}
+        self.assertRaises(exceptions.BadRequest,
+                          self.create_test_server,
+                          meta=meta)
+
+        # GET on a non-existent server should not succeed
+        self.assertRaises(exceptions.NotFound,
+                          self.client.get_server_metadata_item, 999, 'test2')
+
+        # List metadata on a non-existent server should not succeed
+        self.assertRaises(exceptions.NotFound,
+                          self.client.list_server_metadata, 999)
+
+        # Raise BadRequest if key in uri does not match
+        # the key passed in body.
+        meta = {'testkey': 'testvalue'}
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.set_server_metadata_item,
+                          self.server_id, 'key', meta)
+
+        # Set metadata on a non-existent server should not succeed
+        meta = {'meta1': 'data1'}
+        self.assertRaises(exceptions.NotFound,
+                          self.client.set_server_metadata, 999, meta)
+
+        # An update should not happen for a non-existent image
+        meta = {'key1': 'value1', 'key2': 'value2'}
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_server_metadata, 999, meta)
+
+        # Blank key should trigger an error
+        meta = {'': 'data1'}
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_server_metadata,
+                          self.server_id, meta=meta)
+
+        # Should not be able to delete metadata item from a non-existent server
+        self.assertRaises(exceptions.NotFound,
+                          self.client.delete_server_metadata_item, 999, 'd')
+
+        # Raise a 413 OverLimit exception while exceeding metadata items limit
+        # for tenant.
+        _, quota_set = self.quotas.get_quota_set(self.tenant_id)
+        quota_metadata = quota_set['metadata_items']
+        req_metadata = {}
+        for num in range(1, quota_metadata + 2):
+            req_metadata['key' + str(num)] = 'val' + str(num)
+        self.assertRaises(exceptions.OverLimit,
+                          self.client.set_server_metadata,
+                          self.server_id, req_metadata)
+
+        # Raise a 413 OverLimit exception while exceeding metadata items limit
+        # for tenant (update).
+        self.assertRaises(exceptions.OverLimit,
+                          self.client.update_server_metadata,
+                          self.server_id, req_metadata)
+
+        # Raise a bad request error for blank key.
+        # set_server_metadata will replace all metadata with new value
+        meta = {'': 'data1'}
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.set_server_metadata,
+                          self.server_id, meta=meta)
+
+        # Raise a bad request error for a missing metadata field
+        # set_server_metadata will replace all metadata with new value
+        meta = {'meta1': 'data1'}
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.set_server_metadata,
+                          self.server_id, meta=meta, no_metadata_field=True)
+
+
+class ServerMetadataTestXML(ServerMetadataTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/servers/test_server_personality.py b/tempest/api/compute/v3/servers/test_server_personality.py
new file mode 100644
index 0000000..c6d2e44
--- /dev/null
+++ b/tempest/api/compute/v3/servers/test_server_personality.py
@@ -0,0 +1,68 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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 base64
+
+from tempest.api.compute import base
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ServerPersonalityTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ServerPersonalityTestJSON, cls).setUpClass()
+        cls.client = cls.servers_client
+        cls.user_client = cls.limits_client
+
+    @attr(type='gate')
+    def test_personality_files_exceed_limit(self):
+        # Server creation should fail if greater than the maximum allowed
+        # number of files are injected into the server.
+        file_contents = 'This is a test file.'
+        personality = []
+        max_file_limit = \
+            self.user_client.get_specific_absolute_limit("maxPersonality")
+        for i in range(0, int(max_file_limit) + 1):
+            path = 'etc/test' + str(i) + '.txt'
+            personality.append({'path': path,
+                                'contents': base64.b64encode(file_contents)})
+        self.assertRaises(exceptions.OverLimit, self.create_test_server,
+                          personality=personality)
+
+    @attr(type='gate')
+    def test_can_create_server_with_max_number_personality_files(self):
+        # Server should be created successfully if maximum allowed number of
+        # files is injected into the server during creation.
+        file_contents = 'This is a test file.'
+        max_file_limit = \
+            self.user_client.get_specific_absolute_limit("maxPersonality")
+        person = []
+        for i in range(0, int(max_file_limit)):
+            path = 'etc/test' + str(i) + '.txt'
+            person.append({
+                'path': path,
+                'contents': base64.b64encode(file_contents),
+            })
+        resp, server = self.create_test_server(personality=person)
+        self.assertEqual('202', resp['status'])
+
+
+class ServerPersonalityTestXML(ServerPersonalityTestJSON):
+    _interface = "xml"
diff --git a/tempest/services/compute/v3/json/limits_client.py b/tempest/services/compute/v3/json/limits_client.py
new file mode 100644
index 0000000..3e53e3e
--- /dev/null
+++ b/tempest/services/compute/v3/json/limits_client.py
@@ -0,0 +1,40 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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 json
+from tempest.common.rest_client import RestClient
+
+
+class LimitsClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(LimitsClientJSON, self).__init__(config, username, password,
+                                               auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def get_absolute_limits(self):
+        resp, body = self.get("limits")
+        body = json.loads(body)
+        return resp, body['limits']['absolute']
+
+    def get_specific_absolute_limit(self, absolute_limit):
+        resp, body = self.get("limits")
+        body = json.loads(body)
+        if absolute_limit not in body['limits']['absolute']:
+            return None
+        else:
+            return body['limits']['absolute'][absolute_limit]
diff --git a/tempest/services/compute/v3/json/quotas_client.py b/tempest/services/compute/v3/json/quotas_client.py
new file mode 100644
index 0000000..a910dec
--- /dev/null
+++ b/tempest/services/compute/v3/json/quotas_client.py
@@ -0,0 +1,103 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 NTT Data
+# 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 json
+
+from tempest.common.rest_client import RestClient
+
+
+class QuotasClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(QuotasClientJSON, self).__init__(config, username, password,
+                                               auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def get_quota_set(self, tenant_id):
+        """List the quota set for a tenant."""
+
+        url = 'os-quota-sets/%s' % str(tenant_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['quota_set']
+
+    def get_default_quota_set(self, tenant_id):
+        """List the default quota set for a tenant."""
+
+        url = 'os-quota-sets/%s/defaults' % str(tenant_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['quota_set']
+
+    def update_quota_set(self, tenant_id, force=None,
+                         injected_file_content_bytes=None,
+                         metadata_items=None, ram=None, floating_ips=None,
+                         fixed_ips=None, key_pairs=None, instances=None,
+                         security_group_rules=None, injected_files=None,
+                         cores=None, injected_file_path_bytes=None,
+                         security_groups=None):
+        """
+        Updates the tenant's quota limits for one or more resources
+        """
+        post_body = {}
+
+        if force is not None:
+            post_body['force'] = force
+
+        if injected_file_content_bytes is not None:
+            post_body['injected_file_content_bytes'] = \
+                injected_file_content_bytes
+
+        if metadata_items is not None:
+            post_body['metadata_items'] = metadata_items
+
+        if ram is not None:
+            post_body['ram'] = ram
+
+        if floating_ips is not None:
+            post_body['floating_ips'] = floating_ips
+
+        if fixed_ips is not None:
+            post_body['fixed_ips'] = fixed_ips
+
+        if key_pairs is not None:
+            post_body['key_pairs'] = key_pairs
+
+        if instances is not None:
+            post_body['instances'] = instances
+
+        if security_group_rules is not None:
+            post_body['security_group_rules'] = security_group_rules
+
+        if injected_files is not None:
+            post_body['injected_files'] = injected_files
+
+        if cores is not None:
+            post_body['cores'] = cores
+
+        if injected_file_path_bytes is not None:
+            post_body['injected_file_path_bytes'] = injected_file_path_bytes
+
+        if security_groups is not None:
+            post_body['security_groups'] = security_groups
+
+        post_body = json.dumps({'quota_set': post_body})
+        resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body,
+                              self.headers)
+
+        body = json.loads(body)
+        return resp, body['quota_set']
diff --git a/tempest/services/compute/v3/xml/limits_client.py b/tempest/services/compute/v3/xml/limits_client.py
new file mode 100644
index 0000000..704de52
--- /dev/null
+++ b/tempest/services/compute/v3/xml/limits_client.py
@@ -0,0 +1,55 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM Corp.
+# 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 lxml import objectify
+
+from tempest.common.rest_client import RestClientXML
+
+NS = "{http://docs.openstack.org/common/api/v1.0}"
+
+
+class LimitsClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(LimitsClientXML, self).__init__(config, username, password,
+                                              auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def get_absolute_limits(self):
+        resp, body = self.get("limits", self.headers)
+        body = objectify.fromstring(body)
+        lim = NS + 'absolute'
+        ret = {}
+
+        for el in body[lim].iterchildren():
+            attributes = el.attrib
+            ret[attributes['name']] = attributes['value']
+        return resp, ret
+
+    def get_specific_absolute_limit(self, absolute_limit):
+        resp, body = self.get("limits", self.headers)
+        body = objectify.fromstring(body)
+        lim = NS + 'absolute'
+        ret = {}
+
+        for el in body[lim].iterchildren():
+            attributes = el.attrib
+            ret[attributes['name']] = attributes['value']
+        if absolute_limit not in ret:
+            return None
+        else:
+            return ret[absolute_limit]
diff --git a/tempest/services/compute/v3/xml/quotas_client.py b/tempest/services/compute/v3/xml/quotas_client.py
new file mode 100644
index 0000000..ef5362c
--- /dev/null
+++ b/tempest/services/compute/v3/xml/quotas_client.py
@@ -0,0 +1,126 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 NTT Data
+# 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 lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+
+class QuotasClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(QuotasClientXML, self).__init__(config, username, password,
+                                              auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def _format_quota(self, q):
+        quota = {}
+        for k, v in q.items():
+            try:
+                v = int(v)
+            except ValueError:
+                pass
+
+            quota[k] = v
+
+        return quota
+
+    def _parse_array(self, node):
+        return [self._format_quota(xml_to_json(x)) for x in node]
+
+    def get_quota_set(self, tenant_id):
+        """List the quota set for a tenant."""
+
+        url = 'os-quota-sets/%s' % str(tenant_id)
+        resp, body = self.get(url, self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        body = self._format_quota(body)
+        return resp, body
+
+    def get_default_quota_set(self, tenant_id):
+        """List the default quota set for a tenant."""
+
+        url = 'os-quota-sets/%s/defaults' % str(tenant_id)
+        resp, body = self.get(url, self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        body = self._format_quota(body)
+        return resp, body
+
+    def update_quota_set(self, tenant_id, force=None,
+                         injected_file_content_bytes=None,
+                         metadata_items=None, ram=None, floating_ips=None,
+                         fixed_ips=None, key_pairs=None, instances=None,
+                         security_group_rules=None, injected_files=None,
+                         cores=None, injected_file_path_bytes=None,
+                         security_groups=None):
+        """
+        Updates the tenant's quota limits for one or more resources
+        """
+        post_body = Element("quota_set",
+                            xmlns=XMLNS_11)
+
+        if force is not None:
+            post_body.add_attr('force', force)
+
+        if injected_file_content_bytes is not None:
+            post_body.add_attr('injected_file_content_bytes',
+                               injected_file_content_bytes)
+
+        if metadata_items is not None:
+            post_body.add_attr('metadata_items', metadata_items)
+
+        if ram is not None:
+            post_body.add_attr('ram', ram)
+
+        if floating_ips is not None:
+            post_body.add_attr('floating_ips', floating_ips)
+
+        if fixed_ips is not None:
+            post_body.add_attr('fixed_ips', fixed_ips)
+
+        if key_pairs is not None:
+            post_body.add_attr('key_pairs', key_pairs)
+
+        if instances is not None:
+            post_body.add_attr('instances', instances)
+
+        if security_group_rules is not None:
+            post_body.add_attr('security_group_rules', security_group_rules)
+
+        if injected_files is not None:
+            post_body.add_attr('injected_files', injected_files)
+
+        if cores is not None:
+            post_body.add_attr('cores', cores)
+
+        if injected_file_path_bytes is not None:
+            post_body.add_attr('injected_file_path_bytes',
+                               injected_file_path_bytes)
+
+        if security_groups is not None:
+            post_body.add_attr('security_groups', security_groups)
+
+        resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+                              str(Document(post_body)),
+                              self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        body = self._format_quota(body)
+        return resp, body