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 

19import base64 

20import errno 

21import json 

22import socket 

23import time 

24 

25from cowutil import CowImageInfo, CowUtil, getCowUtil 

26import util 

27import xs_errors 

28 

29from linstorjournaler import LinstorJournaler 

30from linstorvolumemanager import LinstorVolumeManager 

31from vditype import VdiType 

32 

33MANAGER_PLUGIN = 'linstor-manager' 

34 

35 

36def call_remote_method(session, host_ref, method, args): 

37 try: 

38 response = session.xenapi.host.call_plugin( 

39 host_ref, MANAGER_PLUGIN, method, args 

40 ) 

41 except Exception as e: 

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

43 host_ref, method, args, e 

44 )) 

45 raise util.SMException(str(e)) 

46 

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

48 host_ref, method, args, response 

49 )) 

50 

51 return response 

52 

53 

54class LinstorCallException(util.SMException): 

55 def __init__(self, cmd_err): 

56 self.cmd_err = cmd_err 

57 

58 @override 

59 def __str__(self) -> str: 

60 return str(self.cmd_err) 

61 

62 

63class ErofsLinstorCallException(LinstorCallException): 

64 pass 

65 

66 

67class NoPathLinstorCallException(LinstorCallException): 

68 pass 

69 

70def log_successful_call(target_host, device_path, vdi_uuid, remote_method, response): 

71 util.SMlog('Successful access on {} for device {} ({}): `{}` => {}'.format( 

72 target_host, device_path, vdi_uuid, remote_method, str(response) 

73 ), priority=util.LOG_DEBUG) 

74 

75def log_failed_call(target_host, next_target, device_path, vdi_uuid, remote_method, e): 

76 util.SMlog('Failed to call method on {} for device {} ({}): {}. Trying accessing on {}... (cause: {})'.format( 

77 target_host, device_path, vdi_uuid, remote_method, next_target, e 

78 ), priority=util.LOG_DEBUG) 

79 

80def linstorhostcall(local_method, remote_method=None): 

81 if not remote_method: 

82 remote_method = local_method 

83 

84 def decorated(response_parser): 

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

86 self = args[0] 

87 vdi_uuid = args[1] 

88 

89 device_path = self._linstor.build_device_path( 

90 self._linstor.get_volume_name(vdi_uuid) 

91 ) 

92 

93 if not self._session: 

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

95 

96 remote_args = { 

97 'devicePath': device_path, 

98 'groupName': self._linstor.group_name, 

99 'vdiType': self._vdi_type 

100 } 

101 remote_args.update(**kwargs) 

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

103 

104 this_host_ref = util.get_this_host_ref(self._session) 

105 def call_method(host_label, host_ref): 

106 if host_ref == this_host_ref: 

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

108 response = call_remote_method(self._session, host_ref, remote_method, remote_args) 

109 log_successful_call(host_label, device_path, vdi_uuid, remote_method, response) 

110 return response_parser(self, vdi_uuid, response) 

111 

112 # 1. Try on attached host. 

113 try: 

114 host_ref_attached = next(iter(util.get_hosts_attached_on(self._session, [vdi_uuid])), None) 

115 if host_ref_attached: 

116 return call_method('attached host', host_ref_attached) 

117 except Exception as e: 

118 log_failed_call('attached host', 'master', device_path, vdi_uuid, remote_method, e) 

119 

120 # 2. Try on master host. 

121 try: 

122 return call_method('master', util.get_master_ref(self._session)) 

123 except Exception as e: 

124 log_failed_call('master', 'primary', device_path, vdi_uuid, remote_method, e) 

125 

126 # 3. Try on a primary. 

127 hosts = self._get_hosts(remote_method, device_path) 

128 

129 nodes, primary_hostname = self._linstor.find_up_to_date_diskful_nodes(vdi_uuid) 

130 if primary_hostname: 

131 try: 

132 return call_method('primary', self._find_host_ref_from_hostname(hosts, primary_hostname)) 

133 except Exception as remote_e: 

134 self._raise_openers_exception(device_path, remote_e) 

135 

136 log_failed_call('primary', 'another node', device_path, vdi_uuid, remote_method, 'no primary') 

137 

138 # 4. Try on any host with local data. 

139 try: 

140 return call_method('another node', next(filter(None, 

141 (self._find_host_ref_from_hostname(hosts, hostname) for hostname in nodes) 

142 ), None)) 

143 except Exception as remote_e: 

144 self._raise_openers_exception(device_path, remote_e) 

145 

146 return wrapper 

147 return decorated 

148 

149 

150def linstormodifier(): 

151 def decorated(func): 

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

