]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/iputil.py
a few documentation and naming convention updates
[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
77 def get_local_addresses_async(target='A.ROOT-SERVERS.NET'):
78     """
79     Return a Deferred that fires with a list of IPv4 addresses (as dotted-quad
80     strings) that are currently configured on this host, sorted in descending
81     order of how likely we think they are to work.
82
83     @param target: we want to learn an IP address they could try using to
84         connect to us; The default value is fine, but it might help if you
85         pass the address of a host that you are actually trying to be
86         reachable to.
87     """
88     addresses = []
89     local_ip = get_local_ip_for(target)
90     if local_ip:
91         addresses.append(local_ip)
92
93     if sys.platform == "cygwin":
94         d = _cygwin_hack_find_addresses(target)
95     else:
96         d = _find_addresses_via_config()
97
98     def _collect(res):
99         for addr in res:
100             if addr != "0.0.0.0" and not addr in addresses:
101                 addresses.append(addr)
102         return addresses
103     d.addCallback(_collect)
104
105     return d
106
107 def get_local_ip_for(target):
108     """Find out what our IP address is for use by a given target.
109
110     @return: the IP address as a dotted-quad string which could be used by
111               to connect to us. It might work for them, it might not. If
112               there is no suitable address (perhaps we don't currently have an
113               externally-visible interface), this will return None.
114     """
115
116     try:
117         target_ipaddr = socket.gethostbyname(target)
118     except socket.gaierror:
119         # DNS isn't running
120         return None
121     udpprot = DatagramProtocol()
122     port = reactor.listenUDP(0, udpprot)
123     try:
124         udpprot.transport.connect(target_ipaddr, 7)
125         localip = udpprot.transport.getHost().host
126     except socket.error:
127         # no route to that host
128         localip = None
129     port.stopListening() # note, this returns a Deferred
130     return localip
131
132 # k: result of sys.platform, v: which kind of IP configuration reader we use
133 _platform_map = {
134     "linux-i386": "linux", # redhat
135     "linux-ppc": "linux",  # redhat
136     "linux2": "linux",     # debian
137     "win32": "win32",
138     "irix6-n32": "irix",
139     "irix6-n64": "irix",
140     "irix6": "irix",
141     "openbsd2": "bsd",
142     "darwin": "bsd",       # Mac OS X
143     "freebsd4": "bsd",
144     "freebsd5": "bsd",
145     "netbsd1": "bsd",
146     "sunos5": "sunos",
147     "cygwin": "cygwin",
148     }
149
150 class UnsupportedPlatformError(Exception):
151     pass
152
153 # Wow, I'm really amazed at home much mileage we've gotten out of calling
154 # the external route.exe program on windows...  It appears to work on all
155 # versions so far.  Still, the real system calls would much be preferred...
156 # ... thus wrote Greg Smith in time immemorial...
157 _win32_path = 'route.exe'
158 _win32_args = ('print',)
159 _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)
160
161 # These work in Redhat 6.x and Debian 2.2 potato
162 _linux_path = '/sbin/ifconfig'
163 _linux_re = re.compile('^\s*inet addr:(?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
164
165 # NetBSD 1.4 (submitted by Rhialto), Darwin, Mac OS X
166 _netbsd_path = '/sbin/ifconfig'
167 _netbsd_args = ('-a',)
168 _netbsd_re = re.compile('^\s+inet (?P<address>\d+\.\d+\.\d+\.\d+)\s.+$', flags=re.M|re.I|re.S)
169
170 # Irix 6.5
171 _irix_path = '/usr/etc/ifconfig'
172
173 # Solaris 2.x
174 _sunos_path = '/usr/sbin/ifconfig'
175
176 class SequentialTrier(object):
177     """ I hold a list of executables to try and try each one in turn
178     until one gives me a list of IP addresses."""
179
180     def __init__(self, exebasename, args, regex):
181         assert not os.path.isabs(exebasename)
182         self.exes_left_to_try = which(exebasename)
183         self.exes_left_to_try.reverse()
184         self.args = args
185         self.regex = regex
186         self.o = observer.OneShotObserverList()
187         self._try_next()
188
189     def _try_next(self):
190         if not self.exes_left_to_try:
191             self.o.fire(None)
192         else:
193             exe = self.exes_left_to_try.pop()
194             d2 = _query(exe, self.args, self.regex)
195
196             def cb(res):
197                 if res:
198                     self.o.fire(res)
199                 else:
200                     self._try_next()
201
202             def eb(why):
203                 self._try_next()
204
205             d2.addCallbacks(cb, eb)
206
207     def when_tried(self):
208         return self.o.when_fired()
209
210 # k: platform string as provided in the value of _platform_map
211 # v: tuple of (path_to_tool, args, regex,)
212 _tool_map = {
213     "linux": (_linux_path, (), _linux_re,),
214     "win32": (_win32_path, _win32_args, _win32_re,),
215     "cygwin": (_win32_path, _win32_args, _win32_re,),
216     "bsd": (_netbsd_path, _netbsd_args, _netbsd_re,),
217     "irix": (_irix_path, _netbsd_args, _netbsd_re,),
218     "sunos": (_sunos_path, _netbsd_args, _netbsd_re,),
219     }
220 def _find_addresses_via_config():
221     # originally by Greg Smith, hacked by Zooko to conform to Brian's API
222
223     platform = _platform_map.get(sys.platform)
224     if not platform:
225         raise UnsupportedPlatformError(sys.platform)
226
227     (pathtotool, args, regex,) = _tool_map[platform]
228
229     # If pathtotool is a fully qualified path then we just try that.
230     # If it is merely an executable name then we use Twisted's
231     # "which()" utility and try each executable in turn until one
232     # gives us something that resembles a dotted-quad IPv4 address.
233
234     if os.path.isabs(pathtotool):
235         return _query(pathtotool, args, regex)
236     else:
237         return SequentialTrier(pathtotool, args, regex).when_tried()
238
239 def _query(path, args, regex):
240     d = getProcessOutput(path, args)
241     def _parse(output):
242         addresses = []
243         outputsplit = output.split('\n')
244         for outline in outputsplit:
245             m = regex.match(outline)
246             if m:
247                 addr = m.groupdict()['address']
248                 if addr not in addresses:
249                     addresses.append(addr)
250
251         return addresses
252     d.addCallback(_parse)
253     return d
254
255 def _cygwin_hack_find_addresses(target):
256     addresses = []
257     for h in [target, "localhost", "127.0.0.1",]:
258         try:
259             addr = get_local_ip_for(h)
260             if addr not in addresses:
261                 addresses.append(addr)
262         except socket.gaierror:
263             pass
264
265     return defer.succeed(addresses)