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

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.""" 

16 

17import os 

18 

19from oslo_config import cfg 

20from oslo_log import log 

21 

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 

28 

29LOG = log.getLogger(__name__) 

30 

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'}"), 

56 

57] 

58 

59CONF = cfg.CONF 

60CONF.register_opts(data_helper_opts) 

61 

62 

63class DataServiceHelper(object): 

64 

65 def __init__(self, context, db, share): 

66 

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) 

74 

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) 

78 

79 # NOTE(ganso): Cleanup methods do not throw exceptions, since the 

80 # exceptions that should be thrown are the ones that call the cleanup 

81 

82 def cleanup_data_access(self, access_ref_list, share_instance): 

83 

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']) 

90 

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']}) 

103 

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']}) 

114 

115 def _change_data_access_to_instance( 

116 self, instance, accesses=None, deny=False): 

117 

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']) 

121 

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] 

128 

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']) 

134 

135 self.share_rpc.update_access(self.context, instance) 

136 

137 utils.wait_for_access_update( 

138 self.context, self.db, instance, self.wait_access_rules_timeout) 

139 

140 def allow_access_to_data_service( 

141 self, share_instance, connection_info_src, 

142 dest_share_instance=None, connection_info_dest=None): 

143 

144 allow_access_to_destination_instance = (dest_share_instance and 

145 connection_info_dest) 

146 

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'] 

160 

161 access_list = self._get_access_entries_according_to_mapping( 

162 access_mapping) 

163 access_ref_list = [] 

164 

165 for access in access_list: 

166 

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 } 

173 

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) 

182 

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) 

186 

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) 

191 

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) 

195 

196 return access_ref_list 

197 

198 def _get_access_entries_according_to_mapping(self, access_mapping): 

199 

200 access_list = [] 

201 

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) 

224 

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) 

232 

233 return access_list 

234 

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) 

242 

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) 

250 

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') 

258 

259 if share_instance_id: 

260 path = os.path.join(mount_path, share_instance_id) 

261 else: 

262 path = '' 

263 

264 # overwrite path in case different mount point is explicitly provided 

265 if mount_point and mount_point != path: 

266 path = mount_point 

267 

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 = '' 

278 

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) 

282 

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) 

299 

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') 

304 

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 = '' 

309 

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 

313 

314 unmount_command = unmount_template % {'path': path} 

315 utils.execute(*(unmount_command.split()), run_as_root=True) 

316 

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)