Hide keyboard shortcuts

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# SMBSR: SMB filesystem based storage repository 

19 

20from sm_typing import override 

21 

22import SR 

23import SRCommand 

24import VDI 

25import FileSR 

26import util 

27import errno 

28import os 

29import xmlrpc.client 

30import xs_errors 

31import lock 

32import cleanup 

33import cifutils 

34 

35CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_CACHING", 

36 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

37 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR", 

38 "VDI_GENERATE_CONFIG", 

39 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

40 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

41 

42CONFIGURATION = [['server', 'Full path to share root on SMB server (required)'], \ 

43 ['username', 'The username to be used during SMB authentication'], \ 

44 ['password', 'The password to be used during SMB authentication']] 

45 

46DRIVER_INFO = { 

47 'name': 'SMB VHD and QCOW2', 

48 'description': 'SR plugin which stores disks as VHD and QCOW2 files on a remote SMB filesystem', 

49 'vendor': 'Citrix Systems Inc', 

50 'copyright': '(C) 2015 Citrix Systems Inc', 

51 'driver_version': '1.0', 

52 'required_api_version': '1.0', 

53 'capabilities': CAPABILITIES, 

54 'configuration': CONFIGURATION 

55 } 

56 

57DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

58 

59# The mountpoint for the directory when performing an sr_probe. All probes 

60# are guaranteed to be serialised by xapi, so this single mountpoint is fine. 

61PROBE_MOUNTPOINT = os.path.join(SR.MOUNT_BASE, "probe") 

62 

63 

64class SMBException(Exception): 

65 def __init__(self, errstr): 

66 self.errstr = errstr 

67 

68 

69# server = //smb-server/vol1 - ie the export path on the SMB server 

70# mountpoint = /var/run/sr-mount/SMB/<smb_server_name>/<share_name>/uuid 

71# linkpath = mountpoint/uuid - path to SR directory on share 

72# path = /var/run/sr-mount/uuid - symlink to SR directory on share 

73class SMBSR(FileSR.SharedFileSR): 

74 """SMB file-based storage repository""" 

75 

76 @override 

77 @staticmethod 

78 def handles(type) -> bool: 

79 return type == 'smb' 

80 

81 @override 

82 def load(self, sr_uuid) -> None: 

83 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

84 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid) 

85 self.sr_vditype = SR.DEFAULT_TAP 

86 self.driver_config = DRIVER_CONFIG 

87 if 'server' not in self.dconf: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true

88 raise xs_errors.XenError('ConfigServerMissing') 

89 self.remoteserver = self.dconf['server'] 

90 if self.sr_ref and self.session is not None: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true

91 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref) 

92 else: 

93 self.sm_config = self.srcmd.params.get('sr_sm_config') or {} 

94 self.mountpoint = os.path.join(SR.MOUNT_BASE, 'SMB', self.__extract_server(), sr_uuid) 

95 self.linkpath = os.path.join(self.mountpoint, 

96 sr_uuid or "") 

97 # Remotepath is the absolute path inside a share that is to be mounted 

98 # For a SMB SR, only the root can be mounted. 

99 self.remotepath = '' 

100 self.path = os.path.join(SR.MOUNT_BASE, sr_uuid) 

101 self._check_o_direct() 

102 

103 def checkmount(self): 

104 return util.ioretry(lambda: ((util.pathexists(self.mountpoint) and \ 

105 util.ismount(self.mountpoint)) and \ 

106 util.pathexists(self.linkpath))) 

107 

108 def makeMountPoint(self, mountpoint): 

109 """Mount the remote SMB export at 'mountpoint'""" 

110 if mountpoint is None: 

111 mountpoint = self.mountpoint 

112 elif not util.is_string(mountpoint) or mountpoint == "": 112 ↛ 115line 112 didn't jump to line 115, because the condition on line 112 was never false

113 raise SMBException("mountpoint not a string object") 

114 

115 try: 

116 if not util.ioretry(lambda: util.isdir(mountpoint)): 116 ↛ 121line 116 didn't jump to line 121, because the condition on line 116 was never false

117 util.ioretry(lambda: util.makedirs(mountpoint)) 

118 except util.CommandException as inst: 

119 raise SMBException("Failed to make directory: code is %d" % 

120 inst.code) 

121 return mountpoint 

122 

123 def mount(self, mountpoint=None): 

124 

125 mountpoint = self.makeMountPoint(mountpoint) 

126 

127 new_env, domain = cifutils.getCIFCredentials(self.dconf, self.session) 

128 

129 options = self.getMountOptions(domain) 

130 if options: 130 ↛ 133line 130 didn't jump to line 133, because the condition on line 130 was never false

131 options = ",".join(str(x) for x in options if x) 

