Coverage for drivers/ISOSR.py : 47%

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