Merge ~raharper/cloud-init:sysconfig-handle-global-static-routes into cloud-init:master

Proposed by Ryan Harper
Status: Work in progress
Proposed branch: ~raharper/cloud-init:sysconfig-handle-global-static-routes
Merge into: cloud-init:master
Diff against target: 151 lines (+58/-0)
2 files modified
cloudinit/net/sysconfig.py (+22/-0)
tests/unittests/test_net.py (+36/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
cloud-init Commiters Pending
Review via email: mp+342102@code.launchpad.net

Commit message

net/sysconfig: handle global static routes

Cloud-init network-config V1 format allows configuration of "global"
static routes which are not directly associated with any interface
in particular. These routes were not rendered under sysconfig which
generally prefers routes to be part of an interface subnet configuration.
If the configuration includes global routes, sysconfig will now render
these static routes to /etc/sysconfig/static-routes file with the 'any'
parameter indicating it will be active for any network interface.

This change does not affect existing subnet routes which continue to
be rendered into /etc/sysconfig/network-scripts/route-$iface and
route6-$iface as needed.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:8fe387770dda040419468007f5f279f502ad32c1
https://jenkins.ubuntu.com/server/job/cloud-init-ci/930/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/930/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

one useful comment/question and some nits.

5afdc68... by Ryan Harper

net/sysconfig refactors

- move ip-route keyword mapping outside of loop
- move prefix 'any' to entry construction

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, fixing up most.

We do have some discussion to be had re: sysconfig capture and redeploy w.r.t what files cloud-init will leave around, also cloud-init clean for network related configuration files.

2b459ab... by Ryan Harper

reduce vertical spacing some more

- move regex out of loop and compress to two lines
- keep list comprehension to filter non route entries from regex search
- convert match into dictionary in one line
- use ternary operator to append matched route when found
- network_config is not needed in _assert_static_routes
- no longer break out network_config to pass to static_routes
- add an else clause to assert_static_routes to make sure we don't
  have rendered static routes file when we shouldnt

c79e5fc... by Ryan Harper

Make tox happy

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:c79e5fc9bbfa5c4c2d75a7684ee812e62eb5135d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/934/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/934/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

I filed a bug
 https://bugs.launchpad.net/cloud-init/+bug/1759324
to handle "make network rendering cleanup after itself."

Unmerged commits

c79e5fc... by Ryan Harper

Make tox happy

2b459ab... by Ryan Harper

reduce vertical spacing some more

- move regex out of loop and compress to two lines
- keep list comprehension to filter non route entries from regex search
- convert match into dictionary in one line
- use ternary operator to append matched route when found
- network_config is not needed in _assert_static_routes
- no longer break out network_config to pass to static_routes
- add an else clause to assert_static_routes to make sure we don't
  have rendered static routes file when we shouldnt

5afdc68... by Ryan Harper

net/sysconfig refactors

- move ip-route keyword mapping outside of loop
- move prefix 'any' to entry construction

194f739... by Ryan Harper

net/sysconfig: handle global static routes

Cloud-init network-config V1 format allows configuration of "global"
static routes which are not directly associated with any interface
in particular. These routes were not rendered under sysconfig which
generally prefers routes to be part of an interface subnet configuration.
If the configuration includes global routes, sysconfig will now render
these static routes to /etc/sysconfig/static-routes file with the 'any'
parameter indicating it will be active for any network interface.

This change does not affect existing subnet routes which continue to
be rendered into /etc/sysconfig/network-scripts/route-$iface and
route6-$iface as needed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
2index 39d89c4..f8627f3 100644
3--- a/cloudinit/net/sysconfig.py
4+++ b/cloudinit/net/sysconfig.py
5@@ -550,6 +550,26 @@ class Renderer(renderer.Renderer):
6 cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
7
8 @classmethod
9+ def _render_static_routes(cls, base_sysconf_dir, network_state, contents):
10+ """Render global routes into /etc/sysconfig/static-routes file"""
11+ sr_content = []
12+ ipr_mapping = {'network': 'net', 'netmask': 'netmask',
13+ 'metric': 'metric', 'gateway': 'gw'}
14+ for route in network_state.iter_routes():
15+ route_fmt = []
16+ # use a specific order to generate a net-tools/route command
17+ for kw in ['network', 'netmask', 'metric', 'gateway']:
18+ if kw in route:
19+ route_fmt.extend([ipr_mapping[kw], str(route[kw])])
20+ if route_fmt:
21+ sr_content.append("any " + " ".join(route_fmt))
22+
23+ if sr_content:
24+ static_routes_fn = os.path.join(base_sysconf_dir, 'static-routes')
25+ sr_content = [_make_header()] + sr_content
26+ contents[static_routes_fn] = "\n".join(sr_content) + "\n"
27+
28+ @classmethod
29 def _render_sysconfig(cls, base_sysconf_dir, network_state):
30 '''Given state, return /etc/sysconfig files + contents'''
31 iface_contents = {}
32@@ -576,6 +596,8 @@ class Renderer(renderer.Renderer):
33 iface_cfg.routes.to_string("ipv4")
34 contents[iface_cfg.routes.path_ipv6] = \
35 iface_cfg.routes.to_string("ipv6")
36+ cls._render_static_routes(base_sysconf_dir, network_state, contents)
37+
38 return contents
39
40 def render_network_state(self, network_state, target=None):
41diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
42index c12a487..21bb672 100644
43--- a/tests/unittests/test_net.py
44+++ b/tests/unittests/test_net.py
45@@ -21,6 +21,7 @@ import gzip
46 import io
47 import json
48 import os
49+import re
50 import textwrap
51 import yaml
52
53@@ -1575,6 +1576,7 @@ class TestSysConfigRendering(CiTestCase):
54 def _render_and_read(self, network_config=None, state=None, dir=None):
55 if dir is None:
56 dir = self.tmp_dir()
57+ self.render_dir = dir
58
59 if network_config:
60 ns = network_state.parse_net_config_data(network_config)
61@@ -1582,6 +1584,7 @@ class TestSysConfigRendering(CiTestCase):
62 ns = state
63 else:
64 raise ValueError("Expected data or state, got neither")
65+ self.network_state = ns
66
67 renderer = sysconfig.Renderer()
68 renderer.render_network_state(ns, dir)
69@@ -1610,6 +1613,32 @@ class TestSysConfigRendering(CiTestCase):
70 if missing:
71 raise AssertionError("Missing headers in: %s" % missing)
72
73+ def _assert_static_routes(self, found):
74+ static_routes = list(self.network_state.iter_routes())
75+ snet_keys = ['network', 'netmask', 'metric', 'gateway']
76+ if len(static_routes) > 0:
77+ self.assertIn('/etc/sysconfig/static-routes', found)
78+ sroute_fn = os.path.join(self.render_dir,
79+ 'etc/sysconfig/static-routes')
80+ self.assertTrue(os.path.exists(sroute_fn))
81+ sroute_content = util.load_file(sroute_fn)
82+ sr_re = (r'^any\snet\s(?P<network>\S+)\snetmask\s(?P<netmask>\S+)'
83+ r'\smetric\s(?P<metric>\S+)\sgw\s(?P<gateway>\S+)')
84+ for line in [line for line in sroute_content.splitlines()
85+ if line.startswith('any')]:
86+ found_route = re.search(sr_re, line).groupdict('')
87+ if 'metric' in found_route:
88+ found_route['metric'] = int(found_route['metric'])
89+ found_match = []
90+ for sroute in static_routes:
91+ missing = [key for key in snet_keys
92+ if found_route[key] != sroute[key]]
93+ if not missing:
94+ found_match.append(sroute)
95+ self.assertEqual(1, len(found_match))
96+ else:
97+ self.assertNotIn('/etc/sysconfig/static-routes', found)
98+
99 @mock.patch("cloudinit.net.sys_dev_path")
100 @mock.patch("cloudinit.net.read_sys_net")
101 @mock.patch("cloudinit.net.get_devicelist")
102@@ -1792,42 +1821,49 @@ USERCTL=no
103 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
104 self._compare_files_to_expected(entry['expected_sysconfig'], found)
105 self._assert_headers(found)
106+ self._assert_static_routes(found)
107
108 def test_vlan_config(self):
109 entry = NETWORK_CONFIGS['vlan']
110 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
111 self._compare_files_to_expected(entry['expected_sysconfig'], found)
112 self._assert_headers(found)
113+ self._assert_static_routes(found)
114
115 def test_bridge_config(self):
116 entry = NETWORK_CONFIGS['bridge']
117 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
118 self._compare_files_to_expected(entry['expected_sysconfig'], found)
119 self._assert_headers(found)
120+ self._assert_static_routes(found)
121
122 def test_manual_config(self):
123 entry = NETWORK_CONFIGS['manual']
124 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
125 self._compare_files_to_expected(entry['expected_sysconfig'], found)
126 self._assert_headers(found)
127+ self._assert_static_routes(found)
128
129 def test_all_config(self):
130 entry = NETWORK_CONFIGS['all']
131 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
132 self._compare_files_to_expected(entry['expected_sysconfig'], found)
133 self._assert_headers(found)
134+ self._assert_static_routes(found)
135
136 def test_small_config(self):
137 entry = NETWORK_CONFIGS['small']
138 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
139 self._compare_files_to_expected(entry['expected_sysconfig'], found)
140 self._assert_headers(found)
141+ self._assert_static_routes(found)
142
143 def test_v4_and_v6_static_config(self):
144 entry = NETWORK_CONFIGS['v4_and_v6_static']
145 found = self._render_and_read(network_config=yaml.load(entry['yaml']))
146 self._compare_files_to_expected(entry['expected_sysconfig'], found)
147 self._assert_headers(found)
148+ self._assert_static_routes(found)
149
150
151 class TestEniNetRendering(CiTestCase):

Subscribers

People subscribed via source and target branches