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