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/env python3 

2# 

3# Original work copyright (C) Citrix systems 

4# Modified work copyright (C) Tappest sp. z o.o., Vates SAS and XCP-ng community 

5# 

6# This program is free software; you can redistribute it and/or modify 

7# it under the terms of the GNU Lesser General Public License as published 

8# by the Free Software Foundation; version 2.1 only. 

9# 

10# This program is distributed in the hope that it will be useful, 

11# but WITHOUT ANY WARRANTY; without even the implied warranty of 

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13# GNU Lesser General Public License for more details. 

14# 

15# You should have received a copy of the GNU Lesser General Public License 

16# along with this program; if not, write to the Free Software Foundation, Inc., 

17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

18# 

19# MooseFSSR: Based on CEPHFSSR and FileSR, mounts MooseFS share 

20 

21from sm_typing import override 

22 

23import errno 

24import os 

25import syslog as _syslog 

26import xmlrpc.client 

27from syslog import syslog 

28 

29# careful with the import order here 

30# FileSR has a circular dependency: 

31# FileSR -> blktap2 -> lvutil -> EXTSR -> FileSR 

32# importing in this order seems to avoid triggering the issue. 

33import SR 

34import SRCommand 

35import FileSR 

36# end of careful 

37import VDI 

38import cleanup 

39import util 

40import vhdutil 

41import xs_errors 

42from lock import Lock 

43 

44CAPABILITIES = ["SR_PROBE", "SR_UPDATE", 

45 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

46 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR", 

47 "VDI_GENERATE_CONFIG", 

48 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE"] 

49 

50CONFIGURATION = [ 

51 ['masterhost', 'MooseFS Master Server hostname or IP address (required, e.g.: "mfsmaster.local.lan" or "10.10.10.1")'], 

52 ['masterport', 'MooseFS Master Server port, default: 9421'], 

53 ['rootpath', 'MooseFS path (required, e.g.: "/")'], 

54 ['options', 'MooseFS Client additional options (e.g.: "mfspassword=PASSWORD,mfstimeout=300")'] 

55] 

56 

57DRIVER_INFO = { 

58 'name': 'MooseFS VHD', 

59 'description': 'SR plugin which stores disks as VHD files on a MooseFS storage', 

60 'vendor': 'Tappest sp. z o.o.', 

61 'copyright': '(C) 2021 Tappest sp. z o.o.', 

62 'driver_version': '1.0', 

63 'required_api_version': '1.0', 

64 'capabilities': CAPABILITIES, 

65 'configuration': CONFIGURATION 

66} 

67 

68DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

69 

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

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

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

73 

74 

75class MooseFSException(Exception): 

76 def __init__(self, errstr): 

77 self.errstr = errstr 

78 

79 

80class MooseFSSR(FileSR.FileSR): 

81 """MooseFS file-based storage""" 

82 

83 DRIVER_TYPE = 'moosefs' 

84 

85 @override 

86 @staticmethod 

87 def handles(sr_type) -> bool: 

88 # fudge, because the parent class (FileSR) checks for smb to alter its behavior 

89 return sr_type == MooseFSSR.DRIVER_TYPE or sr_type == 'smb' 

90 

91 @override 

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

93 if not self._is_moosefs_available(): 93 ↛ 94line 93 didn't jump to line 94, because the condition on line 93 was never true

94 raise xs_errors.XenError( 

95 'SRUnavailable', 

96 opterr='MooseFS Client is not installed!' 

97 ) 

98 

99 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

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

101 self.sr_vditype = SR.DEFAULT_TAP 

102 self.driver_config = DRIVER_CONFIG 

103 if 'masterhost' not in self.dconf: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true

104 raise xs_errors.XenError('ConfigServerMissing') 

105 self.remoteserver = self.dconf['masterhost'] 

106 self.rootpath = self.dconf['rootpath'] 

107 self.remotepath = self.rootpath 

108 # if masterport is not specified, use default: 9421 

109 if 'masterport' not in self.dconf: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false

110 self.remoteport = "9421" 

111 else: 

112 self.remoteport = self.dconf['masterport'] 

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

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

115 else: 

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

117 

118 if self.srcmd.cmd != 'sr_create': 118 ↛ 123line 118 didn't jump to line 123, because the condition on line 118 was never false

119 self.subdir = util.strtobool(self.sm_config.get('subdir')) 

120 if self.subdir: 120 ↛ 121line 120 didn't jump to line 121, because the condition on line 120 was never true

121 self.remotepath = os.path.join(self.remotepath, sr_uuid) 

122 

123 self.attached = False 

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

125 self.mountpoint = self.path 

126 self.linkpath = self.path 

127 self._check_o_direct() 

128 

129 def checkmount(self): 

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

131 util.ismount(self.mountpoint)))) 

