Coverage for libs/sm/drivers/SMBSR.py: 60%

207 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2026-07-10 07:59 +0000

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 

20import errno 

21import os 

22import xmlrpc.client 

23 

24from sm import SR 

25from sm import vhdutil 

26from sm import cleanup 

27from sm import cifutils 

28from sm.drivers import FileSR 

29from sm.core import util 

30from sm.core import xs_errors 

31from sm.core.lock import Lock 

32 

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

34 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

35 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR", 

36 "VDI_GENERATE_CONFIG", 

37 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

38 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

39 

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

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

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

43 

44DRIVER_INFO = { 

45 'name': 'SMB VHD', 

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

47 'vendor': 'Citrix Systems Inc', 

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

49 'driver_version': '1.0', 

50 'required_api_version': '1.0', 

51 'capabilities': CAPABILITIES, 

52 'configuration': CONFIGURATION 

53 } 

54 

55DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

56 

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

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

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

60 

61 

62class SMBException(Exception): 

63 def __init__(self, errstr): 

64 self.errstr = errstr 

65 

66 

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

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

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

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

71class SMBSR(FileSR.SharedFileSR): 

72 """SMB file-based storage repository""" 

73 

74 def handles(type): 

75 return type == 'smb' 

76 handles = staticmethod(handles) 

77 

78 def load(self, sr_uuid): 

79 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

80 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid) 

81 self.sr_vditype = SR.DEFAULT_TAP 

82 self.driver_config = DRIVER_CONFIG 

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

84 raise xs_errors.XenError('ConfigServerMissing') 

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

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

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

88 else: 

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

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

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

92 sr_uuid or "") 

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

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

95 self.remotepath = '' 

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

97 self._check_o_direct() 

98 

99 def checkmount(self): 

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

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

102 util.pathexists(self.linkpath))) 

103 

104 def makeMountPoint(self, mountpoint): 

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

106 if mountpoint is None: 

107 mountpoint = self.mountpoint 

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

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

110 

111 try: 

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

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

114 except util.CommandException as inst: 

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

116 inst.code) 

117 return mountpoint 

118 

119 def mount(self, mountpoint=None): 

120 

121 mountpoint = self.makeMountPoint(mountpoint) 

122 

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

124 

125 options = self.getMountOptions(domain) 

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

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

128 

129 try: 

130 

131 util.ioretry(lambda: 

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

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

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

135 maxretry=2, nofail=True) 

136 except util.CommandException as inst: 

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

138 

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

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

141 try: 

142 util.listdir(mountpoint) 

143 except util.CommandException: 

144 try: 

145 self.unmount(mountpoint, True) 

146 except SMBException: 

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

148 raise SMBException("Permission denied. " 

149 "Please check user privileges.") 

150 

151 def getMountOptions(self, domain): 

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

153 options = ['cache=loose', 

154 'vers=3.0', 

155 'actimeo=0' 

156 ] 

157 

158 if domain: 

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

160 

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

162 # No login details provided. 

163 options.append('guest') 

164 

165 return options 

166 

167 def unmount(self, mountpoint, rmmountpoint): 

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

169 try: 

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

171 except util.CommandException as inst: 

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

173 

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

175 try: 

176 os.rmdir(mountpoint) 

177 except OSError as inst: 

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

179 

180 def __extract_server(self): 

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

182 

183 def __check_license(self): 

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

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

186 raise xs_errors.XenError('NoSMBLicense', 

187 'No session object to talk to XAPI') 

188 restrictions = util.get_pool_restrictions(self.session) 

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

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

191 raise xs_errors.XenError('NoSMBLicense') 

192 

193 def attach(self, sr_uuid): 

194 if not self.checkmount(): 

195 try: 

196 self.mount() 

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

198 self._check_writable() 

199 self._check_hardlinks() 

200 except SMBException as exc: 

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

202 except: 

203 if util.pathexists(self.path): 

204 os.unlink(self.path) 

205 if self.checkmount(): 

206 self.unmount(self.mountpoint, True) 

207 raise 

208 

209 self.attached = True 

210 

211 def probe(self): 

212 err = "SMBMount" 

213 try: 

214 self.mount(PROBE_MOUNTPOINT) 

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

216 err = "SMBUnMount" 

217 self.unmount(PROBE_MOUNTPOINT, True) 

218 except SMBException as inst: 

219 # pylint: disable=used-before-assignment 

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

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

222 raise 

223 

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

225 sr_dict = {sr_uuid: {} for sr_uuid in sr_list} 

226 

227 return util.SRtoXML(sr_dict) 

228 

229 def detach(self, sr_uuid): 

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

231 if not self.checkmount(): 

232 return 

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

234 cleanup.abort(self.uuid) 

235 

236 # Change directory to avoid unmount conflicts 

237 os.chdir(SR.MOUNT_BASE) 

238 

239 try: 

240 self.unmount(self.mountpoint, True) 

241 os.unlink(self.path) 

242 except SMBException as exc: 

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

244 

245 self.attached = False 

246 

247 def create(self, sr_uuid, size): 

248 self.__check_license() 

249 

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

251 raise xs_errors.XenError('SMBAttached') 

252 

253 try: 

254 self.mount() 

255 except SMBException as exc: 

256 try: 

257 os.rmdir(self.mountpoint) 

258 except: 

259 pass 

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

261 

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

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

264 self.detach(sr_uuid) 

265 raise xs_errors.XenError('SRExists') 

266 else: 

267 try: 

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

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

270 except util.CommandException as inst: 

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

272 try: 

273 self.unmount(self.mountpoint, True) 

274 except SMBException: 

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

276 

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

278 raise xs_errors.XenError( 

279 'SharedFileSystemNoWrite', 

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

281 % inst.code) from inst 

282 else: 

283 raise xs_errors.XenError( 

284 'SMBCreate', 

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

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

287 self.detach(sr_uuid) 

288 

289 def delete(self, sr_uuid): 

290 # try to remove/delete non VDI contents first 

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

292 try: 

293 if self.checkmount(): 

294 self.detach(sr_uuid) 

295 

296 self.mount() 

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

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

299 self.unmount(self.mountpoint, True) 

300 except util.CommandException as inst: 

301 self.detach(sr_uuid) 

302 if inst.code != errno.ENOENT: 

303 raise xs_errors.XenError('SMBDelete') 

304 

305 def vdi(self, uuid): 

306 return SMBFileVDI(self, uuid) 

307 

308 

309class SMBFileVDI(FileSR.FileVDI): 

310 def attach(self, sr_uuid, vdi_uuid): 

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

312 self.xenstore_data = {} 

313 

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

315 

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

317 

318 def generate_config(self, sr_uuid, vdi_uuid): 

319 util.SMlog("SMBFileVDI.generate_config") 

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

321 raise xs_errors.XenError('VDIUnavailable') 

322 resp = {} 

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

324 resp['sr_uuid'] = sr_uuid 

325 resp['vdi_uuid'] = vdi_uuid 

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

327 resp['command'] = 'vdi_attach_from_config' 

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

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

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

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

332 

333 def attach_from_config(self, sr_uuid, vdi_uuid): 

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

335 also start a tapdisk on the file""" 

336 util.SMlog("SMBFileVDI.attach_from_config") 

337 try: 

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

339 self.sr.attach(sr_uuid) 

340 except: 

341 util.logException("SMBFileVDI.attach_from_config") 

342 raise xs_errors.XenError('SRUnavailable', \ 

343 opterr='Unable to attach from config') 

344 

345 

346# SR registration at import 

347SR.registerSR(SMBSR)