Coverage for libs/sm/VDI.py: 71%

462 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2026-07-10 07:59 +0000

1# Copyright (C) Citrix Systems Inc. 

2# 

3# This program is free software; you can redistribute it and/or modify 

4# it under the terms of the GNU Lesser General Public License as published 

5# by the Free Software Foundation; version 2.1 only. 

6# 

7# This program is distributed in the hope that it will be useful, 

8# but WITHOUT ANY WARRANTY; without even the implied warranty of 

9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

10# GNU Lesser General Public License for more details. 

11# 

12# You should have received a copy of the GNU Lesser General Public License 

13# along with this program; if not, write to the Free Software Foundation, Inc., 

14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

15# 

16# VDI: Base class for virtual disk instances 

17# 

18 

19from sm import cleanup 

20from sm import SR 

21import xmlrpc.client 

22from sm.core import xs_errors 

23from sm.core import util 

24from sm.core.lock import Lock 

25from sm import vhdutil 

26from sm import cbtutil 

27import os 

28import base64 

29from sm.constants import CBTLOG_TAG 

30from bitarray import bitarray 

31import uuid 

32 

33 

34SM_CONFIG_PASS_THROUGH_FIELDS = ["base_mirror", "key_hash"] 

35 

36SNAPSHOT_SINGLE = 1 # true snapshot: 1 leaf, 1 read-only parent 

37SNAPSHOT_DOUBLE = 2 # regular snapshot/clone that creates 2 leaves 

38SNAPSHOT_INTERNAL = 3 # SNAPSHOT_SINGLE but don't update SR's virtual allocation 

39CBT_BLOCK_SIZE = (64 * 1024) 

40 

41 

42def VDIMetadataSize(type, virtualsize): 

43 size = 0 

44 if type == 'vhd': 

45 size_mb = virtualsize // (1024 * 1024) 

46 #Footer + footer copy + header + possible CoW parent locator fields 

47 size = 3 * 1024 

48 

49 # BAT 4 Bytes per block segment 

