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 http.client 

34import errno 

35import signal 

36import subprocess 

37import syslog as _syslog 

38import glob 

39import json 

40import xs_errors 

41import XenAPI # pylint: disable=import-error 

42import scsiutil 

43from constants import NS_PREFIX_LVM 

44from syslog import openlog, syslog 

45from stat import * # S_ISBLK(), ... 

46from vditype import VdiType 

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 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", "")) 

443 if match and match.group("reason"): 

444 drbd_path = match.group("reason") 

445 if e.get_error_code() in (errno.EROFS, errno.EMEDIUMTYPE) and Tapdisk.abort_linstor_gc(drbd_path): 

446 raise RetryLoop.TransientFailure(e) 

447 raise 

448 

449 unpause_impl() 

450 

451 @classmethod 

452 def shutdown(cls, pid): 

453 # TODO: This should be a real tap-ctl command 

454 os.kill(pid, signal.SIGTERM) 

455 os.waitpid(pid, 0) 

456 

457 @classmethod 

458 def stats(cls, pid, minor): 

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

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

461 

462 @classmethod 

463 def major(cls): 

464 args = ["major"] 

465 major = cls._pread(args) 

466 return int(major) 

467 

468 @classmethod 

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

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

471 cls._pread(args) 

472 

473 @classmethod 

474 def query(cls, pid, minor, quiet=False): 

475 args = ["query", "-p", pid, "-m", minor] 

476 output = cls._pread(args, quiet=quiet) 

477 m = re.match(r"Commit status '(.+)' \((\d+)\/(\d+)\)", output) 

478 status = m.group(1) 

479 coalesced = int(m.group(2)) 

480 total_coalesce = int(m.group(3)) 

481 return (status, coalesced, total_coalesce) 

482 

483 @classmethod 

484 def cancel_commit(cls, pid, minor, wait=True): 

485 args = ["cancel", "-p", pid, "-m", minor] 

486 if wait: 

487 args.append("-w") 

488 cls._pread(args) 

489 

490class TapdiskExists(Exception): 

491 """Tapdisk already running.""" 

492 

493 def __init__(self, tapdisk): 

494 self.tapdisk = tapdisk 

495 

496 @override 

497 def __str__(self) -> str: 

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

499 

500 

501class TapdiskNotRunning(Exception): 

502 """No such Tapdisk.""" 

503 

504 def __init__(self, **attrs): 

505 self.attrs = attrs 

506 

507 @override 

508 def __str__(self) -> str: 

509 items = iter(self.attrs.items()) 

510 attrs = ", ".join("%s=%s" % attr 

511 for attr in items) 

512 return "No such Tapdisk(%s)" % attrs 

513 

514 

515class TapdiskNotUnique(Exception): 

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

517 

518 def __init__(self, tapdisks): 

519 self.tapdisks = tapdisks 

520 

521 @override 

522 def __str__(self) -> str: 

523 tapdisks = map(str, self.tapdisks) 

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

525 

526 

527class TapdiskFailed(Exception): 

528 """Tapdisk launch failure.""" 

529 

530 def __init__(self, arg, err): 

531 self.arg = arg 

532 self.err = err 

533 

534 @override 

535 def __str__(self) -> str: 

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

537 

538 def get_error(self): 

539 return self.err 

540 

541 

542class TapdiskInvalidState(Exception): 

543 """Tapdisk pause/unpause failure""" 

544 

545 def __init__(self, tapdisk): 

546 self.tapdisk = tapdisk 

547 

548 @override 

549 def __str__(self) -> str: 

550 return str(self.tapdisk) 

551 

552 

553def mkdirs(path, mode=0o777): 

554 if not os.path.exists(path): 

555 parent, subdir = os.path.split(path) 

556 assert parent != path 

557 try: 

558 if parent: 

559 mkdirs(parent, mode) 

560 if subdir: 

561 os.mkdir(path, mode) 

562 except OSError as e: 

563 if e.errno != errno.EEXIST: 

564 raise 

565 

566 

567class KObject(object): 

568 

569 SYSFS_CLASSTYPE: ClassVar[str] = "" 

570 

571 @abstractmethod 

572 def sysfs_devname(self) -> str: 

573 pass 

574 

575 

576class Attribute(object): 

577 

578 SYSFS_NODENAME: ClassVar[str] = "" 

579 

580 def __init__(self, path): 

581 self.path = path 

582 

583 @classmethod 

584 def from_kobject(cls, kobj): 

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

586 return cls(path) 

587 

588 class NoSuchAttribute(Exception): 

589 def __init__(self, name): 

590 self.name = name 

591 

592 @override 

593 def __str__(self) -> str: 

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

595 

596 def _open(self, mode='r'): 

597 try: 

598 return open(self.path, mode) 

599 except IOError as e: 

600 if e.errno == errno.ENOENT: 

601 raise self.NoSuchAttribute(self) 

602 raise 

603 

604 def readline(self): 

605 f = self._open('r') 

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

607 f.close() 

608 return s 

609 

610 def writeline(self, val): 

611 f = self._open('w') 

612 f.write(val) 

613 f.close() 

614 

615 

616class ClassDevice(KObject): 

617 

618 @classmethod 

619 def sysfs_class_path(cls): 

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

621 

622 def sysfs_path(self): 

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

624 self.sysfs_devname()) 

625 

626 

627class Blktap(ClassDevice): 

628 

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

630 

631 SYSFS_CLASSTYPE = "blktap2" 

632 

633 def __init__(self, minor): 

634 self.minor = minor 

635 self._pool = None 

636 self._task = None 

637 

638 @classmethod 

639 def allocate(cls): 

640 # FIXME. Should rather go into init. 

641 mkdirs(cls.DEV_BASEDIR) 

642 

643 devname = TapCtl.allocate() 

644 minor = Tapdisk._parse_minor(devname) 

645 return cls(minor) 

646 

647 def free(self): 

648 TapCtl.free(self.minor) 

649 

650 @override 

651 def __str__(self) -> str: 

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

653 

654 @override 

655 def sysfs_devname(self) -> str: 

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

657 

658 class Pool(Attribute): 

659 SYSFS_NODENAME = "pool" 

660 

661 def get_pool_attr(self): 

662 if not self._pool: 

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

664 return self._pool 

665 

666 def get_pool_name(self): 

667 return self.get_pool_attr().readline() 

668 

669 def set_pool_name(self, name): 

670 self.get_pool_attr().writeline(name) 

671 

672 def set_pool_size(self, pages): 

673 self.get_pool().set_size(pages) 

674 

675 def get_pool(self): 

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

677 

678 def set_pool(self, pool): 

679 self.set_pool_name(pool.name) 

680 

681 class Task(Attribute): 

682 SYSFS_NODENAME = "task" 

683 

684 def get_task_attr(self): 

685 if not self._task: 

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

687 return self._task 

688 

689 def get_task_pid(self): 

690 pid = self.get_task_attr().readline() 

691 try: 

692 return int(pid) 

693 except ValueError: 

694 return None 

695 

696 def find_tapdisk(self): 

697 pid = self.get_task_pid() 

698 if pid is None: 

699 return None 

700 

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

702 

703 def get_tapdisk(self): 

704 tapdisk = self.find_tapdisk() 

705 if not tapdisk: 

706 raise TapdiskNotRunning(minor=self.minor) 

707 return tapdisk 

708 

709 

710class Tapdisk(object): 

711 

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

713 

714 def __init__(self, pid, minor, _type, path, state): 

715 self.pid = pid 

716 self.minor = minor 

717 self.type = _type 

718 self.path = path 

719 self.state = state 

720 self._dirty = False 

721 self._blktap = None 

722 

723 @override 

724 def __str__(self) -> str: 

725 state = self.pause_state() 

726 return "Tapdisk(%s, pid=%d, minor=%s, state=%s)" % \ 

727 (self.get_arg(), self.pid, self.minor, state) 

728 

729 @classmethod 

730 def list(cls, **args): 

731 

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

733 

734 args = {'pid': None, 

735 'minor': None, 

736 'state': None, 

737 '_type': None, 

738 'path': None} 

739 

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

741 if key in args: 

742 args[key] = val 

743 

744 if 'args' in row: 744 ↛ 749line 744 didn't jump to line 749, because the condition on line 744 was never false

745 image = Tapdisk.Arg.parse(row['args']) 

746 args['_type'] = image.type 

747 args['path'] = image.path 

748 

749 if None in args.values(): 749 ↛ 750line 749 didn't jump to line 750, because the condition on line 749 was never true

750 continue 

751 

752 yield Tapdisk( ** args) 

753 

754 @classmethod 

755 def find(cls, **args): 

756 

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

758 

759 if len(found) > 1: 759 ↛ 760line 759 didn't jump to line 760, because the condition on line 759 was never true

760 raise TapdiskNotUnique(found) 

761 

762 if found: 

763 return found[0] 

764 

765 return None 

766 

767 @classmethod 

768 def find_by_path(cls, path): 

769 return cls.find(path=path) 

770 

771 @classmethod 

772 def find_by_minor(cls, minor): 

773 return cls.find(minor=minor) 

774 

775 @classmethod 

776 def get(cls, **attrs): 

777 

778 tapdisk = cls.find( ** attrs) 

779 

780 if not tapdisk: 780 ↛ 781line 780 didn't jump to line 781, because the condition on line 780 was never true

781 raise TapdiskNotRunning( ** attrs) 

782 

783 return tapdisk 

784 

785 @classmethod 

786 def from_path(cls, path): 

787 return cls.get(path=path) 

788 

789 @classmethod 

790 def get_pid_for_path(cls, path: str) -> str: 

791 return util.pread2(['/usr/sbin/lsof', '-t', path]).strip() 

792 

793 @classmethod 

794 def from_minor(cls, minor): 

795 pid = None 

796 dev_path = os.path.join(Blktap.DEV_BASEDIR, f"blktap{minor}") 

797 if os.path.exists(dev_path): 797 ↛ 800line 797 didn't jump to line 800, because the condition on line 797 was never false

798 pid = cls.get_pid_for_path(dev_path) 

799 

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

801 

802 @classmethod 

803 def __from_blktap(cls, blktap): 

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

805 tapdisk._blktap = blktap 

806 return tapdisk 

807 

808 def get_blktap(self): 

809 if not self._blktap: 

810 self._blktap = Blktap(self.minor) 

811 return self._blktap 

812 

813 class Arg: 

814 

815 def __init__(self, _type, path): 

816 self.type = _type 

817 self.path = path 

818 

819 @override 

820 def __str__(self) -> str: 

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

822 

823 @classmethod 

824 def parse(cls, arg): 

825 

826 try: 

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

828 except ValueError: 

829 raise cls.InvalidArgument(arg) 

830 

831 if _type not in Tapdisk.TYPES: 831 ↛ 832line 831 didn't jump to line 832, because the condition on line 831 was never true

832 raise cls.InvalidType(_type) 

833 

834 return cls(_type, path) 

835 

836 class InvalidType(Exception): 

837 def __init__(self, _type): 

838 self.type = _type 

839 

840 @override 

841 def __str__(self) -> str: 

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

843 

844 class InvalidArgument(Exception): 

845 def __init__(self, arg): 

846 self.arg = arg 

847 

848 @override 

849 def __str__(self) -> str: 

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

851 

852 def get_arg(self): 

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

854 

855 def get_devpath(self): 

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

857 

858 @classmethod 

859 def launch_from_arg(cls, arg): 

860 arg = cls.Arg.parse(arg) 

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

862 

863 @staticmethod 

864 def cgclassify(pid): 

865 

866 # We dont provide any <controllers>:<path> 

867 # so cgclassify uses /etc/cgrules.conf which 

868 # we have configured in the spec file. 

869 cmd = ["cgclassify", str(pid)] 

870 try: 

871 util.pread2(cmd) 

872 except util.CommandException as e: 

873 util.logException(e) 

874 

