Coverage for manila/volume/cinder.py: 77%

206 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2014 Mirantis Inc. 

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 

16""" 

17Handles all requests relating to volumes + cinder. 

18""" 

19 

20import copy 

21 

22from cinderclient import exceptions as cinder_exception 

23from cinderclient.v3 import client as cinder_client 

24from keystoneauth1 import loading as ks_loading 

25from oslo_config import cfg 

26from oslo_log import log 

27 

28from manila.common import client_auth 

29from manila.common.config import core_opts 

30from manila.common import constants as const 

31import manila.context as ctxt 

32from manila.db import base 

33from manila import exception 

34from manila.i18n import _ 

35from manila import utils 

36 

37LOG = log.getLogger(__name__) 

38 

39CINDER_GROUP = 'cinder' 

40AUTH_OBJ = None 

41 

42cinder_opts = [ 

43 cfg.BoolOpt('cross_az_attach', 

44 default=True, 

45 help='Allow attaching between instances and volumes in ' 

46 'different availability zones.'), 

47 cfg.IntOpt('http_retries', 

48 default=3, 

49 help='Number of cinderclient retries on failed HTTP calls.'), 

50 cfg.StrOpt('endpoint_type', 

51 default='publicURL', 

52 choices=['publicURL', 'internalURL', 'adminURL', 

53 'public', 'internal', 'admin'], 

54 help='Endpoint type to be used with cinder client calls.'), 

55 cfg.StrOpt('region_name', 

56 help='Region name for connecting to cinder.'), 

57 ] 

58 

59 

60CONF = cfg.CONF 

61CONF.register_opts(core_opts) 

62CONF.register_opts(cinder_opts, CINDER_GROUP) 

63ks_loading.register_session_conf_options(CONF, 

64 CINDER_GROUP) 

65ks_loading.register_auth_conf_options(CONF, CINDER_GROUP) 

66 

67 

68def list_opts(): 

69 return client_auth.AuthClientLoader.list_opts(CINDER_GROUP) 

70 

71 

72def cinderclient(context): 

73 global AUTH_OBJ 

74 if not AUTH_OBJ: 

75 AUTH_OBJ = client_auth.AuthClientLoader( 

76 client_class=cinder_client.Client, cfg_group=CINDER_GROUP) 

77 return AUTH_OBJ.get_client(context, 

78 retries=CONF[CINDER_GROUP].http_retries, 

79 endpoint_type=CONF[CINDER_GROUP].endpoint_type, 

80 region_name=CONF[CINDER_GROUP].region_name) 

81 

82 

83def _untranslate_volume_summary_view(context, vol): 

84 """Maps keys for volumes summary view.""" 

85 d = {} 

86 d['id'] = vol.id 

87 d['status'] = vol.status 

88 d['size'] = vol.size 

89 d['availability_zone'] = vol.availability_zone 

90 d['created_at'] = vol.created_at 

91 

92 d['attach_time'] = "" 

93 d['mountpoint'] = "" 

94 

95 if vol.attachments: 

96 att = vol.attachments[0] 

97 d['attach_status'] = 'attached' 

98 d['instance_uuid'] = att['server_id'] 

99 d['mountpoint'] = att['device'] 

100 else: 

101 d['attach_status'] = 'detached' 

102 

103 d['name'] = vol.name 

104 d['description'] = vol.description 

105 

106 d['volume_type_id'] = vol.volume_type 

107 d['snapshot_id'] = vol.snapshot_id 

108 

109 d['volume_metadata'] = {} 

110 for key, value in vol.metadata.items(): 

111 d['volume_metadata'][key] = value 

112 

113 if hasattr(vol, 'volume_image_metadata'): 

114 d['volume_image_metadata'] = copy.deepcopy(vol.volume_image_metadata) 

115 

116 return d 

117 

118 

119def _untranslate_snapshot_summary_view(context, snapshot): 

120 """Maps keys for snapshots summary view.""" 

121 d = {} 

122 

123 d['id'] = snapshot.id 

124 d['status'] = snapshot.status 

125 d['progress'] = snapshot.progress 

126 d['size'] = snapshot.size 

127 d['created_at'] = snapshot.created_at 

128 d['name'] = snapshot.name 

129 d['description'] = snapshot.description 

130 d['volume_id'] = snapshot.volume_id 

131 d['project_id'] = snapshot.project_id 

132 d['volume_size'] = snapshot.size 

133 

134 return d 

135 

136 

137def translate_volume_exception(method): 

