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 update_slaves_on_cbt_disable(self, cbtlog):
562 # Override in implementation as required.
563 pass
565 def configure_blocktracking(self, sr_uuid, vdi_uuid, enable):
566 """Function for configuring blocktracking"""
567 import blktap2
568 vdi_ref = self.sr.srcmd.params['vdi_ref']
570 # Check if raw VDI or snapshot
571 if not VdiType.isCowImage(self.vdi_type) or \
572 self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
573 raise xs_errors.XenError('VDIType',
574 opterr='Raw VDI or snapshot not permitted')
576 # Check if already enabled
577 if self._get_blocktracking_status() == enable:
578 return
580 # Save disk state before pause
581 disk_state = blktap2.VDI.tap_status(self.session, vdi_uuid)
583 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid):
584 error = "Failed to pause VDI %s" % vdi_uuid
585 raise xs_errors.XenError('CBTActivateFailed', opterr=error)
586 logfile = None
588 try:
589 if enable:
590 try:
591 # Check available space
592 self._ensure_cbt_space()
593 logfile = self._create_cbt_log()
594 # Set consistency
595 if disk_state: 595 ↛ 628line 595 didn't jump to line 628, because the condition on line 595 was never false
596 util.SMlog("Setting consistency of cbtlog file to False for VDI: %s"
597 % self.uuid)
598 logpath = self._get_cbt_logpath(self.uuid)
599 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency,
600 logpath, False)
601 except Exception as error:
602 self._delete_cbt_log()
603 raise xs_errors.XenError('CBTActivateFailed',
604 opterr=str(error))
605 else:
606 from lock import Lock
607 lock = Lock("cbtlog", str(vdi_uuid))
608 lock.acquire()
609 try:
610 # Find parent of leaf metadata file, if any,
611 # and nullify its successor
612 logpath = self._get_cbt_logpath(self.uuid)
613 parent = self._cbt_op(self.uuid,
614 cbtutil.get_cbt_parent, logpath)
615 self._delete_cbt_log()
616 parent_path = self._get_cbt_logpath(parent)
617 if self._cbt_log_exists(parent_path): 617 ↛ 620line 617 didn't jump to line 620, because the condition on line 617 was never false
618 self._cbt_op(parent, cbtutil.set_cbt_child,
619 parent_path, uuid.UUID(int=0))
620 if disk_state: 620 ↛ 625line 620 didn't jump to line 625, because the condition on line 620 was never false
621 self.update_slaves_on_cbt_disable(logpath)
622 except Exception as error:
623 raise xs_errors.XenError('CBTDeactivateFailed', str(error))
624 finally:
625 lock.release()
626 lock.cleanup("cbtlog", str(vdi_uuid))
627 finally:
628 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid)
630 def data_destroy(self, sr_uuid, vdi_uuid):
631 """Delete the data associated with a CBT enabled snapshot
633 Can only be called for a snapshot VDI on a COW chain that has
634 had CBT enabled on it at some point. The latter is enforced
635 by upper layers
636 """
638 vdi_ref = self.sr.srcmd.params['vdi_ref']
639 if not self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
640 raise xs_errors.XenError('VDIType',
641 opterr='Only allowed for snapshot VDIs')
643 self.delete(sr_uuid, vdi_uuid, data_only=True)
645 def list_changed_blocks(self):
646 """ List all changed blocks """
647 vdi_from = self.uuid
648 params = self.sr.srcmd.params
649 _VDI = self.session.xenapi.VDI
650 vdi_to = _VDI.get_uuid(params['args'][0])
651 sr_uuid = params['sr_uuid']
653 if vdi_from == vdi_to:
654 raise xs_errors.XenError('CBTChangedBlocksError',
655 "Source and target VDI are same")
657 # Check 1: Check if CBT is enabled on VDIs and they are related
658 if (self._get_blocktracking_status(vdi_from) and
659 self._get_blocktracking_status(vdi_to)):
660 merged_bitmap = None
661 curr_vdi = vdi_from
662 vdi_size = 0
663 logpath = self._get_cbt_logpath(curr_vdi)
665 # Starting at log file after "vdi_from", traverse the CBT chain
666 # through child pointers until one of the following is true
667 # * We've reached destination VDI
668 # * We've reached end of CBT chain originating at "vdi_from"
669 while True:
670 # Check if we have reached end of CBT chain
671 next_vdi = self._cbt_op(curr_vdi, cbtutil.get_cbt_child,
672 logpath)
673 if not self._cbt_log_exists(self._get_cbt_logpath(next_vdi)): 673 ↛ 675line 673 didn't jump to line 675, because the condition on line 673 was never true
674 # VDIs are not part of the same metadata chain
675 break
676 else:
677 curr_vdi = next_vdi
679 logpath = self._get_cbt_logpath(curr_vdi)
680 curr_vdi_size = self._cbt_op(curr_vdi,
681 cbtutil.get_cbt_size, logpath)
682 util.SMlog("DEBUG: Processing VDI %s of size %d"
683 % (curr_vdi, curr_vdi_size))
684 curr_bitmap = bitarray()
685 curr_bitmap.frombytes(self._cbt_op(curr_vdi,
686 cbtutil.get_cbt_bitmap,
687 logpath))
688 curr_bitmap.bytereverse()
689 util.SMlog("Size of bitmap: %d" % len(curr_bitmap))
691 expected_bitmap_len = curr_vdi_size // CBT_BLOCK_SIZE
692 # This should ideally never happen but fail call to calculate
693 # changed blocks instead of returning corrupt data
694 if len(curr_bitmap) < expected_bitmap_len:
695 util.SMlog("Size of bitmap %d is less than expected size %d"
696 % (len(curr_bitmap), expected_bitmap_len))
697 raise xs_errors.XenError('CBTMetadataInconsistent',
698 "Inconsistent bitmaps")
700 if merged_bitmap:
701 # Rule out error conditions
702 # 1) New VDI size < original VDI size
703 # 2) New bitmap size < original bitmap size
704 # 3) new VDI size > original VDI size but new bitmap
705 # is not bigger
706 if (curr_vdi_size < vdi_size or
707 len(curr_bitmap) < len(merged_bitmap) or
708 (curr_vdi_size > vdi_size and
709 len(curr_bitmap) <= len(merged_bitmap))):
710 # Return error: Failure to calculate changed blocks
711 util.SMlog("Cannot calculate changed blocks with"
712 "inconsistent bitmap sizes")
713 raise xs_errors.XenError('CBTMetadataInconsistent',
714 "Inconsistent bitmaps")
716 # Check if disk has been resized
717 if curr_vdi_size > vdi_size:
718 vdi_size = curr_vdi_size
719 extended_size = len(curr_bitmap) - len(merged_bitmap)
720 # Extend merged_bitmap to match size of curr_bitmap
721 extended_bitmap = extended_size * bitarray('0')
722 merged_bitmap += extended_bitmap
724 # At this point bitmap sizes should be same
725 if (len(curr_bitmap) > len(merged_bitmap) and
726 curr_vdi_size == vdi_size):
727 # This is unusual. Log it but calculate merged
728 # bitmap by truncating new bitmap
729 util.SMlog("Bitmap for %s bigger than other bitmaps"
730 "in chain without change in size" % curr_vdi)
731 curr_bitmap = curr_bitmap[:len(merged_bitmap)]
733 merged_bitmap = merged_bitmap | curr_bitmap
734 else:
735 merged_bitmap = curr_bitmap
736 vdi_size = curr_vdi_size
738 # Check if we have reached "vdi_to"
739 if curr_vdi == vdi_to:
740 encoded_string = base64.b64encode(merged_bitmap.tobytes()).decode()
741 return xmlrpc.client.dumps((encoded_string, ), "", True)
742 # TODO: Check 2: If both VDIs still exist,
743 # find common ancestor and find difference
745 # TODO: VDIs are unrelated
746 # return fully populated bitmap size of to VDI
748 raise xs_errors.XenError('CBTChangedBlocksError',
749 "Source and target VDI are unrelated")
751 def _cbt_snapshot(self, snapshot_uuid, consistency_state):
752 """ CBT snapshot"""
753 snap_logpath = self._get_cbt_logpath(snapshot_uuid)
754 vdi_logpath = self._get_cbt_logpath(self.uuid)
756 # Rename vdi vdi.cbtlog to snapshot.cbtlog
757 # and mark it consistent
758 self._rename(vdi_logpath, snap_logpath)
759 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_consistency,
760 snap_logpath, True)
762 #TODO: Make parent detection logic better. Ideally, get_cbt_parent
763 # should return None if the parent is set to a UUID made of all 0s.
764 # In this case, we don't know the difference between whether it is a
765 # NULL UUID or the parent file is missing. See cbtutil for why we can't
766 # do this
767 parent = self._cbt_op(snapshot_uuid,
768 cbtutil.get_cbt_parent, snap_logpath)
769 parent_path = self._get_cbt_logpath(parent)
770 if self._cbt_log_exists(parent_path):
771 self._cbt_op(parent, cbtutil.set_cbt_child,
772 parent_path, snapshot_uuid)
773 try:
774 # Ensure enough space for metadata file
775 self._ensure_cbt_space()
776 # Create new vdi.cbtlog
777 self._create_cbt_log()
778 # Set previous vdi node consistency status
779 if not consistency_state: 779 ↛ 780line 779 didn't jump to line 780, because the condition on line 779 was never true
780 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency,
781 vdi_logpath, consistency_state)
782 # Set relationship pointers
783 # Save the child of the VDI just snapshotted
784 curr_child_uuid = self._cbt_op(snapshot_uuid, cbtutil.get_cbt_child,
785 snap_logpath)
786 self._cbt_op(self.uuid, cbtutil.set_cbt_parent,
787 vdi_logpath, snapshot_uuid)
788 # Set child of new vdi to existing child of snapshotted VDI
789 self._cbt_op(self.uuid, cbtutil.set_cbt_child,
790 vdi_logpath, curr_child_uuid)
791 self._cbt_op(snapshot_uuid, cbtutil.set_cbt_child,
792 snap_logpath, self.uuid)
793 except Exception as ex:
794 alert_name = "VDI_CBT_SNAPSHOT_FAILED"
795 alert_str = ("Creating CBT metadata log for disk %s failed."
796 % self.uuid)
797 self._disable_cbt_on_error(alert_name, alert_str)
799 def _get_blocktracking_status(self, uuid=None) -> bool:
800 """ Get blocktracking status """
801 if not uuid: 801 ↛ 803line 801 didn't jump to line 803, because the condition on line 801 was never false
802 uuid = self.uuid
803 if not VdiType.isCowImage(self.vdi_type): 803 ↛ 804line 803 didn't jump to line 804, because the condition on line 803 was never true
804 return False
805 elif 'VDI_CONFIG_CBT' not in util.sr_get_capability(
806 self.sr.uuid, session=self.sr.session):
807 return False
808 logpath = self._get_cbt_logpath(uuid)
809 return self._cbt_log_exists(logpath)
811 def _set_blocktracking_status(self, vdi_ref, enable):
812 """ Set blocktracking status"""
813 vdi_config = self.session.xenapi.VDI.get_other_config(vdi_ref)
814 if "cbt_enabled" in vdi_config:
815 self.session.xenapi.VDI.remove_from_other_config(
816 vdi_ref, "cbt_enabled")
818 self.session.xenapi.VDI.add_to_other_config(
819 vdi_ref, "cbt_enabled", enable)
821 def _ensure_cbt_space(self) -> None:
822 """ Ensure enough CBT space """
823 pass
825 def _get_cbt_logname(self, uuid):
826 """ Get CBT logname """
827 logName = "%s.%s" % (uuid, CBTLOG_TAG)
828 return logName
830 def _get_cbt_logpath(self, uuid) -> str:
831 """ Get CBT logpath """
832 logName = self._get_cbt_logname(uuid)
833 return os.path.join(self.sr.path, logName)
835 def _create_cbt_log(self) -> str:
836 """ Create CBT log """
837 try:
838 logpath = self._get_cbt_logpath(self.uuid)
839 vdi_ref = self.sr.srcmd.params['vdi_ref']
840 size = self.session.xenapi.VDI.get_virtual_size(vdi_ref)
841 #cbtutil.create_cbt_log(logpath, size)
842 self._cbt_op(self.uuid, cbtutil.create_cbt_log, logpath, size)
843 self._cbt_op(self.uuid, cbtutil.set_cbt_consistency, logpath, True)
844 except Exception as e:
845 try:
846 self._delete_cbt_log()
847 except:
848 pass
849 finally:
850 raise e
852 return logpath
854 def _activate_cbt_log(self, logname) -> bool:
855 """Activate CBT log file
857 SR specific Implementation required for VDIs on block-based SRs.
858 No-op otherwise
859 """
860 return False
862 def _deactivate_cbt_log(self, logname) -> None:
863 """Deactivate CBT log file
865 SR specific Implementation required for VDIs on block-based SRs.
866 No-op otherwise
867 """
868 pass
870 def _cbt_op(self, uuid, func, *args):
871 # Lock cbtlog operations
872 from lock import Lock
873 lock = Lock("cbtlog", str(uuid))
874 lock.acquire()
876 try:
877 logname = self._get_cbt_logname(uuid)
878 activated = self._activate_cbt_log(logname)
879 ret = func( * args)
880 if activated:
881 self._deactivate_cbt_log(logname)
882 return ret
883 finally:
884 lock.release()
886 def _disable_cbt_on_error(self, alert_name, alert_str):
887 util.SMlog(alert_str)
888 self._delete_cbt_log()
889 vdi_ref = self.sr.srcmd.params['vdi_ref']
890 self.sr.session.xenapi.VDI.set_cbt_enabled(vdi_ref, False)
891 alert_prio_warning = "3"
892 alert_obj = "VDI"
893 alert_uuid = str(self.uuid)
894 self.sr.session.xenapi.message.create(alert_name,
895 alert_prio_warning,
896 alert_obj, alert_uuid,
897 alert_str)
899 def disable_leaf_on_secondary(self, vdi_uuid, secondary=None):
900 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid)
901 self.session.xenapi.VDI.remove_from_other_config(
902 vdi_ref, cleanup.VDI.DB_LEAFCLSC)
903 if secondary is not None:
904 util.SMlog(f"We have secondary for {vdi_uuid}, "
905 "blocking leaf coalesce")
906 self.session.xenapi.VDI.add_to_other_config(
907 vdi_ref, cleanup.VDI.DB_LEAFCLSC,
908 cleanup.VDI.LEAFCLSC_DISABLED)