← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/netifaces into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/netifaces into lp:maas.

Commit message:
Replace ifconfig-parser with netifaces.  We need this to avoid fork()ing off ifconfig, which confuses Upstart's fork tracking.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jtv/maas/netifaces/+merge/127420

Briefly discussed with Julian.  This one's needed to get tests passing on the branch that will make celery run as the maas user: we were forking once to get our networks information *before* the crucial fork of the celery "daemon" which we wanted upstart to track.


Jeroen
-- 
https://code.launchpad.net/~jtv/maas/netifaces/+merge/127420
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/netifaces into lp:maas.
=== modified file 'required-packages/base'
--- required-packages/base	2012-09-22 15:38:25 +0000
+++ required-packages/base	2012-10-02 06:34:21 +0000
@@ -23,6 +23,7 @@
 python-httplib2
 python-lockfile
 python-netaddr
+python-netifaces
 python-oauth
 python-oops
 python-oops-amqp

=== modified file 'src/provisioningserver/network.py'
--- src/provisioningserver/network.py	2012-09-26 17:21:53 +0000
+++ src/provisioningserver/network.py	2012-10-02 06:34:21 +0000
@@ -18,17 +18,21 @@
     'discover_networks',
     ]
 
-import os
-from subprocess import check_output
+
+from netifaces import (
+    AF_INET,
+    ifaddresses,
+    interfaces,
+    )
 
 
 class InterfaceInfo:
     """The details of a network interface we are interested in."""
 
-    def __init__(self, interface):
+    def __init__(self, interface, ip=None, subnet_mask=None):
         self.interface = interface
-        self.ip = None
-        self.subnet_mask = None
+        self.ip = ip
+        self.subnet_mask = subnet_mask
 
     def may_be_subnet(self):
         """Could this be a subnet that MAAS is interested in?"""
@@ -51,57 +55,24 @@
         }
 
 
-def run_ifconfig():
-    """Run `ifconfig` to list active interfaces.  Return output."""
-    env = dict(os.environ, LC_ALL='C')
-    output = check_output(['/sbin/ifconfig'], env=env)
-    return output.decode('ascii')
-
-
-def extract_ip_and_subnet_mask(line):
-    """Get IP address and subnet mask from an inet address line."""
-    # This line consists of key:value pairs separated by double spaces.
-    # The "inet addr" key contains a space.  There is typically a
-    # trailing separator.
-    settings = dict(
-        tuple(pair.split(':', 1))
-        for pair in line.split('  '))
-    return settings.get('inet addr'), settings.get('Mask')
-
-
-def parse_stanza(stanza):
-    """Return a :class:`InterfaceInfo` representing this ifconfig stanza.
-
-    May return `None` if it's immediately clear that the interface is not
-    relevant for MAAS.
-    """
-    lines = [line.strip() for line in stanza.splitlines()]
-    header = lines[0]
-    if 'Link encap:Ethernet' not in header:
-        return None
-    info = InterfaceInfo(header.split()[0])
-    for line in lines[1:]:
-        if line.split()[0] == 'inet':
-            info.ip, info.subnet_mask = extract_ip_and_subnet_mask(line)
-    return info
-
-
-def split_stanzas(output):
-    """Split `ifconfig` output into stanzas, one per interface."""
-    stanzas = [stanza.strip() for stanza in output.strip().split('\n\n')]
-    return [stanza for stanza in stanzas if len(stanza) > 0]
-
-
-def parse_ifconfig(output):
-    """List `InterfaceInfo` for each interface found in `ifconfig` output."""
-    infos = [parse_stanza(stanza) for stanza in split_stanzas(output)]
-    return [info for info in infos if info is not None]
+def get_interface_info(interface):
+    """Return a list of `InterfaceInfo` for the named `interface`."""
+    addresses = ifaddresses(interface).get(AF_INET)
+    if addresses is None:
+        return []
+    else:
+        return [
+            InterfaceInfo(
+                interface, ip=address.get('addr'),
+                subnet_mask=address.get('netmask'))
+            for address in addresses]
 
 
 def discover_networks():
     """Find the networks attached to this system."""
-    output = run_ifconfig()
+    infos = sum(
+        [get_interface_info(interface) for interface in interfaces()], [])
     return [
-        interface.as_dict()
-        for interface in parse_ifconfig(output)
-            if interface.may_be_subnet()]
+        info.as_dict()
+        for info in infos
+            if info.may_be_subnet()]