138 """Transforms the exception for the volume, keeps its traceback intact.""" 

139 def wrapper(self, ctx, volume_id, *args, **kwargs): 

140 try: 

141 res = method(self, ctx, volume_id, *args, **kwargs) 

142 except cinder_exception.ClientException as e: 

143 if isinstance(e, cinder_exception.NotFound): 

144 raise exception.VolumeNotFound(volume_id=volume_id) 

145 elif isinstance(e, cinder_exception.BadRequest): 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was always true

146 raise exception.InvalidInput(reason=str(e)) 

147 return res 

148 return wrapper 

149 

150 

151def translate_snapshot_exception(method): 

152 """Transforms the exception for the snapshot. 

153 

154 Note: Keeps its traceback intact. 

155 """ 

156 def wrapper(self, ctx, snapshot_id, *args, **kwargs): 

157 try: 

158 res = method(self, ctx, snapshot_id, *args, **kwargs) 

159 except cinder_exception.ClientException as e: 

160 if isinstance(e, cinder_exception.NotFound): 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was always true

161 raise exception.VolumeSnapshotNotFound(snapshot_id=snapshot_id) 

162 return res 

163 return wrapper 

164 

165 

166class API(base.Base): 

167 """API for interacting with the volume manager.""" 

168 @translate_volume_exception 

169 def get(self, context, volume_id): 

170 item = cinderclient(context).volumes.get(volume_id) 

171 return _untranslate_volume_summary_view(context, item) 

172 

173 def get_all(self, context, search_opts={}): 

174 items = cinderclient(context).volumes.list(detailed=True, 

175 search_opts=search_opts) 

176 rval = [] 

177 

178 for item in items: 

179 rval.append(_untranslate_volume_summary_view(context, item)) 

180 

181 return rval 

182 

183 def check_attached(self, context, volume): 

184 """Raise exception if volume in use.""" 

185 if volume['status'] != "in-use": 

186 msg = _("status must be 'in-use'") 

187 raise exception.InvalidVolume(msg) 

188 

189 def check_attach(self, context, volume, instance=None): 

190 if volume['status'] != "available": 

191 msg = _("status must be 'available'") 

192 raise exception.InvalidVolume(msg) 

193 if volume['attach_status'] == "attached": 

194 msg = _("already attached") 

195 raise exception.InvalidVolume(msg) 

196 if instance and not CONF[CINDER_GROUP].cross_az_attach: 196 ↛ exitline 196 didn't return from function 'check_attach' because the condition on line 196 was always true

197 if instance['availability_zone'] != volume['availability_zone']: 

198 msg = _("Instance and volume not in same availability_zone") 

199 raise exception.InvalidVolume(msg) 

200 

201 def check_detach(self, context, volume): 

202 if volume['status'] == "available": 

203 msg = _("already detached") 

204 raise exception.InvalidVolume(msg) 

205 

206 @translate_volume_exception 

207 def reserve_volume(self, context, volume_id): 

208 cinderclient(context).volumes.reserve(volume_id) 

209 

210 @translate_volume_exception 

211 def unreserve_volume(self, context, volume_id): 

212 cinderclient(context).volumes.unreserve(volume_id) 

213 

214 @translate_volume_exception 

215 def begin_detaching(self, context, volume_id): 

216 cinderclient(context).volumes.begin_detaching(volume_id) 

217 

218 @translate_volume_exception 

219 def roll_detaching(self, context, volume_id): 

220 cinderclient(context).volumes.roll_detaching(volume_id) 

221 

222 @translate_volume_exception 

223 def attach(self, context, volume_id, instance_uuid, mountpoint): 

224 cinderclient(context).volumes.attach(volume_id, instance_uuid, 

225 mountpoint) 

226 

227 @translate_volume_exception 

228 def detach(self, context, volume_id): 

229 cinderclient(context).volumes.detach(volume_id) 

230 

231 @translate_volume_exception 

232 def initialize_connection(self, context, volume_id, connector): 

233 return cinderclient(context).volumes.initialize_connection(volume_id, 

234 connector) 

235 

236 @translate_volume_exception 

237 def terminate_connection(self, context, volume_id, connector): 

238 return cinderclient(context).volumes.terminate_connection(volume_id, 

239 connector) 

240 

241 def create(self, context, size, name, description, snapshot=None, 

242 image_id=None, volume_type=None, metadata=None, 

243 availability_zone=None, source_volid=None): 

244 

