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.getSizePhys, 'getSizePhys') 

252 def get_size_phys(self, vdi_uuid, response): 

253 return int(response) 

254 

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

256 def get_allocated_size(self, vdi_uuid, response): 

257 return int(response) 

258 

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

260 def get_depth(self, vdi_uuid, response): 

261 return int(response) 

262 

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

264 def get_key_hash(self, vdi_uuid, response): 

265 return response or None 

266 

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

268 def get_block_bitmap(self, vdi_uuid, response): 

269 return base64.b64decode(response) 

270 

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

272 def get_drbd_size(self, vdi_uuid, response): 

273 return int(response) 

274 

275 def _get_drbd_size(self, path): 

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

277 if ret == 0: 

278 return int(stdout.strip()) 

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

280 

281 # -------------------------------------------------------------------------- 

282 # Setters: only used locally. 

283 # -------------------------------------------------------------------------- 

284 

285 @linstormodifier() 

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

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

288 

289 @linstormodifier() 

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

291 return self._call_local_method_or_fail(vhdutil.setSizeVirt, path, size, jfile) 

292 

293 @linstormodifier() 

294 def set_size_virt_fast(self, path, size): 

295 return self._call_local_method_or_fail(vhdutil.setSizeVirtFast, path, size) 

296 

297 @linstormodifier() 

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

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

300 

301 @linstormodifier() 

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

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

304 

305 @linstormodifier() 

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

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

308 

309 @linstormodifier() 

310 def set_key(self, path, key_hash): 

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

312 

313 @linstormodifier() 

314 def kill_data(self, path): 

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

316 

317 @linstormodifier() 

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

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

320 

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

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

323 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

324 if new_size <= old_size: 

325 return 

326 

327 util.SMlog( 

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

329 .format(vdi_path, new_size, old_size) 

330 ) 

331 

332 journaler.create( 

333 LinstorJournaler.INFLATE, vdi_uuid, old_size 

334 ) 

335 self._linstor.resize_volume(vdi_uuid, new_size) 

336 

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

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

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

340 if result_size < new_size: 

341 util.SMlog( 

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

343 .format(new_size, result_size) 

344 ) 

345 

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

347 self.set_size_phys(vdi_path, result_size, False) 

348 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid) 

349 

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

351 if zeroize: 

352 assert old_size > vhdutil.VHD_FOOTER_SIZE 

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

354 

355 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

356 if new_size >= old_size: 

357 return 

358 

359 util.SMlog( 

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

361 .format(vdi_path, new_size, old_size) 

362 ) 

363 

364 self.set_size_phys(vdi_path, new_size) 

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

366 

367 # -------------------------------------------------------------------------- 

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

369 # -------------------------------------------------------------------------- 

370 

371 @linstormodifier() 

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

373 kwargs = { 

374 'parentPath': str(parentPath), 

375 'parentRaw': parentRaw 

376 } 

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

378 

379 @linstormodifier() 

380 def force_coalesce(self, path): 

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

382 

383 @linstormodifier() 

384 def force_repair(self, path): 

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

386 

387 @linstormodifier() 

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

389 kwargs = { 

390 'newSize': newSize, 

391 'oldSize': oldSize, 

392 'zeroize': zeroize 

393 } 

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

395 

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

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

398 

399 # -------------------------------------------------------------------------- 

400 # Static helpers. 

401 # -------------------------------------------------------------------------- 

402 

403 @classmethod 

404 def compute_volume_size(cls, virtual_size, image_type): 

405 if image_type == vhdutil.VDI_TYPE_VHD: 

406 # All LINSTOR VDIs have the metadata area preallocated for 

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

408 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE) 

409 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size) 

410 virtual_size += meta_overhead + bitmap_overhead 

411 elif image_type != vhdutil.VDI_TYPE_RAW: 

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

413 

414 return LinstorVolumeManager.round_up_volume_size(virtual_size) 

415 