875 @staticmethod 

876 def abort_linstor_gc(drbd_path: str) -> bool: 

877 if not LINSTOR_AVAILABLE or not drbd_path.startswith("/dev/drbd/by-res/xcp-volume-"): 

878 return False 

879 

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

881 group_name = LinstorVolumeManager.get_volume_group_name(volume_name) 

882 

883 openers = get_all_volume_openers(volume_name, "0") 

884 

885 session = util.timeout(5, util.get_localAPI_session) 

886 try: 

887 srs = util.get_linstor_srs_uuid(session) 

888 pbd_ref = util.find_pbd_ref_from_dconf_value( 

889 session, srs, "group-name", group_name, LinstorVolumeManager.build_group_name 

890 ) 

891 if pbd_ref: 

892 pbd_rec = session.xenapi.PBD.get_record(pbd_ref) 

893 

894 sr_ref = pbd_rec["SR"] 

895 sr_uuid = session.xenapi.SR.get_uuid(sr_ref) 

896 

897 import cleanup # pylint: disable=C0415 

898 if cleanup.LinstorSR.abort_gc_from_openers_sr(sr_uuid, openers): 

899 return True 

900 else: 

901 util.SMlog(f"Unable to find PBD of LINSTOR group `{group_name}`...") 

902 

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

904 finally: 

905 session.xenapi.session.logout() 

906 

907 return False 

908 

909 @classmethod 

910 def launch_on_tap(cls, blktap, path, _type, options): 

911 drbd_path = path 

912 tapdisk = cls.find_by_path(path) 

913 if tapdisk: 913 ↛ 914line 913 didn't jump to line 914, because the condition on line 913 was never true

914 raise TapdiskExists(tapdisk) 

915 

916 minor = blktap.minor 

917 try: 

918 pid = TapCtl.spawn() 

919 cls.cgclassify(pid) 

920 try: 

921 TapCtl.attach(pid, minor) 

922 

923 try: 

924 retry_open = 0 

925 while True: 

926 try: 

927 TapCtl.open(pid, minor, _type, path, options) 

928 break 

929 except TapCtl.CommandFailure as e: 

930 err = e.get_error_code() 

931 match = TAP_CTL_ERROR_PATTERN.search(e.info.get("errmsg", "")) 

932 if match and match.group("reason"): 932 ↛ 933line 932 didn't jump to line 933, because the condition on line 932 was never true

933 drbd_path = match.group("reason") 

934 if err in (errno.EROFS, errno.EMEDIUMTYPE) and cls.abort_linstor_gc(drbd_path): 934 ↛ 935line 934 didn't jump to line 935, because the condition on line 934 was never true

935 continue 

936 

937 if err in (errno.EIO, errno.EAGAIN, errno.EROFS, errno.EMEDIUMTYPE) and retry_open < 1: 937 ↛ 938line 937 didn't jump to line 938, because the condition on line 937 was never true

938 retry_open += 1 

939 time.sleep(1) 

940 continue 

941 raise 

942 try: 

943 tapdisk = cls.__from_blktap(blktap) 

944 node = '/sys/dev/block/%d:%d' % (tapdisk.major(), tapdisk.minor) 

945 util.set_scheduler_sysfs_node(node, ['none', 'noop']) 

946 return tapdisk 

947 except: 

948 TapCtl.close(pid, minor) 

949 raise 

950 

951 except: 

952 TapCtl.detach(pid, minor) 

953 raise 

954 

955 except: 

956 try: 

957 TapCtl.shutdown(pid) 

958 except: 

959 # Best effort to shutdown 

960 pass 

961 raise 

962 

963 except TapCtl.CommandFailure as ctl: 

964 util.logException(ctl) 

965 if ((path.startswith('/dev/xapi/cd/') or path.startswith('/dev/sr')) and 965 ↛ 969line 965 didn't jump to line 969, because the condition on line 965 was never false

966 ctl.has_status and ctl.get_error_code() == 123): # ENOMEDIUM (No medium found) 

967 raise xs_errors.XenError('TapdiskDriveEmpty') 

968 else: 

969 raise TapdiskFailed(cls.Arg(_type, path), ctl) 

970 

971 @classmethod 

972 def launch(cls, path, _type, rdonly): 

973 blktap = Blktap.allocate() 

974 try: 

975 return cls.launch_on_tap(blktap, path, _type, {"rdonly": rdonly}) 

976 except: 

977 blktap.free() 

978 raise 

979 

980 def shutdown(self, force=False): 

981 

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

983 

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

985 

986 self.get_blktap().free() 

987 

988 def pause(self): 

989 

990 if not self.is_running(): 

991 raise TapdiskInvalidState(self) 

992 

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

994 

995 self._set_dirty() 

996 

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

998 

999 if not self.is_paused(): 

1000 raise TapdiskInvalidState(self) 

1001 

1002 # FIXME: should the arguments be optional? 

1003 if _type is None: 

1004 _type = self.type 

1005 if path is None: 

1006 path = self.path 

1007 

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

1009 cbtlog=cbtlog) 

1010 

1011 self._set_dirty() 

1012 

1013 def stats(self): 

1014 return json.loads(TapCtl.stats(self.pid, self.minor)) 

1015 # 

1016 # NB. dirty/refresh: reload attributes on next access 

1017 # 

1018 

1019 def _set_dirty(self): 

1020 self._dirty = True 

1021 

1022 def _refresh(self, __get): 

1023 t = self.from_minor(__get('minor')) 

1024 self.__init__(t.pid, t.minor, t.type, t.path, t.state) 

1025 

1026 @override 

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

1028 def __get(name): 

1029 # NB. avoid(rec(ursion) 

1030 return object.__getattribute__(self, name) 

1031 

1032 if __get('_dirty') and \ 1032 ↛ 1034line 1032 didn't jump to line 1034, because the condition on line 1032 was never true

1033 name in ['minor', 'type', 'path', 'state']: 

1034 self._refresh(__get) 

1035 self._dirty = False 

1036 

1037 return __get(name) 

1038 

1039 class PauseState: 

1040 RUNNING = 'R' 

1041 PAUSING = 'r' 

1042 PAUSED = 'P' 

1043 

1044 class Flags: 

1045 DEAD = 0x0001 

1046 CLOSED = 0x0002 

1047 QUIESCE_REQUESTED = 0x0004 

1048 QUIESCED = 0x0008 

1049 PAUSE_REQUESTED = 0x0010 

1050 PAUSED = 0x0020 

1051 SHUTDOWN_REQUESTED = 0x0040 

1052 LOCKING = 0x0080 

1053 RETRY_NEEDED = 0x0100 

1054 LOG_DROPPED = 0x0200 

1055 

1056 PAUSE_MASK = PAUSE_REQUESTED | PAUSED 

1057 

1058 def is_paused(self): 

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

1060 

1061 def is_running(self): 

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

1063 

1064 def pause_state(self): 

1065 if self.state & self.Flags.PAUSED: 1065 ↛ 1066line 1065 didn't jump to line 1066, because the condition on line 1065 was never true

1066 return self.PauseState.PAUSED 

1067 

1068 if self.state & self.Flags.PAUSE_REQUESTED: 1068 ↛ 1069line 1068 didn't jump to line 1069, because the condition on line 1068 was never true

1069 return self.PauseState.PAUSING 

1070 

1071 return self.PauseState.RUNNING 

1072 

1073 @staticmethod 

1074 def _parse_minor(devpath): 

1075 regex = r'%s/(blktap|tapdev)(\d+)$' % Blktap.DEV_BASEDIR 

1076 pattern = re.compile(regex) 

1077 groups = pattern.search(devpath) 

1078 if not groups: 

1079 raise Exception("malformed tap device: '%s' (%s) " % (devpath, regex)) 

1080 

1081 minor = groups.group(2) 

1082 return int(minor) 

1083 

1084 _major = None 

1085 

1086 @classmethod 

1087 def major(cls): 

1088 if cls._major: 

1089 return cls._major 

1090 

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

1092 for line in devices: 

1093 

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

1095 if len(row) != 2: 

1096 continue 

1097 

1098 major, name = row 

1099 if name != 'tapdev': 

1100 continue 

1101 

1102 cls._major = int(major) 

1103 break 

1104 

1105 devices.close() 

1106 return cls._major 

1107 

1108 

1109class VDI(object): 

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

1111 

1112 CONF_KEY_ALLOW_CACHING = "vdi_allow_caching" 

1113 CONF_KEY_MODE_ON_BOOT = "vdi_on_boot" 

1114 CONF_KEY_CACHE_SR = "local_cache_sr" 

1115 CONF_KEY_O_DIRECT = "o_direct" 

1116 LOCK_CACHE_SETUP = "cachesetup" 

1117 

1118 ATTACH_DETACH_RETRY_SECS = 120 

1119 

1120 def __init__(self, uuid, target, driver_info): 

1121 self.target = self.TargetDriver(target, driver_info) 

1122 self._vdi_uuid = uuid 

1123 self._session = target.session 

1124 self.xenstore_data = scsiutil.update_XS_SCSIdata(uuid, scsiutil.gen_synthetic_page_data(uuid)) 

1125 self.__o_direct = None 

1126 self.__o_direct_reason = None 

1127 self.lock = Lock("vdi", uuid) 

1128 self.tap = None 

1129 

1130 def get_o_direct_capability(self, options): 

1131 """Returns True/False based on licensing and caching_params""" 

1132 if self.__o_direct is not None: 1132 ↛ 1133line 1132 didn't jump to line 1133, because the condition on line 1132 was never true

1133 return self.__o_direct, self.__o_direct_reason 

1134 

1135 if util.read_caching_is_restricted(self._session): 1135 ↛ 1136line 1135 didn't jump to line 1136, because the condition on line 1135 was never true

1136 self.__o_direct = True 

1137 self.__o_direct_reason = "LICENSE_RESTRICTION" 

1138 elif not ((self.target.vdi.sr.handles("nfs") or self.target.vdi.sr.handles("ext") or self.target.vdi.sr.handles("smb"))): 1138 ↛ 1141line 1138 didn't jump to line 1141, because the condition on line 1138 was never false

1139 self.__o_direct = True 

1140 self.__o_direct_reason = "SR_NOT_SUPPORTED" 

1141 elif options.get("rdonly") and not self.target.vdi.parent: 

1142 self.__o_direct = True 

1143 self.__o_direct_reason = "RO_WITH_NO_PARENT" 

1144 elif options.get(self.CONF_KEY_O_DIRECT): 

1145 self.__o_direct = True 

1146 self.__o_direct_reason = "SR_OVERRIDE" 

1147 

1148 if self.__o_direct is None: 1148 ↛ 1149line 1148 didn't jump to line 1149, because the condition on line 1148 was never true

1149 self.__o_direct = False 

1150 self.__o_direct_reason = "" 

1151 

1152 return self.__o_direct, self.__o_direct_reason 

1153 

1154 @classmethod 

1155 def from_cli(cls, uuid): 

1156 session = XenAPI.xapi_local() 

1157 session.xenapi.login_with_password('root', '', '', 'SM') 

1158 

1159 target = sm.VDI.from_uuid(session, uuid) 

1160 driver_info = target.sr.srcmd.driver_info 

1161 

1162 session.xenapi.session.logout() 

1163 

1164 return cls(uuid, target, driver_info) 

1165 

1166 @staticmethod 

1167 def _tap_type(vdi_type): 

1168 """Map a VDI type (e.g. 'raw') to a tapdisk driver type (e.g. 'aio')""" 

1169 return { 

1170 'raw': 'aio', 

1171 'vhd': 'vhd', 

1172 'qcow2': 'qcow2', 

1173 'iso': 'aio', # for ISO SR 

1174 'aio': 'aio', # for LVHD 

1175 'file': 'aio', 

1176 'phy': 'aio' 

1177 }[vdi_type] 

1178 

1179 def get_tap_type(self): 