245 if snapshot is not None: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 snapshot_id = snapshot['id'] 

247 else: 

248 snapshot_id = None 

249 

250 kwargs = dict(snapshot_id=snapshot_id, 

251 name=name, 

252 description=description, 

253 volume_type=volume_type, 

254 user_id=context.user_id, 

255 project_id=context.project_id, 

256 availability_zone=availability_zone, 

257 metadata=metadata, 

258 imageRef=image_id, 

259 source_volid=source_volid 

260 ) 

261 

262 try: 

263 item = cinderclient(context).volumes.create(size, **kwargs) 

264 return _untranslate_volume_summary_view(context, item) 

265 except cinder_exception.BadRequest as e: 

266 raise exception.InvalidInput(reason=str(e)) 

267 except cinder_exception.NotFound: 

268 raise exception.NotFound( 

269 _("Error in creating cinder " 

270 "volume. Cinder volume type %s not exist. Check parameter " 

271 "cinder_volume_type in configuration file.") % volume_type) 

272 except Exception as e: 

273 raise exception.ManilaException(e) 

274 

275 @translate_volume_exception 

276 def extend(self, context, volume_id, new_size): 

277 cinderclient(context).volumes.extend(volume_id, new_size) 

278 

279 @translate_volume_exception 

280 def delete(self, context, volume_id): 

281 cinderclient(context).volumes.delete(volume_id) 

282 

283 @translate_volume_exception 

284 def update(self, context, volume_id, fields): 

285 # Use Manila's context as far as Cinder's is restricted to update 

286 # volumes. 

287 manila_admin_context = ctxt.get_admin_context() 

288 client = cinderclient(manila_admin_context) 

289 item = client.volumes.get(volume_id) 

290 client.volumes.update(item, **fields) 

291 

292 @translate_snapshot_exception 

293 def get_snapshot(self, context, snapshot_id): 

294 item = cinderclient(context).volume_snapshots.get(snapshot_id) 

295 return _untranslate_snapshot_summary_view(context, item) 

296 

297 def get_all_snapshots(self, context, search_opts=None): 

298 items = cinderclient(context).volume_snapshots.list( 

299 detailed=True, 

300 search_opts=search_opts) 

301 rvals = [] 

302 

303 for item in items: 

304 rvals.append(_untranslate_snapshot_summary_view(context, item)) 

305 

306 return rvals 

307 

308 @translate_volume_exception 

309 def create_snapshot(self, context, volume_id, name, description): 

310 item = cinderclient(context).volume_snapshots.create(volume_id, 

311 False, 

312 name, 

313 description) 

314 return _untranslate_snapshot_summary_view(context, item) 

315 

316 @translate_volume_exception 

317 def create_snapshot_force(self, context, volume_id, name, description): 

318 item = cinderclient(context).volume_snapshots.create(volume_id, 

319 True, 

320 name, 

321 description) 

322 

323 return _untranslate_snapshot_summary_view(context, item) 

324 

325 @translate_snapshot_exception 

326 def delete_snapshot(self, context, snapshot_id): 

327 cinderclient(context).volume_snapshots.delete(snapshot_id) 

328 

329 def wait_for_available_volume(self, volume, timeout, 

330 msg_error="Volume failed.", 

331 msg_timeout="Volume action timeout.", 

332 expected_size=None): 

333 

334 class VolumeNotReady(Exception): 

335 pass 

336 

337 @utils.retry( 

338 retry_param=VolumeNotReady, 

339 interval=1, 

340 retries=timeout, 

341 backoff_rate=1, 

342 ) 

343 def check_volume_status(): 

344 vol = self.get(ctxt.get_admin_context(), volume['id']) 

345 if vol['status'] == const.STATUS_AVAILABLE: 

346 if expected_size and vol['size'] != expected_size: 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true

347 LOG.debug("The volume %(vol_id)s is available but the " 

348 "volume size does not match the expected size. " 

349 "A volume resize operation may be pending. " 

350 "Expected size: %(expected_size)s, " 

351 "Actual size: %(volume_size)s.", 

352 dict(vol_id=vol['id'], 

353 expected_size=expected_size, 

354 volume_size=vol['size'])) 

355 raise VolumeNotReady() 

356 return vol 

357 elif 'error' in vol['status'].lower(): 

358 raise exception.ManilaException(msg_error) 

359 raise VolumeNotReady() 

360 

361 try: 

362 return check_volume_status() 

363 except VolumeNotReady: 

364 raise exception.ManilaException(msg_timeout)