Coverage for drivers/nfs.py : 62%

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# 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# nfs.py: NFS related utility functions
18import util
19import errno
20import os
21import xml.dom.minidom
22import time
23# The algorithm for tcp and udp (at least in the linux kernel) for
24# NFS timeout on softmounts is as follows:
25#
26# UDP:
27# As long as the request wasn't started more than timeo * (2 ^ retrans)
28# in the past, keep doubling the timeout.
29#
30# TCP:
31# As long as the request wasn't started more than timeo * (1 + retrans)
32# in the past, keep increaing the timeout by timeo.
33#
34# The time when the retrans may retry has been made will be:
35# For udp: timeo * (2 ^ retrans * 2 - 1)
36# For tcp: timeo * n! where n is the smallest n for which n! > 1 + retrans
37#
38# thus for retrans=1, timeo can be the same for both tcp and udp,
39# because the first doubling (timeo*2) is the same as the first increment
40# (timeo+timeo).
42RPCINFO_BIN = "/usr/sbin/rpcinfo"
43SHOWMOUNT_BIN = "/usr/sbin/showmount"
44NFS_STAT = "/usr/sbin/nfsstat"
46DEFAULT_NFSVERSION = '3'
48NFS_VERSION = [
49 'nfsversion', 'for type=nfs, NFS protocol version - 3, 4, 4.0, 4.1']
51NFS_SERVICE_WAIT = 30
52NFS_SERVICE_RETRY = 6
54NFS4_PSEUDOFS = "/"
55NFS4_TMP_MOUNTPOINT = "/tmp/mnt"
57class NfsException(Exception):
59 def __init__(self, errstr):
60 self.errstr = errstr
62def check_server_tcp(server, transport, nfsversion=DEFAULT_NFSVERSION):
63 """Make sure that NFS over TCP/IP V3 is supported on the server.
65 Returns True if everything is OK
66 False otherwise.
67 """
69 try:
70 sv = get_supported_nfs_versions(server, transport)
71 return (nfsversion[0] in sv)
72 except util.CommandException as inst:
73 raise NfsException("rpcinfo failed or timed out: return code %d" %
74 inst.code)
77def check_server_service(server, transport):
78 """Ensure NFS service is up and available on the remote server.
80 Returns False if fails to detect service after
81 NFS_SERVICE_RETRY * NFS_SERVICE_WAIT
82 """
84 try:
85 sv = get_supported_nfs_versions(server, transport)
86 # Services are not present in NFS4 only, this doesn't mean there's no NFS
87 if "4" in sv: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true
88 return True
89 except NfsException:
90 # Server failed to give us supported versions
91 pass
93 retries = 0
94 errlist = [errno.EPERM, errno.EPIPE, errno.EIO]
96 while True:
97 try:
98 services = util.pread([RPCINFO_BIN, "-s", "%s" % server])
99 services = services.split("\n")
100 for i in range(len(services)):
101 if services[i].find("nfs") > 0:
102 return True
103 except util.CommandException as inst:
104 if not int(inst.code) in errlist:
105 raise
107 util.SMlog("NFS service not ready on server %s" % server)
108 retries += 1
109 if retries >= NFS_SERVICE_RETRY:
110 break
112 time.sleep(NFS_SERVICE_WAIT)
114 return False
117def validate_nfsversion(nfsversion):
118 """Check the validity of 'nfsversion'.
120 Raise an exception for any invalid version.
121 """
122 if not nfsversion:
123 nfsversion = DEFAULT_NFSVERSION
124 else:
125 if not (nfsversion == '3' or nfsversion.startswith('4')):
126 raise NfsException("Invalid nfsversion.")
127 return nfsversion
130def soft_mount(mountpoint, remoteserver, remotepath, transport, useroptions='',
131 timeout=None, nfsversion=DEFAULT_NFSVERSION, retrans=None):
132 """Mount the remote NFS export at 'mountpoint'.
134 The 'timeout' param here is in deciseconds (tenths of a second). See
135 nfs(5) for details.
136 """
137 try:
138 if not util.ioretry(lambda: util.isdir(mountpoint)): 138 ↛ 144line 138 didn't jump to line 144, because the condition on line 138 was never false
139 util.ioretry(lambda: util.makedirs(mountpoint))
140 except util.CommandException as inst:
141 raise NfsException("Failed to make directory: code is %d" %
142 inst.code)
144 mountcommand = 'mount.nfs'
146 options = "soft,proto=%s,vers=%s" % (
147 transport,
148 nfsversion)
149 options += ',acdirmin=0,acdirmax=0'
151 if timeout is not None: 151 ↛ 152line 151 didn't jump to line 152, because the condition on line 151 was never true
152 options += ",timeo=%s" % timeout
153 if retrans is not None: 153 ↛ 154line 153 didn't jump to line 154, because the condition on line 153 was never true
154 options += ",retrans=%s" % retrans
155 if useroptions != '': 155 ↛ 156line 155 didn't jump to line 156, because the condition on line 155 was never true
156 options += ",%s" % useroptions
158 try:
159 if transport in ['tcp6', 'udp6']:
160 remoteserver = '[' + remoteserver + ']'
161 util.ioretry(lambda:
162 util.pread([mountcommand, "%s:%s"
163 % (remoteserver, remotepath),
164 mountpoint, "-o", options]),
165 errlist=[errno.EPIPE, errno.EIO],
166 maxretry=2, nofail=True)
167 except util.CommandException as inst:
168 raise NfsException(
169 "mount failed on server `%s` with return code %d" % (
170 remoteserver, inst.code
171 )
172 )
175def unmount(mountpoint, rmmountpoint):
176 """Unmount the mounted mountpoint"""
177 try:
178 util.pread(["umount", mountpoint])
179 except util.CommandException as inst:
180 raise NfsException("umount failed with return code %d" % inst.code)
182 if rmmountpoint:
183 try:
184 os.rmdir(mountpoint)
185 except OSError as inst:
186 raise NfsException("rmdir failed with error '%s'" % inst.strerror)
189def _scan_exports_nfs3(target, dom, element):
190 """ Scan target and return an XML DOM with target, path and accesslist.
191 Using NFS3 services.
192 """
194 cmd = [SHOWMOUNT_BIN, "--no-headers", "-e", target]
195 for val in util.pread2(cmd).split('\n'):
196 if not len(val): 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true
197 continue
198 entry = dom.createElement("Export")
199 element.appendChild(entry)
201 subentry = dom.createElement("Target")
202 entry.appendChild(subentry)
203 textnode = dom.createTextNode(target)
204 subentry.appendChild(textnode)
206 # Access is not always provided by showmount return
207 # If none is provided we need to assume "*"
208 array = val.split()
209 path = array[0]
210 access = array[1] if len(array) >= 2 else "*"
211 subentry = dom.createElement("Path")
212 entry.appendChild(subentry)
213 textnode = dom.createTextNode(path)
214 subentry.appendChild(textnode)
216 subentry = dom.createElement("Accesslist")
217 entry.appendChild(subentry)
218 textnode = dom.createTextNode(access)
219 subentry.appendChild(textnode)
220 return dom
223def _scan_exports_nfs4(target, transport, dom, element):
224 """ Scan target and return an XML DOM with target, path and accesslist.
225 Using NFS4 only pseudo FS.
226 """
228 mountpoint = "%s/%s" % (NFS4_TMP_MOUNTPOINT, target)
229 soft_mount(mountpoint, target, NFS4_PSEUDOFS, transport, nfsversion="4")
230 paths = os.listdir(mountpoint)
231 unmount(mountpoint, NFS4_PSEUDOFS)
232 for path in paths:
233 entry = dom.createElement("Export")
234 element.appendChild(entry)
236 subentry = dom.createElement("Target")
237 entry.appendChild(subentry)
238 textnode = dom.createTextNode(target)
239 subentry.appendChild(textnode)
240 subentry = dom.createElement("Path")
241 entry.appendChild(subentry)
242 textnode = dom.createTextNode(path)
243 subentry.appendChild(textnode)
245 subentry = dom.createElement("Accesslist")
246 entry.appendChild(subentry)
247 # Assume everyone as we do not have any info about it
248 textnode = dom.createTextNode("*")
249 subentry.appendChild(textnode)
250 return dom
253def scan_exports(target, transport):
254 """Scan target and return an XML DOM with target, path and accesslist."""
255 util.SMlog("scanning")
256 dom = xml.dom.minidom.Document()
257 element = dom.createElement("nfs-exports")
258 dom.appendChild(element)
259 try:
260 return _scan_exports_nfs3(target, dom, element)
261 except Exception:
262 util.SMlog("Unable to scan exports with %s, trying NFSv4" % SHOWMOUNT_BIN)
264 # NFSv4 only
265 try:
266 return _scan_exports_nfs4(target, transport, dom, element)
267 except Exception:
268 util.SMlog("Unable to scan exports with NFSv4 pseudo FS mount")
270 raise NfsException("Failed to read NFS export paths from server %s" %
271 (target))
274def scan_srlist(path, transport, dconf):
275 """Scan and report SR, UUID."""
276 dom = xml.dom.minidom.Document()
277 element = dom.createElement("SRlist")
278 dom.appendChild(element)
279 for val in filter(util.match_uuid, util.ioretry(
280 lambda: util.listdir(path))):
281 fullpath = os.path.join(path, val)
282 if not util.ioretry(lambda: util.isdir(fullpath)):
283 continue
285 entry = dom.createElement('SR')
286 element.appendChild(entry)
288 subentry = dom.createElement("UUID")
289 entry.appendChild(subentry)
290 textnode = dom.createTextNode(val)
291 subentry.appendChild(textnode)
293 from NFSSR import PROBEVERSION
294 if PROBEVERSION in dconf:
295 util.SMlog("Add supported nfs versions to sr-probe")
296 try:
297 supported_versions = get_supported_nfs_versions(dconf.get('server'), transport)
298 supp_ver = dom.createElement("SupportedVersions")
299 element.appendChild(supp_ver)
301 for ver in supported_versions:
302 version = dom.createElement('Version')
303 supp_ver.appendChild(version)
304 textnode = dom.createTextNode(ver)
305 version.appendChild(textnode)
306 except NfsException:
307 # Server failed to give us supported versions
308 pass
310 return dom.toprettyxml()
313def _get_supported_nfs_version_rpcinfo(server):
314 """ Return list of supported nfs versions.
315 Using NFS3 services.
316 *Might* return "4" in the list of supported NFS versions, but might not:
317 There is no requirement for NFS4 to register with rpcbind, even though it can, so
318 a server which supports NFS4 might still only return ["3"] from here.
319 """
321 valid_versions = set(["3", "4"])
322 cv = set()
323 ns = util.pread2([RPCINFO_BIN, "-s", "%s" % server])
324 ns = ns.split("\n")
325 for i in range(len(ns)):
326 if ns[i].find("nfs") > 0:
327 cvi = ns[i].split()[1].split(",")
328 for j in range(len(cvi)):
329 cv.add(cvi[j])
330 return sorted(cv & valid_versions)
333def _is_nfs4_supported(server, transport):
334 """ Return list of supported nfs versions.
335 Using NFS4 pseudo FS.
336 """
338 try:
339 mountpoint = "%s/%s" % (NFS4_TMP_MOUNTPOINT, server)
340 soft_mount(mountpoint, server, NFS4_PSEUDOFS, transport, nfsversion='4')
341 util.pread2([NFS_STAT, "-m"])
342 unmount(mountpoint, NFS4_PSEUDOFS)
343 return True
344 except Exception:
345 return False
348def get_supported_nfs_versions(server, transport):
349 """
350 Return list of supported nfs versions.
352 First check list from rpcinfo and if that does not contain NFS4, probe for it and
353 add it to the list if available.
354 """
355 vers = []
356 try:
357 vers = _get_supported_nfs_version_rpcinfo(server)
358 except Exception:
359 util.SMlog("Unable to obtain list of valid nfs versions with %s, trying NFSv4" % RPCINFO_BIN)
361 # Test for NFS4 if the rpcinfo query did not find it (NFS4 does not *have* to register with rpcbind)
362 if "4" not in vers:
363 if _is_nfs4_supported(server, transport):
364 vers.append("4")
366 if vers:
367 return vers
368 else:
369 raise NfsException("Failed to read supported NFS version from server %s" % (server))
372def get_nfs_timeout(other_config):
373 nfs_timeout = 200
375 if 'nfs-timeout' in other_config: 375 ↛ 376line 375 didn't jump to line 376, because the condition on line 375 was never true
376 val = int(other_config['nfs-timeout'])
377 if val < 1:
378 util.SMlog("Invalid nfs-timeout value: %d" % val)
379 else:
380 nfs_timeout = val
382 return nfs_timeout
385def get_nfs_retrans(other_config):
386 nfs_retrans = 4
388 if 'nfs-retrans' in other_config: 388 ↛ 389line 388 didn't jump to line 389, because the condition on line 388 was never true
389 val = int(other_config['nfs-retrans'])
390 if val < 0:
391 util.SMlog("Invalid nfs-retrans value: %d" % val)
392 else:
393 nfs_retrans = val
395 return nfs_retrans