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/env python3 

2# 

3# Copyright (C) 2020 Vates SAS - ronan.abhamon@vates.fr 

4# 

5# This program is free software: you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation, either version 3 of the License, or 

8# (at your option) any later version. 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU General Public License for more details. 

13# 

14# You should have received a copy of the GNU General Public License 

15# along with this program. If not, see <https://www.gnu.org/licenses/>. 

16 

17from sm_typing import override 

18 

19from linstorjournaler import LinstorJournaler 

20from linstorvolumemanager import LinstorVolumeManager 

21import base64 

22import errno 

23import json 

24import socket 

25import time 

26import util 

27import vhdutil 

28import xs_errors 

29 

30MANAGER_PLUGIN = 'linstor-manager' 

31 

32 

33def call_remote_method(session, host_ref, method, device_path, args): 

34 try: 

35 response = session.xenapi.host.call_plugin( 

36 host_ref, MANAGER_PLUGIN, method, args 

37 ) 

38 except Exception as e: 

39 util.SMlog('call-plugin ({} with {}) exception: {}'.format( 

40 method, args, e 

41 )) 

42 raise util.SMException(str(e)) 

43 

44 util.SMlog('call-plugin ({} with {}) returned: {}'.format( 

45 method, args, response 

46 )) 

47 

48 return response 

49 

50 

51def check_ex(path, ignoreMissingFooter = False, fast = False): 

52 cmd = [vhdutil.VHD_UTIL, "check", vhdutil.OPT_LOG_ERR, "-n", path] 

53 if ignoreMissingFooter: 

54 cmd.append("-i") 

55 if fast: 

56 cmd.append("-B") 

57 

58 vhdutil.ioretry(cmd) 

59 

60 

61class LinstorCallException(util.SMException): 

62 def __init__(self, cmd_err): 

63 self.cmd_err = cmd_err 

64 

65 @override 

66 def __str__(self) -> str: 

67 return str(self.cmd_err) 

68 

69 

70class ErofsLinstorCallException(LinstorCallException): 

71 pass 

72 

73 

74class NoPathLinstorCallException(LinstorCallException): 

75 pass 

76 

77 

78def linstorhostcall(local_method, remote_method): 

79 def decorated(response_parser): 

80 def wrapper(*args, **kwargs): 

81 self = args[0] 

82 vdi_uuid = args[1] 

83 

84 device_path = self._linstor.build_device_path( 

85 self._linstor.get_volume_name(vdi_uuid) 

86 ) 

87 

88 # A. Try a call using directly the DRBD device to avoid 

89 # remote request. 

90 

91 # Try to read locally if the device is not in use or if the device 

92 # is up to date and not diskless. 

93 (node_names, in_use_by) = \ 

94 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid) 

95 

96 local_e = None 

97 try: 

98 if not in_use_by or socket.gethostname() in node_names: 

99 return self._call_local_method(local_method, device_path, *args[2:], **kwargs) 

100 except ErofsLinstorCallException as e: 

101 local_e = e.cmd_err 

102 except Exception as e: 

103 local_e = e 

104 

105 util.SMlog( 

106 'unable to execute `{}` locally, retry using a readable host... (cause: {})'.format( 

107 remote_method, local_e if local_e else 'local diskless + in use or not up to date' 

108 ) 

109 ) 

110 

111 if in_use_by: 

112 node_names = {in_use_by} 

113 

114 # B. Execute the plugin on master or slave. 

115 remote_args = { 

116 'devicePath': device_path, 

117 'groupName': self._linstor.group_name 

118 } 

119 remote_args.update(**kwargs) 

120 remote_args = {str(key): str(value) for key, value in remote_args.items()} 

121 

122 try: 

123 def remote_call(): 

124 host_ref = self._get_readonly_host(vdi_uuid, device_path, node_names) 

125 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args) 

126 response = util.retry(remote_call, 5, 2) 

127 except Exception as remote_e: 

128 self._raise_openers_exception(device_path, local_e or remote_e) 

129 

130 return response_parser(self, vdi_uuid, response) 

131 return wrapper 

132 return decorated 

133 

134 

135def linstormodifier(): 

136 def decorated(func): 

137 def wrapper(*args, **kwargs): 

138 self = args[0] 

139 

140 ret = func(*args, **kwargs) 

141 self._linstor.invalidate_resource_cache() 

142 return ret 

143 return wrapper 

144 return decorated 

145 

146 

147class LinstorVhdUtil: 

148 MAX_SIZE = 2 * 1024 * 1024 * 1024 * 1024 # Max VHD size. 