50 size += (size_mb // 2) * 4 

51 size = util.roundup(512, size) 

52 

53 # BATMAP 1 bit per block segment 

54 size += (size_mb // 2) // 8 

55 size = util.roundup(4096, size) 

56 

57 # Segment bitmaps + Page align offsets 

58 size += (size_mb // 2) * 4096 

59 

60 return size 

61 

62 

63class VDI(object): 

64 """Virtual Disk Instance descriptor. 

65 

66 Attributes: 

67 uuid: string, globally unique VDI identifier conforming to OSF DEC 1.1 

68 label: string, user-generated tag string for identifyng the VDI 

69 description: string, longer user generated description string 

70 size: int, virtual size in bytes of this VDI 

71 utilisation: int, actual size in Bytes of data on disk that is  

72 utilised. For non-sparse disks, utilisation == size 

73 vdi_type: string, disk type, e.g. raw file, partition 

74 parent: VDI object, parent backing VDI if this disk is a  

75 CoW instance 

76 shareable: boolean, does this disk support multiple writer instances? 

77 e.g. shared OCFS disk 

78 attached: boolean, whether VDI is attached 

79 read_only: boolean, whether disk is read-only. 

80 """ 

81 

82 def __init__(self, sr, uuid): 

83 self.sr = sr 

84 # Don't set either the UUID or location to None- no good can 

85 # ever come of this. 

86 if uuid is not None: 

87 self.uuid = uuid 

88 self.location = uuid 

89 self.path = None 

90 else: 

91 # We assume that children class initializors calling without 

92 # uuid will set these attributes themselves somewhere. They 

93 # are VDIs whose physical paths/locations have no direct 

94 # connections with their UUID strings (e.g. ISOSR, udevSR, 

95 # SHMSR). So we avoid overwriting these attributes here. 

96 pass 

97 # deliberately not initialised self.sm_config so that it is 

98 # ommitted from the XML output 

99 

100 self.label = '' 

101 self.description = '' 

102 self.vbds = [] 

103 self.size = 0 

104 self.utilisation = 0 

105 self.vdi_type = '' 

106 self.has_child = 0 

107 self.parent = None 

108 self.shareable = False 

109 self.attached = False 

110 self.status = 0 

111 self.read_only = False 

112 self.xenstore_data = {} 

113 self.deleted = False 

114 self.session = sr.session 

115 self.managed = True 

116 self.sm_config_override = {} 

117 self.sm_config_keep = ["key_hash"] 

118 self.ty = "user" 

119 self.cbt_enabled = False 

120 

121 self.load(uuid) 

122 

123 @staticmethod 

124 def from_uuid(session, vdi_uuid): 

125 

126 _VDI = session.xenapi.VDI 

127 vdi_ref = _VDI.get_by_uuid(vdi_uuid) 

128 sr_ref = _VDI.get_SR(vdi_ref) 

129 

130 _SR = session.xenapi.SR 

131 sr_uuid = _SR.get_uuid(sr_ref) 

132 

133 sr = SR.SR.from_uuid(session, sr_uuid) 

134 

135 sr.srcmd.params['vdi_ref'] = vdi_ref 

136 return sr.vdi(vdi_uuid) 

137 

138 def create(self, sr_uuid, vdi_uuid, size): 

139 """Create a VDI of size <Size> MB on the given SR.  

140 

141 This operation IS NOT idempotent and will fail if the UUID 

142 already exists or if there is insufficient space. The vdi must 

143 be explicitly attached via the attach() command following 

144 creation. The actual disk size created may be larger than the 

145 requested size if the substrate requires a size in multiples 

146 of a certain extent size. The SR must be queried for the exact 

147 size. 

148 """ 

149 raise xs_errors.XenError('Unimplemented') 

150 

151 def update(self, sr_uuid, vdi_uuid): 

152 """Query and update the configuration of a particular VDI. 

153 

154 Given an SR and VDI UUID, this operation returns summary statistics 

155 on the named VDI. Note the XenAPI VDI object will exist when 

156 this call is made. 

157 """ 

158 # no-op unless individual backends implement it 

159 return 

160 

161 def introduce(self, sr_uuid, vdi_uuid): 

162 """Explicitly introduce a particular VDI. 

163 

164 Given an SR and VDI UUID and a disk location (passed in via the <conf> 

165 XML), this operation verifies the existence of the underylying disk 

166 object and then creates the XenAPI VDI object. 

167 """ 

168 raise xs_errors.XenError('Unimplemented') 

169 

170 def attach(self, sr_uuid, vdi_uuid): 

171 """Initiate local access to the VDI. Initialises any device 

172 state required to access the VDI. 

173 

174 This operation IS idempotent and should succeed if the VDI can be 

175 attached or if the VDI is already attached. 

176 

177 Returns: 

178 string, local device path. 

179 """ 

180 struct = {'params': self.path, 

181 'xenstore_data': (self.xenstore_data or {})} 

182 return xmlrpc.client.dumps((struct, ), "", True) 

183 

184 def detach(self, sr_uuid, vdi_uuid): 

185 """Remove local access to the VDI. Destroys any device  

186 state initialised via the vdi.attach() command. 

187 

188 This operation is idempotent. 

189 """ 

190 raise xs_errors.XenError('Unimplemented') 

191 

192 def clone(self, sr_uuid, vdi_uuid): 

193 """Create a mutable instance of the referenced VDI. 

194 

195 This operation is not idempotent and will fail if the UUID 

196 already exists or if there is insufficient space. The SRC VDI 

197 must be in a detached state and deactivated. Upon successful 

198 creation of the clone, the clone VDI must be explicitly 

199 attached via vdi.attach(). If the driver does not support 

200 cloning this operation should raise SRUnsupportedOperation. 

201 

202 Arguments: 

203 Raises: 

204 SRUnsupportedOperation 

205 """ 

206 raise xs_errors.XenError('Unimplemented') 

207 

208 def resize_online(self, sr_uuid, vdi_uuid, size): 

209 """Resize the given VDI which may have active VBDs, which have 

210 been paused for the duration of this call.""" 

211 raise xs_errors.XenError('Unimplemented') 

212 

213 def generate_config(self, sr_uuid, vdi_uuid): 

214 """Generate the XML config required to activate a VDI for use 

215 when XAPI is not running. Activation is handled by the 

216 vdi_attach_from_config() SMAPI call. 

217 """ 

218 raise xs_errors.XenError('Unimplemented') 

219 

220 def compose(self, sr_uuid, vdi1, vdi2): 

221 """Layer the updates from [vdi2] onto [vdi1], calling the result 

222 [vdi2]. 

223 

224 Raises: 

225 SRUnsupportedOperation 

226 """ 

227 raise xs_errors.XenError('Unimplemented') 

228 

229 def attach_from_config(self, sr_uuid, vdi_uuid): 

230 """Activate a VDI based on the config passed in on the CLI. For 

231 use when XAPI is not running. The config is generated by the 

232 Activation is handled by the vdi_generate_config() SMAPI call. 

233 """ 

234 raise xs_errors.XenError('Unimplemented') 

235 

236 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType, 

237 cloneOp=False, secondary=None, cbtlog=None): 

238 raise xs_errors.XenError('Unimplemented') 

239 

240 def _delete_cbt_log(self): 

241 raise xs_errors.XenError('Unimplemented') 

242 

243 def _rename(self, old, new): 

244 raise xs_errors.XenError('Unimplemented') 

245 

246 def _cbt_log_exists(self, logpath): 

247 """Check if CBT log file exists 

248 

249 Must be implemented by all classes inheriting from base VDI class 

250 """ 

251 raise xs_errors.XenError('Unimplemented') 

252 

253 def resize(self, sr_uuid, vdi_uuid, size): 

254 """Resize the given VDI to size <size> MB. Size can 

255 be any valid disk size greater than [or smaller than] 

256 the current value. 

257 

258 This operation IS idempotent and should succeed if the VDI can 

259 be resized to the specified value or if the VDI is already the 

260 specified size. The actual disk size created may be larger 

261 than the requested size if the substrate requires a size in 

262 multiples of a certain extent size. The SR must be queried for 

263 the exact size. This operation does not modify the contents on 

264 the disk such as the filesystem. Responsibility for resizing 

265 the FS is left to the VM administrator. [Reducing the size of 

266 the disk is a very dangerous operation and should be conducted 

267 very carefully.] Disk contents should always be backed up in 

268 advance. 

269 """ 

270 raise xs_errors.XenError('Unimplemented') 

271 

272 def resize_cbt(self, sr_uuid, vdi_uuid, size): 

273 """Resize the given VDI to size <size> MB. Size can 

274 be any valid disk size greater than [or smaller than] 

275 the current value. 

276 

277 This operation IS idempotent and should succeed if the VDI can 

278 be resized to the specified value or if the VDI is already the 

279 specified size. The actual disk size created may be larger 

280 than the requested size if the substrate requires a size in 

281 multiples of a certain extent size. The SR must be queried for 

282 the exact size. This operation does not modify the contents on 

283 the disk such as the filesystem. Responsibility for resizing 

284 the FS is left to the VM administrator. [Reducing the size of 

285 the disk is a very dangerous operation and should be conducted 

286 very carefully.] Disk contents should always be backed up in 

287 advance. 

288 """ 

289 try: 

290 if self._get_blocktracking_status(): 

291 logpath = self._get_cbt_logpath(vdi_uuid) 

292 self._cbt_op(vdi_uuid, cbtutil.set_cbt_size, logpath, size) 

293 except util.CommandException as ex: 

294 alert_name = "VDI_CBT_RESIZE_FAILED" 

295 alert_str = ("Resizing of CBT metadata for disk %s failed." 

296 % vdi_uuid) 

297 self._disable_cbt_on_error(alert_name, alert_str) 

298 

299 def delete(self, sr_uuid, vdi_uuid, data_only=False): 

300 """Delete this VDI. 

301 

302 This operation IS idempotent and should succeed if the VDI 

303 exists and can be deleted or if the VDI does not exist. It is 

304 the responsibility of the higher-level management tool to 

305 ensure that the detach() operation has been explicitly called 

306 prior to deletion, otherwise the delete() will fail if the 

307 disk is still attached. 

308 """ 

309 from sm import blktap2 

310 

311 if data_only == False and self._get_blocktracking_status(): 

312 logpath = self._get_cbt_logpath(vdi_uuid) 

313 parent_uuid = self._cbt_op(vdi_uuid, cbtutil.get_cbt_parent, 

314 logpath) 

315 parent_path = self._get_cbt_logpath(parent_uuid) 

316 child_uuid = self._cbt_op(vdi_uuid, cbtutil.get_cbt_child, logpath) 

317 child_path = self._get_cbt_logpath(child_uuid) 

318 

319 lock = Lock("cbtlog", str(vdi_uuid)) 

320 

321 if self._cbt_log_exists(parent_path): 321 ↛ 325line 321 didn't jump to line 325, because the condition on line 321 was never false

322 self._cbt_op(parent_uuid, cbtutil.set_cbt_child, 

323 parent_path, child_uuid) 

324 

325 if self._cbt_log_exists(child_path): 

326 self._cbt_op(child_uuid, cbtutil.set_cbt_parent, 

327 child_path, parent_uuid) 

328 lock.acquire() 

329 paused_for_coalesce = False 

330 try: 

331 # Coalesce contents of bitmap with child's bitmap 

332 # Check if child bitmap is currently attached 

333 consistent = self._cbt_op(child_uuid, 

334 cbtutil.get_cbt_consistency, 

335 child_path) 

336 if not consistent: 

337 if not blktap2.VDI.tap_pause(self.session, 337 ↛ 339line 337 didn't jump to line 339, because the condition on line 337 was never true

338 sr_uuid, child_uuid): 

339 raise util.SMException("failed to pause VDI %s") 

340 paused_for_coalesce = True 

341 self._activate_cbt_log(self._get_cbt_logname(vdi_uuid)) 

342 self._cbt_op(child_uuid, cbtutil.coalesce_bitmap, 

343 logpath, child_path) 

344 lock.release() 

345 except util.CommandException: 

346 # If there is an exception in coalescing, 

347 # CBT log file is not deleted and pointers are reset 

348 # to what they were 

349 util.SMlog("Exception in coalescing bitmaps on VDI delete," 

350 " restoring to previous state") 

351 try: 

352 if self._cbt_log_exists(parent_path): 352 ↛ 355line 352 didn't jump to line 355, because the condition on line 352 was never false

353 self._cbt_op(parent_uuid, cbtutil.set_cbt_child, 

354 parent_path, vdi_uuid) 

355 if self._cbt_log_exists(child_path): 355 ↛ 359line 355 didn't jump to line 359, because the condition on line 355 was never false

356 self._cbt_op(child_uuid, cbtutil.set_cbt_parent, 

357 child_path, vdi_uuid) 

358 finally: 

359 lock.release() 

360 lock.cleanup("cbtlog", str(vdi_uuid)) 

361 return 

362 finally: 

363 # Unpause tapdisk if it wasn't originally paused 

364 if paused_for_coalesce: 364 ↛ 367line 364 didn't jump to line 367, because the condition on line 364 was never false

365 blktap2.VDI.tap_unpause(self.session, sr_uuid, 365 ↛ exitline 365 didn't return from function 'delete', because the return on line 361 wasn't executed

366 child_uuid) 

367 lock.acquire() 

368 try: 

369 self._delete_cbt_log() 

370 finally: 

371 lock.release() 

372 lock.cleanup("cbtlog", str(vdi_uuid)) 

373 

374 def snapshot(self, sr_uuid, vdi_uuid): 

375 """Save an immutable copy of the referenced VDI. 

376 

377 This operation IS NOT idempotent and will fail if the UUID 

378 already exists or if there is insufficient space. The vdi must 

379 be explicitly attached via the vdi_attach() command following 

380 creation. If the driver does not support snapshotting this 

381 operation should raise SRUnsupportedOperation 

382 

383 Arguments: 

384 Raises: 

385 SRUnsupportedOperation 

386 """ 

387 # logically, "snapshot" should mean SNAPSHOT_SINGLE and "clone" should 

388 # mean "SNAPSHOT_DOUBLE", but in practice we have to do SNAPSHOT_DOUBLE 

389 # in both cases, unless driver_params overrides it 

390 snapType = SNAPSHOT_DOUBLE 

391 if self.sr.srcmd.params['driver_params'].get("type"): 391 ↛ 397line 391 didn't jump to line 397, because the condition on line 391 was never false

392 if self.sr.srcmd.params['driver_params']["type"] == "single": 392 ↛ 393line 392 didn't jump to line 393, because the condition on line 392 was never true

393 snapType = SNAPSHOT_SINGLE 

394 elif self.sr.srcmd.params['driver_params']["type"] == "internal": 394 ↛ 395line 394 didn't jump to line 395, because the condition on line 394 was never true

395 snapType = SNAPSHOT_INTERNAL 

396 

397 secondary = None 

398 if self.sr.srcmd.params['driver_params'].get("mirror"): 

399 secondary = self.sr.srcmd.params['driver_params']["mirror"] 

400 

401 if self._get_blocktracking_status(): 

402 cbtlog = self._get_cbt_logpath(self.uuid) 

403 else: 

404 cbtlog = None 

405 return self._do_snapshot(sr_uuid, vdi_uuid, snapType, 

406 secondary=secondary, cbtlog=cbtlog) 

407 

408 def activate(self, sr_uuid, vdi_uuid): 

409 """Activate VDI - called pre tapdisk open""" 

410 if self._get_blocktracking_status(): 

411 if 'args' in self.sr.srcmd.params: 411 ↛ 412line 411 didn't jump to line 412, because the condition on line 411 was never true

412 read_write = self.sr.srcmd.params['args'][0] 

413 if read_write == "false": 

414 # Disk is being attached in RO mode, 

415 # don't attach metadata log file 

416 return None 

417 

418 lock = Lock("cbtlog", str(vdi_uuid)) 

419 lock.acquire() 

420 

421 try: 

422 logpath = self._get_cbt_logpath(vdi_uuid) 

423 logname = self._get_cbt_logname(vdi_uuid) 

424 

425 # Activate CBT log file, if required 

426 self._activate_cbt_log(logname) 

427 finally: 

428 lock.release() 

429 

430 # Check and update consistency 

431 consistent = self._cbt_op(vdi_uuid, cbtutil.get_cbt_consistency, 

432 logpath) 

433 if not consistent: 

434 alert_name = "VDI_CBT_METADATA_INCONSISTENT" 

435 alert_str = ("Changed Block Tracking metadata is inconsistent" 

436 " for disk %s." % vdi_uuid) 

437 self._disable_cbt_on_error(alert_name, alert_str) 

438 return None 

439 

440 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, 

441 logpath, False) 

442 return {'cbtlog': logpath} 

443 return None 

444 

445 def deactivate(self, sr_uuid, vdi_uuid): 

446 """Deactivate VDI - called post tapdisk close""" 

447 if self._get_blocktracking_status(): 

448 lock = Lock("cbtlog", str(vdi_uuid)) 

449 lock.acquire() 

450 

451 try: 

452 logpath = self._get_cbt_logpath(vdi_uuid) 

453 logname = self._get_cbt_logname(vdi_uuid) 

454 self._cbt_op(vdi_uuid, cbtutil.set_cbt_consistency, logpath, True) 

455 # Finally deactivate log file 

456 self._deactivate_cbt_log(logname) 

457 finally: 

458 lock.release() 

459 

460 def get_params(self): 

461 """ 

462 Returns: 

463 XMLRPC response containing a single struct with fields 

464 'location' and 'uuid' 

465 """ 

466 struct = {'location': self.location, 

467 'uuid': self.uuid} 

468 return xmlrpc.client.dumps((struct, ), "", True) 

469 

470 def load(self, vdi_uuid): 

471 """Post-init hook""" 

472 pass 

473 

474 def _db_introduce(self): 

475 uuid = util.default(self, "uuid", lambda: util.gen_uuid()) 475 ↛ exitline 475 didn't run the lambda on line 475

476 sm_config = util.default(self, "sm_config", lambda: {}) 

477 if "vdi_sm_config" in self.sr.srcmd.params: 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true

478 for key in SM_CONFIG_PASS_THROUGH_FIELDS: 

479 val = self.sr.srcmd.params["vdi_sm_config"].get(key) 

480 if val: 

481 sm_config[key] = val 

482 ty = util.default(self, "ty", lambda: "user") 482 ↛ exitline 482 didn't run the lambda on line 482

483 is_a_snapshot = util.default(self, "is_a_snapshot", lambda: False) 

484 metadata_of_pool = util.default(self, "metadata_of_pool", lambda: "OpaqueRef:NULL") 

485 snapshot_time = util.default(self, "snapshot_time", lambda: "19700101T00:00:00Z") 

486 snapshot_of = util.default(self, "snapshot_of", lambda: "OpaqueRef:NULL") 

487 cbt_enabled = util.default(self, "cbt_enabled", lambda: False) 487 ↛ exitline 487 didn't run the lambda on line 487

488 vdi = self.sr.session.xenapi.VDI.db_introduce(uuid, self.label, self.description, self.sr.sr_ref, ty, self.shareable, self.read_only, {}, self.location, {}, sm_config, self.managed, str(self.size), str(self.utilisation), metadata_of_pool, is_a_snapshot, xmlrpc.client.DateTime(snapshot_time), snapshot_of, cbt_enabled) 

489 return vdi 

490 

491 def _db_forget(self): 

492 self.sr.forget_vdi(self.uuid) 

493 

494 def _override_sm_config(self, sm_config): 

495 for key, val in self.sm_config_override.items(): 

496 if val == sm_config.get(key): 

497 continue 

498 if val: 498 ↛ 502line 498 didn't jump to line 502, because the condition on line 498 was never false

499 util.SMlog("_override_sm_config: %s: %s -> %s" % \ 

500 (key, sm_config.get(key), val)) 

501 sm_config[key] = val 

502 elif key in sm_config: 

503 util.SMlog("_override_sm_config: del %s" % key) 

504 del sm_config[key] 

505 

506 def _db_update_sm_config(self, ref, sm_config): 

507 from sm import cleanup 

508 # List of sm-config keys that should not be modifed by db_update 

509 smconfig_protected_keys = [ 

510 cleanup.VDI.DB_VDI_PAUSED, 

511 cleanup.VDI.DB_VHD_BLOCKS, 

512 cleanup.VDI.DB_VDI_RELINKING, 

513 cleanup.VDI.DB_VDI_ACTIVATING] 

514 

515 current_sm_config = self.sr.session.xenapi.VDI.get_sm_config(ref) 

516 for key, val in sm_config.items(): 

517 if (key.startswith("host_") or 

518 key in smconfig_protected_keys): 

519 continue 

520 if sm_config.get(key) != current_sm_config.get(key): 

521 util.SMlog("_db_update_sm_config: %s sm-config:%s %s->%s" % \ 

522 (self.uuid, key, current_sm_config.get(key), val)) 

523 self.sr.session.xenapi.VDI.remove_from_sm_config(ref, key) 

524 self.sr.session.xenapi.VDI.add_to_sm_config(ref, key, val) 

525 

526 for key in current_sm_config.keys(): 

527 if (key.startswith("host_") or 

528 key in smconfig_protected_keys or 

529 key in self.sm_config_keep): 

530 continue 

531 if not sm_config.get(key): 

532 util.SMlog("_db_update_sm_config: %s del sm-config:%s" % \ 

533 (self.uuid, key)) 

534 self.sr.session.xenapi.VDI.remove_from_sm_config(ref, key) 

535 

536 def _db_update(self): 

537 vdi = self.sr.session.xenapi.VDI.get_by_uuid(self.uuid) 

538 self.sr.session.xenapi.VDI.set_virtual_size(vdi, str(self.size)) 

539 self.sr.session.xenapi.VDI.set_physical_utilisation(vdi, str(self.utilisation)) 

540 self.sr.session.xenapi.VDI.set_read_only(vdi, self.read_only) 

541 sm_config = util.default(self, "sm_config", lambda: {}) 

542 self._override_sm_config(sm_config) 

543 self._db_update_sm_config(vdi, sm_config) 

544 self.sr.session.xenapi.VDI.set_cbt_enabled(vdi, 

545 self._get_blocktracking_status()) 

546 

547 def in_sync_with_xenapi_record(self, x): 

548 """Returns true if this VDI is in sync with the supplied XenAPI record""" 

549 if self.location != util.to_plain_string(x['location']): 

550 util.SMlog("location %s <> %s" % (self.location, x['location'])) 

551 return False 

552 if self.read_only != x['read_only']: 

553 util.SMlog("read_only %s <> %s" % (self.read_only, x['read_only'])) 

554 return False 

555 if str(self.size) != x['virtual_size']: 

556 util.SMlog("virtual_size %s <> %s" % (self.size, x['virtual_size'])) 

557 return False 

558 if str(self.utilisation) != x['physical_utilisation']: 

559 util.SMlog("utilisation %s <> %s" % (self.utilisation, x['physical_utilisation'])) 

560 return False 

561 sm_config = util.default(self, "sm_config", lambda: {}) 

562 if set(sm_config.keys()) != set(x['sm_config'].keys()): 

563 util.SMlog("sm_config %s <> %s" % (repr(sm_config), repr(x['sm_config']))) 

564 return False 

565 for k in sm_config.keys(): 

566 if sm_config[k] != x['sm_config'][k]: 

567 util.SMlog("sm_config %s <> %s" % (repr(sm_config), repr(x['sm_config']))) 

568 return False 

569 if self.cbt_enabled != x['cbt_enabled']: 

570 util.SMlog("cbt_enabled %s <> %s" % ( 

571 self.cbt_enabled, x['cbt_enabled'])) 

572 return False 

573 return True 

574 

575 def update_slaves_on_cbt_disable(self, cbtlog): 

576 # Override in implementation as required. 

577 pass 

578 

579 def configure_blocktracking(self, sr_uuid, vdi_uuid, enable): 

580 """Function for configuring blocktracking""" 

581 from sm import blktap2 

582 vdi_ref = self.sr.srcmd.params['vdi_ref'] 

583 

584 # Check if raw VDI or snapshot 

585 if self.vdi_type == vhdutil.VDI_TYPE_RAW or \ 

586 self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref): 

