diff options
| author | Eric Lafontaine <eric.lafontaine@bell.ca> | 2019-12-09 19:48:37 -0500 | 
|---|---|---|
| committer | Ryan Harper <ryan.harper@canonical.com> | 2019-12-09 18:48:37 -0600 | 
| commit | a47405af306219d804ed262d6f7e2039a75883f2 (patch) | |
| tree | e26cac0fc0ebae67ee984e1901299f7dd9eff965 | |
| parent | 34ec440c1ad61c23c34b46f1798813d0f3ada952 (diff) | |
| download | vyos-cloud-init-a47405af306219d804ed262d6f7e2039a75883f2.tar.gz vyos-cloud-init-a47405af306219d804ed262d6f7e2039a75883f2.zip | |
dhcp: Support RedHat dhcp rfc3442 lease format for option 121 (#76)
RedHat dhcp client writes out rfc3442 classless-static-routes in a different format[1]
than what is found in isc-dhcp clients.  This patch adds support for the RedHat format.
1. Background details on the format
 https://bugzilla.redhat.com/show_bug.cgi?id=516325
 https://github.com/vaijab/fedora-dhcp/blob/e83fb19c51765442d77fa60596bfdb2b3b9fbe2e/dhcp-rfc3442-classless-static-routes.patch#L252 
 https://github.com/heftig/NetworkManager/blob/f56c82d86122fc45304fc829b5f1e4766ed51589/src/dhcp-manager/nm-dhcp-client.c#L978
LP: #1850642
| -rw-r--r-- | cloudinit/net/dhcp.py | 42 | ||||
| -rw-r--r-- | cloudinit/net/tests/test_dhcp.py | 65 | 
2 files changed, 100 insertions, 7 deletions
| diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 17379918..c033cc8e 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -92,9 +92,12 @@ class EphemeralDHCPv4(object):          nmap = {'interface': 'interface', 'ip': 'fixed-address',                  'prefix_or_mask': 'subnet-mask',                  'broadcast': 'broadcast-address', -                'static_routes': 'rfc3442-classless-static-routes', +                'static_routes': [ +                    'rfc3442-classless-static-routes', +                    'classless-static-routes' +                ],                  'router': 'routers'} -        kwargs = dict([(k, self.lease.get(v)) for k, v in nmap.items()]) +        kwargs = self.extract_dhcp_options_mapping(nmap)          if not kwargs['broadcast']:              kwargs['broadcast'] = bcip(kwargs['prefix_or_mask'], kwargs['ip'])          if kwargs['static_routes']: @@ -107,6 +110,25 @@ class EphemeralDHCPv4(object):          self._ephipv4 = ephipv4          return self.lease +    def extract_dhcp_options_mapping(self, nmap): +        result = {} +        for internal_reference, lease_option_names in nmap.items(): +            if isinstance(lease_option_names, list): +                self.get_first_option_value( +                    internal_reference, +                    lease_option_names, +                    result +                ) +            else: +                result[internal_reference] = self.lease.get(lease_option_names) +        return result + +    def get_first_option_value(self, internal_mapping, +                               lease_option_names, result): +        for different_names in lease_option_names: +            if not result.get(internal_mapping): +                result[internal_mapping] = self.lease.get(different_names) +  def maybe_perform_dhcp_discovery(nic=None):      """Perform dhcp discovery if nic valid and dhclient command exists. @@ -281,24 +303,30 @@ def parse_static_routes(rfc3442):      """ parse rfc3442 format and return a list containing tuple of strings.      The tuple is composed of the network_address (including net length) and -    gateway for a parsed static route. +    gateway for a parsed static route.  It can parse two formats of rfc3442, +    one from dhcpcd and one from dhclient (isc). -    @param rfc3442: string in rfc3442 format +    @param rfc3442: string in rfc3442 format (isc or dhcpd)      @returns: list of tuple(str, str) for all valid parsed routes until the                first parsing error.      E.g. -    sr = parse_state_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1") -    sr = [ +    sr=parse_static_routes("32,169,254,169,254,130,56,248,255,0,130,56,240,1") +    sr=[          ("169.254.169.254/32", "130.56.248.255"), ("0.0.0.0/0", "130.56.240.1")      ] +    sr2 = parse_static_routes("24.191.168.128 192.168.128.1,0 192.168.128.1") +    sr2 = [ +        ("191.168.128.0/24", "192.168.128.1"), ("0.0.0.0/0", "192.168.128.1") +    ] +      Python version of isc-dhclient's hooks:         /etc/dhcp/dhclient-exit-hooks.d/rfc3442-classless-routes      """      # raw strings from dhcp lease may end in semi-colon      rfc3442 = rfc3442.rstrip(";") -    tokens = rfc3442.split(',') +    tokens = [tok for tok in re.split(r"[, .]", rfc3442) if tok]      static_routes = []      def _trunc_error(cidr, required, remain): diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py index 91f503c9..c3fa1e04 100644 --- a/cloudinit/net/tests/test_dhcp.py +++ b/cloudinit/net/tests/test_dhcp.py @@ -90,6 +90,32 @@ class TestDHCPRFC3442(CiTestCase):          write_file(lease_file, content)          self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file)) +    def test_parse_lease_finds_classless_static_routes(self): +        """ +        parse_dhcp_lease_file returns classless-static-routes +        for Centos lease format. +        """ +        lease_file = self.tmp_path('leases') +        content = dedent(""" +            lease { +              interface "wlp3s0"; +              fixed-address 192.168.2.74; +              option subnet-mask 255.255.255.0; +              option routers 192.168.2.1; +              option classless-static-routes 0 130.56.240.1; +              renew 4 2017/07/27 18:02:30; +              expire 5 2017/07/28 07:08:15; +            } +        """) +        expected = [ +            {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74', +             'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1', +             'classless-static-routes': '0 130.56.240.1', +             'renew': '4 2017/07/27 18:02:30', +             'expire': '5 2017/07/28 07:08:15'}] +        write_file(lease_file, content) +        self.assertItemsEqual(expected, parse_dhcp_lease_file(lease_file)) +      @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')      @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')      def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4): @@ -112,6 +138,31 @@ class TestDHCPRFC3442(CiTestCase):              'router': '192.168.2.1'}          m_ipv4.assert_called_with(**expected_kwargs) +    @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network') +    @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') +    def test_obtain_centos_lease_parses_static_routes(self, m_maybe, m_ipv4): +        """ +        EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network +        for Centos Lease format +        """ +        lease = [ +            {'interface': 'wlp3s0', 'fixed-address': '192.168.2.74', +             'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1', +             'classless-static-routes': '0 130.56.240.1', +             'renew': '4 2017/07/27 18:02:30', +             'expire': '5 2017/07/28 07:08:15'}] +        m_maybe.return_value = lease +        eph = net.dhcp.EphemeralDHCPv4() +        eph.obtain_lease() +        expected_kwargs = { +            'interface': 'wlp3s0', +            'ip': '192.168.2.74', +            'prefix_or_mask': '255.255.255.0', +            'broadcast': '192.168.2.255', +            'static_routes': [('0.0.0.0/0', '130.56.240.1')], +            'router': '192.168.2.1'} +        m_ipv4.assert_called_with(**expected_kwargs) +  class TestDHCPParseStaticRoutes(CiTestCase): @@ -181,6 +232,20 @@ class TestDHCPParseStaticRoutes(CiTestCase):          logs = self.logs.getvalue()          self.assertIn(rfc3442, logs.splitlines()[0]) +    def test_redhat_format(self): +        redhat_format = "24.191.168.128 192.168.128.1,0 192.168.128.1" +        self.assertEqual(sorted([ +            ("191.168.128.0/24", "192.168.128.1"), +            ("0.0.0.0/0", "192.168.128.1") +        ]), sorted(parse_static_routes(redhat_format))) + +    def test_redhat_format_with_a_space_too_much_after_comma(self): +        redhat_format = "24.191.168.128 192.168.128.1, 0 192.168.128.1" +        self.assertEqual(sorted([ +            ("191.168.128.0/24", "192.168.128.1"), +            ("0.0.0.0/0", "192.168.128.1") +        ]), sorted(parse_static_routes(redhat_format))) +  class TestDHCPDiscoveryClean(CiTestCase):      with_logs = True | 
