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 sm_typing import override
19from linstorjournaler import LinstorJournaler
20from linstorvolumemanager import LinstorVolumeManager
21import base64
22import errno
23import json
24import socket
25import time
26import util
27import vhdutil
28import xs_errors
30MANAGER_PLUGIN = 'linstor-manager'
33def call_remote_method(session, host_ref, method, device_path, args):
34 try:
35 response = session.xenapi.host.call_plugin(
36 host_ref, MANAGER_PLUGIN, method, args
37 )
38 except Exception as e:
39 util.SMlog('call-plugin ({} with {}) exception: {}'.format(
40 method, args, e
41 ))
42 raise util.SMException(str(e))
44 util.SMlog('call-plugin ({} with {}) returned: {}'.format(
45 method, args, response
46 ))
48 return response
51def check_ex(path, ignoreMissingFooter = False, fast = False):
52 cmd = [vhdutil.VHD_UTIL, "check", vhdutil.OPT_LOG_ERR, "-n", path]
53 if ignoreMissingFooter:
54 cmd.append("-i")
55 if fast:
56 cmd.append("-B")
58 vhdutil.ioretry(cmd)
61class LinstorCallException(util.SMException):
62 def __init__(self, cmd_err):
63 self.cmd_err = cmd_err
65 @override
66 def __str__(self) -> str:
67 return str(self.cmd_err)
70class ErofsLinstorCallException(LinstorCallException):
71 pass
74class NoPathLinstorCallException(LinstorCallException):
75 pass
78def linstorhostcall(local_method, remote_method):
79 def decorated(response_parser):
80 def wrapper(*args, **kwargs):
81 self = args[0]
82 vdi_uuid = args[1]
84 device_path = self._linstor.build_device_path(
85 self._linstor.get_volume_name(vdi_uuid)
86 )
88 # A. Try a call using directly the DRBD device to avoid
89 # remote request.
91 # Try to read locally if the device is not in use or if the device
92 # is up to date and not diskless.
93 (node_names, in_use_by) = \
94 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
96 local_e = None
97 try:
98 if not in_use_by or socket.gethostname() in node_names:
99 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
100 except ErofsLinstorCallException as e:
101 local_e = e.cmd_err
102 except Exception as e:
103 local_e = e
105 util.SMlog(
106 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format(
107 remote_method, local_e if local_e else 'local diskless + in use or not up to date'
108 )
109 )
111 if in_use_by:
112 node_names = {in_use_by}
114 # B. Execute the plugin on master or slave.
115 remote_args = {
116 'devicePath': device_path,
117 'groupName': self._linstor.group_name
118 }
119 remote_args.update(**kwargs)
120 remote_args = {str(key): str(value) for key, value in remote_args.items()}
122 try:
123 def remote_call():
124 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names)
125 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
126 response = util.retry(remote_call, 5, 2)
127 except Exception as remote_e:
128 self._raise_openers_exception(device_path, local_e or remote_e)
130 return response_parser(self, vdi_uuid, response)
131 return wrapper
132 return decorated
135def linstormodifier():
136 def decorated(func):
137 def wrapper(*args, **kwargs):
138 self = args[0]
140 ret = func(*args, **kwargs)
141 self._linstor.invalidate_resource_cache()
142 return ret
143 return wrapper
144 return decorated
147class LinstorVhdUtil:
148 MAX_SIZE = 2 * 1024 * 1024 * 1024 * 1024 # Max VHD size.
150 def __init__(self, session, linstor):
151 self._session = session
152 self._linstor = linstor
154 def create_chain_paths(self, vdi_uuid, readonly=False):
155 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit vhdutil calls.
156 # Useful for the snapshot code algorithm.
158 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid)
159 path = leaf_vdi_path
160 while True:
161 if not util.pathexists(path):
162 raise xs_errors.XenError(
163 'VDIUnavailable', opterr='Could not find: {}'.format(path)
164 )
166 # Diskless path can be created on the fly, ensure we can open it.
167 def check_volume_usable():
168 while True:
169 try:
170 with open(path, 'r' if readonly else 'r+'):
171 pass
172 except IOError as e:
173 if e.errno == errno.ENODATA:
174 time.sleep(2)
175 continue
176 if e.errno == errno.EROFS:
177 util.SMlog('Volume not attachable because RO. Openers: {}'.format(
178 self._linstor.get_volume_openers(vdi_uuid)
179 ))
180 raise
181 break
182 util.retry(check_volume_usable, 15, 2)
184 vdi_uuid = self.get_vhd_info(vdi_uuid).parentUuid
185 if not vdi_uuid:
186 break
187 path = self._linstor.get_device_path(vdi_uuid)
188 readonly = True # Non-leaf is always readonly.
190 return leaf_vdi_path
192 # --------------------------------------------------------------------------
193 # Getters: read locally and try on another host in case of failure.
194 # --------------------------------------------------------------------------
196 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
197 kwargs = {
198 'ignoreMissingFooter': ignore_missing_footer,
199 'fast': fast
200 }
201 try:
202 self._check(vdi_uuid, **kwargs) # pylint: disable = E1123
203 return True
204 except Exception as e:
205 util.SMlog('Call to `check` failed: {}'.format(e))
206 return False
208 @linstorhostcall(check_ex, 'check')
209 def _check(self, vdi_uuid, response):
210 return util.strtobool(response)
212 def get_vhd_info(self, vdi_uuid, include_parent=True):
213 kwargs = {
214 'includeParent': include_parent,
215 'resolveParent': False
216 }
217 # TODO: Replace pylint comment with this feature when possible:
218 # https://github.com/PyCQA/pylint/pull/2926
219 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs) # pylint: disable = E1123
221 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo')
222 def _get_vhd_info(self, vdi_uuid, response):
223 obj = json.loads(response)
225 vhd_info = vhdutil.VHDInfo(vdi_uuid)
226 vhd_info.sizeVirt = obj['sizeVirt']
227 vhd_info.sizePhys = obj['sizePhys']
228 if 'parentPath' in obj:
229 vhd_info.parentPath = obj['parentPath']
230 vhd_info.parentUuid = obj['parentUuid']
231 vhd_info.hidden = obj['hidden']
232 vhd_info.path = obj['path']
234 return vhd_info
236 @linstorhostcall(vhdutil.hasParent, 'hasParent')
237 def has_parent(self, vdi_uuid, response):
238 return util.strtobool(response)
240 def get_parent(self, vdi_uuid):
241 return self._get_parent(vdi_uuid, self._extract_uuid)
243 @linstorhostcall(vhdutil.getParent, 'getParent')
244 def _get_parent(self, vdi_uuid, response):
245 return response
247 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt')
248 def get_size_virt(self, vdi_uuid, response):
249 return int(response)
251 @linstorhostcall(vhdutil.getMaxResizeSize, 'getMaxResizeSize')
252 def get_max_resize_size(self, vdi_uuid, response):
253 return int(response)
255 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys')
256 def get_size_phys(self, vdi_uuid, response):
257 return int(response)
259 @linstorhostcall(vhdutil.getAllocatedSize, 'getAllocatedSize')
260 def get_allocated_size(self, vdi_uuid, response):
261 return int(response)
263 @linstorhostcall(vhdutil.getDepth, 'getDepth')
264 def get_depth(self, vdi_uuid, response):
265 return int(response)
267 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash')
268 def get_key_hash(self, vdi_uuid, response):
269 return response or None
271 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap')
272 def get_block_bitmap(self, vdi_uuid, response):
273 return base64.b64decode(response)
275 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
276 def get_drbd_size(self, vdi_uuid, response):
277 return int(response)
279 def _get_drbd_size(self, path):
280 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
281 if ret == 0:
282 return int(stdout.strip())
283 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
285 # --------------------------------------------------------------------------
286 # Setters: only used locally.
287 # --------------------------------------------------------------------------
289 @linstormodifier()
290 def create(self, path, size, static, msize=0):
291 return self._call_local_method_or_fail(vhdutil.create, path, size, static, msize)
293 @linstormodifier()
294 def set_size_phys(self, path, size, debug=True):
295 return self._call_local_method_or_fail(vhdutil.setSizePhys, path, size, debug)
297 @linstormodifier()
298 def set_parent(self, path, parentPath, parentRaw=False):
299 return self._call_local_method_or_fail(vhdutil.setParent, path, parentPath, parentRaw)
301 @linstormodifier()
302 def set_hidden(self, path, hidden=True):
303 return self._call_local_method_or_fail(vhdutil.setHidden, path, hidden)
305 @linstormodifier()
306 def set_key(self, path, key_hash):
307 return self._call_local_method_or_fail(vhdutil.setKey, path, key_hash)
309 @linstormodifier()
310 def kill_data(self, path):
311 return self._call_local_method_or_fail(vhdutil.killData, path)
313 @linstormodifier()
314 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
315 return self._call_local_method_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
317 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
318 # Only inflate if the LINSTOR volume capacity is not enough.
319 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
320 if new_size <= old_size:
321 return
323 util.SMlog(
324 'Inflate {} (size={}, previous={})'
325 .format(vdi_path, new_size, old_size)
326 )
328 journaler.create(
329 LinstorJournaler.INFLATE, vdi_uuid, old_size
330 )
331 self._linstor.resize_volume(vdi_uuid, new_size)
333 # TODO: Replace pylint comment with this feature when possible:
334 # https://github.com/PyCQA/pylint/pull/2926
335 result_size = self.get_drbd_size(vdi_uuid) # pylint: disable = E1120
336 if result_size < new_size:
337 util.SMlog(
338 'WARNING: Cannot inflate volume to {}B, result size: {}B'
339 .format(new_size, result_size)
340 )
342 self._zeroize(vdi_path, result_size - vhdutil.VHD_FOOTER_SIZE)
343 self.set_size_phys(vdi_path, result_size, False)
344 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
346 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
347 if zeroize:
348 assert old_size > vhdutil.VHD_FOOTER_SIZE
349 self._zeroize(vdi_path, old_size - vhdutil.VHD_FOOTER_SIZE)
351 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
352 if new_size >= old_size:
353 return
355 util.SMlog(
356 'Deflate {} (new size={}, previous={})'
357 .format(vdi_path, new_size, old_size)
358 )
360 self.set_size_phys(vdi_path, new_size)
361 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
363 # --------------------------------------------------------------------------
364 # Remote setters: write locally and try on another host in case of failure.
365 # --------------------------------------------------------------------------
367 @linstormodifier()
368 def set_size_virt(self, path, size, jfile):
369 kwargs = {
370 'size': size,
371 'jfile': jfile
372 }
373 return self._call_method(vhdutil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs)
375 @linstormodifier()
376 def set_size_virt_fast(self, path, size):
377 kwargs = {
378 'size': size
379 }
380 return self._call_method(vhdutil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs)
382 @linstormodifier()
383 def force_parent(self, path, parentPath, parentRaw=False):
384 kwargs = {
385 'parentPath': str(parentPath),
386 'parentRaw': parentRaw
387 }
388 return self._call_method(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs)
390 @linstormodifier()
391 def force_coalesce(self, path):
392 return int(self._call_method(vhdutil.coalesce, 'coalesce', path, use_parent=True))
394 @linstormodifier()
395 def force_repair(self, path):
396 return self._call_method(vhdutil.repair, 'repair', path, use_parent=False)
398 @linstormodifier()
399 def force_deflate(self, path, newSize, oldSize, zeroize):
400 kwargs = {
401 'newSize': newSize,
402 'oldSize': oldSize,
403 'zeroize': zeroize
404 }
405 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
407 def _force_deflate(self, path, newSize, oldSize, zeroize):
408 self.deflate(path, newSize, oldSize, zeroize)
410 # --------------------------------------------------------------------------
411 # Static helpers.
412 # --------------------------------------------------------------------------
414 @classmethod
415 def compute_volume_size(cls, virtual_size, image_type):
416 if image_type == vhdutil.VDI_TYPE_VHD:
417 # All LINSTOR VDIs have the metadata area preallocated for
418 # the maximum possible virtual size (for fast online VDI.resize).
419 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE)
420 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size)
421 virtual_size += meta_overhead + bitmap_overhead
422 elif image_type != vhdutil.VDI_TYPE_RAW:
423 raise Exception('Invalid image type: {}'.format(image_type))
425 return LinstorVolumeManager.round_up_volume_size(virtual_size)
427 # --------------------------------------------------------------------------
428 # Helpers.
429 # --------------------------------------------------------------------------
431 def _extract_uuid(self, device_path):
432 # TODO: Remove new line in the vhdutil module. Not here.
433 return self._linstor.get_volume_uuid_from_device_path(
434 device_path.rstrip('\n')
435 )
437 def _get_readonly_host(self, vdi_uuid, device_path, node_names):
438 """
439 When vhd-util is called to fetch VDI info we must find a
440 diskful DRBD disk to read the data. It's the goal of this function.
441 Why? Because when a VHD is open in RO mode, the LVM layer is used
442 directly to bypass DRBD verifications (we can have only one process
443 that reads/writes to disk with DRBD devices).
444 """
446 if not node_names:
447 raise xs_errors.XenError(
448 'VDIUnavailable',
449 opterr='Unable to find diskful node: {} (path={})'
450 .format(vdi_uuid, device_path)
451 )
453 hosts = self._session.xenapi.host.get_all_records()
454 for host_ref, host_record in hosts.items():
455 if host_record['hostname'] in node_names:
456 return host_ref
458 raise xs_errors.XenError(
459 'VDIUnavailable',
460 opterr='Unable to find a valid host from VDI: {} (path={})'
461 .format(vdi_uuid, device_path)
462 )
464 # --------------------------------------------------------------------------
466 def _raise_openers_exception(self, device_path, e):
467 if isinstance(e, util.CommandException):
468 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
469 else:
470 e_str = str(e)
472 try:
473 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
474 device_path
475 )
476 e_wrapper = Exception(
477 e_str + ' (openers: {})'.format(
478 self._linstor.get_volume_openers(volume_uuid)
479 )
480 )
481 except Exception as illformed_e:
482 e_wrapper = Exception(
483 e_str + ' (unable to get openers: {})'.format(illformed_e)
484 )
485 util.SMlog('raise opener exception: {}'.format(e_wrapper))
486 raise e_wrapper # pylint: disable = E0702
488 def _call_local_method(self, local_method, device_path, *args, **kwargs):
489 if isinstance(local_method, str):
490 local_method = getattr(self, local_method)
492 try:
493 def local_call():
494 try:
495 return local_method(device_path, *args, **kwargs)
496 except util.CommandException as e:
497 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
498 raise ErofsLinstorCallException(e) # Break retry calls.
499 if e.code == errno.ENOENT:
500 raise NoPathLinstorCallException(e)
501 raise e
502 # Retry only locally if it's not an EROFS exception.
503 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
504 except util.CommandException as e:
505 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code))
506 raise e
508 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
509 try:
510 return self._call_local_method(local_method, device_path, *args, **kwargs)
511 except ErofsLinstorCallException as e:
512 # Volume is locked on a host, find openers.
513 self._raise_openers_exception(device_path, e.cmd_err)
515 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
516 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method.
517 # Normally in case of failure, if the parent is unused we try to execute the method on
518 # another host using the DRBD opener list. In the other case, if the parent is required,
519 # we must check where this last one is open instead of the child.
521 if isinstance(local_method, str):
522 local_method = getattr(self, local_method)
524 # A. Try to write locally...
525 try:
526 return self._call_local_method(local_method, device_path, *args, **kwargs)
527 except Exception:
528 pass
530 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
532 # B. Execute the command on another host.
533 # B.1. Get host list.
534 try:
535 hosts = self._session.xenapi.host.get_all_records()
536 except Exception as e:
537 raise xs_errors.XenError(
538 'VDIUnavailable',
539 opterr='Unable to get host list to run vhd-util command `{}` (path={}): {}'
540 .format(remote_method, device_path, e)
541 )
543 # B.2. Prepare remote args.
544 remote_args = {
545 'devicePath': device_path,
546 'groupName': self._linstor.group_name
547 }
548 remote_args.update(**kwargs)
549 remote_args = {str(key): str(value) for key, value in remote_args.items()}
551 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
552 device_path
553 )
554 parent_volume_uuid = None
555 if use_parent:
556 parent_volume_uuid = self.get_parent(volume_uuid)
558 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
560 # B.3. Call!
561 def remote_call():
562 try:
563 all_openers = self._linstor.get_volume_openers(openers_uuid)
564 except Exception as e:
565 raise xs_errors.XenError(
566 'VDIUnavailable',
567 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}'
568 .format(remote_method, device_path, e)
569 )
571 no_host_found = True
572 for hostname, openers in all_openers.items():
573 if not openers:
574 continue
576 try:
577 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname)
578 except StopIteration:
579 continue
581 no_host_found = False
582 try:
583 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
584 except Exception:
585 pass
587 if no_host_found:
588 try:
589 return local_method(device_path, *args, **kwargs)
590 except Exception as e:
591 self._raise_openers_exception(device_path, e)
593 raise xs_errors.XenError(
594 'VDIUnavailable',
595 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`)'
596 .format(remote_method, device_path, openers)
597 )
598 return util.retry(remote_call, 5, 2)
600 @staticmethod
601 def _zeroize(path, size):
602 if not util.zeroOut(path, size, vhdutil.VHD_FOOTER_SIZE):
603 raise xs_errors.XenError(
604 'EIO',
605 opterr='Failed to zero out VHD footer {}'.format(path)
606 )