587 raise xs_errors.XenError('VDIType', 

588 opterr='Raw VDI or snapshot not permitted') 

589 

590 # Check if already enabled 

591 if self._get_blocktracking_status() == enable: 

592 return 

593 

594 # Save disk state before pause 

595 disk_state = blktap2.VDI.tap_status(self.session, vdi_uuid) 

596 

597 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid): 

598 error = "Failed to pause VDI %s" % vdi_uuid 

599 raise xs_errors.XenError('CBTActivateFailed', opterr=error) 

600 logfile = None 

601 

602 try: 

603 if enable: 

604 try: 

605 # Check available space 

606 self._ensure_cbt_space() 

607 logfile = self._create_cbt_log() 

608 # Set consistency 

609 if disk_state: 609 ↛ 641line 609 didn't jump to line 641, because the condition on line 609 was never false

610 util.SMlog("Setting consistency of cbtlog file to False for VDI: %s" 

611 % self.uuid) 

612 logpath = self._get_cbt_logpath(self.uuid) 

613 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, 

614 logpath, False) 

615 except Exception as error: 

616 self._delete_cbt_log() 

617 raise xs_errors.XenError('CBTActivateFailed', 

618 opterr=str(error)) 

619 else: 

620 lock = Lock("cbtlog", str(vdi_uuid)) 

