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 util 

25import vhdutil 

26import xs_errors 

27 

28MANAGER_PLUGIN = 'linstor-manager' 

29 

30 

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

32 try: 

33 response = session.xenapi.host.call_plugin( 

34 host_ref, MANAGER_PLUGIN, method, args 

35 ) 

36 except Exception as e: 

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

38 method, args, e 

39 )) 

40 raise util.SMException(str(e)) 

41 

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

43 method, args, response 

44 )) 

45 

46 return response 

47 

48 

49class LinstorCallException(util.SMException): 

50 def __init__(self, cmd_err): 

51 self.cmd_err = cmd_err 

52 

53 def __str__(self): 

54 return str(self.cmd_err) 

55 

56 

57class ErofsLinstorCallException(LinstorCallException): 

58 pass 

59 

60 

61class NoPathLinstorCallException(LinstorCallException): 

62 pass 

63 

64 

65def linstorhostcall(local_method, remote_method): 

66 def decorated(response_parser): 

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

68 self = args[0] 

69 vdi_uuid = args[1] 

70 

71 device_path = self._linstor.build_device_path( 

72 self._linstor.get_volume_name(vdi_uuid) 

73 ) 

74 

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

76 # remote request. 

77 

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

79 # is up to date and not diskless. 

80 (node_names, in_use_by) = \ 

81 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid) 

82 

83 local_e = None 

84 try: 

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

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

87 except ErofsLinstorCallException as e: 

88 local_e = e.cmd_err 

89 except Exception as e: 

90 local_e = e 

91 

92 util.SMlog( 

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

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

95 ) 

96 ) 

97 

98 if in_use_by: 

99 node_names = {in_use_by} 

100 

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

102 remote_args = { 

103 'devicePath': device_path, 

104 'groupName': self._linstor.group_name 

105 } 

106 remote_args.update(**kwargs) 

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

108 

109 try: 

110 def remote_call(): 

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

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

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

114 except Exception as remote_e: 

115 self._raise_openers_exception(device_path, local_e or remote_e) 

116 

117 return response_parser(self, vdi_uuid, response) 

118 return wrapper 

119 return decorated 

120 

121 

122def linstormodifier(): 

123 def decorated(func): 

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

125 self = args[0] 

126 

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

128 self._linstor.invalidate_resource_cache() 

129 return ret 

130 return wrapper 

131 return decorated 

132 

133 

134class LinstorVhdUtil: 

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

136 

137 def __init__(self, session, linstor): 

138 self._session = session 

139 self._linstor = linstor 

140 

141 # -------------------------------------------------------------------------- 

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

143 # -------------------------------------------------------------------------- 

144 

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

146 kwargs = { 

147 'ignoreMissingFooter': ignore_missing_footer, 

148 'fast': fast 

149 } 

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

151 

152 @linstorhostcall(vhdutil.check, 'check') 

153 def _check(self, vdi_uuid, response): 

154 return distutils.util.strtobool(response) 

155 

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

157 kwargs = { 

158 'includeParent': include_parent, 

159 'resolveParent': False 

160 } 

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

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

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

164 

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

166 def _get_vhd_info(self, vdi_uuid, response): 

167 obj = json.loads(response) 

168 

169 vhd_info = vhdutil.VHDInfo(vdi_uuid) 

170 vhd_info.sizeVirt = obj['sizeVirt'] 

171 vhd_info.sizePhys = obj['sizePhys'] 

172 if 'parentPath' in obj: 

173 vhd_info.parentPath = obj['parentPath'] 

174 vhd_info.parentUuid = obj['parentUuid'] 

175 vhd_info.hidden = obj['hidden'] 

176 vhd_info.path = obj['path'] 

177 

178 return vhd_info 

179 

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

181 def has_parent(self, vdi_uuid, response): 

182 return distutils.util.strtobool(response) 

183 

184 def get_parent(self, vdi_uuid): 

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

186 

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

188 def _get_parent(self, vdi_uuid, response): 