153 self = args[0] 

154 

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

156 self._linstor.invalidate_resource_cache() 

157 return ret 

158 return wrapper 

159 return decorated 

160 

161 

162class LinstorCowUtil(object): 

163 def __init__(self, session, linstor, vdi_type: str): 

164 self._session = session 

165 self._linstor = linstor 

166 self._cowutil = getCowUtil(vdi_type) 

167 self._vdi_type = vdi_type 

168 

169 @property 

170 def cowutil(self) -> CowUtil: 

171 return self._cowutil 

172 

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

174 # OPTIMIZE: Add a limit_to_first_allocated_block param to limit cowutil calls. 

175 # Useful for the snapshot code algorithm. 

176 

177 leaf_vdi_path = self._linstor.get_device_path(vdi_uuid) 

178 path = leaf_vdi_path 

179 while True: 

180 if not util.pathexists(path): 

181 raise xs_errors.XenError( 

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

183 ) 

184 

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

186 def check_volume_usable(): 

187 while True: 

188 try: 

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

190 pass 

191 except IOError as e: 

192 if e.errno == errno.ENODATA: 

193 time.sleep(2) 

194 continue 

195 if e.errno == errno.EROFS: 

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

197 self._linstor.get_volume_openers(vdi_uuid) 

198 )) 

199 raise 

200 break 

201 util.retry(check_volume_usable, 15, 2) 

202 

203 vdi_uuid = self.get_info(vdi_uuid).parentUuid 

204 if not vdi_uuid: 

205 break 

206 path = self._linstor.get_device_path(vdi_uuid) 

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

208 

209 return leaf_vdi_path 

210 

211 # -------------------------------------------------------------------------- 

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

213 # -------------------------------------------------------------------------- 

214 

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

216 kwargs = { 

217 'ignoreMissingFooter': ignore_missing_footer, 

218 'fast': fast 

219 } 

220 return self._check(vdi_uuid, **kwargs) 

221 

222 @linstorhostcall('check') 

223 def _check(self, vdi_uuid, response): 

224 return CowUtil.CheckResult(response) 

225 

226 def get_info(self, vdi_uuid, include_parent=True): 

227 kwargs = { 

228 'includeParent': include_parent, 

229 'resolveParent': False 

230 } 

231 return self._get_info(vdi_uuid, self._extract_uuid, **kwargs) 

232 

233 @linstorhostcall('getInfo') 

234 def _get_info(self, vdi_uuid, response): 

235 obj = json.loads(response) 

236 

237 image_info = CowImageInfo(vdi_uuid) 

238 image_info.sizeVirt = obj['sizeVirt'] 

239 image_info.sizePhys = obj['sizePhys'] 

240 if 'parentPath' in obj: 

241 image_info.parentPath = obj['parentPath'] 

242 image_info.parentUuid = obj['parentUuid'] 

243 image_info.hidden = obj['hidden'] 

244 image_info.path = obj['path'] 

245 

246 return image_info 

247 

248 @linstorhostcall('hasParent') 

249 def has_parent(self, vdi_uuid, response): 

250 return util.strtobool(response) 

251 

252 def get_parent(self, vdi_uuid): 

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

254 

255 @linstorhostcall('getParent') 

256 def _get_parent(self, vdi_uuid, response): 

257 return response 

258 

259 @linstorhostcall('getSizeVirt') 

260 def get_size_virt(self, vdi_uuid, response): 

261 return int(response) 

262 

263 @linstorhostcall('getMaxResizeSize') 

264 def get_max_resize_size(self, vdi_uuid, response): 

265 return int(response) 

266 

267 @linstorhostcall('getSizePhys') 

268 def get_size_phys(self, vdi_uuid, response): 

269 return int(response) 

270 

271 @linstorhostcall('getAllocatedSize') 

272 def get_allocated_size(self, vdi_uuid, response): 

273 return int(response) 

274 

275 @linstorhostcall('getDepth') 

276 def get_depth(self, vdi_uuid, response): 

277 return int(response) 

278 

279 @linstorhostcall('getKeyHash') 

280 def get_key_hash(self, vdi_uuid, response): 

281 return response or None 

282 

283 @linstorhostcall('getBlockBitmap') 

284 def get_block_bitmap(self, vdi_uuid, response): 

285 return base64.b64decode(response) 

286 

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

288 def get_drbd_size(self, vdi_uuid, response): 

289 return int(response) 

290 

291 def _get_drbd_size(self, path): 

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

293 if ret == 0: 

294 return int(stdout.strip()) 

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

296 

297 # -------------------------------------------------------------------------- 

