Hide keyboard shortcuts

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 

21 

22from abc import abstractmethod 

23 

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 

47 

48import resetvdis 

49 

50import VDI as sm 

51 

52from cowutil import getCowUtil 

53 

54# For RRDD Plugin Registration 

55from xmlrpc.client import ServerProxy, Transport 

56from socket import socket, AF_UNIX, SOCK_STREAM 

57 

58 

59try: 

60 from linstorvolumemanager import get_controller_uri, get_all_volume_openers, LinstorVolumeManager 

61 LINSTOR_AVAILABLE = True 

62except ImportError: 

63 LINSTOR_AVAILABLE = False 

64 

65PLUGIN_TAP_PAUSE = "tapdisk-pause" 

66PLUGIN_ON_SLAVE = "on-slave" 

67 

68SOCKPATH = "/var/xapi/xcp-rrdd" 

69 

70NUM_PAGES_PER_RING = 32 * 11 

71MAX_FULL_RINGS = 8 

72POOL_NAME_KEY = "mem-pool" 

73POOL_SIZE_KEY = "mem-pool-size-rings" 

74 

75ENABLE_MULTIPLE_ATTACH = "/etc/xensource/allow_multiple_vdi_attach" 

76NO_MULTIPLE_ATTACH = not (os.path.exists(ENABLE_MULTIPLE_ATTACH)) 

77 

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) 

84 

85 

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 

111 

112 

113class RetryLoop(object): 

114 

115 def __init__(self, backoff, limit): 

116 self.backoff = backoff 

117 self.limit = limit 

118 

119 def __call__(self, f): 

120 

121 def loop(*__t, **__d): 

122 attempt = 0 

123 

124 while True: 

125 attempt += 1 

126 

127 try: 

128 return f( * __t, ** __d) 

129 

130 except self.TransientFailure as e: 

131 e = e.exception 

132 

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 

135 

136 time.sleep(self.backoff) 

137 

138 return loop 

139 

140 class TransientFailure(Exception): 

141 def __init__(self, exception): 

142 self.exception = exception 

143 

144 

145def retried(**args): 

146 return RetryLoop( ** args) 

147 

148 

149class TapCtl(object): 

150 """Tapdisk IPC utility calls.""" 

151 

152 PATH = "/usr/sbin/tap-ctl" 

153 

154 def __init__(self, cmd, p): 

155 self.cmd = cmd 

156 self._p = p 

157 self.stdout = p.stdout 

158 

159 class CommandFailure(Exception): 

160 """TapCtl cmd failure.""" 

161 

162 def __init__(self, cmd, **info): 

163 self.cmd = cmd 

164 self.info = info 

165 

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) 

172 

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) 

179 

180 @property 

181 def has_status(self): 

182 return 'status' in self.info 

183 

184 @property 

185 def has_signal(self): 

186 return 'signal' in self.info 

187 

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 

196 

197 @classmethod 

198 def __mkcmd_real(cls, args): 

199 return [cls.PATH] + [str(x) for x in args] 

200 

201 __next_mkcmd = __mkcmd_real 

202 

203 @classmethod 

204 def _mkcmd(cls, args): 

205 

206 __next_mkcmd = cls.__next_mkcmd 

207 cls.__next_mkcmd = cls.__mkcmd_real 

208 

209 return __next_mkcmd(args) 

210 

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) 

218 

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) 

232 

233 return cls(cmd, p) 

234 

235 def _errmsg(self, stderr): 

236 output = map(str.rstrip, stderr) 

237 return "; ".join(output) 

238 

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) 

248 

249 if status == 0: 

250 return stdout 

251 

252 info = {'errmsg': self._errmsg( 

253 stderr if text_mode else stderr.decode()), 

254 'pid': self._p.pid} 

255 

256 if status < 0: 

257 info['signal'] = -status 

258 else: 

259 info['status'] = status 

260 

261 raise self.CommandFailure(self.cmd, ** info) 

262 

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) 

270 

271 output = tapctl._wait(quiet=quiet, text_mode=text_mode) 

272 return output 

273 

274 @staticmethod 

275 def _maybe(opt, parm): 

276 if parm is not None: 

277 return [opt, parm] 

278 return [] 

279 

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) 

287 

288 tapctl = cls._call(args, quiet=True) 

289 stdout = tapctl._wait(quiet=True) 

290 

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 = {} 

299 

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('=') 

304 

305 if key in ('pid', 'minor'): 

306 row[key] = int(val, 10) 

307 

308 elif key in ('state'): 

309 row[key] = int(val, 0x10) 

310 

311 else: 

312 row[key] = val 

313 else: 

314 util.SMlog("Ignoring unexpected tap-ctl output: %s" % repr(field)) 

315 yield row 

316 

317 @classmethod 

318 @retried(backoff=.5, limit=10) 

319 def list(cls, **args): 

320 

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. 

324 

325 try: 

326 return list(cls.__list( ** args)) 

327 

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 

333 

334 @classmethod 

335 def allocate(cls, devpath=None): 

336 args = ["allocate"] 

337 args += cls._maybe("-d", devpath) 

338 return cls._pread(args) 

339 

340 @classmethod 

341 def free(cls, minor): 

342 args = ["free", "-m", minor] 

343 cls._pread(args) 

344 

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 

357 

358 @classmethod 

359 def attach(cls, pid, minor): 

360 args = ["attach", "-p", pid, "-m", minor] 

361 cls._pread(args) 

362 

363 @classmethod 

364 def detach(cls, pid, minor): 

365 args = ["detach", "-p", pid, "-m", minor] 

366 cls._pread(args) 

367 

368 @classmethod 

369 def _load_key(cls, key_hash, vdi_uuid): 

370 import plugins 

371 

372 return plugins.load_key(key_hash, vdi_uuid) 

373 

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) 

403 

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') 

409 

410 cls._pread(args=args, input=input, text_mode=text_mode) 

411 

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) 

418 

419 @classmethod 

420 def pause(cls, pid, minor): 

421 args = ["pause", "-p", pid, "-m", minor] 

422 cls._pread(args) 

423 

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]) 

435 

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 

453 

454 unpause_impl() 

455 

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) 

461 

462 @classmethod 

463 def stats(cls, pid, minor): 

464 args = ["stats", "-p", pid, "-m", minor] 

465 return cls._pread(args, quiet=True) 

466 

467 @classmethod 

468 def major(cls): 

469 args = ["major"] 

470 major = cls._pread(args) 

471 return int(major) 

472 

473 @classmethod 

474 def commit(cls, pid, minor, vdi_type, path): 

475 args = ["commit", "-p", pid, "-m", minor, "-a", path] 

476 cls._pread(args) 

