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 

20import SR 

21import SRCommand 

22import FileSR 

23import util 

24import errno 

25import os 

26import xmlrpc.client 

27import xs_errors 

28import vhdutil 

29from lock import Lock 

30import cleanup 

31import cifutils 

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 = /var/run/sr-mount/SMB/<smb_server_name>/<share_name>/uuid 

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

70# path = /var/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 

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

347 SRCommand.run(SMBSR, DRIVER_INFO) 

348else: 

349 SR.registerSR(SMBSR) 

350#