Coverage for drivers/linstorvhdutil.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
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, 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 on {} ({} with {}) exception: {}'.format(
40 host_ref, method, args, e
41 ))
42 raise util.SMException(str(e))
44 util.SMlog('call-plugin on {} ({} with {}) returned: {}'.format(
45 host_ref, 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
77def log_successful_call(target_host, device_path, vdi_uuid, remote_method, response):
78 util.SMlog('Successful access on {} for device {} ({}): `{}` => {}'.format(
79 target_host, device_path, vdi_uuid, remote_method, str(response)
80 ), priority=util.LOG_DEBUG)
82def log_failed_call(target_host, next_target, device_path, vdi_uuid, remote_method, e):
83 util.SMlog('Failed to call method on {} for device {} ({}): {}. Trying accessing on {}... (cause: {})'.format(
84 target_host, device_path, vdi_uuid, remote_method, next_target, e
85 ), priority=util.LOG_DEBUG)
87def linstorhostcall(local_method, remote_method):
88 def decorated(response_parser):
89 def wrapper(*args, **kwargs):
90 self = args[0]
91 vdi_uuid = args[1]
93 device_path = self._linstor.build_device_path(
94 self._linstor.get_volume_name(vdi_uuid)
95 )
97 if not self._session:
98 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
100 remote_args = {
101 'devicePath': device_path,
102 'groupName': self._linstor.group_name
103 }
104 remote_args.update(**kwargs)
105 remote_args = {str(key): str(value) for key, value in remote_args.items()}
107 this_host_ref = util.get_this_host_ref(self._session)
108 def call_method(host_label, host_ref):
109 if host_ref == this_host_ref:
110 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
111 response = call_remote_method(self._session, host_ref, remote_method, remote_args)
112 log_successful_call(host_label, device_path, vdi_uuid, remote_method, response)
113 return response_parser(self, vdi_uuid, response)
115 # 1. Try on attached host.
116 try:
117 host_ref_attached = next(iter(util.get_hosts_attached_on(self._session, [vdi_uuid])), None)
118 if host_ref_attached:
119 return call_method('attached host', host_ref_attached)
120 except Exception as e:
121 log_failed_call('attached host', 'master', device_path, vdi_uuid, remote_method, e)
123 # 2. Try on master host.
124 try:
125 return call_method('master', util.get_master_ref(self._session))
126 except Exception as e:
127 log_failed_call('master', 'primary', device_path, vdi_uuid, remote_method, e)
129 # 3. Try on a primary.
130 hosts = self._get_hosts(remote_method, device_path)
132 nodes, primary_hostname = self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
133 if primary_hostname:
134 try:
135 return call_method('primary', self._find_host_ref_from_hostname(hosts, primary_hostname))
136 except Exception as remote_e:
137 self._raise_openers_exception(device_path, remote_e)
139 log_failed_call('primary', 'another node', device_path, vdi_uuid, remote_method, 'no primary')
141 # 4. Try on any host with local data.
142 try:
143 return call_method('another node', next(filter(None,
144 (self._find_host_ref_from_hostname(hosts, hostname) for hostname in nodes)
145 ), None))
146 except Exception as remote_e:
147 self._raise_openers_exception(device_path, remote_e)
149 return wrapper
150 return decorated
153def linstormodifier():
154 def decorated(func):
155 def wrapper(*args, **kwargs):
156 self = args[0]
158 ret = func(*args, **kwargs)
159 self._linstor.invalidate_resource_cache()
160 return ret
161 return wrapper
162 return decorated
165class LinstorVhdUtil:
166 MAX_SIZE = 2 * 1024 * 1024 * 1024 * 1024 # Max VHD size.
168 def __init__(self, session, linstor):
169 self._session = session
170 self._linstor = linstor
172 def create_chain_paths(self, vdi_uuid, readonly=False):
173 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit vhdutil calls.
174 # Useful for the snapshot code algorithm.
176 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid)
177 path = leaf_vdi_path
178 while True:
179 if not util.pathexists(path):
180 raise xs_errors.XenError(
181 'VDIUnavailable', opterr='Could not find: {}'.format(path)
182 )
184 # Diskless path can be created on the fly, ensure we can open it.
185 def check_volume_usable():
186 while True:
187 try:
188 with open(path, 'r' if readonly else 'r+'):
189 pass
190 except IOError as e:
191 if e.errno == errno.ENODATA:
192 time.sleep(2)
193 continue
194 if e.errno == errno.EROFS or e.errno == errno.EMEDIUMTYPE:
195 util.SMlog('Volume not attachable because used. Openers: {}'.format(
196 self._linstor.get_volume_openers(vdi_uuid)
197 ))
198 raise
199 break
200 util.retry(check_volume_usable, 15, 2)
202 vdi_uuid = self.get_vhd_info(vdi_uuid).parentUuid
203 if not vdi_uuid:
204 break
205 path = self._linstor.get_device_path(vdi_uuid)
206 readonly = True # Non-leaf is always readonly.
208 return leaf_vdi_path
210 # --------------------------------------------------------------------------
211 # Getters: read locally and try on another host in case of failure.
212 # --------------------------------------------------------------------------
214 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
215 kwargs = {
216 'ignoreMissingFooter': ignore_missing_footer,
217 'fast': fast
218 }
219 try:
220 self._check(vdi_uuid, **kwargs)
221 return True
222 except Exception as e:
223 util.SMlog('Call to `check` failed: {}'.format(e))
224 return False
226 @linstorhostcall(check_ex, 'check')
227 def _check(self, vdi_uuid, response):
228 return util.strtobool(response)
230 def get_vhd_info(self, vdi_uuid, include_parent=True):
231 kwargs = {
232 'includeParent': include_parent,
233 'resolveParent': False
234 }
235 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs)
237 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo')
238 def _get_vhd_info(self, vdi_uuid, response):
239 obj = json.loads(response)
241 vhd_info = vhdutil.VHDInfo(vdi_uuid)
242 vhd_info.sizeVirt = obj['sizeVirt']
243 vhd_info.sizePhys = obj['sizePhys']
244 if 'parentPath' in obj:
245 vhd_info.parentPath = obj['parentPath']
246 vhd_info.parentUuid = obj['parentUuid']
247 vhd_info.hidden = obj['hidden']
248 vhd_info.path = obj['path']
250 return vhd_info
252 @linstorhostcall(vhdutil.hasParent, 'hasParent')
253 def has_parent(self, vdi_uuid, response):
254 return util.strtobool(response)
256 def get_parent(self, vdi_uuid):
257 return self._get_parent(vdi_uuid, self._extract_uuid)
259 @linstorhostcall(vhdutil.getParent, 'getParent')
260 def _get_parent(self, vdi_uuid, response):
261 return response
263 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt')
264 def get_size_virt(self, vdi_uuid, response):
265 return int(response)
267 @linstorhostcall(vhdutil.getMaxResizeSize, 'getMaxResizeSize')
268 def get_max_resize_size(self, vdi_uuid, response):
269 return int(response)
271 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys')
272 def get_size_phys(self, vdi_uuid, response):
273 return int(response)
275 @linstorhostcall(vhdutil.getAllocatedSize, 'getAllocatedSize')
276 def get_allocated_size(self, vdi_uuid, response):
277 return int(response)
279 @linstorhostcall(vhdutil.getDepth, 'getDepth')
280 def get_depth(self, vdi_uuid, response):
281 return int(response)
283 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash')
284 def get_key_hash(self, vdi_uuid, response):
285 return response or None
287 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap')
288 def get_block_bitmap(self, vdi_uuid, response):
289 return base64.b64decode(response)
291 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
292 def get_drbd_size(self, vdi_uuid, response):
293 return int(response)
295 def _get_drbd_size(self, path):
296 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
297 if ret == 0:
298 return int(stdout.strip())
299 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
301 # --------------------------------------------------------------------------
302 # Setters: only used locally.
303 # --------------------------------------------------------------------------
305 @linstormodifier()
306 def create(self, path, size, static, msize=0):
307 return self._call_local_method_or_fail(vhdutil.create, path, size, static, msize)
309 @linstormodifier()
310 def set_size_phys(self, path, size, debug=True):
311 return self._call_local_method_or_fail(vhdutil.setSizePhys, path, size, debug)
313 @linstormodifier()
314 def set_parent(self, path, parentPath, parentRaw=False):
315 return self._call_local_method_or_fail(vhdutil.setParent, path, parentPath, parentRaw)
317 @linstormodifier()
318 def set_hidden(self, path, hidden=True):
319 return self._call_local_method_or_fail(vhdutil.setHidden, path, hidden)
321 @linstormodifier()
322 def set_key(self, path, key_hash):
323 return self._call_local_method_or_fail(vhdutil.setKey, path, key_hash)
325 @linstormodifier()
326 def kill_data(self, path):
327 return self._call_local_method_or_fail(vhdutil.killData, path)
329 @linstormodifier()
330 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
331 return self._call_local_method_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
333 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
334 # Only inflate if the LINSTOR volume capacity is not enough.
335 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
336 if new_size <= old_size:
337 return
339 util.SMlog(
340 'Inflate {} (size={}, previous={})'
341 .format(vdi_path, new_size, old_size)
342 )
344 journaler.create(
345 LinstorJournaler.INFLATE, vdi_uuid, old_size
346 )
347 self._linstor.resize_volume(vdi_uuid, new_size)
349 result_size = self.get_drbd_size(vdi_uuid)
350 if result_size < new_size:
351 util.SMlog(
352 'WARNING: Cannot inflate volume to {}B, result size: {}B'
353 .format(new_size, result_size)
354 )
356 self._zeroize(vdi_path, result_size - vhdutil.VHD_FOOTER_SIZE)
357 self.set_size_phys(vdi_path, result_size, False)
358 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
360 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
361 if zeroize:
362 assert old_size > vhdutil.VHD_FOOTER_SIZE
363 self._zeroize(vdi_path, old_size - vhdutil.VHD_FOOTER_SIZE)
365 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
366 if new_size >= old_size:
367 return
369 util.SMlog(
370 'Deflate {} (new size={}, previous={})'
371 .format(vdi_path, new_size, old_size)
372 )
374 self.set_size_phys(vdi_path, new_size)
375 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
377 # --------------------------------------------------------------------------
378 # Remote setters: write locally and try on another host in case of failure.
379 # --------------------------------------------------------------------------
381 @linstormodifier()
382 def set_size_virt(self, path, size, jfile):
383 kwargs = {
384 'size': size,
385 'jfile': jfile
386 }
387 return self._call_method(vhdutil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs)
389 @linstormodifier()
390 def set_size_virt_fast(self, path, size):
391 kwargs = {
392 'size': size
393 }
394 return self._call_method(vhdutil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs)
396 @linstormodifier()
397 def force_parent(self, path, parentPath, parentRaw=False):
398 kwargs = {
399 'parentPath': str(parentPath),
400 'parentRaw': parentRaw
401 }
402 return self._call_method(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs)
404 @linstormodifier()
405 def force_coalesce(self, path):
406 return int(self._call_method(vhdutil.coalesce, 'coalesce', path, use_parent=True))
408 @linstormodifier()
409 def force_repair(self, path):
410 return self._call_method(vhdutil.repair, 'repair', path, use_parent=False)
412 @linstormodifier()
413 def force_deflate(self, path, newSize, oldSize, zeroize):
414 kwargs = {
415 'newSize': newSize,
416 'oldSize': oldSize,
417 'zeroize': zeroize
418 }
419 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
421 def _force_deflate(self, path, newSize, oldSize, zeroize):
422 self.deflate(path, newSize, oldSize, zeroize)
424 # --------------------------------------------------------------------------
425 # Static helpers.
426 # --------------------------------------------------------------------------
428 @classmethod
429 def compute_volume_size(cls, virtual_size, image_type):
430 if image_type == vhdutil.VDI_TYPE_VHD:
431 # All LINSTOR VDIs have the metadata area preallocated for
432 # the maximum possible virtual size (for fast online VDI.resize).
433 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE)
434 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size)
435 virtual_size += meta_overhead + bitmap_overhead
436 elif image_type != vhdutil.VDI_TYPE_RAW:
437 raise Exception('Invalid image type: {}'.format(image_type))
439 return LinstorVolumeManager.round_up_volume_size(virtual_size)
441 # --------------------------------------------------------------------------
442 # Helpers.
443 # --------------------------------------------------------------------------
445 def _extract_uuid(self, device_path):
446 # TODO: Remove new line in the vhdutil module. Not here.
447 return self._linstor.get_volume_uuid_from_device_path(
448 device_path.rstrip('\n')
449 )
451 def _get_hosts(self, remote_method, device_path):
452 try:
453 return self._session.xenapi.host.get_all_records()
454 except Exception as e:
455 raise xs_errors.XenError(
456 'VDIUnavailable',
457 opterr='Unable to get host list to run vhdutil command `{}` (path={}): {}'
458 .format(remote_method, device_path, e)
459 )
461 # --------------------------------------------------------------------------
463 @staticmethod
464 def _find_host_ref_from_hostname(hosts, hostname):
465 return next((ref for ref, rec in hosts.items() if rec['hostname'] == hostname), None)
467 def _raise_openers_exception(self, device_path, e):
468 if isinstance(e, util.CommandException):
469 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
470 else:
471 e_str = str(e)
473 try:
474 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
475 device_path
476 )
477 e_wrapper = Exception(
478 e_str + ' (openers: {})'.format(
479 self._linstor.get_volume_openers(volume_uuid)
480 )
481 )
482 except Exception as illformed_e:
483 e_wrapper = Exception(
484 e_str + ' (unable to get openers: {})'.format(illformed_e)
485 )
486 util.SMlog('raise opener exception: {}'.format(e_wrapper))
487 raise e_wrapper # pylint: disable = E0702
489 def _call_local_method(self, local_method, device_path, *args, **kwargs):
490 if isinstance(local_method, str):
491 local_method = getattr(self, local_method)
493 try:
494 def local_call():
495 try:
496 return local_method(device_path, *args, **kwargs)
497 except util.CommandException as e:
498 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
499 raise ErofsLinstorCallException(e) # Break retry calls.
500 if e.code == errno.ENOENT:
501 raise NoPathLinstorCallException(e)
502 raise e
503 # Retry only locally if it's not an EROFS exception.
504 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
505 except util.CommandException as e:
506 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code))
507 raise e
509 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
510 try:
511 return self._call_local_method(local_method, device_path, *args, **kwargs)
512 except ErofsLinstorCallException as e:
513 # Volume is locked on a host, find openers.
514 self._raise_openers_exception(device_path, e.cmd_err)
516 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
517 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method.
518 # Normally in case of failure, if the parent is unused we try to execute the method on
519 # another host using the DRBD opener list. In the other case, if the parent is required,
520 # we must check where this last one is open instead of the child.
522 if isinstance(local_method, str):
523 local_method = getattr(self, local_method)
525 # A. Try to write locally...
526 try:
527 return self._call_local_method(local_method, device_path, *args, **kwargs)
528 except Exception:
529 pass
531 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
533 # B. Execute the command on another host.
534 # B.1. Get host list.
535 hosts = self._get_hosts(remote_method, device_path)
537 # B.2. Prepare remote args.
538 remote_args = {
539 'devicePath': device_path,
540 'groupName': self._linstor.group_name
541 }
542 remote_args.update(**kwargs)
543 remote_args = {str(key): str(value) for key, value in remote_args.items()}
545 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
546 device_path
547 )
548 parent_volume_uuid = None
549 if use_parent:
550 parent_volume_uuid = self.get_parent(volume_uuid)
552 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
554 # B.3. Call!
555 def remote_call():
556 try:
557 all_openers = self._linstor.get_volume_openers(openers_uuid)
558 except Exception as e:
559 raise xs_errors.XenError(
560 'VDIUnavailable',
561 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}'
562 .format(remote_method, device_path, e)
563 )
565 no_host_found = True
566 for hostname, openers in all_openers.items():
567 if not openers:
568 continue
570 host_ref = self._find_host_ref_from_hostname(hosts, hostname)
571 if not host_ref:
572 continue
574 no_host_found = False
575 try:
576 return call_remote_method(self._session, host_ref, remote_method, remote_args)
577 except Exception:
578 pass
580 if no_host_found:
581 try:
582 return local_method(device_path, *args, **kwargs)
583 except Exception as e:
584 self._raise_openers_exception(device_path, e)
586 raise xs_errors.XenError(
587 'VDIUnavailable',
588 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`)'
589 .format(remote_method, device_path, openers)
590 )
591 return util.retry(remote_call, 5, 2)
593 @staticmethod
594 def _zeroize(path, size):
595 if not util.zeroOut(path, size, vhdutil.VHD_FOOTER_SIZE):
596 raise xs_errors.XenError(
597 'EIO',
598 opterr='Failed to zero out VHD footer {}'.format(path)
599 )