1180 vdi_type = self.target.get_vdi_type() 

1181 return VDI._tap_type(vdi_type) 

1182 

1183 def get_phy_path(self): 

1184 return self.target.get_vdi_path() 

1185 

1186 class UnexpectedVDIType(Exception): 

1187 

1188 def __init__(self, vdi_type, target): 

1189 self.vdi_type = vdi_type 

1190 self.target = target 

1191 

1192 @override 

1193 def __str__(self) -> str: 

1194 return \ 

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

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

1197 

1198 VDI_PLUG_TYPE = {'phy': 'phy', # for NETAPP 

1199 'raw': 'phy', 

1200 'aio': 'tap', # for LVM raw nodes 

1201 'iso': 'tap', # for ISOSR 

1202 'file': 'tap', 

1203 'vhd': 'tap', 

1204 'qcow2': 'tap'} 

1205 

1206 def tap_wanted(self): 

1207 # 1. Let the target vdi_type decide 

1208 

1209 vdi_type = self.target.get_vdi_type() 

1210 

1211 try: 

1212 plug_type = self.VDI_PLUG_TYPE[vdi_type] 

1213 except KeyError: 

1214 raise self.UnexpectedVDIType(vdi_type, 

1215 self.target.vdi) 

1216 

1217 if plug_type == 'tap': 1217 ↛ 1219line 1217 didn't jump to line 1219, because the condition on line 1217 was never false

1218 return True 

1219 elif self.target.vdi.sr.handles('udev'): 

1220 return True 

1221 # 2. Otherwise, there may be more reasons 

1222 # 

1223 # .. TBD 

1224 

1225 return False 

1226 

1227 class TargetDriver: 

1228 """Safe target driver access.""" 

1229 # NB. *Must* test caps for optional calls. Some targets 

1230 # actually implement some slots, but do not enable them. Just 

1231 # try/except would risk breaking compatibility. 

1232 

1233 def __init__(self, vdi, driver_info): 

1234 self.vdi = vdi 

1235 self._caps = driver_info['capabilities'] 

1236 

1237 def has_cap(self, cap): 

1238 """Determine if target has given capability""" 

1239 return cap in self._caps 

1240 

1241 def attach(self, sr_uuid, vdi_uuid): 

1242 #assert self.has_cap("VDI_ATTACH") 

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

1244 

1245 def detach(self, sr_uuid, vdi_uuid): 

1246 #assert self.has_cap("VDI_DETACH") 

1247 self.vdi.detach(sr_uuid, vdi_uuid) 

1248 

1249 def activate(self, sr_uuid, vdi_uuid): 

1250 if self.has_cap("VDI_ACTIVATE"): 

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

1252 

1253 def deactivate(self, sr_uuid, vdi_uuid): 

1254 if self.has_cap("VDI_DEACTIVATE"): 

1255 self.vdi.deactivate(sr_uuid, vdi_uuid) 

1256 #def resize(self, sr_uuid, vdi_uuid, size): 

1257 # return self.vdi.resize(sr_uuid, vdi_uuid, size) 

1258 

1259 def get_vdi_type(self): 

1260 _type = self.vdi.vdi_type 

1261 if not _type: 

1262 raise VDI.UnexpectedVDIType(_type, self.vdi) 

1263 return _type 

1264 

1265 def get_vdi_path(self): 

1266 return self.vdi.path 

1267 

1268 class Link(object): 

1269 """Relink a node under a common name""" 

1270 # NB. We have to provide the device node path during 

1271 # VDI.attach, but currently do not allocate the tapdisk minor 

1272 # before VDI.activate. Therefore those link steps where we 

1273 # relink existing devices under deterministic path names. 

1274 

1275 BASEDIR: ClassVar[str] = "" 

1276 

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

1278 pass 

1279 

1280 @abstractmethod 

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

1282 pass 

1283 

1284 def __init__(self, path): 

1285 self._path = path 

1286 

1287 @classmethod 

1288 def from_name(cls, name): 

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

1290 return cls(path) 

1291 

1292 @classmethod 

1293 def from_uuid(cls, sr_uuid, vdi_uuid): 

1294 name = "%s/%s" % (sr_uuid, vdi_uuid) 

1295 return cls.from_name(name) 

1296 

1297 def path(self): 

1298 return self._path 

1299 

1300 def stat(self): 

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

1302 

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

1304 

1305 path = self.path() 

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

1307 

1308 mkdirs(os.path.dirname(path)) 

1309 try: 

1310 self._mklink(target) 

1311 except OSError as e: 

1312 # We do unlink during teardown, but have to stay 

1313 # idempotent. However, a *wrong* target should never 

1314 # be seen. 

1315 if e.errno != errno.EEXIST: 

1316 raise 

1317 assert self._equals(target), "'%s' not equal to '%s'" % (path, target) 

1318 

1319 def unlink(self): 

1320 try: 

1321 os.unlink(self.path()) 

1322 except OSError as e: 

1323 if e.errno != errno.ENOENT: 

1324 raise 

1325 

1326 @override 

1327 def __str__(self) -> str: 

1328 path = self.path() 

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

1330 

1331 class SymLink(Link): 

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

1333 

1334 def readlink(self): 

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

1336 

1337 def symlink(self): 

1338 return self.path() 

1339 

1340 @override 

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

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

1343 

1344 @override 

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

1346 return self.readlink() == target 

1347 

1348 class DeviceNode(Link): 

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

1350 

1351 @classmethod 

1352 def _real_stat(cls, target): 

1353 """stat() not on @target, but its realpath()""" 

1354 _target = os.path.realpath(target) 

1355 return os.stat(_target) 

1356 

1357 @classmethod 

1358 def is_block(cls, target): 

1359 """Whether @target refers to a block device.""" 

1360 return S_ISBLK(cls._real_stat(target).st_mode) 

1361 

1362 @override 

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

1364 

1365 st = self._real_stat(target) 

1366 if not S_ISBLK(st.st_mode): 

1367 raise self.NotABlockDevice(target, st) 

1368 

1369 # set group read for disk group as well as root 

1370 os.mknod(self.path(), st.st_mode | stat.S_IRGRP, st.st_rdev) 

1371 os.chown(self.path(), st.st_uid, grp.getgrnam("disk").gr_gid) 

1372 

1373 @override 

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

1375 target_rdev = self._real_stat(target).st_rdev 

1376 return self.stat().st_rdev == target_rdev 

1377 

1378 def rdev(self): 

1379 st = self.stat() 

1380 assert S_ISBLK(st.st_mode) 

1381 return os.major(st.st_rdev), os.minor(st.st_rdev) 

1382 

1383 class NotABlockDevice(Exception): 

1384 

1385 def __init__(self, path, st): 

1386 self.path = path 

1387 self.st = st 

1388 

1389 @override 

1390 def __str__(self) -> str: 

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

1392 

1393 class Hybrid(Link): 

1394 

1395 def __init__(self, path): 

1396 VDI.Link.__init__(self, path) 

1397 self._devnode = VDI.DeviceNode(path) 

1398 self._symlink = VDI.SymLink(path) 

1399 

1400 def rdev(self): 

1401 st = self.stat() 

1402 if S_ISBLK(st.st_mode): 

1403 return self._devnode.rdev() 

1404 raise self._devnode.NotABlockDevice(self.path(), st) 

1405 

1406 @override 

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

1408 if self._devnode.is_block(target): 

1409 self._obj = self._devnode 

1410 else: 

1411 self._obj = self._symlink 

1412 self._obj.mklink(target) 

1413 

1414 @override 

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

1416 return self._obj._equals(target) 

1417 

1418 class PhyLink(SymLink): 

1419 BASEDIR = "/dev/sm/phy" 

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

1421 

1422 class NBDLink(SymLink): 

1423 

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

1425 

1426 class BackendLink(Hybrid): 

1427 BASEDIR = "/dev/sm/backend" 

1428 # NB. Could be SymLinks as well, but saving major,minor pairs in 

1429 # Links enables neat state capturing when managing Tapdisks. Note 

1430 # that we essentially have a tap-ctl list replacement here. For 

1431 # now make it a 'Hybrid'. Likely to collapse into a DeviceNode as 

1432 # soon as ISOs are tapdisks. 

1433 

1434 @staticmethod 

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

1436 

1437 tapdisk = Tapdisk.find_by_path(phy_path) 

1438 if not tapdisk: 1438 ↛ 1439line 1438 didn't jump to line 1439, because the condition on line 1438 was never true

1439 blktap = Blktap.allocate() 

1440 blktap.set_pool_name(sr_uuid) 

1441 if pool_size: 

1442 blktap.set_pool_size(pool_size) 

1443 

1444 try: 

1445 tapdisk = \ 

1446 Tapdisk.launch_on_tap(blktap, 

1447 phy_path, 

1448 VDI._tap_type(vdi_type), 

1449 options) 

1450 except: 

1451 blktap.free() 

1452 raise 

1453 util.SMlog("tap.activate: Launched %s" % tapdisk) 

1454 

1455 else: 

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

1457 

1458 return tapdisk.get_devpath(), tapdisk 

1459 

1460 @staticmethod 

1461 def _tap_deactivate(minor): 

1462 

1463 try: 

1464 tapdisk = Tapdisk.from_minor(minor) 

1465 except TapdiskNotRunning as e: 

1466 util.SMlog("tap.deactivate: Warning, %s" % e) 

1467 # NB. Should not be here unless the agent refcount 

1468 # broke. Also, a clean shutdown should not have leaked 

1469 # the recorded minor. 

1470 else: 

1471 tapdisk.shutdown() 

1472 util.SMlog("tap.deactivate: Shut down %s" % tapdisk) 

1473 

1474 @classmethod 

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

1476 """ 

1477 Pauses the tapdisk. 

1478 

1479 session: a XAPI session 

1480 sr_uuid: the UUID of the SR on which VDI lives 

1481 vdi_uuid: the UUID of the VDI to pause 

1482 failfast: controls whether the VDI lock should be acquired in a 

1483 non-blocking manner 

1484 """ 

1485 util.SMlog("Pause request for %s" % vdi_uuid) 

1486 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1487 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'paused', 'true') 

1488 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref) 

1489 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1489 ↛ 1490line 1489 didn't jump to line 1490, because the loop on line 1489 never started

1490 host_ref = key[len('host_'):] 

1491 util.SMlog("Calling tap-pause on host %s" % host_ref) 

1492 if not cls.call_pluginhandler(session, host_ref, 

1493 sr_uuid, vdi_uuid, "pause", failfast=failfast): 

1494 # Failed to pause node 

1495 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused') 

1496 return False 

1497 return True 

1498 

1499 @classmethod 

1500 def tap_unpause(cls, session, sr_uuid, vdi_uuid, secondary=None, 

1501 activate_parents=False): 

1502 util.SMlog("Unpause request for %s secondary=%s" % (vdi_uuid, secondary)) 

1503 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1504 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref) 

1505 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1505 ↛ 1506line 1505 didn't jump to line 1506, because the loop on line 1505 never started

1506 host_ref = key[len('host_'):] 

1507 util.SMlog("Calling tap-unpause on host %s" % host_ref) 

1508 if not cls.call_pluginhandler(session, host_ref, 

1509 sr_uuid, vdi_uuid, "unpause", secondary, activate_parents): 

1510 # Failed to unpause node 

1511 return False 

1512 session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused') 

1513 return True 

1514 

1515 @classmethod 

1516 def tap_refresh(cls, session, sr_uuid, vdi_uuid, activate_parents=False): 

1517 util.SMlog("Refresh request for %s" % vdi_uuid) 

1518 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1519 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref) 

1520 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 

1521 host_ref = key[len('host_'):] 

1522 util.SMlog("Calling tap-refresh on host %s" % host_ref) 

1523 if not cls.call_pluginhandler(session, host_ref, 

1524 sr_uuid, vdi_uuid, "refresh", None, 

1525 activate_parents=activate_parents): 