477 

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) 

487 

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) 

494 

495class TapdiskExists(Exception): 

496 """Tapdisk already running.""" 

497 

498 def __init__(self, tapdisk): 

499 self.tapdisk = tapdisk 

500 

501 @override 

502 def __str__(self) -> str: 

503 return "%s already running" % self.tapdisk 

504 

505 

506class TapdiskNotRunning(Exception): 

507 """No such Tapdisk.""" 

508 

509 def __init__(self, **attrs): 

510 self.attrs = attrs 

511 

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 

518 

519 

520class TapdiskNotUnique(Exception): 

521 """More than one tapdisk on one path.""" 

522 

523 def __init__(self, tapdisks): 

524 self.tapdisks = tapdisks 

525 

526 @override 

527 def __str__(self) -> str: 

528 tapdisks = map(str, self.tapdisks) 

529 return "Found multiple tapdisks: %s" % tapdisks 

530 

531 

532class TapdiskFailed(Exception): 

533 """Tapdisk launch failure.""" 

534 

535 def __init__(self, arg, err): 

536 self.arg = arg 

537 self.err = err 

538 

539 @override 

540 def __str__(self) -> str: 

541 return "Tapdisk(%s): %s" % (self.arg, self.err) 

542 

543 def get_error(self): 

544 return self.err 

545 

546 

547class TapdiskInvalidState(Exception): 

548 """Tapdisk pause/unpause failure""" 

549 

550 def __init__(self, tapdisk): 

551 self.tapdisk = tapdisk 

552 

553 @override 

554 def __str__(self) -> str: 

555 return str(self.tapdisk) 

556 

557 

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 

570 

571 

572class KObject(object): 

573 

574 SYSFS_CLASSTYPE: ClassVar[str] = "" 

575 

576 @abstractmethod 

577 def sysfs_devname(self) -> str: 

578 pass 

579 

580 

581class Attribute(object): 

582 

583 SYSFS_NODENAME: ClassVar[str] = "" 

584 

585 def __init__(self, path): 

586 self.path = path 

587 

588 @classmethod 

589 def from_kobject(cls, kobj): 

590 path = "%s/%s" % (kobj.sysfs_path(), cls.SYSFS_NODENAME) 

591 return cls(path) 

592 

593 class NoSuchAttribute(Exception): 

594 def __init__(self, name): 

595 self.name = name 

596 

597 @override 

598 def __str__(self) -> str: 

599 return "No such attribute: %s" % self.name 

600 

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 

608 

609 def readline(self): 

610 f = self._open('r') 

611 s = f.readline().rstrip() 

612 f.close() 

613 return s 

614 

615 def writeline(self, val): 

616 f = self._open('w') 

617 f.write(val) 

618 f.close() 

619 

620 

621class ClassDevice(KObject): 

622 

623 @classmethod 

624 def sysfs_class_path(cls): 

625 return "/sys/class/%s" % cls.SYSFS_CLASSTYPE 

626 

627 def sysfs_path(self): 

628 return "%s/%s" % (self.sysfs_class_path(), 

629 self.sysfs_devname()) 

630 

631 

632class Blktap(ClassDevice): 

633 

634 DEV_BASEDIR = '/dev/xen/blktap-2' 

635 

636 SYSFS_CLASSTYPE = "blktap2" 

637 

638 def __init__(self, minor): 

639 self.minor = minor 

640 self._pool = None 

641 self._task = None 

642 

643 @classmethod 

644 def allocate(cls): 

645 # FIXME. Should rather go into init. 

646 mkdirs(cls.DEV_BASEDIR) 

647 

648 devname = TapCtl.allocate() 

649 minor = Tapdisk._parse_minor(devname) 

650 return cls(minor) 

651 

652 def free(self): 

653 TapCtl.free(self.minor) 

654 

655 @override 

656 def __str__(self) -> str: 

657 return "%s(minor=%d)" % (self.__class__.__name__, self.minor) 

658 

659 @override 

660 def sysfs_devname(self) -> str: 

661 return "blktap!blktap%d" % self.minor 

662 

663 class Pool(Attribute): 

664 SYSFS_NODENAME = "pool" 

665 

666 def get_pool_attr(self): 

667 if not self._pool: 

668 self._pool = self.Pool.from_kobject(self) 

669 return self._pool 

670 

671 def get_pool_name(self): 

672 return self.get_pool_attr().readline() 

673 

674 def set_pool_name(self, name): 

675 self.get_pool_attr().writeline(name) 

676 

677 def set_pool_size(self, pages): 

678 self.get_pool().set_size(pages) 

679 

680 def get_pool(self): 

681 return BlktapControl.get_pool(self.get_pool_name()) 

682 

683 def set_pool(self, pool): 

684 self.set_pool_name(pool.name) 

685 

686 class Task(Attribute): 

687 SYSFS_NODENAME = "task" 

688 

689 def get_task_attr(self): 

690 if not self._task: 

691 self._task = self.Task.from_kobject(self) 

692 return self._task 

693 

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 

700 

701 def find_tapdisk(self): 

702 pid = self.get_task_pid() 

703 if pid is None: 

704 return None 

705 

706 return Tapdisk.find(pid=pid, minor=self.minor) 

707 

708 def get_tapdisk(self): 

709 tapdisk = self.find_tapdisk() 

710 if not tapdisk: 

711 raise TapdiskNotRunning(minor=self.minor) 

712 return tapdisk 

713 

714 

715class Tapdisk(object): 

716 

717 TYPES = ['aio', 'vhd', 'qcow2'] 

718 

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 

727 

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) 

733 

734 @classmethod 

735 def list(cls, **args): 

736 

737 for row in TapCtl.list( ** args): 

738 

739 args = {'pid': None, 

740 'minor': None, 

741 'state': None, 

742 '_type': None, 

743 'path': None} 

744 

745 for key, val in row.items(): 

746 if key in args: 

747 args[key] = val 

748 

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 

753 

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 

756 

757 yield Tapdisk( ** args) 

758 

759 @classmethod 

760 def find(cls, **args): 

761 

762 found = list(cls.list( ** args)) 

763 

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) 

766 

767 if found: 

768 return found[0] 

769 

770 return None 

771 

772 @classmethod 

773 def find_by_path(cls, path): 

774 return cls.find(path=path) 

775 

776 @classmethod 

777 def find_by_minor(cls, minor): 

778 return cls.find(minor=minor) 

779 

780 @classmethod 

781 def get(cls, **attrs): 

782 

783 tapdisk = cls.find( ** attrs) 

784 

785 if not tapdisk: 

786 raise TapdiskNotRunning( ** attrs) 

787 

788 return tapdisk 

789 

790 @classmethod 

