Coverage for manila/data/helper.py: 93%
158 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2015 Hitachi Data Systems.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15"""Helper class for Data Service operations."""
17import os
19from oslo_config import cfg
20from oslo_log import log
22from manila.common import constants
23from manila import exception
24from manila.i18n import _
25from manila.share import access as access_manager
26from manila.share import rpcapi as share_rpc
27from manila import utils
29LOG = log.getLogger(__name__)
31data_helper_opts = [
32 cfg.IntOpt(
33 'data_access_wait_access_rules_timeout',
34 default=180,
35 help="Time to wait for access rules to be allowed/denied on backends "
36 "when migrating a share (seconds)."),
37 cfg.ListOpt('data_node_access_ips',
38 default=[],
39 help="A list of the IPs of the node interface connected to "
40 "the admin network. Used for allowing access to the "
41 "mounting shares. Default is []."),
42 cfg.StrOpt(
43 'data_node_access_cert',
44 help="The certificate installed in the data node in order to "
45 "allow access to certificate authentication-based shares."),
46 cfg.StrOpt(
47 'data_node_access_admin_user',
48 help="The admin user name registered in the security service in order "
49 "to allow access to user authentication-based shares."),
50 cfg.DictOpt(
51 'data_node_mount_options',
52 default={},
53 help="Mount options to be included in the mount command for share "
54 "protocols. Use dictionary format, example: "
55 "{'nfs': '-o nfsvers=3', 'cifs': '-o user=foo,pass=bar'}"),
57]
59CONF = cfg.CONF
60CONF.register_opts(data_helper_opts)
63class DataServiceHelper(object):
65 def __init__(self, context, db, share):
67 self.db = db
68 self.share = share
69 self.context = context
70 self.share_rpc = share_rpc.ShareAPI()
71 self.access_helper = access_manager.ShareInstanceAccess(self.db, None)
72 self.wait_access_rules_timeout = (
73 CONF.data_access_wait_access_rules_timeout)
75 def deny_access_to_data_service(self, access_ref_list, share_instance):
76 self._change_data_access_to_instance(
77 share_instance, access_ref_list, deny=True)
79 # NOTE(ganso): Cleanup methods do not throw exceptions, since the
80 # exceptions that should be thrown are the ones that call the cleanup
82 def cleanup_data_access(self, access_ref_list, share_instance):
84 try:
85 self.deny_access_to_data_service(
86 access_ref_list, share_instance)
87 except Exception:
88 LOG.warning("Could not cleanup access rule of share %s.",
89 self.share['id'])
91 def cleanup_temp_folder(self, mount_path, instance_id):
92 try:
93 path = os.path.join(mount_path, instance_id)
94 if os.path.exists(path): 94 ↛ 96line 94 didn't jump to line 96 because the condition on line 94 was always true
95 os.rmdir(path)
96 self._check_dir_not_exists(path)
97 except Exception:
98 LOG.warning("Could not cleanup instance %(instance_id)s "
99 "temporary folders for data copy of "
100 "share %(share_id)s.", {
101 'instance_id': instance_id,
102 'share_id': self.share['id']})
104 def cleanup_unmount_temp_folder(self, unmount_info, mount_path):
105 share_instance_id = unmount_info.get('share_instance_id')
106 try:
107 self.unmount_share_instance_or_backup(unmount_info, mount_path)
108 except Exception:
109 LOG.warning("Could not unmount folder of instance"
110 " %(instance_id)s for data copy of "
111 "share %(share_id)s.", {
112 'instance_id': share_instance_id,
113 'share_id': self.share['id']})
115 def _change_data_access_to_instance(
116 self, instance, accesses=None, deny=False):
118 self.access_helper.get_and_update_share_instance_access_rules_status(
119 self.context, status=constants.SHARE_INSTANCE_RULES_SYNCING,
120 share_instance_id=instance['id'])
122 if deny:
123 if accesses is None: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 accesses = []
125 else:
126 if not isinstance(accesses, list): 126 ↛ 129line 126 didn't jump to line 129 because the condition on line 126 was always true
127 accesses = [accesses]
129 access_filters = {'access_id': [a['id'] for a in accesses]}
130 updates = {'state': constants.ACCESS_STATE_QUEUED_TO_DENY}
131 self.access_helper.get_and_update_share_instance_access_rules(
132 self.context, filters=access_filters, updates=updates,
133 share_instance_id=instance['id'])
135 self.share_rpc.update_access(self.context, instance)
137 utils.wait_for_access_update(
138 self.context, self.db, instance, self.wait_access_rules_timeout)
140 def allow_access_to_data_service(
141 self, share_instance, connection_info_src,
142 dest_share_instance=None, connection_info_dest=None):
144 allow_access_to_destination_instance = (dest_share_instance and
145 connection_info_dest)
147 # NOTE(ganso): intersect the access type compatible with both instances
148 if allow_access_to_destination_instance:
149 access_mapping = {}
150 for a_type, protocols in (
151 connection_info_src['access_mapping'].items()):
152 for proto in protocols:
153 if (a_type in connection_info_dest['access_mapping'] and
154 proto in
155 connection_info_dest['access_mapping'][a_type]):
156 access_mapping[a_type] = access_mapping.get(a_type, [])
157 access_mapping[a_type].append(proto)
158 else:
159 access_mapping = connection_info_src['access_mapping']
161 access_list = self._get_access_entries_according_to_mapping(
162 access_mapping)
163 access_ref_list = []
165 for access in access_list:
167 values = {
168 'share_id': self.share['id'],
169 'access_type': access['access_type'],
170 'access_level': access['access_level'],
171 'access_to': access['access_to'],
172 }
174 # Check if the rule being added already exists. If so, we will
175 # remove it to prevent conflicts
176 old_access_list = self.db.share_access_get_all_by_type_and_access(
177 self.context, self.share['id'], access['access_type'],
178 access['access_to'])
179 if old_access_list: 179 ↛ 183line 179 didn't jump to line 183 because the condition on line 179 was always true
180 self._change_data_access_to_instance(
181 share_instance, old_access_list, deny=True)
183 access_ref = self.db.share_instance_access_create(
184 self.context, values, share_instance['id'])
185 self._change_data_access_to_instance(share_instance)
187 if allow_access_to_destination_instance:
188 access_ref = self.db.share_instance_access_create(
189 self.context, values, dest_share_instance['id'])
190 self._change_data_access_to_instance(dest_share_instance)
192 # The access rule ref used here is a regular Share Access Map,
193 # instead of a Share Instance Access Map.
194 access_ref_list.append(access_ref)
196 return access_ref_list
198 def _get_access_entries_according_to_mapping(self, access_mapping):
200 access_list = []
202 # NOTE(ganso): protocol is not relevant here because we previously
203 # used it to filter the access types we are interested in
204 for access_type, protocols in access_mapping.items():
205 access_to_list = []
206 if access_type.lower() == 'cert' and CONF.data_node_access_cert:
207 access_to_list.append(CONF.data_node_access_cert)
208 elif access_type.lower() == 'ip':
209 ips = CONF.data_node_access_ips
210 if ips:
211 if not isinstance(ips, list):
212 ips = [ips]
213 access_to_list.extend(ips)
214 elif (access_type.lower() == 'user' and
215 CONF.data_node_access_admin_user):
216 access_to_list.append(CONF.data_node_access_admin_user)
217 else:
218 msg = _("Unsupported access type provided: %s.") % access_type
219 raise exception.ShareDataCopyFailed(reason=msg)
220 if not access_to_list:
221 msg = _("Configuration for Data node mounting access type %s "
222 "has not been set.") % access_type
223 raise exception.ShareDataCopyFailed(reason=msg)
225 for access_to in access_to_list:
226 access = {
227 'access_type': access_type,
228 'access_level': constants.ACCESS_LEVEL_RW,
229 'access_to': access_to,
230 }
231 access_list.append(access)
233 return access_list
235 @utils.retry(retry_param=exception.NotFound,
236 interval=1,
237 retries=10,
238 backoff_rate=1)
239 def _check_dir_exists(self, path):
240 if not os.path.exists(path): 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true
241 raise exception.NotFound("Folder %s could not be found." % path)
243 @utils.retry(retry_param=exception.Found,
244 interval=1,
245 retries=10,
246 backoff_rate=1)
247 def _check_dir_not_exists(self, path):
248 if os.path.exists(path):
249 raise exception.Found("Folder %s was found." % path)
251 def mount_share_instance_or_backup(self, mount_info, mount_path):
252 mount_point = mount_info.get('mount_point')
253 mount_template = mount_info.get('mount')
254 share_instance_id = mount_info.get('share_instance_id')
255 backup = mount_info.get('backup')
256 restore = mount_info.get('restore')
257 backup_id = mount_info.get('backup_id')
259 if share_instance_id:
260 path = os.path.join(mount_path, share_instance_id)
261 else:
262 path = ''
264 # overwrite path in case different mount point is explicitly provided
265 if mount_point and mount_point != path:
266 path = mount_point
268 if share_instance_id:
269 share_instance = self.db.share_instance_get(
270 self.context, share_instance_id, with_share_data=True)
271 options = CONF.data_node_mount_options
272 options = {k.lower(): v for k, v in options.items()}
273 proto_options = options.get(
274 share_instance['share_proto'].lower(), '')
275 else:
276 # For backup proto_options are included in mount_template
277 proto_options = ''
279 if not os.path.exists(path): 279 ↛ 281line 279 didn't jump to line 281 because the condition on line 279 was always true
280 os.makedirs(path)
281 self._check_dir_exists(path)
283 mount_command = mount_template % {'path': path,
284 'options': proto_options}
285 utils.execute(*(mount_command.split()), run_as_root=True)
286 if backup:
287 # we create new folder, which named with backup_id. To distinguish
288 # different backup data at mount points
289 backup_folder = os.path.join(path, backup_id)
290 if not os.path.exists(backup_folder): 290 ↛ 292line 290 didn't jump to line 292 because the condition on line 290 was always true
291 os.makedirs(backup_folder)
292 self._check_dir_exists(backup_folder)
293 if restore:
294 # backup_folder should exist after mount, else backup is
295 # already deleted
296 backup_folder = os.path.join(path, backup_id)
297 if not os.path.exists(backup_folder): 297 ↛ 298line 297 didn't jump to line 298 because the condition on line 297 was never true
298 raise exception.ShareBackupNotFound(backup_id=backup_id)
300 def unmount_share_instance_or_backup(self, unmount_info, mount_path):
301 mount_point = unmount_info.get('mount_point')
302 unmount_template = unmount_info.get('unmount')
303 share_instance_id = unmount_info.get('share_instance_id')
305 if share_instance_id: 305 ↛ 308line 305 didn't jump to line 308 because the condition on line 305 was always true
306 path = os.path.join(mount_path, share_instance_id)
307 else:
308 path = ''
310 # overwrite path in case different mount point is explicitly provided
311 if mount_point and mount_point != path: 311 ↛ 312line 311 didn't jump to line 312 because the condition on line 311 was never true
312 path = mount_point
314 unmount_command = unmount_template % {'path': path}
315 utils.execute(*(unmount_command.split()), run_as_root=True)
317 try:
318 if os.path.exists(path): 318 ↛ 320line 318 didn't jump to line 320 because the condition on line 318 was always true
319 os.rmdir(path)
320 self._check_dir_not_exists(path)
321 except Exception:
322 LOG.warning("Folder %s could not be removed.", path)