1526 # Failed to refresh node 

1527 return False 

1528 return True 

1529 

1530 @classmethod 

1531 def tap_status(cls, session, vdi_uuid): 

1532 """Return True if disk is attached, false if it isn't""" 

1533 util.SMlog("Disk status request for %s" % vdi_uuid) 

1534 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1535 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref) 

1536 for key in [x for x in sm_config.keys() if x.startswith('host_')]: 1536 ↛ 1537line 1536 didn't jump to line 1537, because the loop on line 1536 never started

1537 return True 

1538 return False 

1539 

1540 @classmethod 

1541 def call_pluginhandler(cls, session, host_ref, sr_uuid, vdi_uuid, action, 

1542 secondary=None, activate_parents=False, failfast=False): 

1543 """Optionally, activate the parent LV before unpausing""" 

1544 try: 

1545 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi_uuid, 

1546 "failfast": str(failfast)} 

1547 if secondary: 

1548 args["secondary"] = secondary 

1549 if activate_parents: 

1550 args["activate_parents"] = "true" 

1551 ret = session.xenapi.host.call_plugin( 

1552 host_ref, PLUGIN_TAP_PAUSE, action, 

1553 args) 

1554 return ret == "True" 

1555 except Exception as e: 

1556 util.logException("BLKTAP2:call_pluginhandler %s" % e) 

1557 return False 

1558 

1559 def _add_tag(self, vdi_uuid, writable): 

1560 util.SMlog("Adding tag to: %s" % vdi_uuid) 

1561 attach_mode = "RO" 

1562 if writable: 

1563 attach_mode = "RW" 

1564 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1565 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host()) 

1566 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref) 

1567 attached_as = util.attached_as(sm_config) 

1568 if NO_MULTIPLE_ATTACH and (attached_as == "RW" or \ 1568 ↛ 1570line 1568 didn't jump to line 1570, because the condition on line 1568 was never true

1569 (attached_as == "RO" and attach_mode == "RW")): 

1570 util.SMlog("need to reset VDI %s" % vdi_uuid) 

1571 if not resetvdis.reset_vdi(self._session, vdi_uuid, force=False, 

1572 term_output=False, writable=writable): 

1573 raise util.SMException("VDI %s not detached cleanly" % vdi_uuid) 

1574 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref) 

1575 if 'relinking' in sm_config: 

1576 util.SMlog("Relinking key found, back-off and retry" % sm_config) 

1577 return False 

1578 if 'paused' in sm_config: 

1579 util.SMlog("Paused or host_ref key found [%s]" % sm_config) 

1580 return False 

1581 try: 

1582 self._session.xenapi.VDI.add_to_sm_config( 

1583 vdi_ref, 'activating', 'True') 

1584 except XenAPI.Failure as e: 

1585 if e.details[0] == 'MAP_DUPLICATE_KEY' and not writable: 

1586 # Someone else is activating - a retry might succeed 

1587 return False 

1588 raise 

1589 host_key = "host_%s" % host_ref 

1590 assert host_key not in sm_config 

1591 self._session.xenapi.VDI.add_to_sm_config(vdi_ref, host_key, 

1592 attach_mode) 

1593 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref) 

1594 if 'paused' in sm_config or 'relinking' in sm_config: 

1595 util.SMlog("Found %s key, aborting" % ( 

1596 'paused' if 'paused' in sm_config else 'relinking')) 

1597 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key) 

1598 self._session.xenapi.VDI.remove_from_sm_config( 

1599 vdi_ref, 'activating') 

1600 return False 

1601 util.SMlog("Activate lock succeeded") 

1602 return True 

1603 

1604 def _check_tag(self, vdi_uuid): 

1605 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1606 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref) 

1607 if 'paused' in sm_config: 

1608 util.SMlog("Paused key found [%s]" % sm_config) 

1609 return False 

1610 return True 

1611 

1612 def _remove_tag(self, vdi_uuid): 

1613 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1614 host_ref = self._session.xenapi.host.get_by_uuid(util.get_this_host()) 

1615 sm_config = self._session.xenapi.VDI.get_sm_config(vdi_ref) 

1616 host_key = "host_%s" % host_ref 

1617 if host_key in sm_config: 

1618 self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, host_key) 

1619 util.SMlog("Removed host key %s for %s" % (host_key, vdi_uuid)) 

1620 else: 

1621 util.SMlog("_remove_tag: host key %s not found, ignore" % host_key) 

1622 

1623 def _get_pool_config(self, pool_name): 

1624 pool_info = dict() 

1625 vdi_ref = self.target.vdi.sr.srcmd.params.get('vdi_ref') 

1626 if not vdi_ref: 1626 ↛ 1629line 1626 didn't jump to line 1629, because the condition on line 1626 was never true

1627 # attach_from_config context: HA disks don't need to be in any 

1628 # special pool 

1629 return pool_info 

1630 

1631 sr_ref = self.target.vdi.sr.srcmd.params.get('sr_ref') 

1632 sr_config = self._session.xenapi.SR.get_other_config(sr_ref) 

1633 vdi_config = self._session.xenapi.VDI.get_other_config(vdi_ref) 

1634 pool_size_str = sr_config.get(POOL_SIZE_KEY) 

1635 pool_name_override = vdi_config.get(POOL_NAME_KEY) 

1636 if pool_name_override: 1636 ↛ 1641line 1636 didn't jump to line 1641, because the condition on line 1636 was never false

1637 pool_name = pool_name_override 

1638 pool_size_override = vdi_config.get(POOL_SIZE_KEY) 

1639 if pool_size_override: 1639 ↛ 1641line 1639 didn't jump to line 1641, because the condition on line 1639 was never false

1640 pool_size_str = pool_size_override 

1641 pool_size = 0 

1642 if pool_size_str: 1642 ↛ 1652line 1642 didn't jump to line 1652, because the condition on line 1642 was never false

1643 try: 

1644 pool_size = int(pool_size_str) 

1645 if pool_size < 1 or pool_size > MAX_FULL_RINGS: 1645 ↛ 1646line 1645 didn't jump to line 1646, because the condition on line 1645 was never true

1646 raise ValueError("outside of range") 

1647 pool_size = NUM_PAGES_PER_RING * pool_size 

1648 except ValueError: 

1649 util.SMlog("Error: invalid mem-pool-size %s" % pool_size_str) 

1650 pool_size = 0 

1651 

1652 pool_info["mem-pool"] = pool_name 

1653 if pool_size: 1653 ↛ 1656line 1653 didn't jump to line 1656, because the condition on line 1653 was never false

1654 pool_info["mem-pool-size"] = str(pool_size) 

1655 

1656 return pool_info 

1657 

1658 def linkNBD(self, sr_uuid, vdi_uuid): 

1659 if self.tap: 

1660 nbd_path = '/run/blktap-control/nbd%d.%d' % (int(self.tap.pid), 

1661 int(self.tap.minor)) 

1662 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).mklink(nbd_path) 

1663 

1664 def attach(self, sr_uuid, vdi_uuid, writable, activate=False, caching_params={}): 

1665 """Return/dev/sm/backend symlink path""" 

1666 self.xenstore_data.update(self._get_pool_config(sr_uuid)) 

1667 if not self.target.has_cap("ATOMIC_PAUSE") or activate: 

1668 util.SMlog("Attach & activate") 

1669 self._attach(sr_uuid, vdi_uuid) 

1670 dev_path = self._activate(sr_uuid, vdi_uuid, 

1671 {"rdonly": not writable}) 

1672 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path) 

1673 self.linkNBD(sr_uuid, vdi_uuid) 

1674 

1675 # Return backend/ link 

1676 back_path = self.BackendLink.from_uuid(sr_uuid, vdi_uuid).path() 

1677 if self.tap_wanted(): 

1678 # Only have NBD if we also have a tap 

1679 nbd_path = "nbd:unix:{}:exportname={}".format( 

1680 VDI.NBDLink.from_uuid(sr_uuid, vdi_uuid).path(), 

1681 vdi_uuid) 

1682 else: 

1683 nbd_path = "" 

1684 

1685 options = {"rdonly": not writable} 

1686 options.update(caching_params) 

1687 o_direct, o_direct_reason = self.get_o_direct_capability(options) 

1688 struct = {'params': back_path, 

1689 'params_nbd': nbd_path, 

1690 'o_direct': o_direct, 

1691 'o_direct_reason': o_direct_reason, 

1692 'xenstore_data': self.xenstore_data} 

1693 util.SMlog('result: %s' % struct) 

1694 

1695 try: 

1696 f = open("%s.attach_info" % back_path, 'a') 

1697 f.write(xmlrpc.client.dumps((struct, ), "", True)) 

1698 f.close() 

1699 except: 

1700 pass 

1701 

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

1703 

1704 def activate(self, sr_uuid, vdi_uuid, writable, caching_params): 

1705 util.SMlog("blktap2.activate") 

1706 options = {"rdonly": not writable} 

1707 options.update(caching_params) 

1708 

1709 sr_ref = self.target.vdi.sr.srcmd.params.get('sr_ref') 

1710 sr_other_config = self._session.xenapi.SR.get_other_config(sr_ref) 

1711 for i in range(self.ATTACH_DETACH_RETRY_SECS): 1711 ↛ 1718line 1711 didn't jump to line 1718, because the loop on line 1711 didn't complete

1712 try: 

1713 if self._activate_locked(sr_uuid, vdi_uuid, options): 

1714 return 

1715 except util.SRBusyException: 

1716 util.SMlog("SR locked, retrying") 

1717 time.sleep(1) 

1718 raise util.SMException("VDI %s locked" % vdi_uuid) 

1719 

1720 def _get_sr_master_host_ref(self) -> str: 

1721 """ 

1722 Give the host ref of the one responsible for Garbage Collection for a SR. 

1723 Meaning this host for a local SR, the master for a shared SR. 

1724 """ 

1725 sr = self.target.vdi.sr 

1726 if sr.is_shared(): 

1727 host_ref = util.get_master_ref(self._session) 

1728 else: 

1729 host_ref = sr.host_ref 

1730 return host_ref 

1731 

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

1733 vdi_chain = [] 

1734 path = self.target.get_vdi_path() 

1735 

1736 #TODO: Need to add handling of error for getParentNoCheck, e.g. corrupted VDI where we can't read parent 

1737 vdi_chain.append(extractUuid(path)) 

1738 parent = cowutil.getParentNoCheck(path) 

1739 while parent: 

1740 vdi_chain.append(extractUuid(parent)) 

1741 parent = cowutil.getParentNoCheck(parent) 

1742 vdi_chain.reverse() 

1743 return vdi_chain 

1744 

1745 def _check_journal_coalesce_chain(self, sr_uuid: str, vdi_uuid: str) -> bool: 

1746 vdi_type = self.target.get_vdi_type() 

1747 cowutil = getCowUtil(vdi_type) 

1748 

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

1750 return True 

1751 

1752 path = self.target.get_vdi_path() 

1753 

1754 import fjournaler 

1755 import journaler 

1756 from lvmcowutil import LvmCowUtil 

1757 from FileSR import FileVDI 

1758 import lvmcache 

1759 

1760 journal: Union[journaler.Journaler, fjournaler.Journaler] 

1761 # Different extractUUID & journaler function for LVMSR and FileSR 

1762 if path.startswith("/dev/"): #TODO: How to identify SR type easily, we could ask XAPI since we have the sruuid (and even ref) 

1763 vgName = "VG_XenStorage-{}".format(sr_uuid) 

1764 lvmCache = lvmcache.LVMCache(vgName) 

1765 journal = journaler.Journaler(lvmCache) 

1766 

1767 extractUuid = LvmCowUtil.extractUuid 

1768 else: 

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

1770 extractUuid = FileVDI.extractUuid 

1771 

1772 # Get the VDI chain 

1773 vdi_chain = self._get_vdi_chain(cowutil, extractUuid) 

