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) 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 

19from sm_typing import override 

20 

21import errno 

22import os 

23import syslog as _syslog 

24import xmlrpc.client 

25from syslog import syslog 

26 

27# careful with the import order here 

28# FileSR has a circular dependency: FileSR- > blktap2 -> lvutil -> EXTSR -> FileSR 

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

30import SR 

31import SRCommand 

32import FileSR 

33# end of careful 

34import VDI 

35import cleanup 

36import util 

37import vhdutil 

38import xs_errors 

39from lock import Lock 

40 

41CAPABILITIES = ["SR_PROBE", "SR_UPDATE", 

42 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

43 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR", 

44 "VDI_GENERATE_CONFIG", 

45 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE"] 

46 

47CONFIGURATION = [['server', 'Full path to share on gluster server (required, ex: "192.168.0.12:/gv0")'], 

48 ['backupservers', 'list of servers separated by ":"'], 

49 ['fetchattempts', 'number of attempts to fetch files before switching to the backup server'] 

50 ] 

51 

52DRIVER_INFO = { 

53 'name': 'GlusterFS VHD', 

54 'description': 'SR plugin which stores disks as VHD files on a GlusterFS storage', 

55 'vendor': 'Vates SAS', 

56 'copyright': '(C) 2020 Vates SAS', 

57 'driver_version': '1.0', 

58 'required_api_version': '1.0', 

59 'capabilities': CAPABILITIES, 

60 'configuration': CONFIGURATION 

61} 

62 

63DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

64 

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

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

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

68 

69 

70class GlusterFSException(Exception): 

71 def __init__(self, errstr): 

72 self.errstr = errstr 

73 

74 

75# mountpoint = /var/run/sr-mount/GlusterFS/<glusterfs_server_name>/uuid 

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

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

78class GlusterFSSR(FileSR.FileSR): 

79 """Gluster file-based storage repository""" 

80 

81 DRIVER_TYPE = 'glusterfs' 

82 

83 @override 

84 @staticmethod 

85 def handles(sr_type) -> bool: 

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

87 return sr_type == GlusterFSSR.DRIVER_TYPE or sr_type == 'smb' 

88 

89 @override 

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

91 if not self._is_glusterfs_available(): 

92 raise xs_errors.XenError( 

93 'SRUnavailable', 

94 opterr='glusterfs is not installed' 

95 ) 

96 

97 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

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

99 self.sr_vditype = SR.DEFAULT_TAP 

100 self.driver_config = DRIVER_CONFIG 

101 if 'server' not in self.dconf: 

102 raise xs_errors.XenError('ConfigServerMissing') 

103 # Can be None => on-slave plugin hack (is_open function). 

104 self.remoteserver = self.dconf['server'] or '' 

105 if self.sr_ref and self.session is not None: 

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

107 else: 

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

109 self.mountpoint = os.path.join(SR.MOUNT_BASE, 'GlusterFS', self.remoteserver.split(':')[0], sr_uuid) 

110 self.linkpath = os.path.join(self.mountpoint, sr_uuid or "") 

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

112 self._check_o_direct() 

113 

114 def checkmount(self): 

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

116 util.ismount(self.mountpoint)) and 

117 util.pathexists(self.linkpath))) 

118 

119 def mount(self, mountpoint=None): 

120 """Mount the remote gluster export at 'mountpoint'""" 

121 if mountpoint is None: 

122 mountpoint = self.mountpoint 

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

124 raise GlusterFSException("mountpoint not a string object") 

125 

126 try: 

127 if not util.ioretry(lambda: util.isdir(mountpoint)): 

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

129 except util.CommandException as inst: 

130 raise GlusterFSException("Failed to make directory: code is %d" % inst.code) 

131 try: 

132 options = [] 

133 if 'backupservers' in self.dconf: 

134 options.append('backup-volfile-servers=' + self.dconf['backupservers']) 

135 if 'fetchattempts' in self.dconf: 

136 options.append('fetch-attempts=' + self.dconf['fetchattempts']) 

137 if options: 

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

139 command = ["mount", '-t', 'glusterfs', self.remoteserver, mountpoint] + options 

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

141 except util.CommandException as inst: 

142 syslog(_syslog.LOG_ERR, 'GlusterFS mount failed ' + inst.__str__()) 

143 raise GlusterFSException("mount failed with return code %d" % inst.code) 

144 

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

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

147 try: 

