Coverage for drivers/VDI.py : 71%

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# 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_typing import Dict, Optional
21import cleanup
22import SR
23import xmlrpc.client
24import xs_errors
25import util
26import vhdutil
27import cbtutil
28import os
29import base64
30from constants import CBTLOG_TAG
31from bitarray import bitarray
32import uuid
35SM_CONFIG_PASS_THROUGH_FIELDS = ["base_mirror", "key_hash"]
37SNAPSHOT_SINGLE = 1 # true snapshot: 1 leaf, 1 read-only parent
38SNAPSHOT_DOUBLE = 2 # regular snapshot/clone that creates 2 leaves
39SNAPSHOT_INTERNAL = 3 # SNAPSHOT_SINGLE but don't update SR's virtual allocation
40CBT_BLOCK_SIZE = (64 * 1024)
43def VDIMetadataSize(type, virtualsize):
44 size = 0
45 if type == 'vhd':
46 size_mb = virtualsize // (1024 * 1024)
47 #Footer + footer copy + header + possible CoW parent locator fields
48 size = 3 * 1024
50 # BAT 4 Bytes per block segment
51 size += (size_mb // 2) * 4
52 size = util.roundup(512, size)
54 # BATMAP 1 bit per block segment
55 size += (size_mb // 2) // 8
56 size = util.roundup(4096, size)
58 # Segment bitmaps + Page align offsets
59 size += (size_mb // 2) * 4096
61 return size
64class VDI(object):
65 """Virtual Disk Instance descriptor.
67 Attributes:
68 uuid: string, globally unique VDI identifier conforming to OSF DEC 1.1
69 label: string, user-generated tag string for identifyng the VDI
70 description: string, longer user generated description string
71 size: int, virtual size in bytes of this VDI
72 utilisation: int, actual size in Bytes of data on disk that is
73 utilised. For non-sparse disks, utilisation == size
74 vdi_type: string, disk type, e.g. raw file, partition
75 parent: VDI object, parent backing VDI if this disk is a
76 CoW instance
77 shareable: boolean, does this disk support multiple writer instances?
78 e.g. shared OCFS disk
79 attached: boolean, whether VDI is attached
80 read_only: boolean, whether disk is read-only.
81 """
83 def __init__(self, sr, uuid):
84 self.sr = sr
85 # Don't set either the UUID or location to None- no good can
86 # ever come of this.
87 if uuid is not None:
88 self.uuid = uuid
89 self.location = uuid
90 self.path = None
91 else:
92 # We assume that children class initializors calling without
93 # uuid will set these attributes themselves somewhere. They
94 # are VDIs whose physical paths/locations have no direct
95 # connections with their UUID strings (e.g. ISOSR, udevSR,
96 # SHMSR). So we avoid overwriting these attributes here.
97 pass
98 # deliberately not initialised self.sm_config so that it is
99 # ommitted from the XML output
101 self.label = ''
102 self.description = ''
103 self.vbds = []
104 self.size = 0
105 self.utilisation = 0
106 self.vdi_type = ''
107 self.has_child = 0
108 self.parent = None
109 self.shareable = False
110 self.attached = False
111 self.status = 0
112 self.read_only = False
113 self.xenstore_data = {}
114 self.deleted = False
115 self.session = sr.session
116 self.managed = True
117 self.sm_config_override = {}
118 self.sm_config_keep = ["key_hash"]
119 self.ty = "user"
120 self.cbt_enabled = False
122 self.load(uuid)
124 @staticmethod
125 def from_uuid(session, vdi_uuid):
127 _VDI = session.xenapi.VDI
128 vdi_ref = _VDI.get_by_uuid(vdi_uuid)
129 sr_ref = _VDI.get_SR(vdi_ref)
131 _SR = session.xenapi.SR
132 sr_uuid = _SR.get_uuid(sr_ref)
134 sr = SR.SR.from_uuid(session, sr_uuid)
136 sr.srcmd.params['vdi_ref'] = vdi_ref
137 return sr.vdi(vdi_uuid)
139 def create(self, sr_uuid, vdi_uuid, size) -> str:
140 """Create a VDI of size <Size> MB on the given SR.
142 This operation IS NOT idempotent and will fail if the UUID
143 already exists or if there is insufficient space. The vdi must
144 be explicitly attached via the attach() command following
145 creation. The actual disk size created may be larger than the
146 requested size if the substrate requires a size in multiples
147 of a certain extent size. The SR must be queried for the exact
148 size.
149 """
150 raise xs_errors.XenError('Unimplemented')
152 def update(self, sr_uuid, vdi_uuid) -> None:
153 """Query and update the configuration of a particular VDI.
155 Given an SR and VDI UUID, this operation returns summary statistics
156 on the named VDI. Note the XenAPI VDI object will exist when
157 this call is made.
158 """
159 # no-op unless individual backends implement it
160 return
162 def introduce(self, sr_uuid, vdi_uuid) -> str:
163 """Explicitly introduce a particular VDI.
165 Given an SR and VDI UUID and a disk location (passed in via the <conf>
166 XML), this operation verifies the existence of the underylying disk
167 object and then creates the XenAPI VDI object.
168 """
169 raise xs_errors.XenError('Unimplemented')
171 def attach(self, sr_uuid, vdi_uuid) -> str:
172 """Initiate local access to the VDI. Initialises any device
173 state required to access the VDI.
175 This operation IS idempotent and should succeed if the VDI can be
176 attached or if the VDI is already attached.
178 Returns:
179 string, local device path.
180 """
181 struct = {'params': self.path,
182 'xenstore_data': (self.xenstore_data or {})}
183 return xmlrpc.client.dumps((struct, ), "", True)
185 def detach(self, sr_uuid, vdi_uuid) -> None:
186 """Remove local access to the VDI. Destroys any device
187 state initialised via the vdi.attach() command.
189 This operation is idempotent.
190 """
191 raise xs_errors.XenError('Unimplemented')
193 def clone(self, sr_uuid, vdi_uuid) -> str:
194 """Create a mutable instance of the referenced VDI.
196 This operation is not idempotent and will fail if the UUID
197 already exists or if there is insufficient space. The SRC VDI
198 must be in a detached state and deactivated. Upon successful
199 creation of the clone, the clone VDI must be explicitly
200 attached via vdi.attach(). If the driver does not support
201 cloning this operation should raise SRUnsupportedOperation.
203 Arguments:
204 Raises:
205 SRUnsupportedOperation
206 """
207 raise xs_errors.XenError('Unimplemented')
209 def resize_online(self, sr_uuid, vdi_uuid, size):
210 """Resize the given VDI which may have active VBDs, which have
211 been paused for the duration of this call."""
212 raise xs_errors.XenError('Unimplemented')
214 def generate_config(self, sr_uuid, vdi_uuid) -> str:
215 """Generate the XML config required to activate a VDI for use
216 when XAPI is not running. Activation is handled by the
217 vdi_attach_from_config() SMAPI call.
218 """
219 raise xs_errors.XenError('Unimplemented')
221 def compose(self, sr_uuid, vdi1, vdi2) -> None:
222 """Layer the updates from [vdi2] onto [vdi1], calling the result
223 [vdi2].
225 Raises:
226 SRUnsupportedOperation
227 """
228 raise xs_errors.XenError('Unimplemented')
230 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
231 """Activate a VDI based on the config passed in on the CLI. For
232 use when XAPI is not running. The config is generated by the
233 Activation is handled by the vdi_generate_config() SMAPI call.
234 """
235 raise xs_errors.XenError('Unimplemented')
237 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
238 cloneOp=False, secondary=None, cbtlog=None) -> str:
239 raise xs_errors.XenError('Unimplemented')
241 def _delete_cbt_log(self) -> None:
242 raise xs_errors.XenError('Unimplemented')
244 def _rename(self, old, new) -> None:
245 raise xs_errors.XenError('Unimplemented')
247 def _cbt_log_exists(self, logpath) -> bool:
248 """Check if CBT log file exists
250 Must be implemented by all classes inheriting from base VDI class
251 """
252 raise xs_errors.XenError('Unimplemented')
254 def resize(self, sr_uuid, vdi_uuid, size) -> str:
255 """Resize the given VDI to size <size> MB. Size can
256 be any valid disk size greater than [or smaller than]
257 the current value.
259 This operation IS idempotent and should succeed if the VDI can
260 be resized to the specified value or if the VDI is already the
261 specified size. The actual disk size created may be larger
262 than the requested size if the substrate requires a size in
263 multiples of a certain extent size. The SR must be queried for
264 the exact size. This operation does not modify the contents on
265 the disk such as the filesystem. Responsibility for resizing
266 the FS is left to the VM administrator. [Reducing the size of
267 the disk is a very dangerous operation and should be conducted
268 very carefully.] Disk contents should always be backed up in
269 advance.
270 """
271 raise xs_errors.XenError('Unimplemented')
273 def resize_cbt(self, sr_uuid, vdi_uuid, size):
274 """Resize the given VDI to size <size> MB. Size can
275 be any valid disk size greater than [or smaller than]
276 the current value.
278 This operation IS idempotent and should succeed if the VDI can
279 be resized to the specified value or if the VDI is already the
280 specified size. The actual disk size created may be larger
281 than the requested size if the substrate requires a size in
282 multiples of a certain extent size. The SR must be queried for
283 the exact size. This operation does not modify the contents on
284 the disk such as the filesystem. Responsibility for resizing
285 the FS is left to the VM administrator. [Reducing the size of
286 the disk is a very dangerous operation and should be conducted
287 very carefully.] Disk contents should always be backed up in
288 advance.
289 """
290 try:
291 if self._get_blocktracking_status():
292 logpath = self._get_cbt_logpath(vdi_uuid)
293 self._cbt_op(vdi_uuid, cbtutil.set_cbt_size, logpath, size)
294 except util.CommandException as ex:
295 alert_name = "VDI_CBT_RESIZE_FAILED"
296 alert_str = ("Resizing of CBT metadata for disk %s failed."
297 % vdi_uuid)
298 self._disable_cbt_on_error(alert_name, alert_str)
300 def delete(self, sr_uuid, vdi_uuid, data_only=False) -> None:
301 """Delete this VDI.
303 This operation IS idempotent and should succeed if the VDI
304 exists and can be deleted or if the VDI does not exist. It is
305 the responsibility of the higher-level management tool to
306 ensure that the detach() operation has been explicitly called
307 prior to deletion, otherwise the delete() will fail if the
308 disk is still attached.
309 """
310 import blktap2
311 from lock import Lock
313 if data_only == False and self._get_blocktracking_status():
314 logpath = self._get_cbt_logpath(vdi_uuid)
315 parent_uuid = self._cbt_op(vdi_uuid, cbtutil.get_cbt_parent,
316 logpath)
317 parent_path = self._get_cbt_logpath(parent_uuid)
318 child_uuid = self._cbt_op(vdi_uuid, cbtutil.get_cbt_child, logpath)
319 child_path = self._get_cbt_logpath(child_uuid)
321 lock = Lock("cbtlog", str(vdi_uuid))
323 if self._cbt_log_exists(parent_path): 323 ↛ 327line 323 didn't jump to line 327, because the condition on line 323 was never false
324 self._cbt_op(parent_uuid, cbtutil.set_cbt_child,
325 parent_path, child_uuid)
327 if self._cbt_log_exists(child_path):
328 self._cbt_op(child_uuid, cbtutil.set_cbt_parent,
329 child_path, parent_uuid)
330 lock.acquire()
331 paused_for_coalesce = False
332 try:
333 # Coalesce contents of bitmap with child's bitmap
334 # Check if child bitmap is currently attached
335 consistent = self._cbt_op(child_uuid,
336 cbtutil.get_cbt_consistency,
337 child_path)
338 if not consistent:
339 if not blktap2.VDI.tap_pause(self.session, 339 ↛ 341line 339 didn't jump to line 341, because the condition on line 339 was never true
340 sr_uuid, child_uuid):
341 raise util.SMException("failed to pause VDI %s")
342 paused_for_coalesce = True
343 self._activate_cbt_log(self._get_cbt_logname(vdi_uuid))
344 self._cbt_op(child_uuid, cbtutil.coalesce_bitmap,
345 logpath, child_path)
346 lock.release()
347 except util.CommandException:
348 # If there is an exception in coalescing,
349 # CBT log file is not deleted and pointers are reset
350 # to what they were
351 util.SMlog("Exception in coalescing bitmaps on VDI delete,"
352 " restoring to previous state")
353 try:
354 if self._cbt_log_exists(parent_path): 354 ↛ 357line 354 didn't jump to line 357, because the condition on line 354 was never false
355 self._cbt_op(parent_uuid, cbtutil.set_cbt_child,
356 parent_path, vdi_uuid)
357 if self._cbt_log_exists(child_path): 357 ↛ 361line 357 didn't jump to line 361, because the condition on line 357 was never false
358 self._cbt_op(child_uuid, cbtutil.set_cbt_parent,
359 child_path, vdi_uuid)
360 finally:
361 lock.release()
362 lock.cleanup("cbtlog", str(vdi_uuid))
363 return
364 finally:
365 # Unpause tapdisk if it wasn't originally paused
366 if paused_for_coalesce: 366 ↛ 369line 366 didn't jump to line 369, because the condition on line 366 was never false
367 blktap2.VDI.tap_unpause(self.session, sr_uuid, 367 ↛ exitline 367 didn't return from function 'delete', because the return on line 363 wasn't executed
368 child_uuid)
369 lock.acquire()
370 try:
371 self._delete_cbt_log()
372 finally:
373 lock.release()
374 lock.cleanup("cbtlog", str(vdi_uuid))
376 def snapshot(self, sr_uuid, vdi_uuid) -> str:
377 """Save an immutable copy of the referenced VDI.
379 This operation IS NOT idempotent and will fail if the UUID
380 already exists or if there is insufficient space. The vdi must
381 be explicitly attached via the vdi_attach() command following
382 creation. If the driver does not support snapshotting this
383 operation should raise SRUnsupportedOperation
385 Arguments:
386 Raises:
387 SRUnsupportedOperation
388 """
389 # logically, "snapshot" should mean SNAPSHOT_SINGLE and "clone" should
390 # mean "SNAPSHOT_DOUBLE", but in practice we have to do SNAPSHOT_DOUBLE
391 # in both cases, unless driver_params overrides it
392 snapType = SNAPSHOT_DOUBLE
393 if self.sr.srcmd.params['driver_params'].get("type"): 393 ↛ 399line 393 didn't jump to line 399, because the condition on line 393 was never false
394 if self.sr.srcmd.params['driver_params']["type"] == "single": 394 ↛ 395line 394 didn't jump to line 395, because the condition on line 394 was never true
395 snapType = SNAPSHOT_SINGLE
396 elif self.sr.srcmd.params['driver_params']["type"] == "internal": 396 ↛ 397line 396 didn't jump to line 397, because the condition on line 396 was never true
397 snapType = SNAPSHOT_INTERNAL
399 secondary = None
400 if self.sr.srcmd.params['driver_params'].get("mirror"):
401 secondary = self.sr.srcmd.params['driver_params']["mirror"]
403 if self._get_blocktracking_status():
404 cbtlog = self._get_cbt_logpath(self.uuid)
405 else:
406 cbtlog = None
407 return self._do_snapshot(sr_uuid, vdi_uuid, snapType,
408 secondary=secondary, cbtlog=cbtlog)
410 def activate(self, sr_uuid, vdi_uuid) -> Optional[Dict[str, str]]:
411 """Activate VDI - called pre tapdisk open"""
412 if self._get_blocktracking_status():
413 if 'args' in self.sr.srcmd.params: 413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true
414 read_write = self.sr.srcmd.params['args'][0]
415 if read_write == "false":
416 # Disk is being attached in RO mode,
417 # don't attach metadata log file
418 return None
420 from lock import Lock
421 lock = Lock("cbtlog", str(vdi_uuid))
422 lock.acquire()
424 try:
425 logpath = self._get_cbt_logpath(vdi_uuid)
426 logname = self._get_cbt_logname(vdi_uuid)
428 # Activate CBT log file, if required
429 self._activate_cbt_log(logname)
430 finally:
431 lock.release()
433 # Check and update consistency
434 consistent = self._cbt_op(vdi_uuid, cbtutil.get_cbt_consistency,
435 logpath)
436 if not consistent:
437 alert_name = "VDI_CBT_METADATA_INCONSISTENT"
438 alert_str = ("Changed Block Tracking metadata is inconsistent"
439 " for disk %s." % vdi_uuid)
440 self._disable_cbt_on_error(alert_name, alert_str)
441 return None
443 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency,
444 logpath, False)
445 return {'cbtlog': logpath}
446 return None
448 def deactivate(self, sr_uuid, vdi_uuid) -> None:
449 """Deactivate VDI - called post tapdisk close"""
450 if self._get_blocktracking_status():
451 from lock import Lock
452 lock = Lock("cbtlog", str(vdi_uuid))
453 lock.acquire()
455 try:
456 logpath = self._get_cbt_logpath(vdi_uuid)
457 logname = self._get_cbt_logname(vdi_uuid)
458 self._cbt_op(vdi_uuid, cbtutil.set_cbt_consistency, logpath, True)
459 # Finally deactivate log file
460 self._deactivate_cbt_log(logname)
461 finally:
462 lock.release()
464 def get_params(self) -> str:
465 """
466 Returns:
467 XMLRPC response containing a single struct with fields
468 'location' and 'uuid'
469 """
470 struct = {'location': self.location,
471 'uuid': self.uuid}
472 return xmlrpc.client.dumps((struct, ), "", True)
474 def load(self, vdi_uuid) -> None:
475 """Post-init hook"""
476 pass
478 def _db_introduce(self):
479 uuid = util.default(self, "uuid", lambda: util.gen_uuid()) 479 ↛ exitline 479 didn't run the lambda on line 479
480 sm_config = util.default(self, "sm_config", lambda: {})
481 if "vdi_sm_config" in self.sr.srcmd.params: 481 ↛ 482line 481 didn't jump to line 482, because the condition on line 481 was never true
482 for key in SM_CONFIG_PASS_THROUGH_FIELDS:
483 val = self.sr.srcmd.params["vdi_sm_config"].get(key)
484 if val:
485 sm_config[key] = val
486 ty = util.default(self, "ty", lambda: "user") 486 ↛ exitline 486 didn't run the lambda on line 486
487 is_a_snapshot = util.default(self, "is_a_snapshot", lambda: False)
488 metadata_of_pool = util.default(self, "metadata_of_pool", lambda: "OpaqueRef:NULL")
489 snapshot_time = util.default(self, "snapshot_time", lambda: "19700101T00:00:00Z")
490 snapshot_of = util.default(self, "snapshot_of", lambda: "OpaqueRef:NULL")
491 cbt_enabled = util.default(self, "cbt_enabled", lambda: False) 491 ↛ exitline 491 didn't run the lambda on line 491
492 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)
493 return vdi
495 def _db_forget(self):
496 self.sr.forget_vdi(self.uuid)
498 def _override_sm_config(self, sm_config):
499 for key, val in self.sm_config_override.items():
500 if val == sm_config.get(key):
501 continue
502 if val: 502 ↛ 506line 502 didn't jump to line 506, because the condition on line 502 was never false
503 util.SMlog("_override_sm_config: %s: %s -> %s" % \
504 (key, sm_config.get(key), val))
505 sm_config[key] = val
506 elif key in sm_config:
507 util.SMlog("_override_sm_config: del %s" % key)
508 del sm_config[key]
510 def _db_update_sm_config(self, ref, sm_config):
511 import cleanup
512 # List of sm-config keys that should not be modifed by db_update
513 smconfig_protected_keys = [
514 cleanup.VDI.DB_VDI_PAUSED,
515 cleanup.VDI.DB_VHD_BLOCKS,
516 cleanup.VDI.DB_VDI_RELINKING,
517 cleanup.VDI.DB_VDI_ACTIVATING]
519 current_sm_config = self.sr.session.xenapi.VDI.get_sm_config(ref)
520 for key, val in sm_config.items():
521 if (key.startswith("host_") or
522 key in smconfig_protected_keys):
523 continue
524 if sm_config.get(key) != current_sm_config.get(key):
525 util.SMlog("_db_update_sm_config: %s sm-config:%s %s->%s" % \
526 (self.uuid, key, current_sm_config.get(key), val))
527 self.sr.session.xenapi.VDI.remove_from_sm_config(ref, key)
528 self.sr.session.xenapi.VDI.add_to_sm_config(ref, key, val)
530 for key in current_sm_config.keys():
531 if (key.startswith("host_") or
532 key in smconfig_protected_keys or
533 key in self.sm_config_keep):
534 continue
535 if not sm_config.get(key):
536 util.SMlog("_db_update_sm_config: %s del sm-config:%s" % \
537 (self.uuid, key))
538 self.sr.session.xenapi.VDI.remove_from_sm_config(ref, key)
540 def _db_update(self):
541 vdi = self.sr.session.xenapi.VDI.get_by_uuid(self.uuid)
542 self.sr.session.xenapi.VDI.set_virtual_size(vdi, str(self.size))
543 self.sr.session.xenapi.VDI.set_physical_utilisation(vdi, str(self.utilisation))
544 self.sr.session.xenapi.VDI.set_read_only(vdi, self.read_only)
545 sm_config = util.default(self, "sm_config", lambda: {})
546 self._override_sm_config(sm_config)
547 self._db_update_sm_config(vdi, sm_config)
548 self.sr.session.xenapi.VDI.set_cbt_enabled(vdi,
549 self._get_blocktracking_status())
551 def in_sync_with_xenapi_record(self, x):
552 """Returns true if this VDI is in sync with the supplied XenAPI record"""
553 if self.location != util.to_plain_string(x['location']):
554 util.SMlog("location %s <> %s" % (self.location, x['location']))
555 return False
556 if self.read_only != x['read_only']:
557 util.SMlog("read_only %s <> %s" % (self.read_only, x['read_only']))
558 return False
559 if str(self.size) != x['virtual_size']:
560 util.SMlog("virtual_size %s <> %s" % (self.size, x['virtual_size']))
561 return False
562 if str(self.utilisation) != x['physical_utilisation']:
563 util.SMlog("utilisation %s <> %s" % (self.utilisation, x['physical_utilisation']))
564 return False
565 sm_config = util.default(self, "sm_config", lambda: {})
566 if set(sm_config.keys()) != set(x['sm_config'].keys()):
567 util.SMlog("sm_config %s <> %s" % (repr(sm_config), repr(x['sm_config'])))
568 return False
569 for k in sm_config.keys():
570 if sm_config[k] != x['sm_config'][k]:
571 util.SMlog("sm_config %s <> %s" % (repr(sm_config), repr(x['sm_config'])))
572 return False
573 if self.cbt_enabled != x['cbt_enabled']:
574 util.SMlog("cbt_enabled %s <> %s" % (
575 self.cbt_enabled, x['cbt_enabled']))
576 return False
577 return True
579 def configure_blocktracking(self, sr_uuid, vdi_uuid, enable):
580 """Function for configuring blocktracking"""
581 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 ↛ 640line 609 didn't jump to line 640, 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 from lock import Lock
621 lock = Lock("cbtlog", str(vdi_uuid))
622 lock.acquire()
623 try:
624 # Find parent of leaf metadata file, if any,
625 # and nullify its successor
626 logpath = self._get_cbt_logpath(self.uuid)
627 parent = self._cbt_op(self.uuid,
628 cbtutil.get_cbt_parent, logpath)
629 self._delete_cbt_log()
630 parent_path = self._get_cbt_logpath(parent)
631 if self._cbt_log_exists(parent_path): 631 ↛ 637line 631 didn't jump to line 637, because the condition on line 631 was never false
632 self._cbt_op(parent, cbtutil.set_cbt_child,
633 parent_path, uuid.UUID(int=0))
634 except Exception as error:
635 raise xs_errors.XenError('CBTDeactivateFailed', str(error))
636 finally:
637 lock.release()
638 lock.cleanup("cbtlog", str(vdi_uuid))
639 finally:
640 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid)
642 def data_destroy(self, sr_uuid, vdi_uuid):
643 """Delete the data associated with a CBT enabled snapshot
645 Can only be called for a snapshot VDI on a VHD chain that has
646 had CBT enabled on it at some point. The latter is enforced
647 by upper layers
648 """
650 vdi_ref = self.sr.srcmd.params['vdi_ref']
651 if not self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
652 raise xs_errors.XenError('VDIType',
653 opterr='Only allowed for snapshot VDIs')
655 self.delete(sr_uuid, vdi_uuid, data_only=True)
657 def list_changed_blocks(self):
658 """ List all changed blocks """
659 vdi_from = self.uuid
660 params = self.sr.srcmd.params
661 _VDI = self.session.xenapi.VDI
662 vdi_to = _VDI.get_uuid(params['args'][0])
663 sr_uuid = params['sr_uuid']
665 if vdi_from == vdi_to:
666 raise xs_errors.XenError('CBTChangedBlocksError',
667 "Source and target VDI are same")
669 # Check 1: Check if CBT is enabled on VDIs and they are related
670 if (self._get_blocktracking_status(vdi_from) and
671 self._get_blocktracking_status(vdi_to)):
672 merged_bitmap = None
673 curr_vdi = vdi_from
674 vdi_size = 0
675 logpath = self._get_cbt_logpath(curr_vdi)
677 # Starting at log file after "vdi_from", traverse the CBT chain
678 # through child pointers until one of the following is true
679 # * We've reached destination VDI
680 # * We've reached end of CBT chain originating at "vdi_from"
681 while True:
682 # Check if we have reached end of CBT chain
683 next_vdi = self._cbt_op(curr_vdi, cbtutil.get_cbt_child,
684 logpath)
685 if not self._cbt_log_exists(self._get_cbt_logpath(next_vdi)): 685 ↛ 687line 685 didn't jump to line 687, because the condition on line 685 was never true
686 # VDIs are not part of the same metadata chain
687 break
688 else:
689 curr_vdi = next_vdi
691 logpath = self._get_cbt_logpath(curr_vdi)
692 curr_vdi_size = self._cbt_op(curr_vdi,
693 cbtutil.get_cbt_size, logpath)
694 util.SMlog("DEBUG: Processing VDI %s of size %d"
695 % (curr_vdi, curr_vdi_size))
696 curr_bitmap = bitarray()
697 curr_bitmap.frombytes(self._cbt_op(curr_vdi,
698 cbtutil.get_cbt_bitmap,
699 logpath))
700 curr_bitmap.bytereverse()
701 util.SMlog("Size of bitmap: %d" % len(curr_bitmap))
703 expected_bitmap_len = curr_vdi_size // CBT_BLOCK_SIZE
704 # This should ideally never happen but fail call to calculate
705 # changed blocks instead of returning corrupt data
706 if len(curr_bitmap) < expected_bitmap_len:
707 util.SMlog("Size of bitmap %d is less than expected size %d"
708 % (len(curr_bitmap), expected_bitmap_len))
709 raise xs_errors.XenError('CBTMetadataInconsistent',
710 "Inconsistent bitmaps")
712 if merged_bitmap:
713 # Rule out error conditions
714 # 1) New VDI size < original VDI size
715 # 2) New bitmap size < original bitmap size
716 # 3) new VDI size > original VDI size but new bitmap
717 # is not bigger
718 if (curr_vdi_size < vdi_size or
719 len(curr_bitmap) < len(merged_bitmap) or
720 (curr_vdi_size > vdi_size and
721 len(curr_bitmap) <= len(merged_bitmap))):
722 # Return error: Failure to calculate changed blocks
723 util.SMlog("Cannot calculate changed blocks with"
724 "inconsistent bitmap sizes")
725 raise xs_errors.XenError('CBTMetadataInconsistent',
726 "Inconsistent bitmaps")
728 # Check if disk has been resized
729 if curr_vdi_size > vdi_size:
730 vdi_size = curr_vdi_size
731 extended_size = len(curr_bitmap) - len(merged_bitmap)
732 # Extend merged_bitmap to match size of curr_bitmap
733 extended_bitmap = extended_size * bitarray('0')
734 merged_bitmap += extended_bitmap
736 # At this point bitmap sizes should be same
737 if (len(curr_bitmap) > len(merged_bitmap) and
738 curr_vdi_size == vdi_size):
739 # This is unusual. Log it but calculate merged
740 # bitmap by truncating new bitmap
741 util.SMlog("Bitmap for %s bigger than other bitmaps"
742 "in chain without change in size" % curr_vdi)
743 curr_bitmap = curr_bitmap[:len(merged_bitmap)]
745 merged_bitmap = merged_bitmap | curr_bitmap
746 else:
747 merged_bitmap = curr_bitmap
748 vdi_size = curr_vdi_size
750 # Check if we have reached "vdi_to"
751 if curr_vdi == vdi_to:
752 encoded_string = base64.b64encode(merged_bitmap.tobytes()).decode()
753 return xmlrpc.client.dumps((encoded_string, ), "", True)
754 # TODO: Check 2: If both VDIs still exist,
755 # find common ancestor and find difference
757 # TODO: VDIs are unrelated
758 # return fully populated bitmap size of to VDI
760 raise xs_errors.XenError('CBTChangedBlocksError',
761 "Source and target VDI are unrelated")
763 def _cbt_snapshot(self, snapshot_uuid, consistency_state):
764 """ CBT snapshot"""
765 snap_logpath = self._get_cbt_logpath(snapshot_uuid)
766 vdi_logpath = self._get_cbt_logpath(self.uuid)
768 # Rename vdi vdi.cbtlog to snapshot.cbtlog
769 # and mark it consistent
770 self._rename(vdi_logpath, snap_logpath)
771 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_consistency,
772 snap_logpath, True)
774 #TODO: Make parent detection logic better. Ideally, get_cbt_parent
775 # should return None if the parent is set to a UUID made of all 0s.
776 # In this case, we don't know the difference between whether it is a
777 # NULL UUID or the parent file is missing. See cbtutil for why we can't
778 # do this
779 parent = self._cbt_op(snapshot_uuid,
780 cbtutil.get_cbt_parent, snap_logpath)
781 parent_path = self._get_cbt_logpath(parent)
782 if self._cbt_log_exists(parent_path):
783 self._cbt_op(parent, cbtutil.set_cbt_child,
784 parent_path, snapshot_uuid)
785 try:
786 # Ensure enough space for metadata file
787 self._ensure_cbt_space()
788 # Create new vdi.cbtlog
789 self._create_cbt_log()
790 # Set previous vdi node consistency status
791 if not consistency_state: 791 ↛ 792line 791 didn't jump to line 792, because the condition on line 791 was never true
792 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency,
793 vdi_logpath, consistency_state)
794 # Set relationship pointers
795 # Save the child of the VDI just snapshotted
796 curr_child_uuid = self._cbt_op(snapshot_uuid, cbtutil.get_cbt_child,
797 snap_logpath)
798 self._cbt_op(self.uuid, cbtutil.set_cbt_parent,
799 vdi_logpath, snapshot_uuid)
800 # Set child of new vdi to existing child of snapshotted VDI
801 self._cbt_op(self.uuid, cbtutil.set_cbt_child,
802 vdi_logpath, curr_child_uuid)
803 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_child,
804 snap_logpath, self.uuid)
805 except Exception as ex:
806 alert_name = "VDI_CBT_SNAPSHOT_FAILED"
807 alert_str = ("Creating CBT metadata log for disk %s failed."
808 % self.uuid)
809 self._disable_cbt_on_error(alert_name, alert_str)
811 def _get_blocktracking_status(self, uuid=None) -> bool:
812 """ Get blocktracking status """
813 if not uuid: 813 ↛ 815line 813 didn't jump to line 815, because the condition on line 813 was never false
814 uuid = self.uuid
815 if self.vdi_type == vhdutil.VDI_TYPE_RAW: 815 ↛ 816line 815 didn't jump to line 816, because the condition on line 815 was never true
816 return False
817 elif 'VDI_CONFIG_CBT' not in util.sr_get_capability(
818 self.sr.uuid, session=self.sr.session):
819 return False
820 logpath = self._get_cbt_logpath(uuid)
821 return self._cbt_log_exists(logpath)
823 def _set_blocktracking_status(self, vdi_ref, enable):
824 """ Set blocktracking status"""
825 vdi_config = self.session.xenapi.VDI.get_other_config(vdi_ref)
826 if "cbt_enabled" in vdi_config:
827 self.session.xenapi.VDI.remove_from_other_config(
828 vdi_ref, "cbt_enabled")
830 self.session.xenapi.VDI.add_to_other_config(
831 vdi_ref, "cbt_enabled", enable)
833 def _ensure_cbt_space(self) -> None:
834 """ Ensure enough CBT space """
835 pass
837 def _get_cbt_logname(self, uuid):
838 """ Get CBT logname """
839 logName = "%s.%s" % (uuid, CBTLOG_TAG)
840 return logName
842 def _get_cbt_logpath(self, uuid) -> str:
843 """ Get CBT logpath """
844 logName = self._get_cbt_logname(uuid)
845 return os.path.join(self.sr.path, logName)
847 def _create_cbt_log(self) -> str:
848 """ Create CBT log """
849 try:
850 logpath = self._get_cbt_logpath(self.uuid)
851 vdi_ref = self.sr.srcmd.params['vdi_ref']
852 size = self.session.xenapi.VDI.get_virtual_size(vdi_ref)
853 #cbtutil.create_cbt_log(logpath, size)
854 self._cbt_op(self.uuid, cbtutil.create_cbt_log, logpath, size)
855 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, logpath, True)
856 except Exception as e:
857 try:
858 self._delete_cbt_log()
859 except:
860 pass
861 finally:
862 raise e
864 return logpath
866 def _activate_cbt_log(self, logname) -> bool:
867 """Activate CBT log file
869 SR specific Implementation required for VDIs on block-based SRs.
870 No-op otherwise
871 """
872 return False
874 def _deactivate_cbt_log(self, logname) -> None:
875 """Deactivate CBT log file
877 SR specific Implementation required for VDIs on block-based SRs.
878 No-op otherwise
879 """
880 pass
882 def _cbt_op(self, uuid, func, *args):
883 # Lock cbtlog operations
884 from lock import Lock
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:
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)