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