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