132 

133 try: 

134 

135 util.ioretry(lambda: 

136 util.pread(["mount.cifs", self.remoteserver, 

137 mountpoint, "-o", options], new_env=new_env), 

138 errlist=[errno.EPIPE, errno.EIO], 

139 maxretry=2, nofail=True) 

140 except util.CommandException as inst: 

141 raise SMBException("mount failed with return code %d" % inst.code) 

142 

143 # Sanity check to ensure that the user has at least RO access to the 

144 # mounted share. Windows sharing and security settings can be tricky. 

145 try: 

146 util.listdir(mountpoint) 

147 except util.CommandException: 

148 try: 

149 self.unmount(mountpoint, True) 

150 except SMBException: 

151 util.logException('SMBSR.unmount()') 

152 raise SMBException("Permission denied. " 

153 "Please check user privileges.") 

154 

155 def getMountOptions(self, domain): 

156 """Creates option string based on parameters provided""" 

157 options = ['cache=loose', 

158 'vers=3.0', 

159 'actimeo=0' 

160 ] 

161 

162 if domain: 

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

164 

165 if not cifutils.containsCredentials(self.dconf): 165 ↛ 167line 165 didn't jump to line 167, because the condition on line 165 was never true

166 # No login details provided. 

167 options.append('guest') 

168 

169 return options 

170 

171 def unmount(self, mountpoint, rmmountpoint): 

172 """Unmount the remote SMB export at 'mountpoint'""" 

173 try: 

174 util.pread(["umount", mountpoint]) 

175 except util.CommandException as inst: 

176 raise SMBException("umount failed with return code %d" % inst.code) 

177 

178 if rmmountpoint: 178 ↛ exitline 178 didn't return from function 'unmount', because the condition on line 178 was never false

179 try: 

180 os.rmdir(mountpoint) 

181 except OSError as inst: 

182 raise SMBException("rmdir failed with error '%s'" % inst.strerror) 

183 

184 def __extract_server(self): 

185 return self.remoteserver[2:].replace('\\', '/') 

186 

187 def __check_license(self): 

188 """Raises an exception if SMB is not licensed.""" 

189 if self.session is None: 189 ↛ 190line 189 didn't jump to line 190, because the condition on line 189 was never true

190 raise xs_errors.XenError('NoSMBLicense', 

191 'No session object to talk to XAPI') 

192 restrictions = util.get_pool_restrictions(self.session) 

193 if 'restrict_cifs' in restrictions and \ 193 ↛ 195line 193 didn't jump to line 195, because the condition on line 193 was never true

194 restrictions['restrict_cifs'] == "true": 

195 raise xs_errors.XenError('NoSMBLicense') 

196 

197 @override 

198 def attach(self, sr_uuid) -> None: 

199 if not self.checkmount(): 

200 try: 

201 self.mount() 

202 os.symlink(self.linkpath, self.path) 

203 self._check_writable() 

204 self._check_hardlinks() 

205 except SMBException as exc: 

206 raise xs_errors.XenError('SMBMount', opterr=exc.errstr) 

207 except: 

208 if util.pathexists(self.path): 

209 os.unlink(self.path) 

210 if self.checkmount(): 

211 self.unmount(self.mountpoint, True) 

212 raise 

213 

214 self.attached = True 

215 

216 @override 

217 def probe(self) -> str: 

218 err = "SMBMount" 

219 try: 

220 self.mount(PROBE_MOUNTPOINT) 

221 sr_list = filter(util.match_uuid, util.listdir(PROBE_MOUNTPOINT)) 

222 err = "SMBUnMount" 

223 self.unmount(PROBE_MOUNTPOINT, True) 

224 except SMBException as inst: 

225 # pylint: disable=used-before-assignment 

226 raise xs_errors.XenError(err, opterr=inst.errstr) 

227 except (util.CommandException, xs_errors.XenError): 

228 raise 

229 # Create a dictionary from the SR uuids to feed SRtoXML() 

230 return util.SRtoXML({sr_uuid: {} for sr_uuid in sr_list}) 

231 

232 @override 

233 def detach(self, sr_uuid) -> None: 

234 """Detach the SR: Unmounts and removes the mountpoint""" 

235 if not self.checkmount(): 

236 return 

237 util.SMlog("Aborting GC/coalesce") 

238 cleanup.abort(self.uuid) 

239 

240 # Change directory to avoid unmount conflicts 

241 os.chdir(SR.MOUNT_BASE) 

242 

243 try: 

244 self.unmount(self.mountpoint, True) 

245 os.unlink(self.path) 

246 except SMBException as exc: 

247 raise xs_errors.XenError('SMBUnMount', opterr=exc.errstr) 

