Coverage for libs/sm/drivers/ISOSR.py: 43%
443 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
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# ISOSR: remote iso storage repository
17# matches with drivers/ISOSR
19import os
20import re
22from sm import SR
23from sm import VDI
24from sm import nfs
25from sm import cifutils
26from sm.core import util
27from sm.core import xs_errors
29CAPABILITIES = ["VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
30 "SR_SCAN", "SR_ATTACH", "SR_DETACH"]
32CONFIGURATION = [
33 ['location', 'path to mount (required) (e.g. server:/path)'],
34 ['options',
35 'extra options to pass to mount (e.g. \'-o ro\')'],
36 ['type', 'cifs (SMB) or nfs_iso'],
37 nfs.NFS_VERSION,
38 ['vers', 'SMB version, default version 3'],
39 ['username', r'Username to authenticate to SMB share with, can be domain\username'],
40 ['cifspassword_secret', 'Secret ID containing the password to authenticate to SMB'],
41 ['cifspassword', 'Password to authenticate to SMB, (deprecated see cifspassword_secret)']
42]
44DRIVER_INFO = {
45 'name': 'ISO',
46 'description': 'Handles CD images stored as files in iso format',
47 'vendor': 'Citrix Systems Inc',
48 'copyright': '(C) 2008 Citrix Systems Inc',
49 'driver_version': '1.0',
50 'required_api_version': '1.0',
51 'capabilities': CAPABILITIES,
52 'configuration': CONFIGURATION
53 }
55TYPE = "iso"
56SMB_VERSION_1 = '1.0'
57SMB_VERSION_3 = '3.0'
58NFSPORT = 2049
61def list_images(path):
62 """
63 Finds the iso and img files in a given directory that have valid unicode
64 names. Returns a list of these, together with a count of number of image
65 files that had to be ignored due to encoding issues in their names.
66 """
67 # pylint: disable=no-member
69 regex = re.compile(r"\.iso$|\.img$", re.I)
70 images = []
71 num_images_ignored = 0
73 for filename in os.listdir(path):
74 if not regex.search(filename):
75 # Not an image file
76 pass
77 elif os.path.isdir(os.path.join(path, filename)):
78 util.SMlog("list_images: '%s' is a directory. Ignore"
79 % loggable_filename(filename))
80 else:
81 try:
82 if is_consistent_utf8_filename(filename):
83 images.append(filename)
84 else:
85 num_images_ignored += 1
86 util.SMlog("WARNING: ignoring image file '%s' due to"
87 " encoding issues"
88 % loggable_filename(filename))
89 except UnicodeDecodeError as e:
90 num_images_ignored += 1
91 util.SMlog("WARNING: ignoring image file '%s' as its name is"
92 " not UTF-8 compatible"
93 % loggable_filename(filename))
95 return images, num_images_ignored
98def loggable_filename(filename):
99 # Strip the 'b"' and '"' off the string representation of bytes
100 return str(os.fsencode(filename))[2:-1]
103def is_consistent_utf8_filename(filename):
104 """
105 Determines whether a filename, which is assumed to come from a filesystem
106 where names are UTF-8 encoded, is consistent in the sense that its name in
107 the form we'd like to use it (that is, as valid unicode) is the same as
108 the form it needs to take when passed to Python library functions (e.g.
109 open, os.stat).
111 Raises UnicodeDecodeError if the name on the filesystem isn't UTF-8
112 encoded.
113 """
114 # We generally expect that names of files in the mounted file system will
115 # be utf-8 encoded. That need not always be true - for example, mount.cifs
116 # provides an "iocharset" option to control this. But we make no attempt
117 # to cope with, say, latin-1, and so such a file name will cause an
118 # exception.
119 #
120 # Even if a file's name is utf-8 encoded, we might still have to reject it
121 # for being "inconsistent". That's because Python's filesystem encoding
122 # (see `sys.getfilesystemencoding`) might be ascii rather than utf-8, in
123 # which case non-ascii characters in file names will show up as "surrogate
124 # escapes" - which makes them technically not valid as unicode.
125 #
126 # Although it would be easy enough to recover the originally intended name
127 # for such a file, it would be awkward elsewhere in the code base either
128 # to have invalid unicode in file paths, or to have file paths that needed
129 # massaging before they could be used for actual file operations. Hence
130 # we say the name is inconsistent.
131 #
132 # From Python 3.7 onwards it looks like it should be much more likely that
133 # the filesystem encoding will be utf-8, which will hopefully mean that we
134 # would then get previously rejected image files showing up, and working
135 # without further code changes being necessary.
137 filename_bytes = os.fsencode(filename)
138 return filename == filename_bytes.decode("utf-8")
141def tools_iso_name(filename):
142 # The tools ISO used have a "xs-" prefix in its name.
143 # We recognise both and set the name_label accordingly.
144 if filename[:3] == "xs-":
145 return "xs-tools.iso"
146 else:
147 return "guest-tools.iso"
150class ISOSR(SR.SR):
151 """Local file storage repository"""
153 # Some helper functions:
154 def _checkmount(self):
155 """Checks that the mountpoint exists and is mounted"""
156 if not util.pathexists(self.mountpoint): 156 ↛ 158line 156 didn't jump to line 158, because the condition on line 156 was never false
157 return False
158 try:
159 ismount = util.ismount(self.mountpoint)
160 except util.CommandException as inst:
161 return False
162 return ismount
164 def _checkTargetStr(self, location):
165 if 'type' not in self.dconf:
166 return
167 if self.dconf['type'] == 'cifs': 167 ↛ 168line 167 didn't jump to line 168, because the condition on line 167 was never true
168 tgt = ''
169 if re.search('^//', location):
170 tgt = location.split('/')[2]
171 elif re.search(r'^\\', location):
172 l = location.split('\\')
173 for i in location.split('\\'):
174 if i:
175 tgt = i
176 break
177 if not tgt:
178 raise xs_errors.XenError('ISOLocationStringError')
179 else:
180 if location.find(':') == -1: 180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true
181 raise xs_errors.XenError('ISOLocationStringError')
182 tgt = location.split(':')[0]
184 try:
185 util._convertDNS(tgt)
186 except:
187 raise xs_errors.XenError('DNSError')
189 # pylint: disable=no-member
190 uuid_file_regex = re.compile(
191 r"([0-9a-f]{8}-(([0-9a-f]{4})-){3}[0-9a-f]{12})\.(iso|img)", re.I)
193 def _loadvdis(self):
194 """Scan the directory and get uuids either from the VDI filename, \
195 or by creating a new one."""
196 if self.vdis:
197 return
199 image_names, _ = list_images(self.path)
201 for name in image_names:
202 self.vdis[name] = ISOVDI(self, name)
203 # Set the VDI UUID if the filename is of the correct form.
204 # Otherwise, one will be generated later in VDI._db_introduce.
205 m = self.uuid_file_regex.match(name)
206 if m:
207 self.vdis[name].uuid = m.group(1)
209 # Synchronise the read-only status with existing VDI records
210 __xenapi_records = util.list_VDI_records_in_sr(self)
211 __xenapi_locations = {}
212 for vdi in __xenapi_records.keys():
213 __xenapi_locations[__xenapi_records[vdi]['location']] = vdi
214 for vdi in self.vdis.values():
215 if vdi.location in __xenapi_locations:
216 v = __xenapi_records[__xenapi_locations[vdi.location]]
217 sm_config = v['sm_config']
218 if 'created' in sm_config:
219 vdi.sm_config['created'] = sm_config['created']
220 vdi.read_only = False
222# Now for the main functions:
223 def handles(type):
224 """Do we handle this type?"""
225 if type == TYPE:
226 return True
227 return False
228 handles = staticmethod(handles)
230 def content_type(self, sr_uuid):
231 """Returns the content_type XML"""
232 return super(ISOSR, self).content_type(sr_uuid)
234 # pylint: disable=no-member
235 vdi_path_regex = re.compile(r"[a-z0-9.-]+\.(iso|img)", re.I)
237 def vdi(self, uuid):
238 """Create a VDI class. If the VDI does not exist, we determine
239 here what its filename should be."""
241 filename = util.to_plain_string(self.srcmd.params.get('vdi_location'))
242 if filename is None:
243 smconfig = self.srcmd.params.get('vdi_sm_config')
244 if smconfig is None:
245 # uh, oh, a VDI.from_uuid()
246 import XenAPI # pylint: disable=import-error
247 _VDI = self.session.xenapi.VDI
248 try:
249 vdi_ref = _VDI.get_by_uuid(uuid)
250 except XenAPI.Failure as e:
251 if e.details[0] != 'UUID_INVALID':
252 raise
253 else:
254 filename = _VDI.get_location(vdi_ref)
256 if filename is None:
257 # Get the filename from sm-config['path'], or use the UUID
258 # if the path param doesn't exist.
259 if smconfig and 'path' in smconfig:
260 filename = smconfig['path']
261 if not self.vdi_path_regex.match(filename):
262 raise xs_errors.XenError('VDICreate', \
263 opterr='Invalid path "%s"' % filename)
264 else:
265 filename = '%s.img' % uuid
267 return ISOVDI(self, filename)
269 def load(self, sr_uuid):
270 """Initialises the SR"""
271 # First of all, check we've got the correct keys in dconf
272 if 'location' not in self.dconf: 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true
273 raise xs_errors.XenError('ConfigLocationMissing')
275 # Construct the path we're going to mount under:
276 if "legacy_mode" in self.dconf: 276 ↛ 277line 276 didn't jump to line 277, because the condition on line 276 was never true
277 self.mountpoint = util.to_plain_string(self.dconf['location'])
278 else:
279 # Verify the target address
280 self._checkTargetStr(self.dconf['location'])
281 self.mountpoint = os.path.join(SR.MOUNT_BASE, sr_uuid)
283 # Add on the iso_path value if there is one
284 if "iso_path" in self.dconf: 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true
285 iso_path = util.to_plain_string(self.dconf['iso_path'])
286 if iso_path.startswith("/"):
287 iso_path = iso_path[1:]
288 self.path = os.path.join(self.mountpoint, iso_path)
289 else:
290 self.path = self.mountpoint
292 # Handle optional dconf attributes
293 self.nfsversion = nfs.validate_nfsversion(self.dconf.get('nfsversion'))
295 # Fill the required SMB version
296 self.smbversion = SMB_VERSION_3
298 # Check if smb version is specified from client
299 self.is_smbversion_specified = False
301 # Some info we need:
302 self.sr_vditype = 'phy'
304 def delete(self, sr_uuid):
305 pass
307 def attach(self, sr_uuid):
308 """Std. attach"""
309 # Very-Legacy mode means the ISOs are in the local fs - so no need to attach.
310 if 'legacy_mode' in self.dconf: 310 ↛ 312line 310 didn't jump to line 312, because the condition on line 310 was never true
311 # Verify path exists
312 if not os.path.exists(self.mountpoint):
313 raise xs_errors.XenError('ISOLocalPath')
314 return
316 # Check whether we're already mounted
317 if self._checkmount():
318 return
320 location = util.to_plain_string(self.dconf['location'])
321 # TODO: Have XC standardise iso type string
322 if 'type' in self.dconf:
323 protocol = self.dconf['type']
324 elif ":/" in location: 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 protocol = 'nfs_iso'
326 else:
327 protocol = 'cifs'
329 if protocol == 'nfs_iso':
330 self._check_nfs_server(location)
332 # Create the mountpoint if it's not already there
333 if not util.isdir(self.mountpoint): 333 ↛ 336line 333 didn't jump to line 336, because the condition on line 333 was never false
334 util.makedirs(self.mountpoint)
336 mountcmd = []
337 options = ''
339 if 'options' in self.dconf:
340 options = self.dconf['options'].split(' ')
341 if protocol == 'cifs': 341 ↛ 344line 341 didn't jump to line 344, because the condition on line 341 was never false
342 options = [x for x in options if x != ""]
343 else:
344 options = self.getNFSOptions(options)
346 # SMB options are passed differently for create via
347 # XC/xe sr-create and create via xe-mount-iso-sr
348 # In both cases check if SMB version is passed are not.
349 # If not use self.smbversion.
350 if protocol == 'cifs':
351 if 'type' in self.dconf:
352 # Create via XC or sr-create
353 # Check for username and password
354 mountcmd = ["mount.cifs", location, self.mountpoint]
355 if 'vers' in self.dconf:
356 self.is_smbversion_specified = True
357 self.smbversion = self.dconf['vers']
358 util.SMlog("self.dconf['vers'] = %s" % self.dconf['vers'])
359 self.appendCIFSMountOptions(mountcmd)
360 else:
361 # Creation via xe-mount-iso-sr
362 try:
363 mountcmd = ["mount", location, self.mountpoint]
364 if options and options[0] == '-o': 364 ↛ 373line 364 didn't jump to line 373, because the condition on line 364 was never false
365 pos = options[1].find('vers=')
366 if pos == -1: 366 ↛ 367line 366 didn't jump to line 367, because the condition on line 366 was never true
367 options[1] += ',' + self.getSMBVersion()
368 else:
369 self.smbversion = self.getSMBVersionFromOptions(
370 options[1])
371 self.is_smbversion_specified = True
372 else:
373 raise ValueError
374 mountcmd.extend(options)
375 except ValueError:
376 raise xs_errors.XenError('ISOInvalidXeMountOptions')
377 # Check the validity of 'smbversion'.
378 # Raise an exception for any invalid version.
379 if self.smbversion not in [SMB_VERSION_1, SMB_VERSION_3]:
380 raise xs_errors.XenError('ISOInvalidSMBversion')
382 # Attempt mounting
383 smb3_fail_reason = None
384 try:
385 if protocol == 'nfs_iso':
386 # For NFS, do a soft mount with tcp as protocol. Since ISO SR is
387 # going to be r-only, a failure in nfs link can be reported back
388 # to the process waiting.
389 server, path, transport = self._parse_nfs_location(location)
390 # Extract timeout and retrans values, if any
391 io_timeout = nfs.get_nfs_timeout(self.other_config)
392 io_retrans = nfs.get_nfs_retrans(self.other_config)
393 nfs.soft_mount(self.mountpoint, server, path,
394 transport, useroptions=options, nfsversion=self.nfsversion,
395 timeout=io_timeout, retrans=io_retrans)
396 else:
397 if self.smbversion in SMB_VERSION_3:
398 util.SMlog('ISOSR mount over smb 3.0')
399 try:
400 self.mountOverSMB(mountcmd)
401 except util.CommandException as inst:
402 if not self.is_smbversion_specified: 402 ↛ 420line 402 didn't jump to line 420, because the condition on line 402 was never false
403 util.SMlog('Retrying ISOSR mount over smb 1.0')
404 smb3_fail_reason = inst.reason
405 # mountcmd is constructed such that the last two
406 # items will contain -o argument and its value.
407 del mountcmd[-2:]
408 self.smbversion = SMB_VERSION_1
409 if not options: 409 ↛ 412line 409 didn't jump to line 412, because the condition on line 409 was never false
410 self.appendCIFSMountOptions(mountcmd)
411 else:
412 if options[0] == '-o':
413 # regex can be used here since we have
414 # already validated version entry
415 options[1] = re.sub('vers=3.0', 'vers=1.0',
416 options[1])
417 mountcmd.extend(options)
418 self.mountOverSMB(mountcmd)
419 else:
420 raise xs_errors.XenError(
421 'ISOMountFailure', opterr=inst.reason)
422 else:
423 util.SMlog('ISOSR mount over smb 1.0')
424 self.mountOverSMB(mountcmd)
425 except util.CommandException as inst:
426 if not self.is_smbversion_specified: 426 ↛ 430line 426 didn't jump to line 430, because the condition on line 426 was never false
427 raise xs_errors.XenError(
428 'ISOMountFailure', opterr=smb3_fail_reason)
429 else:
430 raise xs_errors.XenError(
431 'ISOMountFailure', opterr=inst.reason)
432 except nfs.NfsException as e:
433 raise xs_errors.XenError('ISOMountFailure', opterr=str(e.errstr))
435 # Check the iso_path is accessible
436 if not self._checkmount(): 436 ↛ 437line 436 didn't jump to line 437, because the condition on line 436 was never true
437 self.detach(sr_uuid)
438 raise xs_errors.XenError('ISOSharenameFailure')
440 def _parse_nfs_location(self, location):
441 """
442 Given the location of an NFS share, parse it to give
443 a tuple of the remove server, remote path, and transport protocol to
444 use.
445 """
446 serv_path = []
447 transport = 'tcp'
448 if location.startswith('['):
449 # IPv6 target: remove brackets around the IPv6
450 transport = 'tcp6'
451 ip6 = location[1:location.index(']')]
452 path = location[location.index(']') + 2:]
453 serv_path = [ip6, path]
454 else:
455 serv_path = location.split(':')
457 return serv_path[0], serv_path[1], transport
459 def _check_nfs_server(self, location):
460 """
461 Given that we want to mount a given NFS share, checks that there is an
462 NFS server running in the remote server, and that it supports the
463 desired NFS version. Raises an appropriate exception if this is not
464 the case.
465 """
466 server, _, transport = self._parse_nfs_location(location)
468 try:
469 util._testHost(server, NFSPORT, 'NFSTarget')
470 if not nfs.check_server_tcp(server, transport, self.nfsversion):
471 raise xs_errors.XenError('NFSVersion',
472 opterr="Unsupported NFS version: %s" % self.nfsversion)
473 except nfs.NfsException as e:
474 raise xs_errors.XenError('NFSTarget', opterr=str(e.errstr))
477 def after_master_attach(self, uuid):
478 """Perform actions required after attaching on the pool master
479 Return:
480 None
481 """
482 # Nothing required here for ISOs and tools ISOs will fail if scanned
483 pass
485 def getSMBVersionFromOptions(self, options):
486 """Extract SMB version from options """
487 smb_ver = None
488 options_list = options.split(',')
489 for option in options_list: 489 ↛ 495line 489 didn't jump to line 495, because the loop on line 489 didn't complete
490 if option.startswith('vers='): 490 ↛ 489line 490 didn't jump to line 489, because the condition on line 490 was never false
491 version = option.split('=')
492 if len(version) == 2: 492 ↛ 494line 492 didn't jump to line 494, because the condition on line 492 was never false
493 smb_ver = version[1]
494 break
495 return smb_ver
497 def getSMBVersion(self):
498 """Pass smb version option to mount.cifs"""
499 smbversion = "vers=%s" % self.smbversion
500 return smbversion
502 def mountOverSMB(self, mountcmd):
503 """This function raises util.CommandException"""
504 new_env, domain = cifutils.getCIFCredentials(self.dconf, self.session,
505 prefix="cifs")
507 util.pread(mountcmd, True, new_env=new_env)
508 try:
509 if not self.is_smbversion_specified:
510 # Store the successful smb version in PBD config
511 self.updateSMBVersInPBDConfig()
512 except Exception as exc:
513 util.SMlog("Exception: %s" % str(exc))
514 if self._checkmount(): 514 ↛ 516line 514 didn't jump to line 516, because the condition on line 514 was never false
515 util.pread(["umount", self.mountpoint])
516 raise xs_errors.XenError('SMBMount') from exc
518 def updateSMBVersInPBDConfig(self):
519 """Store smb version in PBD config"""
520 pbd = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
521 if pbd is not None: 521 ↛ 522line 521 didn't jump to line 522, because the condition on line 521 was never true
522 util.SMlog('Updating SMB version in PBD device config')
523 dconf = self.session.xenapi.PBD.get_device_config(pbd)
524 dconf['vers'] = self.smbversion
525 self.session.xenapi.PBD.set_device_config(pbd, dconf)
526 else:
527 raise Exception('Could not find PBD for corresponding SR')
529 def getNFSOptions(self, options):
530 """Append options to mount.nfs"""
531 #Only return any options specified with -o
532 nfsOptions = ''
533 for index, opt in enumerate(options):
534 if opt == "-o":
535 nfsOptions = options[index + 1]
536 break
538 return nfsOptions
540 def appendCIFSMountOptions(self, mountcmd):
541 """Append options to mount.cifs"""
542 options = []
543 try:
544 options.append(self.getCacheOptions())
546 if not cifutils.containsCredentials(self.dconf, prefix="cifs"):
547 options.append('guest')
548 domain = None
549 else:
550 _, domain = cifutils.splitDomainAndUsername(self.dconf['username'])
552 options.append(self.getSMBVersion())
554 if domain:
555 options.append('domain=' + domain)
556 except:
557 util.SMlog("Exception while attempting to append mount options")
558 raise
560 # Extend mountcmd appropriately
561 if options: 561 ↛ exitline 561 didn't return from function 'appendCIFSMountOptions', because the condition on line 561 was never false
562 options = ",".join(str(x) for x in options if x)
563 mountcmd.extend(["-o", options])
565 def getCacheOptions(self):
566 """Pass cache options to mount.cifs"""
567 return "cache=none"
569 def detach(self, sr_uuid):
570 """Std. detach"""
571 # This handles legacy mode too, so no need to check
572 if not self._checkmount():
573 return
575 try:
576 util.pread(["umount", self.mountpoint])
577 except util.CommandException as inst:
578 raise xs_errors.XenError('NFSUnMount', \
579 opterr='error is %d' % inst.code)
581 def scan(self, sr_uuid):
582 """Scan: see _loadvdis"""
583 if not util.isdir(self.path):
584 raise xs_errors.XenError('SRUnavailable', \
585 opterr='no such directory %s' % self.path)
587 if ('legacy_mode' not in self.dconf) and (not self._checkmount()):
588 raise xs_errors.XenError('SRUnavailable', \
589 opterr='directory not mounted: %s' % self.path)
591 #try:
592 if not self.vdis:
593 self._loadvdis()
594 self.physical_size = util.get_fs_size(self.path)
595 self.physical_utilisation = util.get_fs_utilisation(self.path)
596 self.virtual_allocation = self.physical_size
598 self.other_config = self.session.xenapi.SR.get_other_config(self.sr_ref)
600 if 'xenserver_tools_sr' in self.other_config and \
601 self.other_config['xenserver_tools_sr'] == "true":
602 # Out of all the xs-tools ISOs which exist in this dom0, we mark
603 # only one as the official one.
605 # Pass 1: find the latest version
606 latest_build_vdi = None
607 latest_build_number = "0"
608 for vdi_name in self.vdis:
609 vdi = self.vdis[vdi_name]
611 if latest_build_vdi is None:
612 latest_build_vdi = vdi.location
613 latest_build_number = "0"
615 if 'xs-tools-build' in vdi.sm_config:
616 bld = vdi.sm_config['xs-tools-build']
617 if bld >= latest_build_number:
618 latest_build_vdi = vdi.location
619 latest_build_number = bld
621 # Pass 2: mark all VDIs accordingly
622 for vdi_name in self.vdis:
623 vdi = self.vdis[vdi_name]
624 if vdi.location == latest_build_vdi:
625 vdi.sm_config['xs-tools'] = "true"
626 else:
627 if "xs-tools" in vdi.sm_config:
628 del vdi.sm_config['xs-tools']
630 # Synchronise the VDIs: this will update the sm_config maps of current records
631 scanrecord = SR.ScanRecord(self)
632 scanrecord.synchronise_new()
633 scanrecord.synchronise_existing()
635 # Everything that looks like an xs-tools ISO but which isn't the
636 # primary one will also be renamed "Old version of ..."
637 sr = self.session.xenapi.SR.get_by_uuid(sr_uuid)
638 all_vdis = self.session.xenapi.VDI.get_all_records_where("field \"SR\" = \"%s\"" % sr)
639 for vdi_ref in all_vdis.keys():
640 vdi = all_vdis[vdi_ref]
641 if 'xs-tools-version' in vdi['sm_config']:
642 name = tools_iso_name(vdi['location'])
643 if 'xs-tools' in vdi['sm_config']:
644 self.session.xenapi.VDI.set_name_label(vdi_ref, name)
645 else:
646 self.session.xenapi.VDI.set_name_label(vdi_ref, "Old version of " + name)
649 # never forget old VDI records to cope with rolling upgrade
650 for location in scanrecord.gone:
651 vdi = scanrecord.get_xenapi_vdi(location)
652 util.SMlog("Marking previous version of tools ISO: location=%s uuid=%s" % (vdi['location'], vdi['uuid']))
653 vdi = self.session.xenapi.VDI.get_by_uuid(vdi['uuid'])
654 name_label = self.session.xenapi.VDI.get_name_label(vdi)
655 if not(name_label.startswith("Old version of ")):
656 self.session.xenapi.VDI.set_name_label(vdi, "Old version of " + name_label)
657 # Mark it as missing for informational purposes only
658 self.session.xenapi.VDI.set_missing(vdi, True)
659 self.session.xenapi.VDI.remove_from_sm_config(vdi, 'xs-tools')
661 else:
662 return super(ISOSR, self).scan(sr_uuid)
664 def create(self, sr_uuid, size):
665 self.attach(sr_uuid)
666 if 'type' in self.dconf:
667 smconfig = self.session.xenapi.SR.get_sm_config(self.sr_ref)
668 smconfig['iso_type'] = self.dconf['type']
669 self.session.xenapi.SR.set_sm_config(self.sr_ref, smconfig)
671 _, num_images_ignored = list_images(self.path)
672 if num_images_ignored > 0:
673 xapi = self.session.xenapi
674 xapi.message.create("DISK_IMAGES_IGNORED",
675 "4",
676 "SR",
677 self.uuid,
678 "Ignored disk image file(s) due to"
679 " file name encoding issues")
681 self.detach(sr_uuid)
684class ISOVDI(VDI.VDI):
685 def load(self, vdi_uuid):
686 # Nb, in the vdi_create call, the filename is unset, so the following
687 # will fail.
688 self.vdi_type = "iso"
689 try:
690 stat = os.stat(self.path)
691 self.utilisation = int(stat.st_size)
692 self.size = int(stat.st_size)
693 self.label = self.filename
694 except:
695 pass
697 def __init__(self, mysr, filename):
698 self.path = os.path.join(mysr.path, filename)
699 VDI.VDI.__init__(self, mysr, None)
700 self.location = filename
701 self.filename = filename
702 self.read_only = True
703 self.label = filename
704 self.sm_config = {}
705 if "legacy_mode" in mysr.dconf:
706 if filename.startswith("xs-tools") or filename.startswith("guest-tools"):
707 self.label = tools_iso_name(filename)
708 # Mark this as a Tools CD
709 # self.sm_config['xs-tools'] = 'true'
710 # Extract a version string, if present
711 vsn = filename[filename.find("tools") + len("tools"):][:-len(".iso")].strip("-").split("-", 1)
712 # "4.1.0"
713 if len(vsn) == 1:
714 build_number = "0" # string
715 product_version = vsn[0]
716 # "4.1.0-1234"
717 elif len(vsn) > 1:
718 build_number = vsn[1]
719 product_version = vsn[0]
720 else:
721 build_number = 0
722 product_version = "unknown"
723 util.SMlog("version=%s build=%s" % (product_version, build_number))
724 self.sm_config['xs-tools-version'] = product_version
725 self.sm_config['xs-tools-build'] = build_number
727 def detach(self, sr_uuid, vdi_uuid):
728 pass
730 def attach(self, sr_uuid, vdi_uuid):
731 try:
732 os.stat(self.path)
733 return super(ISOVDI, self).attach(sr_uuid, vdi_uuid)
734 except:
735 raise xs_errors.XenError('VDIMissing')
737 def create(self, sr_uuid, vdi_uuid, size):
738 self.uuid = vdi_uuid
739 self.path = os.path.join(self.sr.path, self.filename)
740 self.size = size
741 self.utilisation = 0
742 self.read_only = False
743 self.sm_config = self.sr.srcmd.params['vdi_sm_config']
744 self.sm_config['created'] = util._getDateString()
746 if util.pathexists(self.path):
747 raise xs_errors.XenError('VDIExists')
749 try:
750 handle = open(self.path, "w")
751 handle.truncate(size)
752 handle.close()
753 self._db_introduce()
754 return super(ISOVDI, self).get_params()
755 except Exception as exn:
756 util.SMlog("Exception when creating VDI: %s" % exn)
757 raise xs_errors.XenError('VDICreate', \
758 opterr='could not create file: "%s"' % self.path)
760 def delete(self, sr_uuid, vdi_uuid):
761 util.SMlog("Deleting...")
763 self.uuid = vdi_uuid
764 self._db_forget()
766 if not util.pathexists(self.path):
767 return
769 try:
770 util.SMlog("Unlinking...")
771 os.unlink(self.path)
772 util.SMlog("Done...")
773 except:
774 raise xs_errors.XenError('VDIDelete')
776 # delete, update, introduce unimplemented. super class will raise
777 # exceptions
779# SR registration at import
780SR.registerSR(ISOSR)