Repository: ftao/python-ifcfg

Issue:
ifcfg does not see multiple ip addresses on one interfaces
Hello,

using 

```
>>> ifcfg.__version__
'0.13'
```

on a system where eth0 has more than one IP address:

```
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:50:39:69 brd ff:ff:ff:ff:ff:ff
    inet 192.168.13.1/24 brd 192.168.13.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 192.168.10.3/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::ba27:ebff:fe50:3969/64 scope link
       valid_lft forever preferred_lft forever
```

ifcfg only sees one of them:

```
Python 3.5.3 (default, Jan 19 2017, 14:11:04)
[GCC 6.3.0 20170124] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ifcfg
>>> from pprint import pprint
>>> ifaces = ifcfg.interfaces()
>>> pprint(ifaces['eth0'])
{'broadcast': '192.168.13.255',
 'device': 'eth0',
 'ether': 'b8:27:eb:50:39:69',
 'flags': '4163<UP,BROADCAST,RUNNING,MULTICAST> ',
 'inet': '192.168.13.1',
 'inet6': ['fe80::ba27:ebff:fe50:3969'],
 'mtu': '1500',
 'netmask': '255.255.255.0'}
```

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 68223c5..88220c4 100644
--- a/README.rst
+++ b/README.rst
@@ -23,7 +23,8 @@ Usage
     for name, interface in ifcfg.interfaces().items():
         # do something with interface
         print interface['device']
-        print interface['inet']
+        print interface['inet']         # First IPv4 found
+        print interface['inet4']        # List of ips
         print interface['inet6']
         print interface['netmask']
         print interface['broadcast']
@@ -43,6 +44,7 @@ following:
             "flags": "4163<up,broadcast,running,multicast> ",
             "hostname": "derks-vm.local",
             "inet": "172.16.217.10",
+            "inet4": ["172.16.217.10"],
             "inet6": ["fe80::20c:29ff:fe0c:da5d"],
             "mtu": "1500",
             "name": "eth0",
@@ -53,6 +55,7 @@ following:
             "flags": "73<up,loopback,running> ",
             "hostname": "localhost",
             "inet": "127.0.0.1",
+            "inet4": ["127.0.0.1"],
             "inet6": ["::1"],
             "mtu": "16436",
             "name": "lo",
@@ -64,6 +67,7 @@ following:
             "flags": "4099<up,broadcast,multicast> ",
             "hostname": "derks-vm.local",
             "inet": "192.168.122.1",
+            "inet4": ["192.168.122.1"],
             "inet6": [],
             "mtu": "1500",
             "name": "virbr0",
@@ -75,6 +79,8 @@ following:
 Release notes
 -------------
 
+* Support for multiple IPv4 addresses in the new 'inet4' field
+
 0.15
 ____
 