148 util.listdir(mountpoint) 

149 except util.CommandException: 

150 try: 

151 self.unmount(mountpoint, True) 

152 except GlusterFSException: 

153 util.logException('GlusterFSSR.unmount()') 

154 raise GlusterFSException("Permission denied. Please check user privileges.") 

155 

156 def unmount(self, mountpoint, rmmountpoint): 

157 try: 

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

159 except util.CommandException as inst: 

160 raise GlusterFSException("umount failed with return code %d" % inst.code) 

161 if rmmountpoint: 

162 try: 

163 os.rmdir(mountpoint) 

164 except OSError as inst: 

165 raise GlusterFSException("rmdir failed with error '%s'" % inst.strerror) 

166 

167 @override 

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

169 if not self.checkmount(): 

170 try: 

171 self.mount() 

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

173 except GlusterFSException as exc: 

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

175 self.attached = True 

176 

177 @override 

178 def probe(self) -> str: 

179 try: 

180 self.mount(PROBE_MOUNTPOINT) 

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

182 self.unmount(PROBE_MOUNTPOINT, True) 

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

184 raise 

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

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

187 

188 @override 

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

190 if not self.checkmount(): 

191 return 

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

193 cleanup.abort(self.uuid) 

194 # Change directory to avoid unmount conflicts 

195 os.chdir(SR.MOUNT_BASE) 

196 self.unmount(self.mountpoint, True) 

197 os.unlink(self.path) 

198 self.attached = False 

199 

200 @override 

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

202 if self.checkmount(): 

203 raise xs_errors.SROSError(113, 'GlusterFS mount point already attached') 

204 

205 try: 

206 self.mount() 

207 except GlusterFSException as exc: 

208 # noinspection PyBroadException 

209 try: 

210 os.rmdir(self.mountpoint) 

211 except: 

212 # we have no recovery strategy 

213 pass 

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

215 

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

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

218 self.detach(sr_uuid) 

219 raise xs_errors.XenError('SRExists') 

220 else: 

221 try: 

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

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

224 except util.CommandException as inst: 

225 if inst.code != errno.EEXIST: 

226 try: 

227 self.unmount(self.mountpoint, True) 

228 except GlusterFSException: 

229 util.logException('GlusterFSSR.unmount()') 

230 raise xs_errors.SROSError(116, 

231 "Failed to create GlusterFS SR. remote directory creation error: {}".format( 

232 os.strerror(inst.code))) 

233 self.detach(sr_uuid) 

234 

235 @override 

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

237 # try to remove/delete non VDI contents first 

238 super(GlusterFSSR, self).delete(sr_uuid) 

239 try: 

240 if self.checkmount(): 

241 self.detach(sr_uuid) 

242 self.mount() 

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

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

245 self.unmount(self.mountpoint, True) 

246 except util.CommandException as inst: 

247 self.detach(sr_uuid) 

248 if inst.code != errno.ENOENT: 

249 raise xs_errors.SROSError(114, "Failed to remove GlusterFS mount point") 

250 

251 @override 

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

253 return GlusterFSFileVDI(self, uuid) 

254 

255 @staticmethod 

256 def _is_glusterfs_available(): 

257 return util.find_executable('glusterfs') 

258 

259 

260class GlusterFSFileVDI(FileSR.FileVDI): 

261 @override 

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

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

264 self.xenstore_data = {} 

265 

266 self.xenstore_data['storage-type'] = GlusterFSSR.DRIVER_TYPE 

267 

268 return super(GlusterFSFileVDI, self).attach(sr_uuid, vdi_uuid) 

269 

270 @override 

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

272 util.SMlog("SMBFileVDI.generate_config") 

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

274 raise xs_errors.XenError('VDIUnavailable') 

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

276 'sr_uuid': sr_uuid, 

277 'vdi_uuid': vdi_uuid, 

278 'sr_sm_config': self.sr.sm_config, 

279 'command': 'vdi_attach_from_config'} 

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

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

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

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

284 

285 @override 

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

287 try: 

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

289 return self.sr.attach(sr_uuid) 

290 except: 

291 util.logException("SMBFileVDI.attach_from_config") 

292 raise xs_errors.XenError('SRUnavailable', 

293 opterr='Unable to attach from config') 

294 return '' 

295 

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

297 SRCommand.run(GlusterFSSR, DRIVER_INFO) 

298else: 

299 SR.registerSR(GlusterFSSR)