621 lock.acquire() 

622 try: 

623 # Find parent of leaf metadata file, if any, 

624 # and nullify its successor 

625 logpath = self._get_cbt_logpath(self.uuid) 

626 parent = self._cbt_op(self.uuid, 

627 cbtutil.get_cbt_parent, logpath) 

628 self._delete_cbt_log() 

629 parent_path = self._get_cbt_logpath(parent) 

630 if self._cbt_log_exists(parent_path): 630 ↛ 633line 630 didn't jump to line 633, because the condition on line 630 was never false

631 self._cbt_op(parent, cbtutil.set_cbt_child, 

632 parent_path, uuid.UUID(int=0)) 

633 if disk_state: 633 ↛ 638line 633 didn't jump to line 638, because the condition on line 633 was never false

634 self.update_slaves_on_cbt_disable(logpath) 

635 except Exception as error: 

636 raise xs_errors.XenError('CBTDeactivateFailed', str(error)) 

637 finally: 

638 lock.release() 

639 lock.cleanup("cbtlog", str(vdi_uuid)) 

640 finally: 

641 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid) 

642 

643 def data_destroy(self, sr_uuid, vdi_uuid): 

644 """Delete the data associated with a CBT enabled snapshot 

645 

646 Can only be called for a snapshot VDI on a VHD chain that has 

647 had CBT enabled on it at some point. The latter is enforced 

648 by upper layers 

649 """ 

