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 

17import base64 

18import distutils.util 

19import errno 

20import json 

21import socket 

22import util 

23import vhdutil 

24import xs_errors 

25 

26MANAGER_PLUGIN = 'linstor-manager' 

27 

28 

29def call_vhd_util_on_host(session, host_ref, method, device_path, args): 

30 try: 

31 response = session.xenapi.host.call_plugin( 

32 host_ref, MANAGER_PLUGIN, method, args 

33 ) 

34 except Exception as e: 

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

36 method, args, e 

37 )) 

38 raise 

39 

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

41 method, args, response 

42 )) 

43 

44 return response 

45 

46 

47class LinstorCallException(Exception): 

48 def __init__(self, cmd_err): 

49 self.cmd_err = cmd_err 

50 

51 def __str__(self): 

52 return str(self.cmd_err) 

53 

54 

55class ErofsLinstorCallException(LinstorCallException): 

56 pass 

57 

58 

59class NoPathLinstorCallException(LinstorCallException): 

60 pass 

61 

62 

63def linstorhostcall(local_method, remote_method): 

64 def decorated(response_parser): 

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

66 self = args[0] 

67 vdi_uuid = args[1] 

68 

69 device_path = self._linstor.build_device_path( 

70 self._linstor.get_volume_name(vdi_uuid) 

71 ) 

72 

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

74 # remote request. 

75 

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

77 # is up to date and not diskless. 

78 (node_names, in_use_by) = \ 

79 self._linstor.find_up_to_date_diskful_nodes(vdi_uuid) 

80 

81 local_e = None 

82 try: 

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

84 return self._call_local_vhd_util(local_method, device_path, *args[2:], **kwargs) 

85 except ErofsLinstorCallException as e: 

86 local_e = e.cmd_err 

87 except Exception as e: 

88 local_e = e 

89 

90 util.SMlog( 

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

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

93 ) 

94 ) 

95 

96 if in_use_by: 

97 node_names = {in_use_by} 

98 

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

100 remote_args = { 

101 'devicePath': device_path, 

102 'groupName': self._linstor.group_name 

103 } 

104 remote_args.update(**kwargs) 

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

106 

107 try: 

108 def remote_call(): 

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

110 return call_vhd_util_on_host(self._session, host_ref, remote_method, device_path, remote_args) 

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

112 except Exception as remote_e: 

113 self._raise_openers_exception(device_path, local_e or remote_e) 

114 

115 return response_parser(self, vdi_uuid, response) 

116 return wrapper 

117 return decorated 

118 

119 

120def linstormodifier(): 

121 def decorated(func): 

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

123 self = args[0] 

124 

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

126 self._linstor.invalidate_resource_cache() 

127 return ret 

128 return wrapper 

129 return decorated 

130 

131 

132class LinstorVhdUtil: 

133 def __init__(self, session, linstor): 

134 self._session = session 

135 self._linstor = linstor 

136 

137 # -------------------------------------------------------------------------- 

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

139 # -------------------------------------------------------------------------- 

140 

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

142 kwargs = { 

143 'ignoreMissingFooter': ignore_missing_footer, 

144 'fast': fast 

145 } 

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

147 

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

149 def _check(self, vdi_uuid, response): 

150 return distutils.util.strtobool(response) 

151 

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

153 kwargs = { 

154 'includeParent': include_parent, 

155 'resolveParent': False 

156 } 

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

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

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

160 

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

162 def _get_vhd_info(self, vdi_uuid, response): 

163 obj = json.loads(response) 

164 

165 vhd_info = vhdutil.VHDInfo(vdi_uuid) 

166 vhd_info.sizeVirt = obj['sizeVirt'] 

167 vhd_info.sizePhys = obj['sizePhys'] 

168 if 'parentPath' in obj: 

169 vhd_info.parentPath = obj['parentPath'] 

170 vhd_info.parentUuid = obj['parentUuid'] 

171 vhd_info.hidden = obj['hidden'] 

172 vhd_info.path = obj['path'] 

173 

174 return vhd_info 

175 

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

177 def has_parent(self, vdi_uuid, response): 

178 return distutils.util.strtobool(response) 

179 

180 def get_parent(self, vdi_uuid): 

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

182 

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

184 def _get_parent(self, vdi_uuid, response): 

185 return response 

186 

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

188 def get_size_virt(self, vdi_uuid, response): 

189 return int(response) 

190 

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

192 def get_size_phys(self, vdi_uuid, response): 

193 return int(response) 

194 

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

196 def get_depth(self, vdi_uuid, response): 

197 return int(response) 

198 

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

200 def get_key_hash(self, vdi_uuid, response): 

201 return response or None 

202 

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

204 def get_block_bitmap(self, vdi_uuid, response): 

205 return base64.b64decode(response) 

206 

207 # -------------------------------------------------------------------------- 

208 # Setters: only used locally. 

209 # -------------------------------------------------------------------------- 

210 

211 @linstormodifier() 

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

213 return self._call_local_vhd_util_or_fail(vhdutil.create, path, size, static, msize) 

214 

215 @linstormodifier() 

216 def set_size_virt_fast(self, path, size): 

217 return self._call_local_vhd_util_or_fail(vhdutil.setSizeVirtFast, path, size) 

218 

219 @linstormodifier() 

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

221 return self._call_local_vhd_util_or_fail(vhdutil.setSizePhys, path, size, debug) 

222 

223 @linstormodifier() 

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

225 return self._call_local_vhd_util_or_fail(vhdutil.setParent, path, parentPath, parentRaw) 

226 

227 @linstormodifier() 

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

