Coverage for libs/sm/drivers/NFSSR.py: 56%

204 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2026-07-10 07:59 +0000

1# Copyright (C) Citrix Systems Inc. 

2# 

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

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

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

6# 

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

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

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

10# GNU Lesser General Public License for more details. 

11# 

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

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

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

15# 

16# NFSSR: NFS-based file storage repository 

17# matches with drivers/NFSSR 

18 

19import socket 

20import errno 

21import os 

22import sys 

23import xmlrpc.client 

24 

25from sm import SR 

26from sm import nfs 

27from sm import vhdutil 

28from sm import cleanup 

29from sm.drivers import FileSR 

30from sm.core import util 

31from sm.core import xs_errors 

32from sm.core.lock import Lock 

33 

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

35 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH", 

36 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", 

37 "VDI_GENERATE_CONFIG", "VDI_MIRROR", 

38 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT", 

39 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"] 

40 

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

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

43 nfs.NFS_VERSION] 

44 

45DRIVER_INFO = { 

46 'name': 'NFS VHD', 

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

48 'vendor': 'Citrix Systems Inc', 

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

50 'driver_version': '1.0', 

51 'required_api_version': '1.0', 

52 'capabilities': CAPABILITIES, 

53 'configuration': CONFIGURATION 

54 } 

55 

56DRIVER_CONFIG = {"ATTACH_FROM_CONFIG_WITH_TAPDISK": True} 

57 

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

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

60PROBE_MOUNTPOINT = "probe" 

61NFSPORT = 2049 

62DEFAULT_TRANSPORT = "tcp" 

63PROBEVERSION = 'probeversion' 

64 

65 

66class NFSSR(FileSR.SharedFileSR): 

67 """NFS file-based storage repository""" 

68 

69 def handles(type): 

70 return type == 'nfs' 

71 handles = staticmethod(handles) 

72 

73 def load(self, sr_uuid): 

74 self.ops_exclusive = FileSR.OPS_EXCLUSIVE 

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

76 self.sr_vditype = SR.DEFAULT_TAP 

77 self.driver_config = DRIVER_CONFIG 

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

79 raise xs_errors.XenError('ConfigServerMissing') 

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

81 self.nosubdir = False 

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

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

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

85 else: 

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

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

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

89 if 'serverpath' in self.dconf: 89 ↛ 92line 89 didn't jump to line 92, because the condition on line 89 was never false

90 self.remotepath = os.path.join(self.dconf['serverpath'], 

91 not self.nosubdir and sr_uuid or "") 

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

93 

94 # Handle optional dconf attributes 

95 self.set_transport() 

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

97 if 'options' in self.dconf: 

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

99 else: 

100 self.options = '' 

101 

102 def validate_remotepath(self, scan): 

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

104 if scan: 

105 try: 

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

107 except: 

108 pass 

109 raise xs_errors.XenError('ConfigServerPathMissing') 

110 

111 def check_server(self): 

112 try: 

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

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

115 if len(sv): 

116 self.nfsversion = sv[0] 

117 else: 

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

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

120 

121 except nfs.NfsException as exc: 

122 raise xs_errors.XenError('NFSVersion', 

123 opterr=exc.errstr) 

124 

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

126 try: 

127 nfs.soft_mount( 

128 mountpoint, self.remoteserver, remotepath, self.transport, 

129 useroptions=self.options, timeout=timeout, 

130 nfsversion=self.nfsversion, retrans=retrans) 

131 except nfs.NfsException as exc: 

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

133 

134 def attach(self, sr_uuid): 

135 if not self._checkmount(): 

136 try: 

137 self.validate_remotepath(False) 

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

139 self.mount_remotepath(sr_uuid) 

140 self._check_writable() 

141 self._check_hardlinks() 

142 except: 

143 if self._checkmount(): 

144 nfs.unmount(self.path, True) 

145 raise 

146 self.attached = True 

147 

148 def mount_remotepath(self, sr_uuid): 

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

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

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

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

153 # in mount instead 

154 self.check_server() 

155 # Extract timeout and retrans values, if any 

156 io_timeout = nfs.get_nfs_timeout(self.other_config) 