650 

651 vdi_ref = self.sr.srcmd.params['vdi_ref'] 

652 if not self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref): 

653 raise xs_errors.XenError('VDIType', 

654 opterr='Only allowed for snapshot VDIs') 

655 

656 self.delete(sr_uuid, vdi_uuid, data_only=True) 

657 

658 def list_changed_blocks(self): 

659 """ List all changed blocks """ 

660 vdi_from = self.uuid 

661 params = self.sr.srcmd.params 

662 _VDI = self.session.xenapi.VDI 

663 vdi_to = _VDI.get_uuid(params['args'][0]) 

664 sr_uuid = params['sr_uuid'] 

665 

666 if vdi_from == vdi_to: 

667 raise xs_errors.XenError('CBTChangedBlocksError', 

668 "Source and target VDI are same") 

669 

670 # Check 1: Check if CBT is enabled on VDIs and they are related 

671 if (self._get_blocktracking_status(vdi_from) and 

672 self._get_blocktracking_status(vdi_to)): 

673 merged_bitmap = None 

674 curr_vdi = vdi_from 

675 vdi_size = 0 

676 logpath = self._get_cbt_logpath(curr_vdi) 

677 

678 # Starting at log file after "vdi_from", traverse the CBT chain 

679 # through child pointers until one of the following is true 