149 

150 def __init__(self, session, linstor): 

151 self._session = session 

152 self._linstor = linstor 

153 

154 def create_chain_paths(self, vdi_uuid, readonly=False): 

155 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit vhdutil calls. 

156 # Useful for the snapshot code algorithm. 

157 

158 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid) 

159 path = leaf_vdi_path 

160 while True: 

161 if not util.pathexists(path): 

162 raise xs_errors.XenError( 

163 'VDIUnavailable', opterr='Could not find: {}'.format(path) 

164 ) 

165 

166 # Diskless path can be created on the fly, ensure we can open it. 

167 def check_volume_usable(): 

168 while True: 

169 try: 

170 with open(path, 'r' if readonly else 'r+'): 

171 pass 

172 except IOError as e: 

173 if e.errno == errno.ENODATA: 

174 time.sleep(2) 

175 continue 

176 if e.errno == errno.EROFS: 

177 util.SMlog('Volume not attachable because RO. Openers: {}'.format( 

178 self._linstor.get_volume_openers(vdi_uuid) 

179 )) 

180 raise 

181 break 

182 util.retry(check_volume_usable, 15, 2) 

183 

184 vdi_uuid = self.get_vhd_info(vdi_uuid).parentUuid 

185 if not vdi_uuid: 

186 break 

187 path = self._linstor.get_device_path(vdi_uuid) 

188 readonly = True # Non-leaf is always readonly. 

189 

190 return leaf_vdi_path 

191 

192 # -------------------------------------------------------------------------- 

193 # Getters: read locally and try on another host in case of failure. 

194 # -------------------------------------------------------------------------- 

195 

196 def check(self, vdi_uuid, ignore_missing_footer=False, fast=False): 

197 kwargs = { 

198 'ignoreMissingFooter': ignore_missing_footer, 

199 'fast': fast 

200 } 

201 try: 

202 self._check(vdi_uuid, **kwargs) # pylint: disable = E1123 

203 return True 

204 except Exception as e: 

205 util.SMlog('Call to `check` failed: {}'.format(e)) 

206 return False 

207 

208 @linstorhostcall(check_ex, 'check') 

209 def _check(self, vdi_uuid, response): 

210 return util.strtobool(response) 

211 

212 def get_vhd_info(self, vdi_uuid, include_parent=True): 

213 kwargs = { 

214 'includeParent': include_parent, 

215 'resolveParent': False 

216 } 

217 # TODO: Replace pylint comment with this feature when possible: 

218 # https://github.com/PyCQA/pylint/pull/2926 

219 return self._get_vhd_info(vdi_uuid, self._extract_uuid, **kwargs) # pylint: disable = E1123 

220 

221 @linstorhostcall(vhdutil.getVHDInfo, 'getVHDInfo') 

222 def _get_vhd_info(self, vdi_uuid, response): 

223 obj = json.loads(response) 

224 

225 vhd_info = vhdutil.VHDInfo(vdi_uuid) 

226 vhd_info.sizeVirt = obj['sizeVirt'] 

227 vhd_info.sizePhys = obj['sizePhys'] 

228 if 'parentPath' in obj: 

229 vhd_info.parentPath = obj['parentPath'] 

230 vhd_info.parentUuid = obj['parentUuid'] 

231 vhd_info.hidden = obj['hidden'] 

232 vhd_info.path = obj['path'] 

233 

234 return vhd_info 

235 

236 @linstorhostcall(vhdutil.hasParent, 'hasParent') 

237 def has_parent(self, vdi_uuid, response): 

238 return util.strtobool(response) 

239 

240 def get_parent(self, vdi_uuid): 

241 return self._get_parent(vdi_uuid, self._extract_uuid) 

242 

243 @linstorhostcall(vhdutil.getParent, 'getParent') 

244 def _get_parent(self, vdi_uuid, response): 

245 return response 

246 

247 @linstorhostcall(vhdutil.getSizeVirt, 'getSizeVirt') 

248 def get_size_virt(self, vdi_uuid, response): 

249 return int(response) 

250 

251 @linstorhostcall(vhdutil.getMaxResizeSize, 'getMaxResizeSize') 

252 def get_max_resize_size(self, vdi_uuid, response): 

253 return int(response) 

254 

255 @linstorhostcall(vhdutil.getSizePhys, 'getSizePhys') 

256 def get_size_phys(self, vdi_uuid, response): 

257 return int(response) 

258 

259 @linstorhostcall(vhdutil.getAllocatedSize, 'getAllocatedSize') 

260 def get_allocated_size(self, vdi_uuid, response): 

