Coverage for drivers/linstorcowutil.py : 22%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Copyright (C) 2020 Vates SAS - ronan.abhamon@vates.fr
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <https://www.gnu.org/licenses/>.
17from sm_typing import override
19import base64
20import errno
21import json
22import socket
23import time
25from cowutil import CowImageInfo, CowUtil, getCowUtil
26import util
27import xs_errors
29from linstorjournaler import LinstorJournaler
30from linstorvolumemanager import LinstorVolumeManager
31from vditype import VdiType
33MANAGER_PLUGIN = 'linstor-manager'
36def call_remote_method(session, host_ref, method, args):
37 try:
38 response = session.xenapi.host.call_plugin(
39 host_ref, MANAGER_PLUGIN, method, args
40 )
41 except Exception as e:
42 util.SMlog('call-plugin ({} with {}) exception: {}'.format(
43 method, args, e
44 ))
45 raise util.SMException(str(e))
47 util.SMlog('call-plugin ({} with {}) returned: {}'.format(
48 method, args, response
49 ))
51 return response
54class LinstorCallException(util.SMException):
55 def __init__(self, cmd_err):
56 self.cmd_err = cmd_err
58 @override
59 def __str__(self) -> str:
60 return str(self.cmd_err)
63class ErofsLinstorCallException(LinstorCallException):
64 pass
67class NoPathLinstorCallException(LinstorCallException):
68 pass
71def linstorhostcall(local_method, remote_method):
72 def decorated(response_parser):
73 def wrapper(*args, **kwargs):
74 self = args[0]
75 vdi_uuid = args[1]
77 device_path = self._linstor.build_device_path(
78 self._linstor.get_volume_name(vdi_uuid)
79 )
81 # A. Try a call using directly the DRBD device to avoid
82 # remote request.
84 # Try to read locally if the device is not in use or if the device
85 # is up to date and not diskless.
86 (node_names, in_use_by) = \
87 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
89 local_e = None
90 try:
91 if not in_use_by or socket.gethostname() in node_names:
92 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
93 except ErofsLinstorCallException as e:
94 local_e = e.cmd_err
95 except Exception as e:
96 local_e = e
98 util.SMlog(
99 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format(
100 remote_method, local_e if local_e else 'local diskless + in use or not up to date'
101 )
102 )
104 if in_use_by:
105 node_names = {in_use_by}
107 # B. Execute the plugin on master or slave.
108 remote_args = {
109 'devicePath': device_path,
110 'groupName': self._linstor.group_name,
111 'vdiType': self._vdi_type
112 }
113 remote_args.update(**kwargs)
114 remote_args = {str(key): str(value) for key, value in remote_args.items()}
116 try:
117 def remote_call():
118 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names)
119 return call_remote_method(self._session, host_ref, remote_method, remote_args)
120 response = util.retry(remote_call, 5, 2)
121 except Exception as remote_e:
122 self._raise_openers_exception(device_path, local_e or remote_e)
124 return response_parser(self, vdi_uuid, response)
125 return wrapper
126 return decorated
129def linstormodifier():
130 def decorated(func):
131 def wrapper(*args, **kwargs):
132 self = args[0]
134 ret = func(*args, **kwargs)
135 self._linstor.invalidate_resource_cache()
136 return ret
137 return wrapper
138 return decorated
141class LinstorCowUtil(object):
142 def __init__(self, session, linstor, vdi_type: str):
143 self._session = session
144 self._linstor = linstor
145 self._cowutil = getCowUtil(vdi_type)
146 self._vdi_type = vdi_type
148 @property
149 def cowutil(self) -> CowUtil:
150 return self._cowutil
152 def create_chain_paths(self, vdi_uuid, readonly=False):
153 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit cowutil calls.
154 # Useful for the snapshot code algorithm.
156 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid)
157 path = leaf_vdi_path
158 while True:
159 if not util.pathexists(path):
160 raise xs_errors.XenError(
161 'VDIUnavailable', opterr='Could not find: {}'.format(path)
162 )
164 # Diskless path can be created on the fly, ensure we can open it.
165 def check_volume_usable():
166 while True:
167 try:
168 with open(path, 'r' if readonly else 'r+'):
169 pass
170 except IOError as e:
171 if e.errno == errno.ENODATA:
172 time.sleep(2)
173 continue
174 if e.errno == errno.EROFS:
175 util.SMlog('Volume not attachable because RO. Openers: {}'.format(
176 self._linstor.get_volume_openers(vdi_uuid)
177 ))
178 raise
179 break
180 util.retry(check_volume_usable, 15, 2)
182 vdi_uuid = self.get_info(vdi_uuid).parentUuid
183 if not vdi_uuid:
184 break
185 path = self._linstor.get_device_path(vdi_uuid)
186 readonly = True # Non-leaf is always readonly.
188 return leaf_vdi_path
190 # --------------------------------------------------------------------------
191 # Getters: read locally and try on another host in case of failure.
192 # --------------------------------------------------------------------------
194 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
195 kwargs = {
196 'ignoreMissingFooter': ignore_missing_footer,
197 'fast': fast
198 }
199 return self._check(vdi_uuid, **kwargs)
201 @linstorhostcall(CowUtil.check, 'check')
202 def _check(self, vdi_uuid, response):
203 return CowUtil.CheckResult(response)
205 def get_info(self, vdi_uuid, include_parent=True):
206 kwargs = {
207 'includeParent': include_parent,
208 'resolveParent': False
209 }
210 return self._get_info(vdi_uuid, self._extract_uuid, **kwargs)
212 @linstorhostcall(CowUtil.getInfo, 'getInfo')
213 def _get_info(self, vdi_uuid, response):
214 obj = json.loads(response)
216 image_info = CowImageInfo(vdi_uuid)
217 image_info.sizeVirt = obj['sizeVirt']
218 image_info.sizePhys = obj['sizePhys']
219 if 'parentPath' in obj:
220 image_info.parentPath = obj['parentPath']
221 image_info.parentUuid = obj['parentUuid']
222 image_info.hidden = obj['hidden']
223 image_info.path = obj['path']
225 return image_info
227 @linstorhostcall(CowUtil.hasParent, 'hasParent')
228 def has_parent(self, vdi_uuid, response):
229 return util.strtobool(response)
231 def get_parent(self, vdi_uuid):
232 return self._get_parent(vdi_uuid, self._extract_uuid)
234 @linstorhostcall(CowUtil.getParent, 'getParent')
235 def _get_parent(self, vdi_uuid, response):
236 return response
238 @linstorhostcall(CowUtil.getSizeVirt, 'getSizeVirt')
239 def get_size_virt(self, vdi_uuid, response):
240 return int(response)
242 @linstorhostcall(CowUtil.getMaxResizeSize, 'getMaxResizeSize')
243 def get_max_resize_size(self, vdi_uuid, response):
244 return int(response)
246 @linstorhostcall(CowUtil.getSizePhys, 'getSizePhys')
247 def get_size_phys(self, vdi_uuid, response):
248 return int(response)
250 @linstorhostcall(CowUtil.getAllocatedSize, 'getAllocatedSize')
251 def get_allocated_size(self, vdi_uuid, response):
252 return int(response)
254 @linstorhostcall(CowUtil.getDepth, 'getDepth')
255 def get_depth(self, vdi_uuid, response):
256 return int(response)
258 @linstorhostcall(CowUtil.getKeyHash, 'getKeyHash')
259 def get_key_hash(self, vdi_uuid, response):
260 return response or None
262 @linstorhostcall(CowUtil.getBlockBitmap, 'getBlockBitmap')
263 def get_block_bitmap(self, vdi_uuid, response):
264 return base64.b64decode(response)
266 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
267 def get_drbd_size(self, vdi_uuid, response):
268 return int(response)
270 def _get_drbd_size(self, cowutil_inst, path):
271 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
272 if ret == 0:
273 return int(stdout.strip())
274 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
276 # --------------------------------------------------------------------------
277 # Setters: only used locally.
278 # --------------------------------------------------------------------------
280 @linstormodifier()
281 def create(self, path, size, static, msize=0):
282 return self._call_local_method_or_fail(CowUtil.create, path, size, static, msize)
284 @linstormodifier()
285 def set_size_phys(self, path, size, debug=True):
286 return self._call_local_method_or_fail(CowUtil.setSizePhys, path, size, debug)
288 @linstormodifier()
289 def set_parent(self, path, parentPath, parentRaw=False):
290 return self._call_local_method_or_fail(CowUtil.setParent, path, parentPath, parentRaw)
292 @linstormodifier()
293 def set_hidden(self, path, hidden=True):
294 return self._call_local_method_or_fail(CowUtil.setHidden, path, hidden)
296 @linstormodifier()
297 def set_key(self, path, key_hash):
298 return self._call_local_method_or_fail(CowUtil.setKey, path, key_hash)
300 @linstormodifier()
301 def kill_data(self, path):
302 return self._call_local_method_or_fail(CowUtil.killData, path)
304 @linstormodifier()
305 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
306 return self._call_local_method_or_fail(CowUtil.snapshot, path, parent, parentRaw, msize, checkEmpty)
308 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
309 # Only inflate if the LINSTOR volume capacity is not enough.
310 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
311 if new_size <= old_size:
312 return
314 util.SMlog(
315 'Inflate {} (size={}, previous={})'
316 .format(vdi_path, new_size, old_size)
317 )
319 journaler.create(
320 LinstorJournaler.INFLATE, vdi_uuid, old_size
321 )
322 self._linstor.resize_volume(vdi_uuid, new_size)
324 result_size = self.get_drbd_size(vdi_uuid)
325 if result_size < new_size:
326 util.SMlog(
327 'WARNING: Cannot inflate volume to {}B, result size: {}B'
328 .format(new_size, result_size)
329 )
331 self._zeroize(vdi_path, result_size - self._cowutil.getFooterSize())
332 self.set_size_phys(vdi_path, result_size, False)
333 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
335 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
336 if zeroize:
337 assert old_size > self._cowutil.getFooterSize()
338 self._zeroize(vdi_path, old_size - self._cowutil.getFooterSize())
340 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
341 if new_size >= old_size:
342 return
344 util.SMlog(
345 'Deflate {} (new size={}, previous={})'
346 .format(vdi_path, new_size, old_size)
347 )
349 self.set_size_phys(vdi_path, new_size)
350 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
352 # --------------------------------------------------------------------------
353 # Remote setters: write locally and try on another host in case of failure.
354 # --------------------------------------------------------------------------
356 @linstormodifier()
357 def set_size_virt(self, path, size, jFile):
358 kwargs = {
359 'size': size,
360 'jFile': jFile
361 }
362 return self._call_method(CowUtil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs)
364 @linstormodifier()
365 def set_size_virt_fast(self, path, size):
366 kwargs = {
367 'size': size
368 }
369 return self._call_method(CowUtil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs)
371 @linstormodifier()
372 def force_parent(self, path, parentPath, parentRaw=False):
373 kwargs = {
374 'parentPath': str(parentPath),
375 'parentRaw': parentRaw
376 }
377 return self._call_method(CowUtil.setParent, 'setParent', path, use_parent=False, **kwargs)
379 @linstormodifier()
380 def force_coalesce(self, path):
381 return int(self._call_method(CowUtil.coalesce, 'coalesce', path, use_parent=True))
383 @linstormodifier()
384 def force_repair(self, path):
385 return self._call_method(CowUtil.repair, 'repair', path, use_parent=False)
387 @linstormodifier()
388 def force_deflate(self, path, newSize, oldSize, zeroize):
389 kwargs = {
390 'newSize': newSize,
391 'oldSize': oldSize,
392 'zeroize': zeroize
393 }
394 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
396 def _force_deflate(self, cowutil_inst, path, newSize, oldSize, zeroize):
397 self.deflate(path, newSize, oldSize, zeroize)
399 # --------------------------------------------------------------------------
400 # Helpers.
401 # --------------------------------------------------------------------------
403 def compute_volume_size(self, virtual_size: int) -> int:
404 if VdiType.isCowImage(self._vdi_type):
405 # All LINSTOR VDIs have the metadata area preallocated for
406 # the maximum possible virtual size (for fast online VDI.resize).
407 meta_overhead = self._cowutil.calcOverheadEmpty(
408 max(virtual_size, self._cowutil.getDefaultPreallocationSizeVirt())
409 )
410 bitmap_overhead = self._cowutil.calcOverheadBitmap(virtual_size)
411 virtual_size += meta_overhead + bitmap_overhead
412 else:
413 raise Exception('Invalid image type: {}'.format(self._vdi_type))
415 return LinstorVolumeManager.round_up_volume_size(virtual_size)
417 def _extract_uuid(self, device_path):
418 # TODO: Remove new line in the vhdutil module. Not here.
419 return self._linstor.get_volume_uuid_from_device_path(
420 device_path.rstrip('\n')
421 )
423 def _get_readonly_host(self, vdi_uuid, device_path, node_names):
424 """
425 When CowUtil is called to fetch VDI info we must find a
426 diskful DRBD disk to read the data. It's the goal of this function.
427 Why? Because when a COW image is open in RO mode, the LVM layer is used
428 directly to bypass DRBD verifications (we can have only one process
429 that reads/writes to disk with DRBD devices).
430 """
432 if not node_names:
433 raise xs_errors.XenError(
434 'VDIUnavailable',
435 opterr='Unable to find diskful node: {} (path={})'
436 .format(vdi_uuid, device_path)
437 )
439 hosts = self._session.xenapi.host.get_all_records()
440 for host_ref, host_record in hosts.items():
441 if host_record['hostname'] in node_names:
442 return host_ref
444 raise xs_errors.XenError(
445 'VDIUnavailable',
446 opterr='Unable to find a valid host from VDI: {} (path={})'
447 .format(vdi_uuid, device_path)
448 )
450 # --------------------------------------------------------------------------
452 def _raise_openers_exception(self, device_path, e):
453 if isinstance(e, util.CommandException):
454 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
455 else:
456 e_str = str(e)
458 try:
459 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
460 device_path
461 )
462 e_wrapper = Exception(
463 e_str + ' (openers: {})'.format(
464 self._linstor.get_volume_openers(volume_uuid)
465 )
466 )
467 except Exception as illformed_e:
468 e_wrapper = Exception(
469 e_str + ' (unable to get openers: {})'.format(illformed_e)
470 )
471 util.SMlog('raise opener exception: {}'.format(e_wrapper))
472 raise e_wrapper # pylint: disable = E0702
474 def _call_local_method(self, local_method, device_path, *args, **kwargs):
475 if isinstance(local_method, str):
476 local_method = getattr(self, local_method)
478 try:
479 def local_call():
480 try:
481 return local_method(self._cowutil, device_path, *args, **kwargs)
482 except util.CommandException as e:
483 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
484 raise ErofsLinstorCallException(e) # Break retry calls.
485 if e.code == errno.ENOENT:
486 raise NoPathLinstorCallException(e)
487 raise e
488 # Retry only locally if it's not an EROFS exception.
489 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
490 except util.CommandException as e:
491 util.SMlog('failed to execute locally CowUtil (sys {})'.format(e.code))
492 raise e
494 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
495 try:
496 return self._call_local_method(local_method, device_path, *args, **kwargs)
497 except ErofsLinstorCallException as e:
498 # Volume is locked on a host, find openers.
499 self._raise_openers_exception(device_path, e.cmd_err)
501 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
502 # Note: `use_parent` exists to know if the COW image parent is used by the local/remote method.
503 # Normally in case of failure, if the parent is unused we try to execute the method on
504 # another host using the DRBD opener list. In the other case, if the parent is required,
505 # we must check where this last one is open instead of the child.
507 if isinstance(local_method, str):
508 local_method = getattr(self, local_method)
510 # A. Try to write locally...
511 try:
512 return self._call_local_method(local_method, device_path, *args, **kwargs)
513 except Exception:
514 pass
516 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
518 # B. Execute the command on another host.
519 # B.1. Get host list.
520 try:
521 hosts = self._session.xenapi.host.get_all_records()
522 except Exception as e:
523 raise xs_errors.XenError(
524 'VDIUnavailable',
525 opterr='Unable to get host list to run CowUtil command `{}` (path={}): {}'
526 .format(remote_method, device_path, e)
527 )
529 # B.2. Prepare remote args.
530 remote_args = {
531 'devicePath': device_path,
532 'groupName': self._linstor.group_name
533 }
534 remote_args.update(**kwargs)
535 remote_args = {str(key): str(value) for key, value in remote_args.items()}
537 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
538 device_path
539 )
540 parent_volume_uuid = None
541 if use_parent:
542 parent_volume_uuid = self.get_parent(volume_uuid)
544 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
546 # B.3. Call!
547 def remote_call():
548 try:
549 all_openers = self._linstor.get_volume_openers(openers_uuid)
550 except Exception as e:
551 raise xs_errors.XenError(
552 'VDIUnavailable',
553 opterr='Unable to get DRBD openers to run CowUtil command `{}` (path={}): {}'
554 .format(remote_method, device_path, e)
555 )
557 no_host_found = True
558 for hostname, openers in all_openers.items():
559 if not openers:
560 continue
562 try:
563 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname)
564 except StopIteration:
565 continue
567 no_host_found = False
568 try:
569 return call_remote_method(self._session, host_ref, remote_method, remote_args)
570 except Exception:
571 pass
573 if no_host_found:
574 try:
575 return local_method(self._cowutil, device_path, *args, **kwargs)
576 except Exception as e:
577 self._raise_openers_exception(device_path, e)
579 raise xs_errors.XenError(
580 'VDIUnavailable',
581 opterr='No valid host found to run CowUtil command `{}` (path=`{}`, openers=`{}`)'
582 .format(remote_method, device_path, openers)
583 )
584 return util.retry(remote_call, 5, 2)
586 def _zeroize(self, path, size):
587 if not util.zeroOut(path, size, self._cowutil.getFooterSize()):
588 raise xs_errors.XenError(
589 'EIO',
590 opterr='Failed to zero out COW image footer {}'.format(path)
591 )