298 # Setters: only used locally. 

299 # -------------------------------------------------------------------------- 

300 

301 @linstormodifier() 

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

303 return self._call_local_method_or_fail(self._cowutil.create, path, size, static, msize) 

304 

305 @linstormodifier() 

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

307 return self._call_local_method_or_fail(self._cowutil.setSizePhys, path, size, debug) 

308 

309 @linstormodifier() 

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

311 return self._call_local_method_or_fail(self._cowutil.setParent, path, parentPath, parentRaw) 

312 

313 @linstormodifier() 

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

315 return self._call_local_method_or_fail(self._cowutil.setHidden, path, hidden) 

316 

317 @linstormodifier() 

318 def set_key(self, path, key_hash): 

319 return self._call_local_method_or_fail(self._cowutil.setKey, path, key_hash) 

320 

321 @linstormodifier() 

322 def kill_data(self, path): 

323 return self._call_local_method_or_fail(self._cowutil.killData, path) 

324 

325 @linstormodifier() 

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

327 return self._call_local_method_or_fail(self._cowutil.snapshot, path, parent, parentRaw, msize, checkEmpty) 

328 

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

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

331 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

332 if new_size <= old_size: 

333 return 

334 

335 util.SMlog( 

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

337 .format(vdi_path, new_size, old_size) 

338 ) 

339 

340 journaler.create( 

341 LinstorJournaler.INFLATE, vdi_uuid, old_size 

342 ) 

343 self._linstor.resize_volume(vdi_uuid, new_size) 

344 

345 result_size = self.get_drbd_size(vdi_uuid) 

346 if result_size < new_size: 

347 util.SMlog( 

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

349 .format(new_size, result_size) 

350 ) 

351 

352 self._zeroize(vdi_path, result_size - self._cowutil.getFooterSize()) 

353 self.set_size_phys(vdi_path, result_size, False) 

354 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid) 

355 

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

357 if zeroize: 

358 assert old_size > self._cowutil.getFooterSize() 

359 self._zeroize(vdi_path, old_size - self._cowutil.getFooterSize()) 

360 

361 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

362 if new_size >= old_size: 

363 return 

364 

365 util.SMlog( 

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

367 .format(vdi_path, new_size, old_size) 

368 ) 

369 

370 self.set_size_phys(vdi_path, new_size) 

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

372 

373 # -------------------------------------------------------------------------- 

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

375 # -------------------------------------------------------------------------- 

376 

377 @linstormodifier() 

378 def set_size_virt(self, path, size, jFile): 

379 kwargs = { 

380 'size': size, 

381 'jFile': jFile 

382 } 

383 return self._call_method(self._cowutil.setSizeVirt, 'setSizeVirt', path, use_parent=False, **kwargs) 

384 

385 @linstormodifier() 

386 def set_size_virt_fast(self, path, size): 

387 kwargs = { 

388 'size': size 

389 } 

390 return self._call_method(self._cowutil.setSizeVirtFast, 'setSizeVirtFast', path, use_parent=False, **kwargs) 

391 

392 @linstormodifier() 

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

394 kwargs = { 

395 'parentPath': str(parentPath), 

396 'parentRaw': parentRaw 

397 } 

398 return self._call_method(self._cowutil.setParent, 'setParent', path, use_parent=False, **kwargs) 

399 

400 @linstormodifier() 

401 def force_coalesce(self, path): 

402 return int(self._call_method(self._cowutil.coalesce, 'coalesce', path, use_parent=True)) 

403 

404 @linstormodifier() 

405 def force_repair(self, path): 

406 return self._call_method(self._cowutil.repair, 'repair', path, use_parent=False) 

407 

408 @linstormodifier() 

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

410 kwargs = { 

411 'newSize': newSize, 

412 'oldSize': oldSize, 

413 'zeroize': zeroize 

414 } 

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

416 

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

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

419 

420 # -------------------------------------------------------------------------- 

421 # Helpers. 

422 # -------------------------------------------------------------------------- 

423 

424 def compute_volume_size(self, virtual_size: int) -> int: 

425 if VdiType.isCowImage(self._vdi_type): 

426 # All LINSTOR VDIs have the metadata area preallocated for 

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

428 meta_overhead = self._cowutil.calcOverheadEmpty( 

429 max(virtual_size, self._cowutil.getDefaultPreallocationSizeVirt()) 

430 ) 

431 bitmap_overhead = self._cowutil.calcOverheadBitmap(virtual_size) 

432 virtual_size += meta_overhead + bitmap_overhead 

433 else: 

434 raise Exception('Invalid image type: {}'.format(self._vdi_type)) 

