Add tempest test for share access metadata

Depends-On: https://review.openstack.org/#/c/570708/
Change-Id: Ia794e1b13fec092b139c4859af48159869d6869e
Partially-implements bp: metadata-for-access-rule
diff --git a/manila_tempest_tests/common/constants.py b/manila_tempest_tests/common/constants.py
index 25cf4ee..c56712e 100644
--- a/manila_tempest_tests/common/constants.py
+++ b/manila_tempest_tests/common/constants.py
@@ -83,3 +83,5 @@
 SHARE_GROUP_TYPE_REQUIRED_KEYS = {
     'id', 'name', 'share_types', 'is_public', 'group_specs',
 }
+
+MIN_SHARE_ACCESS_METADATA_MICROVERSION = '2.45'
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index e737cb4..0517c60 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -30,7 +30,7 @@
                help="The minimum api microversion is configured to be the "
                     "value of the minimum microversion supported by Manila."),
     cfg.StrOpt("max_api_microversion",
-               default="2.44",
+               default="2.45",
                help="The maximum api microversion is configured to be the "
                     "value of the latest microversion supported by Manila."),
     cfg.StrOpt("region",
diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py
index 3f1c8a1..522d467 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -731,7 +731,8 @@
 
     def create_access_rule(self, share_id, access_type="ip",
                            access_to="0.0.0.0", access_level=None,
-                           version=LATEST_MICROVERSION, action_name=None):
+                           version=LATEST_MICROVERSION, metadata=None,
+                           action_name=None):
         post_body = {
             self._get_access_action_name(version, 'os-allow_access'): {
                 "access_type": access_type,
@@ -739,6 +740,8 @@
                 "access_level": access_level,
             }
         }
+        if metadata is not None:
+            post_body['allow_access']['metadata'] = metadata
         body = json.dumps(post_body)
         resp, body = self.post(
             "shares/%s/action" % share_id, body, version=version,
@@ -747,10 +750,34 @@
         return self._parse_resp(body)
 
     def list_access_rules(self, share_id, version=LATEST_MICROVERSION,
-                          action_name=None):
-        body = {self._get_access_action_name(version, 'os-access_list'): None}
-        resp, body = self.post(
-            "shares/%s/action" % share_id, json.dumps(body), version=version)
+                          metadata=None, action_name=None):
+        if utils.is_microversion_lt(version, "2.45"):
+            body = {
+                self._get_access_action_name(version, 'os-access_list'): None
+            }
+            resp, body = self.post(
+                "shares/%s/action" % share_id, json.dumps(body),
+                version=version)
+            self.expected_success(200, resp.status)
+        else:
+            return self.list_access_rules_with_new_API(
+                share_id, metadata=metadata, version=version,
+                action_name=action_name)
+        return self._parse_resp(body)
+
+    def list_access_rules_with_new_API(self, share_id, metadata=None,
+                                       version=LATEST_MICROVERSION,
+                                       action_name=None):
+        metadata = metadata or {}
+        query_string = ''
+
+        params = sorted(
+            [(k, v) for (k, v) in list(metadata.items()) if v])
+        if params:
+            query_string = "&%s" % urlparse.urlencode(params)
+
+        url = 'share-access-rules?share_id=%s' % share_id + query_string
+        resp, body = self.get(url, version=version)
         self.expected_success(200, resp.status)
         return self._parse_resp(body)
 
@@ -767,6 +794,28 @@
         self.expected_success(202, resp.status)
         return body
 
