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# FileSR: local-file storage repository 

19 

20from sm_typing import override 

21 

22import socket 

23 

24import SR 

25import VDI 

26import SRCommand 

27import FileSR 

28import util 

29import errno 

30import os 

31import sys 

32import xmlrpc.client 

33import xs_errors 

34import lock 

35import nfs 

36import cleanup 

37 

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

39 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

40 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", 

41 "VDI_GENERATE_CONFIG", "VDI_MIRROR", 

42 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

43 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

44 

45CONFIGURATION = [['server', 'hostname or IP address of NFS server (required)'], 

46 ['serverpath', 'path on remote server (required)'], 

47 nfs.NFS_VERSION] 

48 

49DRIVER_INFO = { 

50 'name': 'NFS VHD and QCOW2', 

51 'description': 'SR plugin which stores disks as VHD and QCOW2 files on a remote NFS filesystem', 

52 'vendor': 'Citrix Systems Inc', 

53 'copyright': '(C) 2008 Citrix Systems Inc', 

54 'driver_version': '1.0', 

55 'required_api_version': '1.0', 

56 'capabilities': CAPABILITIES, 

57 'configuration': CONFIGURATION 

58 } 

59 

60DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

61 

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

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

64PROBE_MOUNTPOINT = "probe" 

65NFSPORT = 2049 

66DEFAULT_TRANSPORT = "tcp" 

67PROBEVERSION = 'probeversion' 

68 

69 

70class NFSSR(FileSR.SharedFileSR): 

71 """NFS file-based storage repository""" 

72 

73 @override 

74 @staticmethod 

75 def handles(type) -> bool: 

76 return type == 'nfs' 

77 

78 @override 

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

80 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

81 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid) 

82 self.sr_vditype = SR.DEFAULT_TAP 

83 self.driver_config = DRIVER_CONFIG 

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

85 raise xs_errors.XenError('ConfigServerMissing') 

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

87 self.nosubdir = False 

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

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

90 self.other_config = self.session.xenapi.SR.get_other_config(self.sr_ref) 

91 else: 

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

93 self.other_config = self.srcmd.params.get('sr_other_config') or {} 

94 self.nosubdir = self.sm_config.get('nosubdir') == "true" 

95 serverpath = self.dconf.get('serverpath') 

96 if serverpath is not None: 96 ↛ 101line 96 didn't jump to line 101, because the condition on line 96 was never false

97 self.remotepath = os.path.join( 

98 serverpath, 

99 not self.nosubdir and sr_uuid or "" 

100 ) 

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

102 

103 # Handle optional dconf attributes 

104 self.set_transport() 

105 self.nfsversion = nfs.validate_nfsversion(self.dconf.get('nfsversion')) 

106 if 'options' in self.dconf: 

107 self.options = self.dconf['options'] 

108 else: 

109 self.options = '' 

110 

111 def validate_remotepath(self, scan): 

112 serverpath = self.dconf.get('serverpath') 

113 if serverpath is None: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true

114 if scan: 

115 try: 

116 self.scan_exports(self.dconf['server']) 

117 except: 

118 pass 

119 raise xs_errors.XenError('ConfigServerPathMissing') 

120 

121 def check_server(self): 

122 try: 

123 if PROBEVERSION in self.dconf: 123 ↛ 124line 123 didn't jump to line 124, because the condition on line 123 was never true

124 sv = nfs.get_supported_nfs_versions(self.remoteserver, self.transport) 

125 if len(sv): 

126 self.nfsversion = sv[0] 

127 else: 

128 if not nfs.check_server_tcp(self.remoteserver, self.transport, self.nfsversion): 128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true

129 raise nfs.NfsException("Unsupported NFS version: %s" % self.nfsversion) 

130 

131 except nfs.NfsException as exc: 

132 raise xs_errors.XenError('NFSVersion', 

133 opterr=exc.errstr) 

134 