261 return int(response) 

262 

263 @linstorhostcall(vhdutil.getDepth, 'getDepth') 

264 def get_depth(self, vdi_uuid, response): 

265 return int(response) 

266 

267 @linstorhostcall(vhdutil.getKeyHash, 'getKeyHash') 

268 def get_key_hash(self, vdi_uuid, response): 

269 return response or None 

270 

271 @linstorhostcall(vhdutil.getBlockBitmap, 'getBlockBitmap') 

272 def get_block_bitmap(self, vdi_uuid, response): 

273 return base64.b64decode(response) 

274 

275 @linstorhostcall('_get_drbd_size', 'getDrbdSize') 

276 def get_drbd_size(self, vdi_uuid, response): 

277 return int(response) 

278 

279 def _get_drbd_size(self, path): 

280 (ret, stdout, stderr) = util.doexec(['blockdev', '--getsize64', path]) 

281 if ret == 0: 

282 return int(stdout.strip()) 

283 raise util.SMException('Failed to get DRBD size: {}'.format(stderr)) 

284 

285 # -------------------------------------------------------------------------- 

286 # Setters: only used locally. 

287 # -------------------------------------------------------------------------- 

288 

289 @linstormodifier() 

290 def create(self, path, size, static, msize=0): 

291 return self._call_local_method_or_fail(vhdutil.create, path, size, static, msize) 

292 

293 @linstormodifier() 

294 def set_size_phys(self, path, size, debug=True): 

295 return self._call_local_method_or_fail(vhdutil.setSizePhys, path, size, debug) 

296 

297 @linstormodifier() 

298 def set_parent(self, path, parentPath, parentRaw=False): 

299 return self._call_local_method_or_fail(vhdutil.setParent, path, parentPath, parentRaw) 

300 

301 @linstormodifier() 

302 def set_hidden(self, path, hidden=True): 

303 return self._call_local_method_or_fail(vhdutil.setHidden, path, hidden) 

304 

305 @linstormodifier() 

306 def set_key(self, path, key_hash): 

307 return self._call_local_method_or_fail(vhdutil.setKey, path, key_hash) 

308 

309 @linstormodifier() 

310 def kill_data(self, path): 

311 return self._call_local_method_or_fail(vhdutil.killData, path) 

312 

313 @linstormodifier() 

314 def snapshot(self, path, parent, parentRaw, msize=0, checkEmpty=True): 

315 return self._call_local_method_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty) 

316 

317 def inflate(self, journaler, vdi_uuid, vdi_path, new_size, old_size): 

318 # Only inflate if the LINSTOR volume capacity is not enough. 

319 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

320 if new_size <= old_size: 

321 return 

322 

323 util.SMlog( 

324 'Inflate {} (size={}, previous={})' 

325 .format(vdi_path, new_size, old_size) 

326 ) 

327 

328 journaler.create( 

329 LinstorJournaler.INFLATE, vdi_uuid, old_size 

330 ) 

331 self._linstor.resize_volume(vdi_uuid, new_size) 

332 

333 # TODO: Replace pylint comment with this feature when possible: 

334 # https://github.com/PyCQA/pylint/pull/2926 

335 result_size = self.get_drbd_size(vdi_uuid) # pylint: disable = E1120 

336 if result_size < new_size: 

337 util.SMlog( 

338 'WARNING: Cannot inflate volume to {}B, result size: {}B' 

339 .format(new_size, result_size) 

340 ) 

341 

342 self._zeroize(vdi_path, result_size - vhdutil.VHD_FOOTER_SIZE) 

343 self.set_size_phys(vdi_path, result_size, False) 

344 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid) 

345 

346 def deflate(self, vdi_path, new_size, old_size, zeroize=False): 

347 if zeroize: 

348 assert old_size > vhdutil.VHD_FOOTER_SIZE 

349 self._zeroize(vdi_path, old_size - vhdutil.VHD_FOOTER_SIZE) 

350 

351 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

352 if new_size >= old_size: 

353 return 

354 

355 util.SMlog( 

356 'Deflate {} (new size={}, previous={})' 

357 .format(vdi_path, new_size, old_size) 

358 ) 

359 

360 self.set_size_phys(vdi_path, new_size) 

361 # TODO: Change the LINSTOR volume size using linstor.resize_volume. 

362 

363 # -------------------------------------------------------------------------- 

364 # Remote setters: write locally and try on another host in case of failure. 

365 # -------------------------------------------------------------------------- 

366 

367 @linstormodifier() 

368 def set_size_virt(self, path, size, jfile): 