1774 

1775 if len(vdi_chain) == 1: 

1776 # We only have a leaf, do nothing 

1777 util.SMlog("VDI {} is only a leaf, continuing...".format(vdi_uuid)) 

1778 return True 

1779 

1780 # Log the chain of active VDI 

1781 level = 0 

1782 util.SMlog("VDI chain:") 

1783 for vdi in vdi_chain: 

1784 prefix = " " * level 

1785 level += 1 

1786 util.SMlog("{}{}".format(prefix, vdi)) 

1787 

1788 vdi_to_cancel = [] 

1789 for entry in journal.getAll("coalesce").keys(): 

1790 if entry in vdi_chain: 

1791 vdi_to_cancel.append(entry) 

1792 util.SMlog("Coalescing VDI {} in chain".format(entry)) 

1793 

1794 # Get the host_ref from the host doing the GC work 

1795 host_ref = self._get_sr_master_host_ref() 

1796 for vdi in vdi_to_cancel: 

1797 args = {"sr_uuid": sr_uuid, "vdi_uuid": vdi} 

1798 util.SMlog("Calling cancel_coalesce_master with args: {}".format(args)) 

1799 self._session.xenapi.host.call_plugin(\ 

1800 host_ref, PLUGIN_ON_SLAVE, "cancel_coalesce_master", args) 

1801 

1802 return True 

1803 

1804 @locking("VDIUnavailable") 

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

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

1807 

1808 #util.SMlog("VDI.activate %s" % vdi_uuid) 

1809 refresh = False 

1810 if self.tap_wanted(): 1810 ↛ 1815line 1810 didn't jump to line 1815, because the condition on line 1810 was never false

1811 if not self._add_tag(vdi_uuid, not options["rdonly"]): 

1812 return False 

1813 refresh = True 

1814 

1815 try: 

1816 if refresh: 1816 ↛ 1827line 1816 didn't jump to line 1827, because the condition on line 1816 was never false

1817 # it is possible that while the VDI was paused some of its 

1818 # attributes have changed (e.g. its size if it was inflated; or its 

1819 # path if it was leaf-coalesced onto a raw LV), so refresh the 

1820 # object completely 

1821 params = self.target.vdi.sr.srcmd.params 

1822 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid) 

1823 target.sr.srcmd.params = params 

1824 driver_info = target.sr.srcmd.driver_info 

1825 self.target = self.TargetDriver(target, driver_info) 

1826 

1827 util.fistpoint.activate_custom_fn( 1827 ↛ exitline 1827 didn't jump to the function exit

1828 "blktap_activate_inject_failure", 

1829 lambda: util.inject_failure()) 

1830 

1831 # Attach the physical node 

1832 if self.target.has_cap("ATOMIC_PAUSE"): 1832 ↛ 1833line 1832 didn't jump to line 1833, because the condition on line 1832 was never true

1833 self._attach(sr_uuid, vdi_uuid) 

1834 

1835 vdi_type = self.target.get_vdi_type() 

1836 

1837 if not self._check_journal_coalesce_chain(sr_uuid, vdi_uuid): 1837 ↛ 1838line 1837 didn't jump to line 1838, because the condition on line 1837 was never true

1838 return False 

1839 

1840 # Take lvchange-p Lock before running 

1841 # tap-ctl open 

1842 # Needed to avoid race with lvchange -p which is 

1843 # now taking the same lock 

1844 # This is a fix for CA-155766 

1845 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1845 ↛ 1848line 1845 didn't jump to line 1848, because the condition on line 1845 was never true

1846 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \ 

1847 VdiType.isCowImage(vdi_type): 

1848 lock = Lock("lvchange-p", NS_PREFIX_LVM + sr_uuid) 

1849 lock.acquire() 

1850 

1851 # When we attach a static VDI for HA, we cannot communicate with 

1852 # xapi, because has not started yet. These VDIs are raw. 

1853 if VdiType.isCowImage(vdi_type): 1853 ↛ 1854line 1853 didn't jump to line 1854, because the condition on line 1853 was never true

1854 session = self.target.vdi.session 

1855 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1856 # pylint: disable=used-before-assignment 

1857 sm_config = session.xenapi.VDI.get_sm_config(vdi_ref) 

1858 if 'key_hash' in sm_config: 

1859 key_hash = sm_config['key_hash'] 

1860 options['key_hash'] = key_hash 

1861 options['vdi_uuid'] = vdi_uuid 

1862 util.SMlog('Using key with hash {} for VDI {}'.format(key_hash, vdi_uuid)) 

1863 # Activate the physical node 

1864 dev_path = self._activate(sr_uuid, vdi_uuid, options) 

1865 

1866 if hasattr(self.target.vdi.sr, 'DRIVER_TYPE') and \ 1866 ↛ 1869line 1866 didn't jump to line 1869, because the condition on line 1866 was never true

1867 self.target.vdi.sr.DRIVER_TYPE == 'lvhd' and \ 

1868 VdiType.isCowImage(self.target.get_vdi_type()): 

1869 lock.release() 

1870 except: 

1871 util.SMlog("Exception in activate/attach") 

1872 if self.tap_wanted(): 

1873 util.fistpoint.activate_custom_fn( 

1874 "blktap_activate_error_handling", 

1875 lambda: time.sleep(30)) 

1876 while True: 

1877 try: 

1878 self._remove_tag(vdi_uuid) 

1879 break 

1880 except xmlrpc.client.ProtocolError as e: 

1881 # If there's a connection error, keep trying forever. 

1882 if e.errcode == http.HTTPStatus.INTERNAL_SERVER_ERROR.value: 

1883 continue 

1884 else: 

1885 util.SMlog('failed to remove tag: %s' % e) 

1886 break 

1887 except Exception as e: 

1888 util.SMlog('failed to remove tag: %s' % e) 

1889 break 

1890 raise 

1891 finally: 

1892 vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid) 

1893 self._session.xenapi.VDI.remove_from_sm_config( 

1894 vdi_ref, 'activating') 

1895 util.SMlog("Removed activating flag from %s" % vdi_uuid) 1895 ↛ exitline 1895 didn't except from function '_activate_locked', because the raise on line 1890 wasn't executed or line 1895 didn't return from function '_activate_locked', because the return on line 1838 wasn't executed

1896 

1897 # Link result to backend/ 

1898 self.BackendLink.from_uuid(sr_uuid, vdi_uuid).mklink(dev_path) 

1899 self.linkNBD(sr_uuid, vdi_uuid) 

1900 return True 

1901 

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

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

1904 

1905 dev_path = self.setup_cache(sr_uuid, vdi_uuid, options) 

1906 if not dev_path: 1906 ↛ 1920line 1906 didn't jump to line 1920, because the condition on line 1906 was never false

1907 phy_path = self.PhyLink.from_uuid(sr_uuid, vdi_uuid).readlink() 

1908 # Maybe launch a tapdisk on the physical link 

1909 if self.tap_wanted(): 1909 ↛ 1918line 1909 didn't jump to line 1918, because the condition on line 1909 was never false

1910 vdi_type = self.target.get_vdi_type() 

1911 options["o_direct"] = self.get_o_direct_capability(options)[0] 

1912 if vdi_options: 1912 ↛ 1914line 1912 didn't jump to line 1914, because the condition on line 1912 was never false

1913 options.update(vdi_options) 

1914 dev_path, self.tap = self._tap_activate(phy_path, vdi_type, 

1915 sr_uuid, options, 

1916 self._get_pool_config(sr_uuid).get("mem-pool-size")) 

1917 else: 

1918 dev_path = phy_path # Just reuse phy 

1919 

1920 return dev_path 

1921 

1922 def _attach(self, sr_uuid, vdi_uuid): 

1923 attach_info = xmlrpc.client.loads(self.target.attach(sr_uuid, vdi_uuid))[0][0] 

1924 params = attach_info['params'] 

1925 xenstore_data = attach_info['xenstore_data'] 

1926 phy_path = util.to_plain_string(params) 

1927 self.xenstore_data.update(xenstore_data) 

1928 # Save it to phy/ 

1929 self.PhyLink.from_uuid(sr_uuid, vdi_uuid).mklink(phy_path) 

1930 

1931 def deactivate(self, sr_uuid, vdi_uuid, caching_params): 

1932 util.SMlog("blktap2.deactivate") 

1933 for i in range(self.ATTACH_DETACH_RETRY_SECS): 

1934 try: 

1935 if self._deactivate_locked(sr_uuid, vdi_uuid, caching_params): 

1936 return 

1937 except util.SRBusyException as e: 

1938 util.SMlog("SR locked, retrying") 

1939 time.sleep(1) 

1940 raise util.SMException("VDI %s locked" % vdi_uuid) 

1941 

1942 @locking("VDIUnavailable") 

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

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

1945 

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

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

1948 return False 

1949 

1950 self._deactivate(sr_uuid, vdi_uuid, caching_params) 

1951 if self.target.has_cap("ATOMIC_PAUSE"): 

1952 self._detach(sr_uuid, vdi_uuid) 

1953 if self.tap_wanted(): 

1954 self._remove_tag(vdi_uuid) 

1955 

1956 return True 

1957 

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

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

1960 

1961 def detach(self, sr_uuid, vdi_uuid, deactivate=False, caching_params={}): 

1962 if not self.target.has_cap("ATOMIC_PAUSE") or deactivate: 

1963 util.SMlog("Deactivate & detach") 

1964 self._deactivate(sr_uuid, vdi_uuid, caching_params) 

1965 self._detach(sr_uuid, vdi_uuid) 

1966 else: 

1967 pass # nothing to do 

1968 

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

1970 # Shutdown tapdisk 

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

1972 

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

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

1975 return 

1976 

1977 try: 

1978 attach_info_path = "%s.attach_info" % (back_link.path()) 

1979 os.unlink(attach_info_path) 

1980 except: 

1981 util.SMlog("unlink of attach_info failed") 

1982 

1983 try: 

1984 major, minor = back_link.rdev() 

1985 except self.DeviceNode.NotABlockDevice: 

1986 pass 

1987 else: 

1988 if major == Tapdisk.major(): 

1989 self._tap_deactivate(minor) 

1990 self.remove_cache(caching_params) 

1991 

1992 # Remove the backend link 

1993 back_link.unlink() 

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

1995 

1996 # Deactivate & detach the physical node 

1997 if self.tap_wanted() and self.target.vdi.session is not None: 

1998 # it is possible that while the VDI was paused some of its 

1999 # attributes have changed (e.g. its size if it was inflated; or its 

2000 # path if it was leaf-coalesced onto a raw LV), so refresh the 

2001 # object completely 

2002 target = sm.VDI.from_uuid(self.target.vdi.session, vdi_uuid) 

2003 driver_info = target.sr.srcmd.driver_info 

2004 self.target = self.TargetDriver(target, driver_info) 

2005 

2006 self.target.deactivate(sr_uuid, vdi_uuid) 

2007 

2008 def _detach(self, sr_uuid, vdi_uuid): 

2009 self.target.detach(sr_uuid, vdi_uuid) 

2010 

2011 # Remove phy/ 

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

2013 

2014 def _updateCacheRecord(self, session, vdi_uuid, on_boot, caching): 

2015 # Remove existing VDI.sm_config fields 

2016 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

2017 for key in ["on_boot", "caching"]: 

2018 session.xenapi.VDI.remove_from_sm_config(vdi_ref, key) 

2019 if not on_boot is None: 2019 ↛ 2020line 2019 didn't jump to line 2020, because the condition on line 2019 was never true

2020 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'on_boot', on_boot) 

2021 if not caching is None: 

2022 session.xenapi.VDI.add_to_sm_config(vdi_ref, 'caching', caching) 

2023 

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

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

2026 return 

2027 

2028 util.SMlog("Requested local caching") 

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

2030 util.SMlog("Error: local caching not supported by this SR") 

2031 return 

2032 

2033 scratch_mode = False 

