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 linstorjournaler import LinstorJournaler 

18from linstorvolumemanager import LinstorVolumeManager 

19import base64 

20import distutils.util 

21import errno 

22import json 

23import socket 

24import time 

25import util 

26import vhdutil 

27import xs_errors 

28 

29MANAGER_PLUGIN = 'linstor-manager' 

30 

31 

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

33 try: 

34 response = session.xenapi.host.call_plugin( 

35 host_ref, MANAGER_PLUGIN, method, args 

36 ) 

37 except Exception as e: 

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

39 method, args, e 

40 )) 

41 raise util.SMException(str(e)) 

42 

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

44 method, args, response 

45 )) 

46 

47 return response 

48 

49 

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

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

52 if ignoreMissingFooter: 

53 cmd.append("-i") 

54 if fast: 

55 cmd.append("-B") 

56 

57 vhdutil.ioretry(cmd) 

58 

59 

60class LinstorCallException(util.SMException): 

61 def __init__(self, cmd_err): 

62 self.cmd_err = cmd_err 

63 

64 def __str__(self): 

65 return str(self.cmd_err) 

66 

67 

68class ErofsLinstorCallException(LinstorCallException): 

69 pass 

70 

71 

72class NoPathLinstorCallException(LinstorCallException): 

73 pass 

74 

75 

76def linstorhostcall(local_method, remote_method): 

77 def decorated(response_parser): 

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

79 self = args[0] 

80 vdi_uuid = args[1] 

81 

82 device_path = self._linstor.build_device_path( 

83 self._linstor.get_volume_name(vdi_uuid) 

84 ) 

85 

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

87 # remote request. 

88 

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

90 # is up to date and not diskless. 

91 (node_names, in_use_by) = \ 

92 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid) 

93 

94 local_e = None 

95 try: 

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

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

98 except ErofsLinstorCallException as e: 

99 local_e = e.cmd_err 

100 except Exception as e: 

101 local_e = e 

102 

103 util.SMlog( 

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

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

106 ) 

107 ) 

108 

109 if in_use_by: 

110 node_names = {in_use_by} 

111 

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

113 remote_args = { 

114 'devicePath': device_path, 

115 'groupName': self._linstor.group_name 

116 } 

117 remote_args.update(**kwargs) 

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

119 

120 try: 

121 def remote_call(): 

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

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

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

125 except Exception as remote_e: 

126 self._raise_openers_exception(device_path, local_e or remote_e) 

127 

128 return response_parser(self, vdi_uuid, response) 

129 return wrapper 

130 return decorated 

131 

132 

133def linstormodifier(): 

134 def decorated(func): 

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

136 self = args[0] 

137 

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

139 self._linstor.invalidate_resource_cache() 

140 return ret 

141 return wrapper 

142 return decorated 

143 

144 

145class LinstorVhdUtil: 

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

147 

148 def __init__(self, session, linstor): 

149 self._session = session 

150 self._linstor = linstor 

151 

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

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

154 # Useful for the snapshot code algorithm. 

155 

156 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid) 

157 path = leaf_vdi_path 

158 while True: 

159 if not util.pathexists(path): 

160 raise xs_errors.XenError( 

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

162 ) 

163 

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

165 def check_volume_usable(): 

166 while True: 

167 try: 

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

169 pass 

170 except IOError as e: 

171 if e.errno == errno.ENODATA: 

172 time.sleep(2) 

173 continue 

174 if e.errno == errno.EROFS: 

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

176 self._linstor.get_volume_openers(vdi_uuid) 

177 )) 

178 raise 

179 break 

180 util.retry(check_volume_usable, 15, 2) 

181 

182 vdi_uuid = self.get_vhd_info(vdi_uuid).parentUuid 

183 if not vdi_uuid: 

184 break 

185 path = self._linstor.get_device_path(vdi_uuid) 

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

187 

188 return leaf_vdi_path 

189 

190 # -------------------------------------------------------------------------- 

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

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

193 

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

195 kwargs = { 

196 'ignoreMissingFooter': ignore_missing_footer, 

197 'fast': fast 

198 } 

199 try: 

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

201 return True 

202 except Exception as e: 

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

204 return False 

205 

206 @linstorhostcall(check_ex, 'check') 

207 def _check(self, vdi_uuid, response): 

