Coverage for drivers/linstorcowutil.py : 24%
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 on {} ({} with {}) exception: {}'.format(
43 host_ref, method, args, e
44 ))
45 raise util.SMException(str(e))
47 util.SMlog('call-plugin on {} ({} with {}) returned: {}'.format(
48 host_ref, 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
70def log_successful_call(target_host, device_path, vdi_uuid, remote_method, response):
71 util.SMlog('Successful access on {} for device {} ({}): `{}` => {}'.format(
72 target_host, device_path, vdi_uuid, remote_method, str(response)
73 ), priority=util.LOG_DEBUG)
75def log_failed_call(target_host, next_target, device_path, vdi_uuid, remote_method, e):
76 util.SMlog('Failed to call method on {} for device {} ({}): {}. Trying accessing on {}... (cause: {})'.format(
77 target_host, device_path, vdi_uuid, remote_method, next_target, e
78 ), priority=util.LOG_DEBUG)
80def linstorhostcall(local_method, remote_method=None):
81 if not remote_method:
82 remote_method = local_method
84 def decorated(response_parser):
85 def wrapper(*args, **kwargs):
86 self = args[0]
87 vdi_uuid = args[1]
89 device_path = self._linstor.build_device_path(
90 self._linstor.get_volume_name(vdi_uuid)
91 )
93 if not self._session:
94 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
96 remote_args = {
97 'devicePath': device_path,
98 'groupName': self._linstor.group_name,
99 'vdiType': self._vdi_type
100 }
101 remote_args.update(**kwargs)
102 remote_args = {str(key): str(value) for key, value in remote_args.items()}
104 this_host_ref = util.get_this_host_ref(self._session)
105 def call_method(host_label, host_ref):
106 if host_ref == this_host_ref:
107 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
108 response = call_remote_method(self._session, host_ref, remote_method, remote_args)
109 log_successful_call(host_label, device_path, vdi_uuid, remote_method, response)
110 return response_parser(self, vdi_uuid, response)
112 # 1. Try on attached host.
113 try:
114 host_ref_attached = next(iter(util.get_hosts_attached_on(self._session, [vdi_uuid])), None)
115 if host_ref_attached:
116 return call_method('attached host', host_ref_attached)
117 except Exception as e:
118 log_failed_call('attached host', 'master', device_path, vdi_uuid, remote_method, e)
120 # 2. Try on master host.
121 try:
122 return call_method('master', util.get_master_ref(self._session))
123 except Exception as e:
124 log_failed_call('master', 'primary', device_path, vdi_uuid, remote_method, e)
126 # 3. Try on a primary.
127 hosts = self._get_hosts(remote_method, device_path)
129 nodes, primary_hostname = self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
130 if primary_hostname:
131 try:
132 return call_method('primary', self._find_host_ref_from_hostname(hosts, primary_hostname))
133 except Exception as remote_e:
134 self._raise_openers_exception(device_path, remote_e)
136 log_failed_call('primary', 'another node', device_path, vdi_uuid, remote_method, 'no primary')
138 # 4. Try on any host with local data.
139 try:
140 return call_method('another node', next(filter(None,
141 (self._find_host_ref_from_hostname(hosts, hostname) for hostname in nodes)
142 ), None))
143 except Exception as remote_e:
144 self._raise_openers_exception(device_path, remote_e)
146 return wrapper
147 return decorated
150def linstormodifier():
151 def decorated(func):
152 def wrapper(*args, **kwargs):
153 self = args[0]
155 ret = func(*args, **kwargs)
156 self._linstor.invalidate_resource_cache()
157 return ret
158 return wrapper
159 return decorated
162class LinstorCowUtil(object):
163 def __init__(self, session, linstor, vdi_type: str):
164 self._session = session
165 self._linstor = linstor
166 self._cowutil = getCowUtil(vdi_type)
167 self._vdi_type = vdi_type
169 @property
170 def cowutil(self) -> CowUtil:
171 return self._cowutil
173 def create_chain_paths(self, vdi_uuid, readonly=False):
174 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit cowutil calls.
175 # Useful for the snapshot code algorithm.
177 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid)
178 path = leaf_vdi_path
179 while True:
180 if not util.pathexists(path):
181 raise xs_errors.XenError(
182 'VDIUnavailable', opterr='Could not find: {}'.format(path)
183 )
185 # Diskless path can be created on the fly, ensure we can open it.
186 def check_volume_usable():
187 while True:
188 try:
189 with open(path, 'r' if readonly else 'r+'):
190 pass
191 except IOError as e:
192 if e.errno == errno.ENODATA:
193 time.sleep(2)
194 continue
195 if e.errno == errno.EROFS:
196 util.SMlog('Volume not attachable because RO. Openers: {}'.format(
197 self._linstor.get_volume_openers(vdi_uuid)
198 ))
199 raise
200 break
201 util.retry(check_volume_usable, 15, 2)
203 vdi_uuid = self.get_info(vdi_uuid).parentUuid
204 if not vdi_uuid:
205 break
206 path = self._linstor.get_device_path(vdi_uuid)
207 readonly = True # Non-leaf is always readonly.
209 return leaf_vdi_path
211 # --------------------------------------------------------------------------
212 # Getters: read locally and try on another host in case of failure.
213 # --------------------------------------------------------------------------
215 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
216 kwargs = {
217 'ignoreMissingFooter': ignore_missing_footer,
218 'fast': fast
219 }
220 return self._check(vdi_uuid, **kwargs)
222 @linstorhostcall('check')
223 def _check(self, vdi_uuid, response):
224 return CowUtil.CheckResult(response)
226 def get_info(self, vdi_uuid, include_parent=True):
227 kwargs = {
228 'includeParent': include_parent,
229 'resolveParent': False
230 }
231 return self._get_info(vdi_uuid, self._extract_uuid, **kwargs)
233 @linstorhostcall('getInfo')
234 def _get_info(self, vdi_uuid, response):
235 obj = json.loads(response)
237 image_info = CowImageInfo(vdi_uuid)
238 image_info.sizeVirt = obj['sizeVirt']
239 image_info.sizePhys = obj['sizePhys']
240 if 'parentPath' in obj:
241 image_info.parentPath = obj['parentPath']
242 image_info.parentUuid = obj['parentUuid']
243 image_info.hidden = obj['hidden']
244 image_info.path = obj['path']
246 return image_info
248 @linstorhostcall('hasParent')
249 def has_parent(self, vdi_uuid, response):
250 return util.strtobool(response)
252 def get_parent(self, vdi_uuid):
253 return self._get_parent(vdi_uuid, self._extract_uuid)
255 @linstorhostcall('getParent')
256 def _get_parent(self, vdi_uuid, response):
257 return response
259 @linstorhostcall('getSizeVirt')
260 def get_size_virt(self, vdi_uuid, response):
261 return int(response)
263 @linstorhostcall('getMaxResizeSize')
264 def get_max_resize_size(self, vdi_uuid, response):
265 return int(response)
267 @linstorhostcall('getSizePhys')
268 def get_size_phys(self, vdi_uuid, response):
269 return int(response)
271 @linstorhostcall('getAllocatedSize')
272 def get_allocated_size(self, vdi_uuid, response):
273 return int(response)
275 @linstorhostcall('getDepth')
276 def get_depth(self, vdi_uuid, response):
277 return int(response)
279 @linstorhostcall('getKeyHash')
280 def get_key_hash(self, vdi_uuid, response):
281 return response or None
283 @linstorhostcall('getBlockBitmap')
284 def get_block_bitmap(self, vdi_uuid, response):
285 return base64.b64decode(response)
287 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
288 def get_drbd_size(self, vdi_uuid, response):
289 return int(response)
291 def _get_drbd_size(self, path):
292 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
293 if ret == 0:
294 return int(stdout.strip())
295 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
297 # --------------------------------------------------------------------------
298 # Setters: only used locally.
299 # --------------------------------------------------------------------------
301 @linstormodifier()
302 def create(self, path, size, static, msize=0):
303 return self._call_local_method_or_fail(self._cowutil.create, path, size, static, msize)
305 @linstormodifier()
306 def set_size_phys(self, path, size, debug=True):
307 return self._call_local_method_or_fail(self._cowutil.setSizePhys, path, size, debug)
309 @linstormodifier()
310 def set_parent(self, path, parentPath, parentRaw=False):
311 return self._call_local_method_or_fail(self._cowutil.setParent, path, parentPath, parentRaw)
313 @linstormodifier()
314 def set_hidden(self, path, hidden=True):
315 return self._call_local_method_or_fail(self._cowutil.setHidden, path, hidden)
317 @linstormodifier()
318 def set_key(self, path, key_hash):
319 return self._call_local_method_or_fail(self._cowutil.setKey, path, key_hash)
321 @linstormodifier()
322 def kill_data(self, path):
323 return self._call_local_method_or_fail(self._cowutil.killData, path)
325 @linstormodifier()
326 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
327 return self._call_local_method_or_fail(self._cowutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
329 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
330 # Only inflate if the LINSTOR volume capacity is not enough.
331 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
332 if new_size <= old_size:
333 return
335 util.SMlog(
336 'Inflate {} (size={}, previous={})'
337 .format(vdi_path, new_size, old_size)
338 )
340 journaler.create(
341 LinstorJournaler.INFLATE, vdi_uuid, old_size
342 )
343 self._linstor.resize_volume(vdi_uuid, new_size)
345 result_size = self.get_drbd_size(vdi_uuid)
346 if result_size < new_size:
347 util.SMlog(
348 'WARNING: Cannot inflate volume to {}B, result size: {}B'
349 .format(new_size, result_size)
350 )
352 self._zeroize(vdi_path, result_size - self._cowutil.getFooterSize())
353 self.set_size_phys(vdi_path, result_size, False)
354 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
356 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
357 if zeroize:
358 assert old_size > self._cowutil.getFooterSize()
359 self._zeroize(vdi_path, old_size - self._cowutil.getFooterSize())
361 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
362 if new_size >= old_size:
363 return
365 util.SMlog(
366 'Deflate {} (new size={}, previous={})'
367 .format(vdi_path, new_size, old_size)
368 )
370 self.set_size_phys(vdi_path, new_size)
371 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
373 # --------------------------------------------------------------------------
374 # Remote setters: write locally and try on another host in case of failure.
375 # --------------------------------------------------------------------------
377 @linstormodifier()
378 def set_size_virt(self, path, size, jFile):
379 kwargs = {
380 'size': size,
381 'jFile': jFile
382 }
383 return self._call_method(self._cowutil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs)
385 @linstormodifier()
386 def set_size_virt_fast(self, path, size):
387 kwargs = {
388 'size': size
389 }
390 return self._call_method(self._cowutil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs)
392 @linstormodifier()
393 def force_parent(self, path, parentPath, parentRaw=False):
394 kwargs = {
395 'parentPath': str(parentPath),
396 'parentRaw': parentRaw
397 }
398 return self._call_method(self._cowutil.setParent, 'setParent', path, use_parent=False, **kwargs)
400 @linstormodifier()
401 def force_coalesce(self, path):
402 return int(self._call_method(self._cowutil.coalesce, 'coalesce', path, use_parent=True))
404 @linstormodifier()
405 def force_repair(self, path):
406 return self._call_method(self._cowutil.repair, 'repair', path, use_parent=False)
408 @linstormodifier()
409 def force_deflate(self, path, newSize, oldSize, zeroize):
410 kwargs = {
411 'newSize': newSize,
412 'oldSize': oldSize,
413 'zeroize': zeroize
414 }
415 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
417 def _force_deflate(self, path, newSize, oldSize, zeroize):
418 self.deflate(path, newSize, oldSize, zeroize)
420 # --------------------------------------------------------------------------
421 # Helpers.
422 # --------------------------------------------------------------------------
424 def compute_volume_size(self, virtual_size: int) -> int:
425 if VdiType.isCowImage(self._vdi_type):
426 # All LINSTOR VDIs have the metadata area preallocated for
427 # the maximum possible virtual size (for fast online VDI.resize).
428 meta_overhead = self._cowutil.calcOverheadEmpty(
429 max(virtual_size, self._cowutil.getDefaultPreallocationSizeVirt())
430 )
431 bitmap_overhead = self._cowutil.calcOverheadBitmap(virtual_size)
432 virtual_size += meta_overhead + bitmap_overhead
433 else:
434 raise Exception('Invalid image type: {}'.format(self._vdi_type))
436 return LinstorVolumeManager.round_up_volume_size(virtual_size)
438 def _extract_uuid(self, device_path):
439 # TODO: Remove new line in the vhdutil module. Not here.
440 return self._linstor.get_volume_uuid_from_device_path(
441 device_path.rstrip('\n')
442 )
444 def _get_hosts(self, remote_method, device_path):
445 try:
446 return self._session.xenapi.host.get_all_records()
447 except Exception as e:
448 raise xs_errors.XenError(
449 'VDIUnavailable',
450 opterr='Unable to get host list to run cowutil command `{}` (path={}): {}'
451 .format(remote_method, device_path, e)
452 )
454 # --------------------------------------------------------------------------
456 @staticmethod
457 def _find_host_ref_from_hostname(hosts, hostname):
458 return next((ref for ref, rec in hosts.items() if rec['hostname'] == hostname), None)
460 def _raise_openers_exception(self, device_path, e):
461 if isinstance(e, util.CommandException):
462 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
463 else:
464 e_str = str(e)
466 try:
467 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
468 device_path
469 )
470 e_wrapper = Exception(
471 e_str + ' (openers: {})'.format(
472 self._linstor.get_volume_openers(volume_uuid)
473 )
474 )
475 except Exception as illformed_e:
476 e_wrapper = Exception(
477 e_str + ' (unable to get openers: {})'.format(illformed_e)
478 )
479 util.SMlog('raise opener exception: {}'.format(e_wrapper))
480 raise e_wrapper # pylint: disable = E0702
482 def _sanitize_local_method(self, local_method):
483 if isinstance(local_method, str):
484 return getattr(self if local_method.startswith('_') else self._cowutil, local_method)
485 return local_method
487 def _call_local_method(self, local_method, device_path, *args, **kwargs):
488 local_method = self._sanitize_local_method(local_method)
490 try:
491 def local_call():
492 try:
493 return local_method(device_path, *args, **kwargs)
494 except util.CommandException as e:
495 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
496 raise ErofsLinstorCallException(e) # Break retry calls.
497 if e.code == errno.ENOENT:
498 raise NoPathLinstorCallException(e)
499 raise e
500 # Retry only locally if it's not an EROFS exception.
501 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
502 except util.CommandException as e:
503 util.SMlog('failed to execute locally CowUtil (sys {})'.format(e.code))
504 raise e
506 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
507 try:
508 return self._call_local_method(local_method, device_path, *args, **kwargs)
509 except ErofsLinstorCallException as e:
510 # Volume is locked on a host, find openers.
511 self._raise_openers_exception(device_path, e.cmd_err)
513 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
514 # Note: `use_parent` exists to know if the COW image parent is used by the local/remote method.
515 # Normally in case of failure, if the parent is unused we try to execute the method on
516 # another host using the DRBD opener list. In the other case, if the parent is required,
517 # we must check where this last one is open instead of the child.
519 local_method = self._sanitize_local_method(local_method)
521 # A. Try to write locally...
522 try:
523 return self._call_local_method(local_method, device_path, *args, **kwargs)
524 except Exception:
525 pass
527 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
529 # B. Execute the command on another host.
530 # B.1. Get host list.
531 hosts = self._get_hosts(remote_method, device_path)
533 # B.2. Prepare remote args.
534 remote_args = {
535 'devicePath': device_path,
536 'groupName': self._linstor.group_name,
537 'vdiType': self._vdi_type
538 }
539 remote_args.update(**kwargs)
540 remote_args = {str(key): str(value) for key, value in remote_args.items()}
542 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
543 device_path
544 )
545 parent_volume_uuid = None
546 if use_parent:
547 parent_volume_uuid = self.get_parent(volume_uuid)
549 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
551 # B.3. Call!
552 def remote_call():
553 try:
554 all_openers = self._linstor.get_volume_openers(openers_uuid)
555 except Exception as e:
556 raise xs_errors.XenError(
557 'VDIUnavailable',
558 opterr='Unable to get DRBD openers to run CowUtil command `{}` (path={}): {}'
559 .format(remote_method, device_path, e)
560 )
562 no_host_found = True
563 for hostname, openers in all_openers.items():
564 if not openers:
565 continue
567 host_ref = self._find_host_ref_from_hostname(hosts, hostname)
568 if not host_ref:
569 continue
571 no_host_found = False
572 try:
573 return call_remote_method(self._session, host_ref, remote_method, remote_args)
574 except Exception:
575 pass
577 if no_host_found:
578 try:
579 return local_method(device_path, *args, **kwargs)
580 except Exception as e:
581 self._raise_openers_exception(device_path, e)
583 raise xs_errors.XenError(
584 'VDIUnavailable',
585 opterr='No valid host found to run CowUtil command `{}` (path=`{}`, openers=`{}`)'
586 .format(remote_method, device_path, openers)
587 )
588 return util.retry(remote_call, 5, 2)
590 def _zeroize(self, path, size):
591 if not util.zeroOut(path, size, self._cowutil.getFooterSize()):
592 raise xs_errors.XenError(
593 'EIO',
594 opterr='Failed to zero out COW image footer {}'.format(path)
595 )