132 

133 def mount(self, mountpoint=None): 

134 """Mount MooseFS share at 'mountpoint'""" 

135 if mountpoint is None: 135 ↛ 137line 135 didn't jump to line 137, because the condition on line 135 was never false

136 mountpoint = self.mountpoint 

137 elif not util.is_string(mountpoint) or mountpoint == "": 

138 raise MooseFSException("Mountpoint is not a string object") 

139 

140 try: 

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

142 util.ioretry(lambda: util.makedirs(mountpoint)) 142 ↛ 146line 142 didn't jump to line 146

143 except util.CommandException as inst: 

144 raise MooseFSException("Failed to make directory: code is %d" % inst.code) 

145 

146 try: 

147 options = [] 

148 if 'options' in self.dconf: 

149 options.append(self.dconf['options']) 

150 if options: 

151 options = ['-o', ','.join(options)] 

152 remote = '{}:{}:{}'.format( 

153 self.remoteserver, self.remoteport, self.remotepath 

154 ) 

155 command = ["mount", '-t', 'moosefs', remote, mountpoint] + options 

156 util.ioretry(lambda: util.pread(command), errlist=[errno.EPIPE, errno.EIO], maxretry=2, nofail=True) 

157 except util.CommandException as inst: 

158 syslog(_syslog.LOG_ERR, 'MooseFS mount failed ' + inst.__str__()) 

159 raise MooseFSException("Mount failed with return code %d" % inst.code) 

160 

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

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

163 try: 

164 util.listdir(mountpoint) 

165 except util.CommandException: 

166 try: 

167 self.unmount(mountpoint, True) 

168 except MooseFSException: 

169 util.logException('MooseFSSR.unmount()') 

170 raise MooseFSException("Permission denied. Please check user privileges.") 

171 

172 def unmount(self, mountpoint, rmmountpoint): 

173 try: 

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

175 except util.CommandException as inst: 

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

177 if rmmountpoint: 

178 try: 

179 os.rmdir(mountpoint) 

180 except OSError as inst: 

181 raise MooseFSException("Command rmdir failed with error '%s'" % inst.strerror) 

182 

183 @override 

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

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

186 try: 

187 self.mount() 

188 except MooseFSException as exc: 

189 raise xs_errors.SROSError(12, exc.errstr) 

190 self.attached = True 

191 

192 @override 

193 def probe(self) -> str: 

194 try: 

195 self.mount(PROBE_MOUNTPOINT) 

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

197 self.unmount(PROBE_MOUNTPOINT, True) 

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

199 raise 

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

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

202 

203 @override 

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

205 if not self.checkmount(): 205 ↛ 207line 205 didn't jump to line 207, because the condition on line 205 was never false

206 return 

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

208 cleanup.abort(sr_uuid) 

209 # Change directory to avoid unmount conflicts 

210 os.chdir(SR.MOUNT_BASE) 

211 self.unmount(self.mountpoint, True) 

212 self.attached = False 

213 

214 @override 

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

216 if self.checkmount(): 

217 raise xs_errors.SROSError(113, 'MooseFS mount point already attached') 

