tahoe-lafs/src/allmydata/test/test_iputil.py

264 lines
10 KiB
Python
Raw Normal View History

2020-07-27 15:46:03 +00:00
"""
Tests for allmydata.util.iputil.
Ported to Python 3.
"""
2020-07-27 15:14:01 +00:00
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2, native_str
if PY2:
2020-08-05 15:53:23 +00:00
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
2020-07-27 17:06:41 +00:00
import re, errno, subprocess, os, socket
2020-07-29 18:46:49 +00:00
import gc
from twisted.trial import unittest
from tenacity import retry, stop_after_attempt
2020-07-27 17:06:41 +00:00
from foolscap.api import Tub
2020-07-29 18:46:49 +00:00
from allmydata.util import iputil, gcutil
2020-07-24 18:58:08 +00:00
import allmydata.test.common_py3 as testutil
from allmydata.util.namespace import Namespace
2020-07-27 15:14:01 +00:00
DOTTED_QUAD_RE=re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$")
2020-07-24 17:47:56 +00:00
# Mock output from subprocesses should be bytes, that's what happens on both
# Python 2 and Python 3:
MOCK_IPADDR_OUTPUT = b"""\
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN \n\
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host \n\
valid_lft forever preferred_lft forever
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether d4:3d:7e:01:b4:3e brd ff:ff:ff:ff:ff:ff
inet 192.168.0.6/24 brd 192.168.0.255 scope global eth1
inet6 fe80::d63d:7eff:fe01:b43e/64 scope link \n\
valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 90:f6:52:27:15:0a brd ff:ff:ff:ff:ff:ff
inet 192.168.0.2/24 brd 192.168.0.255 scope global wlan0
inet6 fe80::92f6:52ff:fe27:150a/64 scope link \n\
valid_lft forever preferred_lft forever
"""
2020-07-24 17:47:56 +00:00
MOCK_IFCONFIG_OUTPUT = b"""\
eth1 Link encap:Ethernet HWaddr d4:3d:7e:01:b4:3e \n\
inet addr:192.168.0.6 Bcast:192.168.0.255 Mask:255.255.255.0
inet6 addr: fe80::d63d:7eff:fe01:b43e/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:154242234 errors:0 dropped:0 overruns:0 frame:0
TX packets:155461891 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 \n\
RX bytes:84367213640 (78.5 GiB) TX bytes:73401695329 (68.3 GiB)
Interrupt:20 Memory:f4f00000-f4f20000 \n\
lo Link encap:Local Loopback \n\
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:16436 Metric:1
RX packets:27449267 errors:0 dropped:0 overruns:0 frame:0
TX packets:27449267 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0 \n\
RX bytes:192643017823 (179.4 GiB) TX bytes:192643017823 (179.4 GiB)
wlan0 Link encap:Ethernet HWaddr 90:f6:52:27:15:0a \n\
inet addr:192.168.0.2 Bcast:192.168.0.255 Mask:255.255.255.0
inet6 addr: fe80::92f6:52ff:fe27:150a/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:12352750 errors:0 dropped:0 overruns:0 frame:0
TX packets:4501451 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 \n\
RX bytes:3916475942 (3.6 GiB) TX bytes:458353654 (437.1 MiB)
"""
# This is actually from a VirtualBox VM running XP.
2020-07-24 17:47:56 +00:00
MOCK_ROUTE_OUTPUT = b"""\
===========================================================================
Interface List
0x1 ........................... MS TCP Loopback interface
0x2 ...08 00 27 c3 80 ad ...... AMD PCNET Family PCI Ethernet Adapter - Packet Scheduler Miniport
===========================================================================
===========================================================================
Active Routes:
Network Destination Netmask Gateway Interface Metric
0.0.0.0 0.0.0.0 10.0.2.2 10.0.2.15 20
10.0.2.0 255.255.255.0 10.0.2.15 10.0.2.15 20
10.0.2.15 255.255.255.255 127.0.0.1 127.0.0.1 20
10.255.255.255 255.255.255.255 10.0.2.15 10.0.2.15 20
127.0.0.0 255.0.0.0 127.0.0.1 127.0.0.1 1
224.0.0.0 240.0.0.0 10.0.2.15 10.0.2.15 20
255.255.255.255 255.255.255.255 10.0.2.15 10.0.2.15 1
Default Gateway: 10.0.2.2
===========================================================================
Persistent Routes:
None
"""
UNIX_TEST_ADDRESSES = set(["127.0.0.1", "192.168.0.6", "192.168.0.2", "192.168.0.10"])
WINDOWS_TEST_ADDRESSES = set(["127.0.0.1", "10.0.2.15", "192.168.0.10"])
CYGWIN_TEST_ADDRESSES = set(["127.0.0.1", "192.168.0.10"])
2019-05-15 06:17:44 +00:00
class FakeProcess(object):
def __init__(self, output, err):
self.output = output
self.err = err
def communicate(self):
return (self.output, self.err)
class ListAddresses(testutil.SignalMixin, unittest.TestCase):
def test_get_local_ip_for(self):
addr = iputil.get_local_ip_for('127.0.0.1')
self.failUnless(DOTTED_QUAD_RE.match(addr))
2020-07-24 17:47:56 +00:00
# Bytes can be taken as input:
bytes_addr = iputil.get_local_ip_for(b'127.0.0.1')
self.assertEqual(addr, bytes_addr)
# The output is a native string:
2020-07-27 15:14:01 +00:00
self.assertIsInstance(addr, native_str)
def test_list_async(self):
d = iputil.get_local_addresses_async()
def _check(addresses):
self.failUnlessIn("127.0.0.1", addresses)
self.failIfIn("0.0.0.0", addresses)
d.addCallbacks(_check)
return d
# David A.'s OpenSolaris box timed out on this test one time when it was at 2s.
test_list_async.timeout=4
def _test_list_async_mock(self, command, output, expected):
ns = Namespace()
ns.first = True
def call_Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None,
universal_newlines=False, startupinfo=None, creationflags=0):
if ns.first:
ns.first = False
e = OSError("EINTR")
e.errno = errno.EINTR
raise e
elif os.path.basename(args[0]) == command:
return FakeProcess(output, "")
else:
e = OSError("[Errno 2] No such file or directory")
e.errno = errno.ENOENT
raise e
self.patch(subprocess, 'Popen', call_Popen)
Avoid Popen() of executables that don't exist The stdlib 'subprocess' module in python-2.7.4 through 2.7.7 suffers from http://bugs.python.org/issue18851 which causes unrelated file descriptors to be closed when `subprocess.call()` fails the `exec()`, such as when the executable being invoked does not actually exist. There appears to be some randomness involved. This was fixed in python-2.7.8. Tahoe's iputil.py uses subprocess.call on many different "ifconfig"-type executables, most of which don't exist on any given platform (added in git commit 8e31d66cd0b). This results in a lot of file-descriptor closing, which (at least during unit tests) tends to clobber important things like Tub TCP sockets. This seems to be the root cause behind ticket:2121, in which normal code tries to close already-closed sockets, crashing the unit tests. Since different platforms have different ifconfigs, some platforms will experience more failed execs than others, so this bug could easily behave differently on linux vs freebsd, as well as working normally on python-2.7.8 or 2.7.4. This patch inserts a guard to make sure that os.path.isfile() is true before allowing Popen.call() to try executing the target. This ought to be enough to avoid the bug. It changes both iputil.py and allmydata.__init__ (which uses Popen for calling "lsb_release"), which are all the places where 'subprocess' is used outside of unit tests. Other potential fixes: use the 'subprocess32' module from PyPI (which is a bug-free backport of the Python3 stdlib subprocess module, but would introduce a new dependency), or require python >= 2.7.8 (but this would rule out development/deployment on the current OS-X 10.9 release, which ships with 2.7.5, as well as other distributions like Ubuntu 14.04 LTS). I believe this closes ticket:2121, and given the apparent relationship between 2121 and 2023, I think it also closes ticket:2023 (although since 2023 doesn't have copies of the failing log files, it's hard to tell). I'm hoping that this will tide us over until 1.11 is released, at which point we can execute on the plan to remove iputil.py entirely by changing the way that nodes learn their externally-facing IP address.
2014-09-11 20:13:42 +00:00
self.patch(os.path, 'isfile', lambda x: True)
def call_get_local_ip_for(target):
if target in ("localhost", "127.0.0.1"):
return "127.0.0.1"
else:
return "192.168.0.10"
self.patch(iputil, 'get_local_ip_for', call_get_local_ip_for)
def call_which(name):
return [name]
self.patch(iputil, 'which', call_which)
d = iputil.get_local_addresses_async()
def _check(addresses):
self.failUnlessEquals(set(addresses), set(expected))
d.addCallbacks(_check)
return d
def test_list_async_mock_ip_addr(self):
self.patch(iputil, 'platform', "linux2")
return self._test_list_async_mock("ip", MOCK_IPADDR_OUTPUT, UNIX_TEST_ADDRESSES)
def test_list_async_mock_ifconfig(self):
self.patch(iputil, 'platform', "linux2")
return self._test_list_async_mock("ifconfig", MOCK_IFCONFIG_OUTPUT, UNIX_TEST_ADDRESSES)
def test_list_async_mock_route(self):
self.patch(iputil, 'platform', "win32")
return self._test_list_async_mock("route.exe", MOCK_ROUTE_OUTPUT, WINDOWS_TEST_ADDRESSES)
def test_list_async_mock_cygwin(self):
self.patch(iputil, 'platform', "cygwin")
return self._test_list_async_mock(None, None, CYGWIN_TEST_ADDRESSES)
2020-07-27 17:06:41 +00:00
class ListenOnUsed(unittest.TestCase):
"""Tests for listenOnUnused."""
def create_tub(self, basedir):
os.makedirs(basedir)
tubfile = os.path.join(basedir, "tub.pem")
tub = Tub(certFile=tubfile)
tub.setOption("expose-remote-exception-types", False)
tub.startService()
self.addCleanup(tub.stopService)
return tub
@retry(stop=stop_after_attempt(7))
2020-07-27 17:06:41 +00:00
def test_random_port(self):
"""A random port is selected if none is given."""
tub = self.create_tub("utils/ListenOnUsed/test_randomport")
self.assertEqual(len(tub.getListeners()), 0)
portnum = iputil.listenOnUnused(tub)
# We can connect to this port:
s = socket.socket()
s.connect(("127.0.0.1", portnum))
s.close()
self.assertEqual(len(tub.getListeners()), 1)
# Listen on another port:
tub2 = self.create_tub("utils/ListenOnUsed/test_randomport_2")
portnum2 = iputil.listenOnUnused(tub2)
self.assertNotEqual(portnum, portnum2)
2020-07-27 19:16:25 +00:00
@retry(stop=stop_after_attempt(7))
2020-07-27 19:16:25 +00:00
def test_specific_port(self):
"""The given port is used."""
tub = self.create_tub("utils/ListenOnUsed/test_givenport")
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
port2 = iputil.listenOnUnused(tub, port)
self.assertEqual(port, port2)
2020-07-29 18:46:49 +00:00
class GcUtil(unittest.TestCase):
"""Tests for allmydata.util.gcutil, which is used only by listenOnUnused."""
def test_gc_after_allocations(self):
"""The resource tracker triggers allocations every 26 allocations."""
2020-08-03 14:43:21 +00:00
tracker = gcutil._ResourceTracker()
2020-07-29 18:46:49 +00:00
collections = []
self.patch(gc, "collect", lambda: collections.append(1))
for _ in range(2):
for _ in range(25):
2020-08-03 14:43:21 +00:00
tracker.allocate()
2020-07-29 18:46:49 +00:00
self.assertEqual(len(collections), 0)
2020-08-03 14:43:21 +00:00
tracker.allocate()
2020-07-29 18:46:49 +00:00
self.assertEqual(len(collections), 1)
del collections[:]
def test_release_delays_gc(self):
"""Releasing a file descriptor resource delays GC collection."""
2020-08-03 14:43:21 +00:00
tracker = gcutil._ResourceTracker()
2020-07-29 18:46:49 +00:00
collections = []
self.patch(gc, "collect", lambda: collections.append(1))
for _ in range(2):
2020-08-03 14:43:21 +00:00
tracker.allocate()
2020-07-29 18:46:49 +00:00
for _ in range(3):
2020-08-03 14:43:21 +00:00
tracker.release()
2020-07-29 18:46:49 +00:00
for _ in range(25):
2020-08-03 14:43:21 +00:00
tracker.allocate()
2020-07-29 18:46:49 +00:00
self.assertEqual(len(collections), 0)
2020-08-03 14:43:21 +00:00
tracker.allocate()
2020-07-29 18:46:49 +00:00
self.assertEqual(len(collections), 1)