Coverage for drivers/blktap2.py : 47%
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 http.client
34import errno
35import signal
36import subprocess
37import syslog as _syslog
38import glob
39import json
40import xs_errors
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 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", ""))
443 if match and match.group("reason"):
444 drbd_path = match.group("reason")
445 if e.get_error_code() in (errno.EROFS, errno.EMEDIUMTYPE) and Tapdisk.abort_linstor_gc(drbd_path):
446 raise RetryLoop.TransientFailure(e)
447 raise
449 unpause_impl()
451 @classmethod
452 def shutdown(cls, pid):
453 # TODO: This should be a real tap-ctl command
454 os.kill(pid, signal.SIGTERM)
455 os.waitpid(pid, 0)
457 @classmethod
458 def stats(cls, pid, minor):
459 args = ["stats", "-p", pid, "-m", minor]
460 return cls._pread(args, quiet=True)
462 @classmethod
463 def major(cls):
464 args = ["major"]
465 major = cls._pread(args)
466 return int(major)
468 @classmethod
469 def commit(cls, pid, minor, vdi_type, path):
470 args = ["commit", "-p", pid, "-m", minor, "-a", path]
471 cls._pread(args)
473 @classmethod
474 def query(cls, pid, minor, quiet=False):
475 args = ["query", "-p", pid, "-m", minor]
476 output = cls._pread(args, quiet=quiet)
477 m = re.match(r"Commit status '(.+)' \((\d+)\/(\d+)\)", output)
478 status = m.group(1)
479 coalesced = int(m.group(2))
480 total_coalesce = int(m.group(3))
481 return (status, coalesced, total_coalesce)
483 @classmethod
484 def cancel_commit(cls, pid, minor, wait=True):
485 args = ["cancel", "-p", pid, "-m", minor]
486 if wait:
487 args.append("-w")
488 cls._pread(args)
490class TapdiskExists(Exception):
491 """Tapdisk already running."""
493 def __init__(self, tapdisk):
494 self.tapdisk = tapdisk
496 @override
497 def __str__(self) -> str:
498 return "%s already running" % self.tapdisk
501class TapdiskNotRunning(Exception):
502 """No such Tapdisk."""
504 def __init__(self, **attrs):
505 self.attrs = attrs
507 @override
508 def __str__(self) -> str:
509 items = iter(self.attrs.items())
510 attrs = ", ".join("%s=%s" % attr
511 for attr in items)
512 return "No such Tapdisk(%s)" % attrs
515class TapdiskNotUnique(Exception):
516 """More than one tapdisk on one path."""
518 def __init__(self, tapdisks):
519 self.tapdisks = tapdisks
521 @override
522 def __str__(self) -> str:
523 tapdisks = map(str, self.tapdisks)
524 return "Found multiple tapdisks: %s" % tapdisks
527class TapdiskFailed(Exception):
528 """Tapdisk launch failure."""
530 def __init__(self, arg, err):
531 self.arg = arg
532 self.err = err
534 @override
535 def __str__(self) -> str:
536 return "Tapdisk(%s): %s" % (self.arg, self.err)
538 def get_error(self):
539 return self.err
542class TapdiskInvalidState(Exception):
543 """Tapdisk pause/unpause failure"""
545 def __init__(self, tapdisk):
546 self.tapdisk = tapdisk
548 @override
549 def __str__(self) -> str:
550 return str(self.tapdisk)
553def mkdirs(path, mode=0o777):
554 if not os.path.exists(path):
555 parent, subdir = os.path.split(path)
556 assert parent != path
557 try:
558 if parent:
559 mkdirs(parent, mode)
560 if subdir:
561 os.mkdir(path, mode)
562 except OSError as e:
563 if e.errno != errno.EEXIST:
564 raise
567class KObject(object):
569 SYSFS_CLASSTYPE: ClassVar[str] = ""
571 @abstractmethod
572 def sysfs_devname(self) -> str:
573 pass
576class Attribute(object):
578 SYSFS_NODENAME: ClassVar[str] = ""
580 def __init__(self, path):
581 self.path = path
583 @classmethod
584 def from_kobject(cls, kobj):
585 path = "%s/%s" % (kobj.sysfs_path(), cls.SYSFS_NODENAME)
586 return cls(path)
588 class NoSuchAttribute(Exception):
589 def __init__(self, name):
590 self.name = name
592 @override
593 def __str__(self) -> str:
594 return "No such attribute: %s" % self.name
596 def _open(self, mode='r'):
597 try:
598 return open(self.path, mode)
599 except IOError as e:
600 if e.errno == errno.ENOENT:
601 raise self.NoSuchAttribute(self)
602 raise
604 def readline(self):
605 f = self._open('r')
606 s = f.readline().rstrip()
607 f.close()
608 return s
610 def writeline(self, val):
611 f = self._open('w')
612 f.write(val)
613 f.close()
616class ClassDevice(KObject):
618 @classmethod
619 def sysfs_class_path(cls):
620 return "/sys/class/%s" % cls.SYSFS_CLASSTYPE
622 def sysfs_path(self):
623 return "%s/%s" % (self.sysfs_class_path(),
624 self.sysfs_devname())
627class Blktap(ClassDevice):
629 DEV_BASEDIR = '/dev/xen/blktap-2'
631 SYSFS_CLASSTYPE = "blktap2"
633 def __init__(self, minor):
634 self.minor = minor
635 self._pool = None
636 self._task = None
638 @classmethod
639 def allocate(cls):
640 # FIXME. Should rather go into init.
641 mkdirs(cls.DEV_BASEDIR)
643 devname = TapCtl.allocate()
644 minor = Tapdisk._parse_minor(devname)
645 return cls(minor)
647 def free(self):
648 TapCtl.free(self.minor)
650 @override
651 def __str__(self) -> str:
652 return "%s(minor=%d)" % (self.__class__.__name__, self.minor)
654 @override
655 def sysfs_devname(self) -> str:
656 return "blktap!blktap%d" % self.minor
658 class Pool(Attribute):
659 SYSFS_NODENAME = "pool"
661 def get_pool_attr(self):
662 if not self._pool:
663 self._pool = self.Pool.from_kobject(self)
664 return self._pool
666 def get_pool_name(self):
667 return self.get_pool_attr().readline()
669 def set_pool_name(self, name):
670 self.get_pool_attr().writeline(name)
672 def set_pool_size(self, pages):
673 self.get_pool().set_size(pages)
675 def get_pool(self):
676 return BlktapControl.get_pool(self.get_pool_name())
678 def set_pool(self, pool):
679 self.set_pool_name(pool.name)
681 class Task(Attribute):
682 SYSFS_NODENAME = "task"
684 def get_task_attr(self):
685 if not self._task:
686 self._task = self.Task.from_kobject(self)
687 return self._task
689 def get_task_pid(self):
690 pid = self.get_task_attr().readline()
691 try:
692 return int(pid)
693 except ValueError:
694 return None
696 def find_tapdisk(self):
697 pid = self.get_task_pid()
698 if pid is None:
699 return None
701 return Tapdisk.find(pid=pid, minor=self.minor)
703 def get_tapdisk(self):
704 tapdisk = self.find_tapdisk()
705 if not tapdisk:
706 raise TapdiskNotRunning(minor=self.minor)
707 return tapdisk
710class Tapdisk(object):
712 TYPES = ['aio', 'vhd', 'qcow2']
714 def __init__(self, pid, minor, _type, path, state):
715 self.pid = pid
716 self.minor = minor
717 self.type = _type
718 self.path = path
719 self.state = state
720 self._dirty = False
721 self._blktap = None
723 @override
724 def __str__(self) -> str:
725 state = self.pause_state()
726 return "Tapdisk(%s, pid=%d, minor=%s, state=%s)" % \
727 (self.get_arg(), self.pid, self.minor, state)
729 @classmethod
730 def list(cls, **args):
732 for row in TapCtl.list( ** args):
734 args = {'pid': None,
735 'minor': None,
736 'state': None,
737 '_type': None,
738 'path': None}
740 for key, val in row.items():
741 if key in args:
742 args[key] = val
744 if 'args' in row: 744 ↛ 749line 744 didn't jump to line 749, because the condition on line 744 was never false
745 image = Tapdisk.Arg.parse(row['args'])
746 args['_type'] = image.type
747 args['path'] = image.path
749 if None in args.values(): 749 ↛ 750line 749 didn't jump to line 750, because the condition on line 749 was never true
750 continue
752 yield Tapdisk( ** args)
754 @classmethod
755 def find(cls, **args):
757 found = list(cls.list( ** args))
759 if len(found) > 1: 759 ↛ 760line 759 didn't jump to line 760, because the condition on line 759 was never true
760 raise TapdiskNotUnique(found)
762 if found:
763 return found[0]
765 return None
767 @classmethod
768 def find_by_path(cls, path):
769 return cls.find(path=path)
771 @classmethod
772 def find_by_minor(cls, minor):
773 return cls.find(minor=minor)
775 @classmethod
776 def get(cls, **attrs):
778 tapdisk = cls.find( ** attrs)
780 if not tapdisk: 780 ↛ 781line 780 didn't jump to line 781, because the condition on line 780 was never true
781 raise TapdiskNotRunning( ** attrs)
783 return tapdisk
785 @classmethod
786 def from_path(cls, path):
787 return cls.get(path=path)
789 @classmethod
790 def get_pid_for_path(cls, path: str) -> str:
791 return util.pread2(['/usr/sbin/lsof', '-t', path]).strip()
793 @classmethod
794 def from_minor(cls, minor):
795 pid = None
796 dev_path = os.path.join(Blktap.DEV_BASEDIR, f"blktap{minor}")
797 if os.path.exists(dev_path): 797 ↛ 800line 797 didn't jump to line 800, because the condition on line 797 was never false
798 pid = cls.get_pid_for_path(dev_path)
800 return cls.get(minor=minor, pid=pid)
802 @classmethod
803 def __from_blktap(cls, blktap):
804 tapdisk = cls.from_minor(minor=blktap.minor)
805 tapdisk._blktap = blktap
806 return tapdisk
808 def get_blktap(self):
809 if not self._blktap:
810 self._blktap = Blktap(self.minor)
811 return self._blktap
813 class Arg:
815 def __init__(self, _type, path):
816 self.type = _type
817 self.path = path
819 @override
820 def __str__(self) -> str:
821 return "%s:%s" % (self.type, self.path)
823 @classmethod
824 def parse(cls, arg):
826 try:
827 _type, path = arg.split(":", 1)
828 except ValueError:
829 raise cls.InvalidArgument(arg)
831 if _type not in Tapdisk.TYPES: 831 ↛ 832line 831 didn't jump to line 832, because the condition on line 831 was never true
832 raise cls.InvalidType(_type)
834 return cls(_type, path)
836 class InvalidType(Exception):
837 def __init__(self, _type):
838 self.type = _type
840 @override
841 def __str__(self) -> str:
842 return "Not a Tapdisk type: %s" % self.type
844 class InvalidArgument(Exception):
845 def __init__(self, arg):
846 self.arg = arg
848 @override
849 def __str__(self) -> str:
850 return "Not a Tapdisk image: %s" % self.arg
852 def get_arg(self):
853 return self.Arg(self.type, self.path)
855 def get_devpath(self):
856 return "%s/tapdev%d" % (Blktap.DEV_BASEDIR, self.minor)
858 @classmethod
859 def launch_from_arg(cls, arg):
860 arg = cls.Arg.parse(arg)
861 return cls.launch(arg.path, arg.type, False)
863 @staticmethod
864 def cgclassify(pid):
866 # We dont provide any <controllers>:<path>
867 # so cgclassify uses /etc/cgrules.conf which
868 # we have configured in the spec file.
869 cmd = ["cgclassify", str(pid)]
870 try:
871 util.pread2(cmd)
872 except util.CommandException as e:
873 util.logException(e)
875 @staticmethod
876 def abort_linstor_gc(drbd_path: str) -> bool:
877 if not LINSTOR_AVAILABLE or not drbd_path.startswith("/dev/drbd/by-res/xcp-volume-"):
878 return False
880 _, volume_name, _ = drbd_path.rsplit("/", 2)
881 group_name = LinstorVolumeManager.get_volume_group_name(volume_name)
883 openers = get_all_volume_openers(volume_name, "0")
885 session = util.timeout(5, util.get_localAPI_session)
886 try:
887 srs = util.get_linstor_srs_uuid(session)
888 pbd_ref = util.find_pbd_ref_from_dconf_value(
889 session, srs, "group-name", group_name, LinstorVolumeManager.build_group_name
890 )
891 if pbd_ref:
892 pbd_rec = session.xenapi.PBD.get_record(pbd_ref)
894 sr_ref = pbd_rec["SR"]
895 sr_uuid = session.xenapi.SR.get_uuid(sr_ref)
897 import cleanup # pylint: disable=C0415
898 if cleanup.LinstorSR.abort_gc_from_openers_sr(sr_uuid, openers):
899 return True
900 else:
901 util.SMlog(f"Unable to find PBD of LINSTOR group `{group_name}`...")
903 util.SMlog(f"Unable to run tapdisk, openers of DRBD resource `{drbd_path}`: {openers}")
904 finally:
905 session.xenapi.session.logout()
907 return False
909 @classmethod
910 def launch_on_tap(cls, blktap, path, _type, options):
911 drbd_path = path
912 tapdisk = cls.find_by_path(path)
913 if tapdisk: 913 ↛ 914line 913 didn't jump to line 914, because the condition on line 913 was never true
914 raise TapdiskExists(tapdisk)
916 minor = blktap.minor
917 try:
918 pid = TapCtl.spawn()
919 cls.cgclassify(pid)
920 try:
921 TapCtl.attach(pid, minor)
923 try:
924 retry_open = 0
925 while True:
926 try:
927 TapCtl.open(pid, minor, _type, path, options)
928 break
929 except TapCtl.CommandFailure as e:
930 err = e.get_error_code()
931 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", ""))
932 if match and match.group("reason"): 932 ↛ 933line 932 didn't jump to line 933, because the condition on line 932 was never true
933 drbd_path = match.group("reason")
934 if err in (errno.EROFS, errno.EMEDIUMTYPE) and cls.abort_linstor_gc(drbd_path): 934 ↛ 935line 934 didn't jump to line 935, because the condition on line 934 was never true
935 continue
937 if err in (errno.EIO, errno.EAGAIN, errno.EROFS, errno.EMEDIUMTYPE) and retry_open < 1: 937 ↛ 938line 937 didn't jump to line 938, because the condition on line 937 was never true
938 retry_open += 1
939 time.sleep(1)
940 continue
941 raise
942 try:
943 tapdisk = cls.__from_blktap(blktap)
944 node = '/sys/dev/block/%d:%d' % (tapdisk.major(), tapdisk.minor)
945 util.set_scheduler_sysfs_node(node, ['none', 'noop'])
946 return tapdisk
947 except:
948 TapCtl.close(pid, minor)
949 raise
951 except:
952 TapCtl.detach(pid, minor)
953 raise
955 except:
956 try:
957 TapCtl.shutdown(pid)
958 except:
959 # Best effort to shutdown
960 pass
961 raise
963 except TapCtl.CommandFailure as ctl:
964 util.logException(ctl)
965 if ((path.startswith('/dev/xapi/cd/') or path.startswith('/dev/sr')) and 965 ↛ 969line 965 didn't jump to line 969, because the condition on line 965 was never false
966 ctl.has_status and ctl.get_error_code() == 123): # ENOMEDIUM (No medium found)
967 raise xs_errors.XenError('TapdiskDriveEmpty')
968 else:
969 raise TapdiskFailed(cls.Arg(_type, path), ctl)
971 @classmethod
972 def launch(cls, path, _type, rdonly):
973 blktap = Blktap.allocate()
974 try:
975 return cls.launch_on_tap(blktap, path, _type, {"rdonly": rdonly})
976 except:
977 blktap.free()
978 raise
980 def shutdown(self, force=False):
982 TapCtl.close(self.pid, self.minor, force)
984 TapCtl.detach(self.pid, self.minor)
986 self.get_blktap().free()
988 def pause(self):
990 if not self.is_running():
991 raise TapdiskInvalidState(self)
993 TapCtl.pause(self.pid, self.minor)
995 self._set_dirty()
997 def unpause(self, _type=None, path=None, mirror=None, cbtlog=None):
999 if not self.is_paused():
1000 raise TapdiskInvalidState(self)
1002 # FIXME: should the arguments be optional?
1003 if _type is None:
1004 _type = self.type
1005 if path is None:
1006 path = self.path
1008 TapCtl.unpause(self.pid, self.minor, _type, path, mirror=mirror,
1009 cbtlog=cbtlog)
1011 self._set_dirty()
1013 def stats(self):
1014 return json.loads(TapCtl.stats(self.pid, self.minor))
1015 #
1016 # NB. dirty/refresh: reload attributes on next access
1017 #
1019 def _set_dirty(self):
1020 self._dirty = True
1022 def _refresh(self, __get):
1023 t = self.from_minor(__get('minor'))
1024 self.__init__(t.pid, t.minor, t.type, t.path, t.state)
1026 @override
1027 def __getattribute__(self, name) -> Any:
1028 def __get(name):
1029 # NB. avoid(rec(ursion)
1030 return object.__getattribute__(self, name)
1032 if __get('_dirty') and \ 1032 ↛ 1034line 1032 didn't jump to line 1034, because the condition on line 1032 was never true
1033 name in ['minor', 'type', 'path', 'state']:
1034 self._refresh(__get)
1035 self._dirty = False
1037 return __get(name)
1039 class PauseState:
1040 RUNNING = 'R'
1041 PAUSING = 'r'
1042 PAUSED = 'P'
1044 class Flags:
1045 DEAD = 0x0001
1046 CLOSED = 0x0002
1047 QUIESCE_REQUESTED = 0x0004
1048 QUIESCED = 0x0008
1049 PAUSE_REQUESTED = 0x0010
1050 PAUSED = 0x0020
1051 SHUTDOWN_REQUESTED = 0x0040
1052 LOCKING = 0x0080
1053 RETRY_NEEDED = 0x0100
1054 LOG_DROPPED = 0x0200
1056 PAUSE_MASK = PAUSE_REQUESTED | PAUSED
1058 def is_paused(self):
1059 return not not (self.state & self.Flags.PAUSED)
1061 def is_running(self):
1062 return not (self.state & self.Flags.PAUSE_MASK)
1064 def pause_state(self):
1065 if self.state & self.Flags.PAUSED: 1065 ↛ 1066line 1065 didn't jump to line 1066, because the condition on line 1065 was never true
1066 return self.PauseState.PAUSED
1068 if self.state & self.Flags.PAUSE_REQUESTED: 1068 ↛ 1069line 1068 didn't jump to line 1069, because the condition on line 1068 was never true
1069 return self.PauseState.PAUSING
1071 return self.PauseState.RUNNING
1073 @staticmethod
1074 def _parse_minor(devpath):
1075 regex = r'%s/(blktap|tapdev)(\d+)$' % Blktap.DEV_BASEDIR
1076 pattern = re.compile(regex)
1077 groups = pattern.search(devpath)
1078 if not groups:
1079 raise Exception("malformed tap device: '%s' (%s) " % (devpath, regex))
1081 minor = groups.group(2)
1082 return int(minor)
1084 _major = None
1086 @classmethod
1087 def major(cls):
1088 if cls._major:
1089 return cls._major
1091 devices = open("/proc/devices")
1092 for line in devices:
1094 row = line.rstrip().split(' ')
1095 if len(row) != 2:
1096 continue
1098 major, name = row
1099 if name != 'tapdev':
1100 continue
1102 cls._major = int(major)
1103 break
1105 devices.close()
1106 return cls._major
1109class VDI(object):
1110 """SR.vdi driver decorator for blktap2"""
1112 CONF_KEY_ALLOW_CACHING = "vdi_allow_caching"
1113 CONF_KEY_MODE_ON_BOOT = "vdi_on_boot"
1114 CONF_KEY_CACHE_SR = "local_cache_sr"
1115 CONF_KEY_O_DIRECT = "o_direct"
1116 LOCK_CACHE_SETUP = "cachesetup"
1118 ATTACH_DETACH_RETRY_SECS = 120
1120 def __init__(self, uuid, target, driver_info):
1121 self.target = self.TargetDriver(target, driver_info)
1122 self._vdi_uuid = uuid
1123 self._session = target.session
1124 self.xenstore_data = scsiutil.update_XS_SCSIdata(uuid, scsiutil.gen_synthetic_page_data(uuid))
1125 self.__o_direct = None
1126 self.__o_direct_reason = None
1127 self.lock = Lock("vdi", uuid)
1128 self.tap = None
1130 def get_o_direct_capability(self, options):
1131 """Returns True/False based on licensing and caching_params"""
1132 if self.__o_direct is not None: 1132 ↛ 1133line 1132 didn't jump to line 1133, because the condition on line 1132 was never true
1133 return self.__o_direct, self.__o_direct_reason
1135 if util.read_caching_is_restricted(self._session): 1135 ↛ 1136line 1135 didn't jump to line 1136, because the condition on line 1135 was never true
1136 self.__o_direct = True
1137 self.__o_direct_reason = "LICENSE_RESTRICTION"
1138 elif not ((self.target.vdi.sr.handles("nfs") or self.target.vdi.sr.handles("ext") or self.target.vdi.sr.handles("smb"))): 1138 ↛ 1141line 1138 didn't jump to line 1141, because the condition on line 1138 was never false
1139 self.__o_direct = True
1140 self.__o_direct_reason = "SR_NOT_SUPPORTED"
1141 elif options.get("rdonly") and not self.target.vdi.parent:
1142 self.__o_direct = True
1143 self.__o_direct_reason = "RO_WITH_NO_PARENT"
1144 elif options.get(self.CONF_KEY_O_DIRECT):
1145 self.__o_direct = True
1146 self.__o_direct_reason = "SR_OVERRIDE"
1148 if self.__o_direct is None: 1148 ↛ 1149line 1148 didn't jump to line 1149, because the condition on line 1148 was never true
1149 self.__o_direct = False
1150 self.__o_direct_reason = ""
1152 return self.__o_direct, self.__o_direct_reason
1154 @classmethod
1155 def from_cli(cls, uuid):
1156 session = XenAPI.xapi_local()
1157 session.xenapi.login_with_password('root', '', '', 'SM')
1159 target = sm.VDI.from_uuid(session, uuid)
1160 driver_info = target.sr.srcmd.driver_info
1162 session.xenapi.session.logout()
1164 return cls(uuid, target, driver_info)
1166 @staticmethod
1167 def _tap_type(vdi_type):
1168 """Map a VDI type (e.g. 'raw') to a tapdisk driver type (e.g. 'aio')"""
1169 return {
1170 'raw': 'aio',
1171 'vhd': 'vhd',
1172 'qcow2': 'qcow2',
1173 'iso': 'aio', # for ISO SR
1174 'aio': 'aio', # for LVHD
1175 'file': 'aio',
1176 'phy': 'aio'
1177 }[vdi_type]
1179 def get_tap_type(self):
1180 vdi_type = self.target.get_vdi_type()
1181 return VDI._tap_type(vdi_type)
1183 def get_phy_path(self):
1184 return self.target.get_vdi_path()
1186 class UnexpectedVDIType(Exception):
1188 def __init__(self, vdi_type, target):
1189 self.vdi_type = vdi_type
1190 self.target = target
1192 @override
1193 def __str__(self) -> str:
1194 return \
1195 "Target %s has unexpected VDI type '%s'" % \
1196 (type(self.target), self.vdi_type)
1198 VDI_PLUG_TYPE = {'phy': 'phy', # for NETAPP
1199 'raw': 'phy',
1200 'aio': 'tap', # for LVM raw nodes
1201 'iso': 'tap', # for ISOSR
1202 'file': 'tap',
1203 'vhd': 'tap',
1204 'qcow2': 'tap'}
1206 def tap_wanted(self):
1207 # 1. Let the target vdi_type decide
1209 vdi_type = self.target.get_vdi_type()
1211 try:
1212 plug_type = self.VDI_PLUG_TYPE[vdi_type]
1213 except KeyError:
1214 raise self.UnexpectedVDIType(vdi_type,
1215 self.target.vdi)
1217 if plug_type == 'tap': 1217 ↛ 1219line 1217 didn't jump to line 1219, because the condition on line 1217 was never false
1218 return True
1219 elif self.target.vdi.sr.handles('udev'):
1220 return True
1221 # 2. Otherwise, there may be more reasons
1222 #
1223 # .. TBD
1225 return False
1227 class TargetDriver:
1228 """Safe target driver access."""
1229 # NB. *Must* test caps for optional calls. Some targets
1230 # actually implement some slots, but do not enable them. Just
1231 # try/except would risk breaking compatibility.
1233 def __init__(self, vdi, driver_info):
1234 self.vdi = vdi
1235 self._caps = driver_info['capabilities']
1237 def has_cap(self, cap):
1238 """Determine if target has given capability"""
1239 return cap in self._caps
1241 def attach(self, sr_uuid, vdi_uuid):
1242 #assert self.has_cap("VDI_ATTACH")
1243 return self.vdi.attach(sr_uuid, vdi_uuid)
1245 def detach(self, sr_uuid, vdi_uuid):
1246 #assert self.has_cap("VDI_DETACH")
1247 self.vdi.detach(sr_uuid, vdi_uuid)
1249 def activate(self, sr_uuid, vdi_uuid):
1250 if self.has_cap("VDI_ACTIVATE"):
1251 return self.vdi.activate(sr_uuid, vdi_uuid)
1253 def deactivate(self, sr_uuid, vdi_uuid):
1254 if self.has_cap("VDI_DEACTIVATE"):
1255 self.vdi.deactivate(sr_uuid, vdi_uuid)
1256 #def resize(self, sr_uuid, vdi_uuid, size):
1257 # return self.vdi.resize(sr_uuid, vdi_uuid, size)
1259 def get_vdi_type(self):
1260 _type = self.vdi.vdi_type
1261 if not _type:
1262 raise VDI.UnexpectedVDIType(_type, self.vdi)
1263 return _type
1265 def get_vdi_path(self):
1266 return self.vdi.path
1268 class Link(object):
1269 """Relink a node under a common name"""
1270 # NB. We have to provide the device node path during
1271 # VDI.attach, but currently do not allocate the tapdisk minor
1272 # before VDI.activate. Therefore those link steps where we
1273 # relink existing devices under deterministic path names.
1275 BASEDIR: ClassVar[str] = ""
1277 def _mklink(self, target) -> None:
1278 pass
1280 @abstractmethod
1281 def _equals(self, target) -> bool:
1282 pass
1284 def __init__(self, path):
1285 self._path = path
1287 @classmethod
1288 def from_name(cls, name):
1289 path = "%s/%s" % (cls.BASEDIR, name)
1290 return cls(path)
1292 @classmethod
1293 def from_uuid(cls, sr_uuid, vdi_uuid):
1294 name = "%s/%s" % (sr_uuid, vdi_uuid)
1295 return cls.from_name(name)
1297 def path(self):
1298 return self._path
1300 def stat(self):
1301 return os.stat(self.path())
1303 def mklink(self, target) -> None:
1305 path = self.path()
1306 util.SMlog("%s -> %s" % (self, target))
1308 mkdirs(os.path.dirname(path))
1309 try:
1310 self._mklink(target)
1311 except OSError as e:
1312 # We do unlink during teardown, but have to stay
1313 # idempotent. However, a *wrong* target should never
1314 # be seen.
1315 if e.errno != errno.EEXIST:
1316 raise
1317 assert self._equals(target), "'%s' not equal to '%s'" % (path, target)
1319 def unlink(self):
1320 try:
1321 os.unlink(self.path())
1322 except OSError as e:
1323 if e.errno != errno.ENOENT:
1324 raise
1326 @override
1327 def __str__(self) -> str:
1328 path = self.path()
1329 return "%s(%s)" % (self.__class__.__name__, path)
1331 class SymLink(Link):
1332 """Symlink some file to a common name"""
1334 def readlink(self):
1335 return os.readlink(self.path())
1337 def symlink(self):
1338 return self.path()
1340 @override
1341 def _mklink(self, target) -> None:
1342 os.symlink(target, self.path())
1344 @override
1345 def _equals(self, target) -> bool:
1346 return self.readlink() == target
1348 class DeviceNode(Link):
1349 """Relink a block device node to a common name"""
1351 @classmethod
1352 def _real_stat(cls, target):
1353 """stat() not on @target, but its realpath()"""
1354 _target = os.path.realpath(target)
1355 return os.stat(_target)
1357 @classmethod
1358 def is_block(cls, target):
1359 """Whether @target refers to a block device."""
1360 return S_ISBLK(cls._real_stat(target).st_mode)
1362 @override
1363 def _mklink(self, target) -> None:
1365 st = self._real_stat(target)
1366 if not S_ISBLK(st.st_mode):
1367 raise self.NotABlockDevice(target, st)
1369 # set group read for disk group as well as root
1370 os.mknod(self.path(), st.st_mode | stat.S_IRGRP, st.st_rdev)
1371 os.chown(self.path(), st.st_uid, grp.getgrnam("disk").gr_gid)
1373 @override
1374 def _equals(self, target) -> bool:
1375 target_rdev = self._real_stat(target).st_rdev
1376 return self.stat().st_rdev == target_rdev
1378 def rdev(self):
1379 st = self.stat()
1380 assert S_ISBLK(st.st_mode)
1381 return os.major(st.st_rdev), os.minor(st.st_rdev)
1383 class NotABlockDevice(Exception):
1385 def __init__(self, path, st):
1386 self.path = path
1387 self.st = st
1389 @override
1390 def __str__(self) -> str:
1391 return "%s is not a block device: %s" % (self.path, self.st)
1393 class Hybrid(Link):
1395 def __init__(self, path):
1396 VDI.Link.__init__(self, path)
1397 self._devnode = VDI.DeviceNode(path)
1398 self._symlink = VDI.SymLink(path)
1400 def rdev(self):
1401 st = self.stat()
1402 if S_ISBLK(st.st_mode):
1403 return self._devnode.rdev()
1404 raise self._devnode.NotABlockDevice(self.path(), st)
1406 @override
1407 def mklink(self, target) -> None:
1408 if self._devnode.is_block(target):
1409 self._obj = self._devnode
1410 else:
1411 self._obj = self._symlink
1412 self._obj.mklink(target)
1414 @override
1415 def _equals(self, target) -> bool:
1416 return self._obj._equals(target)
1418 class PhyLink(SymLink):
1419 BASEDIR = "/dev/sm/phy"
1420 # NB. Cannot use DeviceNodes, e.g. FileVDIs aren't bdevs.
1422 class NBDLink(SymLink):
1424 BASEDIR = "/run/blktap-control/nbd"
1426 class BackendLink(Hybrid):
1427 BASEDIR = "/dev/sm/backend"
1428 # NB. Could be SymLinks as well, but saving major,minor pairs in
1429 # Links enables neat state capturing when managing Tapdisks. Note
1430 # that we essentially have a tap-ctl list replacement here. For
1431 # now make it a 'Hybrid'. Likely to collapse into a DeviceNode as
1432 # soon as ISOs are tapdisks.
1434 @staticmethod
1435 def _tap_activate(phy_path, vdi_type, sr_uuid, options, pool_size=None):
1437 tapdisk = Tapdisk.find_by_path(phy_path)
1438 if not tapdisk: 1438 ↛ 1439line 1438 didn't jump to line 1439, because the condition on line 1438 was never true
1439 blktap = Blktap.allocate()
1440 blktap.set_pool_name(sr_uuid)
1441 if pool_size:
1442 blktap.set_pool_size(pool_size)
1444 try:
1445 tapdisk = \
1446 Tapdisk.launch_on_tap(blktap,
1447 phy_path,
1448 VDI._tap_type(vdi_type),
1449 options)
1450 except:
1451 blktap.free()
1452 raise
1453 util.SMlog("tap.activate: Launched %s" % tapdisk)
1455 else:
1456 util.SMlog("tap.activate: Found %s" % tapdisk)
1458 return tapdisk.get_devpath(), tapdisk
1460 @staticmethod
1461 def _tap_deactivate(minor):
1463 try:
1464 tapdisk = Tapdisk.from_minor(minor)
1465 except TapdiskNotRunning as e:
1466 util.SMlog("tap.deactivate: Warning, %s" % e)
1467 # NB. Should not be here unless the agent refcount
1468 # broke. Also, a clean shutdown should not have leaked
1469 # the recorded minor.
1470 else:
1471 tapdisk.shutdown()
1472 util.SMlog("tap.deactivate: Shut down %s" % tapdisk)
1474 @classmethod
1475 def tap_pause(cls, session, sr_uuid, vdi_uuid, failfast=False):
1476 """
1477 Pauses the tapdisk.
1479 session: a XAPI session
1480 sr_uuid: the UUID of the SR on which VDI lives
1481 vdi_uuid: the UUID of the VDI to pause
1482 failfast: controls whether the VDI lock should be acquired in a
1483 non-blocking manner
1484 """
1485 util.SMlog("Pause request for %s" % vdi_uuid)
1486 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1487 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'paused', 'true')
1488 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1489 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1489 ↛ 1490line 1489 didn't jump to line 1490, because the loop on line 1489 never started
1490 host_ref = key[len('host_'):]
1491 util.SMlog("Calling tap-pause on host %s" % host_ref)
1492 if not cls.call_pluginhandler(session, host_ref,
1493 sr_uuid, vdi_uuid, "pause", failfast=failfast):
1494 # Failed to pause node
1495 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1496 return False
1497 return True
1499 @classmethod
1500 def tap_unpause(cls, session, sr_uuid, vdi_uuid, secondary=None,
1501 activate_parents=False):
1502 util.SMlog("Unpause request for %s secondary=%s" % (vdi_uuid, secondary))
1503 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1504 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1505 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1505 ↛ 1506line 1505 didn't jump to line 1506, because the loop on line 1505 never started
1506 host_ref = key[len('host_'):]
1507 util.SMlog("Calling tap-unpause on host %s" % host_ref)
1508 if not cls.call_pluginhandler(session, host_ref,
1509 sr_uuid, vdi_uuid, "unpause", secondary, activate_parents):
1510 # Failed to unpause node
1511 return False
1512 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1513 return True
1515 @classmethod
1516 def tap_refresh(cls, session, sr_uuid, vdi_uuid, activate_parents=False):
1517 util.SMlog("Refresh request for %s" % vdi_uuid)
1518 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1519 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1520 for key in [x for x in sm_config.keys() if x.startswith('host_')]:
1521 host_ref = key[len('host_'):]
1522 util.SMlog("Calling tap-refresh on host %s" % host_ref)
1523 if not cls.call_pluginhandler(session, host_ref,
1524 sr_uuid, vdi_uuid, "refresh", None,
1525 activate_parents=activate_parents):
1526 # Failed to refresh node
1527 return False
1528 return True
1530 @classmethod
1531 def tap_status(cls, session, vdi_uuid):
1532 """Return True if disk is attached, false if it isn't"""
1533 util.SMlog("Disk status request for %s" % vdi_uuid)
1534 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1535 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1536 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1536 ↛ 1537line 1536 didn't jump to line 1537, because the loop on line 1536 never started
1537 return True
1538 return False
1540 @classmethod
1541 def call_pluginhandler(cls, session, host_ref, sr_uuid, vdi_uuid, action,
1542 secondary=None, activate_parents=False, failfast=False):
1543 """Optionally, activate the parent LV before unpausing"""
1544 try:
1545 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi_uuid,
1546 "failfast": str(failfast)}
1547 if secondary:
1548 args["secondary"] = secondary
1549 if activate_parents:
1550 args["activate_parents"] = "true"
1551 ret = session.xenapi.host.call_plugin(
1552 host_ref, PLUGIN_TAP_PAUSE, action,
1553 args)
1554 return ret == "True"
1555 except Exception as e:
1556 util.logException("BLKTAP2:call_pluginhandler %s" % e)
1557 return False
1559 def _add_tag(self, vdi_uuid, writable):
1560 util.SMlog("Adding tag to: %s" % vdi_uuid)
1561 attach_mode = "RO"
1562 if writable:
1563 attach_mode = "RW"
1564 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1565 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host())
1566 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1567 attached_as = util.attached_as(sm_config)
1568 if NO_MULTIPLE_ATTACH and (attached_as == "RW" or \ 1568 ↛ 1570line 1568 didn't jump to line 1570, because the condition on line 1568 was never true
1569 (attached_as == "RO" and attach_mode == "RW")):
1570 util.SMlog("need to reset VDI %s" % vdi_uuid)
1571 if not resetvdis.reset_vdi(self._session, vdi_uuid, force=False,
1572 term_output=False, writable=writable):
1573 raise util.SMException("VDI %s not detached cleanly" % vdi_uuid)
1574 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1575 if 'relinking' in sm_config:
1576 util.SMlog("Relinking key found, back-off and retry" % sm_config)
1577 return False
1578 if 'paused' in sm_config:
1579 util.SMlog("Paused or host_ref key found [%s]" % sm_config)
1580 return False
1581 try:
1582 self._session.xenapi.VDI.add_to_sm_config(
1583 vdi_ref, 'activating', 'True')
1584 except XenAPI.Failure as e:
1585 if e.details[0] == 'MAP_DUPLICATE_KEY' and not writable:
1586 # Someone else is activating - a retry might succeed
1587 return False
1588 raise
1589 host_key = "host_%s" % host_ref
1590 assert host_key not in sm_config
1591 self._session.xenapi.VDI.add_to_sm_config(vdi_ref, host_key,
1592 attach_mode)
1593 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1594 if 'paused' in sm_config or 'relinking' in sm_config:
1595 util.SMlog("Found %s key, aborting" % (
1596 'paused' if 'paused' in sm_config else 'relinking'))
1597 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key)
1598 self._session.xenapi.VDI.remove_from_sm_config(
1599 vdi_ref, 'activating')
1600 return False
1601 util.SMlog("Activate lock succeeded")
1602 return True
1604 def _check_tag(self, vdi_uuid):
1605 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1606 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1607 if 'paused' in sm_config:
1608 util.SMlog("Paused key found [%s]" % sm_config)
1609 return False
1610 return True
1612 def _remove_tag(self, vdi_uuid):
1613 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1614 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host())
1615 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref)
1616 host_key = "host_%s" % host_ref
1617 if host_key in sm_config:
1618 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key)
1619 util.SMlog("Removed host key %s for %s" % (host_key, vdi_uuid))
1620 else:
1621 util.SMlog("_remove_tag: host key %s not found, ignore" % host_key)
1623 def _get_pool_config(self, pool_name):
1624 pool_info = dict()
1625 vdi_ref = self.target.vdi.sr.srcmd.params.get('vdi_ref')
1626 if not vdi_ref: 1626 ↛ 1629line 1626 didn't jump to line 1629, because the condition on line 1626 was never true
1627 # attach_from_config context: HA disks don't need to be in any
1628 # special pool
1629 return pool_info
1631 sr_ref = self.target.vdi.sr.srcmd.params.get('sr_ref')
1632 sr_config = self._session.xenapi.SR.get_other_config(sr_ref)
1633 vdi_config = self._session.xenapi.VDI.get_other_config(vdi_ref)
1634 pool_size_str = sr_config.get(POOL_SIZE_KEY)
1635 pool_name_override = vdi_config.get(POOL_NAME_KEY)
1636 if pool_name_override: 1636 ↛ 1641line 1636 didn't jump to line 1641, because the condition on line 1636 was never false
1637 pool_name = pool_name_override
1638 pool_size_override = vdi_config.get(POOL_SIZE_KEY)
1639 if pool_size_override: 1639 ↛ 1641line 1639 didn't jump to line 1641, because the condition on line 1639 was never false
1640 pool_size_str = pool_size_override
1641 pool_size = 0
1642 if pool_size_str: 1642 ↛ 1652line 1642 didn't jump to line 1652, because the condition on line 1642 was never false
1643 try:
1644 pool_size = int(pool_size_str)
1645 if pool_size < 1 or pool_size > MAX_FULL_RINGS: 1645 ↛ 1646line 1645 didn't jump to line 1646, because the condition on line 1645 was never true
1646 raise ValueError("outside of range")
1647 pool_size = NUM_PAGES_PER_RING * pool_size
1648 except ValueError:
1649 util.SMlog("Error: invalid mem-pool-size %s" % pool_size_str)
1650 pool_size = 0
1652 pool_info["mem-pool"] = pool_name
1653 if pool_size: 1653 ↛ 1656line 1653 didn't jump to line 1656, because the condition on line 1653 was never false
1654 pool_info["mem-pool-size"] = str(pool_size)
1656 return pool_info
1658 def linkNBD(self, sr_uuid, vdi_uuid):
1659 if self.tap:
1660 nbd_path = '/run/blktap-control/nbd%d.%d' % (int(self.tap.pid),
1661 int(self.tap.minor))
1662 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).mklink(nbd_path)
1664 def attach(self, sr_uuid, vdi_uuid, writable, activate=False, caching_params={}):
1665 """Return/dev/sm/backend symlink path"""
1666 self.xenstore_data.update(self._get_pool_config(sr_uuid))
1667 if not self.target.has_cap("ATOMIC_PAUSE") or activate:
1668 util.SMlog("Attach & activate")
1669 self._attach(sr_uuid, vdi_uuid)
1670 dev_path = self._activate(sr_uuid, vdi_uuid,
1671 {"rdonly": not writable})
1672 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path)
1673 self.linkNBD(sr_uuid, vdi_uuid)
1675 # Return backend/ link
1676 back_path = self.BackendLink.from_uuid(sr_uuid, vdi_uuid).path()
1677 if self.tap_wanted():
1678 # Only have NBD if we also have a tap
1679 nbd_path = "nbd:unix:{}:exportname={}".format(
1680 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).path(),
1681 vdi_uuid)
1682 else:
1683 nbd_path = ""
1685 options = {"rdonly": not writable}
1686 options.update(caching_params)
1687 o_direct, o_direct_reason = self.get_o_direct_capability(options)
1688 struct = {'params': back_path,
1689 'params_nbd': nbd_path,
1690 'o_direct': o_direct,
1691 'o_direct_reason': o_direct_reason,
1692 'xenstore_data': self.xenstore_data}
1693 util.SMlog('result: %s' % struct)
1695 try:
1696 f = open("%s.attach_info" % back_path, 'a')
1697 f.write(xmlrpc.client.dumps((struct, ), "", True))
1698 f.close()
1699 except:
1700 pass
1702 return xmlrpc.client.dumps((struct, ), "", True)
1704 def activate(self, sr_uuid, vdi_uuid, writable, caching_params):
1705 util.SMlog("blktap2.activate")
1706 options = {"rdonly": not writable}
1707 options.update(caching_params)
1709 sr_ref = self.target.vdi.sr.srcmd.params.get('sr_ref')
1710 sr_other_config = self._session.xenapi.SR.get_other_config(sr_ref)
1711 for i in range(self.ATTACH_DETACH_RETRY_SECS): 1711 ↛ 1718line 1711 didn't jump to line 1718, because the loop on line 1711 didn't complete
1712 try:
1713 if self._activate_locked(sr_uuid, vdi_uuid, options):
1714 return
1715 except util.SRBusyException:
1716 util.SMlog("SR locked, retrying")
1717 time.sleep(1)
1718 raise util.SMException("VDI %s locked" % vdi_uuid)
1720 def _get_sr_master_host_ref(self) -> str:
1721 """
1722 Give the host ref of the one responsible for Garbage Collection for a SR.
1723 Meaning this host for a local SR, the master for a shared SR.
1724 """
1725 sr = self.target.vdi.sr
1726 if sr.is_shared():
1727 host_ref = util.get_master_ref(self._session)
1728 else:
1729 host_ref = sr.host_ref
1730 return host_ref
1732 def _get_vdi_chain(self, cowutil, extractUuid) -> List[str]:
1733 vdi_chain = []
1734 path = self.target.get_vdi_path()
1736 #TODO: Need to add handling of error for getParentNoCheck, e.g. corrupted VDI where we can't read parent
1737 vdi_chain.append(extractUuid(path))
1738 parent = cowutil.getParentNoCheck(path)
1739 while parent:
1740 vdi_chain.append(extractUuid(parent))
1741 parent = cowutil.getParentNoCheck(parent)
1742 vdi_chain.reverse()
1743 return vdi_chain
1745 def _check_journal_coalesce_chain(self, sr_uuid: str, vdi_uuid: str) -> bool:
1746 vdi_type = self.target.get_vdi_type()
1747 cowutil = getCowUtil(vdi_type)
1749 if not cowutil.isCoalesceableOnRemote(): #We only need to stop the coalesce in case of QCOW2
1750 return True
1752 path = self.target.get_vdi_path()
1754 import fjournaler
1755 import journaler
1756 from lvmcowutil import LvmCowUtil
1757 from FileSR import FileVDI
1758 import lvmcache
1760 journal: Union[journaler.Journaler, fjournaler.Journaler]
1761 # Different extractUUID & journaler function for LVMSR and FileSR
1762 if path.startswith("/dev/"): #TODO: How to identify SR type easily, we could ask XAPI since we have the sruuid (and even ref)
1763 vgName = "VG_XenStorage-{}".format(sr_uuid)
1764 lvmCache = lvmcache.LVMCache(vgName)
1765 journal = journaler.Journaler(lvmCache)
1767 extractUuid = LvmCowUtil.extractUuid
1768 else:
1769 journal = fjournaler.Journaler(os.getcwd())
1770 extractUuid = FileVDI.extractUuid
1772 # Get the VDI chain
1773 vdi_chain = self._get_vdi_chain(cowutil, extractUuid)
1775 if len(vdi_chain) == 1:
1776 # We only have a leaf, do nothing
1777 util.SMlog("VDI {} is only a leaf, continuing...".format(vdi_uuid))
1778 return True
1780 # Log the chain of active VDI
1781 level = 0
1782 util.SMlog("VDI chain:")
1783 for vdi in vdi_chain:
1784 prefix = " " * level
1785 level += 1
1786 util.SMlog("{}{}".format(prefix, vdi))
1788 vdi_to_cancel = []
1789 for entry in journal.getAll("coalesce").keys():
1790 if entry in vdi_chain:
1791 vdi_to_cancel.append(entry)
1792 util.SMlog("Coalescing VDI {} in chain".format(entry))
1794 # Get the host_ref from the host doing the GC work
1795 host_ref = self._get_sr_master_host_ref()
1796 for vdi in vdi_to_cancel:
1797 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi}
1798 util.SMlog("Calling cancel_coalesce_master with args: {}".format(args))
1799 self._session.xenapi.host.call_plugin(\
1800 host_ref, PLUGIN_ON_SLAVE, "cancel_coalesce_master", args)
1802 return True
1804 @locking("VDIUnavailable")
1805 def _activate_locked(self, sr_uuid, vdi_uuid, options):
1806 """Wraps target.activate and adds a tapdisk"""
1808 #util.SMlog("VDI.activate %s" % vdi_uuid)
1809 refresh = False
1810 if self.tap_wanted(): 1810 ↛ 1815line 1810 didn't jump to line 1815, because the condition on line 1810 was never false
1811 if not self._add_tag(vdi_uuid, not options["rdonly"]):
1812 return False
1813 refresh = True
1815 try:
1816 if refresh: 1816 ↛ 1827line 1816 didn't jump to line 1827, because the condition on line 1816 was never false
1817 # it is possible that while the VDI was paused some of its
1818 # attributes have changed (e.g. its size if it was inflated; or its
1819 # path if it was leaf-coalesced onto a raw LV), so refresh the
1820 # object completely
1821 params = self.target.vdi.sr.srcmd.params
1822 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid)
1823 target.sr.srcmd.params = params
1824 driver_info = target.sr.srcmd.driver_info
1825 self.target = self.TargetDriver(target, driver_info)
1827 util.fistpoint.activate_custom_fn( 1827 ↛ exitline 1827 didn't jump to the function exit
1828 "blktap_activate_inject_failure",
1829 lambda: util.inject_failure())
1831 # Attach the physical node
1832 if self.target.has_cap("ATOMIC_PAUSE"): 1832 ↛ 1833line 1832 didn't jump to line 1833, because the condition on line 1832 was never true
1833 self._attach(sr_uuid, vdi_uuid)
1835 vdi_type = self.target.get_vdi_type()
1837 if not self._check_journal_coalesce_chain(sr_uuid, vdi_uuid): 1837 ↛ 1838line 1837 didn't jump to line 1838, because the condition on line 1837 was never true
1838 return False
1840 # Take lvchange-p Lock before running
1841 # tap-ctl open
1842 # Needed to avoid race with lvchange -p which is
1843 # now taking the same lock
1844 # This is a fix for CA-155766
1845 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1845 ↛ 1848line 1845 didn't jump to line 1848, because the condition on line 1845 was never true
1846 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \
1847 VdiType.isCowImage(vdi_type):
1848 lock = Lock("lvchange-p", NS_PREFIX_LVM + sr_uuid)
1849 lock.acquire()
1851 # When we attach a static VDI for HA, we cannot communicate with
1852 # xapi, because has not started yet. These VDIs are raw.
1853 if VdiType.isCowImage(vdi_type): 1853 ↛ 1854line 1853 didn't jump to line 1854, because the condition on line 1853 was never true
1854 session = self.target.vdi.session
1855 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
1856 # pylint: disable=used-before-assignment
1857 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref)
1858 if 'key_hash' in sm_config:
1859 key_hash = sm_config['key_hash']
1860 options['key_hash'] = key_hash
1861 options['vdi_uuid'] = vdi_uuid
1862 util.SMlog('Using key with hash {} for VDI {}'.format(key_hash, vdi_uuid))
1863 # Activate the physical node
1864 dev_path = self._activate(sr_uuid, vdi_uuid, options)
1866 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1866 ↛ 1869line 1866 didn't jump to line 1869, because the condition on line 1866 was never true
1867 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \
1868 VdiType.isCowImage(self.target.get_vdi_type()):
1869 lock.release()
1870 except:
1871 util.SMlog("Exception in activate/attach")
1872 if self.tap_wanted():
1873 util.fistpoint.activate_custom_fn(
1874 "blktap_activate_error_handling",
1875 lambda: time.sleep(30))
1876 while True:
1877 try:
1878 self._remove_tag(vdi_uuid)
1879 break
1880 except xmlrpc.client.ProtocolError as e:
1881 # If there's a connection error, keep trying forever.
1882 if e.errcode == http.HTTPStatus.INTERNAL_SERVER_ERROR.value:
1883 continue
1884 else:
1885 util.SMlog('failed to remove tag: %s' % e)
1886 break
1887 except Exception as e:
1888 util.SMlog('failed to remove tag: %s' % e)
1889 break
1890 raise
1891 finally:
1892 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
1893 self._session.xenapi.VDI.remove_from_sm_config(
1894 vdi_ref, 'activating')
1895 util.SMlog("Removed activating flag from %s" % vdi_uuid) 1895 ↛ exitline 1895 didn't except from function '_activate_locked', because the raise on line 1890 wasn't executed or line 1895 didn't return from function '_activate_locked', because the return on line 1838 wasn't executed
1897 # Link result to backend/
1898 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path)
1899 self.linkNBD(sr_uuid, vdi_uuid)
1900 return True
1902 def _activate(self, sr_uuid, vdi_uuid, options):
1903 vdi_options = self.target.activate(sr_uuid, vdi_uuid)
1905 dev_path = self.setup_cache(sr_uuid, vdi_uuid, options)
1906 if not dev_path: 1906 ↛ 1920line 1906 didn't jump to line 1920, because the condition on line 1906 was never false
1907 phy_path = self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink()
1908 # Maybe launch a tapdisk on the physical link
1909 if self.tap_wanted(): 1909 ↛ 1918line 1909 didn't jump to line 1918, because the condition on line 1909 was never false
1910 vdi_type = self.target.get_vdi_type()
1911 options["o_direct"] = self.get_o_direct_capability(options)[0]
1912 if vdi_options: 1912 ↛ 1914line 1912 didn't jump to line 1914, because the condition on line 1912 was never false
1913 options.update(vdi_options)
1914 dev_path, self.tap = self._tap_activate(phy_path, vdi_type,
1915 sr_uuid, options,
1916 self._get_pool_config(sr_uuid).get("mem-pool-size"))
1917 else:
1918 dev_path = phy_path # Just reuse phy
1920 return dev_path
1922 def _attach(self, sr_uuid, vdi_uuid):
1923 attach_info = xmlrpc.client.loads(self.target.attach(sr_uuid, vdi_uuid))[0][0]
1924 params = attach_info['params']
1925 xenstore_data = attach_info['xenstore_data']
1926 phy_path = util.to_plain_string(params)
1927 self.xenstore_data.update(xenstore_data)
1928 # Save it to phy/
1929 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(phy_path)
1931 def deactivate(self, sr_uuid, vdi_uuid, caching_params):
1932 util.SMlog("blktap2.deactivate")
1933 for i in range(self.ATTACH_DETACH_RETRY_SECS):
1934 try:
1935 if self._deactivate_locked(sr_uuid, vdi_uuid, caching_params):
1936 return
1937 except util.SRBusyException as e:
1938 util.SMlog("SR locked, retrying")
1939 time.sleep(1)
1940 raise util.SMException("VDI %s locked" % vdi_uuid)
1942 @locking("VDIUnavailable")
1943 def _deactivate_locked(self, sr_uuid, vdi_uuid, caching_params):
1944 """Wraps target.deactivate and removes a tapdisk"""
1946 #util.SMlog("VDI.deactivate %s" % vdi_uuid)
1947 if self.tap_wanted() and not self._check_tag(vdi_uuid):
1948 return False
1950 self._deactivate(sr_uuid, vdi_uuid, caching_params)
1951 if self.target.has_cap("ATOMIC_PAUSE"):
1952 self._detach(sr_uuid, vdi_uuid)
1953 if self.tap_wanted():
1954 self._remove_tag(vdi_uuid)
1956 return True
1958 def _resetPhylink(self, sr_uuid, vdi_uuid, path):
1959 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(path)
1961 def detach(self, sr_uuid, vdi_uuid, deactivate=False, caching_params={}):
1962 if not self.target.has_cap("ATOMIC_PAUSE") or deactivate:
1963 util.SMlog("Deactivate & detach")
1964 self._deactivate(sr_uuid, vdi_uuid, caching_params)
1965 self._detach(sr_uuid, vdi_uuid)
1966 else:
1967 pass # nothing to do
1969 def _deactivate(self, sr_uuid, vdi_uuid, caching_params):
1970 # Shutdown tapdisk
1971 back_link = self.BackendLink.from_uuid(sr_uuid, vdi_uuid)
1973 if not util.pathexists(back_link.path()):
1974 util.SMlog("Backend path %s does not exist" % back_link.path())
1975 return
1977 try:
1978 attach_info_path = "%s.attach_info" % (back_link.path())
1979 os.unlink(attach_info_path)
1980 except:
1981 util.SMlog("unlink of attach_info failed")
1983 try:
1984 major, minor = back_link.rdev()
1985 except self.DeviceNode.NotABlockDevice:
1986 pass
1987 else:
1988 if major == Tapdisk.major():
1989 self._tap_deactivate(minor)
1990 self.remove_cache(caching_params)
1992 # Remove the backend link
1993 back_link.unlink()
1994 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).unlink()
1996 # Deactivate & detach the physical node
1997 if self.tap_wanted() and self.target.vdi.session is not None:
1998 # it is possible that while the VDI was paused some of its
1999 # attributes have changed (e.g. its size if it was inflated; or its
2000 # path if it was leaf-coalesced onto a raw LV), so refresh the
2001 # object completely
2002 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid)
2003 driver_info = target.sr.srcmd.driver_info
2004 self.target = self.TargetDriver(target, driver_info)
2006 self.target.deactivate(sr_uuid, vdi_uuid)
2008 def _detach(self, sr_uuid, vdi_uuid):
2009 self.target.detach(sr_uuid, vdi_uuid)
2011 # Remove phy/
2012 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).unlink()
2014 def _updateCacheRecord(self, session, vdi_uuid, on_boot, caching):
2015 # Remove existing VDI.sm_config fields
2016 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
2017 for key in ["on_boot", "caching"]:
2018 session.xenapi.VDI.remove_from_sm_config(vdi_ref, key)
2019 if not on_boot is None: 2019 ↛ 2020line 2019 didn't jump to line 2020, because the condition on line 2019 was never true
2020 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'on_boot', on_boot)
2021 if not caching is None:
2022 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'caching', caching)
2024 def setup_cache(self, sr_uuid, vdi_uuid, params):
2025 if params.get(self.CONF_KEY_ALLOW_CACHING) != "true":
2026 return
2028 util.SMlog("Requested local caching")
2029 if not self.target.has_cap("SR_CACHING"):
2030 util.SMlog("Error: local caching not supported by this SR")
2031 return
2033 scratch_mode = False
2034 if params.get(self.CONF_KEY_MODE_ON_BOOT) == "reset":
2035 scratch_mode = True
2036 util.SMlog("Requested scratch mode")
2037 if not self.target.has_cap("VDI_RESET_ON_BOOT/2"): 2037 ↛ 2041line 2037 didn't jump to line 2041, because the condition on line 2037 was never false
2038 util.SMlog("Error: scratch mode not supported by this SR")
2039 return
2041 dev_path = None
2042 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR)
2043 if not local_sr_uuid:
2044 util.SMlog("ERROR: Local cache SR not specified, not enabling")
2045 return
2046 dev_path = self._setup_cache(self._session, sr_uuid, vdi_uuid,
2047 local_sr_uuid, scratch_mode, params)
2049 if dev_path:
2050 self._updateCacheRecord(self._session, self.target.vdi.uuid,
2051 params.get(self.CONF_KEY_MODE_ON_BOOT),
2052 params.get(self.CONF_KEY_ALLOW_CACHING))
2054 return dev_path
2056 def alert_no_cache(self, session, vdi_uuid, cache_sr_uuid, err):
2057 vm_uuid = None
2058 vm_label = ""
2059 try:
2060 cache_sr_ref = session.xenapi.SR.get_by_uuid(cache_sr_uuid)
2061 cache_sr_rec = session.xenapi.SR.get_record(cache_sr_ref)
2062 cache_sr_label = cache_sr_rec.get("name_label")
2064 host_ref = session.xenapi.host.get_by_uuid(util.get_this_host())
2065 host_rec = session.xenapi.host.get_record(host_ref)
2066 host_label = host_rec.get("name_label")
2068 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
2069 vbds = session.xenapi.VBD.get_all_records_where( \
2070 "field \"VDI\" = \"%s\"" % vdi_ref)
2071 for vbd_rec in vbds.values():
2072 vm_ref = vbd_rec.get("VM")
2073 vm_rec = session.xenapi.VM.get_record(vm_ref)
2074 vm_uuid = vm_rec.get("uuid")
2075 vm_label = vm_rec.get("name_label")
2076 except:
2077 util.logException("alert_no_cache")
2079 alert_obj = "SR"
2080 alert_uuid = str(cache_sr_uuid)
2081 alert_str = "No space left in Local Cache SR %s" % cache_sr_uuid
2082 if vm_uuid:
2083 alert_obj = "VM"
2084 alert_uuid = vm_uuid
2085 reason = ""
2086 if err == errno.ENOSPC:
2087 reason = "because there is no space left"
2088 alert_str = "The VM \"%s\" is not using IntelliCache %s on the Local Cache SR (\"%s\") on host \"%s\"" % \
2089 (vm_label, reason, cache_sr_label, host_label)
2091 util.SMlog("Creating alert: (%s, %s, \"%s\")" % \
2092 (alert_obj, alert_uuid, alert_str))
2093 session.xenapi.message.create("No space left in local cache", "3",
2094 alert_obj, alert_uuid, alert_str)
2096 def _setup_cache(self, session, sr_uuid, vdi_uuid, local_sr_uuid,
2097 scratch_mode, options):
2098 import SR
2099 import EXTSR
2101 if self._no_parent(self.target.vdi): 2101 ↛ 2102line 2101 didn't jump to line 2102, because the condition on line 2101 was never true
2102 util.SMlog("ERROR: VDI %s has no parent, not enabling" %
2103 self.target.vdi.uuid)
2104 return
2106 util.SMlog("Setting up cache")
2107 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent)
2109 if shared_target.parent:
2110 util.SMlog("ERROR: Parent VDI %s has parent, not enabling" %
2111 shared_target.uuid)
2112 return
2114 SR.registerSR(EXTSR.EXTSR)
2115 local_sr = SR.SR.from_uuid(session, local_sr_uuid)
2117 vdi_type = self.target.get_vdi_type()
2118 tap_type = VDI._tap_type(vdi_type)
2119 cowutil = getCowUtil(vdi_type)
2121 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid)
2122 lock.acquire()
2124 # read cache
2125 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid)
2126 if util.pathexists(read_cache_path): 2126 ↛ 2130line 2126 didn't jump to line 2130, because the condition on line 2126 was never false
2127 util.SMlog("Read cache node (%s) already exists, not creating" %
2128 read_cache_path)
2129 else:
2130 try:
2131 cowutil.snapshot(read_cache_path, shared_target.path, False)
2132 except util.CommandException as e:
2133 util.SMlog("Error creating parent cache: %s" % e)
2134 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code)
2135 return None
2137 # local write node
2138 leaf_size = cowutil.getSizeVirt(self.target.vdi.path)
2139 local_leaf_path = "%s/%s.vhdcache" % \
2140 (local_sr.path, self.target.vdi.uuid)
2141 if util.pathexists(local_leaf_path): 2141 ↛ 2145line 2141 didn't jump to line 2145, because the condition on line 2141 was never false
2142 util.SMlog("Local leaf node (%s) already exists, deleting" %
2143 local_leaf_path)
2144 os.unlink(local_leaf_path)
2145 try:
2146 cowutil.snapshot(local_leaf_path, read_cache_path, False,
2147 msize=leaf_size, checkEmpty=False)
2148 except util.CommandException as e:
2149 util.SMlog("Error creating leaf cache: %s" % e)
2150 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code)
2151 return None
2153 local_leaf_size = cowutil.getSizeVirt(local_leaf_path)
2154 if leaf_size > local_leaf_size: 2154 ↛ 2155line 2154 didn't jump to line 2155, because the condition on line 2154 was never true
2155 util.SMlog("Leaf size %d > local leaf cache size %d, resizing" %
2156 (leaf_size, local_leaf_size))
2157 cowutil.setSizeVirtFast(local_leaf_path, leaf_size)
2159 prt_tapdisk = Tapdisk.find_by_path(read_cache_path)
2160 if not prt_tapdisk:
2161 parent_options = copy.deepcopy(options)
2162 parent_options["rdonly"] = False
2163 parent_options["lcache"] = True
2165 blktap = Blktap.allocate()
2166 try:
2167 blktap.set_pool_name("lcache-parent-pool-%s" % blktap.minor)
2168 # no need to change pool_size since each parent tapdisk is in
2169 # its own pool
2170 prt_tapdisk = Tapdisk.launch_on_tap(blktap, read_cache_path, tap_type, parent_options)
2171 except:
2172 blktap.free()
2173 raise
2175 secondary = "%s:%s" % (vdi_type, self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink())
2177 util.SMlog("Parent tapdisk: %s" % prt_tapdisk)
2178 leaf_tapdisk = Tapdisk.find_by_path(local_leaf_path)
2179 if not leaf_tapdisk: 2179 ↛ 2195line 2179 didn't jump to line 2195, because the condition on line 2179 was never false
2180 blktap = Blktap.allocate()
2181 child_options = copy.deepcopy(options)
2182 child_options["rdonly"] = False
2183 child_options["lcache"] = (not scratch_mode)
2184 child_options["existing_prt"] = prt_tapdisk.minor
2185 child_options["secondary"] = secondary
2186 child_options["standby"] = scratch_mode
2187 # Disable memory read caching
2188 child_options.pop("o_direct", None)
2189 try:
2190 leaf_tapdisk = Tapdisk.launch_on_tap(blktap, local_leaf_path, tap_type, child_options)
2191 except:
2192 blktap.free()
2193 raise
2195 lock.release()
2197 util.SMlog("Local read cache: %s, local leaf: %s" %
2198 (read_cache_path, local_leaf_path))
2200 self.tap = leaf_tapdisk
2201 return leaf_tapdisk.get_devpath()
2203 def remove_cache(self, params):
2204 if not self.target.has_cap("SR_CACHING"):
2205 return
2207 caching = params.get(self.CONF_KEY_ALLOW_CACHING) == "true"
2209 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR)
2210 if caching and not local_sr_uuid:
2211 util.SMlog("ERROR: Local cache SR not specified, ignore")
2212 return
2214 if caching: 2214 ↛ 2217line 2214 didn't jump to line 2217, because the condition on line 2214 was never false
2215 self._remove_cache(self._session, local_sr_uuid)
2217 if self._session is not None: 2217 ↛ exitline 2217 didn't return from function 'remove_cache', because the condition on line 2217 was never false
2218 self._updateCacheRecord(self._session, self.target.vdi.uuid, None, None)
2220 def _is_tapdisk_in_use(self, minor):
2221 retVal, links, sockets = util.findRunningProcessOrOpenFile("tapdisk")
2222 if not retVal:
2223 # err on the side of caution
2224 return True
2226 for link in links:
2227 if link.find("tapdev%d" % minor) != -1:
2228 return True
2230 socket_re = re.compile(r'^/.*/nbd\d+\.%d' % minor)
2231 for s in sockets:
2232 if socket_re.match(s):
2233 return True
2235 return False
2237 def _remove_cache(self, session, local_sr_uuid):
2238 import SR
2239 import EXTSR
2241 if self._no_parent(self.target.vdi):
2242 util.SMlog("ERROR: No parent for VDI %s, ignore" %
2243 self.target.vdi.uuid)
2244 return
2246 util.SMlog("Tearing down the cache")
2248 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent)
2250 SR.registerSR(EXTSR.EXTSR)
2251 local_sr = SR.SR.from_uuid(session, local_sr_uuid)
2253 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid)
2254 lock.acquire()
2256 # local write node
2257 local_leaf_path = "%s/%s.vhdcache" % \
2258 (local_sr.path, self.target.vdi.uuid)
2259 if util.pathexists(local_leaf_path): 2259 ↛ 2263line 2259 didn't jump to line 2263, because the condition on line 2259 was never false
2260 util.SMlog("Deleting local leaf node %s" % local_leaf_path)
2261 os.unlink(local_leaf_path)
2263 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid)
2264 prt_tapdisk = Tapdisk.find_by_path(read_cache_path)
2265 if not prt_tapdisk: 2265 ↛ 2266line 2265 didn't jump to line 2266, because the condition on line 2265 was never true
2266 util.SMlog("Parent tapdisk not found")
2267 elif not self._is_tapdisk_in_use(prt_tapdisk.minor): 2267 ↛ 2275line 2267 didn't jump to line 2275, because the condition on line 2267 was never false
2268 util.SMlog("Parent tapdisk not in use: shutting down %s" %
2269 read_cache_path)
2270 try:
2271 prt_tapdisk.shutdown()
2272 except:
2273 util.logException("shutting down parent tapdisk")
2274 else:
2275 util.SMlog("Parent tapdisk still in use: %s" % read_cache_path)
2276 # the parent cache files are removed during the local SR's background
2277 # GC run
2279 lock.release()
2281 @staticmethod
2282 def _no_parent(vdi):
2283 return vdi.parent is None or vdi.parent == ''
2286PythonKeyError = KeyError
2289class UEventHandler(object):
2291 def __init__(self):
2292 self._action = None
2294 class KeyError(PythonKeyError):
2295 def __init__(self, args):
2296 super().__init__(args)
2297 self.key = args[0]
2299 @override
2300 def __str__(self) -> str:
2301 return \
2302 "Key '%s' missing in environment. " % self.key + \
2303 "Not called in udev context?"
2305 @classmethod
2306 def getenv(cls, key):
2307 try:
2308 return os.environ[key]
2309 except KeyError as e:
2310 raise cls.KeyError(e.args[0])
2312 def get_action(self):
2313 if not self._action:
2314 self._action = self.getenv('ACTION')
2315 return self._action
2317 class UnhandledEvent(Exception):
2319 def __init__(self, event, handler):
2320 self.event = event
2321 self.handler = handler
2323 @override
2324 def __str__(self) -> str:
2325 return "Uevent '%s' not handled by %s" % \
2326 (self.event, self.handler.__class__.__name__)
2328 ACTIONS: Dict[str, Callable] = {}
2330 def run(self):
2332 action = self.get_action()
2333 try:
2334 fn = self.ACTIONS[action]
2335 except KeyError:
2336 raise self.UnhandledEvent(action, self)
2338 return fn(self)
2340 @override
2341 def __str__(self) -> str:
2342 try:
2343 action = self.get_action()
2344 except:
2345 action = None
2346 return "%s[%s]" % (self.__class__.__name__, action)
2349class __BlktapControl(ClassDevice):
2350 SYSFS_CLASSTYPE = "misc"
2352 def __init__(self):
2353 ClassDevice.__init__(self)
2354 self._default_pool = None
2356 @override
2357 def sysfs_devname(self) -> str:
2358 return "blktap!control"
2360 class DefaultPool(Attribute):
2361 SYSFS_NODENAME = "default_pool"
2363 def get_default_pool_attr(self):
2364 if not self._default_pool:
2365 self._default_pool = self.DefaultPool.from_kobject(self)
2366 return self._default_pool
2368 def get_default_pool_name(self):
2369 return self.get_default_pool_attr().readline()
2371 def set_default_pool_name(self, name):
2372 self.get_default_pool_attr().writeline(name)
2374 def get_default_pool(self):
2375 return BlktapControl.get_pool(self.get_default_pool_name())
2377 def set_default_pool(self, pool):
2378 self.set_default_pool_name(pool.name)
2380 class NoSuchPool(Exception):
2381 def __init__(self, name):
2382 self.name = name
2384 @override
2385 def __str__(self) -> str:
2386 return "No such pool: {}".format(self.name)
2388 def get_pool(self, name):
2389 path = "%s/pools/%s" % (self.sysfs_path(), name)
2391 if not os.path.isdir(path):
2392 raise self.NoSuchPool(name)
2394 return PagePool(path)
2396BlktapControl = __BlktapControl()
2399class PagePool(KObject):
2401 def __init__(self, path):
2402 self.path = path
2403 self._size = None
2405 @override
2406 def sysfs_devname(self) -> str:
2407 return ''
2409 def sysfs_path(self):
2410 return self.path
2412 class Size(Attribute):
2413 SYSFS_NODENAME = "size"
2415 def get_size_attr(self):
2416 if not self._size:
2417 self._size = self.Size.from_kobject(self)
2418 return self._size
2420 def set_size(self, pages):
2421 pages = str(pages)
2422 self.get_size_attr().writeline(pages)
2424 def get_size(self):
2425 pages = self.get_size_attr().readline()
2426 return int(pages)
2429class BusDevice(KObject):
2431 SYSFS_BUSTYPE: ClassVar[str] = ""
2433 @classmethod
2434 def sysfs_bus_path(cls):
2435 return "/sys/bus/%s" % cls.SYSFS_BUSTYPE
2437 def sysfs_path(self):
2438 path = "%s/devices/%s" % (self.sysfs_bus_path(),
2439 self.sysfs_devname())
2441 return path
2444class XenbusDevice(BusDevice):
2445 """Xenbus device, in XS and sysfs"""
2447 XBT_NIL = ""
2449 XENBUS_DEVTYPE: ClassVar[str] = ""
2451 def __init__(self, domid, devid):
2452 self.domid = int(domid)
2453 self.devid = int(devid)
2454 self._xbt = XenbusDevice.XBT_NIL
2456 import xen.lowlevel.xs # pylint: disable=import-error
2457 self.xs = xen.lowlevel.xs.xs()
2459 def xs_path(self, key=None):
2460 path = "backend/%s/%d/%d" % (self.XENBUS_DEVTYPE,
2461 self.domid,
2462 self.devid)
2463 if key is not None:
2464 path = "%s/%s" % (path, key)
2466 return path
2468 def _log(self, prio, msg):
2469 syslog(prio, msg)
2471 def info(self, msg):
2472 self._log(_syslog.LOG_INFO, msg)
2474 def warn(self, msg):
2475 self._log(_syslog.LOG_WARNING, "WARNING: " + msg)
2477 def _xs_read_path(self, path):
2478 val = self.xs.read(self._xbt, path)
2479 #self.info("read %s = '%s'" % (path, val))
2480 return val
2482 def _xs_write_path(self, path, val):
2483 self.xs.write(self._xbt, path, val)
2484 self.info("wrote %s = '%s'" % (path, val))
2486 def _xs_rm_path(self, path):
2487 self.xs.rm(self._xbt, path)
2488 self.info("removed %s" % path)
2490 def read(self, key):
2491 return self._xs_read_path(self.xs_path(key))
2493 def has_xs_key(self, key):
2494 return self.read(key) is not None
2496 def write(self, key, val):
2497 self._xs_write_path(self.xs_path(key), val)
2499 def rm(self, key):
2500 self._xs_rm_path(self.xs_path(key))
2502 def exists(self):
2503 return self.has_xs_key(None)
2505 def begin(self):
2506 assert(self._xbt == XenbusDevice.XBT_NIL)
2507 self._xbt = self.xs.transaction_start()
2509 def commit(self):
2510 ok = self.xs.transaction_end(self._xbt, 0)
2511 self._xbt = XenbusDevice.XBT_NIL
2512 return ok
2514 def abort(self):
2515 ok = self.xs.transaction_end(self._xbt, 1)
2516 assert(ok == True)
2517 self._xbt = XenbusDevice.XBT_NIL
2519 def create_physical_device(self):
2520 """The standard protocol is: toolstack writes 'params', linux hotplug
2521 script translates this into physical-device=%x:%x"""
2522 if self.has_xs_key("physical-device"):
2523 return
2524 try:
2525 params = self.read("params")
2526 frontend = self.read("frontend")
2527 is_cdrom = self._xs_read_path("%s/device-type") == "cdrom"
2528 # We don't have PV drivers for CDROM devices, so we prevent blkback
2529 # from opening the physical-device
2530 if not(is_cdrom):
2531 major_minor = os.stat(params).st_rdev
2532 major, minor = divmod(major_minor, 256)
2533 self.write("physical-device", "%x:%x" % (major, minor))
2534 except:
2535 util.logException("BLKTAP2:create_physical_device")
2537 def signal_hotplug(self, online=True):
2538 xapi_path = "/xapi/%d/hotplug/%s/%d/hotplug" % (self.domid,
2539 self.XENBUS_DEVTYPE,
2540 self.devid)
2541 upstream_path = self.xs_path("hotplug-status")
2542 if online:
2543 self._xs_write_path(xapi_path, "online")
2544 self._xs_write_path(upstream_path, "connected")
2545 else:
2546 self._xs_rm_path(xapi_path)
2547 self._xs_rm_path(upstream_path)
2549 @override
2550 def sysfs_devname(self) -> str:
2551 return "%s-%d-%d" % (self.XENBUS_DEVTYPE,
2552 self.domid, self.devid)
2554 @override
2555 def __str__(self) -> str:
2556 return self.sysfs_devname()
2558 @classmethod
2559 def find(cls):
2560 pattern = "/sys/bus/%s/devices/%s*" % (cls.SYSFS_BUSTYPE,
2561 cls.XENBUS_DEVTYPE)
2562 for path in glob.glob(pattern):
2564 name = os.path.basename(path)
2565 (_type, domid, devid) = name.split('-')
2567 yield cls(domid, devid)
2570class XenBackendDevice(XenbusDevice):
2571 """Xenbus backend device"""
2572 SYSFS_BUSTYPE = "xen-backend"
2574 @classmethod
2575 def from_xs_path(cls, _path):
2576 (_backend, _type, domid, devid) = _path.split('/')
2578 assert _backend == 'backend'
2579 assert _type == cls.XENBUS_DEVTYPE
2581 domid = int(domid)
2582 devid = int(devid)
2584 return cls(domid, devid)
2587class Blkback(XenBackendDevice):
2588 """A blkback VBD"""
2590 XENBUS_DEVTYPE = "vbd"
2592 def __init__(self, domid, devid):
2593 XenBackendDevice.__init__(self, domid, devid)
2594 self._phy = None
2595 self._vdi_uuid = None
2596 self._q_state = None
2597 self._q_events = None
2599 class XenstoreValueError(Exception):
2600 KEY: ClassVar[str] = ""
2602 def __init__(self, vbd, _str):
2603 self.vbd = vbd
2604 self.str = _str
2606 @override
2607 def __str__(self) -> str:
2608 return "Backend %s " % self.vbd + \
2609 "has %s = %s" % (self.KEY, self.str)
2611 class PhysicalDeviceError(XenstoreValueError):
2612 KEY = "physical-device"
2614 class PhysicalDevice(object):
2616 def __init__(self, major, minor):
2617 self.major = int(major)
2618 self.minor = int(minor)
2620 @classmethod
2621 def from_xbdev(cls, xbdev):
2623 phy = xbdev.read("physical-device")
2625 try:
2626 major, minor = phy.split(':')
2627 major = int(major, 0x10)
2628 minor = int(minor, 0x10)
2629 except Exception as e:
2630 raise xbdev.PhysicalDeviceError(xbdev, phy)
2632 return cls(major, minor)
2634 def makedev(self):
2635 return os.makedev(self.major, self.minor)
2637 def is_tap(self):
2638 return self.major == Tapdisk.major()
2640 @override
2641 def __str__(self) -> str:
2642 return "%s:%s" % (self.major, self.minor)
2644 @override
2645 def __eq__(self, other) -> bool:
2646 return \
2647 self.major == other.major and \
2648 self.minor == other.minor
2650 def get_physical_device(self):
2651 if not self._phy:
2652 self._phy = self.PhysicalDevice.from_xbdev(self)
2653 return self._phy
2655 class QueueEvents(Attribute):
2656 """Blkback sysfs node to select queue-state event
2657 notifications emitted."""
2659 SYSFS_NODENAME = "queue_events"
2661 QUEUE_RUNNING = (1 << 0)
2662 QUEUE_PAUSE_DONE = (1 << 1)
2663 QUEUE_SHUTDOWN_DONE = (1 << 2)
2664 QUEUE_PAUSE_REQUEST = (1 << 3)
2665 QUEUE_SHUTDOWN_REQUEST = (1 << 4)
2667 def get_mask(self):
2668 return int(self.readline(), 0x10)
2670 def set_mask(self, mask):
2671 self.writeline("0x%x" % mask)
2673 def get_queue_events(self):
2674 if not self._q_events:
2675 self._q_events = self.QueueEvents.from_kobject(self)
2676 return self._q_events
2678 def get_vdi_uuid(self):
2679 if not self._vdi_uuid:
2680 self._vdi_uuid = self.read("sm-data/vdi-uuid")
2681 return self._vdi_uuid
2683 def pause_requested(self):
2684 return self.has_xs_key("pause")
2686 def shutdown_requested(self):
2687 return self.has_xs_key("shutdown-request")
2689 def shutdown_done(self):
2690 return self.has_xs_key("shutdown-done")
2692 def running(self):
2693 return self.has_xs_key('queue-0/kthread-pid')
2695 @classmethod
2696 def find_by_physical_device(cls, phy):
2697 for dev in cls.find():
2698 try:
2699 _phy = dev.get_physical_device()
2700 except cls.PhysicalDeviceError:
2701 continue
2703 if _phy == phy:
2704 yield dev
2706 @classmethod
2707 def find_by_tap_minor(cls, minor):
2708 phy = cls.PhysicalDevice(Tapdisk.major(), minor)
2709 return cls.find_by_physical_device(phy)
2711 @classmethod
2712 def find_by_tap(cls, tapdisk):
2713 return cls.find_by_tap_minor(tapdisk.minor)
2715 def has_tap(self):
2717 if not self.can_tap():
2718 return False
2720 phy = self.get_physical_device()
2721 if phy:
2722 return phy.is_tap()
2724 return False
2726 def is_bare_hvm(self):
2727 """File VDIs for bare HVM. These are directly accessible by Qemu."""
2728 try:
2729 self.get_physical_device()
2731 except self.PhysicalDeviceError as e:
2732 vdi_type = self.read("type")
2734 self.info("HVM VDI: type=%s" % vdi_type)
2736 if e.str is not None or vdi_type != 'file':
2737 raise
2739 return True
2741 return False
2743 def can_tap(self):
2744 return not self.is_bare_hvm()
2747class BlkbackEventHandler(UEventHandler):
2749 LOG_FACILITY = _syslog.LOG_DAEMON
2751 def __init__(self, ident=None, action=None):
2752 if not ident:
2753 ident = self.__class__.__name__
2755 self.ident = ident
2756 self._vbd = None
2757 self._tapdisk = None
2759 UEventHandler.__init__(self)
2761 @override
2762 def run(self) -> None:
2764 self.xs_path = self.getenv('XENBUS_PATH')
2765 openlog(str(self), 0, self.LOG_FACILITY)
2767 UEventHandler.run(self)
2769 @override
2770 def __str__(self) -> str:
2772 try:
2773 path = self.xs_path
2774 except:
2775 path = None
2777 try:
2778 action = self.get_action()
2779 except:
2780 action = None
2782 return "%s[%s](%s)" % (self.ident, action, path)
2784 def _log(self, prio, msg):
2785 syslog(prio, msg)
2786 util.SMlog("%s: " % self + msg)
2788 def info(self, msg):
2789 self._log(_syslog.LOG_INFO, msg)
2791 def warn(self, msg):
2792 self._log(_syslog.LOG_WARNING, "WARNING: " + msg)
2794 def error(self, msg):
2795 self._log(_syslog.LOG_ERR, "ERROR: " + msg)
2797 def get_vbd(self):
2798 if not self._vbd:
2799 self._vbd = Blkback.from_xs_path(self.xs_path)
2800 return self._vbd
2802 def get_tapdisk(self):
2803 if not self._tapdisk:
2804 minor = self.get_vbd().get_physical_device().minor
2805 self._tapdisk = Tapdisk.from_minor(minor)
2806 return self._tapdisk
2807 #
2808 # Events
2809 #
2811 def __add(self):
2812 vbd = self.get_vbd()
2813 # Manage blkback transitions
2814 # self._manage_vbd()
2816 vbd.create_physical_device()
2818 vbd.signal_hotplug()
2820 @retried(backoff=.5, limit=10)
2821 def add(self):
2822 try:
2823 self.__add()
2824 except Attribute.NoSuchAttribute as e:
2825 #
2826 # FIXME: KOBJ_ADD is racing backend.probe, which
2827 # registers device attributes. So poll a little.
2828 #
2829 self.warn("%s, still trying." % e)
2830 raise RetryLoop.TransientFailure(e)
2832 def __change(self):
2833 vbd = self.get_vbd()
2835 # 1. Pause or resume tapdisk (if there is one)
2837 if vbd.has_tap():
2838 pass
2839 #self._pause_update_tap()
2841 # 2. Signal Xapi.VBD.pause/resume completion
2843 self._signal_xapi()
2845 def change(self):
2846 vbd = self.get_vbd()
2848 # NB. Beware of spurious change events between shutdown
2849 # completion and device removal. Also, Xapi.VM.migrate will
2850 # hammer a couple extra shutdown-requests into the source VBD.
2852 while True:
2853 vbd.begin()
2855 if not vbd.exists() or \
2856 vbd.shutdown_done():
2857 break
2859 self.__change()
2861 if vbd.commit():
2862 return
2864 vbd.abort()
2865 self.info("spurious uevent, ignored.")
2867 def remove(self):
2868 vbd = self.get_vbd()
2870 vbd.signal_hotplug(False)
2872 ACTIONS = {'add': add,
2873 'change': change,
2874 'remove': remove}
2875 #
2876 # VDI.pause
2877 #
2879 def _tap_should_pause(self):
2880 """Enumerate all VBDs on our tapdisk. Returns true iff any was
2881 paused"""
2883 tapdisk = self.get_tapdisk()
2884 TapState = Tapdisk.PauseState
2886 PAUSED = 'P'
2887 RUNNING = 'R'
2888 PAUSED_SHUTDOWN = 'P,S'
2889 # NB. Shutdown/paused is special. We know it's not going
2890 # to restart again, so it's a RUNNING. Still better than
2891 # backtracking a removed device during Vbd.unplug completion.
2893 next = TapState.RUNNING
2894 vbds = {}
2896 for vbd in Blkback.find_by_tap(tapdisk):
2897 name = str(vbd)
2899 pausing = vbd.pause_requested()
2900 closing = vbd.shutdown_requested()
2901 running = vbd.running()
2903 if pausing:
2904 if closing and not running:
2905 vbds[name] = PAUSED_SHUTDOWN
2906 else:
2907 vbds[name] = PAUSED
2908 next = TapState.PAUSED
2910 else:
2911 vbds[name] = RUNNING
2913 self.info("tapdev%d (%s): %s -> %s"
2914 % (tapdisk.minor, tapdisk.pause_state(),
2915 vbds, next))
2917 return next == TapState.PAUSED
2919 def _pause_update_tap(self):
2920 vbd = self.get_vbd()
2922 if self._tap_should_pause():
2923 self._pause_tap()
2924 else:
2925 self._resume_tap()
2927 def _pause_tap(self):
2928 tapdisk = self.get_tapdisk()
2930 if not tapdisk.is_paused():
2931 self.info("pausing %s" % tapdisk)
2932 tapdisk.pause()
2934 def _resume_tap(self):
2935 tapdisk = self.get_tapdisk()
2937 # NB. Raw VDI snapshots. Refresh the physical path and
2938 # type while resuming.
2939 vbd = self.get_vbd()
2940 vdi_uuid = vbd.get_vdi_uuid()
2942 if tapdisk.is_paused():
2943 self.info("loading vdi uuid=%s" % vdi_uuid)
2944 vdi = VDI.from_cli(vdi_uuid)
2945 _type = vdi.get_tap_type()
2946 path = vdi.get_phy_path()
2947 self.info("resuming %s on %s:%s" % (tapdisk, _type, path))
2948 tapdisk.unpause(_type, path)
2949 #
2950 # VBD.pause/shutdown
2951 #
2953 def _manage_vbd(self):
2954 vbd = self.get_vbd()
2955 # NB. Hook into VBD state transitions.
2957 events = vbd.get_queue_events()
2959 mask = 0
2960 mask |= events.QUEUE_PAUSE_DONE # pause/unpause
2961 mask |= events.QUEUE_SHUTDOWN_DONE # shutdown
2962 # TODO: mask |= events.QUEUE_SHUTDOWN_REQUEST, for shutdown=force
2963 # TODO: mask |= events.QUEUE_RUNNING, for ionice updates etc
2965 events.set_mask(mask)
2966 self.info("wrote %s = %#02x" % (events.path, mask))
2968 def _signal_xapi(self):
2969 vbd = self.get_vbd()
2971 pausing = vbd.pause_requested()
2972 closing = vbd.shutdown_requested()
2973 running = vbd.running()
2975 handled = 0
2977 if pausing and not running:
2978 if 'pause-done' not in vbd:
2979 vbd.write('pause-done', '')
2980 handled += 1
2982 if not pausing:
2983 if 'pause-done' in vbd:
2984 vbd.rm('pause-done')
2985 handled += 1
2987 if closing and not running:
2988 if 'shutdown-done' not in vbd:
2989 vbd.write('shutdown-done', '')
2990 handled += 1
2992 if handled > 1:
2993 self.warn("handled %d events, " % handled +
2994 "pausing=%s closing=%s running=%s" % \
2995 (pausing, closing, running))
2997if __name__ == '__main__': 2997 ↛ 2999line 2997 didn't jump to line 2999, because the condition on line 2997 was never true
2999 import sys
3000 prog = os.path.basename(sys.argv[0])
3002 #
3003 # Simple CLI interface for manual operation
3004 #
3005 # tap.* level calls go down to local Tapdisk()s (by physical path)
3006 # vdi.* level calls run the plugin calls across host boundaries.
3007 #
3009 def usage(stream):
3010 print("usage: %s tap.{list|major}" % prog, file=stream)
3011 print(" %s tap.{launch|find|get|pause|" % prog + \
3012 "unpause|shutdown|stats} {[<tt>:]<path>} | [minor=]<int> | .. }", file=stream)
3013 print(" %s vbd.uevent" % prog, file=stream)
3015 try:
3016 cmd = sys.argv[1]
3017 except IndexError:
3018 usage(sys.stderr)
3019 sys.exit(1)
3021 try:
3022 _class, method = cmd.split('.')
3023 except:
3024 usage(sys.stderr)
3025 sys.exit(1)
3027 #
3028 # Local Tapdisks
3029 #
3031 if cmd == 'tap.major':
3033 print("%d" % Tapdisk.major())
3035 elif cmd == 'tap.launch':
3037 tapdisk = Tapdisk.launch_from_arg(sys.argv[2])
3038 print("Launched %s" % tapdisk, file=sys.stderr)
3040 elif _class == 'tap':
3042 attrs: Dict[str, Any] = {}
3043 for item in sys.argv[2:]:
3044 try:
3045 key, val = item.split('=')
3046 attrs[key] = val
3047 continue
3048 except ValueError:
3049 pass
3051 try:
3052 attrs['minor'] = int(item)
3053 continue
3054 except ValueError:
3055 pass
3057 try:
3058 arg = Tapdisk.Arg.parse(item)
3059 attrs['_type'] = arg.type
3060 attrs['path'] = arg.path
3061 continue
3062 except Tapdisk.Arg.InvalidArgument:
3063 pass
3065 attrs['path'] = item
3067 if cmd == 'tap.list':
3069 for tapdisk in Tapdisk.list( ** attrs):
3070 blktap = tapdisk.get_blktap()
3071 print(tapdisk, end=' ')
3072 print("%s: task=%s pool=%s" % \
3073 (blktap,
3074 blktap.get_task_pid(),
3075 blktap.get_pool_name()))
3077 elif cmd == 'tap.vbds':
3078 # Find all Blkback instances for a given tapdisk
3080 for tapdisk in Tapdisk.list( ** attrs):
3081 print("%s:" % tapdisk, end=' ')
3082 for vbd in Blkback.find_by_tap(tapdisk):
3083 print(vbd, end=' ')
3084 print()
3086 else:
3088 if not attrs:
3089 usage(sys.stderr)
3090 sys.exit(1)
3092 try:
3093 tapdisk = Tapdisk.get( ** attrs)
3094 except TypeError:
3095 usage(sys.stderr)
3096 sys.exit(1)
3098 if cmd == 'tap.shutdown':
3099 # Shutdown a running tapdisk, or raise
3100 tapdisk.shutdown()
3101 print("Shut down %s" % tapdisk, file=sys.stderr)
3103 elif cmd == 'tap.pause':
3104 # Pause an unpaused tapdisk, or raise
3105 tapdisk.pause()
3106 print("Paused %s" % tapdisk, file=sys.stderr)
3108 elif cmd == 'tap.unpause':
3109 # Unpause a paused tapdisk, or raise
3110 tapdisk.unpause()
3111 print("Unpaused %s" % tapdisk, file=sys.stderr)
3113 elif cmd == 'tap.stats':
3114 # Gather tapdisk status
3115 stats = tapdisk.stats()
3116 print("%s:" % tapdisk)
3117 print(json.dumps(stats, indent=True))
3119 else:
3120 usage(sys.stderr)
3121 sys.exit(1)
3123 elif cmd == 'vbd.uevent':
3125 hnd = BlkbackEventHandler(cmd)
3127 if not sys.stdin.isatty():
3128 try:
3129 hnd.run()
3130 except Exception as e:
3131 hnd.error("Unhandled Exception: %s" % e)
3133 import traceback
3134 _type, value, tb = sys.exc_info()
3135 trace = traceback.format_exception(_type, value, tb)
3136 for entry in trace:
3137 for line in entry.rstrip().split('\n'):
3138 util.SMlog(line)
3139 else:
3140 hnd.run()
3142 elif cmd == 'vbd.list':
3144 for vbd in Blkback.find():
3145 print(vbd, \
3146 "physical-device=%s" % vbd.get_physical_device(), \
3147 "pause=%s" % vbd.pause_requested())
3149 else:
3150 usage(sys.stderr)
3151 sys.exit(1)