416 # -------------------------------------------------------------------------- 

417 # Helpers. 

418 # -------------------------------------------------------------------------- 

419 

420 def _extract_uuid(self, device_path): 

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

422 return self._linstor.get_volume_uuid_from_device_path( 

423 device_path.rstrip('\n') 

424 ) 

425 

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

427 """ 

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

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

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

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

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

433 """ 

434 

435 if not node_names: 

436 raise xs_errors.XenError( 

437 'VDIUnavailable', 

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

439 .format(vdi_uuid, device_path) 

440 ) 

441 

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

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

444 if host_record['hostname'] in node_names: 

445 return host_ref 

446 

447 raise xs_errors.XenError( 

448 'VDIUnavailable', 

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

450 .format(vdi_uuid, device_path) 

451 ) 

452 

453 # -------------------------------------------------------------------------- 

454 

455 def _raise_openers_exception(self, device_path, e): 

456 if isinstance(e, util.CommandException): 

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

458 else: 

459 e_str = str(e) 

460 

461 try: 

462 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

463 device_path 

464 ) 

465 e_wrapper = Exception( 

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

467 self._linstor.get_volume_openers(volume_uuid) 

468 ) 

469 ) 

470 except Exception as illformed_e: 

471 e_wrapper = Exception( 

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

473 ) 

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

475 raise e_wrapper # pylint: disable = E0702 

476 

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

478 if isinstance(local_method, str): 

479 local_method = getattr(self, local_method) 

480 

481 try: 

482 def local_call(): 

483 try: 

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

485 except util.CommandException as e: 

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

487 raise ErofsLinstorCallException(e) # Break retry calls. 

488 if e.code == errno.ENOENT: 

489 raise NoPathLinstorCallException(e) 

490 raise e 

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

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

493 except util.CommandException as e: 

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

495 raise e 

496 

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

498 try: 

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

500 except ErofsLinstorCallException as e: 

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

502 self._raise_openers_exception(device_path, e.cmd_err) 

503 

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

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

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

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

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

509 

510 if isinstance(local_method, str): 

511 local_method = getattr(self, local_method) 

512 

513 # A. Try to write locally... 

514 try: 

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

516 except Exception: 

517 pass 

518 

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

520 

521 # B. Execute the command on another host. 

522 # B.1. Get host list. 

523 try: 

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

525 except Exception as e: 

526 raise xs_errors.XenError( 

527 'VDIUnavailable', 

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

529 .format(remote_method, device_path, e) 

530 ) 

531 

532 # B.2. Prepare remote args. 

533 remote_args = { 

534 'devicePath': device_path, 

535 'groupName': self._linstor.group_name 

536 } 

537 remote_args.update(**kwargs) 

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

539 

540 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

541 device_path 

542 ) 

543 parent_volume_uuid = None 

544 if use_parent: 

545 parent_volume_uuid = self.get_parent(volume_uuid) 

546 

547 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

548 

549 # B.3. Call! 

550 def remote_call(): 

551 try: 

552 all_openers = self._linstor.get_volume_openers(openers_uuid) 

553 except Exception as e: 

554 raise xs_errors.XenError( 

555 'VDIUnavailable', 

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

557 .format(remote_method, device_path, e) 

558 ) 

559 

560 no_host_found = True 

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

562 if not openers: 

563 continue 

564 

565 try: 

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

567 except StopIteration: 

568 continue 

569 

570 no_host_found = False 

571 try: 

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

573 except Exception: 

574 pass 

575 

576 if no_host_found: 

577 try: 

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

579 except Exception as e: 

580 self._raise_openers_exception(device_path, e) 

581 

582 raise xs_errors.XenError( 

583 'VDIUnavailable', 

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

585 .format(remote_method, device_path, openers) 

586 ) 

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

588 

589 @staticmethod 

590 def _zeroize(path, size): 

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

592 raise xs_errors.XenError( 

593 'EIO', 

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

595 )