135 def mount(self, mountpoint, remotepath, timeout=None, retrans=None): 

136 try: 

137 nfs.soft_mount( 

138 mountpoint, self.remoteserver, remotepath, self.transport, 

139 useroptions=self.options, timeout=timeout, 

140 nfsversion=self.nfsversion, retrans=retrans) 

141 except nfs.NfsException as exc: 

142 raise xs_errors.XenError('NFSMount', opterr=exc.errstr) 

143 

144 @override 

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

146 if not self._checkmount(): 

147 try: 

148 self.validate_remotepath(False) 

149 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget') 

150 self.mount_remotepath(sr_uuid) 

151 self._check_writable() 

152 self._check_hardlinks() 

153 except: 

154 if self._checkmount(): 

155 nfs.unmount(self.path, True) 

156 raise 

157 self.attached = True 

158 

159 def mount_remotepath(self, sr_uuid): 

160 if not self._checkmount(): 160 ↛ exitline 160 didn't return from function 'mount_remotepath', because the condition on line 160 was never false

161 # FIXME: What is the purpose of this check_server? 

162 # It doesn't stop us from continuing if the server 

163 # doesn't support the requested version. We fail 

164 # in mount instead 

165 self.check_server() 

166 # Extract timeout and retrans values, if any 

167 io_timeout = nfs.get_nfs_timeout(self.other_config) 

168 io_retrans = nfs.get_nfs_retrans(self.other_config) 

169 self.mount(self.path, self.remotepath, 

170 timeout=io_timeout, retrans=io_retrans) 

171 

172 @override 

173 def probe(self) -> str: 

174 # Verify NFS target and port 

175 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget') 

176 

177 self.validate_remotepath(True) 

178 self.check_server() 

179 

180 temppath = os.path.join(SR.MOUNT_BASE, PROBE_MOUNTPOINT) 

181 

182 self.mount(temppath, self.remotepath) 

183 try: 

184 return nfs.scan_srlist(temppath, self.transport, self.dconf) 

185 finally: 

186 try: 

187 nfs.unmount(temppath, True) 

188 except: 

189 pass 

190 

191 @override 

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

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

194 if not self._checkmount(): 194 ↛ 196line 194 didn't jump to line 196, because the condition on line 194 was never false

195 return 

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

197 cleanup.abort(self.uuid) 

198 

199 # Change directory to avoid unmount conflicts 

200 os.chdir(SR.MOUNT_BASE) 

201 

202 try: 

203 nfs.unmount(self.path, True) 

204 except nfs.NfsException as exc: 

205 raise xs_errors.XenError('NFSUnMount', opterr=exc.errstr) 

206 

207 self.attached = False 

208 

209 @override 

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

211 util._testHost(self.dconf['server'], NFSPORT, 'NFSTarget') 

212 self.validate_remotepath(True) 

213 if self._checkmount(): 213 ↛ 214line 213 didn't jump to line 214, because the condition on line 213 was never true

214 raise xs_errors.XenError('NFSAttached') 

215 

216 # Set the target path temporarily to the base dir 

217 # so that we can create the target SR directory 

218 self.remotepath = self.dconf['serverpath'] 

219 try: 

220 self.mount_remotepath(sr_uuid) 

221 except Exception as exn: 

222 try: 

223 os.rmdir(self.path) 

224 except: 

225 pass 

226 raise 

227 

228 if not self.nosubdir: 228 ↛ 248line 228 didn't jump to line 248, because the condition on line 228 was never false

229 newpath = os.path.join(self.path, sr_uuid) 

230 if util.ioretry(lambda: util.pathexists(newpath)): 230 ↛ 231line 230 didn't jump to line 231, because the condition on line 230 was never true

231 if len(util.ioretry(lambda: util.listdir(newpath))) != 0: 

232 self.detach(sr_uuid) 

233 raise xs_errors.XenError('SRExists') 

