Coverage for drivers/linstorvhdutil.py : 25%

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/>.
17import base64
18import distutils.util
19import errno
20import json
21import socket
22import util
23import vhdutil
24import xs_errors
26MANAGER_PLUGIN = 'linstor-manager'
29def call_vhd_util_on_host(session, host_ref, method, device_path, args):
30 try:
31 response = session.xenapi.host.call_plugin(
32 host_ref, MANAGER_PLUGIN, method, args
33 )
34 except Exception as e:
35 util.SMlog('call-plugin ({} with {}) exception: {}'.format(
36 method, args, e
37 ))
38 raise
40 util.SMlog('call-plugin ({} with {}) returned: {}'.format(
41 method, args, response
42 ))
44 return response
47class LinstorCallException(Exception):
48 def __init__(self, cmd_err):
49 self.cmd_err = cmd_err
51 def __str__(self):
52 return str(self.cmd_err)
55class ErofsLinstorCallException(LinstorCallException):
56 pass
59class NoPathLinstorCallException(LinstorCallException):
60 pass
63def linstorhostcall(local_method, remote_method):
64 def decorated(response_parser):
65 def wrapper(*args, **kwargs):
66 self = args[0]
67 vdi_uuid = args[1]
69 device_path = self._linstor.build_device_path(
70 self._linstor.get_volume_name(vdi_uuid)
71 )
73 # A. Try a call using directly the DRBD device to avoid
74 # remote request.
76 # Try to read locally if the device is not in use or if the device
77 # is up to date and not diskless.
78 (node_names, in_use_by) = \
79 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid)
81 local_e = None
82 try:
83 if not in_use_by or socket.gethostname() in node_names:
84 return self._call_local_vhd_util(local_method, device_path, *args[2:], **kwargs)
85 except ErofsLinstorCallException as e:
86 local_e = e.cmd_err
87 except Exception as e:
88 local_e = e
90 util.SMlog(
91 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format(
92 remote_method, local_e if local_e else 'local diskless + in use or not up to date'
93 )
94 )
96 if in_use_by:
97 node_names = {in_use_by}
99 # B. Execute the plugin on master or slave.
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 try:
108 def remote_call():
109 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names)
110 return call_vhd_util_on_host(self._session, host_ref, remote_method, device_path, remote_args)
111 response = util.retry(remote_call, 5, 2)
112 except Exception as remote_e:
113 self._raise_openers_exception(device_path, local_e or remote_e)
115 return response_parser(self, vdi_uuid, response)
116 return wrapper
117 return decorated
120def linstormodifier():
121 def decorated(func):
122 def wrapper(*args, **kwargs):
123 self = args[0]
125 ret = func(*args, **kwargs)
126 self._linstor.invalidate_resource_cache()
127 return ret
128 return wrapper
129 return decorated
132class LinstorVhdUtil:
133 def __init__(self, session, linstor):
134 self._session = session
135 self._linstor = linstor
137 # --------------------------------------------------------------------------
138 # Getters: read locally and try on another host in case of failure.
139 # --------------------------------------------------------------------------
141 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False):
142 kwargs = {
143 'ignoreMissingFooter': ignore_missing_footer,
144 'fast': fast
145 }
146 return self._check(vdi_uuid, **kwargs) # pylint: disable = E1123
148 @linstorhostcall(vhdutil.check, 'check')
149 def _check(self, vdi_uuid, response):
150 return distutils.util.strtobool(response)
152 def get_vhd_info(self, vdi_uuid, include_parent=True):
153 kwargs = {
154 'includeParent': include_parent,
155 'resolveParent': False
156 }
157 # TODO: Replace pylint comment with this feature when possible:
158 # https://github.com/PyCQA/pylint/pull/2926
159 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs) # pylint: disable = E1123
161 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo')
162 def _get_vhd_info(self, vdi_uuid, response):
163 obj = json.loads(response)
165 vhd_info = vhdutil.VHDInfo(vdi_uuid)
166 vhd_info.sizeVirt = obj['sizeVirt']
167 vhd_info.sizePhys = obj['sizePhys']
168 if 'parentPath' in obj:
169 vhd_info.parentPath = obj['parentPath']
170 vhd_info.parentUuid = obj['parentUuid']
171 vhd_info.hidden = obj['hidden']
172 vhd_info.path = obj['path']
174 return vhd_info
176 @linstorhostcall(vhdutil.hasParent, 'hasParent')
177 def has_parent(self, vdi_uuid, response):
178 return distutils.util.strtobool(response)
180 def get_parent(self, vdi_uuid):
181 return self._get_parent(vdi_uuid, self._extract_uuid)
183 @linstorhostcall(vhdutil.getParent, 'getParent')
184 def _get_parent(self, vdi_uuid, response):
185 return response
187 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt')
188 def get_size_virt(self, vdi_uuid, response):
189 return int(response)
191 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys')
192 def get_size_phys(self, vdi_uuid, response):
193 return int(response)
195 @linstorhostcall(vhdutil.getDepth, 'getDepth')
196 def get_depth(self, vdi_uuid, response):
197 return int(response)
199 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash')
200 def get_key_hash(self, vdi_uuid, response):
201 return response or None
203 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap')
204 def get_block_bitmap(self, vdi_uuid, response):
205 return base64.b64decode(response)
207 # --------------------------------------------------------------------------
208 # Setters: only used locally.
209 # --------------------------------------------------------------------------
211 @linstormodifier()
212 def create(self, path, size, static, msize=0):
213 return self._call_local_vhd_util_or_fail(vhdutil.create, path, size, static, msize)
215 @linstormodifier()
216 def set_size_virt_fast(self, path, size):
217 return self._call_local_vhd_util_or_fail(vhdutil.setSizeVirtFast, path, size)
219 @linstormodifier()
220 def set_size_phys(self, path, size, debug=True):
221 return self._call_local_vhd_util_or_fail(vhdutil.setSizePhys, path, size, debug)
223 @linstormodifier()
224 def set_parent(self, path, parentPath, parentRaw=False):
225 return self._call_local_vhd_util_or_fail(vhdutil.setParent, path, parentPath, parentRaw)
227 @linstormodifier()
228 def set_hidden(self, path, hidden=True):
229 return self._call_local_vhd_util_or_fail(vhdutil.setHidden, path, hidden)
231 @linstormodifier()
232 def set_key(self, path, key_hash):
233 return self._call_local_vhd_util_or_fail(vhdutil.setKey, path, key_hash)
235 @linstormodifier()
236 def kill_data(self, path):
237 return self._call_local_vhd_util_or_fail(vhdutil.killData, path)
239 @linstormodifier()
240 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True):
241 return self._call_local_vhd_util_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty)
243 # --------------------------------------------------------------------------
244 # Remote setters: write locally and try on another host in case of failure.
245 # --------------------------------------------------------------------------
247 @linstormodifier()
248 def force_parent(self, path, parentPath, parentRaw=False):
249 kwargs = {
250 'parentPath': str(parentPath),
251 'parentRaw': parentRaw
252 }
253 return self._call_vhd_util(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs)
255 @linstormodifier()
256 def force_coalesce(self, path):
257 return self._call_vhd_util(vhdutil.coalesce, 'coalesce', path, use_parent=True)
259 @linstormodifier()
260 def force_repair(self, path):
261 return self._call_vhd_util(vhdutil.repair, 'repair', path, use_parent=False)
263 # --------------------------------------------------------------------------
264 # Helpers.
265 # --------------------------------------------------------------------------
267 def _extract_uuid(self, device_path):
268 # TODO: Remove new line in the vhdutil module. Not here.
269 return self._linstor.get_volume_uuid_from_device_path(
270 device_path.rstrip('\n')
271 )
273 def _get_readonly_host(self, vdi_uuid, device_path, node_names):
274 """
275 When vhd-util is called to fetch VDI info we must find a
276 diskful DRBD disk to read the data. It's the goal of this function.
277 Why? Because when a VHD is open in RO mode, the LVM layer is used
278 directly to bypass DRBD verifications (we can have only one process
279 that reads/writes to disk with DRBD devices).
280 """
282 if not node_names:
283 raise xs_errors.XenError(
284 'VDIUnavailable',
285 opterr='Unable to find diskful node: {} (path={})'
286 .format(vdi_uuid, device_path)
287 )
289 hosts = self._session.xenapi.host.get_all_records()
290 for host_ref, host_record in hosts.items():
291 if host_record['hostname'] in node_names:
292 return host_ref
294 raise xs_errors.XenError(
295 'VDIUnavailable',
296 opterr='Unable to find a valid host from VDI: {} (path={})'
297 .format(vdi_uuid, device_path)
298 )
300 # --------------------------------------------------------------------------
302 def _raise_openers_exception(self, device_path, e):
303 if isinstance(e, util.CommandException):
304 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason)
305 else:
306 e_str = str(e)
308 e_with_openers = None
309 try:
310 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
311 device_path
312 )
313 e_wrapper = Exception(
314 e_str + ' (openers: {})'.format(
315 self._linstor.get_volume_openers(volume_uuid)
316 )
317 )
318 except Exception as illformed_e:
319 e_wrapper = Exception(
320 e_str + ' (unable to get openers: {})'.format(illformed_e)
321 )
322 util.SMlog('raise opener exception: {}'.format(e_wrapper))
323 raise e_wrapper # pylint: disable = E0702
325 def _call_local_vhd_util(self, local_method, device_path, *args, **kwargs):
326 try:
327 def local_call():
328 try:
329 return local_method(device_path, *args, **kwargs)
330 except util.CommandException as e:
331 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE:
332 raise ErofsLinstorCallException(e) # Break retry calls.
333 if e.code == errno.ENOENT:
334 raise NoPathLinstorCallException(e)
335 raise e
336 # Retry only locally if it's not an EROFS exception.
337 return util.retry(local_call, 5, 2, exceptions=[util.CommandException])
338 except util.CommandException as e:
339 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code))
340 raise e
342 def _call_local_vhd_util_or_fail(self, local_method, device_path, *args, **kwargs):
343 try:
344 return self._call_local_vhd_util(local_method, device_path, *args, **kwargs)
345 except ErofsLinstorCallException as e:
346 # Volume is locked on a host, find openers.
347 self._raise_openers_exception(device_path, e.cmd_err)
349 def _call_vhd_util(self, local_method, remote_method, device_path, use_parent, *args, **kwargs):
350 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method.
351 # Normally in case of failure, if the parent is unused we try to execute the method on
352 # another host using the DRBD opener list. In the other case, if the parent is required,
353 # we must check where this last one is open instead of the child.
355 # A. Try to write locally...
356 try:
357 return self._call_local_vhd_util(local_method, device_path, *args, **kwargs)
358 except Exception:
359 pass
361 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method))
363 # B. Execute the command on another host.
364 # B.1. Get host list.
365 try:
366 hosts = self._session.xenapi.host.get_all_records()
367 except Exception as e:
368 raise xs_errors.XenError(
369 'VDIUnavailable',
370 opterr='Unable to get host list to run vhd-util command `{}` (path={}): {}'
371 .format(remote_method, device_path, e)
372 )
374 # B.2. Prepare remote args.
375 remote_args = {
376 'devicePath': device_path,
377 'groupName': self._linstor.group_name
378 }
379 remote_args.update(**kwargs)
380 remote_args = {str(key): str(value) for key, value in remote_args.items()}
382 volume_uuid = self._linstor.get_volume_uuid_from_device_path(
383 device_path
384 )
385 parent_volume_uuid = None
386 if use_parent:
387 parent_volume_uuid = self.get_parent(volume_uuid)
389 openers_uuid = parent_volume_uuid if use_parent else volume_uuid
391 # B.3. Call!
392 def remote_call():
393 try:
394 all_openers = self._linstor.get_volume_openers(openers_uuid)
395 except Exception as e:
396 raise xs_errors.XenError(
397 'VDIUnavailable',
398 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}'
399 .format(remote_method, device_path, e)
400 )
402 no_host_found = True
403 for hostname, openers in all_openers.items():
404 if not openers:
405 continue
407 try:
408 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname)
409 except StopIteration:
410 continue
412 no_host_found = False
413 try:
414 return call_vhd_util_on_host(self._session, host_ref, remote_method, device_path, remote_args)
415 except Exception:
416 pass
418 if no_host_found:
419 try:
420 return local_method(device_path, *args, **kwargs)
421 except Exception as e:
422 self._raise_openers_exception(device_path, e)
424 raise xs_errors.XenError(
425 'VDIUnavailable',
426 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`): {}'
427 .format(remote_method, device_path, openers, e)
428 )
429 return util.retry(remote_call, 5, 2)