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
« 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.
16"""
17Handles all requests relating to volumes + cinder.
18"""
20import copy
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
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
37LOG = log.getLogger(__name__)
39CINDER_GROUP = 'cinder'
40AUTH_OBJ = None
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 ]
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)
68def list_opts():
69 return client_auth.AuthClientLoader.list_opts(CINDER_GROUP)
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)
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
92 d['attach_time'] = ""
93 d['mountpoint'] = ""
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'
103 d['name'] = vol.name
104 d['description'] = vol.description
106 d['volume_type_id'] = vol.volume_type
107 d['snapshot_id'] = vol.snapshot_id
109 d['volume_metadata'] = {}
110 for key, value in vol.metadata.items():
111 d['volume_metadata'][key] = value
113 if hasattr(vol, 'volume_image_metadata'):
114 d['volume_image_metadata'] = copy.deepcopy(vol.volume_image_metadata)
116 return d
119def _untranslate_snapshot_summary_view(context, snapshot):
120 """Maps keys for snapshots summary view."""
121 d = {}
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
134 return d
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
151def translate_snapshot_exception(method):
152 """Transforms the exception for the snapshot.
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
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)
173 def get_all(self, context, search_opts={}):
174 items = cinderclient(context).volumes.list(detailed=True,
175 search_opts=search_opts)
176 rval = []
178 for item in items:
179 rval.append(_untranslate_volume_summary_view(context, item))
181 return rval
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)
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)
201 def check_detach(self, context, volume):
202 if volume['status'] == "available":
203 msg = _("already detached")
204 raise exception.InvalidVolume(msg)
206 @translate_volume_exception
207 def reserve_volume(self, context, volume_id):
208 cinderclient(context).volumes.reserve(volume_id)
210 @translate_volume_exception
211 def unreserve_volume(self, context, volume_id):
212 cinderclient(context).volumes.unreserve(volume_id)
214 @translate_volume_exception
215 def begin_detaching(self, context, volume_id):
216 cinderclient(context).volumes.begin_detaching(volume_id)
218 @translate_volume_exception
219 def roll_detaching(self, context, volume_id):
220 cinderclient(context).volumes.roll_detaching(volume_id)
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)
227 @translate_volume_exception
228 def detach(self, context, volume_id):
229 cinderclient(context).volumes.detach(volume_id)
231 @translate_volume_exception
232 def initialize_connection(self, context, volume_id, connector):
233 return cinderclient(context).volumes.initialize_connection(volume_id,
234 connector)
236 @translate_volume_exception
237 def terminate_connection(self, context, volume_id, connector):
238 return cinderclient(context).volumes.terminate_connection(volume_id,
239 connector)
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):
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
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 )
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)
275 @translate_volume_exception
276 def extend(self, context, volume_id, new_size):
277 cinderclient(context).volumes.extend(volume_id, new_size)
279 @translate_volume_exception
280 def delete(self, context, volume_id):
281 cinderclient(context).volumes.delete(volume_id)
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)
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)
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 = []
303 for item in items:
304 rvals.append(_untranslate_snapshot_summary_view(context, item))
306 return rvals
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)
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)
323 return _untranslate_snapshot_summary_view(context, item)
325 @translate_snapshot_exception
326 def delete_snapshot(self, context, snapshot_id):
327 cinderclient(context).volume_snapshots.delete(snapshot_id)
329 def wait_for_available_volume(self, volume, timeout,
330 msg_error="Volume failed.",
331 msg_timeout="Volume action timeout.",
332 expected_size=None):
334 class VolumeNotReady(Exception):
335 pass
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()
361 try:
362 return check_volume_status()
363 except VolumeNotReady:
364 raise exception.ManilaException(msg_timeout)