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 vhdutil 

32from lock import Lock 

33import cleanup 

34import cifutils 

35 

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

37 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

38 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR", 

39 "VDI_GENERATE_CONFIG", 

40 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

41 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

42 

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

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

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

46 

47DRIVER_INFO = { 

48 'name': 'SMB VHD', 

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

50 'vendor': 'Citrix Systems Inc', 

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

52 'driver_version': '1.0', 

53 'required_api_version': '1.0', 

54 'capabilities': CAPABILITIES, 

55 'configuration': CONFIGURATION 

56 } 

57 

58DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

59 

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

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

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

63 

64 

65class SMBException(Exception): 

66 def __init__(self, errstr): 

67 self.errstr = errstr 

68 

69 

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

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

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

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

74class SMBSR(FileSR.SharedFileSR): 

75 """SMB file-based storage repository""" 

76 

77 @override 

78 @staticmethod 

79 def handles(type) -> bool: 

80 return type == 'smb' 

81 

82 @override 

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

84 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

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

86 self.sr_vditype = SR.DEFAULT_TAP 

87 self.driver_config = DRIVER_CONFIG 

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

89 raise xs_errors.XenError('ConfigServerMissing') 

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

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

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

93 else: 

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

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

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

97 sr_uuid or "") 

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

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

100 self.remotepath = '' 

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

102 self._check_o_direct() 

103 

104 def checkmount(self): 

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

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

107 util.pathexists(self.linkpath))) 

108 

109 def makeMountPoint(self, mountpoint): 

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

111 if mountpoint is None: 

112 mountpoint = self.mountpoint 

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

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

115 

116 try: 

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

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

119 except util.CommandException as inst: 

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

121 inst.code) 

122 return mountpoint 

123 

124 def mount(self, mountpoint=None): 

125 

126 mountpoint = self.makeMountPoint(mountpoint) 

127 

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

129 

130 options = self.getMountOptions(domain) 

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

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

133 

134 try: 

135 

136 util.ioretry(lambda: 

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

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

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

140 maxretry=2, nofail=True) 

141 except util.CommandException as inst: 

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

143 

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

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

146 try: 

147 util.listdir(mountpoint) 

148 except util.CommandException: 

149 try: 

150 self.unmount(mountpoint, True) 

151 except SMBException: 

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

153 raise SMBException("Permission denied. " 

154 "Please check user privileges.") 

155 

156 def getMountOptions(self, domain): 

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

158 options = ['cache=loose', 

159 'vers=3.0', 

160 'actimeo=0' 

161 ] 

162 

163 if domain: 

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

165 

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

167 # No login details provided. 

168 options.append('guest') 

169 

170 return options 

171 

172 def unmount(self, mountpoint, rmmountpoint): 

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

174 try: 

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

176 except util.CommandException as inst: 

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

178 

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

180 try: 

181 os.rmdir(mountpoint) 

182 except OSError as inst: 

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

184 

185 def __extract_server(self): 

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

187 

188 def __check_license(self): 

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

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

191 raise xs_errors.XenError('NoSMBLicense', 

192 'No session object to talk to XAPI') 

193 restrictions = util.get_pool_restrictions(self.session) 

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

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

196 raise xs_errors.XenError('NoSMBLicense') 

197 

198 @override 

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

200 if not self.checkmount(): 

201 try: 

202 self.mount() 

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

204 self._check_writable() 

205 self._check_hardlinks() 

206 except SMBException as exc: 

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

208 except: 

209 if util.pathexists(self.path): 

210 os.unlink(self.path) 

211 if self.checkmount(): 

212 self.unmount(self.mountpoint, True) 

213 raise 

214 

215 self.attached = True 

216 

217 @override 

218 def probe(self) -> str: 

219 err = "SMBMount" 

220 try: 

221 self.mount(PROBE_MOUNTPOINT) 

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

223 err = "SMBUnMount" 

224 self.unmount(PROBE_MOUNTPOINT, True) 

225 except SMBException as inst: 

226 # pylint: disable=used-before-assignment 

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

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

229 raise 

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

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

232 

233 @override 

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

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

236 if not self.checkmount(): 

237 return 

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

239 cleanup.abort(self.uuid) 

240 

241 # Change directory to avoid unmount conflicts 

242 os.chdir(SR.MOUNT_BASE) 

243 

244 try: 

245 self.unmount(self.mountpoint, True) 

246 os.unlink(self.path) 

247 except SMBException as exc: 

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

249 

250 self.attached = False 

251 

252 @override 

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

254 self.__check_license() 

255 

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

257 raise xs_errors.XenError('SMBAttached') 

258 

259 try: 

260 self.mount() 

261 except SMBException as exc: 

262 try: 

263 os.rmdir(self.mountpoint) 

264 except: 

265 pass 

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

267 

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

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

270 self.detach(sr_uuid) 

271 raise xs_errors.XenError('SRExists') 

272 else: 

273 try: 

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

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

276 except util.CommandException as inst: 

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

278 try: 

279 self.unmount(self.mountpoint, True) 

280 except SMBException: 

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

282 

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

284 raise xs_errors.XenError( 

285 'SharedFileSystemNoWrite', 

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

287 % inst.code) from inst 

288 else: 

289 raise xs_errors.XenError( 

290 'SMBCreate', 

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

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

293 self.detach(sr_uuid) 

294 

295 @override 

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

297 # try to remove/delete non VDI contents first 

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

299 try: 

300 if self.checkmount(): 

301 self.detach(sr_uuid) 

302 

303 self.mount() 

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

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

306 self.unmount(self.mountpoint, True) 

307 except util.CommandException as inst: 

308 self.detach(sr_uuid) 

309 if inst.code != errno.ENOENT: 

310 raise xs_errors.XenError('SMBDelete') 

311 

312 @override 

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

314 return SMBFileVDI(self, uuid) 

315 

316 

317class SMBFileVDI(FileSR.FileVDI): 

318 @override 

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

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

321 self.xenstore_data = {} 

322 

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

324 

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

326 

327 @override 

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

329 util.SMlog("SMBFileVDI.generate_config") 

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

331 raise xs_errors.XenError('VDIUnavailable') 

332 resp = {} 

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

334 resp['sr_uuid'] = sr_uuid 

335 resp['vdi_uuid'] = vdi_uuid 

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

337 resp['command'] = 'vdi_attach_from_config' 

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

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

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

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

342 

343 @override 

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

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

346 also start a tapdisk on the file""" 

347 util.SMlog("SMBFileVDI.attach_from_config") 

348 try: 

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

350 return self.sr.attach(sr_uuid) 

351 except: 

352 util.logException("SMBFileVDI.attach_from_config") 

353 raise xs_errors.XenError('SRUnavailable', \ 

354 opterr='Unable to attach from config') 

355 return '' 

356 

357 

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

359 SRCommand.run(SMBSR, DRIVER_INFO) 

360else: 

361 SR.registerSR(SMBSR) 

362#