=== modified file 'src/provisioningserver/tests/test_network.py'
--- src/provisioningserver/tests/test_network.py	2012-09-26 17:21:53 +0000
+++ src/provisioningserver/tests/test_network.py	2012-10-02 06:34:21 +0000
@@ -12,267 +12,79 @@
 __metaclass__ = type
 __all__ = []
 
-from random import (
-    choice,
-    randint,
-    )
-
 from maastesting.factory import factory
 from maastesting.testcase import TestCase
+from netaddr import IPNetwork
+from netifaces import AF_INET
 from provisioningserver import network
 
 
-def make_address_line(**kwargs):
-    """Create an inet address line."""
-    # First word on this line is inet or inet6.
-    kwargs.setdefault('inet', 'inet')
-    kwargs.setdefault('broadcast', '10.255.255.255')
-    kwargs.setdefault('subnet_mask', '255.0.0.0')
-    items = [
-        "%(inet)s addr:%(ip)s"
-        ]
-    if len(kwargs['broadcast']) > 0:
-        items.append("Bcast:%(broadcast)s")
-    items.append("Mask:%(subnet_mask)s")
-    return '  '.join(items) % kwargs
-
-
-def make_stats_line(direction, **kwargs):
-    """Create one of the incoming/outcoming packet-count lines."""
-    assert direction in {'RX', 'TX'}
-    if direction == 'RX':
-        variable_field = 'frame'
-    else:
-        variable_field = 'carrier'
-    kwargs.setdefault('variable_field', variable_field)
-    kwargs.setdefault('packets', randint(0, 100000))
-    kwargs.setdefault('errors', randint(0, 100))
-    kwargs.setdefault('dropped', randint(0, 100))
-    kwargs.setdefault('overruns', randint(0, 100))
-    kwargs.setdefault('variable', randint(0, 100))
-
-    return " ".join([
-        direction,
-        "packets:%(packets)d",
-        "errors:%(errors)d",
-        "dropped:%(dropped)d",
-        "overruns:%(overruns)d",
-        "%(variable_field)s:%(variable)d"
-        ]) % kwargs
-
-
-def make_payload_stats(direction, **kwargs):
-    assert direction in {'RX', 'TX'}
-    kwargs.setdefault('bytes', randint(0, 1000000))
-    kwargs.setdefault('bigger_unit', randint(10, 10240) / 10.0)
-    kwargs.setdefault('unit', choice(['B', 'KB', 'GB']))
-    return " ".join([
-        direction,
-        "bytes:%(bytes)s",
-        "(%(bigger_unit)d %(unit)s)",
-        ]) % kwargs
-
-
-def make_stanza(**kwargs):
-    """Create an ifconfig output stanza.
-
-    Variable values can be specified, but will be given random values by
-    default.  Values that interfaces may not have, such as broadcast
-    address or allocated interrupt, may be set to the empty string to
-    indicate that they should be left out of the output.
-    """
-    kwargs.setdefault('interface', factory.make_name('eth'))
-    kwargs.setdefault('encapsulation', 'Ethernet')
-    kwargs.setdefault('mac', factory.getRandomMACAddress())
-    kwargs.setdefault('ip', factory.getRandomIPAddress())
-    kwargs.setdefault('broadcast', factory.getRandomIPAddress())
-    kwargs.setdefault('mtu', randint(100, 10000))
-    kwargs.setdefault('rxline', make_stats_line('RX', **kwargs))
-    kwargs.setdefault('txline', make_stats_line('TX', **kwargs))
-    kwargs.setdefault('collisions', randint(0, 100))
-    kwargs.setdefault('txqueuelen', randint(0, 100))
-    kwargs.setdefault('rxbytes', make_payload_stats('RX', **kwargs))
-    kwargs.setdefault('txbytes', make_payload_stats('TX', **kwargs))
-    kwargs.setdefault('interrupt', randint(1, 30))
-
-    # The real-life output seems to have two trailing spaces here.
-    header = "%(interface)s Link encap:%(encapsulation)s  HWaddr %(mac)s  "
-    body_lines = [
-        "UP BROADCAST MULTICAST  MTU:%(mtu)d  Metric:1",
-        ]
-    if len(kwargs['ip']) > 0:
-        body_lines.append(make_address_line(inet='inet', **kwargs))
-    body_lines += [
-        "%(rxline)s",
-        "%(txline)s",
-        # This line has a trailing space in the real-life output.
-        "collisions:%(collisions)d txqueuelen:%(txqueuelen)d ",
-        "%(rxbytes)s  %(txbytes)s",
-        ]
-    if kwargs['interrupt'] != '':
-        body_lines.append("Interrupt:%(interrupt)d")
-
-    text = '\n'.join(
-        [header] +
-        [(10 * " ") + line for line in body_lines])
-    return (text + "\n") % kwargs
-
-
-def join_stanzas(stanzas):
-    """Format a sequence of interface stanzas like ifconfig does."""
-    return '\n'.join(stanzas) + '\n'
-
-
-# Tragically can't afford to indent and then dedent() this.  This output
-# isn't entirely realistic: the real output has trailing spaces here and
-# there, which we don't tolerate in our source code.
-sample_output = """\
-eth0      Link encap:Ethernet  HWaddr 00:25:bc:e6:0b:c2
-          UP BROADCAST MULTICAST  MTU:1500  Metric:1
-          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:1000
-          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
-
-eth1      Link encap:Ethernet  HWaddr 00:14:73:ad:29:62
-          inet addr:192.168.12.103  Bcast:192.168.12.255  Mask:255.255.255.0
-          inet6 addr: fe81::210:9ff:fcd3:6120/64 Scope:Link
-          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
-          RX packets:5272 errors:1 dropped:0 overruns:0 frame:3274
-          TX packets:5940 errors:2 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:1000
-          RX bytes:2254714 (2.2 MB)  TX bytes:4045385 (4.0 MB)
-          Interrupt:22
-
-lo        Link encap:Local Loopback
-          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:297493 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:297493 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:0
-          RX bytes:43708 (43.7 KB)  TX bytes:43708 (43.7 KB)
-
-maasbr0   Link encap:Ethernet  HWaddr 46:a1:20:8b:77:14
-          inet addr:192.168.64.1  Bcast:192.168.64.255  Mask:255.255.255.0
-          UP BROADCAST MULTICAST  MTU:1500  Metric:1
-          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:0
-          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
-
-virbr0    Link encap:Ethernet  HWaddr 68:14:23:c0:6d:bf
-          inet addr:192.168.80.1  Bcast:192.168.80.255  Mask:255.255.255.0
-          UP BROADCAST MULTICAST  MTU:1500  Metric:1
-          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
-          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
-          collisions:0 txqueuelen:0
-          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
-
-    """
+def make_inet_address(subnet=None):
+    """Fake an AF_INET address."""
+    if subnet is None:
+        subnet = factory.getRandomNetwork()
+    return {
+        'broadcast': subnet.broadcast,
+        'netmask': subnet.netmask,
+        'addr': factory.getRandomIPInNetwork(subnet),
+    }
+
+
+def make_loopback():
+    """Fake a loopback AF_INET address."""
+    return make_inet_address(IPNetwork('127.0.0.0/8'))
+
+
+def make_interface(inet_address=None):
+    """Minimally fake up an interface definition as returned by netifaces."""
+    if inet_address is None:
+        inet_address = make_inet_address()
+    return {AF_INET: [inet_address]}
 
 
 class TestNetworks(TestCase):
 