208 return distutils.util.strtobool(response) 

209 

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

211 kwargs = { 

212 'includeParent': include_parent, 

213 'resolveParent': False 

214 } 

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

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

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

218 

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

220 def _get_vhd_info(self, vdi_uuid, response): 

221 obj = json.loads(response) 

222 

223 vhd_info = vhdutil.VHDInfo(vdi_uuid) 

224 vhd_info.sizeVirt = obj['sizeVirt'] 

225 vhd_info.sizePhys = obj['sizePhys'] 

226 if 'parentPath' in obj: 

227 vhd_info.parentPath = obj['parentPath'] 

228 vhd_info.parentUuid = obj['parentUuid'] 

229 vhd_info.hidden = obj['hidden'] 

230 vhd_info.path = obj['path'] 

231 

232 return vhd_info 

233 

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

235 def has_parent(self, vdi_uuid, response): 

236 return distutils.util.strtobool(response) 

237 

238 def get_parent(self, vdi_uuid): 

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

240 

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

242 def _get_parent(self, vdi_uuid, response): 

243 return response 

244 

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

246 def get_size_virt(self, vdi_uuid, response): 

247 return int(response) 

248 

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

250 def get_size_phys(self, vdi_uuid, response): 

251 return int(response) 

252 

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

254 def get_allocated_size(self, vdi_uuid, response): 

255 return int(response) 

256 

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

258 def get_depth(self, vdi_uuid, response): 

259 return int(response) 

260 

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

262 def get_key_hash(self, vdi_uuid, response): 

263 return response or None 

264 

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

266 def get_block_bitmap(self, vdi_uuid, response): 

267 return base64.b64decode(response) 

268 

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

270 def get_drbd_size(self, vdi_uuid, response): 

271 return int(response) 

272 

273 def _get_drbd_size(self, path): 

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

275 if ret == 0: 

276 return int(stdout.strip()) 

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

278 

279 # -------------------------------------------------------------------------- 

280 # Setters: only used locally. 

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

282 

283 @linstormodifier() 

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

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

286 

287 @linstormodifier() 

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

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

290 

291 @linstormodifier() 

292 def set_size_virt_fast(self, path, size): 

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

294 

295 @linstormodifier() 

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

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

298 

299 @linstormodifier() 

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

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

302 

303 @linstormodifier() 

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

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

306 

307 @linstormodifier() 

308 def set_key(self, path, key_hash): 

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

310 

311 @linstormodifier() 

312 def kill_data(self, path): 

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

314 

315 @linstormodifier() 

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

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

318 

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

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

321 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

322 if new_size <= old_size: 

323 return 

324 

325 util.SMlog( 

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

327 .format(vdi_path, new_size, old_size) 

328 ) 

329 

330 journaler.create( 

331 LinstorJournaler.INFLATE, vdi_uuid, old_size 

332 ) 

333 self._linstor.resize_volume(vdi_uuid, new_size) 

334 

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

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

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

338 if result_size < new_size: 

339 util.SMlog( 

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

341 .format(new_size, result_size) 

342 ) 

343 

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

345 self.set_size_phys(vdi_path, result_size, False) 

346 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid) 

347 

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

349 if zeroize: 

350 assert old_size > vhdutil.VHD_FOOTER_SIZE 

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

352 

353 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

354 if new_size >= old_size: 

355 return 

356 

357 util.SMlog( 

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

359 .format(vdi_path, new_size, old_size) 

360 ) 

361 

362 self.set_size_phys(vdi_path, new_size) 

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

364 

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

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

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

368 

369 @linstormodifier() 

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

371 kwargs = { 

372 'parentPath': str(parentPath), 

373 'parentRaw': parentRaw 

374 } 

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

376 

377 @linstormodifier() 

378 def force_coalesce(self, path): 

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

380 

381 @linstormodifier() 

382 def force_repair(self, path): 

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

384 

385 @linstormodifier() 

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

387 kwargs = { 

388 'newSize': newSize, 

389 'oldSize': oldSize, 

390 'zeroize': zeroize 

391 } 

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

393 

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

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

396 

397 # -------------------------------------------------------------------------- 

398 # Static helpers. 

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

400 

401 @classmethod 

402 def compute_volume_size(cls, virtual_size, image_type): 