791 def from_path(cls, path): 

792 return cls.get(path=path) 

793 

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 

809 

810 return False 

811 

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 

820 

821 return False 

822 

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') 

827 

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 

832 

833 return None 

834 

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) 

841 

842 return cls.get(minor=minor, pid=pid) 

843 

844 @classmethod 

845 def __from_blktap(cls, blktap): 

846 tapdisk = cls.from_minor(minor=blktap.minor) 

847 tapdisk._blktap = blktap 

848 return tapdisk 

849 

850 def get_blktap(self): 

851 if not self._blktap: 

852 self._blktap = Blktap(self.minor) 

853 return self._blktap 

854 

855 class Arg: 

856 

857 def __init__(self, _type, path): 

858 self.type = _type 

859 self.path = path 

860 

861 @override 

862 def __str__(self) -> str: 

863 return "%s:%s" % (self.type, self.path) 

864 

865 @classmethod 

866 def parse(cls, arg): 

867 

868 try: 

869 _type, path = arg.split(":", 1) 

870 except ValueError: 

871 raise cls.InvalidArgument(arg) 

872 

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) 

875 

876 return cls(_type, path) 

877 

878 class InvalidType(Exception): 

879 def __init__(self, _type): 

880 self.type = _type 

881 

882 @override 

883 def __str__(self) -> str: 

884 return "Not a Tapdisk type: %s" % self.type 

885 

886 class InvalidArgument(Exception): 

887 def __init__(self, arg): 

888 self.arg = arg 

889 

890 @override 

891 def __str__(self) -> str: 

892 return "Not a Tapdisk image: %s" % self.arg 

893 

894 def get_arg(self): 

895 return self.Arg(self.type, self.path) 

896 

897 def get_devpath(self): 

898 return "%s/tapdev%d" % (Blktap.DEV_BASEDIR, self.minor) 

899 

900 @classmethod 

901 def launch_from_arg(cls, arg): 

902 arg = cls.Arg.parse(arg) 

903 return cls.launch(arg.path, arg.type, False) 

904 

905 @staticmethod 

906 def cgclassify(pid): 

907 

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) 

916 

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 

921 

922 _, volume_name, _ = drbd_path.rsplit("/", 2) 

923 group_name = LinstorVolumeManager.get_volume_group_name(volume_name) 

924 

925 openers = get_all_volume_openers(volume_name, "0") 

926 

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) 

936 

937 sr_ref = pbd_rec["SR"] 

938 sr_uuid = api_session.session.xenapi.SR.get_uuid(sr_ref) 

