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

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 

18 

19import os 

20import re 

21 

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 

28 

29CAPABILITIES = ["VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

30 "SR_SCAN", "SR_ATTACH", "SR_DETACH"] 

31 

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] 

43 

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 } 

54 

55TYPE = "iso" 

56SMB_VERSION_1 = '1.0' 

57SMB_VERSION_3 = '3.0' 

58NFSPORT = 2049 

59 

60 

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 

68 

69 regex = re.compile(r"\.iso$|\.img$", re.I) 

70 images = [] 

71 num_images_ignored = 0 

72 

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)) 

94 

95 return images, num_images_ignored 

96 

97 

98def loggable_filename(filename): 

99 # Strip the 'b"' and '"' off the string representation of bytes 

100 return str(os.fsencode(filename))[2:-1] 

101 

102 

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). 

110 

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. 

136 

137 filename_bytes = os.fsencode(filename) 

138 return filename == filename_bytes.decode("utf-8") 

139 

140 

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" 

148 

149 

150class ISOSR(SR.SR): 

151 """Local file storage repository""" 

152 

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 

163 

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] 

183 

184 try: 

185 util._convertDNS(tgt) 

186 except: 

187 raise xs_errors.XenError('DNSError') 

188 

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) 

192 

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 

198 

199 image_names, _ = list_images(self.path) 

200 

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) 

208 

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 

221 

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) 

229 

230 def content_type(self, sr_uuid): 

231 """Returns the content_type XML""" 

232 return super(ISOSR, self).content_type(sr_uuid) 

233 

234 # pylint: disable=no-member 

235 vdi_path_regex = re.compile(r"[a-z0-9.-]+\.(iso|img)", re.I) 

236 

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.""" 

240 

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) 

255 

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 

266 

267 return ISOVDI(self, filename) 

268 

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') 

274 

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) 

282 

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 

291 

292 # Handle optional dconf attributes 

293 self.nfsversion = nfs.validate_nfsversion(self.dconf.get('nfsversion')) 

294 

295 # Fill the required SMB version 

296 self.smbversion = SMB_VERSION_3 

297 

298 # Check if smb version is specified from client 

299 self.is_smbversion_specified = False 

300 

301 # Some info we need: 

302 self.sr_vditype = 'phy' 

303 

304 def delete(self, sr_uuid): 

305 pass 

306 

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 

315 

316 # Check whether we're already mounted 

317 if self._checkmount(): 

318 return 

319 

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' 

328 

329 if protocol == 'nfs_iso': 

330 self._check_nfs_server(location) 

331 

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) 

335 

336 mountcmd = [] 

337 options = '' 

338 

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) 

345 

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') 

381 

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)) 

434 

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') 

439 

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(':') 

456 

457 return serv_path[0], serv_path[1], transport 

458 

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) 

467 

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)) 

475 

476 

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 

484 

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 

496 

497 def getSMBVersion(self): 

498 """Pass smb version option to mount.cifs""" 

499 smbversion = "vers=%s" % self.smbversion 

500 return smbversion 

501 

502 def mountOverSMB(self, mountcmd): 

503 """This function raises util.CommandException""" 

504 new_env, domain = cifutils.getCIFCredentials(self.dconf, self.session, 

505 prefix="cifs") 

506 

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 

517 

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') 

528 

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 

537 

538 return nfsOptions 

539 

540 def appendCIFSMountOptions(self, mountcmd): 

541 """Append options to mount.cifs""" 

542 options = [] 

543 try: 

544 options.append(self.getCacheOptions()) 

545 

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']) 

551 

552 options.append(self.getSMBVersion()) 

553 

554 if domain: 

555 options.append('domain=' + domain) 

556 except: 

557 util.SMlog("Exception while attempting to append mount options") 

558 raise 

559 

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]) 

564 

565 def getCacheOptions(self): 

566 """Pass cache options to mount.cifs""" 

567 return "cache=none" 

568 

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 

574 

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) 

580 

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) 

586 

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) 

590 

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 

597 

598 self.other_config = self.session.xenapi.SR.get_other_config(self.sr_ref) 

599 

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. 

604 

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] 

610 

611 if latest_build_vdi is None: 

612 latest_build_vdi = vdi.location 

613 latest_build_number = "0" 

614 

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 

620 

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'] 

629 

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() 

634 

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) 

647 

648 

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') 

660 

661 else: 

662 return super(ISOSR, self).scan(sr_uuid) 

663 

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) 

670 

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") 

680 

681 self.detach(sr_uuid) 

682 

683 

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 

696 

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 

726 

727 def detach(self, sr_uuid, vdi_uuid): 

728 pass 

729 

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') 

736 

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() 

745 

746 if util.pathexists(self.path): 

747 raise xs_errors.XenError('VDIExists') 

748 

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) 

759 

760 def delete(self, sr_uuid, vdi_uuid): 

761 util.SMlog("Deleting...") 

762 

763 self.uuid = vdi_uuid 

764 self._db_forget() 

765 

766 if not util.pathexists(self.path): 

767 return 

768 

769 try: 

770 util.SMlog("Unlinking...") 

771 os.unlink(self.path) 

772 util.SMlog("Done...") 

773 except: 

774 raise xs_errors.XenError('VDIDelete') 

775 

776 # delete, update, introduce unimplemented. super class will raise 

777 # exceptions 

778 

779# SR registration at import 

780SR.registerSR(ISOSR)