218 

219 assert self.remotepath == self.rootpath 

220 try: 

221 self.mount() 

222 except MooseFSException as exc: 

223 # noinspection PyBroadException 

224 try: 

225 os.rmdir(self.mountpoint) 

226 except: 

227 # we have no recovery strategy 

228 pass 

229 raise xs_errors.SROSError(111, "MooseFS mount error [opterr=%s]" % exc.errstr) 

230 

231 try: 

232 self.subdir = self.sm_config.get('subdir') 

233 if self.subdir is None: 

234 self.subdir = True 

235 else: 

236 self.subdir = util.strtobool(self.subdir) 

237 

238 self.sm_config['subdir'] = str(self.subdir) 

239 self.session.xenapi.SR.set_sm_config(self.sr_ref, self.sm_config) 

240 

241 if not self.subdir: 

242 return 

243 

244 subdir = os.path.join(self.mountpoint, sr_uuid) 

245 if util.ioretry(lambda: util.pathexists(subdir)): 

246 if util.ioretry(lambda: util.isdir(subdir)): 

247 raise xs_errors.XenError('SRExists') 

248 else: 

249 try: 

250 util.ioretry(lambda: util.makedirs(subdir)) 

251 except util.CommandException as e: 

252 if e.code != errno.EEXIST: 

253 raise MooseFSException( 

254 'Failed to create SR subdir: {}'.format(e) 

255 ) 

256 finally: 

257 self.detach(sr_uuid) 

258 

259 @override 

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

261 # try to remove/delete non VDI contents first 

262 super(MooseFSSR, self).delete(sr_uuid) 

263 try: 

264 if self.checkmount(): 

265 self.detach(sr_uuid) 

266 

267 if self.subdir: 

268 # Mount using rootpath (<root>) instead of <root>/<sr_uuid>. 

269 self.remotepath = self.rootpath 

270 self.attach(sr_uuid) 

271 subdir = os.path.join(self.mountpoint, sr_uuid) 

272 if util.ioretry(lambda: util.pathexists(subdir)): 

273 util.ioretry(lambda: os.rmdir(subdir)) 

274 self.detach(sr_uuid) 

275 except util.CommandException as inst: 

276 self.detach(sr_uuid) 

277 if inst.code != errno.ENOENT: 

278 raise xs_errors.SROSError(114, "Failed to remove MooseFS mount point") 

279 

280 @override 

281 def vdi(self, uuid, loadLocked=False) -> VDI.VDI: 

282 return MooseFSFileVDI(self, uuid) 

283 

284 @staticmethod 

285 def _is_moosefs_available(): 

286 return util.find_executable('mfsmount') 

287 

288class MooseFSFileVDI(FileSR.FileVDI): 

289 @override 

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

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

292 self.xenstore_data = {} 

293 

294 self.xenstore_data['storage-type'] = MooseFSSR.DRIVER_TYPE 

295 

296 return super(MooseFSFileVDI, self).attach(sr_uuid, vdi_uuid) 

297 

298 @override 

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

300 util.SMlog("MooseFSFileVDI.generate_config") 

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

302 raise xs_errors.XenError('VDIUnavailable') 

303 resp = {'device_config': self.sr.dconf, 

304 'sr_uuid': sr_uuid, 

305 'vdi_uuid': vdi_uuid, 

306 'sr_sm_config': self.sr.sm_config, 

307 'command': 'vdi_attach_from_config'} 

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

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

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

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

312 

313 @override 

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

315 try: 

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

317 return self.sr.attach(sr_uuid) 

318 except: 

319 util.logException("MooseFSFileVDI.attach_from_config") 

320 raise xs_errors.XenError('SRUnavailable', 

321 opterr='Unable to attach from config') 

322 return '' 

323 

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

325 SRCommand.run(MooseFSSR, DRIVER_INFO) 

326else: 

327 SR.registerSR(MooseFSSR)