403 if image_type == vhdutil.VDI_TYPE_VHD: 

404 # All LINSTOR VDIs have the metadata area preallocated for 

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

406 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE) 

407 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size) 

408 virtual_size += meta_overhead + bitmap_overhead 

409 elif image_type != vhdutil.VDI_TYPE_RAW: 

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

411 

412 return LinstorVolumeManager.round_up_volume_size(virtual_size) 

413 

414 # -------------------------------------------------------------------------- 

415 # Helpers. 

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

417 

418 def _extract_uuid(self, device_path): 

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

420 return self._linstor.get_volume_uuid_from_device_path( 

421 device_path.rstrip('\n') 

422 ) 

423 

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

425 """ 

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

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

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

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

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

431 """ 

432 

433 if not node_names: 

434 raise xs_errors.XenError( 

435 'VDIUnavailable', 

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

437 .format(vdi_uuid, device_path) 

438 ) 

439 

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

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

442 if host_record['hostname'] in node_names: 

443 return host_ref 

444 

445 raise xs_errors.XenError( 

446 'VDIUnavailable', 

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

448 .format(vdi_uuid, device_path) 

449 ) 

450 

451 # -------------------------------------------------------------------------- 

452 

453 def _raise_openers_exception(self, device_path, e): 

454 if isinstance(e, util.CommandException): 

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

456 else: 

457 e_str = str(e) 

458 

459 try: 

460 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

461 device_path 

462 ) 

463 e_wrapper = Exception( 

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

465 self._linstor.get_volume_openers(volume_uuid) 

466 ) 

467 ) 

468 except Exception as illformed_e: 

469 e_wrapper = Exception( 

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

471 ) 

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

473 raise e_wrapper # pylint: disable = E0702 

474 

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

476 if isinstance(local_method, str): 

477 local_method = getattr(self, local_method) 

478 

479 try: 

480 def local_call(): 

481 try: 

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

483 except util.CommandException as e: 

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

485 raise ErofsLinstorCallException(e) # Break retry calls. 

486 if e.code == errno.ENOENT: 

487 raise NoPathLinstorCallException(e) 

488 raise e 

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

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

491 except util.CommandException as e: 

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

493 raise e 

494 

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

496 try: 

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

498 except ErofsLinstorCallException as e: 

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

500 self._raise_openers_exception(device_path, e.cmd_err) 

501 

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

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

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

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

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

507 

508 if isinstance(local_method, str): 

509 local_method = getattr(self, local_method) 

510 

511 # A. Try to write locally... 

512 try: 

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

514 except Exception: 

515 pass 

516 

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

518 

519 # B. Execute the command on another host. 

520 # B.1. Get host list. 

521 try: 

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

523 except Exception as e: 

524 raise xs_errors.XenError( 

525 'VDIUnavailable', 

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

527 .format(remote_method, device_path, e) 

528 ) 

529 

530 # B.2. Prepare remote args. 

531 remote_args = { 

532 'devicePath': device_path, 

533 'groupName': self._linstor.group_name 

534 } 

535 remote_args.update(**kwargs) 

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

537 

538 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

539 device_path 

540 ) 

541 parent_volume_uuid = None 

542 if use_parent: 

543 parent_volume_uuid = self.get_parent(volume_uuid) 

544 

545 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

546 

547 # B.3. Call! 

548 def remote_call(): 

549 try: 

550 all_openers = self._linstor.get_volume_openers(openers_uuid) 

551 except Exception as e: 

552 raise xs_errors.XenError( 

553 'VDIUnavailable', 

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

555 .format(remote_method, device_path, e) 

556 ) 

557 

558 no_host_found = True 

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

560 if not openers: 

561 continue 

562 

563 try: 

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

565 except StopIteration: 

566 continue 

567 

568 no_host_found = False 

569 try: 

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

571 except Exception: 

572 pass 

573 

574 if no_host_found: 

575 try: 

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

577 except Exception as e: 

578 self._raise_openers_exception(device_path, e) 

579 

580 raise xs_errors.XenError( 

581 'VDIUnavailable', 

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

583 .format(remote_method, device_path, openers) 

584 ) 

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

586 

587 @staticmethod 

588 def _zeroize(path, size): 

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

590 raise xs_errors.XenError( 

591 'EIO', 

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

593 )