189 return response 

190 

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

192 def get_size_virt(self, vdi_uuid, response): 

193 return int(response) 

194 

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

196 def get_size_phys(self, vdi_uuid, response): 

197 return int(response) 

198 

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

200 def get_allocated_size(self, vdi_uuid, response): 

201 return int(response) 

202 

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

204 def get_depth(self, vdi_uuid, response): 

205 return int(response) 

206 

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

208 def get_key_hash(self, vdi_uuid, response): 

209 return response or None 

210 

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

212 def get_block_bitmap(self, vdi_uuid, response): 

213 return base64.b64decode(response) 

214 

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

216 def get_drbd_size(self, vdi_uuid, response): 

217 return int(response) 

218 

219 def _get_drbd_size(self, path): 

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

221 if ret == 0: 

222 return int(stdout.strip()) 

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

224 

225 # -------------------------------------------------------------------------- 

226 # Setters: only used locally. 

227 # -------------------------------------------------------------------------- 

228 

229 @linstormodifier() 

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

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

232 

233 @linstormodifier() 

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

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

236 

237 @linstormodifier() 

238 def set_size_virt_fast(self, path, size): 

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

240 

241 @linstormodifier() 

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

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

244 

245 @linstormodifier() 

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

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

248 

249 @linstormodifier() 

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

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

252 

253 @linstormodifier() 

254 def set_key(self, path, key_hash): 

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

256 

257 @linstormodifier() 

258 def kill_data(self, path): 

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

260 

261 @linstormodifier() 

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

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

264 

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

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

267 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

268 if new_size <= old_size: 

269 return 

270 

271 util.SMlog( 

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

273 .format(vdi_path, new_size, old_size) 

274 ) 

275 

276 journaler.create( 

277 LinstorJournaler.INFLATE, vdi_uuid, old_size 

278 ) 

279 self._linstor.resize_volume(vdi_uuid, new_size) 

280 

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

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

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

284 if result_size < new_size: 

285 util.SMlog( 

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

287 .format(new_size, result_size) 

288 ) 

289 

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

291 self.set_size_phys(vdi_path, result_size, False) 

292 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid) 

293 

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

295 if zeroize: 

296 assert old_size > vhdutil.VHD_FOOTER_SIZE 

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

298 

299 new_size = LinstorVolumeManager.round_up_volume_size(new_size) 

300 if new_size >= old_size: 

301 return 

302 

303 util.SMlog( 

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

305 .format(vdi_path, new_size, old_size) 

306 ) 

307 

308 self.set_size_phys(vdi_path, new_size) 

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

310 

311 # -------------------------------------------------------------------------- 

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

313 # -------------------------------------------------------------------------- 

314 

315 @linstormodifier() 

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

317 kwargs = { 

318 'parentPath': str(parentPath), 

319 'parentRaw': parentRaw 

320 } 

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

322 

323 @linstormodifier() 

324 def force_coalesce(self, path): 

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

326 

327 @linstormodifier() 

328 def force_repair(self, path): 

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

330 

331 @linstormodifier() 

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

333 kwargs = { 

334 'newSize': newSize, 

335 'oldSize': oldSize, 

336 'zeroize': zeroize 

337 } 

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

339 

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

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

342 

343 # -------------------------------------------------------------------------- 

344 # Static helpers. 

345 # -------------------------------------------------------------------------- 

346 

347 @classmethod 

348 def compute_volume_size(cls, virtual_size, image_type): 

349 if image_type == vhdutil.VDI_TYPE_VHD: 

350 # All LINSTOR VDIs have the metadata area preallocated for 

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

352 meta_overhead = vhdutil.calcOverheadEmpty(cls.MAX_SIZE) 

353 bitmap_overhead = vhdutil.calcOverheadBitmap(virtual_size) 

354 virtual_size += meta_overhead + bitmap_overhead 

355 elif image_type != vhdutil.VDI_TYPE_RAW: 

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

357 