939 

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}`...") 

945 

946 util.SMlog(f"Unable to run tapdisk, openers of DRBD resource `{drbd_path}`: {openers}") 

947 finally: 

948 api_session.logout() 

949 

950 return False 

951 

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) 

958 

959 minor = blktap.minor 

960 try: 

961 pid = TapCtl.spawn() 

962 cls.cgclassify(pid) 

963 try: 

964 TapCtl.attach(pid, minor) 

965 

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 

979 

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 

993 

994 except: 

995 TapCtl.detach(pid, minor) 

996 raise 

997 

998 except: 

999 try: 

1000 TapCtl.shutdown(pid) 

1001 except: 

1002 # Best effort to shutdown 

1003 pass 

1004 raise 

1005 

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) 

1013 

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 

1022 

1023 def shutdown(self, force=False): 

1024 

1025 TapCtl.close(self.pid, self.minor, force) 

1026 

1027 TapCtl.detach(self.pid, self.minor) 

1028 

1029 self.get_blktap().free() 

1030 

1031 def pause(self): 

1032 

1033 if not self.is_running(): 

1034 raise TapdiskInvalidState(self) 

1035 

1036 TapCtl.pause(self.pid, self.minor) 

1037 

1038 self._set_dirty() 

1039 

1040 def unpause(self, _type=None, path=None, mirror=None, cbtlog=None): 

1041 

1042 if not self.is_paused(): 

1043 raise TapdiskInvalidState(self) 

1044 

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 

1050 

1051 TapCtl.unpause(self.pid, self.minor, _type, path, mirror=mirror, 

1052 cbtlog=cbtlog) 

1053 

1054 self._set_dirty() 

1055 

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 # 

1061 

1062 def _set_dirty(self): 

1063 self._dirty = True 

1064 

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) 

1068 

1069 @override 

1070 def __getattribute__(self, name) -> Any: 

1071 def __get(name): 

1072 # NB. avoid(rec(ursion) 

1073 return object.__getattribute__(self, name) 

1074 

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 

1079 

1080 return __get(name) 

1081 

1082 class PauseState: 

1083 RUNNING = 'R' 

1084 PAUSING = 'r' 

1085 PAUSED = 'P' 

1086 

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 

1098 

1099 PAUSE_MASK = PAUSE_REQUESTED | PAUSED 

1100 

1101 def is_paused(self): 

1102 return not not (self.state & self.Flags.PAUSED) 

1103 

1104 def is_running(self): 

1105 return not (self.state & self.Flags.PAUSE_MASK) 

1106 

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 

1110 

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 

1113 

1114 return self.PauseState.RUNNING 

1115 

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)) 

1123 

1124 minor = groups.group(2) 

1125 return int(minor) 

1126 

1127 _major = None 

1128 

1129 @classmethod 

1130 def major(cls): 

1131 if cls._major: 

1132 return cls._major 

1133 

1134 devices = open("/proc/devices") 

1135 for line in devices: 

1136 

1137 row = line.rstrip().split(' ') 

1138 if len(row) != 2: 

1139 continue 

1140 

1141 major, name = row 

1142 if name != 'tapdev': 

1143 continue 

1144 

1145 cls._major = int(major) 

1146 break 

1147 

1148 devices.close() 

1149 return cls._major 

1150 

1151 

1152class VDI(object): 

1153 """SR.vdi driver decorator for blktap2""" 

1154 

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" 

1160 

1161 ATTACH_DETACH_RETRY_SECS = 120 

1162 

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 

1172 

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 

1177 

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" 

1190 

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 = "" 

1194 

1195 return self.__o_direct, self.__o_direct_reason 

1196 

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) 

1203 

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] 

1216 

1217 def get_tap_type(self): 

1218 vdi_type = self.target.get_vdi_type() 

1219 return VDI._tap_type(vdi_type) 

1220 

1221 def get_phy_path(self): 

1222 return self.target.get_vdi_path() 

1223 

1224 class UnexpectedVDIType(Exception): 

1225 

1226 def __init__(self, vdi_type, target): 

1227 self.vdi_type = vdi_type 

1228 self.target = target 

1229 

1230 @override 

1231 def __str__(self) -> str: 

1232 return \ 

1233 "Target %s has unexpected VDI type '%s'" % \ 

1234 (type(self.target), self.vdi_type) 

1235 

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'} 

1243 

1244 def tap_wanted(self): 

1245 # 1. Let the target vdi_type decide 

1246 

1247 vdi_type = self.target.get_vdi_type() 

1248 

1249 try: 

1250 plug_type = self.VDI_PLUG_TYPE[vdi_type] 

1251 except KeyError: 

1252 raise self.UnexpectedVDIType(vdi_type, 

1253 self.target.vdi) 

1254 

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 

1262 

1263 return False 

1264 

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. 

1270 

1271 def __init__(self, vdi, driver_info): 

1272 self.vdi = vdi 

1273 self._caps = driver_info['capabilities'] 

1274 

1275 def has_cap(self, cap): 

1276 """Determine if target has given capability""" 

1277 return cap in self._caps 

1278 

1279 def attach(self, sr_uuid, vdi_uuid): 

1280 #assert self.has_cap("VDI_ATTACH") 

1281 return self.vdi.attach(sr_uuid, vdi_uuid) 

1282 

1283 def detach(self, sr_uuid, vdi_uuid): 

1284 #assert self.has_cap("VDI_DETACH") 

1285 self.vdi.detach(sr_uuid, vdi_uuid) 

1286 

1287 def activate(self, sr_uuid, vdi_uuid): 

1288 if self.has_cap("VDI_ACTIVATE"): 

1289 return self.vdi.activate(sr_uuid, vdi_uuid) 

1290 

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) 

1296 

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 

1302 

1303 def get_vdi_path(self): 

1304 return self.vdi.path 

1305 

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. 

1312 

1313 BASEDIR: ClassVar[str] = "" 

1314 

1315 def _mklink(self, target) -> None: 

1316 pass 

1317 

1318 @abstractmethod 

1319 def _equals(self, target) -> bool: 

1320 pass 

1321 

1322 def __init__(self, path): 

1323 self._path = path 

1324 

1325 @classmethod 

1326 def from_name(cls, name): 

1327 path = "%s/%s" % (cls.BASEDIR, name) 

1328 return cls(path) 

1329 

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) 

1334 

1335 def path(self): 

1336 return self._path 

1337 

1338 def stat(self): 

1339 return os.stat(self.path()) 

1340 

1341 def mklink(self, target) -> None: 

1342 

1343 path = self.path() 

1344 util.SMlog("%s -> %s" % (self, target)) 

1345 

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) 

1356 

1357 def unlink(self): 

1358 try: 

1359 os.unlink(self.path()) 

1360 except OSError as e: 

1361 if e.errno != errno.ENOENT: 

1362 raise 

1363 

1364 @override 

1365 def __str__(self) -> str: 

1366 path = self.path() 

1367 return "%s(%s)" % (self.__class__.__name__, path) 

1368 

1369 class SymLink(Link): 

1370 """Symlink some file to a common name""" 

1371 

1372 def readlink(self): 

1373 return os.readlink(self.path()) 

1374 

1375 def symlink(self): 

1376 return self.path() 

1377 

1378 @override 

1379 def _mklink(self, target) -> None: 

1380 os.symlink(target, self.path()) 

1381 

1382 @override 

1383 def _equals(self, target) -> bool: 

1384 return self.readlink() == target 

1385 

1386 class DeviceNode(Link): 

1387 """Relink a block device node to a common name""" 

1388 

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) 

1394 

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) 

1399 

1400 @override 

1401 def _mklink(self, target) -> None: 

1402 

1403 st = self._real_stat(target) 

1404 if not S_ISBLK(st.st_mode): 

1405 raise self.NotABlockDevice(target, st) 

1406 

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) 

1410 

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 

1415 

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) 

1420 

1421 class NotABlockDevice(Exception): 

1422 

1423 def __init__(self, path, st): 

1424 self.path = path 

1425 self.st = st 

1426 

1427 @override 

1428 def __str__(self) -> str: 

1429 return "%s is not a block device: %s" % (self.path, self.st) 

1430 

1431 class Hybrid(Link): 

1432 

1433 def __init__(self, path): 

1434 VDI.Link.__init__(self, path) 

1435 self._devnode = VDI.DeviceNode(path) 

1436 self._symlink = VDI.SymLink(path) 

1437 

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) 

1443 

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) 

1451 

1452 @override 

1453 def _equals(self, target) -> bool: 

1454 return self._obj._equals(target) 

1455 

1456 class PhyLink(SymLink): 

1457 BASEDIR = "/dev/sm/phy" 

1458 # NB. Cannot use DeviceNodes, e.g. FileVDIs aren't bdevs. 

1459 

1460 class NBDLink(SymLink): 

1461 

1462 BASEDIR = "/run/blktap-control/nbd" 

1463 

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. 

1471 

1472 @staticmethod 

1473 def _tap_activate(phy_path, vdi_type, sr_uuid, options, pool_size=None): 

1474 

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) 

1481 

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) 

1492 

1493 else: 

1494 util.SMlog("tap.activate: Found %s" % tapdisk) 

1495 

1496 return tapdisk.get_devpath(), tapdisk 

1497 

1498 @staticmethod 

1499 def _tap_deactivate(minor): 

1500 

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) 

1511 

1512 @classmethod 

1513 def tap_pause(cls, session, sr_uuid, vdi_uuid, failfast=False): 

1514 """ 

1515 Pauses the tapdisk. 

1516 

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 

1536 

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 

1552 

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 

1567 

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 

1577 

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 

1596 

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) 

1603 

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 

1648 

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 

1657 

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) 

1669 

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 

1678 

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 

1699 

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) 

1703 

1704 return pool_info 

1705 

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) 

1711 

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) 

1722 

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 = "" 

1732 

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) 

1742 

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 

1749 

1750 return xmlrpc.client.dumps((struct, ), "", True) 

1751 

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) 

1756 

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) 

1765 

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 

1777 

1778 def _get_vdi_chain(self, cowutil, extractUuid) -> List[str]: 

1779 vdi_chain = [] 

1780 path = self.target.get_vdi_path() 

1781 

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 

1790 

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) 

1794 

1795 if not cowutil.isCoalesceableOnRemote(): #We only need to stop the coalesce in case of QCOW2 

1796 return True 

1797 

1798 path = self.target.get_vdi_path() 

1799 

1800 import fjournaler 

1801 import journaler 

1802 from lvmcowutil import LvmCowUtil 

1803 from FileSR import FileVDI 

1804 import lvmcache 

1805 

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) 

1812 

1813 extractUuid = LvmCowUtil.extractUuid 

1814 else: 

1815 journal = fjournaler.Journaler(os.getcwd()) 

1816 extractUuid = FileVDI.extractUuid 

1817 

1818 # Get the VDI chain 

1819 vdi_chain = self._get_vdi_chain(cowutil, extractUuid) 

1820 

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 

1825 

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)) 

1833 

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)) 

1839 

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) 

1847 

1848 return True 

1849 

1850 @locking("VDIUnavailable") 

1851 def _activate_locked(self, sr_uuid, vdi_uuid, options): 

1852 """Wraps target.activate and adds a tapdisk""" 

1853 

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 

1860 

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) 

1872 

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()) 

1876 

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) 

1880 

1881 vdi_type = self.target.get_vdi_type() 

1882 

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 

1885 

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() 

1896 

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) 

1911 

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

1929 

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 

1934 

1935 def _activate(self, sr_uuid, vdi_uuid, options): 

1936 vdi_options = self.target.activate(sr_uuid, vdi_uuid) 

1937 

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 

1952 

1953 return dev_path 

1954 

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) 

1963 

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) 

1974 

1975 @locking("VDIUnavailable") 

1976 def _deactivate_locked(self, sr_uuid, vdi_uuid, caching_params): 

1977 """Wraps target.deactivate and removes a tapdisk""" 

1978 

1979 #util.SMlog("VDI.deactivate %s" % vdi_uuid) 

1980 if self.tap_wanted() and not self._check_tag(vdi_uuid): 

1981 return False 

1982 

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) 

1988 

1989 return True 

1990 

1991 def _resetPhylink(self, sr_uuid, vdi_uuid, path): 

1992 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(path) 

1993 

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 

2001 

2002 def _deactivate(self, sr_uuid, vdi_uuid, caching_params): 

2003 # Shutdown tapdisk 

2004 back_link = self.BackendLink.from_uuid(sr_uuid, vdi_uuid) 

2005 

2006 if not util.pathexists(back_link.path()): 

2007 util.SMlog("Backend path %s does not exist" % back_link.path()) 

2008 return 

2009 

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") 

2015 

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) 

2024 

2025 # Remove the backend link 

2026 back_link.unlink() 

2027 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).unlink() 

2028 

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) 

2038 

2039 self.target.deactivate(sr_uuid, vdi_uuid) 

2040 

2041 def _detach(self, sr_uuid, vdi_uuid): 

2042 self.target.detach(sr_uuid, vdi_uuid) 

2043 

2044 # Remove phy/ 

2045 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).unlink() 

2046 

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) 

2056 

2057 def setup_cache(self, sr_uuid, vdi_uuid, params): 

2058 if params.get(self.CONF_KEY_ALLOW_CACHING) != "true": 

2059 return 

2060 

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 

2065 

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 

2073 

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) 

2081 

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)) 

2086 

2087 return dev_path 

2088 

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") 

2096 

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") 

2100 

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") 

2111 

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) 

2123 

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) 

2128 

2129 def _setup_cache(self, session, sr_uuid, vdi_uuid, local_sr_uuid, 

2130 scratch_mode, options): 

2131 import SR 

2132 import EXTSR 

2133 

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 

2138 

2139 util.SMlog("Setting up cache") 

2140 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent) 

2141 

2142 if shared_target.parent: 

2143 util.SMlog("ERROR: Parent VDI %s has parent, not enabling" % 

2144 shared_target.uuid) 

2145 return 

2146 

2147 SR.registerSR(EXTSR.EXTSR) 

2148 local_sr = SR.SR.from_uuid(session, local_sr_uuid) 

2149 

2150 vdi_type = self.target.get_vdi_type() 

2151 tap_type = VDI._tap_type(vdi_type) 

2152 cowutil = getCowUtil(vdi_type) 

2153 

2154 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid) 

2155 lock.acquire() 

2156 

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 

2169 

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 

2185 

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) 

2191 

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 

2197 

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 

2207 

2208 secondary = "%s:%s" % (vdi_type, self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink()) 

2209 

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 

2227 

2228 lock.release() 

2229 

2230 util.SMlog("Local read cache: %s, local leaf: %s" % 

2231 (read_cache_path, local_leaf_path)) 

2232 

2233 self.tap = leaf_tapdisk 

2234 return leaf_tapdisk.get_devpath() 

2235 

2236 def remove_cache(self, params): 

2237 if not self.target.has_cap("SR_CACHING"): 

2238 return 

2239 

2240 caching = params.get(self.CONF_KEY_ALLOW_CACHING) == "true" 

2241 

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 

2246 

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) 

2249 

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) 

2252 

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 

2258 

2259 for link in links: 

2260 if link.find("tapdev%d" % minor) != -1: 

2261 return True 

2262 

2263 socket_re = re.compile(r'^/.*/nbd\d+\.%d' % minor) 

2264 for s in sockets: 

2265 if socket_re.match(s): 

2266 return True 

2267 

2268 return False 

2269 

2270 def _remove_cache(self, session, local_sr_uuid): 

2271 import SR 

2272 import EXTSR 

2273 

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 

2278 

2279 util.SMlog("Tearing down the cache") 

2280 

2281 shared_target = self.target.vdi.sr.vdi(self.target.vdi.parent) 

2282 

2283 SR.registerSR(EXTSR.EXTSR) 

2284 local_sr = SR.SR.from_uuid(session, local_sr_uuid) 

2285 

2286 lock = Lock(self.LOCK_CACHE_SETUP, shared_target.uuid) 

2287 lock.acquire() 

2288 

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) 

2295 

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 

2311 

2312 lock.release() 

2313 

2314 @staticmethod 

2315 def _no_parent(vdi): 

2316 return vdi.parent is None or vdi.parent == '' 

2317 

2318 

2319PythonKeyError = KeyError 

2320 

2321 

2322class UEventHandler(object): 

2323 

2324 def __init__(self): 

2325 self._action = None 

2326 

2327 class KeyError(PythonKeyError): 

2328 def __init__(self, args): 

2329 super().__init__(args) 

2330 self.key = args[0] 

2331 

2332 @override 

2333 def __str__(self) -> str: 

2334 return \ 

2335 "Key '%s' missing in environment. " % self.key + \ 

2336 "Not called in udev context?" 

2337 

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]) 

2344 

2345 def get_action(self): 

2346 if not self._action: 

2347 self._action = self.getenv('ACTION') 

2348 return self._action 

2349 

2350 class UnhandledEvent(Exception): 

2351 

2352 def __init__(self, event, handler): 

2353 self.event = event 

2354 self.handler = handler 

2355 

2356 @override 

2357 def __str__(self) -> str: 

2358 return "Uevent '%s' not handled by %s" % \ 

2359 (self.event, self.handler.__class__.__name__) 

2360 

2361 ACTIONS: Dict[str, Callable] = {} 

2362 

2363 def run(self): 

2364 

2365 action = self.get_action() 

2366 try: 

2367 fn = self.ACTIONS[action] 

2368 except KeyError: 

2369 raise self.UnhandledEvent(action, self) 

2370 

2371 return fn(self) 

2372 

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) 

2380 

2381 

2382class __BlktapControl(ClassDevice): 

2383 SYSFS_CLASSTYPE = "misc" 

2384 

2385 def __init__(self): 

2386 ClassDevice.__init__(self) 

2387 self._default_pool = None 

2388 

2389 @override 

2390 def sysfs_devname(self) -> str: 

2391 return "blktap!control" 

2392 

2393 class DefaultPool(Attribute): 

2394 SYSFS_NODENAME = "default_pool" 

2395 

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 

2400 

2401 def get_default_pool_name(self): 

2402 return self.get_default_pool_attr().readline() 

2403 

2404 def set_default_pool_name(self, name): 

2405 self.get_default_pool_attr().writeline(name) 

2406 

2407 def get_default_pool(self): 

2408 return BlktapControl.get_pool(self.get_default_pool_name()) 

2409 

2410 def set_default_pool(self, pool): 

2411 self.set_default_pool_name(pool.name) 

2412 

2413 class NoSuchPool(Exception): 

2414 def __init__(self, name): 

2415 self.name = name 

2416 

2417 @override 

2418 def __str__(self) -> str: 

2419 return "No such pool: {}".format(self.name) 

2420 

2421 def get_pool(self, name): 

2422 path = "%s/pools/%s" % (self.sysfs_path(), name) 

2423 

2424 if not os.path.isdir(path): 

2425 raise self.NoSuchPool(name) 

2426 

2427 return PagePool(path) 

2428 

2429BlktapControl = __BlktapControl() 

2430 

2431 

2432class PagePool(KObject): 

2433 

2434 def __init__(self, path): 

2435 self.path = path 

2436 self._size = None 

2437 

2438 @override 

2439 def sysfs_devname(self) -> str: 

2440 return '' 

2441 

2442 def sysfs_path(self): 

2443 return self.path 

2444 

2445 class Size(Attribute): 

2446 SYSFS_NODENAME = "size" 

2447 

2448 def get_size_attr(self): 

2449 if not self._size: 

2450 self._size = self.Size.from_kobject(self) 

2451 return self._size 

2452 

2453 def set_size(self, pages): 

2454 pages = str(pages) 

2455 self.get_size_attr().writeline(pages) 

2456 

2457 def get_size(self): 

2458 pages = self.get_size_attr().readline() 

2459 return int(pages) 

2460 

2461 

2462class BusDevice(KObject): 

2463 

2464 SYSFS_BUSTYPE: ClassVar[str] = "" 

2465 

2466 @classmethod 

2467 def sysfs_bus_path(cls): 

2468 return "/sys/bus/%s" % cls.SYSFS_BUSTYPE 

2469 

2470 def sysfs_path(self): 

2471 path = "%s/devices/%s" % (self.sysfs_bus_path(), 

2472 self.sysfs_devname()) 

2473 

2474 return path 

2475 

2476 

2477class XenbusDevice(BusDevice): 

2478 """Xenbus device, in XS and sysfs""" 

2479 

2480 XBT_NIL = "" 

2481 

2482 XENBUS_DEVTYPE: ClassVar[str] = "" 

2483 

2484 def __init__(self, domid, devid): 

2485 self.domid = int(domid) 

2486 self.devid = int(devid) 

2487 self._xbt = XenbusDevice.XBT_NIL 

2488 

2489 import xen.lowlevel.xs # pylint: disable=import-error 

2490 self.xs = xen.lowlevel.xs.xs() 

2491 

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) 

2498 

2499 return path 

2500 

2501 def _log(self, prio, msg): 

2502 syslog(prio, msg) 

2503 

2504 def info(self, msg): 

2505 self._log(_syslog.LOG_INFO, msg) 

2506 

2507 def warn(self, msg): 

2508 self._log(_syslog.LOG_WARNING, "WARNING: " + msg) 

2509 

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 

2514 

2515 def _xs_write_path(self, path, val): 

2516 self.xs.write(self._xbt, path, val) 

2517 self.info("wrote %s = '%s'" % (path, val)) 

2518 

2519 def _xs_rm_path(self, path): 

2520 self.xs.rm(self._xbt, path) 

2521 self.info("removed %s" % path) 

2522 

2523 def read(self, key): 

2524 return self._xs_read_path(self.xs_path(key)) 

2525 

2526 def has_xs_key(self, key): 

2527 return self.read(key) is not None 

2528 

2529 def write(self, key, val): 

2530 self._xs_write_path(self.xs_path(key), val) 

2531 

2532 def rm(self, key): 

2533 self._xs_rm_path(self.xs_path(key)) 

2534 

2535 def exists(self): 

2536 return self.has_xs_key(None) 

2537 

2538 def begin(self): 

2539 assert(self._xbt == XenbusDevice.XBT_NIL) 

2540 self._xbt = self.xs.transaction_start() 

2541 

2542 def commit(self): 

2543 ok = self.xs.transaction_end(self._xbt, 0) 

2544 self._xbt = XenbusDevice.XBT_NIL 

2545 return ok 

2546 

2547 def abort(self): 

2548 ok = self.xs.transaction_end(self._xbt, 1) 

2549 assert(ok == True) 

2550 self._xbt = XenbusDevice.XBT_NIL 

2551 

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") 

2569 

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) 

2581 

2582 @override 

2583 def sysfs_devname(self) -> str: 

2584 return "%s-%d-%d" % (self.XENBUS_DEVTYPE, 

2585 self.domid, self.devid) 

2586 

2587 @override 

2588 def __str__(self) -> str: 

2589 return self.sysfs_devname() 

2590 

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): 

2596 

2597 name = os.path.basename(path) 

2598 (_type, domid, devid) = name.split('-') 

2599 

2600 yield cls(domid, devid) 

2601 

2602 

2603class XenBackendDevice(XenbusDevice): 

2604 """Xenbus backend device""" 

2605 SYSFS_BUSTYPE = "xen-backend" 

2606 

2607 @classmethod 

2608 def from_xs_path(cls, _path): 

2609 (_backend, _type, domid, devid) = _path.split('/') 

2610 

2611 assert _backend == 'backend' 

2612 assert _type == cls.XENBUS_DEVTYPE 

2613 

2614 domid = int(domid) 

2615 devid = int(devid) 

2616 

2617 return cls(domid, devid) 

2618 

2619 

2620class Blkback(XenBackendDevice): 

2621 """A blkback VBD""" 

2622 

2623 XENBUS_DEVTYPE = "vbd" 

2624 

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 

2631 

2632 class XenstoreValueError(Exception): 

2633 KEY: ClassVar[str] = "" 

2634 

2635 def __init__(self, vbd, _str): 

2636 self.vbd = vbd 

2637 self.str = _str 

2638 

2639 @override 

2640 def __str__(self) -> str: 

2641 return "Backend %s " % self.vbd + \ 

2642 "has %s = %s" % (self.KEY, self.str) 

2643 

2644 class PhysicalDeviceError(XenstoreValueError): 

2645 KEY = "physical-device" 

2646 

2647 class PhysicalDevice(object): 

2648 

2649 def __init__(self, major, minor): 

2650 self.major = int(major) 

2651 self.minor = int(minor) 

2652 

2653 @classmethod 

2654 def from_xbdev(cls, xbdev): 

2655 

2656 phy = xbdev.read("physical-device") 

2657 

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) 

2664 

2665 return cls(major, minor) 

2666 

2667 def makedev(self): 

2668 return os.makedev(self.major, self.minor) 

2669 

2670 def is_tap(self): 

2671 return self.major == Tapdisk.major() 

2672 

2673 @override 

2674 def __str__(self) -> str: 

2675 return "%s:%s" % (self.major, self.minor) 

2676 

2677 @override 

2678 def __eq__(self, other) -> bool: 

2679 return \ 

2680 self.major == other.major and \ 

2681 self.minor == other.minor 

2682 

2683 def get_physical_device(self): 

2684 if not self._phy: 

2685 self._phy = self.PhysicalDevice.from_xbdev(self) 

2686 return self._phy 

2687 

2688 class QueueEvents(Attribute): 

2689 """Blkback sysfs node to select queue-state event 

2690 notifications emitted.""" 

2691 

2692 SYSFS_NODENAME = "queue_events" 

2693 

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) 

2699 

2700 def get_mask(self): 

2701 return int(self.readline(), 0x10) 

2702 

2703 def set_mask(self, mask): 

2704 self.writeline("0x%x" % mask) 

2705 

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 

2710 

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 

2715 

2716 def pause_requested(self): 

2717 return self.has_xs_key("pause") 

2718 

2719 def shutdown_requested(self): 

2720 return self.has_xs_key("shutdown-request") 

2721 

2722 def shutdown_done(self): 

2723 return self.has_xs_key("shutdown-done") 

2724 

2725 def running(self): 

2726 return self.has_xs_key('queue-0/kthread-pid') 

2727 

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 

2735 

2736 if _phy == phy: 

2737 yield dev 

2738 

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) 

2743 

2744 @classmethod 

2745 def find_by_tap(cls, tapdisk): 

2746 return cls.find_by_tap_minor(tapdisk.minor) 

2747 

2748 def has_tap(self): 

2749 

2750 if not self.can_tap(): 

2751 return False 

2752 

2753 phy = self.get_physical_device() 

2754 if phy: 

2755 return phy.is_tap() 

2756 

2757 return False 

2758 

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() 

2763 

2764 except self.PhysicalDeviceError as e: 

2765 vdi_type = self.read("type") 

2766 

2767 self.info("HVM VDI: type=%s" % vdi_type) 

2768 

2769 if e.str is not None or vdi_type != 'file': 

2770 raise 

2771 

2772 return True 

2773 

2774 return False 

2775 

2776 def can_tap(self): 

2777 return not self.is_bare_hvm() 

2778 

2779 

2780class BlkbackEventHandler(UEventHandler): 

2781 

2782 LOG_FACILITY = _syslog.LOG_DAEMON 

2783 

2784 def __init__(self, ident=None, action=None): 

2785 if not ident: 

2786 ident = self.__class__.__name__ 

2787 

2788 self.ident = ident 

2789 self._vbd = None 

2790 self._tapdisk = None 

2791 

2792 UEventHandler.__init__(self) 

2793 

2794 @override 

2795 def run(self) -> None: 

2796 

2797 self.xs_path = self.getenv('XENBUS_PATH') 

2798 openlog(str(self), 0, self.LOG_FACILITY) 

2799 

2800 UEventHandler.run(self) 

2801 

2802 @override 

2803 def __str__(self) -> str: 

2804 

2805 try: 

2806 path = self.xs_path 

2807 except: 

2808 path = None 

2809 

2810 try: 

2811 action = self.get_action() 

2812 except: 

2813 action = None 

2814 

2815 return "%s[%s](%s)" % (self.ident, action, path) 

2816 

2817 def _log(self, prio, msg): 

2818 syslog(prio, msg) 

2819 util.SMlog("%s: " % self + msg) 

2820 

2821 def info(self, msg): 

2822 self._log(_syslog.LOG_INFO, msg) 

2823 

2824 def warn(self, msg): 

2825 self._log(_syslog.LOG_WARNING, "WARNING: " + msg) 

2826 

2827 def error(self, msg): 

2828 self._log(_syslog.LOG_ERR, "ERROR: " + msg) 

2829 

2830 def get_vbd(self): 

2831 if not self._vbd: 

2832 self._vbd = Blkback.from_xs_path(self.xs_path) 

2833 return self._vbd 

2834 

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 # 

2843 

2844 def __add(self): 

2845 vbd = self.get_vbd() 

2846 # Manage blkback transitions 

2847 # self._manage_vbd() 

2848 

2849 vbd.create_physical_device() 

2850 

2851 vbd.signal_hotplug() 

2852 

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) 

2864 

2865 def __change(self): 

2866 vbd = self.get_vbd() 

2867 

2868 # 1. Pause or resume tapdisk (if there is one) 

2869 

2870 if vbd.has_tap(): 

2871 pass 

2872 #self._pause_update_tap() 

2873 

2874 # 2. Signal Xapi.VBD.pause/resume completion 

2875 

2876 self._signal_xapi() 

2877 

2878 def change(self): 

2879 vbd = self.get_vbd() 

2880 

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. 

2884 

2885 while True: 

2886 vbd.begin() 

2887 

2888 if not vbd.exists() or \ 

2889 vbd.shutdown_done(): 

2890 break 

2891 

2892 self.__change() 

2893 

2894 if vbd.commit(): 

2895 return 

2896 

2897 vbd.abort() 

2898 self.info("spurious uevent, ignored.") 

2899 

2900 def remove(self): 

2901 vbd = self.get_vbd() 

2902 

2903 vbd.signal_hotplug(False) 

2904 

2905 ACTIONS = {'add': add, 

2906 'change': change, 

2907 'remove': remove} 

2908 # 

2909 # VDI.pause 

2910 # 

2911 

2912 def _tap_should_pause(self): 

2913 """Enumerate all VBDs on our tapdisk. Returns true iff any was 

2914 paused""" 

2915 

2916 tapdisk = self.get_tapdisk() 

2917 TapState = Tapdisk.PauseState 

2918 

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. 

2925 

2926 next = TapState.RUNNING 

2927 vbds = {} 

2928 

2929 for vbd in Blkback.find_by_tap(tapdisk): 

2930 name = str(vbd) 

2931 

2932 pausing = vbd.pause_requested() 

2933 closing = vbd.shutdown_requested() 

2934 running = vbd.running() 

2935 

2936 if pausing: 

2937 if closing and not running: 

2938 vbds[name] = PAUSED_SHUTDOWN 

2939 else: 

2940 vbds[name] = PAUSED 

2941 next = TapState.PAUSED 

2942 

2943 else: 

2944 vbds[name] = RUNNING 

2945 

2946 self.info("tapdev%d (%s): %s -> %s" 

2947 % (tapdisk.minor, tapdisk.pause_state(), 

2948 vbds, next)) 

2949 

2950 return next == TapState.PAUSED 

2951 

2952 def _pause_update_tap(self): 

2953 vbd = self.get_vbd() 

2954 

2955 if self._tap_should_pause(): 

2956 self._pause_tap() 

2957 else: 

2958 self._resume_tap() 

2959 

2960 def _pause_tap(self): 

2961 tapdisk = self.get_tapdisk() 

2962 

2963 if not tapdisk.is_paused(): 

2964 self.info("pausing %s" % tapdisk) 

2965 tapdisk.pause() 

2966 

2967 def _resume_tap(self): 

2968 tapdisk = self.get_tapdisk() 

2969 

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() 

2974 

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 # 

2985 

2986 def _manage_vbd(self): 

2987 vbd = self.get_vbd() 

2988 # NB. Hook into VBD state transitions. 

2989 

2990 events = vbd.get_queue_events() 

2991 

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 

2997 

2998 events.set_mask(mask) 

2999 self.info("wrote %s = %#02x" % (events.path, mask)) 

3000 

3001 def _signal_xapi(self): 

3002 vbd = self.get_vbd() 

3003 

3004 pausing = vbd.pause_requested() 

3005 closing = vbd.shutdown_requested() 

3006 running = vbd.running() 

3007 

3008 handled = 0 

3009 

3010 if pausing and not running: 

3011 if 'pause-done' not in vbd: 

3012 vbd.write('pause-done', '') 

3013 handled += 1 

3014 

3015 if not pausing: 

3016 if 'pause-done' in vbd: 

3017 vbd.rm('pause-done') 

3018 handled += 1 

3019 

3020 if closing and not running: 

3021 if 'shutdown-done' not in vbd: 

3022 vbd.write('shutdown-done', '') 

3023 handled += 1 

3024 

3025 if handled > 1: 

3026 self.warn("handled %d events, " % handled + 

3027 "pausing=%s closing=%s running=%s" % \ 

3028 (pausing, closing, running)) 

3029 

3030if __name__ == '__main__': 3030 ↛ 3032line 3030 didn't jump to line 3032, because the condition on line 3030 was never true

3031 

3032 import sys 

3033 prog = os.path.basename(sys.argv[0]) 

3034 

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 # 

3041 

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) 

3047 

3048 try: 

3049 cmd = sys.argv[1] 

3050 except IndexError: 

3051 usage(sys.stderr) 

3052 sys.exit(1) 

3053 

3054 try: 

3055 _class, method = cmd.split('.') 

3056 except: 

3057 usage(sys.stderr) 

3058 sys.exit(1) 

3059 

3060 # 

3061 # Local Tapdisks 

3062 # 

3063 

3064 if cmd == 'tap.major': 

3065 

3066 print("%d" % Tapdisk.major()) 

3067 

3068 elif cmd == 'tap.launch': 

3069 

3070 tapdisk = Tapdisk.launch_from_arg(sys.argv[2]) 

3071 print("Launched %s" % tapdisk, file=sys.stderr) 

3072 

3073 elif _class == 'tap': 

3074 

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 

3083 

3084 try: 

3085 attrs['minor'] = int(item) 

3086 continue 

3087 except ValueError: 

3088 pass 

3089 

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 

3097 

3098 attrs['path'] = item 

3099 

3100 if cmd == 'tap.list': 

3101 

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())) 

3109 

3110 elif cmd == 'tap.vbds': 

3111 # Find all Blkback instances for a given tapdisk 

3112 

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() 

3118 

3119 else: 

3120 

3121 if not attrs: 

3122 usage(sys.stderr) 

3123 sys.exit(1) 

3124 

3125 try: 

3126 tapdisk = Tapdisk.get( ** attrs) 

3127 except TypeError: 

3128 usage(sys.stderr) 

3129 sys.exit(1) 

3130 

3131 if cmd == 'tap.shutdown': 

3132 # Shutdown a running tapdisk, or raise 

3133 tapdisk.shutdown() 

3134 print("Shut down %s" % tapdisk, file=sys.stderr) 

3135 

3136 elif cmd == 'tap.pause': 

3137 # Pause an unpaused tapdisk, or raise 

3138 tapdisk.pause() 

3139 print("Paused %s" % tapdisk, file=sys.stderr) 

3140 

3141 elif cmd == 'tap.unpause': 

3142 # Unpause a paused tapdisk, or raise 

3143 tapdisk.unpause() 

3144 print("Unpaused %s" % tapdisk, file=sys.stderr) 

3145 

3146 elif cmd == 'tap.stats': 

3147 # Gather tapdisk status 

3148 stats = tapdisk.stats() 

3149 print("%s:" % tapdisk) 

3150 print(json.dumps(stats, indent=True)) 

3151 

3152 else: 

3153 usage(sys.stderr) 

3154 sys.exit(1) 

3155 

3156 elif cmd == 'vbd.uevent': 

3157 

3158 hnd = BlkbackEventHandler(cmd) 

3159 

3160 if not sys.stdin.isatty(): 

3161 try: 

3162 hnd.run() 

3163 except Exception as e: 

3164 hnd.error("Unhandled Exception: %s" % e) 

3165 

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() 

3174 

3175 elif cmd == 'vbd.list': 

3176 

3177 for vbd in Blkback.find(): 

3178 print(vbd, \ 

3179 "physical-device=%s" % vbd.get_physical_device(), \ 

3180 "pause=%s" % vbd.pause_requested()) 

3181 

3182 else: 

3183 usage(sys.stderr) 

3184 sys.exit(1)