2034 if params.get(self.CONF_KEY_MODE_ON_BOOT) == "reset": 

2035 scratch_mode = True 

2036 util.SMlog("Requested scratch mode") 

2037 if not self.target.has_cap("VDI_RESET_ON_BOOT/2"): 2037 ↛ 2041line 2037 didn't jump to line 2041, because the condition on line 2037 was never false

2038 util.SMlog("Error: scratch mode not supported by this SR") 

2039 return 

2040 

2041 dev_path = None 

2042 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR) 

2043 if not local_sr_uuid: 

2044 util.SMlog("ERROR: Local cache SR not specified, not enabling") 

2045 return 

2046 dev_path = self._setup_cache(self._session, sr_uuid, vdi_uuid, 

2047 local_sr_uuid, scratch_mode, params) 

2048 

2049 if dev_path: 

2050 self._updateCacheRecord(self._session, self.target.vdi.uuid, 

2051 params.get(self.CONF_KEY_MODE_ON_BOOT), 

2052 params.get(self.CONF_KEY_ALLOW_CACHING)) 

2053 

2054 return dev_path 

2055 

2056 def alert_no_cache(self, session, vdi_uuid, cache_sr_uuid, err): 

2057 vm_uuid = None 

2058 vm_label = "" 

2059 try: 

2060 cache_sr_ref = session.xenapi.SR.get_by_uuid(cache_sr_uuid) 

2061 cache_sr_rec = session.xenapi.SR.get_record(cache_sr_ref) 

2062 cache_sr_label = cache_sr_rec.get("name_label") 

2063 

2064 host_ref = session.xenapi.host.get_by_uuid(util.get_this_host()) 

2065 host_rec = session.xenapi.host.get_record(host_ref) 

2066 host_label = host_rec.get("name_label") 

2067 

2068 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) 

2069 vbds = session.xenapi.VBD.get_all_records_where( \ 

2070 "field \"VDI\" = \"%s\"" % vdi_ref) 

2071 for vbd_rec in vbds.values(): 

2072 vm_ref = vbd_rec.get("VM") 

2073 vm_rec = session.xenapi.VM.get_record(vm_ref) 

2074 vm_uuid = vm_rec.get("uuid") 

2075 vm_label = vm_rec.get("name_label") 

2076 except: 

2077 util.logException("alert_no_cache") 

2078 

2079 alert_obj = "SR" 

2080 alert_uuid = str(cache_sr_uuid) 

2081 alert_str = "No space left in Local Cache SR %s" % cache_sr_uuid 

2082 if vm_uuid: 

2083 alert_obj = "VM" 

2084 alert_uuid = vm_uuid 

2085 reason = "" 

2086 if err == errno.ENOSPC: 

2087 reason = "because there is no space left" 

2088 alert_str = "The VM \"%s\" is not using IntelliCache %s on the Local Cache SR (\"%s\") on host \"%s\"" % \ 

2089 (vm_label, reason, cache_sr_label, host_label) 

2090 

2091 util.SMlog("Creating alert: (%s, %s, \"%s\")" % \ 

2092 (alert_obj, alert_uuid, alert_str)) 

2093 session.xenapi.message.create("No space left in local cache", "3", 

2094 alert_obj, alert_uuid, alert_str) 

2095 

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

2097 scratch_mode, options): 

2098 import SR 

2099 import EXTSR 

2100 

2101 if self._no_parent(self.target.vdi): 2101 ↛ 2102line 2101 didn't jump to line 2102, because the condition on line 2101 was never true

2102 util.SMlog("ERROR: VDI %s has no parent, not enabling" % 

2103 self.target.vdi.uuid) 

2104 return 

2105 

2106 util.SMlog("Setting up cache") 

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

2108 

2109 if shared_target.parent: 

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

2111 shared_target.uuid) 

2112 return 

2113 

2114 SR.registerSR(EXTSR.EXTSR) 

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

2116 

2117 vdi_type = self.target.get_vdi_type() 

2118 tap_type = VDI._tap_type(vdi_type) 

2119 cowutil = getCowUtil(vdi_type) 

2120 

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

2122 lock.acquire() 

2123 

2124 # read cache 

2125 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid) 

2126 if util.pathexists(read_cache_path): 2126 ↛ 2130line 2126 didn't jump to line 2130, because the condition on line 2126 was never false

2127 util.SMlog("Read cache node (%s) already exists, not creating" % 

2128 read_cache_path) 

2129 else: 

2130 try: 

2131 cowutil.snapshot(read_cache_path, shared_target.path, False) 

2132 except util.CommandException as e: 

2133 util.SMlog("Error creating parent cache: %s" % e) 

2134 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code) 

2135 return None 

2136 

2137 # local write node 

2138 leaf_size = cowutil.getSizeVirt(self.target.vdi.path) 

2139 local_leaf_path = "%s/%s.vhdcache" % \ 

2140 (local_sr.path, self.target.vdi.uuid) 

2141 if util.pathexists(local_leaf_path): 2141 ↛ 2145line 2141 didn't jump to line 2145, because the condition on line 2141 was never false

2142 util.SMlog("Local leaf node (%s) already exists, deleting" % 

2143 local_leaf_path) 

2144 os.unlink(local_leaf_path) 

2145 try: 

2146 cowutil.snapshot(local_leaf_path, read_cache_path, False, 

2147 msize=leaf_size, checkEmpty=False) 

2148 except util.CommandException as e: 

2149 util.SMlog("Error creating leaf cache: %s" % e) 

2150 self.alert_no_cache(session, vdi_uuid, local_sr_uuid, e.code) 

2151 return None 

2152 

2153 local_leaf_size = cowutil.getSizeVirt(local_leaf_path) 

2154 if leaf_size > local_leaf_size: 2154 ↛ 2155line 2154 didn't jump to line 2155, because the condition on line 2154 was never true

2155 util.SMlog("Leaf size %d > local leaf cache size %d, resizing" % 

2156 (leaf_size, local_leaf_size)) 

2157 cowutil.setSizeVirtFast(local_leaf_path, leaf_size) 

2158 

2159 prt_tapdisk = Tapdisk.find_by_path(read_cache_path) 

2160 if not prt_tapdisk: 

2161 parent_options = copy.deepcopy(options) 

2162 parent_options["rdonly"] = False 

2163 parent_options["lcache"] = True 

2164 

2165 blktap = Blktap.allocate() 

2166 try: 

2167 blktap.set_pool_name("lcache-parent-pool-%s" % blktap.minor) 

2168 # no need to change pool_size since each parent tapdisk is in 

2169 # its own pool 

2170 prt_tapdisk = Tapdisk.launch_on_tap(blktap, read_cache_path, tap_type, parent_options) 

2171 except: 

2172 blktap.free() 

2173 raise 

2174 

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

2176 

2177 util.SMlog("Parent tapdisk: %s" % prt_tapdisk) 

2178 leaf_tapdisk = Tapdisk.find_by_path(local_leaf_path) 

2179 if not leaf_tapdisk: 2179 ↛ 2195line 2179 didn't jump to line 2195, because the condition on line 2179 was never false

2180 blktap = Blktap.allocate() 

2181 child_options = copy.deepcopy(options) 

2182 child_options["rdonly"] = False 

2183 child_options["lcache"] = (not scratch_mode) 

2184 child_options["existing_prt"] = prt_tapdisk.minor 

2185 child_options["secondary"] = secondary 

2186 child_options["standby"] = scratch_mode 

2187 # Disable memory read caching 

2188 child_options.pop("o_direct", None) 

2189 try: 

2190 leaf_tapdisk = Tapdisk.launch_on_tap(blktap, local_leaf_path, tap_type, child_options) 

2191 except: 

2192 blktap.free() 

2193 raise 

2194 

2195 lock.release() 

2196 

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

2198 (read_cache_path, local_leaf_path)) 

2199 

2200 self.tap = leaf_tapdisk 

2201 return leaf_tapdisk.get_devpath() 

2202 

2203 def remove_cache(self, params): 

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

2205 return 

2206 

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

2208 

2209 local_sr_uuid = params.get(self.CONF_KEY_CACHE_SR) 

2210 if caching and not local_sr_uuid: 

2211 util.SMlog("ERROR: Local cache SR not specified, ignore") 

2212 return 

2213 

2214 if caching: 2214 ↛ 2217line 2214 didn't jump to line 2217, because the condition on line 2214 was never false

2215 self._remove_cache(self._session, local_sr_uuid) 

2216 

2217 if self._session is not None: 2217 ↛ exitline 2217 didn't return from function 'remove_cache', because the condition on line 2217 was never false

2218 self._updateCacheRecord(self._session, self.target.vdi.uuid, None, None) 

2219 

2220 def _is_tapdisk_in_use(self, minor): 

2221 retVal, links, sockets = util.findRunningProcessOrOpenFile("tapdisk") 

2222 if not retVal: 

2223 # err on the side of caution 

2224 return True 

2225 

2226 for link in links: 

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

2228 return True 

2229 

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

2231 for s in sockets: 

2232 if socket_re.match(s): 

2233 return True 

2234 

2235 return False 

2236 

2237 def _remove_cache(self, session, local_sr_uuid): 

2238 import SR 

2239 import EXTSR 

2240 

2241 if self._no_parent(self.target.vdi): 

2242 util.SMlog("ERROR: No parent for VDI %s, ignore" % 

2243 self.target.vdi.uuid) 

2244 return 

2245 

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

2247 

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

2249 

2250 SR.registerSR(EXTSR.EXTSR) 

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

2252 

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

2254 lock.acquire() 

2255 

2256 # local write node 

2257 local_leaf_path = "%s/%s.vhdcache" % \ 

2258 (local_sr.path, self.target.vdi.uuid) 

2259 if util.pathexists(local_leaf_path): 2259 ↛ 2263line 2259 didn't jump to line 2263, because the condition on line 2259 was never false

2260 util.SMlog("Deleting local leaf node %s" % local_leaf_path) 

2261 os.unlink(local_leaf_path) 

2262 

2263 read_cache_path = "%s/%s.vhdcache" % (local_sr.path, shared_target.uuid) 

2264 prt_tapdisk = Tapdisk.find_by_path(read_cache_path) 

2265 if not prt_tapdisk: 2265 ↛ 2266line 2265 didn't jump to line 2266, because the condition on line 2265 was never true

2266 util.SMlog("Parent tapdisk not found") 

2267 elif not self._is_tapdisk_in_use(prt_tapdisk.minor): 2267 ↛ 2275line 2267 didn't jump to line 2275, because the condition on line 2267 was never false

2268 util.SMlog("Parent tapdisk not in use: shutting down %s" % 

2269 read_cache_path) 

2270 try: 

2271 prt_tapdisk.shutdown() 

2272 except: 

2273 util.logException("shutting down parent tapdisk") 

2274 else: 

2275 util.SMlog("Parent tapdisk still in use: %s" % read_cache_path) 

2276 # the parent cache files are removed during the local SR's background 

2277 # GC run 

2278 

2279 lock.release() 

2280 

2281 @staticmethod 

2282 def _no_parent(vdi): 

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

2284 

2285 

2286PythonKeyError = KeyError 

2287 

2288 

2289class UEventHandler(object): 

2290 

2291 def __init__(self): 

2292 self._action = None 

2293 

2294 class KeyError(PythonKeyError): 

2295 def __init__(self, args): 

2296 super().__init__(args) 

2297 self.key = args[0] 

2298 

2299 @override 

2300 def __str__(self) -> str: 

2301 return \ 

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

2303 "Not called in udev context?" 

2304 

2305 @classmethod 

2306 def getenv(cls, key): 

2307 try: 

2308 return os.environ[key] 

2309 except KeyError as e: 

2310 raise cls.KeyError(e.args[0]) 

2311 

2312 def get_action(self): 

2313 if not self._action: 

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

2315 return self._action 

2316 

2317 class UnhandledEvent(Exception): 

