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
« 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#
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
34SM_CONFIG_PASS_THROUGH_FIELDS = ["base_mirror", "key_hash"]
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)
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
49 # BAT 4 Bytes per block segment
50 size += (size_mb // 2) * 4
51 size = util.roundup(512, size)
53 # BATMAP 1 bit per block segment
54 size += (size_mb // 2) // 8
55 size = util.roundup(4096, size)
57 # Segment bitmaps + Page align offsets
58 size += (size_mb // 2) * 4096
60 return size
63class VDI(object):
64 """Virtual Disk Instance descriptor.
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 """
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
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
121 self.load(uuid)
123 @staticmethod
124 def from_uuid(session, vdi_uuid):
126 _VDI = session.xenapi.VDI
127 vdi_ref = _VDI.get_by_uuid(vdi_uuid)
128 sr_ref = _VDI.get_SR(vdi_ref)
130 _SR = session.xenapi.SR
131 sr_uuid = _SR.get_uuid(sr_ref)
133 sr = SR.SR.from_uuid(session, sr_uuid)
135 sr.srcmd.params['vdi_ref'] = vdi_ref
136 return sr.vdi(vdi_uuid)
138 def create(self, sr_uuid, vdi_uuid, size):
139 """Create a VDI of size <Size> MB on the given SR.
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')
151 def update(self, sr_uuid, vdi_uuid):
152 """Query and update the configuration of a particular VDI.
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
161 def introduce(self, sr_uuid, vdi_uuid):
162 """Explicitly introduce a particular VDI.
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')
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.
174 This operation IS idempotent and should succeed if the VDI can be
175 attached or if the VDI is already attached.
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)
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.
188 This operation is idempotent.
189 """
190 raise xs_errors.XenError('Unimplemented')
192 def clone(self, sr_uuid, vdi_uuid):
193 """Create a mutable instance of the referenced VDI.
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.
202 Arguments:
203 Raises:
204 SRUnsupportedOperation
205 """
206 raise xs_errors.XenError('Unimplemented')
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')
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')
220 def compose(self, sr_uuid, vdi1, vdi2):
221 """Layer the updates from [vdi2] onto [vdi1], calling the result
222 [vdi2].
224 Raises:
225 SRUnsupportedOperation
226 """
227 raise xs_errors.XenError('Unimplemented')
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')
236 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
237 cloneOp=False, secondary=None, cbtlog=None):
238 raise xs_errors.XenError('Unimplemented')
240 def _delete_cbt_log(self):
241 raise xs_errors.XenError('Unimplemented')
243 def _rename(self, old, new):
244 raise xs_errors.XenError('Unimplemented')
246 def _cbt_log_exists(self, logpath):
247 """Check if CBT log file exists
249 Must be implemented by all classes inheriting from base VDI class
250 """
251 raise xs_errors.XenError('Unimplemented')
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.
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')
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.
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)
299 def delete(self, sr_uuid, vdi_uuid, data_only=False):
300 """Delete this VDI.
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
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)
319 lock = Lock("cbtlog", str(vdi_uuid))
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)
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))
374 def snapshot(self, sr_uuid, vdi_uuid):
375 """Save an immutable copy of the referenced VDI.
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
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
397 secondary = None
398 if self.sr.srcmd.params['driver_params'].get("mirror"):
399 secondary = self.sr.srcmd.params['driver_params']["mirror"]
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)
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
418 lock = Lock("cbtlog", str(vdi_uuid))
419 lock.acquire()
421 try:
422 logpath = self._get_cbt_logpath(vdi_uuid)
423 logname = self._get_cbt_logname(vdi_uuid)
425 # Activate CBT log file, if required
426 self._activate_cbt_log(logname)
427 finally:
428 lock.release()
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
440 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency,
441 logpath, False)
442 return {'cbtlog': logpath}
443 return None
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()
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()
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)
470 def load(self, vdi_uuid):
471 """Post-init hook"""
472 pass
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
491 def _db_forget(self):
492 self.sr.forget_vdi(self.uuid)
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]
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]
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)
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)
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())
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
575 def update_slaves_on_cbt_disable(self, cbtlog):
576 # Override in implementation as required.
577 pass
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']
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')
590 # Check if already enabled
591 if self._get_blocktracking_status() == enable:
592 return
594 # Save disk state before pause
595 disk_state = blktap2.VDI.tap_status(self.session, vdi_uuid)
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
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)
643 def data_destroy(self, sr_uuid, vdi_uuid):
644 """Delete the data associated with a CBT enabled snapshot
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 """
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')
656 self.delete(sr_uuid, vdi_uuid, data_only=True)
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']
666 if vdi_from == vdi_to:
667 raise xs_errors.XenError('CBTChangedBlocksError',
668 "Source and target VDI are same")
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)
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
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))
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")
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")
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
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)]
746 merged_bitmap = merged_bitmap | curr_bitmap
747 else:
748 merged_bitmap = curr_bitmap
749 vdi_size = curr_vdi_size
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
758 # TODO: VDIs are unrelated
759 # return fully populated bitmap size of to VDI
761 raise xs_errors.XenError('CBTChangedBlocksError',
762 "Source and target VDI are unrelated")
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)
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)
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)
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)
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")
831 self.session.xenapi.VDI.add_to_other_config(
832 vdi_ref, "cbt_enabled", enable)
834 def _ensure_cbt_space(self):
835 """ Ensure enough CBT space """
836 pass
838 def _get_cbt_logname(self, uuid):
839 """ Get CBT logname """
840 logName = "%s.%s" % (uuid, CBTLOG_TAG)
841 return logName
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)
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
865 return logpath
867 def _activate_cbt_log(self, logname):
868 """Activate CBT log file
870 SR specific Implementation required for VDIs on block-based SRs.
871 No-op otherwise
872 """
873 return False
875 def _deactivate_cbt_log(self, logname):
876 """Deactivate CBT log file
878 SR specific Implementation required for VDIs on block-based SRs.
879 No-op otherwise
880 """
881 pass
883 def _cbt_op(self, uuid, func, *args):
884 # Lock cbtlog operations
885 lock = Lock("cbtlog", str(uuid))
886 lock.acquire()
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()
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)
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)