248 

249 self.attached = False 

250 

251 @override 

252 def create(self, sr_uuid, size) -> None: 

253 self.__check_license() 

254 

255 if self.checkmount(): 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true

256 raise xs_errors.XenError('SMBAttached') 

257 

258 try: 

259 self.mount() 

260 except SMBException as exc: 

261 try: 

262 os.rmdir(self.mountpoint) 

263 except: 

264 pass 

265 raise xs_errors.XenError('SMBMount', opterr=exc.errstr) 

266 

267 if util.ioretry(lambda: util.pathexists(self.linkpath)): 267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true

268 if len(util.ioretry(lambda: util.listdir(self.linkpath))) != 0: 

269 self.detach(sr_uuid) 

270 raise xs_errors.XenError('SRExists') 

271 else: 

272 try: 

273 util.ioretry(lambda: util.makedirs(self.linkpath)) 

274 os.symlink(self.linkpath, self.path) 

275 except util.CommandException as inst: 

276 if inst.code != errno.EEXIST: 276 ↛ 292line 276 didn't jump to line 292, because the condition on line 276 was never false

277 try: 

278 self.unmount(self.mountpoint, True) 

279 except SMBException: 

280 util.logException('SMBSR.unmount()') 

281 

282 if inst.code in [errno.EROFS, errno.EPERM, errno.EACCES]: 

283 raise xs_errors.XenError( 

284 'SharedFileSystemNoWrite', 

285 opterr='remote filesystem is read-only error is %d' 

286 % inst.code) from inst 

287 else: 

288 raise xs_errors.XenError( 

289 'SMBCreate', 

290 opterr="remote directory creation error: {}" 

291 .format(os.strerror(inst.code))) from inst 

292 self.detach(sr_uuid) 

293 

294 @override 

295 def delete(self, sr_uuid) -> None: 

296 # try to remove/delete non VDI contents first 

297 super(SMBSR, self).delete(sr_uuid) 

298 try: 

299 if self.checkmount(): 

300 self.detach(sr_uuid) 

301 

302 self.mount() 

303 if util.ioretry(lambda: util.pathexists(self.linkpath)): 

304 util.ioretry(lambda: os.rmdir(self.linkpath)) 

305 self.unmount(self.mountpoint, True) 

306 except util.CommandException as inst: 

307 self.detach(sr_uuid) 

308 if inst.code != errno.ENOENT: 

309 raise xs_errors.XenError('SMBDelete') 

310 

311 @override 

312 def vdi(self, uuid) -> VDI.VDI: 

313 return SMBFileVDI(self, uuid) 

314 

315 

316class SMBFileVDI(FileSR.FileVDI): 

317 @override 

318 def attach(self, sr_uuid, vdi_uuid) -> str: 

319 if not hasattr(self, 'xenstore_data'): 

320 self.xenstore_data = {} 

321 

322 self.xenstore_data["storage-type"] = "smb" 

323 

324 return super(SMBFileVDI, self).attach(sr_uuid, vdi_uuid) 

325 

326 @override 

327 def generate_config(self, sr_uuid, vdi_uuid) -> str: 

328 util.SMlog("SMBFileVDI.generate_config") 

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

330 raise xs_errors.XenError('VDIUnavailable') 

331 resp = {} 

332 resp['device_config'] = self.sr.dconf 

333 resp['sr_uuid'] = sr_uuid 

334 resp['vdi_uuid'] = vdi_uuid 

335 resp['sr_sm_config'] = self.sr.sm_config 

336 resp['command'] = 'vdi_attach_from_config' 

337 # Return the 'config' encoded within a normal XMLRPC response so that 

338 # we can use the regular response/error parsing code. 

339 config = xmlrpc.client.dumps(tuple([resp]), "vdi_attach_from_config") 

340 return xmlrpc.client.dumps((config, ), "", True) 

341 

342 @override 

343 def attach_from_config(self, sr_uuid, vdi_uuid) -> str: 

344 """Used for HA State-file only. Will not just attach the VDI but 

345 also start a tapdisk on the file""" 

346 util.SMlog("SMBFileVDI.attach_from_config") 

347 try: 

348 if not util.pathexists(self.sr.path): 

349 return self.sr.attach(sr_uuid) 

350 except: 

351 util.logException("SMBFileVDI.attach_from_config") 

352 raise xs_errors.XenError('SRUnavailable', \ 

353 opterr='Unable to attach from config') 

354 return '' 

355 

356 

357if __name__ == '__main__': 357 ↛ 358line 357 didn't jump to line 358, because the condition on line 357 was never true

358 SRCommand.run(SMBSR, DRIVER_INFO) 

359else: 

360 SR.registerSR(SMBSR) 

361#