2318 

2319 def __init__(self, event, handler): 

2320 self.event = event 

2321 self.handler = handler 

2322 

2323 @override 

2324 def __str__(self) -> str: 

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

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

2327 

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

2329 

2330 def run(self): 

2331 

2332 action = self.get_action() 

2333 try: 

2334 fn = self.ACTIONS[action] 

2335 except KeyError: 

2336 raise self.UnhandledEvent(action, self) 

2337 

2338 return fn(self) 

2339 

2340 @override 

2341 def __str__(self) -> str: 

2342 try: 

2343 action = self.get_action() 

2344 except: 

2345 action = None 

2346 return "%s[%s]" % (self.__class__.__name__, action) 

2347 

2348 

2349class __BlktapControl(ClassDevice): 

2350 SYSFS_CLASSTYPE = "misc" 

2351 

2352 def __init__(self): 

2353 ClassDevice.__init__(self) 

2354 self._default_pool = None 

2355 

2356 @override 

2357 def sysfs_devname(self) -> str: 

2358 return "blktap!control" 

2359 

2360 class DefaultPool(Attribute): 

2361 SYSFS_NODENAME = "default_pool" 

2362 

2363 def get_default_pool_attr(self): 

2364 if not self._default_pool: 

2365 self._default_pool = self.DefaultPool.from_kobject(self) 

2366 return self._default_pool 

2367 

2368 def get_default_pool_name(self): 

2369 return self.get_default_pool_attr().readline() 

2370 

2371 def set_default_pool_name(self, name): 

2372 self.get_default_pool_attr().writeline(name) 

2373 

2374 def get_default_pool(self): 

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

2376 

2377 def set_default_pool(self, pool): 

2378 self.set_default_pool_name(pool.name) 

2379 

2380 class NoSuchPool(Exception): 

2381 def __init__(self, name): 

2382 self.name = name 

2383 

2384 @override 

2385 def __str__(self) -> str: 

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

2387 

2388 def get_pool(self, name): 

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

2390 

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

2392 raise self.NoSuchPool(name) 

2393 

2394 return PagePool(path) 

2395 

2396BlktapControl = __BlktapControl() 

2397 

2398 

2399class PagePool(KObject): 

2400 

2401 def __init__(self, path): 

2402 self.path = path 

2403 self._size = None 

2404 

2405 @override 

2406 def sysfs_devname(self) -> str: 

2407 return '' 

2408 

2409 def sysfs_path(self): 

2410 return self.path 

2411 

2412 class Size(Attribute): 

2413 SYSFS_NODENAME = "size" 

2414 

2415 def get_size_attr(self): 

2416 if not self._size: 

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

2418 return self._size 

2419 

2420 def set_size(self, pages): 

2421 pages = str(pages) 

2422 self.get_size_attr().writeline(pages) 

2423 

2424 def get_size(self): 

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

2426 return int(pages) 

2427 

2428 

2429class BusDevice(KObject): 

2430 

2431 SYSFS_BUSTYPE: ClassVar[str] = "" 

2432 

2433 @classmethod 

2434 def sysfs_bus_path(cls): 

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

2436 

2437 def sysfs_path(self): 

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

2439 self.sysfs_devname()) 

2440 

2441 return path 

2442 

2443 

2444class XenbusDevice(BusDevice): 

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

2446 

2447 XBT_NIL = "" 

2448 

2449 XENBUS_DEVTYPE: ClassVar[str] = "" 

2450 

2451 def __init__(self, domid, devid): 

2452 self.domid = int(domid) 

2453 self.devid = int(devid) 

2454 self._xbt = XenbusDevice.XBT_NIL 

2455 

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

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

2458 

2459 def xs_path(self, key=None): 

2460 path = "backend/%s/%d/%d" % (self.XENBUS_DEVTYPE, 

2461 self.domid, 

2462 self.devid) 

2463 if key is not None: 

2464 path = "%s/%s" % (path, key) 

2465 

2466 return path 

2467 

2468 def _log(self, prio, msg): 

2469 syslog(prio, msg) 

2470 

2471 def info(self, msg): 

2472 self._log(_syslog.LOG_INFO, msg) 

2473 

2474 def warn(self, msg): 

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

2476 

2477 def _xs_read_path(self, path): 

2478 val = self.xs.read(self._xbt, path) 

2479 #self.info("read %s = '%s'" % (path, val)) 

2480 return val 

2481 

2482 def _xs_write_path(self, path, val): 

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

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

2485 

2486 def _xs_rm_path(self, path): 

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

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

2489 

2490 def read(self, key): 

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

2492 

2493 def has_xs_key(self, key): 

2494 return self.read(key) is not None 

2495 

2496 def write(self, key, val): 

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

2498 

2499 def rm(self, key): 

2500 self._xs_rm_path(self.xs_path(key)) 

2501 

2502 def exists(self): 

2503 return self.has_xs_key(None) 

2504 

2505 def begin(self): 

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

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

2508 

2509 def commit(self): 

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

2511 self._xbt = XenbusDevice.XBT_NIL 

2512 return ok 

2513 

2514 def abort(self): 

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

2516 assert(ok == True) 

2517 self._xbt = XenbusDevice.XBT_NIL 

2518 

2519 def create_physical_device(self): 

2520 """The standard protocol is: toolstack writes 'params', linux hotplug 

2521 script translates this into physical-device=%x:%x""" 

2522 if self.has_xs_key("physical-device"): 

2523 return 

2524 try: 

2525 params = self.read("params") 

2526 frontend = self.read("frontend") 

2527 is_cdrom = self._xs_read_path("%s/device-type") == "cdrom" 

2528 # We don't have PV drivers for CDROM devices, so we prevent blkback 

2529 # from opening the physical-device 

2530 if not(is_cdrom): 

2531 major_minor = os.stat(params).st_rdev 

2532 major, minor = divmod(major_minor, 256) 

2533 self.write("physical-device", "%x:%x" % (major, minor)) 

2534 except: 

2535 util.logException("BLKTAP2:create_physical_device") 

2536 

2537 def signal_hotplug(self, online=True): 

2538 xapi_path = "/xapi/%d/hotplug/%s/%d/hotplug" % (self.domid, 

2539 self.XENBUS_DEVTYPE, 

2540 self.devid) 

2541 upstream_path = self.xs_path("hotplug-status") 

2542 if online: 

2543 self._xs_write_path(xapi_path, "online") 

2544 self._xs_write_path(upstream_path, "connected") 

2545 else: 

2546 self._xs_rm_path(xapi_path) 

2547 self._xs_rm_path(upstream_path) 

2548 

2549 @override 

2550 def sysfs_devname(self) -> str: 

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

2552 self.domid, self.devid) 

2553 

2554 @override 

2555 def __str__(self) -> str: 

2556 return self.sysfs_devname() 

2557 

2558 @classmethod 

2559 def find(cls): 

2560 pattern = "/sys/bus/%s/devices/%s*" % (cls.SYSFS_BUSTYPE, 

2561 cls.XENBUS_DEVTYPE) 

2562 for path in glob.glob(pattern): 

2563 

2564 name = os.path.basename(path) 

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

2566 

2567 yield cls(domid, devid) 

2568 

2569 

2570class XenBackendDevice(XenbusDevice): 

2571 """Xenbus backend device""" 

2572 SYSFS_BUSTYPE = "xen-backend" 

2573 

2574 @classmethod 

2575 def from_xs_path(cls, _path): 

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

2577 

2578 assert _backend == 'backend' 

2579 assert _type == cls.XENBUS_DEVTYPE 

2580 

2581 domid = int(domid) 

2582 devid = int(devid) 

2583 

2584 return cls(domid, devid) 

2585 

2586 

2587class Blkback(XenBackendDevice): 

2588 """A blkback VBD""" 

2589 

2590 XENBUS_DEVTYPE = "vbd" 

2591 

2592 def __init__(self, domid, devid): 

2593 XenBackendDevice.__init__(self, domid, devid) 

2594 self._phy = None 

2595 self._vdi_uuid = None 

2596 self._q_state = None 

2597 self._q_events = None 

2598 

2599 class XenstoreValueError(Exception): 

2600 KEY: ClassVar[str] = "" 

2601 

2602 def __init__(self, vbd, _str): 

2603 self.vbd = vbd 

2604 self.str = _str 

2605 

2606 @override 

2607 def __str__(self) -> str: 

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

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

2610 

2611 class PhysicalDeviceError(XenstoreValueError): 

2612 KEY = "physical-device" 

2613 

2614 class PhysicalDevice(object): 

2615 

2616 def __init__(self, major, minor): 

2617 self.major = int(major) 

2618 self.minor = int(minor) 

2619 

2620 @classmethod 

2621 def from_xbdev(cls, xbdev): 

2622 

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

2624 

2625 try: 

2626 major, minor = phy.split(':') 

2627 major = int(major, 0x10) 

2628 minor = int(minor, 0x10) 

2629 except Exception as e: 

2630 raise xbdev.PhysicalDeviceError(xbdev, phy) 

2631 

2632 return cls(major, minor) 

2633 

2634 def makedev(self): 

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

2636 

2637 def is_tap(self): 

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

2639 

2640 @override 

2641 def __str__(self) -> str: 

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

2643 

2644 @override 

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

2646 return \ 

2647 self.major == other.major and \ 

2648 self.minor == other.minor 

2649 

2650 def get_physical_device(self): 

2651 if not self._phy: 

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

2653 return self._phy 

2654 

2655 class QueueEvents(Attribute): 

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

