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 nfs 

35import vhdutil 

36from lock import Lock 

37import cleanup 

38 

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

40 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

41 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", 

42 "VDI_GENERATE_CONFIG", "VDI_MIRROR", 

43 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

44 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

45 

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

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

48 nfs.NFS_VERSION] 

49 

50DRIVER_INFO = { 

51 'name': 'NFS VHD', 

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

53 'vendor': 'Citrix Systems Inc', 

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

55 'driver_version': '1.0', 

56 'required_api_version': '1.0', 

57 'capabilities': CAPABILITIES, 

58 'configuration': CONFIGURATION 

59 } 

60 

61DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

62 

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

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

65PROBE_MOUNTPOINT = "probe" 

66NFSPORT = 2049 

67DEFAULT_TRANSPORT = "tcp" 

68PROBEVERSION = 'probeversion' 

69 

70 

71class NFSSR(FileSR.SharedFileSR): 

72 """NFS file-based storage repository""" 

73 

74 @override 

75 @staticmethod 

76 def handles(type) -> bool: 

77 return type == 'nfs' 

78 

79 @override 

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

81 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

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

83 self.sr_vditype = SR.DEFAULT_TAP 

84 self.driver_config = DRIVER_CONFIG 

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

86 raise xs_errors.XenError('ConfigServerMissing') 

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

88 self.nosubdir = False 

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

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

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

92 else: 

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

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

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

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

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

98 self.remotepath = os.path.join( 

99 serverpath, 

100 not self.nosubdir and sr_uuid or "" 

101 ) 

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

103 

104 # Handle optional dconf attributes 

105 self.set_transport() 

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

107 if 'options' in self.dconf: 

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

109 else: 

110 self.options = '' 

111 

112 def validate_remotepath(self, scan): 

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

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

115 if scan: 

116 try: 

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

118 except: 

119 pass 

120 raise xs_errors.XenError('ConfigServerPathMissing') 

121 

122 def check_server(self): 

123 try: 

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

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

126 if len(sv): 

127 self.nfsversion = sv[0] 

128 else: 

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

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

131 

132 except nfs.NfsException as exc: 

133 raise xs_errors.XenError('NFSVersion', 

134 opterr=exc.errstr) 

135 

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

137 try: 

138 nfs.soft_mount( 

139 mountpoint, self.remoteserver, remotepath, self.transport, 

140 useroptions=self.options, timeout=timeout, 

141 nfsversion=self.nfsversion, retrans=retrans) 

142 except nfs.NfsException as exc: 

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

144 

145 @override 

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

147 if not self._checkmount(): 

148 try: 

149 self.validate_remotepath(False) 

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

151 self.mount_remotepath(sr_uuid) 

152 self._check_writable() 

153 self._check_hardlinks() 

154 except: 

155 if self._checkmount(): 

156 nfs.unmount(self.path, True) 

157 raise 

158 self.attached = True 

159 

160 def mount_remotepath(self, sr_uuid): 

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

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

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

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

165 # in mount instead 

166 self.check_server() 

167 # Extract timeout and retrans values, if any 

168 io_timeout = nfs.get_nfs_timeout(self.other_config) 

169 io_retrans = nfs.get_nfs_retrans(self.other_config) 

170 self.mount(self.path, self.remotepath, 

171 timeout=io_timeout, retrans=io_retrans) 

172 

173 @override 

174 def probe(self) -> str: 

175 # Verify NFS target and port 

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

177 

178 self.validate_remotepath(True) 

179 self.check_server() 

180 

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

182 

183 self.mount(temppath, self.remotepath) 

184 try: 

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

186 finally: 

187 try: 

188 nfs.unmount(temppath, True) 

189 except: 

190 pass 

191 

192 @override 

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

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

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

196 return 

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

198 cleanup.abort(self.uuid) 

199 

200 # Change directory to avoid unmount conflicts 

201 os.chdir(SR.MOUNT_BASE) 

202 

203 try: 

204 nfs.unmount(self.path, True) 

205 except nfs.NfsException as exc: 

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

207 

208 self.attached = False 

209 

210 @override 

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

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

213 self.validate_remotepath(True) 

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

215 raise xs_errors.XenError('NFSAttached') 

216 

217 # Set the target path temporarily to the base dir 

218 # so that we can create the target SR directory 

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

220 try: 

221 self.mount_remotepath(sr_uuid) 

222 except Exception as exn: 

223 try: 

224 os.rmdir(self.path) 

225 except: 

226 pass 

227 raise 

228 

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

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

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

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

233 self.detach(sr_uuid) 

234 raise xs_errors.XenError('SRExists') 

235 else: 

236 try: 

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

238 except util.CommandException as inst: 

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

240 self.detach(sr_uuid) 

241 if inst.code == errno.EROFS: 

242 raise xs_errors.XenError('SharedFileSystemNoWrite', 

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

244 % inst.code) 

245 else: 

246 raise xs_errors.XenError('NFSCreate', 

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

248 % inst.code) 

249 self.detach(sr_uuid) 

250 

251 @override 

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

253 # try to remove/delete non VDI contents first 

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

255 try: 

256 if self._checkmount(): 

257 self.detach(sr_uuid) 

258 

259 # Set the target path temporarily to the base dir 

260 # so that we can remove the target SR directory 

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

262 self.mount_remotepath(sr_uuid) 

263 if not self.nosubdir: 

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

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

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

267 self.detach(sr_uuid) 

268 except util.CommandException as inst: 

269 self.detach(sr_uuid) 

270 if inst.code != errno.ENOENT: 

271 raise xs_errors.XenError('NFSDelete') 

272 

273 @override 

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

275 return NFSFileVDI(self, uuid) 

276 

277 def scan_exports(self, target): 

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

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

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

281 

282 def set_transport(self): 

283 self.transport = DEFAULT_TRANSPORT 

284 if self.remoteserver is None: 

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

286 return 

287 

288 try: 

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

290 except Exception: 

291 return 

292 

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

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

295 self.transport = 'tcp6' 

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

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

298 

299 

300class NFSFileVDI(FileSR.FileVDI): 

301 @override 

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

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

304 self.xenstore_data = {} 

305 

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

307 

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

309 

310 @override 

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

312 util.SMlog("NFSFileVDI.generate_config") 

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

314 raise xs_errors.XenError('VDIUnavailable') 

315 resp = {} 

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

317 resp['sr_uuid'] = sr_uuid 

318 resp['vdi_uuid'] = vdi_uuid 

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

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

321 resp['command'] = 'vdi_attach_from_config' 

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

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

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

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

326 

327 @override 

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

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

330 also start a tapdisk on the file""" 

331 util.SMlog("NFSFileVDI.attach_from_config") 

332 try: 

333 return self.sr.attach(sr_uuid) 

334 except: 

335 util.logException("NFSFileVDI.attach_from_config") 

336 raise xs_errors.XenError('SRUnavailable', \ 

337 opterr='Unable to attach from config') 

338 

339 

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

341 SRCommand.run(NFSSR, DRIVER_INFO) 

342else: 

343 SR.registerSR(NFSSR)