Coverage for drivers/linstorvhdutil.py : 21%

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 linstorjournaler import LinstorJournaler
18from linstorvolumemanager import LinstorVolumeManager
19import base64
20import distutils.util
21import errno
22import json
23import socket
24import time
25import util
26import vhdutil
27import xs_errors
29MANAGER_PLUGIN = 'linstor-manager'
32def call_remote_method(session, host_ref, method, device_path, args):
33 try:
34 response = session.xenapi.host.call_plugin(
35 host_ref, MANAGER_PLUGIN, method, args
36 )
37 except Exception as e:
38 util.SMlog('call-plugin ({} with {}) exception: {}'.format(
39 method, args, e
40 ))
41 raise util.SMException(str(e))
43 util.SMlog('call-plugin ({} with {}) returned: {}'.format(
44 method, args, response
45 ))
47 return response
50def check_ex(path, ignoreMissingFooter = False, fast = False):
51 cmd = [vhdutil.VHD_UTIL, "check", vhdutil.OPT_LOG_ERR, "-n", path]
52 if ignoreMissingFooter:
53 cmd.append("-i")
54 if fast:
55 cmd.append("-B")
57 vhdutil.ioretry(cmd)
60class LinstorCallException(util.SMException):
61 def __init__(self, cmd_err):
62 self.cmd_err = cmd_err
64 def __str__(self):
65 return str(self.cmd_err)
68class ErofsLinstorCallException(LinstorCallException):
69 pass
72class NoPathLinstorCallException(LinstorCallException):
73 pass
76def linstorhostcall(local_method, remote_method):
77 def decorated(response_parser):
78 def wrapper(*args, **kwargs):
79 self = args[0]
80 vdi_uuid = args[1]
82 device_path = self._linstor.build_device_path(
83 self._linstor.get_volume_name(vdi_uuid)
84 )
86 # A. Try a call using directly the DRBD device to avoid
87 # remote request.
89 # Try to read locally if the device is not in use or if the device
90 # is up to date and not diskless.
91 (node_names, in_use_by) = \
92 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
94 local_e = None
95 try:
96 if not in_use_by or socket.gethostname() in node_names:
97 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
98 except ErofsLinstorCallException as e:
99 local_e = e.cmd_err
100 except Exception as e:
101 local_e = e
103 util.SMlog(
104 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format(
105 remote_method, local_e if local_e else 'local diskless + in use or not up to date'
106 )
107 )
109 if in_use_by:
110 node_names = {in_use_by}
112 # B. Execute the plugin on master or slave.
113 remote_args = {
114 'devicePath': device_path,
115 'groupName': self._linstor.group_name
116 }
117 remote_args.update(**kwargs)
118 remote_args = {str(key): str(value) for key, value in remote_args.items()}
120 try:
121 def remote_call():
122 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names)
123 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
124 response = util.retry(remote_call, 5, 2)
125 except Exception as remote_e:
126 self._raise_openers_exception(device_path, local_e or remote_e)
128 return response_parser(self, vdi_uuid, response)
129 return wrapper
130 return decorated
133def linstormodifier():
134 def decorated(func):
135 def wrapper(*args, **kwargs):
136 self = args[0]
138 ret = func(*args, **kwargs)
139 self._linstor.invalidate_resource_cache()
140 return ret
141 return wrapper
142 return decorated
145class LinstorVhdUtil:
146 MAX_SIZE = 2 * 1024 * 1024 * 1024 * 1024 # Max VHD size.
148 def __init__(self, session, linstor):
149 self._session = session
150 self._linstor = linstor
152 def create_chain_paths(self, vdi_uuid, readonly=False):
153 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit vhdutil 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_vhd_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 try:
200 self._check(vdi_uuid, **kwargs) # pylint: disable = E1123
201 return True
202 except Exception as e:
203 util.SMlog('Call to `check` failed: {}'.format(e))
204 return False
206 @linstorhostcall(check_ex, 'check')
207 def _check(self, vdi_uuid, response):
208 return distutils.util.strtobool(response)
210 def get_vhd_info(self, vdi_uuid, include_parent=True):
211 kwargs = {
212 'includeParent': include_parent,
213 'resolveParent': False
214 }
215 # TODO: Replace pylint comment with this feature when possible:
216 # https://github.com/PyCQA/pylint/pull/2926
217 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs) # pylint: disable = E1123
219 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo')
220 def _get_vhd_info(self, vdi_uuid, response):
221 obj = json.loads(response)
223 vhd_info = vhdutil.VHDInfo(vdi_uuid)
224 vhd_info.sizeVirt = obj['sizeVirt']
225 vhd_info.sizePhys = obj['sizePhys']
226 if 'parentPath' in obj:
227 vhd_info.parentPath = obj['parentPath']
228 vhd_info.parentUuid = obj['parentUuid']
229 vhd_info.hidden = obj['hidden']
230 vhd_info.path = obj['path']
232 return vhd_info
234 @linstorhostcall(vhdutil.hasParent, 'hasParent')
235 def has_parent(self, vdi_uuid, response):
236 return distutils.util.strtobool(response)
238 def get_parent(self, vdi_uuid):
239 return self._get_parent(vdi_uuid, self._extract_uuid)
241 @linstorhostcall(vhdutil.getParent, 'getParent')
242 def _get_parent(self, vdi_uuid, response):
243 return response
245 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt')
246 def get_size_virt(self, vdi_uuid, response):
247 return int(response)
249 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys')
250 def get_size_phys(self, vdi_uuid, response):
251 return int(response)
253 @linstorhostcall(vhdutil.getAllocatedSize, 'getAllocatedSize')
254 def get_allocated_size(self, vdi_uuid, response):
255 return int(response)
257 @linstorhostcall(vhdutil.getDepth, 'getDepth')
258 def get_depth(self, vdi_uuid, response):
259 return int(response)
261 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash')
262 def get_key_hash(self, vdi_uuid, response):
263 return response or None
265 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap')
266 def get_block_bitmap(self, vdi_uuid, response):
267 return base64.b64decode(response)
269 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
270 def get_drbd_size(self, vdi_uuid, response):
271 return int(response)
273 def _get_drbd_size(self, path):
274 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
275 if ret == 0:
276 return int(stdout.strip())
277 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
279 # --------------------------------------------------------------------------
280 # Setters: only used locally.
281 # --------------------------------------------------------------------------
283 @linstormodifier()
284 def create(self, path, size, static, msize=0):
285 return self._call_local_method_or_fail(vhdutil.create, path, size, static, msize)
287 @linstormodifier()
288 def set_size_virt(self, path, size, jfile):
289 return self._call_local_method_or_fail(vhdutil.setSizeVirt, path, size, jfile)
291 @linstormodifier()
292 def set_size_virt_fast(self, path, size):
293 return self._call_local_method_or_fail(vhdutil.setSizeVirtFast, path, size)
295 @linstormodifier()
296 def set_size_phys(self, path, size, debug=True):
297 return self._call_local_method_or_fail(vhdutil.setSizePhys, path, size, debug)
299 @linstormodifier()
300 def set_parent(self, path, parentPath, parentRaw=False):
301 return self._call_local_method_or_fail(vhdutil.setParent, path, parentPath, parentRaw)
303 @linstormodifier()
304 def set_hidden(self, path, hidden=True):
305 return self._call_local_method_or_fail(vhdutil.setHidden, path, hidden)
307 @linstormodifier()
308 def set_key(self, path, key_hash):
309 return self._call_local_method_or_fail(vhdutil.setKey, path, key_hash)
311 @linstormodifier()
312 def kill_data(self, path):
313 return self._call_local_method_or_fail(vhdutil.killData, path)
315 @linstormodifier()
316 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
317 return self._call_local_method_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
319 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
320 # Only inflate if the LINSTOR volume capacity is not enough.
321 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
322 if new_size <= old_size:
323 return
325 util.SMlog(
326 'Inflate {} (size={}, previous={})'
327 .format(vdi_path, new_size, old_size)
328 )
330 journaler.create(
331 LinstorJournaler.INFLATE, vdi_uuid, old_size
332 )
333 self._linstor.resize_volume(vdi_uuid, new_size)
335 # TODO: Replace pylint comment with this feature when possible:
336 # https://github.com/PyCQA/pylint/pull/2926
337 result_size = self.get_drbd_size(vdi_uuid) # pylint: disable = E1120
338 if result_size < new_size:
339 util.SMlog(
340 'WARNING: Cannot inflate volume to {}B, result size: {}B'
341 .format(new_size, result_size)
342 )
344 self._zeroize(vdi_path, result_size - vhdutil.VHD_FOOTER_SIZE)
345 self.set_size_phys(vdi_path, result_size, False)
346 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
348 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
349 if zeroize:
350 assert old_size > vhdutil.VHD_FOOTER_SIZE
351 self._zeroize(vdi_path, old_size - vhdutil.VHD_FOOTER_SIZE)
353 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
354 if new_size >= old_size:
355 return
357 util.SMlog(
358 'Deflate {} (new size={}, previous={})'
359 .format(vdi_path, new_size, old_size)
360 )
362 self.set_size_phys(vdi_path, new_size)
363 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
365 # --------------------------------------------------------------------------
366 # Remote setters: write locally and try on another host in case of failure.
367 # --------------------------------------------------------------------------
369 @linstormodifier()
370 def force_parent(self, path, parentPath, parentRaw=False):
371 kwargs = {
372 'parentPath': str(parentPath),
373 'parentRaw': parentRaw
374 }
375 return self._call_method(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs)
377 @linstormodifier()
378 def force_coalesce(self, path):
379 return self._call_method(vhdutil.coalesce, 'coalesce', path, use_parent=True)
381 @linstormodifier()
382 def force_repair(self, path):
383 return self._call_method(vhdutil.repair, 'repair', path, use_parent=False)
385 @linstormodifier()
386 def force_deflate(self, path, newSize, oldSize, zeroize):
387 kwargs = {
388 'newSize': newSize,
389 'oldSize': oldSize,
390 'zeroize': zeroize
391 }
392 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
394 def _force_deflate(self, path, newSize, oldSize, zeroize):
395 self.deflate(path, newSize, oldSize, zeroize)
397 # --------------------------------------------------------------------------
398 # Static helpers.
399 # --------------------------------------------------------------------------
401 @classmethod
402 def compute_volume_size(cls, virtual_size, image_type):
403 if image_type == vhdutil.VDI_TYPE_VHD:
404 # All LINSTOR VDIs have the metadata area preallocated for
405 # the maximum possible virtual size (for fast online VDI.resize).
406 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE)
407 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size)
408 virtual_size += meta_overhead + bitmap_overhead
409 elif image_type != vhdutil.VDI_TYPE_RAW:
410 raise Exception('Invalid image type: {}'.format(image_type))
412 return LinstorVolumeManager.round_up_volume_size(virtual_size)
414 # --------------------------------------------------------------------------
415 # Helpers.
416 # --------------------------------------------------------------------------
418 def _extract_uuid(self, device_path):
419 # TODO: Remove new line in the vhdutil module. Not here.
420 return self._linstor.get_volume_uuid_from_device_path(
421 device_path.rstrip('\n')
422 )
424 def _get_readonly_host(self, vdi_uuid, device_path, node_names):
425 """
426 When vhd-util is called to fetch VDI info we must find a
427 diskful DRBD disk to read the data. It's the goal of this function.
428 Why? Because when a VHD is open in RO mode, the LVM layer is used
429 directly to bypass DRBD verifications (we can have only one process
430 that reads/writes to disk with DRBD devices).
431 """
433 if not node_names:
434 raise xs_errors.XenError(
435 'VDIUnavailable',
436 opterr='Unable to find diskful node: {} (path={})'
437 .format(vdi_uuid, device_path)
438 )
440 hosts = self._session.xenapi.host.get_all_records()
441 for host_ref, host_record in hosts.items():
442 if host_record['hostname'] in node_names:
443 return host_ref
445 raise xs_errors.XenError(
446 'VDIUnavailable',
447 opterr='Unable to find a valid host from VDI: {} (path={})'
448 .format(vdi_uuid, device_path)
449 )
451 # --------------------------------------------------------------------------
453 def _raise_openers_exception(self, device_path, e):
454 if isinstance(e, util.CommandException):
455 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
456 else:
457 e_str = str(e)
459 try:
460 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
461 device_path
462 )
463 e_wrapper = Exception(
464 e_str + ' (openers: {})'.format(
465 self._linstor.get_volume_openers(volume_uuid)
466 )
467 )
468 except Exception as illformed_e:
469 e_wrapper = Exception(
470 e_str + ' (unable to get openers: {})'.format(illformed_e)
471 )
472 util.SMlog('raise opener exception: {}'.format(e_wrapper))
473 raise e_wrapper # pylint: disable = E0702
475 def _call_local_method(self, local_method, device_path, *args, **kwargs):
476 if isinstance(local_method, str):
477 local_method = getattr(self, local_method)
479 try:
480 def local_call():
481 try:
482 return local_method(device_path, *args, **kwargs)
483 except util.CommandException as e:
484 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
485 raise ErofsLinstorCallException(e) # Break retry calls.
486 if e.code == errno.ENOENT:
487 raise NoPathLinstorCallException(e)
488 raise e
489 # Retry only locally if it's not an EROFS exception.
490 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
491 except util.CommandException as e:
492 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code))
493 raise e
495 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
496 try:
497 return self._call_local_method(local_method, device_path, *args, **kwargs)
498 except ErofsLinstorCallException as e:
499 # Volume is locked on a host, find openers.
500 self._raise_openers_exception(device_path, e.cmd_err)
502 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
503 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method.
504 # Normally in case of failure, if the parent is unused we try to execute the method on
505 # another host using the DRBD opener list. In the other case, if the parent is required,
506 # we must check where this last one is open instead of the child.
508 if isinstance(local_method, str):
509 local_method = getattr(self, local_method)
511 # A. Try to write locally...
512 try:
513 return self._call_local_method(local_method, device_path, *args, **kwargs)
514 except Exception:
515 pass
517 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
519 # B. Execute the command on another host.
520 # B.1. Get host list.
521 try:
522 hosts = self._session.xenapi.host.get_all_records()
523 except Exception as e:
524 raise xs_errors.XenError(
525 'VDIUnavailable',
526 opterr='Unable to get host list to run vhd-util command `{}` (path={}): {}'
527 .format(remote_method, device_path, e)
528 )
530 # B.2. Prepare remote args.
531 remote_args = {
532 'devicePath': device_path,
533 'groupName': self._linstor.group_name
534 }
535 remote_args.update(**kwargs)
536 remote_args = {str(key): str(value) for key, value in remote_args.items()}
538 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
539 device_path
540 )
541 parent_volume_uuid = None
542 if use_parent:
543 parent_volume_uuid = self.get_parent(volume_uuid)
545 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
547 # B.3. Call!
548 def remote_call():
549 try:
550 all_openers = self._linstor.get_volume_openers(openers_uuid)
551 except Exception as e:
552 raise xs_errors.XenError(
553 'VDIUnavailable',
554 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}'
555 .format(remote_method, device_path, e)
556 )
558 no_host_found = True
559 for hostname, openers in all_openers.items():
560 if not openers:
561 continue
563 try:
564 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname)
565 except StopIteration:
566 continue
568 no_host_found = False
569 try:
570 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
571 except Exception:
572 pass
574 if no_host_found:
575 try:
576 return local_method(device_path, *args, **kwargs)
577 except Exception as e:
578 self._raise_openers_exception(device_path, e)
580 raise xs_errors.XenError(
581 'VDIUnavailable',
582 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`)'
583 .format(remote_method, device_path, openers)
584 )
585 return util.retry(remote_call, 5, 2)
587 @staticmethod
588 def _zeroize(path, size):
589 if not util.zeroOut(path, size, vhdutil.VHD_FOOTER_SIZE):
590 raise xs_errors.XenError(
591 'EIO',
592 opterr='Failed to zero out VHD footer {}'.format(path)
593 )