680 # * We've reached destination VDI 

681 # * We've reached end of CBT chain originating at "vdi_from" 

682 while True: 

683 # Check if we have reached end of CBT chain 

684 next_vdi = self._cbt_op(curr_vdi, cbtutil.get_cbt_child, 

685 logpath) 

686 if not self._cbt_log_exists(self._get_cbt_logpath(next_vdi)): 686 ↛ 688line 686 didn't jump to line 688, because the condition on line 686 was never true

687 # VDIs are not part of the same metadata chain 

688 break 

689 else: 

690 curr_vdi = next_vdi 

691 

692 logpath = self._get_cbt_logpath(curr_vdi) 

693 curr_vdi_size = self._cbt_op(curr_vdi, 

694 cbtutil.get_cbt_size, logpath) 

695 util.SMlog("DEBUG: Processing VDI %s of size %d" 

696 % (curr_vdi, curr_vdi_size)) 

697 curr_bitmap = bitarray() 

698 curr_bitmap.frombytes(self._cbt_op(curr_vdi, 

699 cbtutil.get_cbt_bitmap, 

700 logpath)) 

701 curr_bitmap.bytereverse() 

702 util.SMlog("Size of bitmap: %d" % len(curr_bitmap)) 

703 

704 expected_bitmap_len = curr_vdi_size // CBT_BLOCK_SIZE 

