]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/iputil.py
Make platform-detection code tolerate linux-3.0, patch by zooko.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / util / iputil.py
1 # from the Python Standard Library
2 import os, re, socket, sys, subprocess
3
4 # from Twisted
5 from twisted.internet import defer, threads, reactor
6 from twisted.internet.protocol import DatagramProtocol
7 from twisted.python.procutils import which
8 from twisted.python import log
9
10 try:
11     import resource
12     def increase_rlimits():
13         # We'd like to raise our soft resource.RLIMIT_NOFILE, since certain
14         # systems (OS-X, probably solaris) start with a relatively low limit
15         # (256), and some unit tests want to open up more sockets than this.
16         # Most linux systems start with both hard and soft limits at 1024,
17         # which is plenty.
18
19         # unfortunately the values to pass to setrlimit() vary widely from
20         # one system to another. OS-X reports (256, HUGE), but the real hard
21         # limit is 10240, and accepts (-1,-1) to mean raise it to the
22         # maximum. Cygwin reports (256, -1), then ignores a request of
23         # (-1,-1): instead you have to guess at the hard limit (it appears to
24         # be 3200), so using (3200,-1) seems to work. Linux reports a
25         # sensible (1024,1024), then rejects (-1,-1) as trying to raise the
26         # maximum limit, so you could set it to (1024,1024) but you might as
27         # well leave it alone.
28
29         try:
30             current = resource.getrlimit(resource.RLIMIT_NOFILE)
31         except AttributeError:
32             # we're probably missing RLIMIT_NOFILE
33             return
34
35         if current[0] >= 1024:
36             # good enough, leave it alone
37             return
38
39         try:
40             if current[1] > 0 and current[1] < 1000000:
41                 # solaris reports (256, 65536)
42                 resource.setrlimit(resource.RLIMIT_NOFILE,
43                                    (current[1], current[1]))
44             else:
45                 # this one works on OS-X (bsd), and gives us 10240, but
46                 # it doesn't work on linux (on which both the hard and
47                 # soft limits are set to 1024 by default).
48                 resource.setrlimit(resource.RLIMIT_NOFILE, (-1,-1))
49                 new = resource.getrlimit(resource.RLIMIT_NOFILE)
50                 if new[0] == current[0]:
51                     # probably cygwin, which ignores -1. Use a real value.
52                     resource.setrlimit(resource.RLIMIT_NOFILE, (3200,-1))
53
54         except ValueError:
55             log.msg("unable to set RLIMIT_NOFILE: current value %s"
56                      % (resource.getrlimit(resource.RLIMIT_NOFILE),))
57         except:
58             # who knows what. It isn't very important, so log it and continue
59             log.err()
60 except ImportError:
61     def _increase_rlimits():
62         # TODO: implement this for Windows.  Although I suspect the
63         # solution might be "be running under the iocp reactor and
64         # make this function be a no-op".
65         pass
66     # pyflakes complains about two 'def FOO' statements in the same time,
67     # since one might be shadowing the other. This hack appeases pyflakes.
68     increase_rlimits = _increase_rlimits
69
70
71 def get_local_addresses_async(target="198.41.0.4"): # A.ROOT-SERVERS.NET
72     """
73     Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad
74     strings) that are currently configured on this host, sorted in descending
75     order of how likely we think they are to work.
76
77     @param target: we want to learn an IP address they could try using to
78         connect to us; The default value is fine, but it might help if you
79         pass the address of a host that you are actually trying to be
80         reachable to.
81     """
82     addresses = []
83     local_ip = get_local_ip_for(target)
84     if local_ip:
85         addresses.append(local_ip)
86
87     if sys.platform == "cygwin":
88         d = _cygwin_hack_find_addresses(target)
89     else:
90         d = _find_addresses_via_config()
91
92     def _collect(res):
93         for addr in res:
94             if addr != "0.0.0.0" and not addr in addresses:
95                 addresses.append(addr)
96         return addresses
97     d.addCallback(_collect)
98
99     return d
100
101 def get_local_ip_for(target):
102     """Find out what our IP address is for use by a given target.
103
104     @return: the IP address as a dotted-quad string which could be used by
105               to connect to us. It might work for them, it might not. If
106               there is no suitable address (perhaps we don't currently have an
107               externally-visible interface), this will return None.
108     """
109
110     try:
111         target_ipaddr = socket.gethostbyname(target)
112     except socket.gaierror:
113         # DNS isn't running, or somehow we encountered an error
114
115         # note: if an interface is configured and up, but nothing is
116         # connected to it, gethostbyname("A.ROOT-SERVERS.NET") will take 20
117         # seconds to raise socket.gaierror . This is synchronous and occurs
118         # for each node being started, so users of
119         # test.common.SystemTestMixin (like test_system) will see something
120         # like 120s of delay, which may be enough to hit the default trial
121         # timeouts. For that reason, get_local_addresses_async() was changed
122         # to default to the numerical ip address for A.ROOT-SERVERS.NET, to
123         # avoid this DNS lookup. This also makes node startup fractionally
124         # faster.
125         return None
126     udpprot = DatagramProtocol()
127     port = reactor.listenUDP(0, udpprot)
128     try:
129         udpprot.transport.connect(target_ipaddr, 7)
130         localip = udpprot.transport.getHost().host
131     except socket.error:
132         # no route to that host
133         localip = None
134     port.stopListening() # note, this returns a Deferred
135     return localip
136
137 # k: result of sys.platform, v: which kind of IP configuration reader we use
138 _platform_map = {
139     "linux-i386": "linux", # redhat
140     "linux-ppc": "linux",  # redhat
141     "linux2": "linux",     # debian
142     "linux3": "linux",     # debian
143     "win32": "win32",
144     "irix6-n32": "irix",
145     "irix6-n64": "irix",
146     "irix6": "irix",
147     "openbsd2": "bsd",
148     "openbsd3": "bsd",
149     "openbsd4": "bsd",
150     "darwin": "bsd",       # Mac OS X
151     "freebsd4": "bsd",
152     "freebsd5": "bsd",
153     "freebsd6": "bsd",
154     "freebsd7": "bsd",
155     "freebsd8": "bsd",
156     "freebsd9": "bsd",
157     "netbsd1": "bsd",
158     "netbsd2": "bsd",
159     "netbsd3": "bsd",
160     "netbsd4": "bsd",
161     "netbsd5": "bsd",
162     "netbsd6": "bsd",
163     "sunos5": "sunos",
164     "cygwin": "cygwin",
165     }
166
167 class UnsupportedPlatformError(Exception):
168     pass
169
170 # Wow, I'm really amazed at home much mileage we've gotten out of calling
171 # the external route.exe program on windows...  It appears to work on all
172 # versions so far.  Still, the real system calls would much be preferred...
173 # ... thus wrote Greg Smith in time immemorial...
174 _win32_path = 'route.exe'
175 _win32_args = ('print',)
176 _win32_re = re.compile('^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P<address>\d+\.\d+\.\d+\.\d+)\s+(?P<metric>\d+)\s*$', flags=re.M|re.I|re.S)
177
178 # These work in Redhat 6.x and Debian 2.2 potato
179 _linux_path = '/sbin/ifconfig'
180 _linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
181
182 # NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X
183 _netbsd_path = '/sbin/ifconfig'
184 _netbsd_args = ('-a',)
185 _netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
186
187 # Irix 6.5
188 _irix_path = '/usr/etc/ifconfig'
189
190 # Solaris 2.x
191 _sunos_path = '/usr/sbin/ifconfig'
192
193
194 # k: platform string as provided in the value of _platform_map
195 # v: tuple of (path_to_tool, args, regex,)
196 _tool_map = {
197     "linux": (_linux_path, (), _linux_re,),
198     "win32": (_win32_path, _win32_args, _win32_re,),
199     "cygwin": (_win32_path, _win32_args, _win32_re,),
200     "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,),
201     "irix": (_irix_path, _netbsd_args, _netbsd_re,),
202     "sunos": (_sunos_path, _netbsd_args, _netbsd_re,),
203     }
204
205 def _find_addresses_via_config():
206     return threads.deferToThread(_synchronously_find_addresses_via_config)
207
208 def _synchronously_find_addresses_via_config():
209     # originally by Greg Smith, hacked by Zooko to conform to Brian's API
210
211     platform = _platform_map.get(sys.platform)
212     if not platform:
213         raise UnsupportedPlatformError(sys.platform)
214
215     (pathtotool, args, regex,) = _tool_map[platform]
216
217     # If pathtotool is a fully qualified path then we just try that.
218     # If it is merely an executable name then we use Twisted's
219     # "which()" utility and try each executable in turn until one
220     # gives us something that resembles a dotted-quad IPv4 address.
221
222     if os.path.isabs(pathtotool):
223         return _query(pathtotool, args, regex)
224     else:
225         exes_to_try = which(pathtotool)
226         for exe in exes_to_try:
227             try:
228                 addresses = _query(exe, args, regex)
229             except Exception:
230                 addresses = []
231             if addresses:
232                 return addresses
233         return []
234
235 def _query(path, args, regex):
236     env = {'LANG': 'en_US.UTF-8'}
237     p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
238     (output, err) = p.communicate()
239
240     addresses = []
241     outputsplit = output.split('\n')
242     for outline in outputsplit:
243         m = regex.match(outline)
244         if m:
245             addr = m.groupdict()['address']
246             if addr not in addresses:
247                 addresses.append(addr)
248
249     return addresses
250
251 def _cygwin_hack_find_addresses(target):
252     addresses = []
253     for h in [target, "localhost", "127.0.0.1",]:
254         try:
255             addr = get_local_ip_for(h)
256             if addr not in addresses:
257                 addresses.append(addr)
258         except socket.gaierror:
259             pass
260
261     return defer.succeed(addresses)