369 kwargs = { 

370 'size': size, 

371 'jfile': jfile 

372 } 

373 return self._call_method(vhdutil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs) 

374 

375 @linstormodifier() 

376 def set_size_virt_fast(self, path, size): 

377 kwargs = { 

378 'size': size 

379 } 

380 return self._call_method(vhdutil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs) 

381 

382 @linstormodifier() 

383 def force_parent(self, path, parentPath, parentRaw=False): 

384 kwargs = { 

385 'parentPath': str(parentPath), 

386 'parentRaw': parentRaw 

387 } 

388 return self._call_method(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs) 

389 

390 @linstormodifier() 

391 def force_coalesce(self, path): 

392 return int(self._call_method(vhdutil.coalesce, 'coalesce', path, use_parent=True)) 

393 

394 @linstormodifier() 

395 def force_repair(self, path): 

396 return self._call_method(vhdutil.repair, 'repair', path, use_parent=False) 

397 

398 @linstormodifier() 

399 def force_deflate(self, path, newSize, oldSize, zeroize): 

400 kwargs = { 

401 'newSize': newSize, 

402 'oldSize': oldSize, 

403 'zeroize': zeroize 

404 } 

405 return self._call_method('_force_deflate', 'deflate', path, use_parent=False, **kwargs) 

406 

407 def _force_deflate(self, path, newSize, oldSize, zeroize): 

408 self.deflate(path, newSize, oldSize, zeroize) 

409 

410 # -------------------------------------------------------------------------- 

411 # Static helpers. 

412 # -------------------------------------------------------------------------- 

413 

414 @classmethod 

415 def compute_volume_size(cls, virtual_size, image_type): 

416 if image_type == vhdutil.VDI_TYPE_VHD: 

417 # All LINSTOR VDIs have the metadata area preallocated for 

418 # the maximum possible virtual size (for fast online VDI.resize). 

419 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE) 

420 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size) 

421 virtual_size += meta_overhead + bitmap_overhead 

422 elif image_type != vhdutil.VDI_TYPE_RAW: 

423 raise Exception('Invalid image type: {}'.format(image_type)) 

424 

425 return LinstorVolumeManager.round_up_volume_size(virtual_size) 

426 

427 # -------------------------------------------------------------------------- 

428 # Helpers. 

429 # -------------------------------------------------------------------------- 

430 

431 def _extract_uuid(self, device_path): 

432 # TODO: Remove new line in the vhdutil module. Not here. 

433 return self._linstor.get_volume_uuid_from_device_path( 

434 device_path.rstrip('\n') 

435 ) 

436 

437 def _get_readonly_host(self, vdi_uuid, device_path, node_names): 

438 """ 

439 When vhd-util is called to fetch VDI info we must find a 

440 diskful DRBD disk to read the data. It's the goal of this function. 

441 Why? Because when a VHD is open in RO mode, the LVM layer is used 

442 directly to bypass DRBD verifications (we can have only one process 

443 that reads/writes to disk with DRBD devices). 

444 """ 

445 

446 if not node_names: 

447 raise xs_errors.XenError( 

448 'VDIUnavailable', 

449 opterr='Unable to find diskful node: {} (path={})' 

450 .format(vdi_uuid, device_path) 

451 ) 

452 

453 hosts = self._session.xenapi.host.get_all_records() 

454 for host_ref, host_record in hosts.items(): 

455 if host_record['hostname'] in node_names: 

456 return host_ref 

457 

458 raise xs_errors.XenError( 

459 'VDIUnavailable', 

460 opterr='Unable to find a valid host from VDI: {} (path={})' 

461 .format(vdi_uuid, device_path) 

462 ) 

463 

464 # -------------------------------------------------------------------------- 

465 

466 def _raise_openers_exception(self, device_path, e): 

467 if isinstance(e, util.CommandException): 

468 e_str = 'cmd: `{}`, code: `{}`, reason: `{}`'.format(e.cmd, e.code, e.reason) 

469 else: 

470 e_str = str(e) 

471 

472 try: 

473 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

474 device_path 

475 ) 

476 e_wrapper = Exception( 

477 e_str + ' (openers: {})'.format( 

478 self._linstor.get_volume_openers(volume_uuid) 

479 ) 

480 ) 

481 except Exception as illformed_e: 

482 e_wrapper = Exception( 

483 e_str + ' (unable to get openers: {})'.format(illformed_e) 

484 ) 

485 util.SMlog('raise opener exception: {}'.format(e_wrapper)) 

486 raise e_wrapper # pylint: disable = E0702 

487 

