Coverage for drivers/SR.py : 56%
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#!/usr/bin/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# SR: Base class for storage repositories
19#
21from sm_typing import Dict, List, Optional
23import VDI
24import xml.dom.minidom
25import xs_errors
26import XenAPI # pylint: disable=import-error
27import xmlrpc.client
28import util
29import copy
30import os
31import traceback
33from cowutil import (
34 getCowUtilFromImageFormat,
35 getImageStringFromVdiType,
36 getVdiTypeFromImageFormat,
37 ImageFormat,
38 parseImageFormats,
39 STR_TO_IMAGE_FORMAT,
40 IMAGE_FORMAT_TO_STR,
41)
43from vditype import VdiType
45MOUNT_BASE = '/var/run/sr-mount'
46DEFAULT_TAP = "vhd,qcow2"
47MASTER_LVM_CONF = '/etc/lvm/master'
49# LUN per VDI key for XenCenter
50LUNPERVDI = "LUNperVDI"
52DEFAULT_PREFERRED_IMAGE_FORMATS = [ImageFormat.VHD, ImageFormat.QCOW2]
53DEFAULT_SUPPORTED_IMAGE_FORMATS = [ImageFormat.RAW, ImageFormat.VHD, ImageFormat.QCOW2]
58def deviceCheck(op):
59 def wrapper(self, *args):
60 if 'device' not in self.dconf:
61 raise xs_errors.XenError('ConfigDeviceMissing')
62 return op(self, *args)
63 return wrapper
66backends: List["SR"] = []
69def registerSR(SRClass):
70 """Register SR with handler. All SR subclasses should call this in
71 the module file
72 """
73 backends.append(SRClass)
76def driver(type):
77 """Find the SR for the given dconf string"""
78 for d in backends: 78 ↛ 81line 78 didn't jump to line 81, because the loop on line 78 didn't complete
79 if d.handles(type):
80 return d
81 raise xs_errors.XenError('SRUnknownType')
84class SR(object):
85 """Semi-abstract storage repository object.
87 Attributes:
88 uuid: string, UUID
89 label: string
90 description: string
91 vdis: dictionary, VDI objects indexed by UUID
92 physical_utilisation: int, bytes consumed by VDIs
93 virtual_allocation: int, bytes allocated to this repository (virtual)
94 physical_size: int, bytes consumed by this repository
95 sr_vditype: string, repository type
96 """
98 @staticmethod
99 def handles(type) -> bool:
100 """Returns True if this SR class understands the given dconf string"""
101 return False
103 def __init__(self, srcmd, sr_uuid):
104 """Base class initializer. All subclasses should call SR.__init__
105 in their own
106 initializers.
108 Arguments:
109 srcmd: SRCommand instance, contains parsed arguments
110 """
111 try:
112 self.other_config = {}
113 self.srcmd = srcmd
114 self.dconf = srcmd.dconf
115 if 'session_ref' in srcmd.params:
116 self.session_ref = srcmd.params['session_ref']
117 self.session = XenAPI.xapi_local()
118 self.session._session = self.session_ref
119 if 'subtask_of' in self.srcmd.params: 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true
120 self.session.transport.add_extra_header('Subtask-of', self.srcmd.params['subtask_of'])
121 else:
122 self.session = None
124 if 'host_ref' not in self.srcmd.params:
125 self.host_ref = ""
126 else:
127 self.host_ref = self.srcmd.params['host_ref']
129 self.sr_ref = self.srcmd.params.get('sr_ref')
131 if 'device_config' in self.srcmd.params:
132 if self.dconf.get("SRmaster") == "true":
133 os.environ['LVM_SYSTEM_DIR'] = MASTER_LVM_CONF
135 if 'device_config' in self.srcmd.params:
136 if 'SCSIid' in self.srcmd.params['device_config']:
137 dev_path = '/dev/disk/by-scsid/' + self.srcmd.params['device_config']['SCSIid']
138 os.environ['LVM_DEVICE'] = dev_path
139 util.SMlog('Setting LVM_DEVICE to %s' % dev_path)
141 except TypeError:
142 raise Exception(traceback.format_exc())
143 except Exception as e:
144 raise e
145 raise xs_errors.XenError('SRBadXML')
147 self.uuid = sr_uuid
149 self.label = ''
150 self.description = ''
151 self.cmd = srcmd.params['command']
152 self.vdis = {}
153 self.physical_utilisation = 0
154 self.virtual_allocation = 0
155 self.physical_size = 0
156 self.sr_vditype = ''
157 self.passthrough = False
158 # XXX: if this is really needed then we must make a deep copy
159 self.original_srcmd = copy.deepcopy(self.srcmd)
160 self.default_vdi_visibility = True
161 self.scheds = ['none', 'noop']
162 self._mpathinit()
163 self.direct = False
164 self.ops_exclusive = []
165 self.driver_config = {}
166 self._is_shared = None
168 self.load(sr_uuid)
170 @staticmethod
171 def from_uuid(session, sr_uuid):
172 import importlib.util
174 _SR = session.xenapi.SR
175 sr_ref = _SR.get_by_uuid(sr_uuid)
176 sm_type = _SR.get_type(sr_ref)
177 # NB. load the SM driver module
179 _SM = session.xenapi.SM
180 sms = _SM.get_all_records_where('field "type" = "%s"' % sm_type)
181 sm_ref, sm = sms.popitem()
182 assert not sms
184 driver_path = _SM.get_driver_filename(sm_ref)
185 driver_real = os.path.realpath(driver_path)
186 module_name = os.path.basename(driver_path)
188 spec = importlib.util.spec_from_file_location(module_name, driver_real)
189 module = importlib.util.module_from_spec(spec)
190 spec.loader.exec_module(module)
192 target = driver(sm_type)
193 # NB. get the host pbd's device_config
195 host_ref = util.get_localhost_ref(session)
197 _PBD = session.xenapi.PBD
198 pbds = _PBD.get_all_records_where('field "SR" = "%s" and' % sr_ref +
199 'field "host" = "%s"' % host_ref)
200 pbd_ref, pbd = pbds.popitem()
201 assert not pbds
203 device_config = _PBD.get_device_config(pbd_ref)
204 # NB. make srcmd, to please our supersized SR constructor.
205 # FIXME
207 from SRCommand import SRCommand
208 cmd = SRCommand(module.DRIVER_INFO)
209 cmd.dconf = device_config
210 cmd.params = {'session_ref': session._session,
211 'host_ref': host_ref,
212 'device_config': device_config,
213 'sr_ref': sr_ref,
214 'sr_uuid': sr_uuid,
215 'command': 'nop'}
217 return target(cmd, sr_uuid)
219 def block_setscheduler(self, dev):
220 try:
221 realdev = os.path.realpath(dev)
222 disk = util.diskFromPartition(realdev)
224 # the normal case: the sr default scheduler (typically none/noop),
225 # potentially overridden by SR.other_config:scheduler
226 other_config = self.session.xenapi.SR.get_other_config(self.sr_ref)
227 sched = other_config.get('scheduler')
228 if not sched or sched in self.scheds: 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true
229 scheds = self.scheds
230 else:
231 scheds = [sched]
233 # special case: BFQ/CFQ if the underlying disk holds dom0's file systems.
234 if disk in util.dom0_disks(): 234 ↛ 235, 234 ↛ 2372 missed branches: 1) line 234 didn't jump to line 235, because the condition on line 234 was never true, 2) line 234 didn't jump to line 237, because the condition on line 234 was never false
235 scheds = ['bfq', 'cfq']
237 util.SMlog("Block scheduler: %s (%s) wants %s" % (dev, disk, scheds))
238 util.set_scheduler(realdev[5:], scheds)
239 except Exception as e:
240 util.SMlog("Failed to set block scheduler on %s: %s" % (dev, e))
242 def _addLUNperVDIkey(self):
243 try:
244 self.session.xenapi.SR.add_to_sm_config(self.sr_ref, LUNPERVDI, "true")
245 except:
246 pass
248 def is_shared(self):
249 if not self._is_shared:
250 self._is_shared = self.session.xenapi.SR.get_shared(self.sr_ref)
251 return self._is_shared
253 def create(self, uuid, size) -> None:
254 """Create this repository.
255 This operation may delete existing data.
257 The operation is NOT idempotent. The operation will fail
258 if an SR of the same UUID and driver type already exits.
260 Returns:
261 None
262 Raises:
263 SRUnimplementedMethod
264 """
265 raise xs_errors.XenError('Unimplemented')
267 def delete(self, uuid) -> None:
268 """Delete this repository and its contents.
270 This operation IS idempotent -- it will succeed if the repository
271 exists and can be deleted or if the repository does not exist.
272 The caller must ensure that all VDIs are deactivated and detached
273 and that the SR itself has been detached before delete().
274 The call will FAIL if any VDIs in the SR are in use.
276 Returns:
277 None
278 Raises:
279 SRUnimplementedMethod
280 """
281 raise xs_errors.XenError('Unimplemented')
283 def update(self, uuid) -> None:
284 """Refresh the fields in the SR object
286 Returns:
287 None
288 Raises:
289 SRUnimplementedMethod
290 """
291 # no-op unless individual backends implement it
292 return
294 def attach(self, uuid) -> None:
295 """Initiate local access to the SR. Initialises any
296 device state required to access the substrate.
298 Idempotent.
300 Returns:
301 None
302 Raises:
303 SRUnimplementedMethod
304 """
305 raise xs_errors.XenError('Unimplemented')
307 def after_master_attach(self, uuid) -> None:
308 """Perform actions required after attaching on the pool master
309 Return:
310 None
311 """
312 try:
313 self.scan(uuid)
314 except Exception as e:
315 util.SMlog("Error in SR.after_master_attach %s" % e)
316 msg_name = "POST_ATTACH_SCAN_FAILED"
317 msg_body = "Failed to scan SR %s after attaching, " \
318 "error %s" % (uuid, e)
319 self.session.xenapi.message.create(
320 msg_name, 2, "SR", uuid, msg_body)
322 def detach(self, uuid) -> None:
323 """Remove local access to the SR. Destroys any device
324 state initiated by the sr_attach() operation.
326 Idempotent. All VDIs must be detached in order for the operation
327 to succeed.
329 Returns:
330 None
331 Raises:
332 SRUnimplementedMethod
333 """
334 raise xs_errors.XenError('Unimplemented')
336 def probe(self) -> str:
337 """Perform a backend-specific scan, using the current dconf. If the
338 dconf is complete, then this will return a list of the SRs present of
339 this type on the device, if any. If the dconf is partial, then a
340 backend-specific scan will be performed, returning results that will
341 guide the user in improving the dconf.
343 Idempotent.
345 xapi will ensure that this is serialised wrt any other probes, or
346 attach or detach operations on this host.
348 Returns:
349 An XML fragment containing the scan results. These are specific
350 to the scan being performed, and the current backend.
351 Raises:
352 SRUnimplementedMethod
353 """
354 raise xs_errors.XenError('Unimplemented')
356 def scan(self, uuid) -> None:
357 """
358 Returns:
359 """
360 # Update SR parameters
361 self._db_update()
362 # Synchronise VDI list
363 scanrecord = ScanRecord(self)
364 scanrecord.synchronise()
366 def replay(self, uuid) -> None:
367 """Replay a multi-stage log entry
369 Returns:
370 None
371 Raises:
372 SRUnimplementedMethod
373 """
374 raise xs_errors.XenError('Unimplemented')
376 def content_type(self, uuid) -> str:
377 """Returns the 'content_type' of an SR as a string"""
378 return xmlrpc.client.dumps((str(self.sr_vditype), ), "", True)
380 def load(self, sr_uuid) -> None:
381 """Post-init hook"""
382 pass
384 def check_sr(self, sr_uuid) -> None:
385 """Hook to check SR health"""
386 pass
388 def vdi(self, uuid) -> 'VDI.VDI':
389 """Return VDI object owned by this repository"""
390 raise xs_errors.XenError('Unimplemented')
392 def forget_vdi(self, uuid) -> None:
393 vdi = self.session.xenapi.VDI.get_by_uuid(uuid)
394 self.session.xenapi.VDI.db_forget(vdi)
396 def cleanup(self) -> None:
397 # callback after the op is done
398 pass
400 def _db_update(self):
401 sr = self.session.xenapi.SR.get_by_uuid(self.uuid)
402 self.session.xenapi.SR.set_virtual_allocation(sr, str(self.virtual_allocation))
403 self.session.xenapi.SR.set_physical_size(sr, str(self.physical_size))
404 self.session.xenapi.SR.set_physical_utilisation(sr, str(self.physical_utilisation))
406 def _toxml(self):
407 dom = xml.dom.minidom.Document()
408 element = dom.createElement("sr")
409 dom.appendChild(element)
411 # Add default uuid, physical_utilisation, physical_size and
412 # virtual_allocation entries
413 for attr in ('uuid', 'physical_utilisation', 'virtual_allocation',
414 'physical_size'):
415 try:
416 aval = getattr(self, attr)
417 except AttributeError:
418 raise xs_errors.XenError(
419 'InvalidArg', opterr='Missing required field [%s]' % attr)
421 entry = dom.createElement(attr)
422 element.appendChild(entry)
423 textnode = dom.createTextNode(str(aval))
424 entry.appendChild(textnode)
426 # Add the default_vdi_visibility entry
427 entry = dom.createElement('default_vdi_visibility')
428 element.appendChild(entry)
429 if not self.default_vdi_visibility:
430 textnode = dom.createTextNode('False')
431 else:
432 textnode = dom.createTextNode('True')
433 entry.appendChild(textnode)
435 # Add optional label and description entries
436 for attr in ('label', 'description'):
437 try:
438 aval = getattr(self, attr)
439 except AttributeError:
440 continue
441 if aval:
442 entry = dom.createElement(attr)
443 element.appendChild(entry)
444 textnode = dom.createTextNode(str(aval))
445 entry.appendChild(textnode)
447 # Create VDI sub-list
448 if self.vdis:
449 for uuid in self.vdis:
450 if not self.vdis[uuid].deleted:
451 vdinode = dom.createElement("vdi")
452 element.appendChild(vdinode)
453 self.vdis[uuid]._toxml(dom, vdinode)
455 return dom
457 def _fromxml(self, str, tag):
458 dom = xml.dom.minidom.parseString(str)
459 objectlist = dom.getElementsByTagName(tag)[0]
460 taglist = {}
461 for node in objectlist.childNodes:
462 taglist[node.nodeName] = ""
463 for n in node.childNodes:
464 if n.nodeType == n.TEXT_NODE:
465 taglist[node.nodeName] += n.data
466 return taglist
468 def _splitstring(self, str):
469 elementlist = []
470 for i in range(0, len(str)):
471 elementlist.append(str[i])
472 return elementlist
474 def _mpathinit(self):
475 self.mpath = "false"
476 try:
477 if 'multipathing' in self.dconf and \ 477 ↛ 479line 477 didn't jump to line 479, because the condition on line 477 was never true
478 'multipathhandle' in self.dconf:
479 self.mpath = self.dconf['multipathing']
480 self.mpathhandle = self.dconf['multipathhandle']
481 else:
482 hconf = self.session.xenapi.host.get_other_config(self.host_ref)
483 self.mpath = hconf['multipathing']
484 self.mpathhandle = hconf.get('multipathhandle', 'dmp')
486 if self.mpath != "true": 486 ↛ 490line 486 didn't jump to line 490, because the condition on line 486 was never false
487 self.mpath = "false"
488 self.mpathhandle = "null"
490 if not os.path.exists("/opt/xensource/sm/mpath_%s.py" % self.mpathhandle): 490 ↛ 495line 490 didn't jump to line 495, because the condition on line 490 was never false
491 raise IOError("File does not exist = %s" % self.mpathhandle)
492 except:
493 self.mpath = "false"
494 self.mpathhandle = "null"
495 module_name = "mpath_%s" % self.mpathhandle
496 self.mpathmodule = __import__(module_name)
498 def _mpathHandle(self):
499 if self.mpath == "true": 499 ↛ 500line 499 didn't jump to line 500, because the condition on line 499 was never true
500 self.mpathmodule.activate()
501 else:
502 self.mpathmodule.deactivate()
504 def _pathrefresh(self, obj):
505 SCSIid = getattr(self, 'SCSIid')
506 self.dconf['device'] = self.mpathmodule.path(SCSIid)
507 super(obj, self).load(self.uuid)
509 def _setMultipathableFlag(self, SCSIid=''):
510 try:
511 sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
512 sm_config['multipathable'] = 'true'
513 self.session.xenapi.SR.set_sm_config(self.sr_ref, sm_config)
515 if self.mpath == "true" and len(SCSIid): 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true
516 util.kickpipe_mpathcount()
517 except:
518 pass
520 def check_dconf(self, key_list, raise_flag=True):
521 """ Checks if all keys in 'key_list' exist in 'self.dconf'.
523 Input:
524 key_list: a list of keys to check if they exist in self.dconf
525 raise_flag: if true, raise an exception if there are 1 or more
526 keys missing
528 Return: set() containing the missing keys (empty set() if all exist)
529 Raise: xs_errors.XenError('ConfigParamsMissing')
530 """
532 missing_keys = {key for key in key_list if key not in self.dconf}
534 if missing_keys and raise_flag:
535 errstr = 'device-config is missing the following parameters: ' + \
536 ', '.join([key for key in missing_keys])
537 raise xs_errors.XenError('ConfigParamsMissing', opterr=errstr)
539 return missing_keys
541 @staticmethod
542 def read_config_image_format(config: Dict[str, str]) -> Optional[ImageFormat]:
543 str_image_format = config.get("image-format") or config.get("type")
544 if not str_image_format: 544 ↛ 545line 544 didn't jump to line 545, because the condition on line 544 was never true
545 return None
547 image_format = STR_TO_IMAGE_FORMAT.get(str_image_format)
548 if image_format: 548 ↛ 551line 548 didn't jump to line 551, because the condition on line 548 was never false
549 return image_format
551 raise xs_errors.XenError('VDIType', opterr=f'Unknown image format `{str_image_format}`')
553 def _init_image_formats(
554 self,
555 *,
556 preferred_image_formats = DEFAULT_PREFERRED_IMAGE_FORMATS,
557 supported_image_formats = DEFAULT_SUPPORTED_IMAGE_FORMATS
558 ) -> None:
559 self.preferred_image_formats = parseImageFormats(
560 self.dconf and self.dconf.get('preferred-image-formats'),
561 preferred_image_formats,
562 supported_image_formats
563 )
564 self.supported_image_formats = supported_image_formats
566 def _resolve_vdi_type_from_image_format(self, image_format: ImageFormat) -> str:
567 if image_format in self.supported_image_formats: 567 ↛ 569line 567 didn't jump to line 569, because the condition on line 567 was never false
568 return getVdiTypeFromImageFormat(image_format)
569 raise xs_errors.XenError('VDIType', opterr=f'Unsupported image format `{IMAGE_FORMAT_TO_STR[image_format]}`')
571 def _get_snap_vdi_type(self, vdi_type: str, size: int) -> str:
572 if VdiType.isCowImage(vdi_type): 572 ↛ 574line 572 didn't jump to line 574, because the condition on line 572 was never false
573 return vdi_type
574 if vdi_type == VdiType.RAW:
575 for image_format in self.preferred_image_formats:
576 if getCowUtilFromImageFormat(image_format).canSnapshotRaw(size):
577 return getVdiTypeFromImageFormat(image_format)
578 raise xs_errors.XenError('VDISnapshot', opterr=f"cannot snap from `{vdi_type}`")
580class ScanRecord:
581 def __init__(self, sr):
582 self.sr = sr
583 self.__xenapi_locations = {}
584 self.__xenapi_records = util.list_VDI_records_in_sr(sr)
585 for vdi in list(self.__xenapi_records.keys()): 585 ↛ 586line 585 didn't jump to line 586, because the loop on line 585 never started
586 self.__xenapi_locations[util.to_plain_string(self.__xenapi_records[vdi]['location'])] = vdi
587 self.__sm_records = {}
588 for vdi in list(sr.vdis.values()):
589 # We initialise the sm_config field with the values from the database
590 # The sm_config_overrides contains any new fields we want to add to
591 # sm_config, and also any field to delete (by virtue of having
592 # sm_config_overrides[key]=None)
593 try:
594 if not hasattr(vdi, "sm_config"): 594 ↛ 600line 594 didn't jump to line 600, because the condition on line 594 was never false
595 vdi.sm_config = self.__xenapi_records[self.__xenapi_locations[vdi.location]]['sm_config'].copy()
596 except:
597 util.SMlog("missing config for vdi: %s" % vdi.location)
598 vdi.sm_config = {}
600 if "image-format" not in vdi.sm_config: 600 ↛ 606line 600 didn't jump to line 606, because the condition on line 600 was never false
601 try:
602 vdi.sm_config["image-format"] = getImageStringFromVdiType(vdi.vdi_type)
603 except:
604 pass # No image format for this VDI type.
606 vdi._override_sm_config(vdi.sm_config)
608 self.__sm_records[vdi.location] = vdi
610 xenapi_locations = set(self.__xenapi_locations.keys())
611 sm_locations = set(self.__sm_records.keys())
613 # These ones are new on disk
614 self.new = sm_locations.difference(xenapi_locations)
615 # These have disappeared from the disk
616 self.gone = xenapi_locations.difference(sm_locations)
617 # These are the ones which are still present but might have changed...
618 existing = sm_locations.intersection(xenapi_locations)
619 # Synchronise the uuid fields using the location as the primary key
620 # This ensures we know what the UUIDs are even though they aren't stored
621 # in the storage backend.
622 for location in existing: 622 ↛ 623line 622 didn't jump to line 623, because the loop on line 622 never started
623 sm_vdi = self.get_sm_vdi(location)
624 xenapi_vdi = self.get_xenapi_vdi(location)
625 sm_vdi.uuid = util.default(sm_vdi, "uuid", lambda: xenapi_vdi['uuid'])
627 # Only consider those whose configuration looks different
628 self.existing = [x for x in existing if not(self.get_sm_vdi(x).in_sync_with_xenapi_record(self.get_xenapi_vdi(x)))]
630 if len(self.new) != 0:
631 util.SMlog("new VDIs on disk: " + repr(self.new))
632 if len(self.gone) != 0: 632 ↛ 633line 632 didn't jump to line 633, because the condition on line 632 was never true
633 util.SMlog("VDIs missing from disk: " + repr(self.gone))
634 if len(self.existing) != 0: 634 ↛ 635line 634 didn't jump to line 635, because the condition on line 634 was never true
635 util.SMlog("VDIs changed on disk: " + repr(self.existing))
637 def get_sm_vdi(self, location):
638 return self.__sm_records[location]
640 def get_xenapi_vdi(self, location):
641 return self.__xenapi_records[self.__xenapi_locations[location]]
643 def all_xenapi_locations(self):
644 return set(self.__xenapi_locations.keys())
646 def synchronise_new(self):
647 """Add XenAPI records for new disks"""
648 for location in self.new:
649 vdi = self.get_sm_vdi(location)
650 util.SMlog("Introducing VDI with location=%s" % (vdi.location))
651 vdi._db_introduce()
653 def synchronise_gone(self):
654 """Delete XenAPI record for old disks"""
655 for location in self.gone: 655 ↛ 656line 655 didn't jump to line 656, because the loop on line 655 never started
656 vdi = self.get_xenapi_vdi(location)
657 util.SMlog("Forgetting VDI with location=%s uuid=%s" % (util.to_plain_string(vdi['location']), vdi['uuid']))
658 try:
659 self.sr.forget_vdi(vdi['uuid'])
660 except XenAPI.Failure as e:
661 if util.isInvalidVDI(e):
662 util.SMlog("VDI %s not found, ignoring exception" %
663 vdi['uuid'])
664 else:
665 raise
667 def synchronise_existing(self):
668 """Update existing XenAPI records"""
669 for location in self.existing: 669 ↛ 670line 669 didn't jump to line 670, because the loop on line 669 never started
670 vdi = self.get_sm_vdi(location)
672 util.SMlog("Updating VDI with location=%s uuid=%s" % (vdi.location, vdi.uuid))
673 vdi._db_update()
675 def synchronise(self):
676 """Perform the default SM -> xenapi synchronisation; ought to be good enough
677 for most plugins."""
678 self.synchronise_new()
679 self.synchronise_gone()
680 self.synchronise_existing()