229 return self._call_local_vhd_util_or_fail(vhdutil.setHidden, path, hidden) 

230 

231 @linstormodifier() 

232 def set_key(self, path, key_hash): 

233 return self._call_local_vhd_util_or_fail(vhdutil.setKey, path, key_hash) 

234 

235 @linstormodifier() 

236 def kill_data(self, path): 

237 return self._call_local_vhd_util_or_fail(vhdutil.killData, path) 

238 

239 @linstormodifier() 

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

241 return self._call_local_vhd_util_or_fail(vhdutil.snapshot, path, parent, parentRaw, msize, checkEmpty) 

242 

243 # -------------------------------------------------------------------------- 

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

245 # -------------------------------------------------------------------------- 

246 

247 @linstormodifier() 

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

249 kwargs = { 

250 'parentPath': str(parentPath), 

251 'parentRaw': parentRaw 

252 } 

253 return self._call_vhd_util(vhdutil.setParent, 'setParent', path, use_parent=False, **kwargs) 

254 

255 @linstormodifier() 

256 def force_coalesce(self, path): 

257 return self._call_vhd_util(vhdutil.coalesce, 'coalesce', path, use_parent=True) 

258 

259 @linstormodifier() 

260 def force_repair(self, path): 

261 return self._call_vhd_util(vhdutil.repair, 'repair', path, use_parent=False) 

262 

263 # -------------------------------------------------------------------------- 

264 # Helpers. 

265 # -------------------------------------------------------------------------- 

266 

267 def _extract_uuid(self, device_path): 

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

269 return self._linstor.get_volume_uuid_from_device_path( 

270 device_path.rstrip('\n') 

271 ) 

272 

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

274 """ 

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

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

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

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

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

280 """ 

281 

282 if not node_names: 

283 raise xs_errors.XenError( 

284 'VDIUnavailable', 

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

286 .format(vdi_uuid, device_path) 

287 ) 

288 

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

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

291 if host_record['hostname'] in node_names: 

292 return host_ref 

293 

294 raise xs_errors.XenError( 

295 'VDIUnavailable', 

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

297 .format(vdi_uuid, device_path) 

298 ) 

299 

300 # -------------------------------------------------------------------------- 

301 

302 def _raise_openers_exception(self, device_path, e): 

303 if isinstance(e, util.CommandException): 

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

305 else: 

306 e_str = str(e) 

307 

308 e_with_openers = None 

309 try: 

310 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

311 device_path 

312 ) 

313 e_wrapper = Exception( 

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

315 self._linstor.get_volume_openers(volume_uuid) 

316 ) 

317 ) 

318 except Exception as illformed_e: 

319 e_wrapper = Exception( 

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

321 ) 

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

323 raise e_wrapper # pylint: disable = E0702 

324 

325 def _call_local_vhd_util(self, local_method, device_path, *args, **kwargs): 

326 try: 

327 def local_call(): 

328 try: 

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

330 except util.CommandException as e: 

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

332 raise ErofsLinstorCallException(e) # Break retry calls. 

333 if e.code == errno.ENOENT: 

334 raise NoPathLinstorCallException(e) 

335 raise e 

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

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

338 except util.CommandException as e: 

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

340 raise e 

341 

342 def _call_local_vhd_util_or_fail(self, local_method, device_path, *args, **kwargs): 

343 try: 

344 return self._call_local_vhd_util(local_method, device_path, *args, **kwargs) 

345 except ErofsLinstorCallException as e: 

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

347 self._raise_openers_exception(device_path, e.cmd_err) 

348 

349 def _call_vhd_util(self, local_method, remote_method, device_path, use_parent, *args, **kwargs): 

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

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

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

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

354 

355 # A. Try to write locally... 

356 try: 

357 return self._call_local_vhd_util(local_method, device_path, *args, **kwargs) 

358 except Exception: 

359 pass 

360 

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

362 

363 # B. Execute the command on another host. 

364 # B.1. Get host list. 

365 try: 

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

367 except Exception as e: 

368 raise xs_errors.XenError( 

369 'VDIUnavailable', 

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

371 .format(remote_method, device_path, e) 

372 ) 

373 

374 # B.2. Prepare remote args. 

375 remote_args = { 

376 'devicePath': device_path, 

377 'groupName': self._linstor.group_name 

378 } 

379 remote_args.update(**kwargs) 

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

381 

382 volume_uuid = self._linstor.get_volume_uuid_from_device_path( 

383 device_path 

384 ) 

385 parent_volume_uuid = None 

386 if use_parent: 

387 parent_volume_uuid = self.get_parent(volume_uuid) 

388 

389 openers_uuid = parent_volume_uuid if use_parent else volume_uuid 

390 

391 # B.3. Call! 

392 def remote_call(): 

393 try: 

394 all_openers = self._linstor.get_volume_openers(openers_uuid) 

395 except Exception as e: 

396 raise xs_errors.XenError( 

397 'VDIUnavailable', 

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

399 .format(remote_method, device_path, e) 

400 ) 

401 

402 no_host_found = True 

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

404 if not openers: 

405 continue 

406 

407 try: 

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

409 except StopIteration: 

410 continue 

411 

412 no_host_found = False 

413 try: 

414 return call_vhd_util_on_host(self._session, host_ref, remote_method, device_path, remote_args) 

415 except Exception: 

416 pass 

417 

418 if no_host_found: 

419 try: 

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

421 except Exception as e: 

422 self._raise_openers_exception(device_path, e) 

423 

424 raise xs_errors.XenError( 

425 'VDIUnavailable', 

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

427 .format(remote_method, device_path, openers, e) 

428 ) 

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