705 # This should ideally never happen but fail call to calculate 

706 # changed blocks instead of returning corrupt data 

707 if len(curr_bitmap) < expected_bitmap_len: 

708 util.SMlog("Size of bitmap %d is less than expected size %d" 

709 % (len(curr_bitmap), expected_bitmap_len)) 

710 raise xs_errors.XenError('CBTMetadataInconsistent', 

711 "Inconsistent bitmaps") 

712 

713 if merged_bitmap: 

714 # Rule out error conditions 

715 # 1) New VDI size < original VDI size 

716 # 2) New bitmap size < original bitmap size 

717 # 3) new VDI size > original VDI size but new bitmap 

718 # is not bigger 

719 if (curr_vdi_size < vdi_size or 

720 len(curr_bitmap) < len(merged_bitmap) or 

721 (curr_vdi_size > vdi_size and 

722 len(curr_bitmap) <= len(merged_bitmap))): 

723 # Return error: Failure to calculate changed blocks 

724 util.SMlog("Cannot calculate changed blocks with" 

725 "inconsistent bitmap sizes") 

726 raise xs_errors.XenError('CBTMetadataInconsistent', 

727 "Inconsistent bitmaps") 

728 

729 # Check if disk has been resized 

730 if curr_vdi_size > vdi_size: 

731 vdi_size = curr_vdi_size 

732 extended_size = len(curr_bitmap) - len(merged_bitmap) 

733 # Extend merged_bitmap to match size of curr_bitmap 

734 extended_bitmap = extended_size * bitarray('0') 

735 merged_bitmap += extended_bitmap 

736 

737 # At this point bitmap sizes should be same 

738 if (len(curr_bitmap) > len(merged_bitmap) and 

739 curr_vdi_size == vdi_size): 

740 # This is unusual. Log it but calculate merged 

741 # bitmap by truncating new bitmap 

742 util.SMlog("Bitmap for %s bigger than other bitmaps" 

743 "in chain without change in size" % curr_vdi) 

744 curr_bitmap = curr_bitmap[:len(merged_bitmap)] 

745 

746 merged_bitmap = merged_bitmap | curr_bitmap 

747 else: 

748 merged_bitmap = curr_bitmap 

749 vdi_size = curr_vdi_size 

750 

751 # Check if we have reached "vdi_to" 

752 if curr_vdi == vdi_to: 

753 encoded_string = base64.b64encode(merged_bitmap.tobytes()).decode() 

754 return xmlrpc.client.dumps((encoded_string, ), "", True) 

755 # TODO: Check 2: If both VDIs still exist, 

756 # find common ancestor and find difference 

757 

758 # TODO: VDIs are unrelated 

759 # return fully populated bitmap size of to VDI 

760 

761 raise xs_errors.XenError('CBTChangedBlocksError', 

762 "Source and target VDI are unrelated") 

763 

764 def _cbt_snapshot(self, snapshot_uuid, consistency_state): 

765 """ CBT snapshot""" 

766 snap_logpath = self._get_cbt_logpath(snapshot_uuid) 

767 vdi_logpath = self._get_cbt_logpath(self.uuid) 

768 

769 # Rename vdi vdi.cbtlog to snapshot.cbtlog 

770 # and mark it consistent 

771 self._rename(vdi_logpath, snap_logpath) 

772 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_consistency, 

773 snap_logpath, True) 

774 

775 #TODO: Make parent detection logic better. Ideally, get_cbt_parent 

776 # should return None if the parent is set to a UUID made of all 0s. 

777 # In this case, we don't know the difference between whether it is a 

778 # NULL UUID or the parent file is missing. See cbtutil for why we can't 

779 # do this 

780 parent = self._cbt_op(snapshot_uuid, 

781 cbtutil.get_cbt_parent, snap_logpath) 

782 parent_path = self._get_cbt_logpath(parent) 

783 if self._cbt_log_exists(parent_path): 

784 self._cbt_op(parent, cbtutil.set_cbt_child, 

785 parent_path, snapshot_uuid) 

786 try: 

787 # Ensure enough space for metadata file 

788 self._ensure_cbt_space() 

789 # Create new vdi.cbtlog 

790 self._create_cbt_log() 

791 # Set previous vdi node consistency status 

792 if not consistency_state: 792 ↛ 793line 792 didn't jump to line 793, because the condition on line 792 was never true

793 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, 

794 vdi_logpath, consistency_state) 

795 # Set relationship pointers 

796 # Save the child of the VDI just snapshotted 

797 curr_child_uuid = self._cbt_op(snapshot_uuid, cbtutil.get_cbt_child, 

798 snap_logpath) 