diff --git a/src/ifcfg/parser.py b/src/ifcfg/parser.py
index 22ceeb8..f22135f 100644
--- a/src/ifcfg/parser.py
+++ b/src/ifcfg/parser.py
@@ -13,6 +13,7 @@ Log = minimal_logger(__name__)
 #: These will always be present for each device (even if they are None)
 DEVICE_PROPERTY_DEFAULTS = {
     'inet': None,
+    'inet4': "LIST",
     'ether': None,
     'inet6': "LIST",  # Because lists are mutable
     'netmask': None,
@@ -78,6 +79,10 @@ class Parser(object):
                             self._interfaces[cur][k] = v
                         elif hasattr(self._interfaces[cur][k], 'append'):
                             self._interfaces[cur][k].append(v)
+                        elif self._interfaces[cur][k] == v:
+                            # Silently ignore if the it's the same value as last. Example: Multiple
+                            # inet4 addresses, result in multiple netmasks. Cardinality mismatch
+                            continue
                         else:
                             raise RuntimeError(
                                 "Tried to add {}={} multiple times to {}, it was already: {}".format(
@@ -90,6 +95,11 @@ class Parser(object):
                     else:
                         self._interfaces[cur][k] = v
 
+        # Copy the first 'inet4' ip address to 'inet' for backwards compatibility
+        for device, device_dict in self._interfaces.items():
+            if len(device_dict['inet4']) > 0:
+                device_dict['inet'] = device_dict['inet4'][0]
+
         # fix it up
         self._interfaces = self.alter(self._interfaces)
 
@@ -108,6 +118,8 @@ class Parser(object):
         """
         # fixup some things
         for device, device_dict in interfaces.items():
+            if len(device_dict['inet4']) > 0:
+                device_dict['inet'] = device_dict['inet4'][0]
             if 'inet' in device_dict and not device_dict['inet'] is None:
                 try:
                     host = socket.gethostbyaddr(device_dict['inet'])[0]
@@ -174,7 +186,7 @@ class WindowsParser(Parser):
         return [
             r"^(?P<device>\w.+):",
             r"^   Physical Address. . . . . . . . . : (?P<ether>[ABCDEFabcdef\d-]+)",
-            r"^   IPv4 Address. . . . . . . . . . . : (?P<inet>[^\s\(]+)",
+            r"^   IPv4 Address. . . . . . . . . . . : (?P<inet4>[^\s\(]+)",
             r"^   IPv6 Address. . . . . . . . . . . : (?P<inet6>[ABCDEFabcdef\d\:\%]+)",
         ]
 
@@ -210,7 +222,7 @@ class UnixParser(Parser):
     def get_patterns(cls):
         return [
             '(?P<device>^[-a-zA-Z0-9:]+): flags=(?P<flags>.*) mtu (?P<mtu>.*)',
-            '.*inet\s+(?P<inet>[\d\.]+).*',
+            '.*inet\s+(?P<inet4>[\d\.]+).*',
             '.*inet6\s+(?P<inet6>[\d\:abcdef]+).*',
             '.*broadcast (?P<broadcast>[^\s]*).*',
             '.*netmask (?P<netmask>[^\s]*).*',
@@ -257,7 +269,7 @@ class LinuxParser(UnixParser):
         return super(LinuxParser, cls).get_patterns() + [
             '(?P<device>^[a-zA-Z0-9:_-]+)(.*)Link encap:(.*).*',
             '(.*)Link encap:(.*)(HWaddr )(?P<ether>[^\s]*).*',
-            '.*(inet addr:\s*)(?P<inet>[^\s]+).*',
+            '.*(inet addr:\s*)(?P<inet4>[^\s]+).*',
             '.*(inet6 addr:\s*)(?P<inet6>[^\s\/]+)',
             '.*(P-t-P:)(?P<ptp>[^\s]*).*',
             '.*(Bcast:)(?P<broadcast>[^\s]*).*',
@@ -288,7 +300,7 @@ class UnixIPParser(UnixParser):
     def get_patterns(cls):
         return [
             '\s*[0-9]+:\s+(?P<device>[a-zA-Z0-9]+):.*mtu (?P<mtu>.*)',
-            '.*(inet\s)(?P<inet>[\d\.]+)',
+            '.*(inet\s)(?P<inet4>[\d\.]+)',
             '.*(inet6 )(?P<inet6>[^/]*).*',
             '.*(ether )(?P<ether>[^\s]*).*',
             '.*inet\s.*(brd )(?P<broadcast>[^\s]*).*',


Test patch:
diff --git a/tests/ifconfig_out.py b/tests/ifconfig_out.py
index c0f557e..0f788ba 100644
--- a/tests/ifconfig_out.py
+++ b/tests/ifconfig_out.py
@@ -132,6 +132,72 @@ lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
 """  # noqa
 
 
+MACOSX2 = """
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
+        options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
+        inet 127.0.0.1 netmask 0xff000000
+        inet6 ::1 prefixlen 128
+        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
+        inet 127.0.1.99 netmask 0xff000000
+        nd6 options=201<PERFORMNUD,DAD>
+en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+        ether 8c:85:90:00:aa:bb
+        inet6 fe80::1c8b:e7cc:c695:a6de%en0 prefixlen 64 secured scopeid 0x6
+        inet 10.0.1.12 netmask 0xffff0000 broadcast 10.0.255.255
+        inet6 2601:980:c000:7c5a:da:6a59:1e94:b03 prefixlen 64 autoconf secured
+        inet6 2601:980:c000:7c5a:fdb3:b90c:80d6:abcd prefixlen 64 deprecated autoconf temporary
+        inet6 2601:980:c000:7c5a:34b0:676d:93f7:0123 prefixlen 64 autoconf temporary
+        nd6 options=201<PERFORMNUD,DAD>
+        media: autoselect
+        status: active
+en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
+        options=60<TSO4,TSO6>
+        ether 7a:00:48:a1:b2:00
+        media: autoselect <full-duplex>
+        status: inactive
+p2p0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 2304
+        ether 0e:85:90:0f:ab:cd
+        media: autoselect
+        status: inactive
+awdl0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1484
+        ether 2e:51:48:37:99:0a
+        inet6 fe80::2c51:48ff:fe37:86f8%awdl0 prefixlen 64 scopeid 0xc
+        nd6 options=201<PERFORMNUD,DAD>
+        media: autoselect
+        status: active
+bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+        options=63<RXCSUM,TXCSUM,TSO4,TSO6>
+        ether 7a:00:48:c8:aa:bd
+        Configuration:
+                id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
+                maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
+                root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
+                ipfilter disabled flags 0x2
+        member: en1 flags=3<LEARNING,DISCOVER>
+                ifmaxaddr 0 port 7 priority 0 path cost 0
+        member: en2 flags=3<LEARNING,DISCOVER>
+                ifmaxaddr 0 port 8 priority 0 path cost 0
+        member: en3 flags=3<LEARNING,DISCOVER>
+                ifmaxaddr 0 port 9 priority 0 path cost 0
+        member: en4 flags=3<LEARNING,DISCOVER>
+                ifmaxaddr 0 port 10 priority 0 path cost 0
+        nd6 options=201<PERFORMNUD,DAD>
+        media: <unknown type>
+        status: inactive
+utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 2000
+        inet6 fe80::ebb7:7d4a:49e8:e4fc%utun0 prefixlen 64 scopeid 0xe
+        nd6 options=201<PERFORMNUD,DAD>
+vboxnet0: flags=8842<BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+        ether 0a:00:27:00:00:00
+en5: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+        ether ac:de:48:00:22:11
+        inet6 fe80::aede:48ff:fe00:2211%en5 prefixlen 64 scopeid 0x5
+        nd6 options=281<PERFORMNUD,INSECURE,DAD>
+        media: autoselect
+        status: active
+"""
+
+
 ROUTE_OUTPUT = """
 Kernel IP routing table
 Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
diff --git a/tests/ifconfig_tests.py b/tests/ifconfig_tests.py
index 1b3c324..dc662d3 100644
--- a/tests/ifconfig_tests.py
+++ b/tests/ifconfig_tests.py
@@ -88,6 +88,16 @@ class IfcfgTestCase(IfcfgTestCase):
         eq_(interfaces['en0']['broadcast'], '192.168.0.255')
         eq_(interfaces['en0']['netmask'], '255.255.255.0')
 
+    def test_macosx2(self):
+        ifcfg.distro = 'MacOSX'
+        ifcfg.Parser = ifcfg.get_parser_class()
+        parser = ifcfg.get_parser(ifconfig=ifconfig_out.MACOSX2)
+        interfaces = parser.interfaces
+        self.assertEqual(len(interfaces.keys()), 9)
+        eq_(interfaces['lo0']['inet'], '127.0.0.1')
+        eq_(interfaces['lo0']['inet4'], ['127.0.0.1', '127.0.1.99'])
+        eq_(interfaces['lo0']['netmask'], '255.0.0.0')
+
     def test_default_interface(self):
         ifcfg.distro = 'Linux'
         ifcfg.Parser = ifcfg.get_parser_class()
diff --git a/tests/ip_out.py b/tests/ip_out.py
index 05ef84a..b4721e3 100644
--- a/tests/ip_out.py
+++ b/tests/ip_out.py
@@ -27,6 +27,18 @@ LINUX = """1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN g
     link/ether a0:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
 """
 
+LINUX_MULTI_IPV4 = """
+2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
+    link/ether b8:27:eb:50:39:69 brd ff:ff:ff:ff:ff:ff
+    inet 192.168.13.1/24 brd 192.168.13.255 scope global eth0
+       valid_lft forever preferred_lft forever
+    inet 192.168.10.3/24 scope global eth0
+       valid_lft forever preferred_lft forever
+    inet6 fe80::ba27:ebff:fe50:3969/64 scope link
+       valid_lft forever preferred_lft forever
+"""
+
+
 ROUTE_OUTPUT = """
 Kernel IP routing table
 Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
diff --git a/tests/ip_tests.py b/tests/ip_tests.py
index 7f6f8fe..dbe9344 100644
--- a/tests/ip_tests.py
+++ b/tests/ip_tests.py
@@ -31,6 +31,17 @@ class IpTestCase(IfcfgTestCase):
         eq_(interfaces['wlp3s0']['broadcast'], '192.168.12.255')
         eq_(interfaces['wlp3s0']['netmask'], '/24')
 
+    def test_linux_multi_inet4(self):
+        ifcfg.Parser = UnixIPParser
+        parser = ifcfg.get_parser(ifconfig=ip_out.LINUX_MULTI_IPV4)
+        interfaces = parser.interfaces
+        # Connected interface
+        eq_(interfaces['eth0']['ether'], 'b8:27:eb:50:39:69')
+        eq_(interfaces['eth0']['inet'], '192.168.13.1')
+        eq_(interfaces['eth0']['inet4'], ['192.168.13.1', '192.168.10.3'])
+        eq_(interfaces['eth0']['broadcast'], '192.168.13.255')
+        eq_(interfaces['eth0']['netmask'], '/24')
+
     def test_default_interface(self):
         ifcfg.distro = 'Linux'
         ifcfg.Parser = UnixIPParser