-    def test_run_ifconfig_returns_ifconfig_output(self):
-        text = join_stanzas([make_stanza()])
-        self.patch(network, 'check_output').return_value = text.encode('ascii')
-        self.assertEqual(text, network.run_ifconfig())
-
-    def test_parse_ifconfig_produces_interface_info(self):
-        num_interfaces = randint(1, 3)
-        text = join_stanzas([
-            make_stanza()
-            for counter in range(num_interfaces)])
-        info = network.parse_ifconfig(text)
-        self.assertEqual(num_interfaces, len(info))
-        self.assertIsInstance(info[0], network.InterfaceInfo)
-
-    def test_parse_stanza_reads_interface_with_ip_and_interrupt(self):
-        parms = {
-            'interface': factory.make_name('eth'),
-            'ip': factory.getRandomIPAddress(),
-            'subnet_mask': '255.255.255.128',
-        }
-        info = network.parse_stanza(make_stanza(**parms))
-        self.assertEqual(parms, info.as_dict())
-
-    def test_parse_stanza_reads_interface_without_interrupt(self):
-        parms = {
-            'interface': factory.make_name('eth'),
-            'ip': factory.getRandomIPAddress(),
-            'subnet_mask': '255.255.255.128',
-            'interrupt': '',
-        }
-        info = network.parse_stanza(make_stanza(**parms))
-        expected = parms.copy()
-        del expected['interrupt']
-        self.assertEqual(expected, info.as_dict())
-
-    def test_parse_stanza_reads_interface_without_ip(self):
-        parms = {
-            'interface': factory.make_name('eth'),
-            'ip': '',
-        }
-        info = network.parse_stanza(make_stanza(**parms))
-        expected = parms.copy()
-        expected['ip'] = None
-        expected['subnet_mask'] = None
-        self.assertEqual(expected, info.as_dict())
-
-    def test_parse_stanza_returns_nothing_for_loopback(self):
-        parms = {
-            'interface': 'lo',
-            'ip': '127.1.2.3',
-            'subnet_mask': '255.0.0.0',
-            'encapsulation': 'Local Loopback',
-            'broadcast': '',
-            'interrupt': '',
-        }
-        self.assertIsNone(network.parse_stanza(make_stanza(**parms)))
-
-    def test_split_stanzas_returns_empty_for_empty_input(self):
-        self.assertEqual([], network.split_stanzas(''))
-
-    def test_split_stanzas_returns_single_stanza(self):
-        stanza = make_stanza()
-        self.assertEqual([stanza.strip()], network.split_stanzas(stanza))
-
-    def test_split_stanzas_splits_multiple_stanzas(self):
-        stanzas = [make_stanza() for counter in range(3)]
-        full_output = join_stanzas(stanzas)
+    def patch_netifaces(self, interfaces):
+        """Patch up netifaces to pretend we have given `interfaces`.
+
+        :param interfaces: A dict mapping each interface's name to its
+            definition as `netifaces` would return it.
+        """
+        self.patch(network, 'interfaces').return_value = interfaces.keys()
+        self.patch(
+            network, 'ifaddresses', lambda interface: interfaces[interface])
+
+    def test_discover_networks_ignores_interface_without_IP_address(self):
+        self.patch_netifaces({factory.make_name('eth'): {}})
+        self.assertEqual([], network.discover_networks())
+
+    def test_discover_networks_ignores_loopback(self):
+        self.patch_netifaces({'lo': make_interface(make_loopback())})
+        self.assertEqual([], network.discover_networks())
+
+    def test_discover_networks_represents_interface(self):
+        eth = factory.make_name('eth')
+        interface = make_interface()
+        self.patch_netifaces({eth: interface})
+        self.assertEqual([{
+            'interface': eth,
+            'ip': interface[AF_INET][0]['addr'],
+            'subnet_mask': interface[AF_INET][0]['netmask'],
+            }],
+            network.discover_networks())
+
+    def test_discover_networks_returns_suitable_interfaces(self):
+        eth = factory.make_name('eth')
+        self.patch_netifaces({
+            eth: make_interface(),
+            'lo': make_interface(make_loopback()),
+            factory.make_name('dummy'): make_interface({}),
+            })
         self.assertEqual(
-            [stanza.strip() for stanza in stanzas],
-            network.split_stanzas(full_output))
+            [eth], [
+                interface['interface']
+                for interface in network.discover_networks()])
 
     def test_discover_networks_runs_in_real_life(self):
         interfaces = network.discover_networks()
         self.assertIsInstance(interfaces, list)
-
-    def test_discover_networks_returns_suitable_interfaces(self):
-        params = {
-            'interface': factory.make_name('eth'),
-            'ip': factory.getRandomIPAddress(),
-            'subnet_mask': '255.255.255.0',
-        }
-        regular_interface = make_stanza(**params)
-        loopback = make_stanza(
-            interface='lo', encapsulation='Local loopback', broadcast='',
-            interrupt='')
-        disabled_interface = make_stanza(ip='', broadcast='', subnet_mask='')
-
-        text = join_stanzas([regular_interface, loopback, disabled_interface])
-        self.patch(network, 'run_ifconfig').return_value = text
-
-        interfaces = network.discover_networks()
-
-        self.assertEqual(
-            [params],
-            [interface for interface in interfaces])
-
-    def test_discover_networks_processes_real_ifconfig_output(self):
-        self.patch(network, 'run_ifconfig').return_value = sample_output
-        info = network.discover_networks()
-        self.assertEqual(
-            ['eth1', 'maasbr0', 'virbr0'],
-            [interface['interface'] for interface in info])