358 return LinstorVolumeManager.round_up_volume_size(virtual_size) 

359 

360 # -------------------------------------------------------------------------- 

361 # Helpers. 

362 # -------------------------------------------------------------------------- 

363 

364 def _extract_uuid(self, device_path): 

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

366 return self._linstor.get_volume_uuid_from_device_path( 

367 device_path.rstrip('\n') 

368 ) 

369 

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

371 """ 

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

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

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

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

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

377 """ 

378 

379 if not node_names: 

380 raise xs_errors.XenError( 

381 'VDIUnavailable', 

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

383 .format(vdi_uuid, device_path) 

384 ) 

385 

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

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

388 if host_record['hostname'] in node_names: 

389 return host_ref 

390 

391 raise xs_errors.XenError( 

392 'VDIUnavailable', 

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

394 .format(vdi_uuid, device_path) 

395 ) 

396 

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

398 

399 def _raise_openers_exception(self, device_path, e): 

400 if isinstance(e, util.CommandException): 

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

402 else: 

403 e_str = str(e) 

404 

405 try: 

406 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

407 device_path 

408 ) 

409 e_wrapper = Exception( 

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

411 self._linstor.get_volume_openers(volume_uuid) 

412 ) 

413 ) 

414 except Exception as illformed_e: 

415 e_wrapper = Exception( 

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

417 ) 

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

419 raise e_wrapper # pylint: disable = E0702 

420 

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

422 if isinstance(local_method, str): 

423 local_method = getattr(self, local_method) 

424 

425 try: 

426 def local_call(): 

427 try: 

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

429 except util.CommandException as e: 

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

431 raise ErofsLinstorCallException(e) # Break retry calls. 

432 if e.code == errno.ENOENT: 

433 raise NoPathLinstorCallException(e) 

434 raise e 

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

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

437 except util.CommandException as e: 

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

439 raise e 

440 

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

442 try: 

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

444 except ErofsLinstorCallException as e: 

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

446 self._raise_openers_exception(device_path, e.cmd_err) 

447 

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

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

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

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

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

453 

454 if isinstance(local_method, str): 

455 local_method = getattr(self, local_method) 

456 

457 # A. Try to write locally... 

458 try: 

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

460 except Exception: 

461 pass 

462 

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

464 

465 # B. Execute the command on another host. 

466 # B.1. Get host list. 

467 try: 

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

469 except Exception as e: 

470 raise xs_errors.XenError( 

471 'VDIUnavailable', 

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

473 .format(remote_method, device_path, e) 

474 ) 

475 

476 # B.2. Prepare remote args. 

477 remote_args = { 

478 'devicePath': device_path, 

479 'groupName': self._linstor.group_name 

480 } 

481 remote_args.update(**kwargs) 

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

483 

484 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

485 device_path 

486 ) 

487 parent_volume_uuid = None 

488 if use_parent: 

489 parent_volume_uuid = self.get_parent(volume_uuid) 

490 

491 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

492 

493 # B.3. Call! 

494 def remote_call(): 

495 try: 

496 all_openers = self._linstor.get_volume_openers(openers_uuid) 

497 except Exception as e: 

498 raise xs_errors.XenError( 

499 'VDIUnavailable', 

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

501 .format(remote_method, device_path, e) 

502 ) 

503 

504 no_host_found = True 

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

506 if not openers: 

507 continue 

508 

509 try: 

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

511 except StopIteration: 

512 continue 

513 

514 no_host_found = False 

515 try: 

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

517 except Exception: 

518 pass 

519 

520 if no_host_found: 

521 try: 

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

523 except Exception as e: 

524 self._raise_openers_exception(device_path, e) 

525 

526 raise xs_errors.XenError( 

527 'VDIUnavailable', 

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

529 .format(remote_method, device_path, openers) 

530 ) 

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

532 

533 @staticmethod 

534 def _zeroize(path, size): 

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

536 raise xs_errors.XenError( 

537 'EIO', 

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

539 )