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