]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/iputil.py
f372985828466564f2cd68f63dcb57417a358171
[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     "win32": "win32",
143     "irix6-n32": "irix",
144     "irix6-n64": "irix",
145     "irix6": "irix",
146     "openbsd2": "bsd",
147     "openbsd3": "bsd",
148     "openbsd4": "bsd",
149     "darwin": "bsd",       # Mac OS X
150     "freebsd4": "bsd",
151     "freebsd5": "bsd",
152     "freebsd6": "bsd",
153     "freebsd7": "bsd",
154     "freebsd8": "bsd",
155     "freebsd9": "bsd",
156     "netbsd1": "bsd",
157     "netbsd2": "bsd",
158     "netbsd3": "bsd",
159     "netbsd4": "bsd",
160     "netbsd5": "bsd",
161     "netbsd6": "bsd",
162     "sunos5": "sunos",
163     "cygwin": "cygwin",
164     }
165
166 class UnsupportedPlatformError(Exception):
167     pass
168
169 # Wow, I'm really amazed at home much mileage we've gotten out of calling
170 # the external route.exe program on windows...  It appears to work on all
171 # versions so far.  Still, the real system calls would much be preferred...
172 # ... thus wrote Greg Smith in time immemorial...
173 _win32_path = 'route.exe'
174 _win32_args = ('print',)
175 _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)
176
177 # These work in Redhat 6.x and Debian 2.2 potato
178 _linux_path = '/sbin/ifconfig'
179 _linux_re = re.compile('^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
180
181 # NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X
182 _netbsd_path = '/sbin/ifconfig'
183 _netbsd_args = ('-a',)
184 _netbsd_re = re.compile('^\s+inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
185
186 # Irix 6.5
187 _irix_path = '/usr/etc/ifconfig'
188
189 # Solaris 2.x
190 _sunos_path = '/usr/sbin/ifconfig'
191
192
193 # k: platform string as provided in the value of _platform_map
194 # v: tuple of (path_to_tool, args, regex,)
195 _tool_map = {
196     "linux": (_linux_path, (), _linux_re,),
197     "win32": (_win32_path, _win32_args, _win32_re,),
198     "cygwin": (_win32_path, _win32_args, _win32_re,),
199     "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,),
200     "irix": (_irix_path, _netbsd_args, _netbsd_re,),
201     "sunos": (_sunos_path, _netbsd_args, _netbsd_re,),
202     }
203
204 def _find_addresses_via_config():
205     return threads.deferToThread(_synchronously_find_addresses_via_config)
206
207 def _synchronously_find_addresses_via_config():
208     # originally by Greg Smith, hacked by Zooko to conform to Brian's API
209
210     platform = _platform_map.get(sys.platform)
211     if not platform:
212         raise UnsupportedPlatformError(sys.platform)
213
214     (pathtotool, args, regex,) = _tool_map[platform]
215
216     # If pathtotool is a fully qualified path then we just try that.
217     # If it is merely an executable name then we use Twisted's
218     # "which()" utility and try each executable in turn until one
219     # gives us something that resembles a dotted-quad IPv4 address.
220
221     if os.path.isabs(pathtotool):
222         return _query(pathtotool, args, regex)
223     else:
224         exes_to_try = which(pathtotool)
225         for exe in exes_to_try:
226             try:
227                 addresses = _query(exe, args, regex)
228             except Exception:
229                 addresses = []
230             if addresses:
231                 return addresses
232         return []
233
234 def _query(path, args, regex):
235     env = {'LANG': 'en_US.UTF-8'}
236     p = subprocess.Popen([path] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
237     (output, err) = p.communicate()
238
239     addresses = []
240     outputsplit = output.split('\n')
241     for outline in outputsplit:
242         m = regex.match(outline)
243         if m:
244             addr = m.groupdict()['address']
245             if addr not in addresses:
246                 addresses.append(addr)
247
248     return addresses
249
250 def _cygwin_hack_find_addresses(target):
251     addresses = []
252     for h in [target, "localhost", "127.0.0.1",]:
253         try:
254             addr = get_local_ip_for(h)
255             if addr not in addresses:
256                 addresses.append(addr)
257         except socket.gaierror:
258             pass
259
260     return defer.succeed(addresses)