Coverage for drivers/linstorvhdutil.py : 23%

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 util
25import vhdutil
26import xs_errors
28MANAGER_PLUGIN = 'linstor-manager'
31def call_remote_method(session, host_ref, method, device_path, args):
32 try:
33 response = session.xenapi.host.call_plugin(
34 host_ref, MANAGER_PLUGIN, method, args
35 )
36 except Exception as e:
37 util.SMlog('call-plugin ({} with {}) exception: {}'.format(
38 method, args, e
39 ))
40 raise util.SMException(str(e))
42 util.SMlog('call-plugin ({} with {}) returned: {}'.format(
43 method, args, response
44 ))
46 return response
49class LinstorCallException(util.SMException):
50 def __init__(self, cmd_err):
51 self.cmd_err = cmd_err
53 def __str__(self):
54 return str(self.cmd_err)
57class ErofsLinstorCallException(LinstorCallException):
58 pass
61class NoPathLinstorCallException(LinstorCallException):
62 pass
65def linstorhostcall(local_method, remote_method):
66 def decorated(response_parser):
67 def wrapper(*args, **kwargs):
68 self = args[0]
69 vdi_uuid = args[1]
71 device_path = self._linstor.build_device_path(
72 self._linstor.get_volume_name(vdi_uuid)
73 )
75 # A. Try a call using directly the DRBD device to avoid
76 # remote request.
78 # Try to read locally if the device is not in use or if the device
79 # is up to date and not diskless.
80 (node_names, in_use_by) = \
81 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
83 local_e = None
84 try:
85 if not in_use_by or socket.gethostname() in node_names:
86 return self._call_local_method(local_method, device_path, *args[2:], **kwargs)
87 except ErofsLinstorCallException as e:
88 local_e = e.cmd_err
89 except Exception as e:
90 local_e = e
92 util.SMlog(
93 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format(
94 remote_method, local_e if local_e else 'local diskless + in use or not up to date'
95 )
96 )
98 if in_use_by:
99 node_names = {in_use_by}
101 # B. Execute the plugin on master or slave.
102 remote_args = {
103 'devicePath': device_path,
104 'groupName': self._linstor.group_name
105 }
106 remote_args.update(**kwargs)
107 remote_args = {str(key): str(value) for key, value in remote_args.items()}
109 try:
110 def remote_call():
111 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names)
112 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
113 response = util.retry(remote_call, 5, 2)
114 except Exception as remote_e:
115 self._raise_openers_exception(device_path, local_e or remote_e)
117 return response_parser(self, vdi_uuid, response)
118 return wrapper
119 return decorated
122def linstormodifier():
123 def decorated(func):
124 def wrapper(*args, **kwargs):
125 self = args[0]
127 ret = func(*args, **kwargs)
128 self._linstor.invalidate_resource_cache()
129 return ret
130 return wrapper
131 return decorated
134class LinstorVhdUtil:
135 MAX_SIZE = 2 * 1024 * 1024 * 1024 * 1024 # Max VHD size.
137 def __init__(self, session, linstor):
138 self._session = session
139 self._linstor = linstor
141 # --------------------------------------------------------------------------
142 # Getters: read locally and try on another host in case of failure.
143 # --------------------------------------------------------------------------
145 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
146 kwargs = {
147 'ignoreMissingFooter': ignore_missing_footer,
148 'fast': fast
149 }
150 return self._check(vdi_uuid, **kwargs) # pylint: disable = E1123
152 @linstorhostcall(vhdutil.check, 'check')
153 def _check(self, vdi_uuid, response):
154 return distutils.util.strtobool(response)
156 def get_vhd_info(self, vdi_uuid, include_parent=True):
157 kwargs = {
158 'includeParent': include_parent,
159 'resolveParent': False
160 }
161 # TODO: Replace pylint comment with this feature when possible:
162 # https://github.com/PyCQA/pylint/pull/2926
163 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs) # pylint: disable = E1123
165 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo')
166 def _get_vhd_info(self, vdi_uuid, response):
167 obj = json.loads(response)
169 vhd_info = vhdutil.VHDInfo(vdi_uuid)
170 vhd_info.sizeVirt = obj['sizeVirt']
171 vhd_info.sizePhys = obj['sizePhys']
172 if 'parentPath' in obj:
173 vhd_info.parentPath = obj['parentPath']
174 vhd_info.parentUuid = obj['parentUuid']
175 vhd_info.hidden = obj['hidden']
176 vhd_info.path = obj['path']
178 return vhd_info
180 @linstorhostcall(vhdutil.hasParent, 'hasParent')
181 def has_parent(self, vdi_uuid, response):
182 return distutils.util.strtobool(response)
184 def get_parent(self, vdi_uuid):
185 return self._get_parent(vdi_uuid, self._extract_uuid)
187 @linstorhostcall(vhdutil.getParent, 'getParent')
188 def _get_parent(self, vdi_uuid, response):
189 return response
191 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt')
192 def get_size_virt(self, vdi_uuid, response):
193 return int(response)
195 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys')
196 def get_size_phys(self, vdi_uuid, response):
197 return int(response)
199 @linstorhostcall(vhdutil.getAllocatedSize, 'getAllocatedSize')
200 def get_allocated_size(self, vdi_uuid, response):
201 return int(response)
203 @linstorhostcall(vhdutil.getDepth, 'getDepth')
204 def get_depth(self, vdi_uuid, response):
205 return int(response)
207 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash')
208 def get_key_hash(self, vdi_uuid, response):
209 return response or None
211 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap')
212 def get_block_bitmap(self, vdi_uuid, response):
213 return base64.b64decode(response)
215 @linstorhostcall('_get_drbd_size', 'getDrbdSize')
216 def get_drbd_size(self, vdi_uuid, response):
217 return int(response)
219 def _get_drbd_size(self, path):
220 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path])
221 if ret == 0:
222 return int(stdout.strip())
223 raise util.SMException('Failed to get DRBD size: {}'.format(stderr))
225 # --------------------------------------------------------------------------
226 # Setters: only used locally.
227 # --------------------------------------------------------------------------
229 @linstormodifier()
230 def create(self, path, size, static, msize=0):
231 return self._call_local_method_or_fail(vhdutil.create, path, size, static, msize)
233 @linstormodifier()
234 def set_size_virt(self, path, size, jfile):
235 return self._call_local_method_or_fail(vhdutil.setSizeVirt, path, size, jfile)
237 @linstormodifier()
238 def set_size_virt_fast(self, path, size):
239 return self._call_local_method_or_fail(vhdutil.setSizeVirtFast, path, size)
241 @linstormodifier()
242 def set_size_phys(self, path, size, debug=True):
243 return self._call_local_method_or_fail(vhdutil.setSizePhys, path, size, debug)
245 @linstormodifier()
246 def set_parent(self, path, parentPath, parentRaw=False):
247 return self._call_local_method_or_fail(vhdutil.setParent, path, parentPath, parentRaw)
249 @linstormodifier()
250 def set_hidden(self, path, hidden=True):
251 return self._call_local_method_or_fail(vhdutil.setHidden, path, hidden)
253 @linstormodifier()
254 def set_key(self, path, key_hash):
255 return self._call_local_method_or_fail(vhdutil.setKey, path, key_hash)
257 @linstormodifier()
258 def kill_data(self, path):
259 return self._call_local_method_or_fail(vhdutil.killData, path)
261 @linstormodifier()
262 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
263 return self._call_local_method_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
265 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size):
266 # Only inflate if the LINSTOR volume capacity is not enough.
267 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
268 if new_size <= old_size:
269 return
271 util.SMlog(
272 'Inflate {} (size={}, previous={})'
273 .format(vdi_path, new_size, old_size)
274 )
276 journaler.create(
277 LinstorJournaler.INFLATE, vdi_uuid, old_size
278 )
279 self._linstor.resize_volume(vdi_uuid, new_size)
281 # TODO: Replace pylint comment with this feature when possible:
282 # https://github.com/PyCQA/pylint/pull/2926
283 result_size = self.get_drbd_size(vdi_uuid) # pylint: disable = E1120
284 if result_size < new_size:
285 util.SMlog(
286 'WARNING: Cannot inflate volume to {}B, result size: {}B'
287 .format(new_size, result_size)
288 )
290 self._zeroize(vdi_path, result_size - vhdutil.VHD_FOOTER_SIZE)
291 self.set_size_phys(vdi_path, result_size, False)
292 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
294 def deflate(self, vdi_path, new_size, old_size, zeroize=False):
295 if zeroize:
296 assert old_size > vhdutil.VHD_FOOTER_SIZE
297 self._zeroize(vdi_path, old_size - vhdutil.VHD_FOOTER_SIZE)
299 new_size = LinstorVolumeManager.round_up_volume_size(new_size)
300 if new_size >= old_size:
301 return
303 util.SMlog(
304 'Deflate {} (new size={}, previous={})'
305 .format(vdi_path, new_size, old_size)
306 )
308 self.set_size_phys(vdi_path, new_size)
309 # TODO: Change the LINSTOR volume size using linstor.resize_volume.
311 # --------------------------------------------------------------------------
312 # Remote setters: write locally and try on another host in case of failure.
313 # --------------------------------------------------------------------------
315 @linstormodifier()
316 def force_parent(self, path, parentPath, parentRaw=False):
317 kwargs = {
318 'parentPath': str(parentPath),
319 'parentRaw': parentRaw
320 }
321 return self._call_method(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs)
323 @linstormodifier()
324 def force_coalesce(self, path):
325 return int(self._call_method(vhdutil.coalesce, 'coalesce', path, use_parent=True))
327 @linstormodifier()
328 def force_repair(self, path):
329 return self._call_method(vhdutil.repair, 'repair', path, use_parent=False)
331 @linstormodifier()
332 def force_deflate(self, path, newSize, oldSize, zeroize):
333 kwargs = {
334 'newSize': newSize,
335 'oldSize': oldSize,
336 'zeroize': zeroize
337 }
338 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs)
340 def _force_deflate(self, path, newSize, oldSize, zeroize):
341 self.deflate(path, newSize, oldSize, zeroize)
343 # --------------------------------------------------------------------------
344 # Static helpers.
345 # --------------------------------------------------------------------------
347 @classmethod
348 def compute_volume_size(cls, virtual_size, image_type):
349 if image_type == vhdutil.VDI_TYPE_VHD:
350 # All LINSTOR VDIs have the metadata area preallocated for
351 # the maximum possible virtual size (for fast online VDI.resize).
352 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE)
353 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size)
354 virtual_size += meta_overhead + bitmap_overhead
355 elif image_type != vhdutil.VDI_TYPE_RAW:
356 raise Exception('Invalid image type: {}'.format(image_type))
358 return LinstorVolumeManager.round_up_volume_size(virtual_size)
360 # --------------------------------------------------------------------------
361 # Helpers.
362 # --------------------------------------------------------------------------
364 def _extract_uuid(self, device_path):
365 # TODO: Remove new line in the vhdutil module. Not here.
366 return self._linstor.get_volume_uuid_from_device_path(
367 device_path.rstrip('\n')
368 )
370 def _get_readonly_host(self, vdi_uuid, device_path, node_names):
371 """
372 When vhd-util is called to fetch VDI info we must find a
373 diskful DRBD disk to read the data. It's the goal of this function.
374 Why? Because when a VHD is open in RO mode, the LVM layer is used
375 directly to bypass DRBD verifications (we can have only one process
376 that reads/writes to disk with DRBD devices).
377 """
379 if not node_names:
380 raise xs_errors.XenError(
381 'VDIUnavailable',
382 opterr='Unable to find diskful node: {} (path={})'
383 .format(vdi_uuid, device_path)
384 )
386 hosts = self._session.xenapi.host.get_all_records()
387 for host_ref, host_record in hosts.items():
388 if host_record['hostname'] in node_names:
389 return host_ref
391 raise xs_errors.XenError(
392 'VDIUnavailable',
393 opterr='Unable to find a valid host from VDI: {} (path={})'
394 .format(vdi_uuid, device_path)
395 )
397 # --------------------------------------------------------------------------
399 def _raise_openers_exception(self, device_path, e):
400 if isinstance(e, util.CommandException):
401 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
402 else:
403 e_str = str(e)
405 try:
406 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
407 device_path
408 )
409 e_wrapper = Exception(
410 e_str + ' (openers: {})'.format(
411 self._linstor.get_volume_openers(volume_uuid)
412 )
413 )
414 except Exception as illformed_e:
415 e_wrapper = Exception(
416 e_str + ' (unable to get openers: {})'.format(illformed_e)
417 )
418 util.SMlog('raise opener exception: {}'.format(e_wrapper))
419 raise e_wrapper # pylint: disable = E0702
421 def _call_local_method(self, local_method, device_path, *args, **kwargs):
422 if isinstance(local_method, str):
423 local_method = getattr(self, local_method)
425 try:
426 def local_call():
427 try:
428 return local_method(device_path, *args, **kwargs)
429 except util.CommandException as e:
430 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
431 raise ErofsLinstorCallException(e) # Break retry calls.
432 if e.code == errno.ENOENT:
433 raise NoPathLinstorCallException(e)
434 raise e
435 # Retry only locally if it's not an EROFS exception.
436 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
437 except util.CommandException as e:
438 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code))
439 raise e
441 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs):
442 try:
443 return self._call_local_method(local_method, device_path, *args, **kwargs)
444 except ErofsLinstorCallException as e:
445 # Volume is locked on a host, find openers.
446 self._raise_openers_exception(device_path, e.cmd_err)
448 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
449 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method.
450 # Normally in case of failure, if the parent is unused we try to execute the method on
451 # another host using the DRBD opener list. In the other case, if the parent is required,
452 # we must check where this last one is open instead of the child.
454 if isinstance(local_method, str):
455 local_method = getattr(self, local_method)
457 # A. Try to write locally...
458 try:
459 return self._call_local_method(local_method, device_path, *args, **kwargs)
460 except Exception:
461 pass
463 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
465 # B. Execute the command on another host.
466 # B.1. Get host list.
467 try:
468 hosts = self._session.xenapi.host.get_all_records()
469 except Exception as e:
470 raise xs_errors.XenError(
471 'VDIUnavailable',
472 opterr='Unable to get host list to run vhd-util command `{}` (path={}): {}'
473 .format(remote_method, device_path, e)
474 )
476 # B.2. Prepare remote args.
477 remote_args = {
478 'devicePath': device_path,
479 'groupName': self._linstor.group_name
480 }
481 remote_args.update(**kwargs)
482 remote_args = {str(key): str(value) for key, value in remote_args.items()}
484 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
485 device_path
486 )
487 parent_volume_uuid = None
488 if use_parent:
489 parent_volume_uuid = self.get_parent(volume_uuid)
491 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
493 # B.3. Call!
494 def remote_call():
495 try:
496 all_openers = self._linstor.get_volume_openers(openers_uuid)
497 except Exception as e:
498 raise xs_errors.XenError(
499 'VDIUnavailable',
500 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}'
501 .format(remote_method, device_path, e)
502 )
504 no_host_found = True
505 for hostname, openers in all_openers.items():
506 if not openers:
507 continue
509 try:
510 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname)
511 except StopIteration:
512 continue
514 no_host_found = False
515 try:
516 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args)
517 except Exception:
518 pass
520 if no_host_found:
521 try:
522 return local_method(device_path, *args, **kwargs)
523 except Exception as e:
524 self._raise_openers_exception(device_path, e)
526 raise xs_errors.XenError(
527 'VDIUnavailable',
528 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`)'
529 .format(remote_method, device_path, openers)
530 )
531 return util.retry(remote_call, 5, 2)
533 @staticmethod
534 def _zeroize(path, size):
535 if not util.zeroOut(path, size, vhdutil.VHD_FOOTER_SIZE):
536 raise xs_errors.XenError(
537 'EIO',
538 opterr='Failed to zero out VHD footer {}'.format(path)
539 )