2657 notifications emitted.""" 

2658 

2659 SYSFS_NODENAME = "queue_events" 

2660 

2661 QUEUE_RUNNING = (1 << 0) 

2662 QUEUE_PAUSE_DONE = (1 << 1) 

2663 QUEUE_SHUTDOWN_DONE = (1 << 2) 

2664 QUEUE_PAUSE_REQUEST = (1 << 3) 

2665 QUEUE_SHUTDOWN_REQUEST = (1 << 4) 

2666 

2667 def get_mask(self): 

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

2669 

2670 def set_mask(self, mask): 

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

2672 

2673 def get_queue_events(self): 

2674 if not self._q_events: 

2675 self._q_events = self.QueueEvents.from_kobject(self) 

2676 return self._q_events 

2677 

2678 def get_vdi_uuid(self): 

2679 if not self._vdi_uuid: 

2680 self._vdi_uuid = self.read("sm-data/vdi-uuid") 

2681 return self._vdi_uuid 

2682 

2683 def pause_requested(self): 

2684 return self.has_xs_key("pause") 

2685 

2686 def shutdown_requested(self): 

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

2688 

2689 def shutdown_done(self): 

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

2691 

2692 def running(self): 

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

2694 

2695 @classmethod 

2696 def find_by_physical_device(cls, phy): 

2697 for dev in cls.find(): 

2698 try: 

2699 _phy = dev.get_physical_device() 

2700 except cls.PhysicalDeviceError: 

2701 continue 

2702 

2703 if _phy == phy: 

2704 yield dev 

2705 

2706 @classmethod 

2707 def find_by_tap_minor(cls, minor): 

2708 phy = cls.PhysicalDevice(Tapdisk.major(), minor) 

2709 return cls.find_by_physical_device(phy) 

2710 

2711 @classmethod 

2712 def find_by_tap(cls, tapdisk): 

2713 return cls.find_by_tap_minor(tapdisk.minor) 

2714 

2715 def has_tap(self): 

2716 

2717 if not self.can_tap(): 

2718 return False 

2719 

2720 phy = self.get_physical_device() 

2721 if phy: 

2722 return phy.is_tap() 

2723 

2724 return False 

2725 

2726 def is_bare_hvm(self): 

2727 """File VDIs for bare HVM. These are directly accessible by Qemu.""" 

2728 try: 

2729 self.get_physical_device() 

2730 

2731 except self.PhysicalDeviceError as e: 

2732 vdi_type = self.read("type") 

2733 

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

2735 

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

2737 raise 

2738 

2739 return True 

2740 

2741 return False 

2742 

2743 def can_tap(self): 

2744 return not self.is_bare_hvm() 

2745 

2746 

2747class BlkbackEventHandler(UEventHandler): 

2748 

2749 LOG_FACILITY = _syslog.LOG_DAEMON 

2750 

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

2752 if not ident: 

2753 ident = self.__class__.__name__ 

2754 

2755 self.ident = ident 

2756 self._vbd = None 

2757 self._tapdisk = None 

2758 

2759 UEventHandler.__init__(self) 

2760 

2761 @override 

2762 def run(self) -> None: 

2763 

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

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

2766 

2767 UEventHandler.run(self) 

2768 

2769 @override 

2770 def __str__(self) -> str: 

2771 

2772 try: 

2773 path = self.xs_path 

2774 except: 

2775 path = None 

2776 

2777 try: 

2778 action = self.get_action() 

2779 except: 

2780 action = None 

2781 

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

2783 

2784 def _log(self, prio, msg): 

2785 syslog(prio, msg) 

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

2787 

2788 def info(self, msg): 

2789 self._log(_syslog.LOG_INFO, msg) 

2790 

2791 def warn(self, msg): 

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

2793 

2794 def error(self, msg): 

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

2796 

2797 def get_vbd(self): 

2798 if not self._vbd: 

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

2800 return self._vbd 

2801 

2802 def get_tapdisk(self): 

2803 if not self._tapdisk: 

2804 minor = self.get_vbd().get_physical_device().minor 

2805 self._tapdisk = Tapdisk.from_minor(minor) 

2806 return self._tapdisk 

2807 # 

2808 # Events 

2809 # 

2810 

2811 def __add(self): 

2812 vbd = self.get_vbd() 

2813 # Manage blkback transitions 

2814 # self._manage_vbd() 

2815 

2816 vbd.create_physical_device() 

2817 

2818 vbd.signal_hotplug() 

2819 

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

2821 def add(self): 

2822 try: 

2823 self.__add() 

2824 except Attribute.NoSuchAttribute as e: 

2825 # 

2826 # FIXME: KOBJ_ADD is racing backend.probe, which 

2827 # registers device attributes. So poll a little. 

2828 # 

2829 self.warn("%s, still trying." % e) 

2830 raise RetryLoop.TransientFailure(e) 

2831 

2832 def __change(self): 

2833 vbd = self.get_vbd() 

2834 

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

2836 

2837 if vbd.has_tap(): 

2838 pass 

2839 #self._pause_update_tap() 

2840 

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

2842 

2843 self._signal_xapi() 

2844 

2845 def change(self): 

2846 vbd = self.get_vbd() 

2847 

2848 # NB. Beware of spurious change events between shutdown 

2849 # completion and device removal. Also, Xapi.VM.migrate will 

2850 # hammer a couple extra shutdown-requests into the source VBD. 

2851 

2852 while True: 

2853 vbd.begin() 

2854 

2855 if not vbd.exists() or \ 

2856 vbd.shutdown_done(): 

2857 break 

2858 

2859 self.__change() 

2860 

2861 if vbd.commit(): 

2862 return 

2863 

2864 vbd.abort() 

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

2866 

2867 def remove(self): 

2868 vbd = self.get_vbd() 

2869 

2870 vbd.signal_hotplug(False) 

2871 

2872 ACTIONS = {'add': add, 

2873 'change': change, 

2874 'remove': remove} 

2875 # 

2876 # VDI.pause 

2877 # 

2878 

2879 def _tap_should_pause(self): 

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

2881 paused""" 

2882 

2883 tapdisk = self.get_tapdisk() 

2884 TapState = Tapdisk.PauseState 

2885 

2886 PAUSED = 'P' 

2887 RUNNING = 'R' 

2888 PAUSED_SHUTDOWN = 'P,S' 

2889 # NB. Shutdown/paused is special. We know it's not going 

2890 # to restart again, so it's a RUNNING. Still better than 

2891 # backtracking a removed device during Vbd.unplug completion. 

2892 

2893 next = TapState.RUNNING 

2894 vbds = {} 

2895 

2896 for vbd in Blkback.find_by_tap(tapdisk): 

2897 name = str(vbd) 

2898 

2899 pausing = vbd.pause_requested() 

2900 closing = vbd.shutdown_requested() 

2901 running = vbd.running() 

2902 

2903 if pausing: 

2904 if closing and not running: 

2905 vbds[name] = PAUSED_SHUTDOWN 

2906 else: 

2907 vbds[name] = PAUSED 

2908 next = TapState.PAUSED 

2909 

2910 else: 

2911 vbds[name] = RUNNING 

2912 

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

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

2915 vbds, next)) 

2916 

2917 return next == TapState.PAUSED 

2918 

2919 def _pause_update_tap(self): 

2920 vbd = self.get_vbd() 

2921 

2922 if self._tap_should_pause(): 

2923 self._pause_tap() 

2924 else: 

2925 self._resume_tap() 

2926 

2927 def _pause_tap(self): 

2928 tapdisk = self.get_tapdisk() 

2929 

2930 if not tapdisk.is_paused(): 

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

2932 tapdisk.pause() 

2933 

2934 def _resume_tap(self): 

2935 tapdisk = self.get_tapdisk() 

2936 

2937 # NB. Raw VDI snapshots. Refresh the physical path and 

2938 # type while resuming. 

2939 vbd = self.get_vbd() 

2940 vdi_uuid = vbd.get_vdi_uuid() 

2941 

2942 if tapdisk.is_paused(): 

2943 self.info("loading vdi uuid=%s" % vdi_uuid) 

2944 vdi = VDI.from_cli(vdi_uuid) 

2945 _type = vdi.get_tap_type() 

2946 path = vdi.get_phy_path() 

2947 self.info("resuming %s on %s:%s" % (tapdisk, _type, path)) 

2948 tapdisk.unpause(_type, path) 

2949 # 

2950 # VBD.pause/shutdown 

2951 # 

2952 

2953 def _manage_vbd(self): 

2954 vbd = self.get_vbd() 

2955 # NB. Hook into VBD state transitions. 

2956 

2957 events = vbd.get_queue_events() 

2958 

2959 mask = 0 

2960 mask |= events.QUEUE_PAUSE_DONE # pause/unpause 

2961 mask |= events.QUEUE_SHUTDOWN_DONE # shutdown 

2962 # TODO: mask |= events.QUEUE_SHUTDOWN_REQUEST, for shutdown=force 

2963 # TODO: mask |= events.QUEUE_RUNNING, for ionice updates etc 

2964 

2965 events.set_mask(mask) 

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

2967 

2968 def _signal_xapi(self): 

2969 vbd = self.get_vbd() 

2970 

2971 pausing = vbd.pause_requested() 

2972 closing = vbd.shutdown_requested() 

2973 running = vbd.running() 

2974 

2975 handled = 0 

2976 

2977 if pausing and not running: 

2978 if 'pause-done' not in vbd: 

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

2980 handled += 1 

2981 

2982 if not pausing: 

2983 if 'pause-done' in vbd: 

2984 vbd.rm('pause-done') 

2985 handled += 1 

2986 

2987 if closing and not running: 

2988 if 'shutdown-done' not in vbd: 

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

2990 handled += 1 

2991 

2992 if handled > 1: 

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

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

2995 (pausing, closing, running)) 

2996 

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

2998 

2999 import sys 

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

3001 

3002 # 

3003 # Simple CLI interface for manual operation 

3004 # 

3005 # tap.* level calls go down to local Tapdisk()s (by physical path) 

3006 # vdi.* level calls run the plugin calls across host boundaries. 

3007 # 

3008 

3009 def usage(stream): 

3010 print("usage: %s tap.{list|major}" % prog, file=stream) 

3011 print(" %s tap.{launch|find|get|pause|" % prog + \ 

3012 "unpause|shutdown|stats} {[<tt>:]<path>} | [minor=]<int> | .. }", file=stream) 

3013 print(" %s vbd.uevent" % prog, file=stream) 

3014 

3015 try: 

3016 cmd = sys.argv[1] 

3017 except IndexError: 

3018 usage(sys.stderr) 

3019 sys.exit(1) 

3020 

3021 try: 

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

3023 except: 

3024 usage(sys.stderr) 

3025 sys.exit(1) 

3026 

3027 # 

3028 # Local Tapdisks 

3029 # 

3030 

3031 if cmd == 'tap.major': 

3032 

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

3034 

3035 elif cmd == 'tap.launch': 

3036 

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

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

3039 

3040 elif _class == 'tap': 

3041 

3042 attrs: Dict[str, Any] = {} 

3043 for item in sys.argv[2:]: 

3044 try: 

3045 key, val = item.split('=') 

3046 attrs[key] = val 

3047 continue 

3048 except ValueError: 

3049 pass 

3050 

3051 try: 

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

3053 continue 

3054 except ValueError: 

3055 pass 

3056 

3057 try: 

3058 arg = Tapdisk.Arg.parse(item) 

3059 attrs['_type'] = arg.type 

3060 attrs['path'] = arg.path 

3061 continue 

3062 except Tapdisk.Arg.InvalidArgument: 

3063 pass 

3064 

3065 attrs['path'] = item 

3066 

3067 if cmd == 'tap.list': 

3068 

3069 for tapdisk in Tapdisk.list( ** attrs): 

3070 blktap = tapdisk.get_blktap() 

3071 print(tapdisk, end=' ') 

3072 print("%s: task=%s pool=%s" % \ 

3073 (blktap, 

3074 blktap.get_task_pid(), 

3075 blktap.get_pool_name())) 

3076 

3077 elif cmd == 'tap.vbds': 

3078 # Find all Blkback instances for a given tapdisk 

3079 

3080 for tapdisk in Tapdisk.list( ** attrs): 

3081 print("%s:" % tapdisk, end=' ') 

3082 for vbd in Blkback.find_by_tap(tapdisk): 

3083 print(vbd, end=' ') 

3084 print() 

3085 

3086 else: 

3087 

3088 if not attrs: 

3089 usage(sys.stderr) 

3090 sys.exit(1) 

3091 

3092 try: 

3093 tapdisk = Tapdisk.get( ** attrs) 

3094 except TypeError: 

3095 usage(sys.stderr) 

3096 sys.exit(1) 

3097 

3098 if cmd == 'tap.shutdown': 

3099 # Shutdown a running tapdisk, or raise 

3100 tapdisk.shutdown() 

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

3102 

3103 elif cmd == 'tap.pause': 

3104 # Pause an unpaused tapdisk, or raise 

3105 tapdisk.pause() 

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

3107 

3108 elif cmd == 'tap.unpause': 

3109 # Unpause a paused tapdisk, or raise 

3110 tapdisk.unpause() 

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

3112 

3113 elif cmd == 'tap.stats': 

3114 # Gather tapdisk status 

3115 stats = tapdisk.stats() 

3116 print("%s:" % tapdisk) 

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

3118 

3119 else: 

3120 usage(sys.stderr) 

3121 sys.exit(1) 

3122 

3123 elif cmd == 'vbd.uevent': 

3124 

3125 hnd = BlkbackEventHandler(cmd) 

3126 

3127 if not sys.stdin.isatty(): 

3128 try: 

3129 hnd.run() 

3130 except Exception as e: 

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

3132 

3133 import traceback 

3134 _type, value, tb = sys.exc_info() 

3135 trace = traceback.format_exception(_type, value, tb) 

3136 for entry in trace: 

3137 for line in entry.rstrip().split('\n'): 

3138 util.SMlog(line) 

3139 else: 

3140 hnd.run() 

3141 

3142 elif cmd == 'vbd.list': 

3143 

3144 for vbd in Blkback.find(): 

3145 print(vbd, \ 

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

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

3148 

3149 else: 

3150 usage(sys.stderr) 

3151 sys.exit(1)