435 

436 return LinstorVolumeManager.round_up_volume_size(virtual_size) 

437 

438 def _extract_uuid(self, device_path): 

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

440 return self._linstor.get_volume_uuid_from_device_path( 

441 device_path.rstrip('\n') 

442 ) 

443 

444 def _get_hosts(self, remote_method, device_path): 

445 try: 

446 return self._session.xenapi.host.get_all_records() 

447 except Exception as e: 

448 raise xs_errors.XenError( 

449 'VDIUnavailable', 

450 opterr='Unable to get host list to run cowutil command `{}` (path={}): {}' 

451 .format(remote_method, device_path, e) 

452 ) 

453 

454 # -------------------------------------------------------------------------- 

455 

456 @staticmethod 

457 def _find_host_ref_from_hostname(hosts, hostname): 

458 return next((ref for ref, rec in hosts.items() if rec['hostname'] == hostname), None) 

459 

460 def _raise_openers_exception(self, device_path, e): 

461 if isinstance(e, util.CommandException): 

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

463 else: 

464 e_str = str(e) 

465 

466 try: 

467 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

468 device_path 

469 ) 

470 e_wrapper = Exception( 

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

472 self._linstor.get_volume_openers(volume_uuid) 

473 ) 

474 ) 

475 except Exception as illformed_e: 

476 e_wrapper = Exception( 

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

478 ) 

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

480 raise e_wrapper # pylint: disable = E0702 

481 

482 def _sanitize_local_method(self, local_method): 

483 if isinstance(local_method, str): 

484 return getattr(self if local_method.startswith('_') else self._cowutil, local_method) 

485 return local_method 

486 

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

488 local_method = self._sanitize_local_method(local_method) 

489 

490 try: 

491 def local_call(): 

492 try: 

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

494 except util.CommandException as e: 

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

496 raise ErofsLinstorCallException(e) # Break retry calls. 

497 if e.code == errno.ENOENT: 

498 raise NoPathLinstorCallException(e) 

499 raise e 

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

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

502 except util.CommandException as e: 

503 util.SMlog('failed to execute locally CowUtil (sys {})'.format(e.code)) 

504 raise e 

505 

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

507 try: 

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

509 except ErofsLinstorCallException as e: 

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

511 self._raise_openers_exception(device_path, e.cmd_err) 

512 

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

514 # Note: `use_parent` exists to know if the COW image parent is used by the local/remote method. 

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

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

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

518 

519 local_method = self._sanitize_local_method(local_method) 

520 

521 # A. Try to write locally... 

522 try: 

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

524 except Exception: 

525 pass 

526 

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

528 

529 # B. Execute the command on another host. 

530 # B.1. Get host list. 

531 hosts = self._get_hosts(remote_method, device_path) 

532 

533 # B.2. Prepare remote args. 

534 remote_args = { 

535 'devicePath': device_path, 

536 'groupName': self._linstor.group_name, 

537 'vdiType': self._vdi_type 

538 } 

539 remote_args.update(**kwargs) 

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

541 

542 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

543 device_path 

544 ) 

545 parent_volume_uuid = None 

546 if use_parent: 

547 parent_volume_uuid = self.get_parent(volume_uuid) 

548 

549 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

550 

551 # B.3. Call! 

552 def remote_call(): 

553 try: 

554 all_openers = self._linstor.get_volume_openers(openers_uuid) 

555 except Exception as e: 

556 raise xs_errors.XenError( 

557 'VDIUnavailable', 

558 opterr='Unable to get DRBD openers to run CowUtil command `{}` (path={}): {}' 

559 .format(remote_method, device_path, e) 

560 ) 

561 

562 no_host_found = True 

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

564 if not openers: 

565 continue 

566 

567 host_ref = self._find_host_ref_from_hostname(hosts, hostname) 

568 if not host_ref: 

569 continue 

570 

571 no_host_found = False 

572 try: 

573 return call_remote_method(self._session, host_ref, remote_method, remote_args) 

574 except Exception: 

575 pass 

576 

577 if no_host_found: 

578 try: 

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

580 except Exception as e: 

581 self._raise_openers_exception(device_path, e) 

582 

583 raise xs_errors.XenError( 

584 'VDIUnavailable', 

585 opterr='No valid host found to run CowUtil command `{}` (path=`{}`, openers=`{}`)' 

586 .format(remote_method, device_path, openers) 

587 ) 

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

589 

590 def _zeroize(self, path, size): 

591 if not util.zeroOut(path, size, self._cowutil.getFooterSize()): 

592 raise xs_errors.XenError( 

593 'EIO', 

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

595 )