157 io_retrans = nfs.get_nfs_retrans(self.other_config) 

158 self.mount(self.path, self.remotepath, 

159 timeout=io_timeout, retrans=io_retrans) 

160 

161 def probe(self): 

162 # Verify NFS target and port 

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

164 

165 self.validate_remotepath(True) 

166 self.check_server() 

167 

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

169 

170 self.mount(temppath, self.remotepath) 

171 try: 

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

173 finally: 

174 try: 

175 nfs.unmount(temppath, True) 

176 except: 

177 pass 

178 

179 def detach(self, sr_uuid): 

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

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

182 return 

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

184 cleanup.abort(self.uuid) 

185 

186 # Change directory to avoid unmount conflicts 

187 os.chdir(SR.MOUNT_BASE) 

188 

189 try: 

190 nfs.unmount(self.path, True) 

191 except nfs.NfsException as exc: 

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

193 

194 self.attached = False 

195 

196 def create(self, sr_uuid, size): 

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

198 self.validate_remotepath(True) 

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

200 raise xs_errors.XenError('NFSAttached') 

201 

202 # Set the target path temporarily to the base dir 

203 # so that we can create the target SR directory 

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

205 try: 

206 self.mount_remotepath(sr_uuid) 

207 except Exception as exn: 

208 try: 

209 os.rmdir(self.path) 

210 except: 

211 pass 

212 raise 

213 

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

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

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

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

218 self.detach(sr_uuid) 

219 raise xs_errors.XenError('SRExists') 

220 else: 

221 try: 

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

223 except util.CommandException as inst: 

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

225 self.detach(sr_uuid) 

226 if inst.code == errno.EROFS: 

227 raise xs_errors.XenError('SharedFileSystemNoWrite', 

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

229 % inst.code) 

230 else: 

231 raise xs_errors.XenError('NFSCreate', 

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

233 % inst.code) 

234 self.detach(sr_uuid) 

235 

236 def delete(self, sr_uuid): 

237 # try to remove/delete non VDI contents first 

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

239 try: 

240 if self._checkmount(): 

241 self.detach(sr_uuid) 

242 

243 # Set the target path temporarily to the base dir 

244 # so that we can remove the target SR directory 

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

246 self.mount_remotepath(sr_uuid) 

247 if not self.nosubdir: 

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

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

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

251 self.detach(sr_uuid) 

252 except util.CommandException as inst: 

253 self.detach(sr_uuid) 

254 if inst.code != errno.ENOENT: 

255 raise xs_errors.XenError('NFSDelete') 

256 

257 def vdi(self, uuid): 

258 return NFSFileVDI(self, uuid) 

259 

260 def scan_exports(self, target): 

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

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

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

264 

265 def set_transport(self): 

266 self.transport = DEFAULT_TRANSPORT 

267 if self.remoteserver is None: 

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

269 return 

270 

271 try: 

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

273 except Exception: 

274 return 

275 

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

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

278 self.transport = 'tcp6' 

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

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

281 

282 

283class NFSFileVDI(FileSR.FileVDI): 

284 def attach(self, sr_uuid, vdi_uuid): 

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

286 self.xenstore_data = {} 

287 

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

289 

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

291 

292 def generate_config(self, sr_uuid, vdi_uuid): 

293 util.SMlog("NFSFileVDI.generate_config") 

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

295 raise xs_errors.XenError('VDIUnavailable') 

296 resp = {} 

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

298 resp['sr_uuid'] = sr_uuid 

299 resp['vdi_uuid'] = vdi_uuid 

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

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

302 resp['command'] = 'vdi_attach_from_config' 

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

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

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

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

307 

308 def attach_from_config(self, sr_uuid, vdi_uuid): 

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

310 also start a tapdisk on the file""" 

311 util.SMlog("NFSFileVDI.attach_from_config") 

312 try: 

313 self.sr.attach(sr_uuid) 

314 except: 

315 util.logException("NFSFileVDI.attach_from_config") 

316 raise xs_errors.XenError('SRUnavailable', \ 

317 opterr='Unable to attach from config') 

318 

319# SR registration at import 

320SR.registerSR(NFSSR)