+    def get_access(self, access_id, version=LATEST_MICROVERSION):
+        resp, body = self.get("share-access-rules/%s" % access_id,
+                              version=version)
+        self.expected_success(200, resp.status)
+        return self._parse_resp(body)
+
+    def update_access_metadata(self, access_id, metadata,
+                               version=LATEST_MICROVERSION):
+        url = 'share-access-rules/%s/metadata' % access_id
+        body = {"metadata": metadata}
+        resp, body = self.put(url, json.dumps(body), version=version)
+        self.expected_success(200, resp.status)
+        return self._parse_resp(body)
+
+    def delete_access_metadata(self, access_id, key,
+                               version=LATEST_MICROVERSION):
+        url = "share-access-rules/%s/metadata/%s" % (access_id, key)
+        resp, body = self.delete(url, version=version)
+        self.expected_success(200, resp.status)
+        return body
+
+
 ###############
 
     def list_availability_zones(self, url='availability-zones',
diff --git a/manila_tempest_tests/tests/api/test_access_rules_metadata.py b/manila_tempest_tests/tests/api/test_access_rules_metadata.py
new file mode 100644
index 0000000..d3f25d7
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_access_rules_metadata.py
@@ -0,0 +1,133 @@
+# Copyright 2018 Huawei 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.
+
+import ddt
+from tempest import config
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import constants
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+@base.skip_if_microversion_lt(
+    constants.MIN_SHARE_ACCESS_METADATA_MICROVERSION)
+@ddt.ddt
+class AccessRulesMetadataTest(base.BaseSharesTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(AccessRulesMetadataTest, cls).resource_setup()
+        # The share access rule metadata doesn't care about the value of
+        # access type, access protocol, access_to, so we only get one of
+        # the value that the driver support.
+        if not (any(p in CONF.share.enable_ip_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_user_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_cert_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_cephx_rules_for_protocols
+                    for p in cls.protocols)):
+            cls.message = "Rule tests are disabled"
+            raise cls.skipException(cls.message)
+        if CONF.share.enable_ip_rules_for_protocols:
+            cls.protocol = CONF.share.enable_ip_rules_for_protocols[0]
+            cls.access_type = "ip"
+        elif CONF.share.enable_user_rules_for_protocols:
+            cls.protocol = CONF.share.enable_user_rules_for_protocols[0]
+            cls.access_type = "user"
+        elif CONF.share.enable_cert_rules_for_protocols:
+            cls.protocol = CONF.share.enable_cert_rules_for_protocols[0]
+            cls.access_type = "cert"
+        elif CONF.share.enable_cephx_rules_for_protocols:
+            cls.protocol = CONF.share.enable_cephx_rules_for_protocols[0]
+            cls.access_type = "cephx"
+        cls.shares_v2_client.share_protocol = cls.protocol
+        int_range = range(20, 50)
+        cls.access_to = {
+            # list of unique values is required for ability to create lots
+            # of access rules for one share using different API microversions.
+            'ip': set([utils.rand_ipv6_ip() for i in int_range]),
+            # following users are fakes and access rules that use it are
+            # expected to fail, but they are used only for API testing.
+            'user': ['foo_user_%d' % i for i in int_range],
+            'cert': ['tenant_%d.example.com' % i for i in int_range],
+            'cephx': ['eve%d' % i for i in int_range],
+        }
+        cls.share = cls.create_share()
+        cls.md1 = {"key1": "value1", "key2": "value2"}
+        cls.access = cls.shares_v2_client.create_access_rule(
+            cls.share["id"], cls.access_type,
+            cls.access_to[cls.access_type].pop(), 'rw', metadata=cls.md1)
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_set_get_delete_access_metadata(self):
+        data = {"key1": "v" * 255, "k" * 255: "value2"}
+        # set metadata
+        access = self.shares_v2_client.create_access_rule(
+            self.share["id"], self.access_type,
+            self.access_to[self.access_type].pop(), 'rw', metadata=data)
+
+        # read metadata
+        get_access = self.shares_v2_client.get_access(access["id"])
+
+        # verify metadata
+        self.assertEqual(data, get_access['metadata'])
+
+        # delete metadata
+        for key in data.keys():
+            self.shares_v2_client.delete_access_metadata(access["id"], key)
+
+        # verify deletion of metadata
+        access_without_md = self.shares_v2_client.get_access(access["id"])
+        self.assertEqual({}, access_without_md['metadata'])
+        self.shares_v2_client.delete_access_rule(self.share["id"],
+                                                 access["id"])
+        self.shares_v2_client.wait_for_resource_deletion(
+            rule_id=access["id"], share_id=self.share["id"])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_update_metadata_by_key(self):
+        md2 = {"key7": "value7", "key2": "value6_new"}
+
+        # update metadata
+        self.shares_v2_client.update_access_metadata(
+            access_id=self.access['id'], metadata=md2)
+        # get metadata
+        get_access = self.shares_v2_client.get_access(self.access['id'])
+
+        # verify metadata
+        self.md1.update(md2)
+        self.assertEqual(self.md1, get_access['metadata'])
+
+    @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
+    def test_list_access_filter_by_metadata(self):
+        data = {"key3": "v3", "key4": "value4"}
+        # set metadata
+        access = self.shares_v2_client.create_access_rule(
+            self.share["id"], self.access_type,
+            self.access_to[self.access_type].pop(), 'rw', metadata=data)
+
+        # list metadata with metadata filter
+        list_access = self.shares_v2_client.list_access_rules(
+            share_id=self.share["id"], metadata={'metadata': data})
+
+        # verify metadata
+        self.assertEqual(1, len(list_access))
+        self.assertEqual(access['metadata'], list_access[0]['metadata'])
+        self.assertEqual(access['id'], list_access[0]['id'])
diff --git a/manila_tempest_tests/tests/api/test_access_rules_metadata_negative.py b/manila_tempest_tests/tests/api/test_access_rules_metadata_negative.py
new file mode 100644
index 0000000..75790ea
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_access_rules_metadata_negative.py
@@ -0,0 +1,80 @@
+# Copyright 2018 Huawei 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.
+
+import ddt
+from tempest import config
+from tempest.lib import exceptions as lib_exc
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import constants
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+@base.skip_if_microversion_lt(constants.MIN_SHARE_GROUP_MICROVERSION)
+@ddt.ddt
+class AccessesMetadataNegativeTest(base.BaseSharesTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(AccessesMetadataNegativeTest, cls).resource_setup()
+        if not (any(p in CONF.share.enable_ip_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_user_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_cert_rules_for_protocols
+                    for p in cls.protocols) or
+                any(p in CONF.share.enable_cephx_rules_for_protocols
+                    for p in cls.protocols)):
+            cls.message = "Rule tests are disabled"
+            raise cls.skipException(cls.message)
+        if CONF.share.enable_ip_rules_for_protocols:
+            cls.protocol = CONF.share.enable_ip_rules_for_protocols[0]
+            cls.access_type = "ip"
+            cls.access_to = utils.rand_ip()
+        elif CONF.share.enable_user_rules_for_protocols:
+            cls.protocol = CONF.share.enable_user_rules_for_protocols[0]
+            cls.access_type = "user"
+            cls.access_to = CONF.share.username_for_user_rules
+        elif CONF.share.enable_cert_rules_for_protocols:
+            cls.protocol = CONF.share.enable_cert_rules_for_protocols[0]
+            cls.access_type = "cert"
+            cls.access_to = "client3.com"
+        elif CONF.share.enable_cephx_rules_for_protocols:
+            cls.protocol = CONF.share.enable_cephx_rules_for_protocols[0]
+            cls.access_type = "cephx"
+            cls.access_to = "eve"
+        cls.shares_v2_client.share_protocol = cls.protocol
+        cls.share = cls.create_share()
+        cls.access = cls.shares_v2_client.create_access_rule(
+            cls.share["id"], cls.access_type, cls.access_to,
+            'rw', metadata={u"key1": u"value1"})
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    @ddt.data({'data': {"": "value"}}, {'data': {"k" * 256: "value"}},
+              {'data': {"key": "x" * 1024}})
+    @ddt.unpack
+    def test_try_upd_access_metadata_error(self, data):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.shares_v2_client.update_access_metadata,
+                          self.access["id"], data)
+
+    @tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
+    def test_try_delete_unexisting_access_metadata(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.shares_v2_client.delete_access_metadata,
+                          self.access["id"], "wrong_key")
diff --git a/manila_tempest_tests/tests/api/test_rules.py b/manila_tempest_tests/tests/api/test_rules.py
index 0b44a5c..bb4bfd0 100644
--- a/manila_tempest_tests/tests/api/test_rules.py
+++ b/manila_tempest_tests/tests/api/test_rules.py
@@ -488,7 +488,8 @@
         cls.share = cls.create_share()
 
     @tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
-    @ddt.data(*set(['1.0', '2.9', '2.27', '2.28', LATEST_MICROVERSION]))
+    @ddt.data(*set(
+        ['1.0', '2.9', '2.27', '2.28', '2.45', LATEST_MICROVERSION]))
     def test_list_access_rules(self, version):
         if (utils.is_microversion_lt(version, '2.13') and
                 CONF.share.enable_cephx_rules_for_protocols):
@@ -496,6 +497,9 @@
                    "version >= 2.13." % version)
             raise self.skipException(msg)
 
+        metadata = None
+        if utils.is_microversion_ge(version, '2.45'):
+            metadata = {'key1': 'v1', 'key2': 'v2'}
         # create rule
         if utils.is_microversion_eq(version, '1.0'):
             rule = self.shares_client.create_access_rule(
@@ -503,7 +507,7 @@
         else:
             rule = self.shares_v2_client.create_access_rule(
                 self.share["id"], self.access_type, self.access_to,
-                version=version)
+                metadata=metadata, version=version)
 
         # verify added rule keys since 2.33 when create rule
         if utils.is_microversion_ge(version, '2.33'):
@@ -543,6 +547,8 @@
             keys += ("access_key", )
         if utils.is_microversion_ge(version, '2.33'):
             keys += ("created_at", "updated_at", )
+        if utils.is_microversion_ge(version, '2.45'):
+            keys += ("metadata",)
         for key in keys:
             [self.assertIn(key, r.keys()) for r in rules]
         for key in ('deleted', 'deleted_at', 'instance_mappings'):
@@ -625,7 +631,11 @@
             self.assertRaises(lib_exc.NotFound,
                               self.shares_client.list_access_rules,
                               share['id'])
-        else:
+        elif utils.is_microversion_lt(version, '2.45'):
             self.assertRaises(lib_exc.NotFound,
                               self.shares_v2_client.list_access_rules,
                               share['id'], version)
+        else:
+            self.assertRaises(lib_exc.BadRequest,
+                              self.shares_v2_client.list_access_rules,
+                              share['id'], version)