799 self._cbt_op(self.uuid, cbtutil.set_cbt_parent, 

800 vdi_logpath, snapshot_uuid) 

801 # Set child of new vdi to existing child of snapshotted VDI 

802 self._cbt_op(self.uuid, cbtutil.set_cbt_child, 

803 vdi_logpath, curr_child_uuid) 

804 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_child, 

805 snap_logpath, self.uuid) 

806 except Exception as ex: 

807 alert_name = "VDI_CBT_SNAPSHOT_FAILED" 

808 alert_str = ("Creating CBT metadata log for disk %s failed." 

809 % self.uuid) 

810 self._disable_cbt_on_error(alert_name, alert_str) 

811 

812 def _get_blocktracking_status(self, uuid=None): 

813 """ Get blocktracking status """ 

814 if not uuid: 814 ↛ 816line 814 didn't jump to line 816, because the condition on line 814 was never false

815 uuid = self.uuid 

816 if self.vdi_type == vhdutil.VDI_TYPE_RAW: 816 ↛ 817line 816 didn't jump to line 817, because the condition on line 816 was never true

817 return False 

818 elif 'VDI_CONFIG_CBT' not in util.sr_get_capability( 

819 self.sr.uuid, session=self.sr.session): 

820 return False 

821 logpath = self._get_cbt_logpath(uuid) 

822 return self._cbt_log_exists(logpath) 

823 

824 def _set_blocktracking_status(self, vdi_ref, enable): 

825 """ Set blocktracking status""" 

826 vdi_config = self.session.xenapi.VDI.get_other_config(vdi_ref) 

827 if "cbt_enabled" in vdi_config: 

828 self.session.xenapi.VDI.remove_from_other_config( 

829 vdi_ref, "cbt_enabled") 

830 

831 self.session.xenapi.VDI.add_to_other_config( 

832 vdi_ref, "cbt_enabled", enable) 

833 

834 def _ensure_cbt_space(self): 

835 """ Ensure enough CBT space """ 

836 pass 

837 

838 def _get_cbt_logname(self, uuid): 

839 """ Get CBT logname """ 

840 logName = "%s.%s" % (uuid, CBTLOG_TAG) 

841 return logName 

842 

843 def _get_cbt_logpath(self, uuid): 

844 """ Get CBT logpath """ 

845 logName = self._get_cbt_logname(uuid) 

846 return os.path.join(self.sr.path, logName) 

847 

848 def _create_cbt_log(self): 

849 """ Create CBT log """ 

850 try: 

851 logpath = self._get_cbt_logpath(self.uuid) 

852 vdi_ref = self.sr.srcmd.params['vdi_ref'] 

853 size = self.session.xenapi.VDI.get_virtual_size(vdi_ref) 

854 #cbtutil.create_cbt_log(logpath, size) 

855 self._cbt_op(self.uuid, cbtutil.create_cbt_log, logpath, size) 

856 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, logpath, True) 

857 except Exception as e: 

858 try: 

859 self._delete_cbt_log() 

860 except: 

861 pass 

862 finally: 

863 raise e 

864 

865 return logpath 

866 

867 def _activate_cbt_log(self, logname): 

868 """Activate CBT log file 

869 

870 SR specific Implementation required for VDIs on block-based SRs. 

871 No-op otherwise 

872 """ 

873 return False 

874 

875 def _deactivate_cbt_log(self, logname): 

876 """Deactivate CBT log file 

877 

878 SR specific Implementation required for VDIs on block-based SRs. 

879 No-op otherwise 

880 """ 

881 pass 

882 

883 def _cbt_op(self, uuid, func, *args): 

884 # Lock cbtlog operations 

885 lock = Lock("cbtlog", str(uuid)) 

886 lock.acquire() 

887 

888 try: 

889 logname = self._get_cbt_logname(uuid) 

890 activated = self._activate_cbt_log(logname) 

891 ret = func( * args) 

892 if activated: 892 ↛ 893line 892 didn't jump to line 893, because the condition on line 892 was never true

893 self._deactivate_cbt_log(logname) 

894 return ret 

895 finally: 

896 lock.release() 

897 

898 def _disable_cbt_on_error(self, alert_name, alert_str): 

899 util.SMlog(alert_str) 

900 self._delete_cbt_log() 

901 vdi_ref = self.sr.srcmd.params['vdi_ref'] 

902 self.sr.session.xenapi.VDI.set_cbt_enabled(vdi_ref, False) 

903 alert_prio_warning = "3" 

904 alert_obj = "VDI" 

905 alert_uuid = str(self.uuid) 

906 self.sr.session.xenapi.message.create(alert_name, 

907 alert_prio_warning, 

908 alert_obj, alert_uuid, 

909 alert_str) 

910 

911 def disable_leaf_on_secondary(self, vdi_uuid, secondary=None): 

912 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid) 

913 self.session.xenapi.VDI.remove_from_other_config( 

914 vdi_ref, cleanup.VDI.DB_LEAFCLSC) 

915 if secondary is not None: 

916 util.SMlog(f"We have secondary for {vdi_uuid}, " 

917 "blocking leaf coalesce") 

918 self.session.xenapi.VDI.add_to_other_config( 

919 vdi_ref, cleanup.VDI.DB_LEAFCLSC, 

920 cleanup.VDI.LEAFCLSC_DISABLED)