488 def _call_local_method(self, local_method, device_path, *args, **kwargs): 

489 if isinstance(local_method, str): 

490 local_method = getattr(self, local_method) 

491 

492 try: 

493 def local_call(): 

494 try: 

495 return local_method(device_path, *args, **kwargs) 

496 except util.CommandException as e: 

497 if e.code == errno.EROFS or e.code == errno.EMEDIUMTYPE: 

498 raise ErofsLinstorCallException(e) # Break retry calls. 

499 if e.code == errno.ENOENT: 

500 raise NoPathLinstorCallException(e) 

501 raise e 

502 # Retry only locally if it's not an EROFS exception. 

503 return util.retry(local_call, 5, 2, exceptions=[util.CommandException]) 

504 except util.CommandException as e: 

505 util.SMlog('failed to execute locally vhd-util (sys {})'.format(e.code)) 

506 raise e 

507 

508 def _call_local_method_or_fail(self, local_method, device_path, *args, **kwargs): 

509 try: 

510 return self._call_local_method(local_method, device_path, *args, **kwargs) 

511 except ErofsLinstorCallException as e: 

512 # Volume is locked on a host, find openers. 

513 self._raise_openers_exception(device_path, e.cmd_err) 

514 

515 def _call_method(self, local_method, remote_method, device_path, use_parent, *args, **kwargs): 

516 # Note: `use_parent` exists to know if the VHD parent is used by the local/remote method. 

517 # Normally in case of failure, if the parent is unused we try to execute the method on 

518 # another host using the DRBD opener list. In the other case, if the parent is required, 

519 # we must check where this last one is open instead of the child. 

520 

521 if isinstance(local_method, str): 

522 local_method = getattr(self, local_method) 

523 

524 # A. Try to write locally... 

525 try: 

526 return self._call_local_method(local_method, device_path, *args, **kwargs) 

527 except Exception: 

528 pass 

529 

530 util.SMlog('unable to execute `{}` locally, retry using a writable host...'.format(remote_method)) 

531 

532 # B. Execute the command on another host. 

533 # B.1. Get host list. 

534 try: 

535 hosts = self._session.xenapi.host.get_all_records() 

536 except Exception as e: 

537 raise xs_errors.XenError( 

538 'VDIUnavailable', 

539 opterr='Unable to get host list to run vhd-util command `{}` (path={}): {}' 

540 .format(remote_method, device_path, e) 

541 ) 

542 

543 # B.2. Prepare remote args. 

544 remote_args = { 

545 'devicePath': device_path, 

546 'groupName': self._linstor.group_name 

547 } 

548 remote_args.update(**kwargs) 

549 remote_args = {str(key): str(value) for key, value in remote_args.items()} 

550 

551 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

552 device_path 

553 ) 

554 parent_volume_uuid = None 

555 if use_parent: 

556 parent_volume_uuid = self.get_parent(volume_uuid) 

557 

558 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

559 

560 # B.3. Call! 

561 def remote_call(): 

562 try: 

563 all_openers = self._linstor.get_volume_openers(openers_uuid) 

564 except Exception as e: 

565 raise xs_errors.XenError( 

566 'VDIUnavailable', 

567 opterr='Unable to get DRBD openers to run vhd-util command `{}` (path={}): {}' 

568 .format(remote_method, device_path, e) 

569 ) 

570 

571 no_host_found = True 

572 for hostname, openers in all_openers.items(): 

573 if not openers: 

574 continue 

575 

576 try: 

577 host_ref = next(ref for ref, rec in hosts.items() if rec['hostname'] == hostname) 

578 except StopIteration: 

579 continue 

580 

581 no_host_found = False 

582 try: 

583 return call_remote_method(self._session, host_ref, remote_method, device_path, remote_args) 

584 except Exception: 

585 pass 

586 

587 if no_host_found: 

588 try: 

589 return local_method(device_path, *args, **kwargs) 

590 except Exception as e: 

591 self._raise_openers_exception(device_path, e) 

592 

593 raise xs_errors.XenError( 

594 'VDIUnavailable', 

595 opterr='No valid host found to run vhd-util command `{}` (path=`{}`, openers=`{}`)' 

596 .format(remote_method, device_path, openers) 

597 ) 

598 return util.retry(remote_call, 5, 2) 

599 

600 @staticmethod 

601 def _zeroize(path, size): 

602 if not util.zeroOut(path, size, vhdutil.VHD_FOOTER_SIZE): 

603 raise xs_errors.XenError( 

604 'EIO', 

605 opterr='Failed to zero out VHD footer {}'.format(path) 

606 )