234 else: 

235 try: 

236 util.ioretry(lambda: util.makedirs(newpath)) 

237 except util.CommandException as inst: 

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

239 self.detach(sr_uuid) 

240 if inst.code == errno.EROFS: 

241 raise xs_errors.XenError('SharedFileSystemNoWrite', 

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

243 % inst.code) 

244 else: 

245 raise xs_errors.XenError('NFSCreate', 

246 opterr='remote directory creation error is %d' 

247 % inst.code) 

248 self.detach(sr_uuid) 

249 

250 @override 

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

252 # try to remove/delete non VDI contents first 

253 super(NFSSR, self).delete(sr_uuid) 

254 try: 

255 if self._checkmount(): 

256 self.detach(sr_uuid) 

257 

258 # Set the target path temporarily to the base dir 

259 # so that we can remove the target SR directory 

260 self.remotepath = self.dconf['serverpath'] 

261 self.mount_remotepath(sr_uuid) 

262 if not self.nosubdir: 

263 newpath = os.path.join(self.path, sr_uuid) 

264 if util.ioretry(lambda: util.pathexists(newpath)): 

265 util.ioretry(lambda: os.rmdir(newpath)) 

266 self.detach(sr_uuid) 

267 except util.CommandException as inst: 

268 self.detach(sr_uuid) 

269 if inst.code != errno.ENOENT: 

270 raise xs_errors.XenError('NFSDelete') 

271 

272 @override 

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

274 return NFSFileVDI(self, uuid) 

275 

276 def scan_exports(self, target): 

277 util.SMlog("scanning2 (target=%s)" % target) 

278 dom = nfs.scan_exports(target, self.transport) 

279 print(dom.toprettyxml(), file=sys.stderr) 

280 

281 def set_transport(self): 

282 self.transport = DEFAULT_TRANSPORT 

283 if self.remoteserver is None: 

284 # CA-365359: on_slave.is_open sends a dconf with {"server": None} 

285 return 

286 

287 try: 

288 addr_info = socket.getaddrinfo(self.remoteserver, 0)[0] 

289 except Exception: 

290 return 

291 

292 use_ipv6 = addr_info[0] == socket.AF_INET6 

293 if use_ipv6: 293 ↛ 295line 293 didn't jump to line 295, because the condition on line 293 was never false

294 self.transport = 'tcp6' 

295 if 'useUDP' in self.dconf and self.dconf['useUDP'] == 'true': 295 ↛ 296line 295 didn't jump to line 296, because the condition on line 295 was never true

296 self.transport = 'udp6' if use_ipv6 else 'udp' 

297 

298 

299class NFSFileVDI(FileSR.FileVDI): 

300 @override 

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

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

303 self.xenstore_data = {} 

304 

305 self.xenstore_data["storage-type"] = "nfs" 

306 

307 return super(NFSFileVDI, self).attach(sr_uuid, vdi_uuid) 

308 

309 @override 

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

311 util.SMlog("NFSFileVDI.generate_config") 

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

313 raise xs_errors.XenError('VDIUnavailable') 

314 resp = {} 

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

316 resp['sr_uuid'] = sr_uuid 

317 resp['vdi_uuid'] = vdi_uuid 

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

319 resp['sr_other_config'] = self.sr.other_config 

320 resp['command'] = 'vdi_attach_from_config' 

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

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

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

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

325 

326 @override 

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

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

329 also start a tapdisk on the file""" 

330 util.SMlog("NFSFileVDI.attach_from_config") 

331 try: 

332 return self.sr.attach(sr_uuid) 

333 except: 

334 util.logException("NFSFileVDI.attach_from_config") 

335 raise xs_errors.XenError('SRUnavailable', \ 

336 opterr='Unable to attach from config') 

337 

338 

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

340 SRCommand.run(NFSSR, DRIVER_INFO) 

341else: 

342 SR.registerSR(NFSSR)