Add tests for encryption_key_ref
Positive test -
1. create barbican secret and create share using secret UUID
Negative test -
1. invalid encryption key ref
2. invalid share type extra-spec
3. absent encryption key ref
partially-implements: blueprint share-encryption
Depends-On: https://review.opendev.org/c/openstack/requirements/+/963685
Change-Id: I3145f9cd6847464b2920f1b0a35e6c211e45b26e
Signed-off-by: Kiran Pawar <kinpaa@gmail.com>
diff --git a/manila_tempest_tests/common/barbican_client_mgr.py b/manila_tempest_tests/common/barbican_client_mgr.py
new file mode 100644
index 0000000..ee8b53f
--- /dev/null
+++ b/manila_tempest_tests/common/barbican_client_mgr.py
@@ -0,0 +1,73 @@
+# Copyright 2025 Cloudifcation GmbH. 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
+import secrets
+
+from oslo_log import log as logging
+from tempest import config
+from tempest.lib.services import clients
+from tempest import test
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class BarbicanClientManager(test.BaseTestCase):
+ """Class for interacting with the barbican service.
+
+ This class is an abstraction for interacting with the barbican service.
+ """
+
+ credentials = ['primary',]
+
+ @classmethod
+ def setup_clients(cls, tempest_client_mgr):
+ super(BarbicanClientManager, cls).setup_clients()
+ if CONF.identity.auth_version == 'v3':
+ auth_uri = CONF.identity.uri_v3
+ else:
+ auth_uri = CONF.identity.uri
+ service_clients = clients.ServiceClients(
+ tempest_client_mgr.credentials,
+ auth_uri)
+ cls.secret_client = service_clients.secret_v1.SecretClient(
+ service='key-manager')
+
+ @classmethod
+ def ref_to_uuid(cls, href):
+ return href.split('/')[-1]
+
+ def store_secret(self):
+ """Store a secret in barbican.
+
+ :returns: The barbican secret_ref.
+ """
+
+ key = secrets.token_bytes(32)
+
+ manila_secret = self.secret_client.create_secret(
+ algorithm='AES',
+ bit_length=256,
+ secret_type='symmetric',
+ payload=base64.b64encode(key).decode(),
+ payload_content_type='application/octet-stream',
+ payload_content_encoding='base64',
+ mode='CBC'
+ )
+ LOG.debug('Manila Secret has ref %s', manila_secret.get('secret_ref'))
+ return manila_secret.get('secret_ref')
+
+ def delete_secret(self, secret_ref):
+ self.secret_client.delete_secret(secret_ref)
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 60d4164..5539bb5 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -40,7 +40,7 @@
"This value is only used to validate the versions "
"response from Manila."),
cfg.StrOpt("max_api_microversion",
- default="2.88",
+ default="2.90",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",
@@ -220,6 +220,11 @@
"ss_type:<ldap, kerberos or active_directory>, "
"ss_dns_ip:value, ss_user:value, ss_password=value, "
"ss_domain:value, ss_server:value"),
+ cfg.ListOpt("capability_encryption_support",
+ default=[],
+ help="Encryption support capability. Possible values are "
+ "share_server, share etc. "),
+
# Switching ON/OFF test suites filtered by features
cfg.BoolOpt("run_quota_tests",
@@ -386,4 +391,7 @@
cfg.DictOpt("driver_assisted_backup_test_driver_options",
default={'dummy': True},
help="Share backup driver options specified as dict."),
+ cfg.BoolOpt("run_encryption_tests",
+ default=False,
+ help="Enable or disable share encryption tests."),
]
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 4b35687..7457fff 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -248,7 +248,7 @@
share_type_id=None, is_public=False,
share_group_id=None, availability_zone=None,
version=LATEST_MICROVERSION, experimental=False,
- scheduler_hints=None):
+ scheduler_hints=None, encryption_key_ref=None):
headers = EXPERIMENTAL if experimental else None
metadata = metadata or {}
scheduler_hints = scheduler_hints or {}
@@ -283,6 +283,8 @@
post_body["share"]["share_group_id"] = share_group_id
if scheduler_hints:
post_body["share"]["scheduler_hints"] = scheduler_hints
+ if encryption_key_ref:
+ post_body["share"]["encryption_key_ref"] = encryption_key_ref
body = json.dumps(post_body)
resp, body = self.post("shares", body, headers=headers,
@@ -1102,7 +1104,7 @@
share_networks=None,
share_groups=None, share_group_snapshots=None,
force=True, share_type=None, share_replicas=None,
- replica_gigabytes=None, url=None,
+ replica_gigabytes=None, encryption_keys=None, url=None,
version=LATEST_MICROVERSION):
if url is None:
url = self._get_quotas_url(version)
@@ -1130,6 +1132,8 @@
put_body["share_replicas"] = share_replicas
if replica_gigabytes is not None:
put_body["replica_gigabytes"] = replica_gigabytes
+ if encryption_keys is not None:
+ put_body["encryption_keys"] = encryption_keys
put_body = json.dumps({"quota_set": put_body})
resp, body = self.put(url, put_body, version=version)
diff --git a/manila_tempest_tests/tests/api/admin/test_quotas.py b/manila_tempest_tests/tests/api/admin/test_quotas.py
index e8377e1..74c4622 100644
--- a/manila_tempest_tests/tests/api/admin/test_quotas.py
+++ b/manila_tempest_tests/tests/api/admin/test_quotas.py
@@ -66,6 +66,8 @@
if utils.share_replica_quotas_are_supported():
self.assertGreater(int(quotas["share_replicas"]), -2)
self.assertGreater(int(quotas["replica_gigabytes"]), -2)
+ if utils.encryption_keys_quota_supported():
+ self.assertGreater(int(quotas["encryption_keys"]), -2)
@decorators.idempotent_id('1ff57cfa-cd8d-495f-86eb-9fead307428e')
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
@@ -82,6 +84,8 @@
if utils.share_replica_quotas_are_supported():
self.assertGreater(int(quotas["share_replicas"]), -2)
self.assertGreater(int(quotas["replica_gigabytes"]), -2)
+ if utils.encryption_keys_quota_supported():
+ self.assertGreater(int(quotas["encryption_keys"]), -2)
@decorators.idempotent_id('9b96dd45-7c0d-41ee-88e4-600185f61358')
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
@@ -471,6 +475,19 @@
self.assertEqual(new_quota, int(updated["share_networks"]))
+ @decorators.idempotent_id('78957d97-afad-4371-a21e-79641fff83f7')
+ @tc.attr(base.TAG_POSITIVE, base.TAG_API)
+ @utils.skip_if_microversion_not_supported("2.90")
+ def test_update_tenant_quota_encryption_keys(self):
+ # get current quotas
+ quotas = self.client.show_quotas(self.tenant_id)['quota_set']
+ new_quota = int(quotas["encryption_keys"]) + 2
+
+ # set new quota for encryption keys
+ updated = self.update_quotas(self.tenant_id, encryption_keys=new_quota)
+
+ self.assertEqual(new_quota, int(updated["encryption_keys"]))
+
@decorators.idempotent_id('84e24c32-ee78-461e-ac1f-f9e4d99f88e2')
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
def test_reset_tenant_quotas(self):
@@ -496,6 +513,8 @@
if utils.share_replica_quotas_are_supported():
data["share_replicas"] = int(custom["share_replicas"]) + 2
data["replica_gigabytes"] = int(custom["replica_gigabytes"]) + 2
+ if utils.encryption_keys_quota_supported():
+ data["encryption_keys"] = int(custom["encryption_keys"]) + 2
# set new quota, turn off cleanup - we'll do it right below
updated = self.update_quotas(self.tenant_id, cleanup=False, **data)
@@ -518,6 +537,9 @@
data["share_replicas"], int(updated["share_replicas"]))
self.assertEqual(
data["replica_gigabytes"], int(updated["replica_gigabytes"]))
+ if utils.encryption_keys_quota_supported():
+ self.assertEqual(
+ data["encryption_keys"], int(updated["encryption_keys"]))
# Reset customized quotas
self.client.reset_quotas(self.tenant_id)
@@ -545,6 +567,10 @@
self.assertEqual(
int(default["replica_gigabytes"]),
int(reseted["replica_gigabytes"]))
+ if utils.encryption_keys_quota_supported():
+ self.assertEqual(
+ int(default["encryption_keys"]),
+ int(reseted["encryption_keys"]))
def _get_new_replica_quota_values(self, default_quotas, value_to_set):
new_values = {
diff --git a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
index 5b4ff1d..2534104 100644
--- a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
+++ b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py
@@ -80,6 +80,7 @@
{"gigabytes": -2},
{"snapshot_gigabytes": -2},
{"share_networks": -2},
+ {"encryption_keys": -2},
)
@decorators.idempotent_id('07d3e69a-7cda-4ca7-9fea-c32f6830fdd3')
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
diff --git a/manila_tempest_tests/tests/api/test_share_encryption.py b/manila_tempest_tests/tests/api/test_share_encryption.py
new file mode 100644
index 0000000..e199114
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_encryption.py
@@ -0,0 +1,94 @@
+# Copyright 2025 Cloudifcation GmbH. 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 oslo_log import log
+from tempest import config
+from tempest.lib import decorators
+from testtools import testcase as tc
+
+from manila_tempest_tests.common import barbican_client_mgr
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
+@ddt.ddt
+class ShareEncryptionNFSTest(base.BaseSharesMixedTest):
+ """Covers share functionality, that is related to NFS share type."""
+ protocol = "nfs"
+
+ @classmethod
+ def skip_checks(cls):
+ super(ShareEncryptionNFSTest, cls).skip_checks()
+ if not CONF.share.run_encryption_tests:
+ raise cls.skipException('Encryption tests are disabled.')
+ utils.check_skip_if_microversion_not_supported("2.90")
+
+ if cls.protocol not in CONF.share.enable_protocols:
+ message = "%s tests are disabled" % cls.protocol
+ raise cls.skipException(message)
+
+ if ('share_server' not in CONF.share.capability_encryption_support and
+ 'share' not in CONF.share.capability_encryption_support):
+ message = "Unsupported value of encryption support capability"
+ raise cls.skipException(message)
+
+ @classmethod
+ def resource_setup(cls):
+ super(ShareEncryptionNFSTest, cls).resource_setup()
+
+ extra_specs = {
+ 'driver_handles_share_servers': CONF.share.multitenancy_enabled,
+ }
+ if 'share_server' in CONF.share.capability_encryption_support:
+ extra_specs.update({'encryption_support': 'share_server'})
+ elif 'share' in CONF.share.capability_encryption_support:
+ extra_specs.update({'encryption_support': 'share'})
+
+ # create share_type
+ cls.share_type_enc = cls.create_share_type(extra_specs=extra_specs)
+ cls.share_type_enc_id = cls.share_type_enc['id']
+
+ # setup barbican client
+ cls.barbican_mgr = barbican_client_mgr.BarbicanClientManager()
+ cls.barbican_mgr.setup_clients(cls.os_primary)
+
+ @decorators.idempotent_id('21ad41fb-04cf-493c-bc2f-66c80220898c')
+ @tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
+ def test_create_share_with_share_server_encryption_key_ref(self):
+
+ secret_href = self.barbican_mgr.store_secret()
+ secret_href_uuid = self.barbican_mgr.ref_to_uuid(secret_href)
+
+ share = self.create_share(
+ share_protocol=self.protocol,
+ share_type_id=self.share_type_enc_id,
+ share_network_id=self.shares_v2_client.share_network_id,
+ size=1,
+ name="encrypted_share",
+ encryption_key_ref=secret_href_uuid,
+ cleanup_in_class=False)
+
+ self.assertEqual(share['encryption_key_ref'], secret_href_uuid)
+
+ # Delete Barbican secret
+ self.barbican_mgr.delete_secret(secret_href_uuid)
+
+
+class ShareEncryptionCIFSTest(ShareEncryptionNFSTest):
+ """Covers share functionality, that is related to CIFS share type."""
+ protocol = "cifs"
diff --git a/manila_tempest_tests/tests/api/test_share_encryption_negative.py b/manila_tempest_tests/tests/api/test_share_encryption_negative.py
new file mode 100644
index 0000000..3d37426
--- /dev/null
+++ b/manila_tempest_tests/tests/api/test_share_encryption_negative.py
@@ -0,0 +1,74 @@
+# Copyright 2025 Cloudifcation GmbH. 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 import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+from testtools import testcase as tc
+
+from manila_tempest_tests.tests.api import base
+from manila_tempest_tests import utils
+
+CONF = config.CONF
+
+
+class SharesEncryptionNegativeTest(base.BaseSharesMixedTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(SharesEncryptionNegativeTest, cls).skip_checks()
+ if not CONF.share.run_encryption_tests:
+ raise cls.skipException('Encryption tests are disabled.')
+ utils.check_skip_if_microversion_not_supported("2.90")
+
+ @classmethod
+ def resource_setup(cls):
+ super(SharesEncryptionNegativeTest, cls).resource_setup()
+ # create share_type
+ cls.no_encryption_type = cls.create_share_type()
+ cls.no_encryption_type_id = cls.no_encryption_type['id']
+ cls.encryption_type = cls.create_share_type(
+ extra_specs={
+ 'encryption_support': 'share_server',
+ })
+ cls.encryption_type_id = cls.encryption_type['id']
+
+ @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe81')
+ @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+ def test_create_share_with_invalid_share_type(self):
+ # should not create share when encryption isn't supported by
+ # share type
+ self.assertRaises(lib_exc.BadRequest,
+ self.shares_v2_client.create_share,
+ share_type_id=self.no_encryption_type_id,
+ encryption_key_ref='fake_ref')
+
+ @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe88')
+ @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+ def test_create_share_with_invalid_encryption_key_ref(self):
+ # should not create share when key ref is invalid UUID
+ self.assertRaises(lib_exc.BadRequest,
+ self.shares_v2_client.create_share,
+ share_type_id=self.encryption_type_id,
+ encryption_key_ref='fake_ref')
+
+ @decorators.idempotent_id('b8097d56-067e-4d7c-8401-31bc7021fe82')
+ @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
+ def test_create_share_with_encryption_key_ref_absent_in_barbican(self):
+ # should not create share when key ref is not present in barbican
+ self.assertRaises(
+ lib_exc.BadRequest,
+ self.shares_v2_client.create_share,
+ share_type_id=self.encryption_type_id,
+ encryption_key_ref='cfbe8ae1-7932-43f2-bf82-3fd3ddba30c3')
diff --git a/manila_tempest_tests/utils.py b/manila_tempest_tests/utils.py
index d7d86f1..56a28e2 100644
--- a/manila_tempest_tests/utils.py
+++ b/manila_tempest_tests/utils.py
@@ -27,6 +27,7 @@
CONF = config.CONF
SHARE_NETWORK_SUBNETS_MICROVERSION = '2.51'
SHARE_REPLICA_QUOTAS_MICROVERSION = "2.53"
+ENCRYPTION_KEYS_QUOTA_MICROVERSION = "2.90"
EXPERIMENTAL = {'X-OpenStack-Manila-API-Experimental': 'True'}
@@ -276,6 +277,10 @@
return is_microversion_supported(SHARE_REPLICA_QUOTAS_MICROVERSION)
+def encryption_keys_quota_supported():
+ return is_microversion_supported(ENCRYPTION_KEYS_QUOTA_MICROVERSION)
+
+
def share_network_get_default_subnet(share_network):
return next((
subnet for subnet in share_network.get('share_network_subnets', [])