Coverage for drivers/blktap2.py : 48%
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/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
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 Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# blktap2: blktap/tapdisk management layer
19#
20from sm_typing import Any, Callable, ClassVar, Dict, override, List, Union
22from abc import abstractmethod
24import grp
25import os
26import re
27import stat
28import time
29import copy
30from lock import Lock
31import util
32import xmlrpc.client
33import errno
34import signal
35import subprocess
36import syslog as _syslog
37import glob
38import json
39import xs_errors
40from typing import Optional
41import XenAPI # pylint: disable=import-error
42import scsiutil
43from constants import NS_PREFIX_LVM
44from syslog import openlog, syslog
45from stat import * # S_ISBLK(), ...
46from vditype import VdiType
48import resetvdis
50import VDI as sm
52from cowutil import getCowUtil
54# For RRDD Plugin Registration
55from xmlrpc.client import ServerProxy, Transport
56from socket import socket, AF_UNIX, SOCK_STREAM
59try:
60 from linstorvolumemanager import get_controller_uri, get_all_volume_openers, LinstorVolumeManager
61 LINSTOR_AVAILABLE = True
62except ImportError:
63 LINSTOR_AVAILABLE = False
65PLUGIN_TAP_PAUSE = "tapdisk-pause"
66PLUGIN_ON_SLAVE = "on-slave"
68SOCKPATH = "/var/xapi/xcp-rrdd"
70NUM_PAGES_PER_RING = 32 * 11
71MAX_FULL_RINGS = 8
72POOL_NAME_KEY = "mem-pool"
73POOL_SIZE_KEY = "mem-pool-size-rings"
75ENABLE_MULTIPLE_ATTACH = "/etc/xensource/allow_multiple_vdi_attach"
76NO_MULTIPLE_ATTACH = not (os.path.exists(ENABLE_MULTIPLE_ATTACH))
78# Including DRBD in the pattern to prevent matching with other SRs than LinstorSR
79TAP_CTL_ERROR_PATTERN = re.compile(
80 r"(?P<status>ERROR|SUCCESS)\s"
81 r"\[(?P<code>-?[0-9]+)\s-\s(?P<category>.+?)]:\s"
82 r"(?P<message>.*)\sReason:\s(?P<reason>.+drbd.+)"
83)
86def locking(excType, override=True):
87 def locking2(op):
88 def wrapper(self, *args):
89 self.lock.acquire()
90 try:
91 try:
92 ret = op(self, * args)
93 except (util.CommandException, util.SMException, XenAPI.Failure) as e: 93 ↛ 103line 93 didn't jump to line 103
94 util.logException("BLKTAP2:%s" % op)
95 msg = str(e)
96 if isinstance(e, util.CommandException): 96 ↛ 97line 96 didn't jump to line 97, because the condition on line 96 was never true
97 msg = "Command %s failed (%s): %s" % \
98 (e.cmd, e.code, e.reason)
99 if override: 99 ↛ 102line 99 didn't jump to line 102, because the condition on line 99 was never false
100 raise xs_errors.XenError(excType, opterr=msg)
101 else:
102 raise
103 except:
104 util.logException("BLKTAP2:%s" % op)
105 raise
106 finally:
107 self.lock.release()
108 return ret
109 return wrapper
110 return locking2
113class RetryLoop(object):
115 def __init__(self, backoff, limit):
116 self.backoff = backoff
117 self.limit = limit
119 def __call__(self, f):
121 def loop(*__t, **__d):
122 attempt = 0
124 while True:
125 attempt += 1
127 try:
128 return f( * __t, ** __d)
130 except self.TransientFailure as e:
131 e = e.exception
133 if attempt >= self.limit: 133 ↛ 134line 133 didn't jump to line 134, because the condition on line 133 was never true
134 raise e
136 time.sleep(self.backoff)
138 return loop
140 class TransientFailure(Exception):
141 def __init__(self, exception):
142 self.exception = exception
145def retried(**args):
146 return RetryLoop( ** args)
149class TapCtl(object):
150 """Tapdisk IPC utility calls."""
152 PATH = "/usr/sbin/tap-ctl"
154 def __init__(self, cmd, p):
155 self.cmd = cmd
156 self._p = p
157 self.stdout = p.stdout
159 class CommandFailure(Exception):
160 """TapCtl cmd failure."""
162 def __init__(self, cmd, **info):
163 self.cmd = cmd
164 self.info = info
166 @override
167 def __str__(self) -> str:
168 items = self.info.items()
169 info = ", ".join("%s=%s" % item
170 for item in items)
171 return "%s failed: %s" % (self.cmd, info)
173 # Trying to get a non-existent attribute throws an AttributeError
174 # exception
175 def __getattr__(self, key):
176 if key in self.info: 176 ↛ 178line 176 didn't jump to line 178, because the condition on line 176 was never false
177 return self.info[key]
178 return object.__getattribute__(self, key)
180 @property
181 def has_status(self):
182 return 'status' in self.info
184 @property
185 def has_signal(self):
186 return 'signal' in self.info
188 # Retrieves the error code returned by the command. If the error code
189 # was not supplied at object-construction time, zero is returned.
190 def get_error_code(self):
191 key = 'status'
192 if key in self.info: 192 ↛ 195line 192 didn't jump to line 195, because the condition on line 192 was never false
193 return self.info[key]
194 else:
195 return 0
197 @classmethod
198 def __mkcmd_real(cls, args):
199 return [cls.PATH] + [str(x) for x in args]
201 __next_mkcmd = __mkcmd_real
203 @classmethod
204 def _mkcmd(cls, args):
206 __next_mkcmd = cls.__next_mkcmd
207 cls.__next_mkcmd = cls.__mkcmd_real
209 return __next_mkcmd(args)
211 @classmethod
212 def _call(cls, args, quiet=False, input=None, text_mode=True):
213 """
214 Spawn a tap-ctl process. Return a TapCtl invocation.
215 Raises a TapCtl.CommandFailure if subprocess creation failed.
216 """
217 cmd = cls._mkcmd(args)
219 if not quiet:
220 util.SMlog(cmd)
221 try:
222 p = subprocess.Popen(cmd,
223 stdin=subprocess.PIPE,
224 stdout=subprocess.PIPE,
225 stderr=subprocess.PIPE,
226 close_fds=True,
227 universal_newlines=text_mode)
228 if input:
229 p.stdin.write(input)
230 except OSError as e:
231 raise cls.CommandFailure(cmd, errno=e.errno)
233 return cls(cmd, p)
235 def _errmsg(self, stderr):
236 output = map(str.rstrip, stderr)
237 return "; ".join(output)
239 def _wait(self, quiet=False, text_mode=True):
240 """
241 Reap the child tap-ctl process of this invocation.
242 Raises a TapCtl.CommandFailure on non-zero exit status.
243 """
244 stdout, stderr = self._p.communicate()
245 status = self._p.returncode
246 if not quiet:
247 util.SMlog(" = %d" % status)
249 if status == 0:
250 return stdout
252 info = {'errmsg': self._errmsg(
253 stderr if text_mode else stderr.decode()),
254 'pid': self._p.pid}
256 if status < 0:
257 info['signal'] = -status
258 else:
259 info['status'] = status
261 raise self.CommandFailure(self.cmd, ** info)
263 @classmethod
264 def _pread(cls, args, quiet=False, input=None, text_mode=True):
265 """
266 Spawn a tap-ctl invocation and read a single line.
267 """
268 tapctl = cls._call(args=args, quiet=quiet, input=input,
269 text_mode=text_mode)
271 output = tapctl._wait(quiet=quiet, text_mode=text_mode)
272 return output
274 @staticmethod
275 def _maybe(opt, parm):
276 if parm is not None:
277 return [opt, parm]
278 return []
280 @classmethod
281 def __list(cls, minor=None, pid=None, _type=None, path=None):
282 args = ["list"]
283 args += cls._maybe("-m", minor)
284 args += cls._maybe("-p", pid)
285 args += cls._maybe("-t", _type)
286 args += cls._maybe("-f", path)
288 tapctl = cls._call(args, quiet=True)
289 stdout = tapctl._wait(quiet=True)
291 for stdout_line in stdout.splitlines():
292 # FIXME: tap-ctl writes error messages to stdout and
293 # confuses this parser
294 if stdout_line == "blktap kernel module not installed\n": 294 ↛ 297line 294 didn't jump to line 297, because the condition on line 294 was never true
295 # This isn't pretty but (a) neither is confusing stdout/stderr
296 # and at least causes the error to describe the fix
297 raise Exception("blktap kernel module not installed: try 'modprobe blktap'")
298 row = {}
300 for field in stdout_line.rstrip().split(' ', 3):
301 bits = field.split('=')
302 if len(bits) == 2: 302 ↛ 314line 302 didn't jump to line 314, because the condition on line 302 was never false
303 key, val = field.split('=')
305 if key in ('pid', 'minor'):
306 row[key] = int(val, 10)
308 elif key in ('state'):
309 row[key] = int(val, 0x10)
311 else:
312 row[key] = val
313 else:
314 util.SMlog("Ignoring unexpected tap-ctl output: %s" % repr(field))
315 yield row
317 @classmethod
318 @retried(backoff=.5, limit=10)
319 def list(cls, **args):
321 # FIXME. We typically get an EPROTO when uevents interleave
322 # with SM ops and a tapdisk shuts down under our feet. Should
323 # be fixed in SM.
325 try:
326 return list(cls.__list( ** args))
328 except cls.CommandFailure as e:
329 transient = [errno.EPROTO, errno.ENOENT]
330 if e.has_status and e.status in transient:
331 raise RetryLoop.TransientFailure(e)
332 raise
334 @classmethod
335 def allocate(cls, devpath=None):
336 args = ["allocate"]
337 args += cls._maybe("-d", devpath)
338 return cls._pread(args)
340 @classmethod
341 def free(cls, minor):
342 args = ["free", "-m", minor]
343 cls._pread(args)
345 @classmethod
346 @retried(backoff=.5, limit=10)
347 def spawn(cls):
348 args = ["spawn"]
349 try:
350 pid = cls._pread(args)
351 return int(pid)
352 except cls.CommandFailure as ce:
353 # intermittent failures to spawn. CA-292268
354 if ce.status == 1:
355 raise RetryLoop.TransientFailure(ce)
356 raise
358 @classmethod
359 def attach(cls, pid, minor):
360 args = ["attach", "-p", pid, "-m", minor]
361 cls._pread(args)
363 @classmethod
364 def detach(cls, pid, minor):
365 args = ["detach", "-p", pid, "-m", minor]
366 cls._pread(args)
368 @classmethod
369 def _load_key(cls, key_hash, vdi_uuid):
370 import plugins
372 return plugins.load_key(key_hash, vdi_uuid)
374 @classmethod
375 def open(cls, pid, minor, _type, _file, options):
376 params = Tapdisk.Arg(_type, _file)
377 args = ["open", "-p", pid, "-m", minor, '-a', str(params)]
378 text_mode = True
379 input = None
380 if options.get("rdonly"):
381 args.append('-R')
382 if options.get("lcache"):
383 args.append("-r")
384 if options.get("existing_prt") is not None:
385 args.append("-e")
386 args.append(str(options["existing_prt"]))
387 if options.get("secondary"):
388 args.append("-2")
389 args.append(options["secondary"])
390 if options.get("standby"):
391 args.append("-s")
392 if options.get("timeout"):
393 args.append("-t")
394 args.append(str(options["timeout"]))
395 if not options.get("o_direct", True):
396 args.append("-D")
397 if options.get('cbtlog'):
398 args.extend(['-C', options['cbtlog']])
399 if options.get('key_hash'):
400 key_hash = options['key_hash']
401 vdi_uuid = options['vdi_uuid']
402 key = cls._load_key(key_hash, vdi_uuid)
404 if not key:
405 raise util.SMException("No key found with key hash {}".format(key_hash))
406 input = key
407 text_mode = False
408 args.append('-E')
410 cls._pread(args=args, input=input, text_mode=text_mode)
412 @classmethod
413 def close(cls, pid, minor, force=False):
414 args = ["close", "-p", pid, "-m", minor, "-t", "120"]
415 if force:
416 args += ["-f"]
417 cls._pread(args)
419 @classmethod
420 def pause(cls, pid, minor):
421 args = ["pause", "-p", pid, "-m", minor]
422 cls._pread(args)
424 @classmethod
425 def unpause(cls, pid, minor, _type=None, _file=None, mirror=None,
426 cbtlog=None):
427 args = ["unpause", "-p", pid, "-m", minor]
428 if mirror:
429 args.extend(["-2", mirror])
430 if _type and _file:
431 params = Tapdisk.Arg(_type, _file)
432 args += ["-a", str(params)]
433 if cbtlog:
434 args.extend(["-c", cbtlog])
436 @retried(backoff=.5, limit=3)
437 def unpause_impl():
438 drbd_path = _file
439 try:
440 cls._pread(args)
441 except TapCtl.CommandFailure as e:
442 error_code = e.get_error_code()
443 if error_code == errno.ENODATA:
444 raise RetryLoop.TransientFailure(e)
445 if error_code not in (errno.EROFS, errno.EMEDIUMTYPE):
446 raise
447 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", ""))
448 if match and match.group("reason"):
449 drbd_path = match.group("reason")
450 if Tapdisk.abort_linstor_gc(drbd_path):
451 raise RetryLoop.TransientFailure(e)
452 raise
454 unpause_impl()
456 @classmethod
457 def shutdown(cls, pid):
458 # TODO: This should be a real tap-ctl command
459 os.kill(pid, signal.SIGTERM)
460 os.waitpid(pid, 0)
462 @classmethod
463 def stats(cls, pid, minor):
464 args = ["stats", "-p", pid, "-m", minor]
465 return cls._pread(args, quiet=True)
467 @classmethod
468 def major(cls):
469 args = ["major"]
470 major = cls._pread(args)
471 return int(major)
473 @classmethod
474 def commit(cls, pid, minor, vdi_type, path):
475 args = ["commit", "-p", pid, "-m", minor, "-a", path]
476 cls._pread(args)
478 @classmethod
479 def query(cls, pid, minor, quiet=False):
480 args = ["query", "-p", pid, "-m", minor]
481 output = cls._pread(args, quiet=quiet)
482 m = re.match(r"Commit status '(.+)' \((\d+)\/(\d+)\)", output)
483 status = m.group(1)
484 coalesced = int(m.group(2))
485 total_coalesce = int(m.group(3))
486 return (status, coalesced, total_coalesce)
488 @classmethod
489 def cancel_commit(cls, pid, minor, wait=True):
490 args = ["cancel", "-p", pid, "-m", minor]
491 if wait:
492 args.append("-w")
493 cls._pread(args)
495class TapdiskExists(Exception):
496 """Tapdisk already running."""
498 def __init__(self, tapdisk):
499 self.tapdisk = tapdisk
501 @override
502 def __str__(self) -> str:
503 return "%s already running" % self.tapdisk
506class TapdiskNotRunning(Exception):
507 """No such Tapdisk."""
509 def __init__(self, **attrs):
510 self.attrs = attrs
512 @override
513 def __str__(self) -> str:
514 items = iter(self.attrs.items())
515 attrs = ", ".join("%s=%s" % attr
516 for attr in items)
517 return "No such Tapdisk(%s)" % attrs
520class TapdiskNotUnique(Exception):
521 """More than one tapdisk on one path."""
523 def __init__(self, tapdisks):
524 self.tapdisks = tapdisks
526 @override
527 def __str__(self) -> str:
528 tapdisks = map(str, self.tapdisks)
529 return "Found multiple tapdisks: %s" % tapdisks
532class TapdiskFailed(Exception):
533 """Tapdisk launch failure."""
535 def __init__(self, arg, err):
536 self.arg = arg
537 self.err = err
539 @override
540 def __str__(self) -> str:
541 return "Tapdisk(%s): %s" % (self.arg, self.err)
543 def get_error(self):
544 return self.err
547class TapdiskInvalidState(Exception):
548 """Tapdisk pause/unpause failure"""
550 def __init__(self, tapdisk):
551 self.tapdisk = tapdisk
553 @override
554 def __str__(self) -> str:
555 return str(self.tapdisk)
558def mkdirs(path, mode=0o777):
559 if not os.path.exists(path):
560 parent, subdir = os.path.split(path)
561 assert parent != path
562 try:
563 if parent:
564 mkdirs(parent, mode)
565 if subdir:
566 os.mkdir(path, mode)
567 except OSError as e:
568 if e.errno != errno.EEXIST:
569 raise
572class KObject(object):
574 SYSFS_CLASSTYPE: ClassVar[str] = ""
576 @abstractmethod
577 def sysfs_devname(self) -> str:
578 pass
581class Attribute(object):
583 SYSFS_NODENAME: ClassVar[str] = ""
585 def __init__(self, path):
586 self.path = path
588 @classmethod
589 def from_kobject(cls, kobj):
590 path = "%s/%s" % (kobj.sysfs_path(), cls.SYSFS_NODENAME)
591 return cls(path)
593 class NoSuchAttribute(Exception):
594 def __init__(self, name):
595 self.name = name
597 @override
598 def __str__(self) -> str:
599 return "No such attribute: %s" % self.name
601 def _open(self, mode='r'):
602 try:
603 return open(self.path, mode)
604 except IOError as e:
605 if e.errno == errno.ENOENT:
606 raise self.NoSuchAttribute(self)
607 raise
609 def readline(self):
610 f = self._open('r')
611 s = f.readline().rstrip()
612 f.close()
613 return s
615 def writeline(self, val):
616 f = self._open('w')
617 f.write(val)
618 f.close()
621class ClassDevice(KObject):
623 @classmethod
624 def sysfs_class_path(cls):
625 return "/sys/class/%s" % cls.SYSFS_CLASSTYPE
627 def sysfs_path(self):
628 return "%s/%s" % (self.sysfs_class_path(),
629 self.sysfs_devname())
632class Blktap(ClassDevice):
634 DEV_BASEDIR = '/dev/xen/blktap-2'
636 SYSFS_CLASSTYPE = "blktap2"
638 def __init__(self, minor):
639 self.minor = minor
640 self._pool = None
641 self._task = None
643 @classmethod
644 def allocate(cls):
645 # FIXME. Should rather go into init.
646 mkdirs(cls.DEV_BASEDIR)
648 devname = TapCtl.allocate()
649 minor = Tapdisk._parse_minor(devname)
650 return cls(minor)
652 def free(self):
653 TapCtl.free(self.minor)
655 @override
656 def __str__(self) -> str:
657 return "%s(minor=%d)" % (self.__class__.__name__, self.minor)
659 @override
660 def sysfs_devname(self) -> str:
661 return "blktap!blktap%d" % self.minor
663 class Pool(Attribute):
664 SYSFS_NODENAME = "pool"
666 def get_pool_attr(self):
667 if not self._pool:
668 self._pool = self.Pool.from_kobject(self)
669 return self._pool
671 def get_pool_name(self):
672 return self.get_pool_attr().readline()
674 def set_pool_name(self, name):
675 self.get_pool_attr().writeline(name)
677 def set_pool_size(self, pages):
678 self.get_pool().set_size(pages)
680 def get_pool(self):
681 return BlktapControl.get_pool(self.get_pool_name())
683 def set_pool(self, pool):
684 self.set_pool_name(pool.name)
686 class Task(Attribute):
687 SYSFS_NODENAME = "task"
689 def get_task_attr(self):
690 if not self._task:
691 self._task = self.Task.from_kobject(self)
692 return self._task
694 def get_task_pid(self):
695 pid = self.get_task_attr().readline()
696 try:
697 return int(pid)
698 except ValueError:
699 return None
701 def find_tapdisk(self):
702 pid = self.get_task_pid()
703 if pid is None:
704 return None
706 return Tapdisk.find(pid=pid, minor=self.minor)
708 def get_tapdisk(self):
709 tapdisk = self.find_tapdisk()
710 if not tapdisk:
711 raise TapdiskNotRunning(minor=self.minor)
712 return tapdisk
715class Tapdisk(object):
717 TYPES = ['aio', 'vhd', 'qcow2']
719 def __init__(self, pid, minor, _type, path, state):
720 self.pid = pid
721 self.minor = minor
722 self.type = _type
723 self.path = path
724 self.state = state
725 self._dirty = False
726 self._blktap = None
728 @override
729 def __str__(self) -> str:
730 state = self.pause_state()
731 return "Tapdisk(%s, pid=%d, minor=%s, state=%s)" % \
732 (self.get_arg(), self.pid, self.minor, state)
734 @classmethod
735 def list(cls, **args):
737 for row in TapCtl.list( ** args):
739 args = {'pid': None,
740 'minor': None,
741 'state': None,
742 '_type': None,
743 'path': None}
745 for key, val in row.items():
746 if key in args:
747 args[key] = val
749 if 'args' in row: 749 ↛ 754line 749 didn't jump to line 754, because the condition on line 749 was never false
750 image = Tapdisk.Arg.parse(row['args'])
751 args['_type'] = image.type
752 args['path'] = image.path
754 if None in args.values(): 754 ↛ 755line 754 didn't jump to line 755, because the condition on line 754 was never true
755 continue
757 yield Tapdisk( ** args)
759 @classmethod
760 def find(cls, **args):
762 found = list(cls.list( ** args))
764 if len(found) > 1: 764 ↛ 765line 764 didn't jump to line 765, because the condition on line 764 was never true
765 raise TapdiskNotUnique(found)
767 if found:
768 return found[0]
770 return None
772 @classmethod
773 def find_by_path(cls, path):
774 return cls.find(path=path)
776 @classmethod
777 def find_by_minor(cls, minor):
778 return cls.find(minor=minor)
780 @classmethod
781 def get(cls, **attrs):
783 tapdisk = cls.find( ** attrs)
785 if not tapdisk:
786 raise TapdiskNotRunning( ** attrs)
788 return tapdisk
790 @classmethod
791 def from_path(cls, path):
792 return cls.get(path=path)
794 @classmethod
795 def path_opened(cls, pid: str, path: str) -> bool:
796 pid_fd_path = os.path.join('/proc', pid, 'fd')
797 try:
798 fd_info = os.listdir(pid_fd_path)
799 for fd in fd_info:
800 try:
801 if os.readlink(os.path.join(pid_fd_path, fd)) == path:
802 return True
803 except FileNotFoundError:
804 # fd might be closed between calls
805 pass
806 except FileNotFoundError:
807 # Process might exit before we get here
808 pass
810 return False
812 @classmethod
813 def check_process_is_tapdisk(cls, pid: str) -> bool:
814 try:
815 return (os.path.exists(os.path.join('/proc/', pid, 'exe')) and os.readlink(
816 os.path.join('/proc/', pid, 'exe')) == '/usr/libexec/tapdisk')
817 except FileNotFoundError:
818 # process might exit between calls
819 pass
821 return False
823 @classmethod
824 def get_pid_for_path(cls, path: str) -> Optional[str]:
825 PID_MATCHER = re.compile(r'^\d+$')
826 dir = os.listdir('/proc')
828 for pid in [x for x in dir if PID_MATCHER.match(x) and
829 cls.check_process_is_tapdisk(x)]:
830 if cls.path_opened(pid, path):
831 return pid
833 return None
835 @classmethod
836 def from_minor(cls, minor):
837 pid = None
838 dev_path = os.path.join(Blktap.DEV_BASEDIR, f"blktap{minor}")
839 if os.path.exists(dev_path): 839 ↛ 842line 839 didn't jump to line 842, because the condition on line 839 was never false
840 pid = cls.get_pid_for_path(dev_path)
842 return cls.get(minor=minor, pid=pid)
844 @classmethod
845 def __from_blktap(cls, blktap):
846 tapdisk = cls.from_minor(minor=blktap.minor)
847 tapdisk._blktap = blktap
848 return tapdisk
850 def get_blktap(self):
851 if not self._blktap:
852 self._blktap = Blktap(self.minor)
853 return self._blktap
855 class Arg:
857 def __init__(self, _type, path):
858 self.type = _type
859 self.path = path
861 @override
862 def __str__(self) -> str:
863 return "%s:%s" % (self.type, self.path)
865 @classmethod
866 def parse(cls, arg):
868 try:
869 _type, path = arg.split(":", 1)
870 except ValueError:
871 raise cls.InvalidArgument(arg)
873 if _type not in Tapdisk.TYPES: 873 ↛ 874line 873 didn't jump to line 874, because the condition on line 873 was never true
874 raise cls.InvalidType(_type)
876 return cls(_type, path)
878 class InvalidType(Exception):
879 def __init__(self, _type):
880 self.type = _type
882 @override
883 def __str__(self) -> str:
884 return "Not a Tapdisk type: %s" % self.type
886 class InvalidArgument(Exception):
887 def __init__(self, arg):
888 self.arg = arg
890 @override
891 def __str__(self) -> str:
892 return "Not a Tapdisk image: %s" % self.arg
894 def get_arg(self):
895 return self.Arg(self.type, self.path)
897 def get_devpath(self):
898 return "%s/tapdev%d" % (Blktap.DEV_BASEDIR, self.minor)
900 @classmethod
901 def launch_from_arg(cls, arg):
902 arg = cls.Arg.parse(arg)
903 return cls.launch(arg.path, arg.type, False)
905 @staticmethod
906 def cgclassify(pid):
908 # We dont provide any <controllers>:<path>
909 # so cgclassify uses /etc/cgrules.conf which
910 # we have configured in the spec file.
911 cmd = ["cgclassify", str(pid)]
912 try:
913 util.pread2(cmd)
914 except util.CommandException as e:
915 util.logException(e)
917 @staticmethod
918 def abort_linstor_gc(drbd_path: str) -> bool:
919 if not LINSTOR_AVAILABLE or not drbd_path.startswith("/dev/drbd/by-res/xcp-volume-"):
920 return False
922 _, volume_name, _ = drbd_path.rsplit("/", 2)
923 group_name = LinstorVolumeManager.get_volume_group_name(volume_name)
925 openers = get_all_volume_openers(volume_name, "0")
927 api_session = util.timeout(5, util.ApiSession, "SM-blktap2-abort-linstor-gc")
928 try:
929 srs = util.get_linstor_srs_uuid(api_session.session)
930 pbd_ref = util.find_pbd_ref_from_dconf_value(
931 api_session.session, srs, "group-name", group_name,
932 LinstorVolumeManager.build_group_name,
933 )
934 if pbd_ref:
935 pbd_rec = api_session.session.xenapi.PBD.get_record(pbd_ref)
937 sr_ref = pbd_rec["SR"]
938 sr_uuid = api_session.session.xenapi.SR.get_uuid(sr_ref)
940 import cleanup # pylint: disable=C0415
941 if cleanup.LinstorSR.abort_gc_from_openers_sr(sr_uuid, openers):
942 return True
943 else:
944 util.SMlog(f"Unable to find PBD of LINSTOR group `{group_name}`...")
946 util.SMlog(f"Unable to run tapdisk, openers of DRBD resource `{drbd_path}`: {openers}")
947 finally:
948 api_session.logout()
950 return False
952 @classmethod
953 def launch_on_tap(cls, blktap, path, _type, options):
954 drbd_path = path
955 tapdisk = cls.find_by_path(path)
956 if tapdisk: 956 ↛ 957line 956 didn't jump to line 957, because the condition on line 956 was never true
957 raise TapdiskExists(tapdisk)
959 minor = blktap.minor
960 try:
961 pid = TapCtl.spawn()
962 cls.cgclassify(pid)
963 try:
964 TapCtl.attach(pid, minor)
966 try:
967 retry_open = 0
968 while True:
969 try:
970 TapCtl.open(pid, minor, _type, path, options)
971 break
972 except TapCtl.CommandFailure as e:
973 err = e.get_error_code()
974 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", ""))
975 if match and match.group("reason"): 975 ↛ 976line 975 didn't jump to line 976, because the condition on line 975 was never true
976 drbd_path = match.group("reason")
977 if err in (errno.EROFS, errno.EMEDIUMTYPE) and cls.abort_linstor_gc(drbd_path): 977 ↛ 978line 977 didn't jump to line 978, because the condition on line 977 was never true
978 continue
980 if err in (errno.EIO, errno.EAGAIN, errno.EROFS, errno.EMEDIUMTYPE, errno.ENODATA) and retry_open < 1: 980 ↛ 981line 980 didn't jump to line 981, because the condition on line 980 was never true
981 retry_open += 1
982 time.sleep(1)
983 continue
984 raise
985 try:
986 tapdisk = cls.__from_blktap(blktap)
987 node = '/sys/dev/block/%d:%d' % (tapdisk.major(), tapdisk.minor)
988 util.set_scheduler_sysfs_node(node, ['none', 'noop'])
989 return tapdisk
990 except:
991 TapCtl.close(pid, minor)
992 raise
994 except:
995 TapCtl.detach(pid, minor)
996 raise
998 except:
999 try:
1000 TapCtl.shutdown(pid)
1001 except:
1002 # Best effort to shutdown
1003 pass
1004 raise
1006 except TapCtl.CommandFailure as ctl:
1007 util.logException(ctl)
1008 if ((path.startswith('/dev/xapi/cd/') or path.startswith('/dev/sr')) and 1008 ↛ 1012line 1008 didn't jump to line 1012, because the condition on line 1008 was never false
1009 ctl.has_status and ctl.get_error_code() == 123): # ENOMEDIUM (No medium found)
1010 raise xs_errors.XenError('TapdiskDriveEmpty')
1011 else:
1012 raise TapdiskFailed(cls.Arg(_type, path), ctl)
1014 @classmethod
1015 def launch(cls, path, _type, rdonly):
1016 blktap = Blktap.allocate()
1017 try:
1018 return cls.launch_on_tap(blktap, path, _type, {"rdonly": rdonly})
1019 except:
1020 blktap.free()
1021 raise
1023 def shutdown(self, force=False):
1025 TapCtl.close(self.pid, self.minor, force)
1027 TapCtl.detach(self.pid, self.minor)
1029 self.get_blktap().free()
1031 def pause(self):
1033 if not self.is_running():
1034 raise TapdiskInvalidState(self)
1036 TapCtl.pause(self.pid, self.minor)
1038 self._set_dirty()
1040 def unpause(self, _type=None, path=None, mirror=None, cbtlog=None):
1042 if not self.is_paused():
1043 raise TapdiskInvalidState(self)
1045 # FIXME: should the arguments be optional?
1046 if _type is None:
1047 _type = self.type
1048 if path is None:
1049 path = self.path
1051 TapCtl.unpause(self.pid, self.minor, _type, path, mirror=mirror,
1052 cbtlog=cbtlog)
1054 self._set_dirty()
1056 def stats(self):
1057 return json.loads(TapCtl.stats(self.pid, self.minor))
1058 #
1059 # NB. dirty/refresh: reload attributes on next access
1060 #
1062 def _set_dirty(self):
1063 self._dirty = True
1065 def _refresh(self, __get):
1066 t = self.from_minor(__get('minor'))
1067 self.__init__(t.pid, t.minor, t.type, t.path, t.state)
1069 @override
1070 def __getattribute__(self, name) -> Any:
1071 def __get(name):
1072 # NB. avoid(rec(ursion)
1073 return object.__getattribute__(self, name)
1075 if __get('_dirty') and \ 1075 ↛ 1077line 1075 didn't jump to line 1077, because the condition on line 1075 was never true
1076 name in ['minor', 'type', 'path', 'state']:
1077 self._refresh(__get)
1078 self._dirty = False
1080 return __get(name)
1082 class PauseState:
1083 RUNNING = 'R'
1084 PAUSING = 'r'
1085 PAUSED = 'P'
1087 class Flags:
1088 DEAD = 0x0001
1089 CLOSED = 0x0002
1090 QUIESCE_REQUESTED = 0x0004
1091 QUIESCED = 0x0008
1092 PAUSE_REQUESTED = 0x0010
1093 PAUSED = 0x0020
1094 SHUTDOWN_REQUESTED = 0x0040
1095 LOCKING = 0x0080
1096 RETRY_NEEDED = 0x0100
1097 LOG_DROPPED = 0x0200
1099 PAUSE_MASK = PAUSE_REQUESTED | PAUSED
1101 def is_paused(self):
1102 return not not (self.state & self.Flags.PAUSED)
1104 def is_running(self):
1105 return not (self.state & self.Flags.PAUSE_MASK)
1107 def pause_state(self):
1108 if self.state & self.Flags.PAUSED: 1108 ↛ 1109line 1108 didn't jump to line 1109, because the condition on line 1108 was never true
1109 return self.PauseState.PAUSED
1111 if self.state & self.Flags.PAUSE_REQUESTED: 1111 ↛ 1112line 1111 didn't jump to line 1112, because the condition on line 1111 was never true
1112 return self.PauseState.PAUSING
1114 return self.PauseState.RUNNING
1116 @staticmethod
1117 def _parse_minor(devpath):
1118 regex = r'%s/(blktap|tapdev)(\d+)$' % Blktap.DEV_BASEDIR
1119 pattern = re.compile(regex)
1120 groups = pattern.search(devpath)
1121 if not groups:
1122 raise Exception("malformed tap device: '%s' (%s) " % (devpath, regex))
1124 minor = groups.group(2)
1125 return int(minor)
1127 _major = None
1129 @classmethod
1130 def major(cls):
1131 if cls._major:
1132 return cls._major
1134 devices = open("/proc/devices")
1135 for line in devices:
1137 row = line.rstrip().split(' ')
1138 if len(row) != 2:
1139 continue
1141 major, name = row
1142 if name != 'tapdev':
1143 continue
1145 cls._major = int(major)
1146 break
1148 devices.close()
1149 return cls._major
1152class VDI(object):
1153 """SR.vdi driver decorator for blktap2"""
1155 CONF_KEY_ALLOW_CACHING = "vdi_allow_caching"
1156 CONF_KEY_MODE_ON_BOOT = "vdi_on_boot"
1157 CONF_KEY_CACHE_SR = "local_cache_sr"
1158 CONF_KEY_O_DIRECT = "o_direct"
1159 LOCK_CACHE_SETUP = "cachesetup"
1161 ATTACH_DETACH_RETRY_SECS = 120
1163 def __init__(self, uuid, target, driver_info):
1164 self.target = self.TargetDriver(target, driver_info)
1165 self._vdi_uuid = uuid
1166 self._session = target.session
1167 self.xenstore_data = scsiutil.update_XS_SCSIdata(uuid, scsiutil.gen_synthetic_page_data(uuid))
1168 self.__o_direct = None
1169 self.__o_direct_reason = None
1170 self.lock = Lock("vdi", uuid)
1171 self.tap = None
1173 def get_o_direct_capability(self, options):
1174 """Returns True/False based on licensing and caching_params"""
1175 if self.__o_direct is not None: 1175 ↛ 1176line 1175 didn't jump to line 1176, because the condition on line 1175 was never true
1176 return self.__o_direct, self.__o_direct_reason
1178 if util.read_caching_is_restricted(self._session): 1178 ↛ 1179line 1178 didn't jump to line 1179, because the condition on line 1178 was never true
1179 self.__o_direct = True
1180 self.__o_direct_reason = "LICENSE_RESTRICTION"
1181 elif not ((self.target.vdi.sr.handles("nfs") or self.target.vdi.sr.handles("ext") or self.target.vdi.sr.handles("smb"))): 1181 ↛ 1184line 1181 didn't jump to line 1184, because the condition on line 1181 was never false
1182 self.__o_direct = True
1183 self.__o_direct_reason = "SR_NOT_SUPPORTED"
1184 elif options.get("rdonly") and not self.target.vdi.parent:
1185 self.__o_direct = True
1186 self.__o_direct_reason = "RO_WITH_NO_PARENT"
1187 elif options.get(self.CONF_KEY_O_DIRECT):
1188 self.__o_direct = True
1189 self.__o_direct_reason = "SR_OVERRIDE"
1191 if self.__o_direct is None: 1191 ↛ 1192line 1191 didn't jump to line 1192, because the condition on line 1191 was never true
1192 self.__o_direct = False
1193 self.__o_direct_reason = ""
1195 return self.__o_direct, self.__o_direct_reason
1197 @classmethod
1198 def from_cli(cls, uuid):
1199 with util.ApiSession("SM-blktap2") as session:
1200 target = sm.VDI.from_uuid(session, uuid)
1201 driver_info = target.sr.srcmd.driver_info
1202 return cls(uuid, target, driver_info)
1204 @staticmethod
1205 def _tap_type(vdi_type):
1206 """Map a VDI type (e.g. 'raw') to a tapdisk driver type (e.g. 'aio')"""
1207 return {
1208 'raw': 'aio',
1209 'vhd': 'vhd',
1210 'qcow2': 'qcow2',
1211 'iso': 'aio', # for ISO SR
1212 'aio': 'aio', # for LVHD
1213 'file': 'aio',
1214 'phy': 'aio'
1215 }[vdi_type]
1217 def get_tap_type(self):
1218 vdi_type = self.target.get_vdi_type()
1219 return VDI._tap_type(vdi_type)
1221 def get_phy_path(self):
1222 return self.target.get_vdi_path()
1224 class UnexpectedVDIType(Exception):
1226 def __init__(self, vdi_type, target):
1227 self.vdi_type = vdi_type
1228 self.target = target
1230 @override
1231 def __str__(self) -> str:
1232 return \
1233 "Target %s has unexpected VDI type '%s'" % \
1234 (type(self.target), self.vdi_type)
1236 VDI_PLUG_TYPE = {'phy': 'phy', # for NETAPP
1237 'raw': 'phy',
1238 'aio': 'tap', # for LVM raw nodes
1239 'iso': 'tap', # for ISOSR
1240 'file': 'tap',
1241 'vhd': 'tap',
1242 'qcow2': 'tap'}
1244 def tap_wanted(self):
1245 # 1. Let the target vdi_type decide
1247 vdi_type = self.target.get_vdi_type()
1249 try:
1250 plug_type = self.VDI_PLUG_TYPE[vdi_type]
1251 except KeyError:
1252 raise self.UnexpectedVDIType(vdi_type,
1253 self.target.vdi)
1255 if plug_type == 'tap': 1255 ↛ 1257line 1255 didn't jump to line 1257, because the condition on line 1255 was never false
1256 return True
1257 elif self.target.vdi.sr.handles('udev'):
1258 return True
1259 # 2. Otherwise, there may be more reasons
1260 #
1261 # .. TBD
1263 return False
1265 class TargetDriver:
1266 """Safe target driver access."""
1267 # NB. *Must* test caps for optional calls. Some targets
1268 # actually implement some slots, but do not enable them. Just
1269 # try/except would risk breaking compatibility.
1271 def __init__(self, vdi, driver_info):
1272 self.vdi = vdi
1273 self._caps = driver_info['capabilities']
1275 def has_cap(self, cap):
1276 """Determine if target has given capability"""
1277 return cap in self._caps
1279 def attach(self, sr_uuid, vdi_uuid):
1280 #assert self.has_cap("VDI_ATTACH")
1281 return self.vdi.attach(sr_uuid, vdi_uuid)
1283 def detach(self, sr_uuid, vdi_uuid):
1284 #assert self.has_cap("VDI_DETACH")
1285 self.vdi.detach(sr_uuid, vdi_uuid)
1287 def activate(self, sr_uuid, vdi_uuid):
1288 if self.has_cap("VDI_ACTIVATE"):
1289 return self.vdi.activate(sr_uuid, vdi_uuid)
1291 def deactivate(self, sr_uuid, vdi_uuid):
1292 if self.has_cap("VDI_DEACTIVATE"):
1293 self.vdi.deactivate(sr_uuid, vdi_uuid)
1294 #def resize(self, sr_uuid, vdi_uuid, size):
1295 # return self.vdi.resize(sr_uuid, vdi_uuid, size)
1297 def get_vdi_type(self):
1298 _type = self.vdi.vdi_type
1299 if not _type:
1300 raise VDI.UnexpectedVDIType(_type, self.vdi)
1301 return _type
1303 def get_vdi_path(self):
1304 return self.vdi.path
1306 class Link(object):
1307 """Relink a node under a common name"""
1308 # NB. We have to provide the device node path during
1309 # VDI.attach, but currently do not allocate the tapdisk minor
1310 # before VDI.activate. Therefore those link steps where we
1311 # relink existing devices under deterministic path names.
1313 BASEDIR: ClassVar[str] = ""
1315 def _mklink(self, target) -> None:
1316 pass
1318 @abstractmethod
1319 def _equals(self, target) -> bool:
1320 pass
1322 def __init__(self, path):
1323 self._path = path
1325 @classmethod
1326 def from_name(cls, name):
1327 path = "%s/%s" % (cls.BASEDIR, name)
1328 return cls(path)
1330 @classmethod
1331 def from_uuid(cls, sr_uuid, vdi_uuid):
1332 name = "%s/%s" % (sr_uuid, vdi_uuid)
1333 return cls.from_name(name)
1335 def path(self):
1336 return self._path
1338 def stat(self):
1339 return os.stat(self.path())
1341 def mklink(self, target) -> None:
1343 path = self.path()
1344 util.SMlog("%s -> %s" % (self, target))
1346 mkdirs(os.path.dirname(path))
1347 try:
1348 self._mklink(target)
1349 except OSError as e:
1350 # We do unlink during teardown, but have to stay
1351 # idempotent. However, a *wrong* target should never
1352 # be seen.
1353 if e.errno != errno.EEXIST:
1354 raise
1355 assert self._equals(target), "'%s' not equal to '%s'" % (path, target)
1357 def unlink(self):
1358 try:
1359 os.unlink(self.path())
1360 except OSError as e:
1361 if e.errno != errno.ENOENT:
1362 raise
1364 @override
1365 def __str__(self) -> str:
1366 path = self.path()
1367 return "%s(%s)" % (self.__class__.__name__, path)
1369 class SymLink(Link):
1370 """Symlink some file to a common name"""
1372 def readlink(self):
1373 return os.readlink(self.path())
1375 def symlink(self):
1376 return self.path()
1378 @override
1379 def _mklink(self, target) -> None:
1380 os.symlink(target, self.path())
1382 @override
1383 def _equals(self, target) -> bool:
1384 return self.readlink() == target
1386 class DeviceNode(Link):
1387 """Relink a block device node to a common name"""
1389 @classmethod
1390 def _real_stat(cls, target):
1391 """stat() not on @target, but its realpath()"""
1392 _target = os.path.realpath(target)
1393 return os.stat(_target)
1395 @classmethod
1396 def is_block(cls, target):
1397 """Whether @target refers to a block device."""
1398 return S_ISBLK(cls._real_stat(target).st_mode)
1400 @override
1401 def _mklink(self, target) -> None:
1403 st = self._real_stat(target)
1404 if not S_ISBLK(st.st_mode):
1405 raise self.NotABlockDevice(target, st)
1407 # set group read for disk group as well as root
1408 os.mknod(self.path(), st.st_mode | stat.S_IRGRP, st.st_rdev)
1409 os.chown(self.path(), st.st_uid, grp.getgrnam("disk").gr_gid)
1411 @override
1412 def _equals(self, target) -> bool:
1413 target_rdev = self._real_stat(target).st_rdev
1414 return self.stat().st_rdev == target_rdev
1416 def rdev(self):
1417 st = self.stat()
1418 assert S_ISBLK(st.st_mode)
1419 return os.major(st.st_rdev), os.minor(st.st_rdev)
1421 class NotABlockDevice(Exception):
1423 def __init__(self, path, st):
1424 self.path = path
1425 self.st = st
1427 @override
1428 def __str__(self) -> str:
1429 return "%s is not a block device: %s" % (self.path, self.st)
1431 class Hybrid(Link):
1433 def __init__(self, path):
1434 VDI.Link.__init__(self, path)
1435 self._devnode = VDI.DeviceNode(path)
1436 self._symlink = VDI.SymLink(path)
1438 def rdev(self):
1439 st = self.stat()
1440 if S_ISBLK(st.st_mode):
1441 return self._devnode.rdev()
1442 raise self._devnode.NotABlockDevice(self.path(), st)
1444 @override
1445 def mklink(self, target) -> None:
1446 if self._devnode.is_block(target):
1447 self._obj = self._devnode
1448 else:
1449 self._obj = self._symlink
1450 self._obj.mklink(target)
1452 @override
1453 def _equals(self, target) -> bool:
1454 return self._obj._equals(target)
1456 class PhyLink(SymLink):
1457 BASEDIR = "/dev/sm/phy"
1458 # NB. Cannot use DeviceNodes, e.g. FileVDIs aren't bdevs.
1460 class NBDLink(SymLink):
1462 BASEDIR = "/run/blktap-control/nbd"
1464 class BackendLink(Hybrid):
1465 BASEDIR = "/dev/sm/backend"
1466 # NB. Could be SymLinks as well, but saving major,minor pairs in
1467 # Links enables neat state capturing when managing Tapdisks. Note
1468 # that we essentially have a tap-ctl list replacement here. For
1469 # now make it a 'Hybrid'. Likely to collapse into a DeviceNode as
1470 # soon as ISOs are tapdisks.
1472 @staticmethod
1473 def _tap_activate(phy_path, vdi_type, sr_uuid, options, pool_size=None):
1475 tapdisk = Tapdisk.find_by_path(phy_path)
1476 if not tapdisk: 1476 ↛ 1477line 1476 didn't jump to line 1477, because the condition on line 1476 was never true
1477 blktap = Blktap.allocate()
1478 blktap.set_pool_name(sr_uuid)
1479 if pool_size:
1480 blktap.set_pool_size(pool_size)
1482 try:
1483 tapdisk = \
1484 Tapdisk.launch_on_tap(blktap,
1485 phy_path,
1486 VDI._tap_type(vdi_type),
1487 options)
1488 except:
1489 blktap.free()
1490 raise
1491 util.SMlog("tap.activate: Launched %s" % tapdisk)
1493 else:
1494 util.SMlog("tap.activate: Found %s" % tapdisk)
1496 return tapdisk.get_devpath(), tapdisk
1498 @staticmethod
1499 def _tap_deactivate(minor):
1501 try:
1502 tapdisk = Tapdisk.from_minor(minor)
1503 except TapdiskNotRunning as e:
1504 util.SMlog("tap.deactivate: Warning, %s" % e)
1505 # NB. Should not be here unless the agent refcount
1506 # broke. Also, a clean shutdown should not have leaked
1507 # the recorded minor.
1508 else:
1509 tapdisk.shutdown()
1510 util.SMlog("tap.deactivate: Shut down %s" % tapdisk)
1512 @classmethod
1513 def tap_pause(cls, session, sr_uuid, vdi_uuid, failfast=False):
1514 """
1515 Pauses the tapdisk.
1517 session: a XAPI session
1518 sr_uuid: the UUID of the SR on which VDI lives
1519 vdi_uuid: the UUID of the VDI to pause
1520 failfast: controls whether the VDI lock should be acquired in a
1521 non-blocking manner
1522 """
1523 util.SMlog("Pause request for %s" % vdi_uuid)
1524 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1525 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'paused', 'true')
1526 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1527 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1527 ↛ 1528line 1527 didn't jump to line 1528, because the loop on line 1527 never started
1528 host_ref = key[len('host_'):]
1529 util.SMlog("Calling tap-pause on host %s" % host_ref)
1530 if not cls.call_pluginhandler(session, host_ref,
1531 sr_uuid, vdi_uuid, "pause", failfast=failfast):
1532 # Failed to pause node
1533 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1534 return False
1535 return True
1537 @classmethod
1538 def tap_unpause(cls, session, sr_uuid, vdi_uuid, secondary=None,
1539 activate_parents=False):
1540 util.SMlog("Unpause request for %s secondary=%s" % (vdi_uuid, secondary))
1541 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1542 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1543 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1543 ↛ 1544line 1543 didn't jump to line 1544, because the loop on line 1543 never started
1544 host_ref = key[len('host_'):]
1545 util.SMlog("Calling tap-unpause on host %s" % host_ref)
1546 if not cls.call_pluginhandler(session, host_ref,
1547 sr_uuid, vdi_uuid, "unpause", secondary, activate_parents):
1548 # Failed to unpause node
1549 return False
1550 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1551 return True
1553 @classmethod
1554 def tap_refresh(cls, session, sr_uuid, vdi_uuid, activate_parents=False):
1555 util.SMlog("Refresh request for %s" % vdi_uuid)
1556 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1557 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1558 for key in [x for x in sm_config.keys() if x.startswith('host_')]:
1559 host_ref = key[len('host_'):]
1560 util.SMlog("Calling tap-refresh on host %s" % host_ref)
1561 if not cls.call_pluginhandler(session, host_ref,
1562 sr_uuid, vdi_uuid, "refresh", None,
1563 activate_parents=activate_parents):
1564 # Failed to refresh node
1565 return False
1566 return True
1568 @classmethod
1569 def tap_status(cls, session, vdi_uuid):
1570 """Return True if disk is attached, false if it isn't"""
1571 util.SMlog("Disk status request for %s" % vdi_uuid)
1572 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1573 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1574 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1574 ↛ 1575line 1574 didn't jump to line 1575, because the loop on line 1574 never started
1575 return True
1576 return False
1578 @classmethod
1579 def call_pluginhandler(cls, session, host_ref, sr_uuid, vdi_uuid, action,
1580 secondary=None, activate_parents=False, failfast=False):
1581 """Optionally, activate the parent LV before unpausing"""
1582 try:
1583 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi_uuid,
1584 "failfast": str(failfast)}
1585 if secondary:
1586 args["secondary"] = secondary
1587 if activate_parents:
1588 args["activate_parents"] = "true"
1589 ret = session.xenapi.host.call_plugin(
1590 host_ref, PLUGIN_TAP_PAUSE, action,
1591 args)
1592 return ret == "True"
1593 except Exception as e:
1594 util.logException("BLKTAP2:call_pluginhandler %s" % e)
1595 return False
1597 @util.xapi_safe_call
1598 def _call_xapi_plugin(self, *args):
1599 """
1600 wrapper to retry xenapi.host.call_plugin on XAPI HTTP failure.
1601 """
1602 return self._session.xenapi.host.call_plugin(*args)
1604 def _add_tag(self, vdi_uuid, writable):
1605 util.SMlog("Adding tag to: %s" % vdi_uuid)
1606 attach_mode = "RO"
1607 if writable:
1608 attach_mode = "RW"
1609 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1610 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host())
1611 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1612 attached_as = util.attached_as(sm_config)
1613 if NO_MULTIPLE_ATTACH and (attached_as == "RW" or \ 1613 ↛ 1615line 1613 didn't jump to line 1615, because the condition on line 1613 was never true
1614 (attached_as == "RO" and attach_mode == "RW")):
1615 util.SMlog("need to reset VDI %s" % vdi_uuid)
1616 if not resetvdis.reset_vdi(self._session, vdi_uuid, force=False,
1617 term_output=False, writable=writable):
1618 raise util.SMException("VDI %s not detached cleanly" % vdi_uuid)
1619 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1620 if 'relinking' in sm_config:
1621 util.SMlog("Relinking key found, back-off and retry" % sm_config)
1622 return False
1623 if 'paused' in sm_config:
1624 util.SMlog("Paused or host_ref key found [%s]" % sm_config)
1625 return False
1626 try:
1627 self._session.xenapi.VDI.add_to_sm_config(
1628 vdi_ref, 'activating', 'True')
1629 except XenAPI.Failure as e:
1630 if e.details[0] == 'MAP_DUPLICATE_KEY' and not writable:
1631 # Someone else is activating - a retry might succeed
1632 return False
1633 raise
1634 host_key = "host_%s" % host_ref
1635 assert host_key not in sm_config
1636 self._session.xenapi.VDI.add_to_sm_config(vdi_ref, host_key,
1637 attach_mode)
1638 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1639 if 'paused' in sm_config or 'relinking' in sm_config:
1640 util.SMlog("Found %s key, aborting" % (
1641 'paused' if 'paused' in sm_config else 'relinking'))
1642 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key)
1643 self._session.xenapi.VDI.remove_from_sm_config(
1644 vdi_ref, 'activating')
1645 return False
1646 util.SMlog("Activate lock succeeded")
1647 return True
1649 @util.xapi_safe_call
1650 def _check_tag(self, vdi_uuid):
1651 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1652 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1653 if 'paused' in sm_config:
1654 util.SMlog("Paused key found [%s]" % sm_config)
1655 return False
1656 return True
1658 @util.xapi_safe_call
1659 def _remove_tag(self, vdi_uuid):
1660 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1661 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host())
1662 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1663 host_key = "host_%s" % host_ref
1664 if host_key in sm_config:
1665 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key)
1666 util.SMlog("Removed host key %s for %s" % (host_key, vdi_uuid))
1667 else:
1668 util.SMlog("_remove_tag: host key %s not found, ignore" % host_key)
1670 @util.xapi_safe_call
1671 def _get_pool_config(self, pool_name):
1672 pool_info = {}
1673 vdi_ref = self.target.vdi.sr.srcmd.params.get('vdi_ref')
1674 if not vdi_ref: 1674 ↛ 1677line 1674 didn't jump to line 1677, because the condition on line 1674 was never true
1675 # attach_from_config context: HA disks don't need to be in any
1676 # special pool
1677 return pool_info
1679 sr_ref = self.target.vdi.sr.srcmd.params.get('sr_ref')
1680 sr_config = self._session.xenapi.SR.get_other_config(sr_ref)
1681 vdi_config = self._session.xenapi.VDI.get_other_config(vdi_ref)
1682 pool_size_str = sr_config.get(POOL_SIZE_KEY)
1683 pool_name_override = vdi_config.get(POOL_NAME_KEY)
1684 if pool_name_override: 1684 ↛ 1689line 1684 didn't jump to line 1689, because the condition on line 1684 was never false
1685 pool_name = pool_name_override
1686 pool_size_override = vdi_config.get(POOL_SIZE_KEY)
1687 if pool_size_override: 1687 ↛ 1689line 1687 didn't jump to line 1689, because the condition on line 1687 was never false
1688 pool_size_str = pool_size_override
1689 pool_size = 0
1690 if pool_size_str: 1690 ↛ 1700line 1690 didn't jump to line 1700, because the condition on line 1690 was never false
1691 try:
1692 pool_size = int(pool_size_str)
1693 if pool_size < 1 or pool_size > MAX_FULL_RINGS: 1693 ↛ 1694line 1693 didn't jump to line 1694, because the condition on line 1693 was never true
1694 raise ValueError("outside of range")
1695 pool_size = NUM_PAGES_PER_RING * pool_size
1696 except ValueError:
1697 util.SMlog("Error: invalid mem-pool-size %s" % pool_size_str)
1698 pool_size = 0
1700 pool_info["mem-pool"] = pool_name
1701 if pool_size: 1701 ↛ 1704line 1701 didn't jump to line 1704, because the condition on line 1701 was never false
1702 pool_info["mem-pool-size"] = str(pool_size)
1704 return pool_info
1706 def linkNBD(self, sr_uuid, vdi_uuid):
1707 if self.tap:
1708 nbd_path = '/run/blktap-control/nbd%d.%d' % (int(self.tap.pid),
1709 int(self.tap.minor))
1710 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).mklink(nbd_path)
1712 def attach(self, sr_uuid, vdi_uuid, writable, activate=False, caching_params={}):
1713 """Return/dev/sm/backend symlink path"""
1714 self.xenstore_data.update(self._get_pool_config(sr_uuid))
1715 if not self.target.has_cap("ATOMIC_PAUSE") or activate:
1716 util.SMlog("Attach & activate")
1717 self._attach(sr_uuid, vdi_uuid)
1718 dev_path = self._activate(sr_uuid, vdi_uuid,
1719 {"rdonly": not writable})
1720 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path)
1721 self.linkNBD(sr_uuid, vdi_uuid)
1723 # Return backend/ link
1724 back_path = self.BackendLink.from_uuid(sr_uuid, vdi_uuid).path()
1725 if self.tap_wanted():
1726 # Only have NBD if we also have a tap
1727 nbd_path = "nbd:unix:{}:exportname={}".format(
1728 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).path(),
1729 vdi_uuid)
1730 else:
1731 nbd_path = ""
1733 options = {"rdonly": not writable}
1734 options.update(caching_params)
1735 o_direct, o_direct_reason = self.get_o_direct_capability(options)
1736 struct = {'params': back_path,
1737 'params_nbd': nbd_path,
1738 'o_direct': o_direct,
1739 'o_direct_reason': o_direct_reason,
1740 'xenstore_data': self.xenstore_data}
1741 util.SMlog('result: %s' % struct)
1743 try:
1744 f = open("%s.attach_info" % back_path, 'a')
1745 f.write(xmlrpc.client.dumps((struct, ), "", True))
1746 f.close()
1747 except:
1748 pass
1750 return xmlrpc.client.dumps((struct, ), "", True)
1752 def activate(self, sr_uuid, vdi_uuid, writable, caching_params):
1753 util.SMlog("blktap2.activate")
1754 options = {"rdonly": not writable}
1755 options.update(caching_params)
1757 for _ in range(self.ATTACH_DETACH_RETRY_SECS): 1757 ↛ 1764line 1757 didn't jump to line 1764, because the loop on line 1757 didn't complete
1758 try:
1759 if self._activate_locked(sr_uuid, vdi_uuid, options):
1760 return
1761 except util.SRBusyException:
1762 util.SMlog("SR locked, retrying")
1763 time.sleep(1)
1764 raise util.SMException("VDI %s locked" % vdi_uuid)
1766 def _get_sr_master_host_ref(self) -> str:
1767 """
1768 Give the host ref of the one responsible for Garbage Collection for a SR.
1769 Meaning this host for a local SR, the master for a shared SR.
1770 """
1771 sr = self.target.vdi.sr
1772 if sr.is_shared():
1773 host_ref = util.get_master_ref(self._session)
1774 else:
1775 host_ref = sr.host_ref
1776 return host_ref
1778 def _get_vdi_chain(self, cowutil, extractUuid) -> List[str]:
1779 vdi_chain = []
1780 path = self.target.get_vdi_path()
1782 #TODO(XCPNG-3500): Need to add handling of error for getParentNoCheck, e.g. corrupted VDI where we can't read parent
1783 vdi_chain.append(extractUuid(path))
1784 parent = cowutil.getParentNoCheck(path)
1785 while parent:
1786 vdi_chain.append(extractUuid(parent))
1787 parent = cowutil.getParentNoCheck(parent)
1788 vdi_chain.reverse()
1789 return vdi_chain
1791 def _check_journal_coalesce_chain(self, sr_uuid: str, vdi_uuid: str) -> bool:
1792 vdi_type = self.target.get_vdi_type()
1793 cowutil = getCowUtil(vdi_type)
1795 if not cowutil.isCoalesceableOnRemote(): #We only need to stop the coalesce in case of QCOW2
1796 return True
1798 path = self.target.get_vdi_path()
1800 import fjournaler
1801 import journaler
1802 from lvmcowutil import LvmCowUtil
1803 from FileSR import FileVDI
1804 import lvmcache
1806 journal: Union[journaler.Journaler, fjournaler.Journaler]
1807 # Different extractUUID & journaler function for LVMSR and FileSR
1808 if path.startswith("/dev/"): #TODO(XCPNG-3501): How to identify SR type easily, we could ask XAPI since we have the sruuid (and even ref)
1809 vgName = "VG_XenStorage-{}".format(sr_uuid)
1810 lvmCache = lvmcache.LVMCache(vgName)
1811 journal = journaler.Journaler(lvmCache)
1813 extractUuid = LvmCowUtil.extractUuid
1814 else:
1815 journal = fjournaler.Journaler(os.getcwd())
1816 extractUuid = FileVDI.extractUuid
1818 # Get the VDI chain
1819 vdi_chain = self._get_vdi_chain(cowutil, extractUuid)
1821 if len(vdi_chain) == 1:
1822 # We only have a leaf, do nothing
1823 util.SMlog("VDI {} is only a leaf, continuing...".format(vdi_uuid))
1824 return True
1826 # Log the chain of active VDI
1827 level = 0
1828 util.SMlog("VDI chain:")
1829 for vdi in vdi_chain:
1830 prefix = " " * level
1831 level += 1
1832 util.SMlog("{}{}".format(prefix, vdi))
1834 vdi_to_cancel = []
1835 for entry in journal.getAll("coalesce").keys():
1836 if entry in vdi_chain:
1837 vdi_to_cancel.append(entry)
1838 util.SMlog("Coalescing VDI {} in chain".format(entry))
1840 # Get the host_ref from the host doing the GC work
1841 host_ref = self._get_sr_master_host_ref()
1842 for vdi in vdi_to_cancel:
1843 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi}
1844 util.SMlog(f"Calling cancel_coalesce_master with args: {args}")
1845 self._call_xapi_plugin(
1846 host_ref, PLUGIN_ON_SLAVE, "cancel_coalesce_master", args)
1848 return True
1850 @locking("VDIUnavailable")
1851 def _activate_locked(self, sr_uuid, vdi_uuid, options):
1852 """Wraps target.activate and adds a tapdisk"""
1854 #util.SMlog("VDI.activate %s" % vdi_uuid)
1855 refresh = False
1856 if self.tap_wanted(): 1856 ↛ 1861line 1856 didn't jump to line 1861, because the condition on line 1856 was never false
1857 if not self._add_tag(vdi_uuid, not options["rdonly"]):
1858 return False
1859 refresh = True
1861 try:
1862 if refresh: 1862 ↛ 1873line 1862 didn't jump to line 1873, because the condition on line 1862 was never false
1863 # it is possible that while the VDI was paused some of its
1864 # attributes have changed (e.g. its size if it was inflated; or its
1865 # path if it was leaf-coalesced onto a raw LV), so refresh the
1866 # object completely
1867 params = self.target.vdi.sr.srcmd.params
1868 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid)
1869 target.sr.srcmd.params = params
1870 driver_info = target.sr.srcmd.driver_info
1871 self.target = self.TargetDriver(target, driver_info)
1873 util.fistpoint.activate_custom_fn( 1873 ↛ exitline 1873 didn't jump to the function exit
1874 "blktap_activate_inject_failure",
1875 lambda: util.inject_failure())
1877 # Attach the physical node
1878 if self.target.has_cap("ATOMIC_PAUSE"): 1878 ↛ 1879line 1878 didn't jump to line 1879, because the condition on line 1878 was never true
1879 self._attach(sr_uuid, vdi_uuid)
1881 vdi_type = self.target.get_vdi_type()
1883 if not self._check_journal_coalesce_chain(sr_uuid, vdi_uuid): 1883 ↛ 1884line 1883 didn't jump to line 1884, because the condition on line 1883 was never true
1884 return False
1886 # Take lvchange-p Lock before running
1887 # tap-ctl open
1888 # Needed to avoid race with lvchange -p which is
1889 # now taking the same lock
1890 # This is a fix for CA-155766
1891 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1891 ↛ 1894line 1891 didn't jump to line 1894, because the condition on line 1891 was never true
1892 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \
1893 VdiType.isCowImage(vdi_type):
1894 lock = Lock("lvchange-p", NS_PREFIX_LVM + sr_uuid)
1895 lock.acquire()
1897 # When we attach a static VDI for HA, we cannot communicate with
1898 # xapi, because has not started yet. These VDIs are raw.
1899 if VdiType.isCowImage(vdi_type): 1899 ↛ 1900line 1899 didn't jump to line 1900, because the condition on line 1899 was never true
1900 session = self.target.vdi.session
1901 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1902 # pylint: disable=used-before-assignment
1903 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1904 if 'key_hash' in sm_config:
1905 key_hash = sm_config['key_hash']
1906 options['key_hash'] = key_hash
1907 options['vdi_uuid'] = vdi_uuid
1908 util.SMlog('Using key with hash {} for VDI {}'.format(key_hash, vdi_uuid))
1909 # Activate the physical node
1910 dev_path = self._activate(sr_uuid, vdi_uuid, options)
1912 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1912 ↛ 1915line 1912 didn't jump to line 1915, because the condition on line 1912 was never true
1913 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \
1914 VdiType.isCowImage(self.target.get_vdi_type()):
1915 lock.release()
1916 except:
1917 util.SMlog("Exception in activate/attach")
1918 if self.tap_wanted():
1919 util.fistpoint.activate_custom_fn(
1920 "blktap_activate_error_handling",
1921 lambda: time.sleep(30))
1922 self._remove_tag(vdi_uuid)
1923 raise
1924 finally:
1925 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1926 self._session.xenapi.VDI.remove_from_sm_config(
1927 vdi_ref, 'activating')
1928 util.SMlog("Removed activating flag from %s" % vdi_uuid) 1928 ↛ exitline 1928 didn't except from function '_activate_locked', because the raise on line 1923 wasn't executed or line 1928 didn't return from function '_activate_locked', because the return on line 1884 wasn't executed
1930 # Link result to backend/
1931 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path)
1932 self.linkNBD(sr_uuid, vdi_uuid)
1933 return True
1935 def _activate(self, sr_uuid, vdi_uuid, options):
1936 vdi_options = self.target.activate(sr_uuid, vdi_uuid)
1938 dev_path = self.setup_cache(sr_uuid, vdi_uuid, options)
1939 if not dev_path: 1939 ↛ 1953line 1939 didn't jump to line 1953, because the condition on line 1939 was never false
1940 phy_path = self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink()
1941 # Maybe launch a tapdisk on the physical link
1942 if self.tap_wanted(): 1942 ↛ 1951line 1942 didn't jump to line 1951, because the condition on line 1942 was never false
1943 vdi_type = self.target.get_vdi_type()
1944 options["o_direct"] = self.get_o_direct_capability(options)[0]
1945 if vdi_options: 1945 ↛ 1947line 1945 didn't jump to line 1947, because the condition on line 1945 was never false
1946 options.update(vdi_options)
1947 dev_path, self.tap = self._tap_activate(phy_path, vdi_type,
1948 sr_uuid, options,
1949 self._get_pool_config(sr_uuid).get("mem-pool-size"))
1950 else:
1951 dev_path = phy_path # Just reuse phy
1953 return dev_path
1955 def _attach(self, sr_uuid, vdi_uuid):
1956 attach_info = xmlrpc.client.loads(self.target.attach(sr_uuid, vdi_uuid))[0][0]
1957 params = attach_info['params']
1958 xenstore_data = attach_info['xenstore_data']
1959 phy_path = util.to_plain_string(params)
1960 self.xenstore_data.update(xenstore_data)
1961 # Save it to phy/
1962 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(phy_path)
1964 def deactivate(self, sr_uuid, vdi_uuid, caching_params):
1965 util.SMlog("blktap2.deactivate")
1966 for i in range(self.ATTACH_DETACH_RETRY_SECS):
1967 try:
1968 if self._deactivate_locked(sr_uuid, vdi_uuid, caching_params):
1969 return
1970 except util.SRBusyException as e:
1971 util.SMlog("SR locked, retrying")
1972 time.sleep(1)
1973 raise util.SMException("VDI %s locked" % vdi_uuid)
1975 @locking("VDIUnavailable")
1976 def _deactivate_locked(self, sr_uuid, vdi_uuid, caching_params):
1977 """Wraps target.deactivate and removes a tapdisk"""
1979 #util.SMlog("VDI.deactivate %s" % vdi_uuid)
1980 if self.tap_wanted() and not self._check_tag(vdi_uuid):
1981 return False
1983 self._deactivate(sr_uuid, vdi_uuid, caching_params)
1984 if self.target.has_cap("ATOMIC_PAUSE"):
1985 self._detach(sr_uuid, vdi_uuid)
1986 if self.tap_wanted():
1987 self._remove_tag(vdi_uuid)
1989 return True
1991 def _resetPhylink(self, sr_uuid, vdi_uuid, path):
1992 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(path)
1994 def detach(self, sr_uuid, vdi_uuid, deactivate=False, caching_params={}):
1995 if not self.target.has_cap("ATOMIC_PAUSE") or deactivate:
1996 util.SMlog("Deactivate & detach")
1997 self._deactivate(sr_uuid, vdi_uuid, caching_params)
1998 self._detach(sr_uuid, vdi_uuid)
1999 else:
2000 pass # nothing to do
2002 def _deactivate(self, sr_uuid, vdi_uuid, caching_params):
2003 # Shutdown tapdisk
2004 back_link = self.BackendLink.from_uuid(sr_uuid, vdi_uuid)
2006 if not util.pathexists(back_link.path()):
2007 util.SMlog("Backend path %s does not exist" % back_link.path())
2008 return
2010 try:
2011 attach_info_path = "%s.attach_info" % (back_link.path())
2012 os.unlink(attach_info_path)
2013 except:
2014 util.SMlog("unlink of attach_info failed")
2016 try:
2017 major, minor = back_link.rdev()
2018 except self.DeviceNode.NotABlockDevice:
2019 pass
2020 else:
2021 if major == Tapdisk.major():
2022 self._tap_deactivate(minor)
2023 self.remove_cache(caching_params)
2025 # Remove the backend link
2026 back_link.unlink()
2027 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).unlink()
2029 # Deactivate & detach the physical node
2030 if self.tap_wanted() and self.target.vdi.session is not None:
2031 # it is possible that while the VDI was paused some of its
2032 # attributes have changed (e.g. its size if it was inflated; or its
2033 # path if it was leaf-coalesced onto a raw LV), so refresh the
2034 # object completely
2035 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid)
2036 driver_info = target.sr.srcmd.driver_info
2037 self.target = self.TargetDriver(target, driver_info)
2039 self.target.deactivate(sr_uuid, vdi_uuid)
2041 def _detach(self, sr_uuid, vdi_uuid):
2042 self.target.detach(sr_uuid, vdi_uuid)
2044 # Remove phy/
2045 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).unlink()
2047 def _updateCacheRecord(self, session, vdi_uuid, on_boot, caching):
2048 # Remove existing VDI.sm_config fields
2049 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
2050 for key in ["on_boot", "caching"]:
2051 session.xenapi.VDI.remove_from_sm_config(vdi_ref, key)
2052 if not on_boot is None: 2052 ↛ 2053line 2052 didn't jump to line 2053, because the condition on line 2052 was never true
2053 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'on_boot', on_boot)
2054 if not caching is None:
2055 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'caching', caching)
2057 def setup_cache(self, sr_uuid, vdi_uuid, params):
2058 if params.get(self.CONF_KEY_ALLOW_CACHING) != "true":
2059 return
2061 util.SMlog("Requested local caching")
2062 if not self.target.has_cap("SR_CACHING"):
2063 util.SMlog("Error: local caching not supported by this SR")
2064 return
2066 scratch_mode = False
2067 if params.get(self.CONF_KEY_MODE_ON_BOOT) == "reset":
2068 scratch_mode = True
2069 util.SMlog("Requested scratch mode")
2070 if not self.target.has_cap("VDI_RESET_ON_BOOT/2"): 2070 ↛ 2074line 2070 didn't jump to line 2074, because the condition on line 2070 was never false
2071 util.SMlog("Error: scratch mode not supported by this SR")
2072 return
2074 dev_path = None
2075 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR)
2076 if not local_sr_uuid:
2077 util.SMlog("ERROR: Local cache SR not specified, not enabling")
2078 return
2079 dev_path = self._setup_cache(self._session, sr_uuid, vdi_uuid,
2080 local_sr_uuid, scratch_mode, params)
2082 if dev_path:
2083 self._updateCacheRecord(self._session, self.target.vdi.uuid,
2084 params.get(self.CONF_KEY_MODE_ON_BOOT),
2085 params.get(self.CONF_KEY_ALLOW_CACHING))
2087 return dev_path
2089 def alert_no_cache(self, session, vdi_uuid, cache_sr_uuid, err):
2090 vm_uuid = None
2091 vm_label = ""
2092 try:
2093 cache_sr_ref = session.xenapi.SR.get_by_uuid(cache_sr_uuid)
2094 cache_sr_rec = session.xenapi.SR.get_record(cache_sr_ref)
2095 cache_sr_label = cache_sr_rec.get("name_label")
2097 host_ref = session.xenapi.host.get_by_uuid(util.get_this_host())
2098 host_rec = session.xenapi.host.get_record(host_ref)
2099 host_label = host_rec.get("name_label")
2101 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
2102 vbds = session.xenapi.VBD.get_all_records_where( \
2103 "field \"VDI\" = \"%s\"" % vdi_ref)
2104 for vbd_rec in vbds.values():
2105 vm_ref = vbd_rec.get("VM")
2106 vm_rec = session.xenapi.VM.get_record(vm_ref)
2107 vm_uuid = vm_rec.get("uuid")
2108 vm_label = vm_rec.get("name_label")
2109 except:
2110 util.logException("alert_no_cache")
2112 alert_obj = "SR"
2113 alert_uuid = str(cache_sr_uuid)
2114 alert_str = "No space left in Local Cache SR %s" % cache_sr_uuid
2115 if vm_uuid:
2116 alert_obj = "VM"
2117 alert_uuid = vm_uuid
2118 reason = ""
2119 if err == errno.ENOSPC:
2120 reason = "because there is no space left"
2121 alert_str = "The VM \"%s\" is not using IntelliCache %s on the Local Cache SR (\"%s\") on host \"%s\"" % \
2122 (vm_label, reason, cache_sr_label, host_label)
2124 util.SMlog("Creating alert: (%s, %s, \"%s\")" % \
2125 (alert_obj, alert_uuid, alert_str))
2126 session.xenapi.message.create("No space left in local cache", "3",
2127 alert_obj, alert_uuid, alert_str)
2129 def _setup_cache(self, session, sr_uuid, vdi_uuid, local_sr_uuid,
2130 scratch_mode, options):
2131 import SR
2132 import EXTSR
2134 if self._no_parent(self.target.vdi): 2134 ↛ 2135line 2134 didn't jump to line 2135, because the condition on line 2134 was never true
2135 util.SMlog("ERROR: VDI %s has no parent, not enabling" %
2136 self.target.vdi.uuid)
2137 return
2139 util.SMlog("Setting up cache")
2140 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent)
2142 if shared_target.parent:
2143 util.SMlog("ERROR: Parent VDI %s has parent, not enabling" %
2144 shared_target.uuid)
2145 return
2147 SR.registerSR(EXTSR.EXTSR)
2148 local_sr = SR.SR.from_uuid(session, local_sr_uuid)
2150 vdi_type = self.target.get_vdi_type()
2151 tap_type = VDI._tap_type(vdi_type)
2152 cowutil = getCowUtil(vdi_type)
2154 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid)
2155 lock.acquire()
2157 # read cache
2158 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid)
2159 if util.pathexists(read_cache_path): 2159 ↛ 2163line 2159 didn't jump to line 2163, because the condition on line 2159 was never false
2160 util.SMlog("Read cache node (%s) already exists, not creating" %
2161 read_cache_path)
2162 else:
2163 try:
2164 cowutil.snapshot(read_cache_path, shared_target.path, False)
2165 except util.CommandException as e:
2166 util.SMlog("Error creating parent cache: %s" % e)
2167 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code)
2168 return None
2170 # local write node
2171 leaf_size = cowutil.getSizeVirt(self.target.vdi.path)
2172 local_leaf_path = "%s/%s.vhdcache" % \
2173 (local_sr.path, self.target.vdi.uuid)
2174 if util.pathexists(local_leaf_path): 2174 ↛ 2178line 2174 didn't jump to line 2178, because the condition on line 2174 was never false
2175 util.SMlog("Local leaf node (%s) already exists, deleting" %
2176 local_leaf_path)
2177 os.unlink(local_leaf_path)
2178 try:
2179 cowutil.snapshot(local_leaf_path, read_cache_path, False,
2180 msize=leaf_size, checkEmpty=False)
2181 except util.CommandException as e:
2182 util.SMlog("Error creating leaf cache: %s" % e)
2183 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code)
2184 return None
2186 local_leaf_size = cowutil.getSizeVirt(local_leaf_path)
2187 if leaf_size > local_leaf_size: 2187 ↛ 2188line 2187 didn't jump to line 2188, because the condition on line 2187 was never true
2188 util.SMlog("Leaf size %d > local leaf cache size %d, resizing" %
2189 (leaf_size, local_leaf_size))
2190 cowutil.setSizeVirtFast(local_leaf_path, leaf_size)
2192 prt_tapdisk = Tapdisk.find_by_path(read_cache_path)
2193 if not prt_tapdisk:
2194 parent_options = copy.deepcopy(options)
2195 parent_options["rdonly"] = False
2196 parent_options["lcache"] = True
2198 blktap = Blktap.allocate()
2199 try:
2200 blktap.set_pool_name("lcache-parent-pool-%s" % blktap.minor)
2201 # no need to change pool_size since each parent tapdisk is in
2202 # its own pool
2203 prt_tapdisk = Tapdisk.launch_on_tap(blktap, read_cache_path, tap_type, parent_options)
2204 except:
2205 blktap.free()
2206 raise
2208 secondary = "%s:%s" % (vdi_type, self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink())
2210 util.SMlog("Parent tapdisk: %s" % prt_tapdisk)
2211 leaf_tapdisk = Tapdisk.find_by_path(local_leaf_path)
2212 if not leaf_tapdisk: 2212 ↛ 2228line 2212 didn't jump to line 2228, because the condition on line 2212 was never false
2213 blktap = Blktap.allocate()
2214 child_options = copy.deepcopy(options)
2215 child_options["rdonly"] = False
2216 child_options["lcache"] = (not scratch_mode)
2217 child_options["existing_prt"] = prt_tapdisk.minor
2218 child_options["secondary"] = secondary
2219 child_options["standby"] = scratch_mode
2220 # Disable memory read caching
2221 child_options.pop("o_direct", None)
2222 try:
2223 leaf_tapdisk = Tapdisk.launch_on_tap(blktap, local_leaf_path, tap_type, child_options)
2224 except:
2225 blktap.free()
2226 raise
2228 lock.release()
2230 util.SMlog("Local read cache: %s, local leaf: %s" %
2231 (read_cache_path, local_leaf_path))
2233 self.tap = leaf_tapdisk
2234 return leaf_tapdisk.get_devpath()
2236 def remove_cache(self, params):
2237 if not self.target.has_cap("SR_CACHING"):
2238 return
2240 caching = params.get(self.CONF_KEY_ALLOW_CACHING) == "true"
2242 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR)
2243 if caching and not local_sr_uuid:
2244 util.SMlog("ERROR: Local cache SR not specified, ignore")
2245 return
2247 if caching: 2247 ↛ 2250line 2247 didn't jump to line 2250, because the condition on line 2247 was never false
2248 self._remove_cache(self._session, local_sr_uuid)
2250 if self._session is not None: 2250 ↛ exitline 2250 didn't return from function 'remove_cache', because the condition on line 2250 was never false
2251 self._updateCacheRecord(self._session, self.target.vdi.uuid, None, None)
2253 def _is_tapdisk_in_use(self, minor):
2254 retVal, links, sockets = util.findRunningProcessOrOpenFile("tapdisk")
2255 if not retVal:
2256 # err on the side of caution
2257 return True
2259 for link in links:
2260 if link.find("tapdev%d" % minor) != -1:
2261 return True
2263 socket_re = re.compile(r'^/.*/nbd\d+\.%d' % minor)
2264 for s in sockets:
2265 if socket_re.match(s):
2266 return True
2268 return False
2270 def _remove_cache(self, session, local_sr_uuid):
2271 import SR
2272 import EXTSR
2274 if self._no_parent(self.target.vdi):
2275 util.SMlog("ERROR: No parent for VDI %s, ignore" %
2276 self.target.vdi.uuid)
2277 return
2279 util.SMlog("Tearing down the cache")
2281 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent)
2283 SR.registerSR(EXTSR.EXTSR)
2284 local_sr = SR.SR.from_uuid(session, local_sr_uuid)
2286 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid)
2287 lock.acquire()
2289 # local write node
2290 local_leaf_path = "%s/%s.vhdcache" % \
2291 (local_sr.path, self.target.vdi.uuid)
2292 if util.pathexists(local_leaf_path): 2292 ↛ 2296line 2292 didn't jump to line 2296, because the condition on line 2292 was never false
2293 util.SMlog("Deleting local leaf node %s" % local_leaf_path)
2294 os.unlink(local_leaf_path)
2296 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid)
2297 prt_tapdisk = Tapdisk.find_by_path(read_cache_path)
2298 if not prt_tapdisk: 2298 ↛ 2299line 2298 didn't jump to line 2299, because the condition on line 2298 was never true
2299 util.SMlog("Parent tapdisk not found")
2300 elif not self._is_tapdisk_in_use(prt_tapdisk.minor): 2300 ↛ 2308line 2300 didn't jump to line 2308, because the condition on line 2300 was never false
2301 util.SMlog("Parent tapdisk not in use: shutting down %s" %
2302 read_cache_path)
2303 try:
2304 prt_tapdisk.shutdown()
2305 except:
2306 util.logException("shutting down parent tapdisk")
2307 else:
2308 util.SMlog("Parent tapdisk still in use: %s" % read_cache_path)
2309 # the parent cache files are removed during the local SR's background
2310 # GC run
2312 lock.release()
2314 @staticmethod
2315 def _no_parent(vdi):
2316 return vdi.parent is None or vdi.parent == ''
2319PythonKeyError = KeyError
2322class UEventHandler(object):
2324 def __init__(self):
2325 self._action = None
2327 class KeyError(PythonKeyError):
2328 def __init__(self, args):
2329 super().__init__(args)
2330 self.key = args[0]
2332 @override
2333 def __str__(self) -> str:
2334 return \
2335 "Key '%s' missing in environment. " % self.key + \
2336 "Not called in udev context?"
2338 @classmethod
2339 def getenv(cls, key):
2340 try:
2341 return os.environ[key]
2342 except KeyError as e:
2343 raise cls.KeyError(e.args[0])
2345 def get_action(self):
2346 if not self._action:
2347 self._action = self.getenv('ACTION')
2348 return self._action
2350 class UnhandledEvent(Exception):
2352 def __init__(self, event, handler):
2353 self.event = event
2354 self.handler = handler
2356 @override
2357 def __str__(self) -> str:
2358 return "Uevent '%s' not handled by %s" % \
2359 (self.event, self.handler.__class__.__name__)
2361 ACTIONS: Dict[str, Callable] = {}
2363 def run(self):
2365 action = self.get_action()
2366 try:
2367 fn = self.ACTIONS[action]
2368 except KeyError:
2369 raise self.UnhandledEvent(action, self)
2371 return fn(self)
2373 @override
2374 def __str__(self) -> str:
2375 try:
2376 action = self.get_action()
2377 except:
2378 action = None
2379 return "%s[%s]" % (self.__class__.__name__, action)
2382class __BlktapControl(ClassDevice):
2383 SYSFS_CLASSTYPE = "misc"
2385 def __init__(self):
2386 ClassDevice.__init__(self)
2387 self._default_pool = None
2389 @override
2390 def sysfs_devname(self) -> str:
2391 return "blktap!control"
2393 class DefaultPool(Attribute):
2394 SYSFS_NODENAME = "default_pool"
2396 def get_default_pool_attr(self):
2397 if not self._default_pool:
2398 self._default_pool = self.DefaultPool.from_kobject(self)
2399 return self._default_pool
2401 def get_default_pool_name(self):
2402 return self.get_default_pool_attr().readline()
2404 def set_default_pool_name(self, name):
2405 self.get_default_pool_attr().writeline(name)
2407 def get_default_pool(self):
2408 return BlktapControl.get_pool(self.get_default_pool_name())
2410 def set_default_pool(self, pool):
2411 self.set_default_pool_name(pool.name)
2413 class NoSuchPool(Exception):
2414 def __init__(self, name):
2415 self.name = name
2417 @override
2418 def __str__(self) -> str:
2419 return "No such pool: {}".format(self.name)
2421 def get_pool(self, name):
2422 path = "%s/pools/%s" % (self.sysfs_path(), name)
2424 if not os.path.isdir(path):
2425 raise self.NoSuchPool(name)
2427 return PagePool(path)
2429BlktapControl = __BlktapControl()
2432class PagePool(KObject):
2434 def __init__(self, path):
2435 self.path = path
2436 self._size = None
2438 @override
2439 def sysfs_devname(self) -> str:
2440 return ''
2442 def sysfs_path(self):
2443 return self.path
2445 class Size(Attribute):
2446 SYSFS_NODENAME = "size"
2448 def get_size_attr(self):
2449 if not self._size:
2450 self._size = self.Size.from_kobject(self)
2451 return self._size
2453 def set_size(self, pages):
2454 pages = str(pages)
2455 self.get_size_attr().writeline(pages)
2457 def get_size(self):
2458 pages = self.get_size_attr().readline()
2459 return int(pages)
2462class BusDevice(KObject):
2464 SYSFS_BUSTYPE: ClassVar[str] = ""
2466 @classmethod
2467 def sysfs_bus_path(cls):
2468 return "/sys/bus/%s" % cls.SYSFS_BUSTYPE
2470 def sysfs_path(self):
2471 path = "%s/devices/%s" % (self.sysfs_bus_path(),
2472 self.sysfs_devname())
2474 return path
2477class XenbusDevice(BusDevice):
2478 """Xenbus device, in XS and sysfs"""
2480 XBT_NIL = ""
2482 XENBUS_DEVTYPE: ClassVar[str] = ""
2484 def __init__(self, domid, devid):
2485 self.domid = int(domid)
2486 self.devid = int(devid)
2487 self._xbt = XenbusDevice.XBT_NIL
2489 import xen.lowlevel.xs # pylint: disable=import-error
2490 self.xs = xen.lowlevel.xs.xs()
2492 def xs_path(self, key=None):
2493 path = "backend/%s/%d/%d" % (self.XENBUS_DEVTYPE,
2494 self.domid,
2495 self.devid)
2496 if key is not None:
2497 path = "%s/%s" % (path, key)
2499 return path
2501 def _log(self, prio, msg):
2502 syslog(prio, msg)
2504 def info(self, msg):
2505 self._log(_syslog.LOG_INFO, msg)
2507 def warn(self, msg):
2508 self._log(_syslog.LOG_WARNING, "WARNING: " + msg)
2510 def _xs_read_path(self, path):
2511 val = self.xs.read(self._xbt, path)
2512 #self.info("read %s = '%s'" % (path, val))
2513 return val
2515 def _xs_write_path(self, path, val):
2516 self.xs.write(self._xbt, path, val)
2517 self.info("wrote %s = '%s'" % (path, val))
2519 def _xs_rm_path(self, path):
2520 self.xs.rm(self._xbt, path)
2521 self.info("removed %s" % path)
2523 def read(self, key):
2524 return self._xs_read_path(self.xs_path(key))
2526 def has_xs_key(self, key):
2527 return self.read(key) is not None
2529 def write(self, key, val):
2530 self._xs_write_path(self.xs_path(key), val)
2532 def rm(self, key):
2533 self._xs_rm_path(self.xs_path(key))
2535 def exists(self):
2536 return self.has_xs_key(None)
2538 def begin(self):
2539 assert(self._xbt == XenbusDevice.XBT_NIL)
2540 self._xbt = self.xs.transaction_start()
2542 def commit(self):
2543 ok = self.xs.transaction_end(self._xbt, 0)
2544 self._xbt = XenbusDevice.XBT_NIL
2545 return ok
2547 def abort(self):
2548 ok = self.xs.transaction_end(self._xbt, 1)
2549 assert(ok == True)
2550 self._xbt = XenbusDevice.XBT_NIL
2552 def create_physical_device(self):
2553 """The standard protocol is: toolstack writes 'params', linux hotplug
2554 script translates this into physical-device=%x:%x"""
2555 if self.has_xs_key("physical-device"):
2556 return
2557 try:
2558 params = self.read("params")
2559 frontend = self.read("frontend")
2560 is_cdrom = self._xs_read_path("%s/device-type") == "cdrom"
2561 # We don't have PV drivers for CDROM devices, so we prevent blkback
2562 # from opening the physical-device
2563 if not(is_cdrom):
2564 major_minor = os.stat(params).st_rdev
2565 major, minor = divmod(major_minor, 256)
2566 self.write("physical-device", "%x:%x" % (major, minor))
2567 except:
2568 util.logException("BLKTAP2:create_physical_device")
2570 def signal_hotplug(self, online=True):
2571 xapi_path = "/xapi/%d/hotplug/%s/%d/hotplug" % (self.domid,
2572 self.XENBUS_DEVTYPE,
2573 self.devid)
2574 upstream_path = self.xs_path("hotplug-status")
2575 if online:
2576 self._xs_write_path(xapi_path, "online")
2577 self._xs_write_path(upstream_path, "connected")
2578 else:
2579 self._xs_rm_path(xapi_path)
2580 self._xs_rm_path(upstream_path)
2582 @override
2583 def sysfs_devname(self) -> str:
2584 return "%s-%d-%d" % (self.XENBUS_DEVTYPE,
2585 self.domid, self.devid)
2587 @override
2588 def __str__(self) -> str:
2589 return self.sysfs_devname()
2591 @classmethod
2592 def find(cls):
2593 pattern = "/sys/bus/%s/devices/%s*" % (cls.SYSFS_BUSTYPE,
2594 cls.XENBUS_DEVTYPE)
2595 for path in glob.glob(pattern):
2597 name = os.path.basename(path)
2598 (_type, domid, devid) = name.split('-')
2600 yield cls(domid, devid)
2603class XenBackendDevice(XenbusDevice):
2604 """Xenbus backend device"""
2605 SYSFS_BUSTYPE = "xen-backend"
2607 @classmethod
2608 def from_xs_path(cls, _path):
2609 (_backend, _type, domid, devid) = _path.split('/')
2611 assert _backend == 'backend'
2612 assert _type == cls.XENBUS_DEVTYPE
2614 domid = int(domid)
2615 devid = int(devid)
2617 return cls(domid, devid)
2620class Blkback(XenBackendDevice):
2621 """A blkback VBD"""
2623 XENBUS_DEVTYPE = "vbd"
2625 def __init__(self, domid, devid):
2626 XenBackendDevice.__init__(self, domid, devid)
2627 self._phy = None
2628 self._vdi_uuid = None
2629 self._q_state = None
2630 self._q_events = None
2632 class XenstoreValueError(Exception):
2633 KEY: ClassVar[str] = ""
2635 def __init__(self, vbd, _str):
2636 self.vbd = vbd
2637 self.str = _str
2639 @override
2640 def __str__(self) -> str:
2641 return "Backend %s " % self.vbd + \
2642 "has %s = %s" % (self.KEY, self.str)
2644 class PhysicalDeviceError(XenstoreValueError):
2645 KEY = "physical-device"
2647 class PhysicalDevice(object):
2649 def __init__(self, major, minor):
2650 self.major = int(major)
2651 self.minor = int(minor)
2653 @classmethod
2654 def from_xbdev(cls, xbdev):
2656 phy = xbdev.read("physical-device")
2658 try:
2659 major, minor = phy.split(':')
2660 major = int(major, 0x10)
2661 minor = int(minor, 0x10)
2662 except Exception as e:
2663 raise xbdev.PhysicalDeviceError(xbdev, phy)
2665 return cls(major, minor)
2667 def makedev(self):
2668 return os.makedev(self.major, self.minor)
2670 def is_tap(self):
2671 return self.major == Tapdisk.major()
2673 @override
2674 def __str__(self) -> str:
2675 return "%s:%s" % (self.major, self.minor)
2677 @override
2678 def __eq__(self, other) -> bool:
2679 return \
2680 self.major == other.major and \
2681 self.minor == other.minor
2683 def get_physical_device(self):
2684 if not self._phy:
2685 self._phy = self.PhysicalDevice.from_xbdev(self)
2686 return self._phy
2688 class QueueEvents(Attribute):
2689 """Blkback sysfs node to select queue-state event
2690 notifications emitted."""
2692 SYSFS_NODENAME = "queue_events"
2694 QUEUE_RUNNING = (1 << 0)
2695 QUEUE_PAUSE_DONE = (1 << 1)
2696 QUEUE_SHUTDOWN_DONE = (1 << 2)
2697 QUEUE_PAUSE_REQUEST = (1 << 3)
2698 QUEUE_SHUTDOWN_REQUEST = (1 << 4)
2700 def get_mask(self):
2701 return int(self.readline(), 0x10)
2703 def set_mask(self, mask):
2704 self.writeline("0x%x" % mask)
2706 def get_queue_events(self):
2707 if not self._q_events:
2708 self._q_events = self.QueueEvents.from_kobject(self)
2709 return self._q_events
2711 def get_vdi_uuid(self):
2712 if not self._vdi_uuid:
2713 self._vdi_uuid = self.read("sm-data/vdi-uuid")
2714 return self._vdi_uuid
2716 def pause_requested(self):
2717 return self.has_xs_key("pause")
2719 def shutdown_requested(self):
2720 return self.has_xs_key("shutdown-request")
2722 def shutdown_done(self):
2723 return self.has_xs_key("shutdown-done")
2725 def running(self):
2726 return self.has_xs_key('queue-0/kthread-pid')
2728 @classmethod
2729 def find_by_physical_device(cls, phy):
2730 for dev in cls.find():
2731 try:
2732 _phy = dev.get_physical_device()
2733 except cls.PhysicalDeviceError:
2734 continue
2736 if _phy == phy:
2737 yield dev
2739 @classmethod
2740 def find_by_tap_minor(cls, minor):
2741 phy = cls.PhysicalDevice(Tapdisk.major(), minor)
2742 return cls.find_by_physical_device(phy)
2744 @classmethod
2745 def find_by_tap(cls, tapdisk):
2746 return cls.find_by_tap_minor(tapdisk.minor)
2748 def has_tap(self):
2750 if not self.can_tap():
2751 return False
2753 phy = self.get_physical_device()
2754 if phy:
2755 return phy.is_tap()
2757 return False
2759 def is_bare_hvm(self):
2760 """File VDIs for bare HVM. These are directly accessible by Qemu."""
2761 try:
2762 self.get_physical_device()
2764 except self.PhysicalDeviceError as e:
2765 vdi_type = self.read("type")
2767 self.info("HVM VDI: type=%s" % vdi_type)
2769 if e.str is not None or vdi_type != 'file':
2770 raise
2772 return True
2774 return False
2776 def can_tap(self):
2777 return not self.is_bare_hvm()
2780class BlkbackEventHandler(UEventHandler):
2782 LOG_FACILITY = _syslog.LOG_DAEMON
2784 def __init__(self, ident=None, action=None):
2785 if not ident:
2786 ident = self.__class__.__name__
2788 self.ident = ident
2789 self._vbd = None
2790 self._tapdisk = None
2792 UEventHandler.__init__(self)
2794 @override
2795 def run(self) -> None:
2797 self.xs_path = self.getenv('XENBUS_PATH')
2798 openlog(str(self), 0, self.LOG_FACILITY)
2800 UEventHandler.run(self)
2802 @override
2803 def __str__(self) -> str:
2805 try:
2806 path = self.xs_path
2807 except:
2808 path = None
2810 try:
2811 action = self.get_action()
2812 except:
2813 action = None
2815 return "%s[%s](%s)" % (self.ident, action, path)
2817 def _log(self, prio, msg):
2818 syslog(prio, msg)
2819 util.SMlog("%s: " % self + msg)
2821 def info(self, msg):
2822 self._log(_syslog.LOG_INFO, msg)
2824 def warn(self, msg):
2825 self._log(_syslog.LOG_WARNING, "WARNING: " + msg)
2827 def error(self, msg):
2828 self._log(_syslog.LOG_ERR, "ERROR: " + msg)
2830 def get_vbd(self):
2831 if not self._vbd:
2832 self._vbd = Blkback.from_xs_path(self.xs_path)
2833 return self._vbd
2835 def get_tapdisk(self):
2836 if not self._tapdisk:
2837 minor = self.get_vbd().get_physical_device().minor
2838 self._tapdisk = Tapdisk.from_minor(minor)
2839 return self._tapdisk
2840 #
2841 # Events
2842 #
2844 def __add(self):
2845 vbd = self.get_vbd()
2846 # Manage blkback transitions
2847 # self._manage_vbd()
2849 vbd.create_physical_device()
2851 vbd.signal_hotplug()
2853 @retried(backoff=.5, limit=10)
2854 def add(self):
2855 try:
2856 self.__add()
2857 except Attribute.NoSuchAttribute as e:
2858 #
2859 # FIXME: KOBJ_ADD is racing backend.probe, which
2860 # registers device attributes. So poll a little.
2861 #
2862 self.warn("%s, still trying." % e)
2863 raise RetryLoop.TransientFailure(e)
2865 def __change(self):
2866 vbd = self.get_vbd()
2868 # 1. Pause or resume tapdisk (if there is one)
2870 if vbd.has_tap():
2871 pass
2872 #self._pause_update_tap()
2874 # 2. Signal Xapi.VBD.pause/resume completion
2876 self._signal_xapi()
2878 def change(self):
2879 vbd = self.get_vbd()
2881 # NB. Beware of spurious change events between shutdown
2882 # completion and device removal. Also, Xapi.VM.migrate will
2883 # hammer a couple extra shutdown-requests into the source VBD.
2885 while True:
2886 vbd.begin()
2888 if not vbd.exists() or \
2889 vbd.shutdown_done():
2890 break
2892 self.__change()
2894 if vbd.commit():
2895 return
2897 vbd.abort()
2898 self.info("spurious uevent, ignored.")
2900 def remove(self):
2901 vbd = self.get_vbd()
2903 vbd.signal_hotplug(False)
2905 ACTIONS = {'add': add,
2906 'change': change,
2907 'remove': remove}
2908 #
2909 # VDI.pause
2910 #
2912 def _tap_should_pause(self):
2913 """Enumerate all VBDs on our tapdisk. Returns true iff any was
2914 paused"""
2916 tapdisk = self.get_tapdisk()
2917 TapState = Tapdisk.PauseState
2919 PAUSED = 'P'
2920 RUNNING = 'R'
2921 PAUSED_SHUTDOWN = 'P,S'
2922 # NB. Shutdown/paused is special. We know it's not going
2923 # to restart again, so it's a RUNNING. Still better than
2924 # backtracking a removed device during Vbd.unplug completion.
2926 next = TapState.RUNNING
2927 vbds = {}
2929 for vbd in Blkback.find_by_tap(tapdisk):
2930 name = str(vbd)
2932 pausing = vbd.pause_requested()
2933 closing = vbd.shutdown_requested()
2934 running = vbd.running()
2936 if pausing:
2937 if closing and not running:
2938 vbds[name] = PAUSED_SHUTDOWN
2939 else:
2940 vbds[name] = PAUSED
2941 next = TapState.PAUSED
2943 else:
2944 vbds[name] = RUNNING
2946 self.info("tapdev%d (%s): %s -> %s"
2947 % (tapdisk.minor, tapdisk.pause_state(),
2948 vbds, next))
2950 return next == TapState.PAUSED
2952 def _pause_update_tap(self):
2953 vbd = self.get_vbd()
2955 if self._tap_should_pause():
2956 self._pause_tap()
2957 else:
2958 self._resume_tap()
2960 def _pause_tap(self):
2961 tapdisk = self.get_tapdisk()
2963 if not tapdisk.is_paused():
2964 self.info("pausing %s" % tapdisk)
2965 tapdisk.pause()
2967 def _resume_tap(self):
2968 tapdisk = self.get_tapdisk()
2970 # NB. Raw VDI snapshots. Refresh the physical path and
2971 # type while resuming.
2972 vbd = self.get_vbd()
2973 vdi_uuid = vbd.get_vdi_uuid()
2975 if tapdisk.is_paused():
2976 self.info("loading vdi uuid=%s" % vdi_uuid)
2977 vdi = VDI.from_cli(vdi_uuid)
2978 _type = vdi.get_tap_type()
2979 path = vdi.get_phy_path()
2980 self.info("resuming %s on %s:%s" % (tapdisk, _type, path))
2981 tapdisk.unpause(_type, path)
2982 #
2983 # VBD.pause/shutdown
2984 #
2986 def _manage_vbd(self):
2987 vbd = self.get_vbd()
2988 # NB. Hook into VBD state transitions.
2990 events = vbd.get_queue_events()
2992 mask = 0
2993 mask |= events.QUEUE_PAUSE_DONE # pause/unpause
2994 mask |= events.QUEUE_SHUTDOWN_DONE # shutdown
2995 # TODO: mask |= events.QUEUE_SHUTDOWN_REQUEST, for shutdown=force
2996 # TODO: mask |= events.QUEUE_RUNNING, for ionice updates etc
2998 events.set_mask(mask)
2999 self.info("wrote %s = %#02x" % (events.path, mask))
3001 def _signal_xapi(self):
3002 vbd = self.get_vbd()
3004 pausing = vbd.pause_requested()
3005 closing = vbd.shutdown_requested()
3006 running = vbd.running()
3008 handled = 0
3010 if pausing and not running:
3011 if 'pause-done' not in vbd:
3012 vbd.write('pause-done', '')
3013 handled += 1
3015 if not pausing:
3016 if 'pause-done' in vbd:
3017 vbd.rm('pause-done')
3018 handled += 1
3020 if closing and not running:
3021 if 'shutdown-done' not in vbd:
3022 vbd.write('shutdown-done', '')
3023 handled += 1
3025 if handled > 1:
3026 self.warn("handled %d events, " % handled +
3027 "pausing=%s closing=%s running=%s" % \
3028 (pausing, closing, running))
3030if __name__ == '__main__': 3030 ↛ 3032line 3030 didn't jump to line 3032, because the condition on line 3030 was never true
3032 import sys
3033 prog = os.path.basename(sys.argv[0])
3035 #
3036 # Simple CLI interface for manual operation
3037 #
3038 # tap.* level calls go down to local Tapdisk()s (by physical path)
3039 # vdi.* level calls run the plugin calls across host boundaries.
3040 #
3042 def usage(stream):
3043 print("usage: %s tap.{list|major}" % prog, file=stream)
3044 print(" %s tap.{launch|find|get|pause|" % prog + \
3045 "unpause|shutdown|stats} {[<tt>:]<path>} | [minor=]<int> | .. }", file=stream)
3046 print(" %s vbd.uevent" % prog, file=stream)
3048 try:
3049 cmd = sys.argv[1]
3050 except IndexError:
3051 usage(sys.stderr)
3052 sys.exit(1)
3054 try:
3055 _class, method = cmd.split('.')
3056 except:
3057 usage(sys.stderr)
3058 sys.exit(1)
3060 #
3061 # Local Tapdisks
3062 #
3064 if cmd == 'tap.major':
3066 print("%d" % Tapdisk.major())
3068 elif cmd == 'tap.launch':
3070 tapdisk = Tapdisk.launch_from_arg(sys.argv[2])
3071 print("Launched %s" % tapdisk, file=sys.stderr)
3073 elif _class == 'tap':
3075 attrs: Dict[str, Any] = {}
3076 for item in sys.argv[2:]:
3077 try:
3078 key, val = item.split('=')
3079 attrs[key] = val
3080 continue
3081 except ValueError:
3082 pass
3084 try:
3085 attrs['minor'] = int(item)
3086 continue
3087 except ValueError:
3088 pass
3090 try:
3091 arg = Tapdisk.Arg.parse(item)
3092 attrs['_type'] = arg.type
3093 attrs['path'] = arg.path
3094 continue
3095 except Tapdisk.Arg.InvalidArgument:
3096 pass
3098 attrs['path'] = item
3100 if cmd == 'tap.list':
3102 for tapdisk in Tapdisk.list( ** attrs):
3103 blktap = tapdisk.get_blktap()
3104 print(tapdisk, end=' ')
3105 print("%s: task=%s pool=%s" % \
3106 (blktap,
3107 blktap.get_task_pid(),
3108 blktap.get_pool_name()))
3110 elif cmd == 'tap.vbds':
3111 # Find all Blkback instances for a given tapdisk
3113 for tapdisk in Tapdisk.list( ** attrs):
3114 print("%s:" % tapdisk, end=' ')
3115 for vbd in Blkback.find_by_tap(tapdisk):
3116 print(vbd, end=' ')
3117 print()
3119 else:
3121 if not attrs:
3122 usage(sys.stderr)
3123 sys.exit(1)
3125 try:
3126 tapdisk = Tapdisk.get( ** attrs)
3127 except TypeError:
3128 usage(sys.stderr)
3129 sys.exit(1)
3131 if cmd == 'tap.shutdown':
3132 # Shutdown a running tapdisk, or raise
3133 tapdisk.shutdown()
3134 print("Shut down %s" % tapdisk, file=sys.stderr)
3136 elif cmd == 'tap.pause':
3137 # Pause an unpaused tapdisk, or raise
3138 tapdisk.pause()
3139 print("Paused %s" % tapdisk, file=sys.stderr)
3141 elif cmd == 'tap.unpause':
3142 # Unpause a paused tapdisk, or raise
3143 tapdisk.unpause()
3144 print("Unpaused %s" % tapdisk, file=sys.stderr)
3146 elif cmd == 'tap.stats':
3147 # Gather tapdisk status
3148 stats = tapdisk.stats()
3149 print("%s:" % tapdisk)
3150 print(json.dumps(stats, indent=True))
3152 else:
3153 usage(sys.stderr)
3154 sys.exit(1)
3156 elif cmd == 'vbd.uevent':
3158 hnd = BlkbackEventHandler(cmd)
3160 if not sys.stdin.isatty():
3161 try:
3162 hnd.run()
3163 except Exception as e:
3164 hnd.error("Unhandled Exception: %s" % e)
3166 import traceback
3167 _type, value, tb = sys.exc_info()
3168 trace = traceback.format_exception(_type, value, tb)
3169 for entry in trace:
3170 for line in entry.rstrip().split('\n'):
3171 util.SMlog(line)
3172 else:
3173 hnd.run()
3175 elif cmd == 'vbd.list':
3177 for vbd in Blkback.find():
3178 print(vbd, \
3179 "physical-device=%s" % vbd.get_physical_device(), \
3180 "pause=%s" % vbd.pause_requested())
3182 else:
3183 usage(sys.stderr)
3184 sys.exit(1)