Use encryption key ref during share create

- Share type extra-spec "encryption_support" should be set to
 "share_server".
- Use encryption_key_ref from user provided share create API option
- Create ACL for barbican user from current user context, so barbican
user can fetch the secret from secret_ref in backend driver.
- Create server_encryption_keys project level quota which helps to
control the encrypted share servers. Such share servers do not support
migration/unmanage etc.
- Create application credentials for barbican user during share server
creation and delete during share server deletion.

partially-implements: blueprint share-encryption
Change-Id: I9e3ff98e12027eaf75cdc5f6089a016c102315d3
Signed-off-by: Kiran Pawar <kinpaa@gmail.com>
This commit is contained in:
Kiran Pawar committed 2025-08-27 15:40:45 +00:00
1 parent d49fbdc9b7
commit 82a94bbcec
59 files changed
+1470 -58

No files matched your search

+59
View File
@@ -344,6 +344,14 @@ detail_id:
type: string
description: >
The ID of the message detail.
encryption_key_ref_query:
description: |
The encryption key ref that can be used to filter shares or
share instances.
in: query
required: false
type: string
min_version: 2.90
export_location_id_query:
description: |
The export location UUID that can be used to filter shares or
@@ -1369,6 +1377,21 @@ driver_options:
in: body
required: false
type: object
encryption_key_ref_request:
description: |
The encryption key ref is valid barbican secret UUID that will be used to
get encryption key by the storage drivers.
in: body
required: false
type: object
min_version: 2.90
encryption_key_ref_response:
description: |
The encryption key ref is valid barbican secret UUID that will be used to
get encryption key by the storage drivers.
in: body
required: true
type: object
export_location:
description: |
The export location. For newer API versions it is available in
@@ -1805,6 +1828,20 @@ maxTotalBackupGigabytesOptional:
required: false
type: integer
min_version: 2.80
maxTotalEncryptionKeys:
description: |
The number of encryption keys allowed in a project.
in: body
required: true
type: integer
min_version: 2.90
maxTotalEncryptionKeysOptional:
description: |
The number of encryption keys allowed in a project.
in: body
required: false
type: integer
min_version: 2.90
maxTotalReplicaGigabytes:
description: |
The maximum number of replica gigabytes that are allowed in a project.
@@ -2384,6 +2421,28 @@ quota_class_set:
in: body
required: true
type: object
quota_encryption_keys:
description: |
The number of encryption keys allowed for each project.
in: body
required: true
min_version: 2.90
type: integer
quota_encryption_keys_detail:
description: |
The limit, in_use, reserved number of encryption keys
for each project.
in: body
min_version: 2.90
required: true
type: object
quota_encryption_keys_request:
description: |
The number of encryption keys allowed for each project.
in: body
min_version: 2.90
required: false
type: integer
quota_gigabytes:
description: |
The number of gigabytes allowed for each project.
+4
View File
@@ -12,6 +12,7 @@ Quota classes can be shown and updated for a project.
version 2.62.
Share backups and backup gigabytes were added to quota management
APIs in API version 2.80.
Encryption keys was added to quota management APIs in API version 2.90.
Show quota classes for a project
@@ -62,6 +63,7 @@ Response Parameters
- per_share_gigabytes: perShareGigabytes
- backups: maxTotalShareBackups
- backup_gigabytes: maxTotalBackupGigabytes
- encryption_keys: maxTotalEncryptionKeys
Response Example
----------------
@@ -108,6 +110,7 @@ Request
- per-share-gigabytes: perShareGigabytesOptional
- backups: maxTotalShareBackupsOptional
- backup-gigabytes: maxTotalBackupGigabytesOptional
- encryption_keys: maxTotalEncryptionKeysOptional
Request Example
---------------
@@ -134,6 +137,7 @@ Response Parameters
- per_share_gigabytes: perShareGigabytes
- backups: maxTotalShareBackups
- backup_gigabytes: maxTotalBackupGigabytes
- encryption_keys: maxTotalEncryptionKeys
Response Example
----------------
+8
View File
@@ -24,6 +24,7 @@ Provides quotas management support.
- ``per_share_gigabytes`` (since API version 2.62)
- ``backups`` (since API version 2.80)
- ``backup_gigabytes`` (since API version 2.80)
- ``encryption_keys`` (since API version 2.90)
In order to manipulate share type quotas, the requests will be similar
to the examples below, except that the ``user_id={user_id}`` must be
@@ -41,6 +42,8 @@ Provides quotas management support.
Share backups and backup gigabytes were added to quota management
APIs in API version 2.80.
Encryption keys was added to quota management APIs in API version 2.90.
Show default quota set
~~~~~~~~~~~~~~~~~~~~~~
@@ -90,6 +93,7 @@ Response parameters
- per_share_gigabytes: quota_per_share_gigabytes
- backups: quota_backups
- backup_gigabytes: quota_backup_gigabytes
- encryption_keys: quota_encryption_keys
Response example
----------------
@@ -151,6 +155,7 @@ Response parameters
- per_share_gigabytes: quota_per_share_gigabytes
- backups: quota_backups
- backup_gigabytes: quota_backup_gigabytes
- encryption_keys: quota_encryption_keys
Response example
----------------
@@ -214,6 +219,7 @@ Response parameters
- per_share_gigabytes: quota_per_share_gigabytes_detail
- backups: quota_backups_detail
- backup_gigabytes: quota_backup_gigabytes_detail
- encryption_keys: quota_encryption_keys_detail
Response example
----------------
@@ -269,6 +275,7 @@ Request
- per_share_gigabytes: quota_per_share_gigabytes_request
- backups: quota_backups_request
- backup_gigabytes: quota_backup_gigabytes_request
- encryption_keys: quota_encryption_keys_request
Request example
---------------
@@ -295,6 +302,7 @@ Response parameters
- per_share_gigabytes: quota_per_share_gigabytes
- backups: quota_backups
- backup_gigabytes: quota_backup_gigabytes
- encryption_keys: quota_encryption_keys
Response example
----------------
@@ -16,6 +16,7 @@
"scheduler_hints": {
"same_host": "d9c66489-cf02-4156-b0f2-527f3211b243,4ffee55f-ba98-42d2-a8ce-e7cecb169182",
"different_host": "903685eb-f242-4105-903d-4bef2db94be4"
}
},
"encryption_key_ref": "86babe9b-7277-4c3a-a081-6eb3eac9231d"
}
}
+2
View File
@@ -135,6 +135,7 @@ Response parameters
- is_auto_deletable: share_server_show_is_auto_deletable
- share_network_subnet_id: share_network_subnet_id_share_server_body
- security_service_update_support: security_service_update_support
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -281,6 +282,7 @@ Response parameters
- identifier: identifier
- share_network_subnet_id: share_network_subnet_id_manage_server_body
- security_service_update_support: security_service_update_support
- encryption_key_ref: encryption_key_ref_response
Response examples
+9
View File
@@ -144,6 +144,7 @@ Request
- offset: offset
- sort_key: sort_key
- sort_dir: sort_dir
- encryption_key_ref: encryption_key_ref_query
Response parameters
-------------------
@@ -214,6 +215,7 @@ Request
- offset: offset
- sort_key: sort_key
- sort_dir: sort_dir
- encryption_key_ref: encryption_key_ref_query
Response parameters
-------------------
@@ -257,6 +259,7 @@ Response parameters
- export_locations: export_locations
- is_soft_deleted: is_soft_deleted_response
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
- encryption_key_ref: encryption_key_ref_response
Response example
@@ -337,6 +340,7 @@ Response parameters
- export_locations: export_locations
- is_soft_deleted: is_soft_deleted_response
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -405,6 +409,7 @@ Response parameters
- access_rules_status: access_rules_status
- share_type_id: share_type_id
- id: id_13
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -455,6 +460,7 @@ Request
- share_network_id: share_network_id_request
- availability_zone: availability_zone_request
- scheduler_hints: scheduler_hints
- encryption_key_ref: encryption_key_ref_request
Request example
---------------
@@ -503,6 +509,7 @@ Response parameters
- export_locations: export_locations
- is_soft_deleted: is_soft_deleted_response
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -610,6 +617,7 @@ Response parameters
- export_locations: export_locations
- is_soft_deleted: is_soft_deleted_response
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -710,6 +718,7 @@ Response parameters
- export_locations: export_locations
- is_soft_deleted: is_soft_deleted_response
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
- encryption_key_ref: encryption_key_ref_response
Response example
----------------
@@ -63,6 +63,10 @@ important terms:
+------------------------------------+-------------------------+---------------------------------------------------------+
| mount_point_name_support | true or false | share can or cannot have customized export location |
+------------------------------------+-------------------------+---------------------------------------------------------+
| encryption_support | share | share is encrypted with share encryption key |
| +-------------------------+---------------------------------------------------------+
| | share_server | share is encrypted with share server encryption key |
+------------------------------------+-------------------------+---------------------------------------------------------+
| provisioning:mount_point_prefix | string | prefix used for custom export location |
+------------------------------------+-------------------------+---------------------------------------------------------+
@@ -505,6 +509,58 @@ Create a share
+---------------------------------------+-------------------------------------------------------------------------+
* Create a share using encryption key reference.
User can create share using their own encryption key. The key must be stored
in key-manager (Openstack Barbican) service. First create share type and
specify extra-spec ``encryption_support``. It can have value ``share`` or
``share_server`` based on support by backend storage driver. Then use
`--encryption-key-ref` option in share create command. Users can use
encryption key reference or UUID of key reference here.
.. code-block:: console
$ openstack share create NFS 1 \
--name myshare3 \
--description "My Manila share - Encrypted" \
--share-network mysharenetwork \
--share-type encrypted_share_type \
--encryption-key-ref 86babe9b-7277-4c3a-a081-6eb3eac9231d
+---------------------------------------+-----------------------------------------------------------------------+
| Property | Value |
+---------------------------------------+-----------------------------------------------------------------------+
| id | 40de4f4c-4588-4d9c-844b-f74d8951053a |
| size | 1 |
| availability_zone | None |
| created_at | 2020-08-07T05:24:14.000000 |
| status | creating |
| name | myshare3 |
| description | My Manila share - Encrypted |
| project_id | d9932a60d9ee4087b6cff9ce6e9b4e3b |
| snapshot_id | None |
| share_network_id | c4bfdd5e-7502-4a65-8876-0ce8b9914a64 |
| share_proto | NFS |
| metadata | {} |
| share_type | af7b64ec-cdb3-4a5f-93c9-51672d72e172 |
| is_public | False |
| snapshot_support | True |
| task_state | None |
| share_type_name | encrypted_share_type |
| access_rules_status | active |
| replication_type | None |
| has_replicas | False |
| user_id | 2cebd96a794f431caa06ce5215e0da21 |
| create_share_from_snapshot_support | True |
| revert_to_snapshot_support | True |
| share_group_id | None |
| encryption_key_ref | 86babe9b-7277-4c3a-a081-6eb3eac9231d |
| source_share_group_snapshot_member_id | None |
| mount_snapshot_support | True |
| progress | None |
+---------------------------------------+-----------------------------------------------------------------------+
Grant and revoke share access
-----------------------------
+2 -1
View File
@@ -208,13 +208,14 @@ REST_API_VERSION_HISTORY = """
* 2.88 - Added support for update Share access rule.
* 2.89 - Added support for passing Share network subnet metadata updates
to driver.
* 2.90 - Added encryption key reference option to share create API.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.89"
_MAX_API_VERSION = "2.90"
DEFAULT_API_VERSION = _MIN_API_VERSION
@@ -477,3 +477,8 @@ user documentation.
------------------------------
Added support for passing share network subnet metadata updates to share
backend driver.
2.90
----
Added ``encryption_key_ref`` option to share create API.
+24 -4
View File
@@ -257,7 +257,7 @@ class ShareMixin(object):
def _create(self, req, body,
check_create_share_from_snapshot_support=False,
check_availability_zones_extra_spec=False,
scheduler_hints=None):
scheduler_hints=None, encryption_key_ref=None):
"""Creates a new share."""
context = req.environ['manila.context']
@@ -413,11 +413,16 @@ class ShareMixin(object):
# Only use in create share feature. Create share from snapshot
# and create share with share group features not
# need this check.
if share_type and share_type.get('extra_specs'):
dhss = (strutils.bool_from_string(
share_type.get('extra_specs').get(
'driver_handles_share_servers')))
else:
dhss = False
if (not share_network_id and not snapshot
and not share_group_id
and share_type and share_type.get('extra_specs')
and (strutils.bool_from_string(share_type.get('extra_specs').
get('driver_handles_share_servers')))):
and dhss):
msg = _('Share network must be set when the '
'driver_handles_share_servers is true.')
raise exc.HTTPBadRequest(explanation=msg)
@@ -440,12 +445,27 @@ class ShareMixin(object):
payload = {'type': type_chosen, 'az': availability_zone}
raise exc.HTTPBadRequest(explanation=msg % payload)
if share_type and encryption_key_ref:
type_enc = share_type.get(
'extra_specs', {}).get('encryption_support')
if type_enc not in constants.SUPPORTED_ENCRYPTION_TYPES:
msg = _("Share type %(type)s extra-specs 'encryption_support' "
"is missing valid value e.g. share, share_server.")
payload = {'type': share_type}
raise exc.HTTPBadRequest(explanation=msg % payload)
if not dhss:
msg = _("Share type %(type)s must set dhss=True for share "
"encryption.")
payload = {'type': share_type}
raise exc.HTTPBadRequest(explanation=msg % payload)
if share_type:
kwargs['share_type'] = share_type
if share_network_id:
kwargs['share_network_id'] = share_network_id
kwargs['scheduler_hints'] = scheduler_hints
kwargs['encryption_key_ref'] = encryption_key_ref
if req.api_version_request >= api_version.APIVersionRequest("2.84"):
kwargs['mount_point_name'] = share.pop('mount_point_name', None)
+5 -1
View File
@@ -151,7 +151,8 @@ class QuotaSetsMixin(object):
body.get('replica_gigabytes') is None and
body.get('per_share_gigabytes') is None and
body.get('backups') is None and
body.get('backup_gigabytes') is None):
body.get('backup_gigabytes') is None and
body.get('encryption_keys') is None):
msg = _("Must supply at least one quota field to update.")
raise webob.exc.HTTPBadRequest(explanation=msg)
@@ -351,6 +352,9 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
elif req.api_version_request < api_version.APIVersionRequest("2.80"):
self._ensure_specific_microversion_args_are_absent(
body, ['backups', 'backup_gigabytes'], "2.80")
elif req.api_version_request < api_version.APIVersionRequest("2.90"):
self._ensure_specific_microversion_args_are_absent(
body, ['encryption_keys'], "2.90")
return self._update(req, id, body)
@wsgi.Controller.api_version('2.7')
+5
View File
@@ -176,6 +176,11 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
"since it has been soft deleted.") % share_id
raise exc.HTTPForbidden(explanation=msg)
if share_ref.get('encryption_key_ref'):
msg = _("Replica cannot be created for share '%s' "
"since it is encrypted.") % share_id
raise exc.HTTPForbidden(explanation=msg)
share_network_id = body.get('share_replica').get('share_network_id')
if share_network_id:
if req.api_version_request < api_version.APIVersionRequest("2.72"):
+15
View File
@@ -114,6 +114,11 @@ class ShareServerController(share_servers.ShareServerController,
"subnets.")
raise exc.HTTPBadRequest(explanation=msg)
if share_server.get('encryption_key_ref'):
msg = _("Cannot unmanage the share server containing encryption "
"key reference")
raise exc.HTTPBadRequest(explanation=msg)
share_network_id = share_server['share_network_id']
share_network = db_api.share_network_get(context, share_network_id)
common.check_share_network_is_active(share_network)
@@ -242,6 +247,11 @@ class ShareServerController(share_servers.ShareServerController,
if not params:
raise exc.HTTPBadRequest(explanation=_("Request is missing body."))
if share_server['encryption_key_ref']:
msg = _("Cannot migrate the share server containing encryption "
"key reference")
raise exc.HTTPBadRequest(explanation=msg)
bool_params = ['writable', 'nondisruptive', 'preserve_snapshots']
mandatory_params = bool_params + ['host']
@@ -370,6 +380,11 @@ class ShareServerController(share_servers.ShareServerController,
if not params:
raise exc.HTTPBadRequest(explanation=_("Request is missing body."))
if share_server.get('encryption_key_ref'):
msg = _("Cannot migrate the share server containing encryption "
"key reference")
raise exc.HTTPBadRequest(explanation=msg)
bool_params = ['writable', 'nondisruptive', 'preserve_snapshots']
mandatory_params = bool_params + ['host']
+29 -5
View File
@@ -194,20 +194,38 @@ class ShareController(wsgi.Controller,
return data
@wsgi.Controller.api_version("2.65")
@wsgi.Controller.api_version("2.90")
def create(self, req, body):
if not self.is_valid_body(body, 'share'):
raise exc.HTTPUnprocessableEntity()
share = body['share']
scheduler_hints = share.pop('scheduler_hints', None)
encryption_key_ref = share.pop('encryption_key_ref', None)
return self._create(
req, body,
check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True,
scheduler_hints=scheduler_hints,
encryption_key_ref=encryption_key_ref)
@wsgi.Controller.api_version("2.65", "2.89")
def create(self, req, body): # pylint: disable=function-redefined # noqa F811
if not self.is_valid_body(body, 'share'):
raise exc.HTTPUnprocessableEntity()
share = body['share']
scheduler_hints = share.pop('scheduler_hints', None)
if req.api_version_request < api_version.APIVersionRequest("2.67"):
if scheduler_hints:
scheduler_hints.pop('only_host', None)
return self._create(req, body,
check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True,
scheduler_hints=scheduler_hints)
return self._create(
req, body,
check_create_share_from_snapshot_support=True,
check_availability_zones_extra_spec=True,
scheduler_hints=scheduler_hints)
@wsgi.Controller.api_version("2.48", "2.64") # noqa
def create(self, req, body): # pylint: disable=function-redefined # noqa F811
@@ -605,6 +623,9 @@ class ShareController(wsgi.Controller,
if req.api_version_request < api_version.APIVersionRequest("2.69"):
req.GET.pop('is_soft_deleted', None)
if req.api_version_request < api_version.APIVersionRequest("2.90"):
req.GET.pop('encryption_key_ref', None)
return self._get_shares(req, is_detail=False)
@wsgi.Controller.api_version("2.0")
@@ -623,6 +644,9 @@ class ShareController(wsgi.Controller,
if req.api_version_request < api_version.APIVersionRequest("2.69"):
req.GET.pop('is_soft_deleted', None)
if req.api_version_request < api_version.APIVersionRequest("2.90"):
req.GET.pop('encryption_key_ref', None)
return self._get_shares(req, is_detail=True)
def _validate_metadata_for_update(self, req, share_id, metadata,
+5
View File
@@ -24,6 +24,7 @@ class ViewBuilder(common.ViewBuilder):
"add_share_replica_quotas",
"add_per_share_gigabytes_quotas",
"add_share_backup_quotas",
"add_encryption_keys_quotas",
]
def detail_list(self, request, quota_class_set, quota_class=None):
@@ -64,3 +65,7 @@ class ViewBuilder(common.ViewBuilder):
def add_share_backup_quotas(self, context, view, quota_class_set):
view['backups'] = quota_class_set.get('backups')
view['backup_gigabytes'] = quota_class_set.get('backup_gigabytes')
@common.ViewBuilder.versioned_method("2.90")
def add_encryption_keys_quotas(self, context, view, quota_class_set):
view['encryption_keys'] = quota_class_set.get('encryption_keys')
+5
View File
@@ -24,6 +24,7 @@ class ViewBuilder(common.ViewBuilder):
"add_share_replica_quotas",
"add_per_share_gigabytes_quotas",
"add_share_backup_quotas",
"add_encryption_keys_quotas",
]
def detail_list(self, request, quota_set, project_id=None,
@@ -70,3 +71,7 @@ class ViewBuilder(common.ViewBuilder):
def add_share_backup_quotas(self, context, view, quota_set):
view['backups'] = quota_set.get('backups')
view['backup_gigabytes'] = quota_set.get('backup_gigabytes')
@common.ViewBuilder.versioned_method("2.90")
def add_encryption_keys_quotas(self, context, view, quota_set):
view['encryption_keys'] = quota_set.get('encryption_keys')
+8 -1
View File
@@ -25,7 +25,8 @@ class ViewBuilder(common.ViewBuilder):
"add_share_network_subnet_id_field",
"add_task_state_and_source_server_fields",
"add_sec_service_update_fields",
"add_share_network_subnet_ids_and_network_allocation_update_support"
"add_share_network_subnet_ids_and_network_allocation_update_support",
"add_encryption_key_ref_field",
]
def build_share_server(self, request, share_server):
@@ -100,3 +101,9 @@ class ViewBuilder(common.ViewBuilder):
share_server['share_network_subnet_ids'])
share_server_dict['network_allocation_update_support'] = (
share_server['network_allocation_update_support'])
@common.ViewBuilder.versioned_method("2.90")
def add_encryption_key_ref_field(
self, context, share_server_dict, share_server):
share_server_dict['encryption_key_ref'] = share_server[
'encryption_key_ref']
+6
View File
@@ -39,6 +39,7 @@ class ViewBuilder(common.ViewBuilder):
"translate_creating_from_snapshot_status",
"add_share_recycle_bin_field",
"add_source_backup_id_field",
"add_encryption_key_ref_field",
]
def summary_list(self, request, shares, count=None):
@@ -210,3 +211,8 @@ class ViewBuilder(common.ViewBuilder):
@common.ViewBuilder.versioned_method("2.80")
def add_source_backup_id_field(self, context, share_dict, share):
share_dict['source_backup_id'] = share.get('source_backup_id')
@common.ViewBuilder.versioned_method("2.90")
def add_encryption_key_ref_field(self, context, share_dict, share):
share_dict['encryption_key_ref'] = share.get('instance', {}).get(
'encryption_key_ref')
+6
View File
@@ -156,6 +156,8 @@ INVALID_SHARE_INSTANCE_STATUSES_FOR_ACCESS_RULE_UPDATES = (
TRANSITIONAL_STATUSES
)
SUPPORTED_ENCRYPTION_TYPES = ['share', 'share_server']
SUPPORTED_SHARE_PROTOCOLS = (
'NFS', 'CIFS', 'GLUSTERFS', 'HDFS', 'CEPHFS', 'MAPRFS')
@@ -313,6 +315,7 @@ class ExtraSpecs(object):
MOUNT_SNAPSHOT_SUPPORT = "mount_snapshot_support"
MOUNT_POINT_NAME_SUPPORT = "mount_point_name_support"
AVAILABILITY_ZONES = "availability_zones"
ENCRYPTION_SUPPORT = "encryption_support"
PROVISIONING_MAX_SHARE_SIZE = "provisioning:max_share_size"
PROVISIONING_MIN_SHARE_SIZE = "provisioning:min_share_size"
PROVISIONING_MAX_SHARE_EXTEND_SIZE = "provisioning:max_share_extend_size"
@@ -331,6 +334,7 @@ class ExtraSpecs(object):
MOUNT_SNAPSHOT_SUPPORT,
MOUNT_POINT_NAME_SUPPORT,
AVAILABILITY_ZONES,
ENCRYPTION_SUPPORT,
PROVISIONING_MAX_SHARE_SIZE,
PROVISIONING_MIN_SHARE_SIZE,
PROVISIONING_MAX_SHARE_EXTEND_SIZE,
@@ -362,6 +366,8 @@ class ExtraSpecs(object):
REPLICATION_TYPES = ('writable', 'readable', 'dr')
ENCRYPTION_TYPES = ('share', 'share_server')
class AdminOnlyMetadata(object):
AFFINITY_KEY = "__affinity_same_host"
+12
View File
@@ -1892,3 +1892,15 @@ def resource_lock_get(context, lock_id):
def resource_lock_get_all(context, **kwargs):
"""Retrieve all resource locks."""
return IMPL.resource_lock_get_all(context, **kwargs)
##################
def encryption_keys_get_count(context, filters=None):
"""Get count of encryption keys."""
return IMPL.encryption_keys_get_count(context, filters=filters)
def encryption_keys_get_all(context, filters=None):
"""Get all encryption keys."""
return IMPL.encryption_keys_get_all(context, filters=filters)
@@ -0,0 +1,111 @@
# 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.
"""Add share server encryption
Revision ID: e975ea83b712
Revises: 0d8c8f6d54a4
Create Date: 2025-01-20 14:25:29.141460
"""
# revision identifiers, used by Alembic.
revision = 'e975ea83b712'
down_revision = '0d8c8f6d54a4'
from alembic import op
from oslo_log import log
import sqlalchemy as sa
LOG = log.getLogger(__name__)
encryption_refs_table = 'encryption_refs'
share_servers_table = 'share_servers'
share_instances_table = 'share_instances'
def upgrade():
try:
op.add_column(
share_servers_table,
sa.Column('encryption_key_ref', sa.String(36), nullable=True))
except Exception:
LOG.error("Column 'encryption_key_ref' can not be added to the "
"'share_servers' table!")
raise
try:
op.add_column(
share_servers_table,
sa.Column('application_credential_id',
sa.String(36), nullable=True))
except Exception:
LOG.error("Column 'application_credential_id' can not be added to the "
"'share_servers' table!")
raise
try:
op.add_column(
share_instances_table,
sa.Column('encryption_key_ref', sa.String(36), nullable=True))
except Exception:
LOG.error("Column can not be added to the 'share_instances' table!")
raise
context = op.get_context()
mysql_dl = context.bind.dialect.name == 'mysql'
datetime_type = (sa.dialects.mysql.DATETIME(fsp=6)
if mysql_dl else sa.DateTime)
try:
op.create_table(
encryption_refs_table,
sa.Column('id', sa.String(36), primary_key=True, nullable=False),
sa.Column('share_server_id', sa.String(length=36),
sa.ForeignKey('share_servers.id'), unique=True),
sa.Column('share_instance_id', sa.String(length=36),
sa.ForeignKey('share_instances.id'), unique=True),
sa.Column('encryption_key_ref', sa.String(36), nullable=True),
sa.Column('project_id', sa.String(length=255), nullable=False),
sa.Column('deleted', sa.String(36), default='False'),
sa.Column('created_at', datetime_type),
sa.Column('updated_at', datetime_type),
sa.Column('deleted_at', datetime_type),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
except Exception:
LOG.error("Table |%s| not created!", encryption_refs_table)
raise
def downgrade():
try:
op.drop_table(encryption_refs_table)
except Exception:
LOG.error("%s table not dropped", encryption_refs_table)
raise
try:
op.drop_column(share_servers_table, 'encryption_key_ref')
op.drop_column(share_servers_table, 'application_credential_id')
except Exception:
LOG.error("Column can not be dropped for 'share_servers' table!")
raise
try:
op.drop_column(share_instances_table, 'encryption_key_ref')
except Exception:
LOG.error("Column can not be dropped for 'share_instances' table!")
raise
+86 -2
View File
@@ -504,6 +504,13 @@ def _sync_replica_gigabytes(context, project_id, user_id, share_type_id=None):
return {'replica_gigabytes': replica_gigs}
def _sync_encryption_keys(context, project_id, user_id, share_type_id=None):
encryption_keys = _count_encryption_keys_for_project(
context, project_id, user_id
)
return {'encryption_keys': encryption_keys}
QUOTA_SYNC_FUNCTIONS = {
'_sync_shares': _sync_shares,
'_sync_snapshots': _sync_snapshots,
@@ -516,6 +523,7 @@ QUOTA_SYNC_FUNCTIONS = {
'_sync_replica_gigabytes': _sync_replica_gigabytes,
'_sync_backups': _sync_backups,
'_sync_backup_gigabytes': _sync_backup_gigabytes,
'_sync_encryption_keys': _sync_encryption_keys,
}
@@ -1619,7 +1627,7 @@ def _extract_share_instance_values(values):
'status', 'host', 'scheduled_at', 'launched_at', 'terminated_at',
'share_server_id', 'share_network_id', 'availability_zone_id',
'replica_state', 'share_type_id', 'share_type', 'access_rules_status',
'mount_point_name',
'mount_point_name', 'encryption_key_ref',
]
share_instance_values, share_values = (
_extract_subdict_by_fields(values, share_instance_model_fields)
@@ -1851,6 +1859,11 @@ def _share_instance_get_all(context, filters=None):
if status:
query = query.filter(models.ShareInstance.status == status)
encryption_key_ref = filters.get('encryption_key_ref')
if encryption_key_ref:
query = query.filter(
models.ShareInstance.encryption_key_ref == encryption_key_ref)
# Returns list of share instances that satisfy filters.
query = query.all()
return query
@@ -2209,7 +2222,8 @@ def _process_share_filters(query, filters, project_id=None, is_public=False):
share_filter_keys = ['share_group_id', 'snapshot_id',
'is_soft_deleted', 'source_backup_id']
instance_filter_keys = ['share_server_id', 'status', 'share_type_id',
'host', 'share_network_id', 'mount_point_name']
'host', 'share_network_id', 'mount_point_name',
'encryption_key_ref']
share_filters = {}
instance_filters = {}
@@ -5411,7 +5425,20 @@ def share_server_create(context, values):
# updated_at is needed for judgement of automatic cleanup
server_ref.updated_at = timeutils.utcnow()
server_ref.update(values)
# If encryption_key_ref is present, create associated record
encryption_key_ref = values.get('encryption_key_ref')
if encryption_key_ref:
encryption_ref = models.EncryptionRef(
id=uuidutils.generate_uuid(),
share_server_id=server_ref['id'],
encryption_key_ref=encryption_key_ref,
project_id=context.project_id,
)
server_ref.server_encryption_ref_entry = encryption_ref
server_ref.save(session=context.session)
# NOTE(u_glide): Do so to prevent errors with relationships
return _share_server_get(context, server_ref['id'])
@@ -5428,6 +5455,11 @@ def share_server_delete(context, id):
_share_server_backend_details_delete(context, id)
server_ref.soft_delete(session=context.session, update_status=True)
# If encryption_key_ref is present, delete associated encryption ref entry
if server_ref.server_encryption_ref_entry:
server_ref.server_encryption_ref_entry.soft_delete(
session=context.session)
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@@ -5580,6 +5612,9 @@ def _share_server_get_all_with_filters(context, filters):
if filters.get('source_share_server_id'):
query = query.filter_by(
source_share_server_id=filters.get('source_share_server_id'))
if filters.get('encryption_key_ref'):
query = query.filter_by(
encryption_key_ref=filters.get('encryption_key_ref'))
if filters.get('share_network_id'):
query = query.join(
models.ShareServerShareNetworkSubnetMapping,
@@ -6637,6 +6672,15 @@ def _share_replica_data_get_for_project(
return result[0] or 0, result[1] or 0
@require_context
def _count_encryption_keys_for_project(
context, project_id, user_id=None,
):
return encryption_keys_get_count(
context, filters={'project_id': project_id}
)
@require_context
@context_manager.reader
def count_share_group_snapshots_in_share_group(context, share_group_id):
@@ -7716,3 +7760,43 @@ def resource_lock_get_all(context, filters=None, limit=None, offset=None,
offset=offset)
return query.all(), count
###############################
@require_context
@context_manager.reader
def encryption_keys_get_count(context, filters=None):
if filters:
project_id = filters.get('project_id')
else:
project_id = context.project_id
query = model_query(
context, models.EncryptionRef, read_deleted="no"
).filter_by(
project_id=project_id,
)
return query.count()
@require_context
@context_manager.reader
def encryption_keys_get_all(context, filters=None):
if filters:
project_id = filters.get('project_id')
else:
project_id = context.project_id
query = model_query(
context, models.EncryptionRef, read_deleted="no"
).filter_by(
project_id=project_id,
)
encryption_key_ref = filters.get('encryption_key_ref')
if encryption_key_ref:
query = query.filter_by(encryption_key_ref=encryption_key_ref)
return query.all()
+41
View File
@@ -203,6 +203,11 @@ class Share(BASE, ManilaBase):
if len(self.instances) > 0:
return self.instance.export_location
@property
def encryption_key_ref(self):
if len(self.instances) > 0:
return self.instance.encryption_key_ref
@property
def is_busy(self):
# Make sure share is not busy, i.e., not part of a migration
@@ -386,6 +391,7 @@ class ShareInstance(BASE, ManilaBase):
scheduled_at = Column(DateTime)
launched_at = Column(DateTime)
terminated_at = Column(DateTime)
encryption_key_ref = Column(String(36), nullable=True)
replica_state = Column(String(255), nullable=True)
cast_rules_to_readonly = Column(Boolean, default=False, nullable=False)
share_type_id = Column(String(36), ForeignKey('share_types.id'),
@@ -1132,6 +1138,8 @@ class ShareServer(BASE, ManilaBase):
Boolean, nullable=False, default=False)
share_replicas_migration_support = Column(
Boolean, nullable=False, default=False)
encryption_key_ref = Column(String(36), nullable=True)
application_credential_id = Column(String(36), nullable=True)
status = Column(Enum(
constants.STATUS_INACTIVE, constants.STATUS_ACTIVE,
constants.STATUS_ERROR, constants.STATUS_DELETING,
@@ -1197,6 +1205,39 @@ class ShareServer(BASE, ManilaBase):
_extra_keys = ['backend_details', 'share_network_subnet_ids']
class EncryptionRef(BASE, ManilaBase):
"""Represents a share server with encryption keys."""
__tablename__ = 'encryption_refs'
id = Column(String(36), primary_key=True, nullable=False)
share_server_id = Column(
String(36), ForeignKey('share_servers.id'), unique=True)
share_instance_id = Column(
String(36), ForeignKey('share_instances.id'), unique=True)
encryption_key_ref = Column(String(36), nullable=True)
project_id = Column(String(255), nullable=True)
deleted = Column(String(36), default='False')
share_server = orm.relationship(
ShareServer,
backref=orm.backref(
'server_encryption_ref_entry', lazy='joined', uselist=False),
foreign_keys=share_server_id,
primaryjoin='and_('
'EncryptionRef.share_server_id == ShareServer.id,'
'EncryptionRef.deleted == "False")'
)
share_instance = orm.relationship(
ShareInstance,
backref=orm.backref(
'instance_encryption_ref_entry', lazy='joined', uselist=False),
foreign_keys=share_instance_id,
primaryjoin='and_('
'EncryptionRef.share_instance_id == ShareInstance.id,'
'EncryptionRef.deleted == "False")'
)
class ShareServerBackendDetails(BASE, ManilaBase):
"""Represents a metadata key/value pair for a share server."""
__tablename__ = 'share_server_backend_details'
+14
View File
@@ -500,6 +500,11 @@ class ShareReplicaSizeExceedsAvailableQuota(QuotaError):
"gigabytes quota.")
class EncryptionKeysLimitExceeded(QuotaError):
message = _(
"Maximum number of allowed encryption keys is exceeded.")
class GlusterfsException(ManilaException):
message = _("Unknown Gluster exception.")
@@ -783,6 +788,15 @@ class ShareTypeUpdateFailed(ManilaException):
message = _("Cannot update share_type %(id)s.")
class ManilaBarbicanACLError(ManilaException):
message = _("Failed while communicating to Barbican. "
"Please check the provided credentials .")
class ManilaBarbicanAppCredsError(ManilaException):
message = _("Error occured while dealing with barbican for App Creds.")
class ShareGroupTypeCreateFailed(ManilaException):
message = _("Cannot create share group type with "
"name %(name)s and specs %(group_specs)s.")
+18
View File
@@ -0,0 +1,18 @@
# 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 castellan import options as castellan_opts
from oslo_config import cfg
CONF = cfg.CONF
castellan_opts.set_defaults(CONF)
+295
View File
@@ -0,0 +1,295 @@
# Copyright 2025 Cloudification 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 urllib
from barbicanclient import client as barbican_client
from castellan import options as castellan_options
from keystoneauth1 import identity as ks_identity
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import session as ks_session
from keystoneclient.v3 import client as ks_client
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import uuidutils
from manila import exception
from manila.i18n import _
BARBICAN_GROUP = 'barbican'
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class BarbicanSecretACL(object):
def __init__(self, conf):
self.conf = conf
def get_client_and_href(self, context, secret_ref):
"""Get user barbican client and a secret href"""
castellan_options.set_defaults(self.conf)
backend = self.conf.key_manager.backend or ''
backend = backend.split('.')[-1]
if backend not in ('barbican', 'BarbicanKeyManager'):
LOG.warning("The '%s' key_manager backend is not supported. Please"
" use barbican as key_manager.", backend)
raise exception.ManilaBarbicanACLError()
if not getattr(self.conf, 'barbican', None) or \
not getattr(self.conf.barbican, 'auth_endpoint', None):
LOG.error("Missing auth_endpoint for barbican connection")
raise exception.ManilaBarbicanACLError()
if not secret_ref:
LOG.error("Missing secret_ref provided in current user context.")
raise exception.ManilaBarbicanACLError()
# Establish a Barbican client session of current user and keystone
# session of barbican user to get its user_id. Grant ACL to barbican
# user that it will be used for the key_ref handover process.
try:
user_auth = ks_identity.V3Token(
auth_url=self.conf.barbican.auth_endpoint,
token=context.auth_token,
project_id=context.project_id)
user_sess = ks_session.Session(auth=user_auth)
user_barbican_client = barbican_client.Client(session=user_sess)
barbican_endpoint = self._get_barbican_endpoint(user_auth,
user_sess)
base_url = self._create_base_url(user_auth, user_sess,
barbican_endpoint)
secret_ref = self._create_secret_ref(base_url, secret_ref)
except Exception as e:
LOG.error("Failed to create barbican client. Error: %s", e)
raise exception.ManilaBarbicanACLError()
return user_barbican_client, secret_ref
def create_secret_access(self, context, secret_ref):
try:
user_barbican_client, secret_href = self.get_client_and_href(
context, secret_ref)
ks_loading.register_auth_conf_options(self.conf, BARBICAN_GROUP)
barbican_auth = ks_loading.load_auth_from_conf_options(
self.conf, BARBICAN_GROUP)
barbican_sess = ks_session.Session(auth=barbican_auth)
barbican_ks_client = ks_client.Client(session=barbican_sess)
barbican_user_id = barbican_ks_client.session.get_user_id()
# Create a Barbican ACL so the barbican user can access it.
acl = user_barbican_client.acls.create(entity_ref=secret_href,
users=[barbican_user_id],
project_access=False)
acl.submit()
except Exception as e:
LOG.error("Failed to create secret ACL. Error: %s", e)
raise exception.ManilaBarbicanACLError()
def delete_secret_access(self, context, secret_ref):
try:
user_barbican_client, secret_href = self.get_client_and_href(
context, secret_ref)
ks_loading.register_auth_conf_options(self.conf, BARBICAN_GROUP)
barbican_auth = ks_loading.load_auth_from_conf_options(
self.conf, BARBICAN_GROUP)
barbican_sess = ks_session.Session(auth=barbican_auth)
barbican_ks_client = ks_client.Client(session=barbican_sess)
barbican_user_id = barbican_ks_client.session.get_user_id()
# Remove a Barbican ACL for the barbican user.
acl_entity = user_barbican_client.acls.get(entity_ref=secret_href)
existing_users = acl_entity.read.users
remove_users = [barbican_user_id]
updated_users = set(existing_users).difference(remove_users)
acl_entity.read.users = list(updated_users)
acl_entity.submit()
except Exception as e:
LOG.error("Failed to delete secret ACL. Error: %s", e)
def get_secret_href(self, context, secret_ref):
try:
user_barbican_client, secret_href = self.get_client_and_href(
context, secret_ref)
return secret_href
except Exception as e:
LOG.error("Failed to get barbican secret href. Error: %s", e)
raise exception.ManilaBarbicanACLError()
def _get_barbican_endpoint(self, auth, sess):
if self.conf.barbican.barbican_endpoint:
return self.conf.barbican.barbican_endpoint
elif getattr(auth, 'service_catalog', None):
endpoint_data = auth.service_catalog.endpoint_data_for(
service_type='key-manager',
interface=self.conf.barbican.barbican_endpoint_type,
region_name=self.conf.barbican.barbican_region_name)
return endpoint_data.url
else:
return auth.get_endpoint(
sess,
service_type='key-manager',
interface=self.conf.barbican.barbican_endpoint_type,
region_name=self.conf.barbican.barbican_region_name)
def _create_base_url(self, auth, sess, endpoint):
api_version = None
if self.conf.barbican.barbican_api_version:
api_version = self.conf.barbican.barbican_api_version
elif getattr(auth, 'service_catalog', None):
endpoint_data = auth.service_catalog.endpoint_data_for(
service_type='key-manager',
interface=self.conf.barbican.barbican_endpoint_type,
region_name=self.conf.barbican.barbican_region_name)
api_version = endpoint_data.api_version
elif getattr(auth, 'get_discovery', None):
discovery = auth.get_discovery(sess, url=endpoint)
raw_data = discovery.raw_version_data()
if len(raw_data) == 0:
msg = _(
"Could not find discovery information for %s") % endpoint
LOG.error(msg)
raise exception.KeyManagerError(reason=msg)
latest_version = raw_data[-1]
api_version = latest_version.get('id')
if not endpoint.endswith('/'):
endpoint += '/'
base_url = urllib.parse.urljoin(endpoint, api_version)
return base_url
def _create_secret_ref(self, base_url, object_id):
if not object_id:
msg = _("Key ID is None")
raise exception.KeyManagerError(reason=msg)
if not base_url.endswith('/'):
base_url += '/'
return urllib.parse.urljoin(base_url, "secrets/" + object_id)
class BarbicanUserAppCreds(object):
def __init__(self, conf):
self.conf = conf
@property
def client(self):
return self.get_client()
def get_client(self):
castellan_options.set_defaults(self.conf)
backend = self.conf.key_manager.backend or ''
backend = backend.split('.')[-1]
if backend not in ('barbican', 'BarbicanKeyManager'):
LOG.warning("The '%s' key_manager backend is not supported. Please"
" use barbican key_manager backend.", backend)
if not getattr(self.conf, 'barbican', None) or \
not getattr(self.conf.barbican, 'auth_endpoint', None):
LOG.error("Missing auth_endpoint for barbican connection")
raise exception.ManilaBarbicanACLError()
ks_loading.register_auth_conf_options(self.conf, BARBICAN_GROUP)
auth = ks_loading.load_auth_from_conf_options(self.conf,
BARBICAN_GROUP)
sess = ks_session.Session(auth=auth)
return ks_client.Client(session=sess)
def get_application_credentials(self, context, application_credential_id):
if not application_credential_id:
LOG.warning("Missing application credentials ID")
raise exception.ManilaBarbicanAppCredsError()
try:
return self.client.application_credentials.get(
application_credential=application_credential_id)
except Exception as e:
LOG.error("Aborting App Creds request due to error: %s", e)
raise exception.ManilaBarbicanAppCredsError()
def create_application_credentials(self, context, secret):
try:
secrets_path = "/key-manager/v1/secrets"
return self.client.application_credentials.create(
name='manila_barbican_' + uuidutils.generate_uuid(),
user=self.client.session.get_user_id(),
roles=[{'name': 'service'}],
secret=str(secret),
access_rules=[
{
"path": secrets_path + "/%s" % secret,
"method": "GET",
"service": "key-manager",
},
{
"path": secrets_path + "/%s/payload" % secret,
"method": "GET",
"service": "key-manager",
}
]
)
except Exception as e:
LOG.error("Aborting App Creds create due to error: %s", e)
raise exception.ManilaBarbicanAppCredsError()
def delete_application_credentials(self, context,
application_credential_id):
if not application_credential_id:
LOG.warning("Missing application credentials ID")
raise exception.ManilaBarbicanAppCredsError()
try:
return self.client.application_credentials.delete(
application_credential=application_credential_id)
except Exception as e:
LOG.error("Aborting App Creds request due to error: %s", e)
raise exception.ManilaBarbicanAppCredsError()
def create_secret_access(context, secret_ref, conf=CONF):
BarbicanSecretACL(conf).create_secret_access(context, secret_ref)
def delete_secret_access(context, secret_ref, conf=CONF):
BarbicanSecretACL(conf).delete_secret_access(context, secret_ref)
def get_secret_href(context, secret_ref, conf=CONF):
return BarbicanSecretACL(conf).get_secret_href(context, secret_ref)
def create_application_credentials(context, secret, conf=CONF):
return BarbicanUserAppCreds(conf).create_application_credentials(
context,
secret)
def get_application_credentials(context, application_credential_id, conf=CONF):
return BarbicanUserAppCreds(conf).get_application_credentials(
context,
application_credential_id)
def delete_application_credentials(context,
application_credential_id, conf=CONF):
BarbicanUserAppCreds(conf).delete_application_credentials(
context,
application_credential_id)
+8 -2
View File
@@ -108,7 +108,10 @@ quota_opts = [
help='Total amount of storage, in gigabytes, allowed '
'for backups per project.',
deprecated_group='DEFAULT',
deprecated_name='quota_backup_gigabytes'), ]
deprecated_name='quota_backup_gigabytes'),
cfg.IntOpt('encryption_keys',
default=100,
help='Number of encryption keys allowed per project.'), ]
CONF = cfg.CONF
CONF.register_opts(quota_opts, QUOTA_GROUP)
@@ -1186,7 +1189,10 @@ resources = [
'replica_gigabytes'),
ReservableResource('backups', '_sync_backups', 'backups'),
ReservableResource('backup_gigabytes', '_sync_backup_gigabytes',
'backup_gigabytes')
'backup_gigabytes'),
ReservableResource('encryption_keys',
'_sync_encryption_keys',
'encryption_keys'),
]
+7
View File
@@ -161,6 +161,7 @@ class HostState(object):
self.share_server_multiple_subnet_support = False
self.mount_point_name_support = False
self.share_replicas_migration_support = False
self.encryption_support = None
# PoolState for all pools
self.pools = {}
@@ -370,6 +371,10 @@ class HostState(object):
pool_cap['share_replicas_migration_support'] = (
self.share_replicas_migration_support)
if 'encryption_support' not in pool_cap:
pool_cap['encryption_support'] = (
self.encryption_support)
if self.ipv4_support is not None:
pool_cap['ipv4_support'] = self.ipv4_support
@@ -407,6 +412,7 @@ class HostState(object):
'share_server_multiple_subnet_support', False)
self.share_replicas_migration_support = capability.get(
'share_replicas_migration_support', False)
self.encryption_support = capability.get('encryption_support', None)
def consume_from_share(self, share):
"""Incrementally update host state from an share."""
@@ -515,6 +521,7 @@ class PoolState(HostState):
'share_server_multiple_subnet_support', False)
self.share_replicas_migration_support = capability.get(
'share_replicas_migration_support', False)
self.encryption_support = capability.get('encryption_support')
def update_pools(self, capability):
# Do nothing, since we don't have pools within pool, yet
+1
View File
@@ -71,6 +71,7 @@ def generate_stats(host_state, properties):
host_state.mount_point_name_support),
'share_replicas_migration_support': (
host_state.share_replicas_migration_support),
'encryption_support': host_state.encryption_support,
}
host_caps = host_state.capabilities
+48 -7
View File
@@ -206,6 +206,24 @@ class API(base.Base):
compatible_azs_name.append(az['name'])
return compatible_azs_name, compatible_azs_multiple
@staticmethod
def check_if_encryption_keys_quotas_exceeded(context, quota_exception):
overs = quota_exception.kwargs['overs']
usages = quota_exception.kwargs['usages']
quotas = quota_exception.kwargs['quotas']
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
if 'encryption_keys' in overs:
LOG.warning("Encryption keys quota exceeded for %(s_pid)s "
"(%(d_consumed)d of %(d_quota)d keys already "
"consumed).", {
's_pid': context.project_id,
'd_consumed': _consumed('encryption_keys'),
'd_quota': quotas['encryption_keys']})
raise exception.EncryptionKeysLimitExceeded()
@staticmethod
def check_if_share_quotas_exceeded(context, quota_exception,
share_size, operation='create'):
@@ -286,7 +304,7 @@ class API(base.Base):
share_group_id=None, share_group_snapshot_member=None,
availability_zones=None, scheduler_hints=None,
az_request_multiple_subnet_support_map=None,
mount_point_name=None):
mount_point_name=None, encryption_key_ref=None):
"""Create new share."""
api_common.check_metadata_properties(metadata)
@@ -382,11 +400,28 @@ class API(base.Base):
deltas.update(
{'share_replicas': 1, 'replica_gigabytes': size})
if encryption_key_ref:
# Make sure encryption_key_ref is valid UUID
if not uuidutils.is_uuid_like(encryption_key_ref):
msg = _('Encryption key ref is not valid UUID')
raise exception.InvalidInput(reason=msg)
# Check if its new encryption_key_ref
filters = {
'encryption_key_ref': encryption_key_ref,
'project_id': context.project_id,
}
is_existing_key = self.db.encryption_keys_get_all(
context, filters=filters)
if not is_existing_key:
deltas.update({'encryption_keys': 1})
try:
reservations = QUOTAS.reserve(
context, share_type_id=share_type_id, **deltas)
except exception.OverQuota as e:
self.check_if_share_quotas_exceeded(context, e, size)
self.check_if_encryption_keys_quotas_exceeded(context, e)
if share_type_supports_replication:
self.check_if_replica_quotas_exceeded(context, e, size,
resource_type='share')
@@ -517,7 +552,9 @@ class API(base.Base):
snapshot_host=snapshot_host, scheduler_hints=scheduler_hints,
az_request_multiple_subnet_support_map=(
az_request_multiple_subnet_support_map),
mount_point_name=mount_point_name)
mount_point_name=mount_point_name,
encryption_key_ref=encryption_key_ref,
)
# Retrieve the share with instance details
share = self.db.share_get(context, share['id'])
@@ -677,7 +714,7 @@ class API(base.Base):
share_type_id=None, availability_zones=None,
snapshot_host=None, scheduler_hints=None,
az_request_multiple_subnet_support_map=None,
mount_point_name=None):
mount_point_name=None, encryption_key_ref=None):
request_spec, share_instance = (
self.create_share_instance_and_get_request_spec(
context, share, availability_zone=availability_zone,
@@ -688,7 +725,8 @@ class API(base.Base):
snapshot_host=snapshot_host,
az_request_multiple_subnet_support_map=(
az_request_multiple_subnet_support_map),
mount_point_name=mount_point_name))
mount_point_name=mount_point_name,
encryption_key_ref=encryption_key_ref))
if share_group_snapshot_member:
# Inherit properties from the share_group_snapshot_member
@@ -732,7 +770,7 @@ class API(base.Base):
share_type_id=None, cast_rules_to_readonly=False,
availability_zones=None, snapshot_host=None,
az_request_multiple_subnet_support_map=None,
mount_point_name=None):
mount_point_name=None, encryption_key_ref=None):
availability_zone_id = None
if availability_zone:
@@ -753,6 +791,7 @@ class API(base.Base):
'share_type_id': share_type_id,
'cast_rules_to_readonly': cast_rules_to_readonly,
'mount_point_name': mount_point_name,
'encryption_key_ref': encryption_key_ref,
}
)
@@ -786,7 +825,8 @@ class API(base.Base):
'host': share_instance['host'],
'status': share_instance['status'],
'replica_state': share_instance['replica_state'],
'share_type_id': share_instance['share_type_id']
'share_type_id': share_instance['share_type_id'],
'encryption_key_ref': share_instance['encryption_key_ref'],
}
share_type = None
@@ -2339,7 +2379,8 @@ class API(base.Base):
'display_description', 'display_description~', 'snapshot_id',
'status', 'share_type_id', 'project_id', 'export_location_id',
'export_location_path', 'limit', 'offset', 'host',
'share_network_id', 'is_soft_deleted', 'mount_point_name']
'share_network_id', 'is_soft_deleted', 'mount_point_name',
'encryption_key_ref']
for key in filter_keys:
if key in search_opts:
+5 -1
View File
@@ -293,6 +293,7 @@ class ShareDriver(object):
self.network_allocation_update_support = False
self.dhss_mandatory_security_service_association = {}
self.share_replicas_migration_support = False
self.encryption_support = None
self.pools = []
if self.configuration:
@@ -970,7 +971,8 @@ class ShareDriver(object):
def choose_share_server_compatible_with_share(self, context, share_servers,
share, snapshot=None,
share_group=None):
share_group=None,
encryption_key_ref=None):
"""Method that allows driver to choose share server for provided share.
If compatible share-server is not found, method should return None.
@@ -980,6 +982,7 @@ class ShareDriver(object):
:param share: share model
:param snapshot: snapshot model
:param share_group: ShareGroup model with shares
:param encryption_key_ref: Encryption key reference
:returns: share-server or None
"""
# If creating in a share group, use its share server
@@ -1362,6 +1365,7 @@ class ShareDriver(object):
mount_point_name_support=False,
share_replicas_migration_support=(
self.share_replicas_migration_support),
encryption_support=self.encryption_support,
)
if isinstance(data, dict):
common.update(data)
+2 -1
View File
@@ -370,7 +370,8 @@ class HPE3ParShareDriver(driver.ShareDriver):
def choose_share_server_compatible_with_share(self, context, share_servers,
share, snapshot=None,
share_group=None):
share_group=None,
encryption_key_ref=None):
"""Method that allows driver to choose share server for provided share.
If compatible share-server is not found, method should return None.
+129 -6
View File
@@ -26,6 +26,7 @@ import hashlib
import json
from operator import xor
from keystoneauth1 import loading as ks_loading
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
@@ -40,6 +41,7 @@ from manila import coordination
from manila.data import rpcapi as data_rpcapi
from manila import exception
from manila.i18n import _
from manila.keymgr import barbican as barbican_api
from manila import manager
from manila.message import api as message_api
from manila.message import message_field
@@ -167,11 +169,22 @@ share_manager_opts = [
'snapshots in backend driver.'),
]
ks_opts = [
cfg.StrOpt('auth_url',
help='Keystone authentication URL.'),
]
CONF = cfg.CONF
CONF.register_opts(share_manager_opts)
CONF.import_opt('periodic_hooks_interval', 'manila.share.hook')
CONF.import_opt('periodic_interval', 'manila.service')
KEYSTONE_AUTHTOKEN_GROUP = 'keystone_authtoken'
CONF.register_opts(ks_opts, KEYSTONE_AUTHTOKEN_GROUP)
ks_loading.register_auth_conf_options(CONF, KEYSTONE_AUTHTOKEN_GROUP)
keystone_url = getattr(CONF.keystone_authtoken, 'auth_url')
# Drivers that need to change module paths or class names can add their
# old/new path here to maintain backward compatibility.
MAPPING = {
@@ -785,6 +798,8 @@ class ShareManager(manager.SchedulerDependentManager):
context, available_share_servers,
share_instance=share_instance))
encryption_key_ref = share_instance.get('encryption_key_ref')
compatible_share_server = None
if available_share_servers:
try:
@@ -792,7 +807,8 @@ class ShareManager(manager.SchedulerDependentManager):
self.driver.choose_share_server_compatible_with_share(
context, available_share_servers, share_instance,
snapshot=snapshot.instance if snapshot else None,
share_group=share_group
share_group=share_group,
encryption_key_ref=encryption_key_ref,
)
)
except Exception as e:
@@ -800,7 +816,38 @@ class ShareManager(manager.SchedulerDependentManager):
error("Cannot choose compatible share server: %s",
e)
share_server_should_be_encrypted = (
(encryption_key_ref and self.driver.encryption_support) and
("share_server" in self.driver.encryption_support))
app_cred = None
if not compatible_share_server:
if share_server_should_be_encrypted:
# Create secret_ref ACL for Barbican User
try:
barbican_api.create_secret_access(context,
encryption_key_ref)
LOG.debug('Created Barbican ACL for encryption key '
'reference %s.', encryption_key_ref)
except Exception as e:
self._delete_encryption_keys_quota(context)
with excutils.save_and_reraise_exception():
error("Cannot create ACL for Barbican user %s", e)
# Create application credentials for barbican user
try:
app_cred = (
barbican_api.create_application_credentials(
context, encryption_key_ref).to_dict())
LOG.debug("Created app cred id %s", app_cred['id'])
except Exception as e:
self._delete_encryption_keys_quota(context)
barbican_api.delete_secret_access(context,
encryption_key_ref)
with excutils.save_and_reraise_exception():
error("Cannot create application credential: "
"%s", e)
compatible_share_server = self.db.share_server_create(
context,
{
@@ -813,8 +860,25 @@ class ShareManager(manager.SchedulerDependentManager):
self.driver.network_allocation_update_support),
'share_replicas_migration_support': (
self.driver.share_replicas_migration_support),
'encryption_key_ref': (
encryption_key_ref if
share_server_should_be_encrypted else None),
'application_credential_id': (
app_cred['id'] if app_cred else None),
}
)
else:
if share_server_should_be_encrypted:
# Get application credentials for barbican user
try:
app_cred = barbican_api.get_application_credentials(
context,
compatible_share_server.get(
'application_credential_id')).to_dict()
LOG.debug('Got app cred id %s', app_cred['id'])
except Exception as e:
with excutils.save_and_reraise_exception():
error("Cannot get application credential: %s", e)
msg = ("Using share_server %(share_server)s for share instance"
" %(share_instance_id)s")
@@ -829,9 +893,15 @@ class ShareManager(manager.SchedulerDependentManager):
{'share_server_id': compatible_share_server['id']},
with_share_data=True
)
if create_on_backend:
metadata = self._build_server_metadata(
share_instance['host'], share_instance['share_type_id'])
context,
share_instance['host'],
share_instance['share_type_id'],
app_cred=app_cred,
encryption_key_ref=encryption_key_ref
)
compatible_share_server = (
self._create_share_server_in_backend(
context, compatible_share_server, metadata))
@@ -840,11 +910,29 @@ class ShareManager(manager.SchedulerDependentManager):
return _wrapped_provide_share_server_for_share()
def _build_server_metadata(self, host, share_type_id):
return {
def _build_server_metadata(self, context, host, share_type_id,
app_cred=None,
encryption_key_ref=None):
encryption_key_href = None
if encryption_key_ref:
encryption_key_href = barbican_api.get_secret_href(
context, encryption_key_ref)
LOG.debug("Generated encryption_key_href %s for backend share "
"server.", encryption_key_href)
metadata = {
'request_host': host,
'share_type_id': share_type_id,
'encryption_key_ref': encryption_key_href,
'keystone_url': keystone_url,
}
if app_cred:
metadata.update({
'application_credential_id': app_cred.get('id'),
'application_credential_secret': encryption_key_ref,
})
return metadata
def _provide_share_server_for_migration(self, context,
source_share_server,
@@ -963,7 +1051,7 @@ class ShareManager(manager.SchedulerDependentManager):
share_server = self.db.share_server_get(context, share_server_id)
share = self.db.share_instance_get(
context, share_instance_id, with_share_data=True)
metadata = self._build_server_metadata(share['host'],
metadata = self._build_server_metadata(context, share['host'],
share['share_type_id'])
self._create_share_server_in_backend(context, share_server, metadata)
@@ -1113,6 +1201,7 @@ class ShareManager(manager.SchedulerDependentManager):
if compatible_share_server['status'] == constants.STATUS_CREATING:
# Create share server on backend with data from db.
metadata = self._build_server_metadata(
context,
share_group_ref['host'],
share_group_ref['share_types'][0]['share_type_id'])
compatible_share_server = self._setup_server(
@@ -4708,7 +4797,7 @@ class ShareManager(manager.SchedulerDependentManager):
# has been changed.
server_id = share_server['id']
try:
self.db.share_server_get(
server = self.db.share_server_get(
context, server_id)
except exception.ShareServerNotFound:
raise
@@ -4740,6 +4829,21 @@ class ShareManager(manager.SchedulerDependentManager):
share_net,
share_net_subnet)
application_credential_id = server.get(
'application_credential_id')
if application_credential_id:
# Delete application credentials for barbican user
try:
barbican_api.delete_application_credentials(
context, application_credential_id)
except Exception:
LOG.warning('Application credentials not found '
'during deletion of share server.')
encryption_key_ref = server.get('encryption_key_ref')
barbican_api.delete_secret_access(context,
encryption_key_ref)
LOG.debug("Deleting share server '%s'", server_id)
security_services = []
for ss_name in constants.SECURITY_SERVICES_ALLOWED_TYPES:
@@ -4758,6 +4862,9 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_server_update(
context, server_id, {'status': constants.STATUS_ERROR})
else:
encryption_key_ref = server.get('encryption_key_ref')
if encryption_key_ref:
self._delete_encryption_keys_quota(context)
self.db.share_server_delete(context, share_server['id'])
_wrapped_delete_share_server()
@@ -4765,6 +4872,22 @@ class ShareManager(manager.SchedulerDependentManager):
"Share server '%s' has been deleted successfully.",
share_server['id'])
def _delete_encryption_keys_quota(self, context):
reservations = None
try:
reservations = QUOTAS.reserve(
context, project_id=context.project_id,
encryption_keys=-1,
)
except Exception:
LOG.exception("Failed to update encryption_keys quota "
"usages while deleting share server.")
if reservations:
QUOTAS.commit(
context, reservations, project_id=context.project_id,
)
@add_hooks
@utils.require_driver_initialized
def extend_share(self, context, share_id, new_size, reservations):
+2
View File
@@ -366,6 +366,8 @@ def is_valid_optional_extra_spec(key, value):
return is_valid_csv(value)
elif key == constants.ExtraSpecs.PROVISIONING_MOUNT_POINT_PREFIX:
return is_valid_string(value, min_length=0)
elif key == constants.ExtraSpecs.ENCRYPTION_SUPPORT:
return value in constants.ExtraSpecs.ENCRYPTION_TYPES
elif key in [constants.ExtraSpecs.PROVISIONING_MAX_SHARE_SIZE,
constants.ExtraSpecs.PROVISIONING_MIN_SHARE_SIZE,
constants.ExtraSpecs.PROVISIONING_MAX_SHARE_EXTEND_SIZE]:
+1
View File
@@ -56,6 +56,7 @@ def stub_share(id, **kwargs):
'share_server_id': 'fake_share_server_id',
'access_rules_status': 'active',
'share_type_id': '1',
'encryption_key_ref': None,
}
if 'instance' in kwargs:
share_instance.update(kwargs.pop('instance'))
+11 -3
View File
@@ -45,7 +45,8 @@ fake_share_server_list = {
'source_share_server_id': None,
'identifier': 'fake_id',
'security_service_update_support': False,
'network_allocation_update_support': False
'network_allocation_update_support': False,
'encryption_key_ref': None
},
{
'status': constants.STATUS_ERROR,
@@ -61,7 +62,8 @@ fake_share_server_list = {
'source_share_server_id': None,
'identifier': 'fake_id_2',
'security_service_update_support': False,
'network_allocation_update_support': False
'network_allocation_update_support': False,
'encryption_key_ref': None
},
]
@@ -102,7 +104,8 @@ fake_share_server_get_result = {
'source_share_server_id': None,
'identifier': 'fake_id',
'security_service_update_support': False,
'network_allocation_update_support': False
'network_allocation_update_support': False,
'encryption_key_ref': None
}
}
@@ -145,6 +148,7 @@ class FakeShareServer(object):
self.network_allocation_update_support = kwargs.get(
'network_allocation_update_support', False)
self.share_network_id = kwargs.get('share_network_id', 'fake_sn_id')
self.encryption_key_ref = kwargs.get('encryption_key_ref', None)
def __getitem__(self, item):
return getattr(self, item)
@@ -358,6 +362,7 @@ class ShareServerAPITest(test.TestCase):
self.assertEqual(0, len(result['share_servers']))
@ddt.data({'version': '2.70', 'share_network_name': ''},
{'version': '2.90', 'share_network_name': 'fake_sn_name'},
{'version': '2.70', 'share_network_name': 'fake_sn_name'},
{'version': '2.68', 'share_network_name': 'fake_sn_name'})
@ddt.unpack
@@ -384,6 +389,9 @@ class ShareServerAPITest(test.TestCase):
else:
share_server['share_network_name'] = share_network['id']
if version < '2.90':
share_server.pop('encryption_key_ref')
self.mock_object(db_api, 'share_network_get', mock.Mock(
return_value=share_network))
result = self.controller.show(
@@ -63,6 +63,8 @@ class QuotaSetsControllerTest(test.TestCase):
('', '2.7', quota_class_sets.QuotaClassSetsController),
('', '2.53', quota_class_sets.QuotaClassSetsController),
('', '2.62', quota_class_sets.QuotaClassSetsController),
('', '2.80', quota_class_sets.QuotaClassSetsController),
('', '2.90', quota_class_sets.QuotaClassSetsController),
)
@ddt.unpack
def test_show_quota(self, url, version, controller):
@@ -100,6 +102,8 @@ class QuotaSetsControllerTest(test.TestCase):
if req.api_version_request >= api_version.APIVersionRequest("2.80"):
expected['quota_class_set']['backups'] = 10
expected['quota_class_set']['backup_gigabytes'] = 1000
if req.api_version_request >= api_version.APIVersionRequest("2.90"):
expected['quota_class_set']['encryption_keys'] = 100
result = controller().show(req, self.class_name)
@@ -399,6 +399,27 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'create')
def test_create_has_been_encrypted(self):
share_ref = fake_share.fake_share(encryption_key_ref='fake')
body = {
'share_replica': {
'share_id': 'FAKE_SHAREID',
'availability_zone': 'FAKE_AZ'
}
}
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
self.mock_object(share_replicas.db, 'share_get',
mock.Mock(return_value=share_ref))
self.assertRaises(exc.HTTPForbidden,
self.controller.create,
self.replicas_req, body)
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'create')
def test_create_invalid_network_id(self):
fake_replica, _ = self._get_fake_replica(
replication_type='writable')
+4 -1
View File
@@ -1158,7 +1158,10 @@ class ShareServerControllerTest(test.TestCase):
else:
body = {}
server_get = mock.Mock()
server = db_utils.create_share_server(
id='fake_server_id', status=constants.STATUS_ACTIVE)
server_get = mock.Mock(return_value=server)
network_get = mock.Mock()
if raise_server_get_exception:
server_get = mock.Mock(
+68 -1
View File
@@ -151,6 +151,7 @@ class ShareAPITest(test.TestCase):
'2.31': {'share_group_id': None,
'source_share_group_snapshot_member_id': None},
'2.32': {'mount_snapshot_support': False},
'2.90': {'encryption_key_ref': None},
}
# Apply all the share transformations
@@ -606,7 +607,8 @@ class ShareAPITest(test.TestCase):
metadata=None,
snapshot_id=None,
availability_zone=az_name,
scheduler_hints=None)
scheduler_hints=None,
encryption_key_ref=None)
def test_share_create_with_sg_and_different_availability_zone(self):
sg_id = 'fake_sg_id'
@@ -663,6 +665,71 @@ class ShareAPITest(test.TestCase):
db.share_group_get.assert_called_once_with(
req.environ['manila.context'], sg_id)
@ddt.data({'encryption_support': True, 'dhss': True},
{'encryption_support': True, 'dhss': False},
{'encryption_support': False, 'dhss': True},
{'encryption_support': False, 'dhss': False})
@ddt.unpack
def test_share_create_with_encryption(self, encryption_support, dhss):
share = {
"id": "1",
"size": 100,
"display_name": "Share Test Name",
"display_description": "Share Test Desc",
"share_proto": "fakeproto",
"share_network_id": "fakenetid"
}
fake_network = {'id': 'fakenetid'}
create_mock = mock.Mock(
return_value=stubs.stub_share(
'1',
size=100,
display_name=share['display_name'],
display_description=share['display_description'],
share_proto=share['share_proto'].upper(),
share_network_id=share['share_network_id'],
instance={
'encryption_key_ref': 'fake_key_uuid',
})
)
self.mock_object(share_api.API, 'create', create_mock)
self.mock_object(share_api.API, 'get_share_network', mock.Mock(
return_value=fake_network))
self.mock_object(common, 'check_share_network_is_active',
mock.Mock(return_value=True))
stype = copy.deepcopy(self.vt)
if dhss:
stype['extra_specs']['driver_handles_share_servers'] = 'true'
else:
stype['extra_specs']['driver_handles_share_servers'] = 'false'
if encryption_support:
stype['extra_specs']['encryption_support'] = 'share'
else:
stype['extra_specs']['encryption_support'] = 'fake'
self.mock_object(share_types, 'get_share_type_by_name', mock.Mock(
return_value=stype))
share['encryption_key_ref'] = 'fake_key_uuid'
request_args = copy.deepcopy(share)
request_args['share_type'] = 'fake_volume_type_name'
body = {"share": request_args}
req = fakes.HTTPRequest.blank('/v2/fake/shares', version='2.90')
if dhss and encryption_support:
result = self.controller.create(req, body)
expected = self._get_expected_share_detailed_response(
share, version='2.90')
self.assertEqual(
expected['share']['encryption_key_ref'],
result['share']['encryption_key_ref'])
else:
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, body)
def test_share_create_with_valid_default_share_type(self):
self.mock_object(share_types, 'get_share_type_by_name',
mock.Mock(return_value=self.vt))
@@ -37,6 +37,7 @@ class ViewBuilderTestCase(test.TestCase):
("fake_quota_class", "2.53"), (None, "2.53"),
("fake_quota_class", "2.62"), (None, "2.62"),
("fake_quota_class", "2.80"), (None, "2.80"),
("fake_quota_class", "2.90"), (None, "2.90"),
)
@ddt.unpack
def test_detail_list_with_share_type(self, quota_class, microversion):
@@ -93,6 +94,13 @@ class ViewBuilderTestCase(test.TestCase):
quota_class_set['backups'] = fake_share_backups_value
quota_class_set['backup_gigabytes'] = fake_backup_gigabytes_value
if req.api_version_request >= api_version.APIVersionRequest("2.90"):
fake_encryption_keys = 10
expected[self.builder._collection_name][
"encryption_keys"] = fake_encryption_keys
quota_class_set['encryption_keys'] = (
fake_encryption_keys)
result = self.builder.detail_list(
req, quota_class_set, quota_class=quota_class)
@@ -48,6 +48,8 @@ class ViewBuilderTestCase(test.TestCase):
(None, None, "2.62"),
('fake_project_id', None, "2.80"),
(None, None, "2.80"),
('fake_project_id', None, "2.90"),
(None, None, "2.90"),
)
@ddt.unpack
def test_detail_list_with_share_type(self, project_id, share_type,
@@ -107,6 +109,13 @@ class ViewBuilderTestCase(test.TestCase):
quota_set['backups'] = fake_share_backups_value
quota_set['backup_gigabytes'] = fake_backup_gigabytes_value
if req.api_version_request >= api_version.APIVersionRequest("2.90"):
fake_encryption_keys = 10
expected[self.builder._collection_name][
"encryption_keys"] = (
fake_per_share_gigabytes)
quota_set['encryption_keys'] = fake_encryption_keys
result = self.builder.detail_list(
req, quota_set, project_id=project_id, share_type=share_type)
+7
View File
@@ -4048,6 +4048,13 @@ class ShareServerDatabaseAPITestCase(test.TestCase):
for ss in share_servers:
self.assertEqual(constants.STATUS_NETWORK_CHANGE, ss['status'])
def test_encryption_keys_get_count(self):
servers = [db_utils.create_share_server(
encryption_key_ref=uuidutils.generate_uuid())
for __ in range(1, 4)]
count = db_api.encryption_keys_get_count(context.get_admin_context())
self.assertEqual(count, len(servers))
class ServiceDatabaseAPITestCase(test.TestCase):
View File
Whitespace-only changes.
+113
View File
@@ -0,0 +1,113 @@
# Copyright 2025 Cloudification 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 unittest import mock
from oslo_config import cfg
from oslo_utils import uuidutils
from manila import exception
from manila.keymgr import barbican as barbican_module
from manila import test
CONF = cfg.CONF
class BarbicanSecretACLTestCase(test.TestCase):
def setUp(self):
super(BarbicanSecretACLTestCase, self).setUp()
self.context = mock.Mock()
self.secret_ref = 'mock-secret-id'
self.barbican_acl = barbican_module.BarbicanSecretACL(CONF)
@mock.patch('barbicanclient.client.Client')
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.identity.v3.Token')
def test_get_client_and_href_with_valid_secret(self, mock_token,
mock_session, mock_client):
mock_href = uuidutils.generate_uuid()
mock_instance = mock.Mock()
mock_client.return_value = mock_instance
self.mock_object(self.barbican_acl, '_get_barbican_endpoint')
self.mock_object(self.barbican_acl, '_create_base_url')
self.mock_object(self.barbican_acl, '_create_secret_ref',
mock.Mock(return_value=mock_href))
result_client, result_href = self.barbican_acl.get_client_and_href(
self.context, mock_href)
self.assertEqual(mock_instance, result_client)
self.assertIn(mock_href, result_href)
def test_get_client_and_href_missing_backend(self):
CONF.set_default('backend', 'wrong.backend', group='key_manager')
self.assertRaises(
exception.ManilaBarbicanACLError,
self.barbican_acl.get_client_and_href,
self.context,
self.secret_ref)
def test_get_client_and_href_missing_secret_ref(self):
self.assertRaises(
exception.ManilaBarbicanACLError,
self.barbican_acl.get_client_and_href,
self.context,
None)
class BarbicanUserAppCredsTestCase(test.TestCase):
def setUp(self):
super(BarbicanUserAppCredsTestCase, self).setUp()
self.context = mock.Mock()
self.app_creds = barbican_module.BarbicanUserAppCreds(CONF)
@mock.patch('keystoneclient.v3.client.Client')
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.loading.load_auth_from_conf_options')
def test_get_application_credentials_success(self, mock_auth,
mock_session, mock_client):
fake_cred = mock.Mock()
mock_instance = mock.Mock()
mock_instance.application_credentials.get.return_value = fake_cred
mock_client.return_value = mock_instance
result = self.app_creds.get_application_credentials(
self.context, 'fake_id')
self.assertEqual(fake_cred, result)
def test_get_application_credentials_missing_id(self):
self.assertRaises(
exception.ManilaBarbicanAppCredsError,
self.app_creds.get_application_credentials,
self.context,
None)
@mock.patch('keystoneclient.v3.client.Client')
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.loading.load_auth_from_conf_options')
def test_delete_application_credentials(self, mock_auth,
mock_session, mock_client):
mock_instance = mock.Mock()
mock_client.return_value = mock_instance
self.app_creds.delete_application_credentials(self.context, 'cred_id')
mock_instance.application_credentials.delete.assert_called_once()
def test_delete_application_credentials_missing_id(self):
self.assertRaises(
exception.ManilaBarbicanAppCredsError,
self.app_creds.delete_application_credentials,
self.context,
None)
+21 -10
View File
@@ -218,7 +218,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host2@back1#BBB',
@@ -253,7 +254,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host2@back2#CCC',
@@ -288,7 +290,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
},
]
@@ -345,7 +348,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host2@BBB#pool2',
@@ -381,7 +385,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host3@CCC#pool3',
@@ -417,7 +422,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host4@DDD#pool4a',
@@ -453,7 +459,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host4@DDD#pool4b',
@@ -489,7 +496,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None,
},
},
]
@@ -558,7 +566,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
}, {
'name': 'host2@back1#BBB',
@@ -593,7 +602,8 @@ class HostManagerTestCase(test.TestCase):
'network_allocation_update_support': False,
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False
'share_replicas_migration_support': False,
'encryption_support': None
},
},
]
@@ -657,6 +667,7 @@ class HostManagerTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'mount_point_name_support': False,
'share_replicas_migration_support': False,
'encryption_support': None
},
},
]
@@ -183,6 +183,7 @@ class EMCShareFrameworkTestCase(test.TestCase):
data['share_server_multiple_subnet_support'] = False
data['network_allocation_update_support'] = False
data['share_replicas_migration_support'] = False
data['encryption_support'] = None
self.assertEqual(data, self.driver._stats)
def _fake_safe_get(self, value):
+17 -4
View File
@@ -40,6 +40,7 @@ from oslo_utils import timeutils
from manila.common import constants
from manila import exception
from manila.i18n import _
from manila.keymgr import barbican as barbican_api
from manila.share import configuration
from manila.share import driver
from manila.share.manager import share_manager_opts # noqa
@@ -150,6 +151,7 @@ class DummyDriver(driver.ShareDriver):
self.security_service_update_support = True
self.network_allocation_update_support = True
self.share_replicas_migration_support = True
self.encryption_support = ['share_server']
def _verify_configuration(self):
allowed_driver_methods = [m for m in dir(self) if m[0] != '_']
@@ -251,12 +253,19 @@ class DummyDriver(driver.ShareDriver):
return export_locations
def _create_share(self, share, share_server=None):
def _create_share(self, context, share, share_server=None):
share_proto = share["share_proto"]
if share_proto not in ("NFS", "CIFS"):
msg = _("Unsupported share protocol provided - %s.") % share_proto
raise exception.InvalidShareAccess(reason=msg)
encryption_key_ref = share.get('encryption_key_ref')
if encryption_key_ref and context:
encryption_key_href = barbican_api.get_secret_href(
context, encryption_key_ref)
LOG.debug("Generated encryption_key_href %s for share create "
"request.", encryption_key_href)
share_name = self._get_share_name(share)
mountpoint = "/path/to/fake/share/%s" % share_name
self.private_storage.update(
@@ -271,13 +280,14 @@ class DummyDriver(driver.ShareDriver):
@slow_me_down
def create_share(self, context, share, share_server=None):
"""Is called to create share."""
return self._create_share(share, share_server=share_server)
return self._create_share(context, share, share_server=share_server)
@slow_me_down
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None, parent_share=None):
"""Is called to create share from snapshot."""
export_locations = self._create_share(share, share_server=share_server)
export_locations = self._create_share(
context, share, share_server=share_server)
return {
'export_locations': export_locations,
'status': constants.STATUS_AVAILABLE
@@ -361,7 +371,10 @@ class DummyDriver(driver.ShareDriver):
old_export = self.private_storage.get(
old_share_id, key='export_location')
if old_export.split(":/")[-1] == new_export.split(":/")[-1]:
result = {"size": 1, "export_locations": self._create_share(share)}
result = {
"size": 1,
"export_locations": self._create_share(None, share)
}
self.private_storage.delete(old_share_id)
return result
else:
@@ -275,6 +275,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'network_allocation_update_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
}
self.assertEqual(test_data, self._driver._stats)
@@ -755,6 +755,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'network_allocation_update_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
}
result = self.driver.get_share_stats(refresh=True)
@@ -836,6 +837,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'network_allocation_update_support': False,
'mount_snapshot_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
'share_group_stats': {
'consistent_snapshot_support': None,
},
@@ -888,6 +890,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'network_allocation_update_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
'mount_snapshot_support': False,
'share_group_stats': {
'consistent_snapshot_support': None,
@@ -2440,6 +2440,7 @@ class HuaweiShareDriverTestCase(test.TestCase):
"share_server_multiple_subnet_support": False,
"network_allocation_update_support": False,
"share_replicas_migration_support": False,
'encryption_support': None,
}
if replication_support:
@@ -452,6 +452,7 @@ class ACCESSShareDriverTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'network_allocation_update_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
}
self.assertEqual(data, self._driver._stats)
@@ -383,6 +383,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
'share_server_multiple_subnet_support': False,
'network_allocation_update_support': False,
'share_replicas_migration_support': False,
'encryption_support': None,
}
if replication_domain:
expected['replication_type'] = 'readable'
+38 -2
View File
@@ -32,6 +32,7 @@ from manila.data import rpcapi as data_rpc
from manila import db as db_api
from manila.db.sqlalchemy import models
from manila import exception
from manila.keymgr import barbican as barbican_api
from manila import policy
from manila import quota
from manila import share
@@ -947,6 +948,7 @@ class ShareAPITestCase(test.TestCase):
az_request_multiple_subnet_support_map=compatible_azs_multiple,
snapshot_host=None,
scheduler_hints=None, mount_point_name=None,
encryption_key_ref=None,
)
db_api.share_get.assert_called_once()
@@ -1202,6 +1204,38 @@ class ShareAPITestCase(test.TestCase):
share_data['display_description']
)
@ddt.data(
{'overs': {'encryption_keys': 'fake'},
'expected_exception': (
exception.EncryptionKeysLimitExceeded)
})
@ddt.unpack
def test_create_share_over_encryption_keys_quota(self, overs,
expected_exception):
share, share_data = self._setup_create_mocks()
quota.CONF.set_default("encryption_keys", 2, 'quota')
share_data['encryption_key_ref'] = uuidutils.generate_uuid()
usages = {'encryption_keys': {'reserved': 0, 'in_use': 2}}
quotas = {'encryption_keys': 2}
exc = exception.OverQuota(overs=overs, usages=usages, quotas=quotas)
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock(side_effect=exc))
self.mock_object(db_api, 'share_server_get_all_with_filters',
mock.Mock(return_value={}))
self.mock_object(barbican_api, 'create_secret_access')
self.mock_object(db_api, 'encryption_keys_get_count',
mock.Mock(return_value=2))
self.assertRaises(
expected_exception,
self.api.create,
self.context,
share_data['share_proto'],
share_data['size'],
share_data['display_name'],
share_data['display_description'],
encryption_key_ref=share_data['encryption_key_ref']
)
@ddt.data(exception.QuotaError, exception.InvalidShare)
def test_create_share_error_on_quota_commit(self, expected_exception):
share, share_data = self._setup_create_mocks()
@@ -1245,6 +1279,7 @@ class ShareAPITestCase(test.TestCase):
'share_type_id': 'fake_share_type',
'cast_rules_to_readonly': False,
'mount_point_name': None,
'encryption_key_ref': None
}
)
db_api.share_type_get.assert_called_once_with(
@@ -1279,6 +1314,7 @@ class ShareAPITestCase(test.TestCase):
'share_type_id': 'fake_share_type',
'cast_rules_to_readonly': False,
'mount_point_name': 'fake_mp',
'encryption_key_ref': None
}
)
@@ -1749,7 +1785,6 @@ class ShareAPITestCase(test.TestCase):
task_state=None)
self.mock_object(db_api, 'share_update', mock.Mock())
self.api.unmanage(self.context, share)
self.share_rpcapi.unmanage_share.assert_called_once_with(
@@ -2689,7 +2724,8 @@ class ShareAPITestCase(test.TestCase):
availability_zones=None,
az_request_multiple_subnet_support_map=None,
snapshot_host=snapshot['share']['instance']['host'],
scheduler_hints=None, mount_point_name=None)
scheduler_hints=None, mount_point_name=None,
encryption_key_ref=None)
share_api.policy.check_policy.assert_called_once_with(
self.context, 'share_snapshot', 'get_snapshot',
snapshot, do_raise=False)
+78 -3
View File
@@ -33,6 +33,7 @@ from manila.data import rpcapi as data_rpc
from manila import db
from manila.db.sqlalchemy import models
from manila import exception
from manila.keymgr import barbican as barbican_api
from manila.message import message_field
from manila import quota
from manila.share import api
@@ -3296,7 +3297,80 @@ class ShareManagerTestCase(test.TestCase):
)
driver_method_mock.assert_called_once_with(
self.context, [fake_share_server], share.instance,
snapshot=None, share_group=None)
snapshot=None, share_group=None, encryption_key_ref=None)
@ddt.data(True, False)
def test_provide_share_server_encryption_key_ref(self, acl_error):
fake_share_network = db_utils.create_share_network(id='fake_sn_id')
fake_share_net_subnets = [db_utils.create_share_network_subnet(
id='fake_sns_id', share_network_id=fake_share_network['id']
)]
fake_share_server = db_utils.create_share_server(
id='fake',
encryption_key_ref='fake_ref'
)
share = db_utils.create_share(encryption_key_ref='fake_ref')
self.share_manager.driver.encryption_support = 'share_server'
db_method_mock = self.mock_object(
db, 'share_network_subnets_get_all_by_availability_zone_id',
mock.Mock(return_value=fake_share_net_subnets))
self.mock_object(
db, 'share_server_get_all_by_host_and_share_subnet_valid',
mock.Mock(return_value=[fake_share_server]))
self.mock_object(
self.share_manager, '_check_share_server_backend_limits',
mock.Mock(return_value=[fake_share_server]))
self.mock_object(
self.share_manager.driver,
"choose_share_server_compatible_with_share",
mock.Mock(return_value=None)
)
mock_server_create = self.mock_object(
db, 'share_server_create',
mock.Mock(return_value=fake_share_server))
fake_exception = exception.ManilaBarbicanACLError()
self.mock_object(
barbican_api, 'create_secret_access',
mock.Mock(side_effect=fake_exception) if acl_error else
mock.Mock())
mock_obj = mock.Mock()
mock_obj.to_dict.return_value = {
'id': 'fake_application_credential_id'
}
mock_create_app_cred = self.mock_object(
barbican_api, 'create_application_credentials',
mock.Mock(return_value=mock_obj))
mock_get_secret_ref = self.mock_object(
barbican_api, 'get_secret_href',
mock.Mock(return_value='fake_secret_href'))
if acl_error:
self.assertRaises(
exception.ManilaBarbicanACLError,
self.share_manager._provide_share_server_for_share,
self.context, fake_share_network['id'], share.instance)
else:
self.share_manager._provide_share_server_for_share(
self.context, fake_share_network['id'], share.instance)
mock_server_create.assert_called()
mock_create_app_cred.assert_called()
mock_get_secret_ref.assert_called()
db_method_mock.assert_called_once_with(
self.context, fake_share_network['id'],
availability_zone_id=share.instance.get('availability_zone_id')
)
driver_mock = self.share_manager.driver
driver_method_mock = (
driver_mock.choose_share_server_compatible_with_share
)
driver_method_mock.assert_called_once_with(
self.context, [fake_share_server], share.instance,
snapshot=None, share_group=None, encryption_key_ref='fake_ref')
def test_provide_share_server_for_share_invalid_arguments(self):
self.assertRaises(ValueError,
@@ -9086,10 +9160,11 @@ class ShareManagerTestCase(test.TestCase):
def test__build_server_metadata(self):
share = {'host': 'host', 'share_type_id': 'id'}
expected_metadata = {'request_host': 'host', 'share_type_id': 'id'}
expected_metadata = {'request_host': 'host', 'share_type_id': 'id',
'encryption_key_ref': None, 'keystone_url': None}
metadata = self.share_manager._build_server_metadata(
share['host'], share['share_type_id'])
self.context, share['host'], share['share_type_id'])
self.assertDictEqual(expected_metadata, metadata)
+6 -1
View File
@@ -412,12 +412,17 @@ class ShareTypesTestCase(test.TestCase):
list(itertools.product(
(constants.ExtraSpecs.REPLICATION_TYPE_SPEC,),
constants.ExtraSpecs.REPLICATION_TYPES)) +
list(itertools.product(
(constants.ExtraSpecs.ENCRYPTION_SUPPORT,),
constants.ExtraSpecs.ENCRYPTION_TYPES)) +
[(constants.ExtraSpecs.AVAILABILITY_ZONES, 'zone a, zoneb$c'),
(constants.ExtraSpecs.AVAILABILITY_ZONES, ' zonea, zoneb'),
(constants.ExtraSpecs.AVAILABILITY_ZONES, 'zone1')] +
[(constants.ExtraSpecs.PROVISIONING_MOUNT_POINT_PREFIX, 'gold'),
(constants.ExtraSpecs.PROVISIONING_MOUNT_POINT_PREFIX, 'silver'),
(constants.ExtraSpecs.PROVISIONING_MOUNT_POINT_PREFIX, 'bronze')]
(constants.ExtraSpecs.PROVISIONING_MOUNT_POINT_PREFIX, 'bronze')] +
[(constants.ExtraSpecs.ENCRYPTION_SUPPORT, 'share'),
(constants.ExtraSpecs.ENCRYPTION_SUPPORT, 'share_server')]
))
@ddt.unpack
def test_is_valid_optional_extra_spec_valid(self, key, value):
+2 -1
View File
@@ -717,5 +717,6 @@ class QuotaEngineTestCase(test.TestCase):
sorted(['gigabytes', 'per_share_gigabytes', 'replica_gigabytes',
'share_group_snapshots', 'share_groups', 'share_networks',
'share_replicas', 'shares', 'snapshot_gigabytes',
'snapshots', 'backups', 'backup_gigabytes']),
'snapshots', 'backups', 'backup_gigabytes',
'encryption_keys']),
quota.QUOTAS.resources)
@@ -0,0 +1,18 @@
---
features:
- |
Starting with microversion 2.90, Manila allows end users to provide
barbican secret_ref as encryption key reference in share create API. This
will be used as either share encryption key reference or share server
encryption key reference, based on backend driver support for share
encryption.
issues:
- |
When using share types with ``encryption_support=share_server``, share
servers created cannot currently be unmanaged from Manila. This limitation
will be addressed in a future release.
Additionally, care must be taken when managing existing shares with
``driver_handles_share_servers=True`` (DHSS=True). You must use a share type
that supports encryption if the share must be encrypted or if its existing
encryption status needs to be retained.
+2
View File
@@ -6,6 +6,7 @@
pbr>=5.5.0 # Apache-2.0
alembic>=1.4.2 # MIT
castellan>=3.7.0 # Apache-2.0
defusedxml>=0.7.1 # PSFL
eventlet>=0.27.0 # MIT
greenlet>=0.4.16 # MIT
@@ -42,6 +43,7 @@ SQLAlchemy>=1.4.0 # MIT
SQLAlchemy-Utils>=0.38.3 # BSD License
stevedore>=3.2.2 # Apache-2.0
tooz>=2.7.1 # Apache-2.0
python-barbicanclient>=5.0.1 # Apache-2.0
python-cinderclient>=4.0.1 # Apache-2.0
python-novaclient>=17.2.1 # Apache-2.0
python-glanceclient>=3.2.2 # Apache-2.0