Merge lp:~harlowja/cloud-init/cloud-init-net-sysconfig into lp:~cloud-init-dev/cloud-init/trunk
- cloud-init-net-sysconfig
- Merge into trunk
Proposed by
Joshua Harlow
Status: | Merged |
---|---|
Merged at revision: | 1242 |
Proposed branch: | lp:~harlowja/cloud-init/cloud-init-net-sysconfig |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
1119 lines (+731/-103) (has conflicts) 8 files modified
cloudinit/distros/debian.py (+10/-2) cloudinit/distros/rhel.py (+8/-0) cloudinit/net/__init__.py (+0/-1) cloudinit/net/eni.py (+30/-39) cloudinit/net/network_state.py (+75/-29) cloudinit/net/renderer.py (+48/-0) cloudinit/net/sysconfig.py (+400/-0) tests/unittests/test_net.py (+160/-32) Text conflict in cloudinit/distros/debian.py |
To merge this branch: | bzr merge lp:~harlowja/cloud-init/cloud-init-net-sysconfig |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email: mp+297115@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 1235. By Joshua Harlow
-
Add a sysconfig rendering test
- 1236. By Joshua Harlow
-
Add a bunch more sample tests for sysconfig
- 1237. By Joshua Harlow
-
Refactor some of sysconfig changes -> network_state module
- 1238. By Joshua Harlow
-
Make the os samples easier to extend (for new samples)
Revision history for this message
Ryan Harper (raharper) : | # |
Revision history for this message
Joshua Harlow (harlowja) : | # |
Revision history for this message
Scott Moser (smoser) wrote : | # |
Revision history for this message
Scott Moser (smoser) wrote : | # |
links_prefix support would be ncie in the renderer.
for persistent naming with systemd.link
that might be shared with eni
- 1239. By Joshua Harlow
-
Fixup code review comments
- 1240. By Joshua Harlow
-
Fix line length issues
Revision history for this message
Joshua Harlow (harlowja) wrote : | # |
done.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cloudinit/distros/debian.py' | |||
2 | --- cloudinit/distros/debian.py 2016-06-10 21:40:05 +0000 | |||
3 | +++ cloudinit/distros/debian.py 2016-06-15 23:16:02 +0000 | |||
4 | @@ -57,7 +57,11 @@ | |||
5 | 57 | # should only happen say once per instance...) | 57 | # should only happen say once per instance...) |
6 | 58 | self._runner = helpers.Runners(paths) | 58 | self._runner = helpers.Runners(paths) |
7 | 59 | self.osfamily = 'debian' | 59 | self.osfamily = 'debian' |
9 | 60 | self._net_renderer = eni.Renderer() | 60 | self._net_renderer = eni.Renderer({ |
10 | 61 | 'eni_path': self.network_conf_fn, | ||
11 | 62 | 'links_prefix_path': self.links_prefix, | ||
12 | 63 | 'netrules_path': None, | ||
13 | 64 | }) | ||
14 | 61 | 65 | ||
15 | 62 | def apply_locale(self, locale, out_fn=None): | 66 | def apply_locale(self, locale, out_fn=None): |
16 | 63 | if not out_fn: | 67 | if not out_fn: |
17 | @@ -81,13 +85,17 @@ | |||
18 | 81 | return ['all'] | 85 | return ['all'] |
19 | 82 | 86 | ||
20 | 83 | def _write_network_config(self, netconfig): | 87 | def _write_network_config(self, netconfig): |
21 | 88 | <<<<<<< TREE | ||
22 | 84 | ns = parse_net_config_data(netconfig) | 89 | ns = parse_net_config_data(netconfig) |
23 | 85 | self._net_renderer.render_network_state( | 90 | self._net_renderer.render_network_state( |
24 | 86 | target="/", network_state=ns, | 91 | target="/", network_state=ns, |
25 | 87 | eni=self.network_conf_fn, links_prefix=self.links_prefix, | 92 | eni=self.network_conf_fn, links_prefix=self.links_prefix, |
26 | 88 | netrules=None) | 93 | netrules=None) |
27 | 94 | ======= | ||
28 | 95 | ns = net.parse_net_config_data(netconfig) | ||
29 | 96 | self._net_renderer.render_network_state("/", ns) | ||
30 | 97 | >>>>>>> MERGE-SOURCE | ||
31 | 89 | _maybe_remove_legacy_eth0() | 98 | _maybe_remove_legacy_eth0() |
32 | 90 | |||
33 | 91 | return [] | 99 | return [] |
34 | 92 | 100 | ||
35 | 93 | def _bring_up_interfaces(self, device_names): | 101 | def _bring_up_interfaces(self, device_names): |
36 | 94 | 102 | ||
37 | === modified file 'cloudinit/distros/rhel.py' | |||
38 | --- cloudinit/distros/rhel.py 2015-06-02 20:27:57 +0000 | |||
39 | +++ cloudinit/distros/rhel.py 2016-06-15 23:16:02 +0000 | |||
40 | @@ -23,6 +23,8 @@ | |||
41 | 23 | from cloudinit import distros | 23 | from cloudinit import distros |
42 | 24 | from cloudinit import helpers | 24 | from cloudinit import helpers |
43 | 25 | from cloudinit import log as logging | 25 | from cloudinit import log as logging |
44 | 26 | from cloudinit.net.network_state import parse_net_config_data | ||
45 | 27 | from cloudinit.net import sysconfig | ||
46 | 26 | from cloudinit import util | 28 | from cloudinit import util |
47 | 27 | 29 | ||
48 | 28 | from cloudinit.distros import net_util | 30 | from cloudinit.distros import net_util |
49 | @@ -59,10 +61,16 @@ | |||
50 | 59 | # should only happen say once per instance...) | 61 | # should only happen say once per instance...) |
51 | 60 | self._runner = helpers.Runners(paths) | 62 | self._runner = helpers.Runners(paths) |
52 | 61 | self.osfamily = 'redhat' | 63 | self.osfamily = 'redhat' |
53 | 64 | self._net_renderer = sysconfig.Renderer() | ||
54 | 62 | 65 | ||
55 | 63 | def install_packages(self, pkglist): | 66 | def install_packages(self, pkglist): |
56 | 64 | self.package_command('install', pkgs=pkglist) | 67 | self.package_command('install', pkgs=pkglist) |
57 | 65 | 68 | ||
58 | 69 | def _write_network_config(self, netconfig): | ||
59 | 70 | ns = parse_net_config_data(netconfig) | ||
60 | 71 | self._net_renderer.render_network_state("/", ns) | ||
61 | 72 | return [] | ||
62 | 73 | |||
63 | 66 | def _write_network(self, settings): | 74 | def _write_network(self, settings): |
64 | 67 | # TODO(harlowja) fix this... since this is the ubuntu format | 75 | # TODO(harlowja) fix this... since this is the ubuntu format |
65 | 68 | entries = net_util.translate_network(settings) | 76 | entries = net_util.translate_network(settings) |
66 | 69 | 77 | ||
67 | === modified file 'cloudinit/net/__init__.py' | |||
68 | --- cloudinit/net/__init__.py 2016-06-07 01:42:29 +0000 | |||
69 | +++ cloudinit/net/__init__.py 2016-06-15 23:16:02 +0000 | |||
70 | @@ -26,7 +26,6 @@ | |||
71 | 26 | LOG = logging.getLogger(__name__) | 26 | LOG = logging.getLogger(__name__) |
72 | 27 | SYS_CLASS_NET = "/sys/class/net/" | 27 | SYS_CLASS_NET = "/sys/class/net/" |
73 | 28 | DEFAULT_PRIMARY_INTERFACE = 'eth0' | 28 | DEFAULT_PRIMARY_INTERFACE = 'eth0' |
74 | 29 | LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-" | ||
75 | 30 | 29 | ||
76 | 31 | 30 | ||
77 | 32 | def sys_dev_path(devname, path=""): | 31 | def sys_dev_path(devname, path=""): |
78 | 33 | 32 | ||
79 | === modified file 'cloudinit/net/eni.py' | |||
80 | --- cloudinit/net/eni.py 2016-06-14 19:18:53 +0000 | |||
81 | +++ cloudinit/net/eni.py 2016-06-15 23:16:02 +0000 | |||
82 | @@ -16,10 +16,9 @@ | |||
83 | 16 | import os | 16 | import os |
84 | 17 | import re | 17 | import re |
85 | 18 | 18 | ||
86 | 19 | from . import LINKS_FNAME_PREFIX | ||
87 | 20 | from . import ParserError | 19 | from . import ParserError |
88 | 21 | 20 | ||
90 | 22 | from .udev import generate_udev_rule | 21 | from . import renderer |
91 | 23 | 22 | ||
92 | 24 | from cloudinit import util | 23 | from cloudinit import util |
93 | 25 | 24 | ||
94 | @@ -297,21 +296,17 @@ | |||
95 | 297 | 'config': [devs[d] for d in sorted(devs)]} | 296 | 'config': [devs[d] for d in sorted(devs)]} |
96 | 298 | 297 | ||
97 | 299 | 298 | ||
99 | 300 | class Renderer(object): | 299 | class Renderer(renderer.Renderer): |
100 | 301 | """Renders network information in a /etc/network/interfaces format.""" | 300 | """Renders network information in a /etc/network/interfaces format.""" |
101 | 302 | 301 | ||
114 | 303 | def _render_persistent_net(self, network_state): | 302 | def __init__(self, config=None): |
115 | 304 | """Given state, emit udev rules to map mac to ifname.""" | 303 | if not config: |
116 | 305 | content = "" | 304 | config = {} |
117 | 306 | interfaces = network_state.get('interfaces') | 305 | self.eni_path = config.get('eni_path', 'etc/network/interfaces') |
118 | 307 | for iface in interfaces.values(): | 306 | self.links_path_prefix = config.get( |
119 | 308 | # for physical interfaces write out a persist net udev rule | 307 | 'links_path_prefix', 'etc/systemd/network/50-cloud-init-') |
120 | 309 | if iface['type'] == 'physical' and \ | 308 | self.netrules_path = config.get( |
121 | 310 | 'name' in iface and iface.get('mac_address'): | 309 | 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') |
110 | 311 | content += generate_udev_rule(iface['name'], | ||
111 | 312 | iface['mac_address']) | ||
112 | 313 | |||
113 | 314 | return content | ||
122 | 315 | 310 | ||
123 | 316 | def _render_route(self, route, indent=""): | 311 | def _render_route(self, route, indent=""): |
124 | 317 | """When rendering routes for an iface, in some cases applying a route | 312 | """When rendering routes for an iface, in some cases applying a route |
125 | @@ -360,7 +355,15 @@ | |||
126 | 360 | '''Given state, emit etc/network/interfaces content.''' | 355 | '''Given state, emit etc/network/interfaces content.''' |
127 | 361 | 356 | ||
128 | 362 | content = "" | 357 | content = "" |
130 | 363 | interfaces = network_state.get('interfaces') | 358 | content += "auto lo\niface lo inet loopback\n" |
131 | 359 | |||
132 | 360 | nameservers = network_state.dns_nameservers | ||
133 | 361 | if nameservers: | ||
134 | 362 | content += " dns-nameservers %s\n" % (" ".join(nameservers)) | ||
135 | 363 | searchdomains = network_state.dns_searchdomains | ||
136 | 364 | if searchdomains: | ||
137 | 365 | content += " dns-search %s\n" % (" ".join(searchdomains)) | ||
138 | 366 | |||
139 | 364 | ''' Apply a sort order to ensure that we write out | 367 | ''' Apply a sort order to ensure that we write out |
140 | 365 | the physical interfaces first; this is critical for | 368 | the physical interfaces first; this is critical for |
141 | 366 | bonding | 369 | bonding |
142 | @@ -371,12 +374,7 @@ | |||
143 | 371 | 'bridge': 2, | 374 | 'bridge': 2, |
144 | 372 | 'vlan': 3, | 375 | 'vlan': 3, |
145 | 373 | } | 376 | } |
152 | 374 | content += "auto lo\niface lo inet loopback\n" | 377 | for iface in sorted(network_state.iter_interfaces(), |
147 | 375 | for dnskey, value in network_state.get('dns', {}).items(): | ||
148 | 376 | if len(value): | ||
149 | 377 | content += " dns-{} {}\n".format(dnskey, " ".join(value)) | ||
150 | 378 | |||
151 | 379 | for iface in sorted(interfaces.values(), | ||
153 | 380 | key=lambda k: (order[k['type']], k['name'])): | 378 | key=lambda k: (order[k['type']], k['name'])): |
154 | 381 | 379 | ||
155 | 382 | if content[-2:] != "\n\n": | 380 | if content[-2:] != "\n\n": |
156 | @@ -409,40 +407,33 @@ | |||
157 | 409 | content += "iface {name} {inet} {mode}\n".format(**iface) | 407 | content += "iface {name} {inet} {mode}\n".format(**iface) |
158 | 410 | content += _iface_add_attrs(iface) | 408 | content += _iface_add_attrs(iface) |
159 | 411 | 409 | ||
161 | 412 | for route in network_state.get('routes'): | 410 | for route in network_state.iter_routes(): |
162 | 413 | content += self._render_route(route) | 411 | content += self._render_route(route) |
163 | 414 | 412 | ||
164 | 415 | # global replacements until v2 format | 413 | # global replacements until v2 format |
165 | 416 | content = content.replace('mac_address', 'hwaddress') | 414 | content = content.replace('mac_address', 'hwaddress') |
166 | 417 | return content | 415 | return content |
167 | 418 | 416 | ||
175 | 419 | def render_network_state( | 417 | def render_network_state(self, target, network_state): |
176 | 420 | self, target, network_state, eni="etc/network/interfaces", | 418 | fpeni = os.path.join(target, self.eni_path) |
170 | 421 | links_prefix=LINKS_FNAME_PREFIX, | ||
171 | 422 | netrules='etc/udev/rules.d/70-persistent-net.rules', | ||
172 | 423 | writer=None): | ||
173 | 424 | |||
174 | 425 | fpeni = os.path.sep.join((target, eni,)) | ||
177 | 426 | util.ensure_dir(os.path.dirname(fpeni)) | 419 | util.ensure_dir(os.path.dirname(fpeni)) |
178 | 427 | util.write_file(fpeni, self._render_interfaces(network_state)) | 420 | util.write_file(fpeni, self._render_interfaces(network_state)) |
179 | 428 | 421 | ||
182 | 429 | if netrules: | 422 | if self.netrules_path: |
183 | 430 | netrules = os.path.sep.join((target, netrules,)) | 423 | netrules = os.path.join(target, self.netrules_path) |
184 | 431 | util.ensure_dir(os.path.dirname(netrules)) | 424 | util.ensure_dir(os.path.dirname(netrules)) |
185 | 432 | util.write_file(netrules, | 425 | util.write_file(netrules, |
186 | 433 | self._render_persistent_net(network_state)) | 426 | self._render_persistent_net(network_state)) |
187 | 434 | 427 | ||
189 | 435 | if links_prefix: | 428 | if self.links_path_prefix: |
190 | 436 | self._render_systemd_links(target, network_state, | 429 | self._render_systemd_links(target, network_state, |
192 | 437 | links_prefix=links_prefix) | 430 | links_prefix=self.links_path_prefix) |
193 | 438 | 431 | ||
197 | 439 | def _render_systemd_links(self, target, network_state, | 432 | def _render_systemd_links(self, target, network_state, links_prefix): |
198 | 440 | links_prefix=LINKS_FNAME_PREFIX): | 433 | fp_prefix = os.path.join(target, links_prefix) |
196 | 441 | fp_prefix = os.path.sep.join((target, links_prefix)) | ||
199 | 442 | for f in glob.glob(fp_prefix + "*"): | 434 | for f in glob.glob(fp_prefix + "*"): |
200 | 443 | os.unlink(f) | 435 | os.unlink(f) |
203 | 444 | interfaces = network_state.get('interfaces') | 436 | for iface in network_state.iter_interfaces(): |
202 | 445 | for iface in interfaces.values(): | ||
204 | 446 | if (iface['type'] == 'physical' and 'name' in iface and | 437 | if (iface['type'] == 'physical' and 'name' in iface and |
205 | 447 | iface.get('mac_address')): | 438 | iface.get('mac_address')): |
206 | 448 | fname = fp_prefix + iface['name'] + ".link" | 439 | fname = fp_prefix + iface['name'] + ".link" |
207 | 449 | 440 | ||
208 | === modified file 'cloudinit/net/network_state.py' | |||
209 | --- cloudinit/net/network_state.py 2016-06-07 17:59:27 +0000 | |||
210 | +++ cloudinit/net/network_state.py 2016-06-15 23:16:02 +0000 | |||
211 | @@ -38,10 +38,10 @@ | |||
212 | 38 | """ | 38 | """ |
213 | 39 | state = None | 39 | state = None |
214 | 40 | if 'version' in net_config and 'config' in net_config: | 40 | if 'version' in net_config and 'config' in net_config: |
219 | 41 | ns = NetworkState(version=net_config.get('version'), | 41 | nsi = NetworkStateInterpreter(version=net_config.get('version'), |
220 | 42 | config=net_config.get('config')) | 42 | config=net_config.get('config')) |
221 | 43 | ns.parse_config(skip_broken=skip_broken) | 43 | nsi.parse_config(skip_broken=skip_broken) |
222 | 44 | state = ns.network_state | 44 | state = nsi.network_state |
223 | 45 | return state | 45 | return state |
224 | 46 | 46 | ||
225 | 47 | 47 | ||
226 | @@ -57,11 +57,10 @@ | |||
227 | 57 | 57 | ||
228 | 58 | 58 | ||
229 | 59 | def from_state_file(state_file): | 59 | def from_state_file(state_file): |
230 | 60 | network_state = None | ||
231 | 61 | state = util.read_conf(state_file) | 60 | state = util.read_conf(state_file) |
235 | 62 | network_state = NetworkState() | 61 | nsi = NetworkStateInterpreter() |
236 | 63 | network_state.load(state) | 62 | nsi.load(state) |
237 | 64 | return network_state | 63 | return nsi |
238 | 65 | 64 | ||
239 | 66 | 65 | ||
240 | 67 | def diff_keys(expected, actual): | 66 | def diff_keys(expected, actual): |
241 | @@ -113,8 +112,50 @@ | |||
242 | 113 | parents, dct) | 112 | parents, dct) |
243 | 114 | 113 | ||
244 | 115 | 114 | ||
245 | 115 | class NetworkState(object): | ||
246 | 116 | |||
247 | 117 | def __init__(self, network_state, version=NETWORK_STATE_VERSION): | ||
248 | 118 | self._network_state = copy.deepcopy(network_state) | ||
249 | 119 | self._version = version | ||
250 | 120 | |||
251 | 121 | @property | ||
252 | 122 | def version(self): | ||
253 | 123 | return self._version | ||
254 | 124 | |||
255 | 125 | def iter_routes(self, filter_func=None): | ||
256 | 126 | for route in self._network_state.get('routes', []): | ||
257 | 127 | if filter_func is not None: | ||
258 | 128 | if filter_func(route): | ||
259 | 129 | yield route | ||
260 | 130 | else: | ||
261 | 131 | yield route | ||
262 | 132 | |||
263 | 133 | @property | ||
264 | 134 | def dns_nameservers(self): | ||
265 | 135 | try: | ||
266 | 136 | return self._network_state['dns']['nameservers'] | ||
267 | 137 | except KeyError: | ||
268 | 138 | return [] | ||
269 | 139 | |||
270 | 140 | @property | ||
271 | 141 | def dns_searchdomains(self): | ||
272 | 142 | try: | ||
273 | 143 | return self._network_state['dns']['search'] | ||
274 | 144 | except KeyError: | ||
275 | 145 | return [] | ||
276 | 146 | |||
277 | 147 | def iter_interfaces(self, filter_func=None): | ||
278 | 148 | ifaces = self._network_state.get('interfaces', {}) | ||
279 | 149 | for iface in six.itervalues(ifaces): | ||
280 | 150 | if filter_func is None: | ||
281 | 151 | yield iface | ||
282 | 152 | else: | ||
283 | 153 | if filter_func(iface): | ||
284 | 154 | yield iface | ||
285 | 155 | |||
286 | 156 | |||
287 | 116 | @six.add_metaclass(CommandHandlerMeta) | 157 | @six.add_metaclass(CommandHandlerMeta) |
289 | 117 | class NetworkState(object): | 158 | class NetworkStateInterpreter(object): |
290 | 118 | 159 | ||
291 | 119 | initial_network_state = { | 160 | initial_network_state = { |
292 | 120 | 'interfaces': {}, | 161 | 'interfaces': {}, |
293 | @@ -126,22 +167,27 @@ | |||
294 | 126 | } | 167 | } |
295 | 127 | 168 | ||
296 | 128 | def __init__(self, version=NETWORK_STATE_VERSION, config=None): | 169 | def __init__(self, version=NETWORK_STATE_VERSION, config=None): |
300 | 129 | self.version = version | 170 | self._version = version |
301 | 130 | self.config = config | 171 | self._config = config |
302 | 131 | self.network_state = copy.deepcopy(self.initial_network_state) | 172 | self._network_state = copy.deepcopy(self.initial_network_state) |
303 | 173 | self._parsed = False | ||
304 | 174 | |||
305 | 175 | @property | ||
306 | 176 | def network_state(self): | ||
307 | 177 | return NetworkState(self._network_state, version=self._version) | ||
308 | 132 | 178 | ||
309 | 133 | def dump(self): | 179 | def dump(self): |
310 | 134 | state = { | 180 | state = { |
314 | 135 | 'version': self.version, | 181 | 'version': self._version, |
315 | 136 | 'config': self.config, | 182 | 'config': self._config, |
316 | 137 | 'network_state': self.network_state, | 183 | 'network_state': self._network_state, |
317 | 138 | } | 184 | } |
318 | 139 | return util.yaml_dumps(state) | 185 | return util.yaml_dumps(state) |
319 | 140 | 186 | ||
320 | 141 | def load(self, state): | 187 | def load(self, state): |
321 | 142 | if 'version' not in state: | 188 | if 'version' not in state: |
322 | 143 | LOG.error('Invalid state, missing version field') | 189 | LOG.error('Invalid state, missing version field') |
324 | 144 | raise Exception('Invalid state, missing version field') | 190 | raise ValueError('Invalid state, missing version field') |
325 | 145 | 191 | ||
326 | 146 | required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']] | 192 | required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']] |
327 | 147 | missing_keys = diff_keys(required_keys, state) | 193 | missing_keys = diff_keys(required_keys, state) |
328 | @@ -155,11 +201,11 @@ | |||
329 | 155 | setattr(self, key, state[key]) | 201 | setattr(self, key, state[key]) |
330 | 156 | 202 | ||
331 | 157 | def dump_network_state(self): | 203 | def dump_network_state(self): |
333 | 158 | return util.yaml_dumps(self.network_state) | 204 | return util.yaml_dumps(self._network_state) |
334 | 159 | 205 | ||
335 | 160 | def parse_config(self, skip_broken=True): | 206 | def parse_config(self, skip_broken=True): |
336 | 161 | # rebuild network state | 207 | # rebuild network state |
338 | 162 | for command in self.config: | 208 | for command in self._config: |
339 | 163 | command_type = command['type'] | 209 | command_type = command['type'] |
340 | 164 | try: | 210 | try: |
341 | 165 | handler = self.command_handlers[command_type] | 211 | handler = self.command_handlers[command_type] |
342 | @@ -189,7 +235,7 @@ | |||
343 | 189 | } | 235 | } |
344 | 190 | ''' | 236 | ''' |
345 | 191 | 237 | ||
347 | 192 | interfaces = self.network_state.get('interfaces') | 238 | interfaces = self._network_state.get('interfaces', {}) |
348 | 193 | iface = interfaces.get(command['name'], {}) | 239 | iface = interfaces.get(command['name'], {}) |
349 | 194 | for param, val in command.get('params', {}).items(): | 240 | for param, val in command.get('params', {}).items(): |
350 | 195 | iface.update({param: val}) | 241 | iface.update({param: val}) |
351 | @@ -215,7 +261,7 @@ | |||
352 | 215 | 'gateway': None, | 261 | 'gateway': None, |
353 | 216 | 'subnets': subnets, | 262 | 'subnets': subnets, |
354 | 217 | }) | 263 | }) |
356 | 218 | self.network_state['interfaces'].update({command.get('name'): iface}) | 264 | self._network_state['interfaces'].update({command.get('name'): iface}) |
357 | 219 | self.dump_network_state() | 265 | self.dump_network_state() |
358 | 220 | 266 | ||
359 | 221 | @ensure_command_keys(['name', 'vlan_id', 'vlan_link']) | 267 | @ensure_command_keys(['name', 'vlan_id', 'vlan_link']) |
360 | @@ -228,7 +274,7 @@ | |||
361 | 228 | hwaddress ether BC:76:4E:06:96:B3 | 274 | hwaddress ether BC:76:4E:06:96:B3 |
362 | 229 | vlan-raw-device eth0 | 275 | vlan-raw-device eth0 |
363 | 230 | ''' | 276 | ''' |
365 | 231 | interfaces = self.network_state.get('interfaces') | 277 | interfaces = self._network_state.get('interfaces', {}) |
366 | 232 | self.handle_physical(command) | 278 | self.handle_physical(command) |
367 | 233 | iface = interfaces.get(command.get('name'), {}) | 279 | iface = interfaces.get(command.get('name'), {}) |
368 | 234 | iface['vlan-raw-device'] = command.get('vlan_link') | 280 | iface['vlan-raw-device'] = command.get('vlan_link') |
369 | @@ -263,12 +309,12 @@ | |||
370 | 263 | ''' | 309 | ''' |
371 | 264 | 310 | ||
372 | 265 | self.handle_physical(command) | 311 | self.handle_physical(command) |
374 | 266 | interfaces = self.network_state.get('interfaces') | 312 | interfaces = self._network_state.get('interfaces') |
375 | 267 | iface = interfaces.get(command.get('name'), {}) | 313 | iface = interfaces.get(command.get('name'), {}) |
376 | 268 | for param, val in command.get('params').items(): | 314 | for param, val in command.get('params').items(): |
377 | 269 | iface.update({param: val}) | 315 | iface.update({param: val}) |
378 | 270 | iface.update({'bond-slaves': 'none'}) | 316 | iface.update({'bond-slaves': 'none'}) |
380 | 271 | self.network_state['interfaces'].update({iface['name']: iface}) | 317 | self._network_state['interfaces'].update({iface['name']: iface}) |
381 | 272 | 318 | ||
382 | 273 | # handle bond slaves | 319 | # handle bond slaves |
383 | 274 | for ifname in command.get('bond_interfaces'): | 320 | for ifname in command.get('bond_interfaces'): |
384 | @@ -280,13 +326,13 @@ | |||
385 | 280 | # inject placeholder | 326 | # inject placeholder |
386 | 281 | self.handle_physical(cmd) | 327 | self.handle_physical(cmd) |
387 | 282 | 328 | ||
389 | 283 | interfaces = self.network_state.get('interfaces') | 329 | interfaces = self._network_state.get('interfaces', {}) |
390 | 284 | bond_if = interfaces.get(ifname) | 330 | bond_if = interfaces.get(ifname) |
391 | 285 | bond_if['bond-master'] = command.get('name') | 331 | bond_if['bond-master'] = command.get('name') |
392 | 286 | # copy in bond config into slave | 332 | # copy in bond config into slave |
393 | 287 | for param, val in command.get('params').items(): | 333 | for param, val in command.get('params').items(): |
394 | 288 | bond_if.update({param: val}) | 334 | bond_if.update({param: val}) |
396 | 289 | self.network_state['interfaces'].update({ifname: bond_if}) | 335 | self._network_state['interfaces'].update({ifname: bond_if}) |
397 | 290 | 336 | ||
398 | 291 | @ensure_command_keys(['name', 'bridge_interfaces', 'params']) | 337 | @ensure_command_keys(['name', 'bridge_interfaces', 'params']) |
399 | 292 | def handle_bridge(self, command): | 338 | def handle_bridge(self, command): |
400 | @@ -319,7 +365,7 @@ | |||
401 | 319 | 365 | ||
402 | 320 | # find one of the bridge port ifaces to get mac_addr | 366 | # find one of the bridge port ifaces to get mac_addr |
403 | 321 | # handle bridge_slaves | 367 | # handle bridge_slaves |
405 | 322 | interfaces = self.network_state.get('interfaces') | 368 | interfaces = self._network_state.get('interfaces', {}) |
406 | 323 | for ifname in command.get('bridge_interfaces'): | 369 | for ifname in command.get('bridge_interfaces'): |
407 | 324 | if ifname in interfaces: | 370 | if ifname in interfaces: |
408 | 325 | continue | 371 | continue |
409 | @@ -330,7 +376,7 @@ | |||
410 | 330 | # inject placeholder | 376 | # inject placeholder |
411 | 331 | self.handle_physical(cmd) | 377 | self.handle_physical(cmd) |
412 | 332 | 378 | ||
414 | 333 | interfaces = self.network_state.get('interfaces') | 379 | interfaces = self._network_state.get('interfaces', {}) |
415 | 334 | self.handle_physical(command) | 380 | self.handle_physical(command) |
416 | 335 | iface = interfaces.get(command.get('name'), {}) | 381 | iface = interfaces.get(command.get('name'), {}) |
417 | 336 | iface['bridge_ports'] = command['bridge_interfaces'] | 382 | iface['bridge_ports'] = command['bridge_interfaces'] |
418 | @@ -341,7 +387,7 @@ | |||
419 | 341 | 387 | ||
420 | 342 | @ensure_command_keys(['address']) | 388 | @ensure_command_keys(['address']) |
421 | 343 | def handle_nameserver(self, command): | 389 | def handle_nameserver(self, command): |
423 | 344 | dns = self.network_state.get('dns') | 390 | dns = self._network_state.get('dns') |
424 | 345 | if 'address' in command: | 391 | if 'address' in command: |
425 | 346 | addrs = command['address'] | 392 | addrs = command['address'] |
426 | 347 | if not type(addrs) == list: | 393 | if not type(addrs) == list: |
427 | @@ -357,7 +403,7 @@ | |||
428 | 357 | 403 | ||
429 | 358 | @ensure_command_keys(['destination']) | 404 | @ensure_command_keys(['destination']) |
430 | 359 | def handle_route(self, command): | 405 | def handle_route(self, command): |
432 | 360 | routes = self.network_state.get('routes') | 406 | routes = self._network_state.get('routes', []) |
433 | 361 | network, cidr = command['destination'].split("/") | 407 | network, cidr = command['destination'].split("/") |
434 | 362 | netmask = cidr2mask(int(cidr)) | 408 | netmask = cidr2mask(int(cidr)) |
435 | 363 | route = { | 409 | route = { |
436 | 364 | 410 | ||
437 | === added file 'cloudinit/net/renderer.py' | |||
438 | --- cloudinit/net/renderer.py 1970-01-01 00:00:00 +0000 | |||
439 | +++ cloudinit/net/renderer.py 2016-06-15 23:16:02 +0000 | |||
440 | @@ -0,0 +1,48 @@ | |||
441 | 1 | # Copyright (C) 2013-2014 Canonical Ltd. | ||
442 | 2 | # | ||
443 | 3 | # Author: Scott Moser <scott.moser@canonical.com> | ||
444 | 4 | # Author: Blake Rouse <blake.rouse@canonical.com> | ||
445 | 5 | # | ||
446 | 6 | # Curtin is free software: you can redistribute it and/or modify it under | ||
447 | 7 | # the terms of the GNU Affero General Public License as published by the | ||
448 | 8 | # Free Software Foundation, either version 3 of the License, or (at your | ||
449 | 9 | # option) any later version. | ||
450 | 10 | # | ||
451 | 11 | # Curtin is distributed in the hope that it will be useful, but WITHOUT ANY | ||
452 | 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||
453 | 13 | # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for | ||
454 | 14 | # more details. | ||
455 | 15 | # | ||
456 | 16 | # You should have received a copy of the GNU Affero General Public License | ||
457 | 17 | # along with Curtin. If not, see <http://www.gnu.org/licenses/>. | ||
458 | 18 | |||
459 | 19 | import six | ||
460 | 20 | |||
461 | 21 | from .udev import generate_udev_rule | ||
462 | 22 | |||
463 | 23 | |||
464 | 24 | def filter_by_type(match_type): | ||
465 | 25 | return lambda iface: match_type == iface['type'] | ||
466 | 26 | |||
467 | 27 | |||
468 | 28 | def filter_by_name(match_name): | ||
469 | 29 | return lambda iface: match_name == iface['name'] | ||
470 | 30 | |||
471 | 31 | |||
472 | 32 | filter_by_physical = filter_by_type('physical') | ||
473 | 33 | |||
474 | 34 | |||
475 | 35 | class Renderer(object): | ||
476 | 36 | |||
477 | 37 | @staticmethod | ||
478 | 38 | def _render_persistent_net(network_state): | ||
479 | 39 | """Given state, emit udev rules to map mac to ifname.""" | ||
480 | 40 | # TODO(harlowja): this seems shared between eni renderer and | ||
481 | 41 | # this, so move it to a shared location. | ||
482 | 42 | content = six.StringIO() | ||
483 | 43 | for iface in network_state.iter_interfaces(filter_by_physical): | ||
484 | 44 | # for physical interfaces write out a persist net udev rule | ||
485 | 45 | if 'name' in iface and iface.get('mac_address'): | ||
486 | 46 | content.write(generate_udev_rule(iface['name'], | ||
487 | 47 | iface['mac_address'])) | ||
488 | 48 | return content.getvalue() | ||
489 | 0 | 49 | ||
490 | === added file 'cloudinit/net/sysconfig.py' | |||
491 | --- cloudinit/net/sysconfig.py 1970-01-01 00:00:00 +0000 | |||
492 | +++ cloudinit/net/sysconfig.py 2016-06-15 23:16:02 +0000 | |||
493 | @@ -0,0 +1,400 @@ | |||
494 | 1 | # vi: ts=4 expandtab | ||
495 | 2 | # | ||
496 | 3 | # This program is free software: you can redistribute it and/or modify | ||
497 | 4 | # it under the terms of the GNU General Public License version 3, as | ||
498 | 5 | # published by the Free Software Foundation. | ||
499 | 6 | # | ||
500 | 7 | # This program is distributed in the hope that it will be useful, | ||
501 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
502 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
503 | 10 | # GNU General Public License for more details. | ||
504 | 11 | # | ||
505 | 12 | # You should have received a copy of the GNU General Public License | ||
506 | 13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
507 | 14 | |||
508 | 15 | import os | ||
509 | 16 | import re | ||
510 | 17 | |||
511 | 18 | import six | ||
512 | 19 | |||
513 | 20 | from cloudinit.distros.parsers import resolv_conf | ||
514 | 21 | from cloudinit import util | ||
515 | 22 | |||
516 | 23 | from . import renderer | ||
517 | 24 | |||
518 | 25 | |||
519 | 26 | def _make_header(sep='#'): | ||
520 | 27 | lines = [ | ||
521 | 28 | "Created by cloud-init on instance boot automatically, do not edit.", | ||
522 | 29 | "", | ||
523 | 30 | ] | ||
524 | 31 | for i in range(0, len(lines)): | ||
525 | 32 | if lines[i]: | ||
526 | 33 | lines[i] = sep + " " + lines[i] | ||
527 | 34 | else: | ||
528 | 35 | lines[i] = sep | ||
529 | 36 | return "\n".join(lines) | ||
530 | 37 | |||
531 | 38 | |||
532 | 39 | def _is_default_route(route): | ||
533 | 40 | if route['network'] == '::' and route['netmask'] == 0: | ||
534 | 41 | return True | ||
535 | 42 | if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': | ||
536 | 43 | return True | ||
537 | 44 | return False | ||
538 | 45 | |||
539 | 46 | |||
540 | 47 | def _quote_value(value): | ||
541 | 48 | if re.search(r"\s", value): | ||
542 | 49 | # This doesn't handle complex cases... | ||
543 | 50 | if value.startswith('"') and value.endswith('"'): | ||
544 | 51 | return value | ||
545 | 52 | else: | ||
546 | 53 | return '"%s"' % value | ||
547 | 54 | else: | ||
548 | 55 | return value | ||
549 | 56 | |||
550 | 57 | |||
551 | 58 | class ConfigMap(object): | ||
552 | 59 | """Sysconfig like dictionary object.""" | ||
553 | 60 | |||
554 | 61 | # Why does redhat prefer yes/no to true/false?? | ||
555 | 62 | _bool_map = { | ||
556 | 63 | True: 'yes', | ||
557 | 64 | False: 'no', | ||
558 | 65 | } | ||
559 | 66 | |||
560 | 67 | def __init__(self): | ||
561 | 68 | self._conf = {} | ||
562 | 69 | |||
563 | 70 | def __setitem__(self, key, value): | ||
564 | 71 | self._conf[key] = value | ||
565 | 72 | |||
566 | 73 | def drop(self, key): | ||
567 | 74 | self._conf.pop(key, None) | ||
568 | 75 | |||
569 | 76 | def __len__(self): | ||
570 | 77 | return len(self._conf) | ||
571 | 78 | |||
572 | 79 | def to_string(self): | ||
573 | 80 | buf = six.StringIO() | ||
574 | 81 | buf.write(_make_header()) | ||
575 | 82 | if self._conf: | ||
576 | 83 | buf.write("\n") | ||
577 | 84 | for key in sorted(self._conf.keys()): | ||
578 | 85 | value = self._conf[key] | ||
579 | 86 | if isinstance(value, bool): | ||
580 | 87 | value = self._bool_map[value] | ||
581 | 88 | if not isinstance(value, six.string_types): | ||
582 | 89 | value = str(value) | ||
583 | 90 | buf.write("%s=%s\n" % (key, _quote_value(value))) | ||
584 | 91 | return buf.getvalue() | ||
585 | 92 | |||
586 | 93 | |||
587 | 94 | class Route(ConfigMap): | ||
588 | 95 | """Represents a route configuration.""" | ||
589 | 96 | |||
590 | 97 | route_fn_tpl = '%(base)s/network-scripts/route-%(name)s' | ||
591 | 98 | |||
592 | 99 | def __init__(self, route_name, base_sysconf_dir): | ||
593 | 100 | super(Route, self).__init__() | ||
594 | 101 | self.last_idx = 1 | ||
595 | 102 | self.has_set_default = False | ||
596 | 103 | self._route_name = route_name | ||
597 | 104 | self._base_sysconf_dir = base_sysconf_dir | ||
598 | 105 | |||
599 | 106 | def copy(self): | ||
600 | 107 | r = Route(self._route_name, self._base_sysconf_dir) | ||
601 | 108 | r._conf = self._conf.copy() | ||
602 | 109 | r.last_idx = self.last_idx | ||
603 | 110 | r.has_set_default = self.has_set_default | ||
604 | 111 | return r | ||
605 | 112 | |||
606 | 113 | @property | ||
607 | 114 | def path(self): | ||
608 | 115 | return self.route_fn_tpl % ({'base': self._base_sysconf_dir, | ||
609 | 116 | 'name': self._route_name}) | ||
610 | 117 | |||
611 | 118 | |||
612 | 119 | class NetInterface(ConfigMap): | ||
613 | 120 | """Represents a sysconfig/networking-script (and its config + children).""" | ||
614 | 121 | |||
615 | 122 | iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s' | ||
616 | 123 | |||
617 | 124 | iface_types = { | ||
618 | 125 | 'ethernet': 'Ethernet', | ||
619 | 126 | 'bond': 'Bond', | ||
620 | 127 | 'bridge': 'Bridge', | ||
621 | 128 | } | ||
622 | 129 | |||
623 | 130 | def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'): | ||
624 | 131 | super(NetInterface, self).__init__() | ||
625 | 132 | self.children = [] | ||
626 | 133 | self.routes = Route(iface_name, base_sysconf_dir) | ||
627 | 134 | self._kind = kind | ||
628 | 135 | self._iface_name = iface_name | ||
629 | 136 | self._conf['DEVICE'] = iface_name | ||
630 | 137 | self._conf['TYPE'] = self.iface_types[kind] | ||
631 | 138 | self._base_sysconf_dir = base_sysconf_dir | ||
632 | 139 | |||
633 | 140 | @property | ||
634 | 141 | def name(self): | ||
635 | 142 | return self._iface_name | ||
636 | 143 | |||
637 | 144 | @name.setter | ||
638 | 145 | def name(self, iface_name): | ||
639 | 146 | self._iface_name = iface_name | ||
640 | 147 | self._conf['DEVICE'] = iface_name | ||
641 | 148 | |||
642 | 149 | @property | ||
643 | 150 | def kind(self): | ||
644 | 151 | return self._kind | ||
645 | 152 | |||
646 | 153 | @kind.setter | ||
647 | 154 | def kind(self, kind): | ||
648 | 155 | self._kind = kind | ||
649 | 156 | self._conf['TYPE'] = self.iface_types[kind] | ||
650 | 157 | |||
651 | 158 | @property | ||
652 | 159 | def path(self): | ||
653 | 160 | return self.iface_fn_tpl % ({'base': self._base_sysconf_dir, | ||
654 | 161 | 'name': self.name}) | ||
655 | 162 | |||
656 | 163 | def copy(self, copy_children=False, copy_routes=False): | ||
657 | 164 | c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind) | ||
658 | 165 | c._conf = self._conf.copy() | ||
659 | 166 | if copy_children: | ||
660 | 167 | c.children = list(self.children) | ||
661 | 168 | if copy_routes: | ||
662 | 169 | c.routes = self.routes.copy() | ||
663 | 170 | return c | ||
664 | 171 | |||
665 | 172 | |||
666 | 173 | class Renderer(renderer.Renderer): | ||
667 | 174 | """Renders network information in a /etc/sysconfig format.""" | ||
668 | 175 | |||
669 | 176 | # See: https://access.redhat.com/documentation/en-US/\ | ||
670 | 177 | # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\ | ||
671 | 178 | # s1-networkscripts-interfaces.html (or other docs for | ||
672 | 179 | # details about this) | ||
673 | 180 | |||
674 | 181 | iface_defaults = tuple([ | ||
675 | 182 | ('ONBOOT', True), | ||
676 | 183 | ('USERCTL', False), | ||
677 | 184 | ('NM_CONTROLLED', False), | ||
678 | 185 | ('BOOTPROTO', 'none'), | ||
679 | 186 | ]) | ||
680 | 187 | |||
681 | 188 | # If these keys exist, then there values will be used to form | ||
682 | 189 | # a BONDING_OPTS grouping; otherwise no grouping will be set. | ||
683 | 190 | bond_tpl_opts = tuple([ | ||
684 | 191 | ('bond_mode', "mode=%s"), | ||
685 | 192 | ('bond_xmit_hash_policy', "xmit_hash_policy=%s"), | ||
686 | 193 | ('bond_miimon', "miimon=%s"), | ||
687 | 194 | ]) | ||
688 | 195 | |||
689 | 196 | bridge_opts_keys = tuple([ | ||
690 | 197 | ('bridge_stp', 'STP'), | ||
691 | 198 | ('bridge_ageing', 'AGEING'), | ||
692 | 199 | ('bridge_bridgeprio', 'PRIO'), | ||
693 | 200 | ]) | ||
694 | 201 | |||
695 | 202 | def __init__(self, config=None): | ||
696 | 203 | if not config: | ||
697 | 204 | config = {} | ||
698 | 205 | self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/') | ||
699 | 206 | self.netrules_path = config.get( | ||
700 | 207 | 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules') | ||
701 | 208 | self.dns_path = config.get('dns_path', 'etc/resolv.conf') | ||
702 | 209 | |||
703 | 210 | @classmethod | ||
704 | 211 | def _render_iface_shared(cls, iface, iface_cfg): | ||
705 | 212 | for k, v in cls.iface_defaults: | ||
706 | 213 | iface_cfg[k] = v | ||
707 | 214 | for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]: | ||
708 | 215 | old_value = iface.get(old_key) | ||
709 | 216 | if old_value is not None: | ||
710 | 217 | iface_cfg[new_key] = old_value | ||
711 | 218 | |||
712 | 219 | @classmethod | ||
713 | 220 | def _render_subnet(cls, iface_cfg, route_cfg, subnet): | ||
714 | 221 | subnet_type = subnet.get('type') | ||
715 | 222 | if subnet_type == 'dhcp6': | ||
716 | 223 | iface_cfg['DHCPV6C'] = True | ||
717 | 224 | iface_cfg['IPV6INIT'] = True | ||
718 | 225 | iface_cfg['BOOTPROTO'] = 'dhcp' | ||
719 | 226 | elif subnet_type in ['dhcp4', 'dhcp']: | ||
720 | 227 | iface_cfg['BOOTPROTO'] = 'dhcp' | ||
721 | 228 | elif subnet_type == 'static': | ||
722 | 229 | iface_cfg['BOOTPROTO'] = 'static' | ||
723 | 230 | if subnet.get('ipv6'): | ||
724 | 231 | iface_cfg['IPV6ADDR'] = subnet['address'] | ||
725 | 232 | iface_cfg['IPV6INIT'] = True | ||
726 | 233 | else: | ||
727 | 234 | iface_cfg['IPADDR'] = subnet['address'] | ||
728 | 235 | else: | ||
729 | 236 | raise ValueError("Unknown subnet type '%s' found" | ||
730 | 237 | " for interface '%s'" % (subnet_type, | ||
731 | 238 | iface_cfg.name)) | ||
732 | 239 | if 'netmask' in subnet: | ||
733 | 240 | iface_cfg['NETMASK'] = subnet['netmask'] | ||
734 | 241 | for route in subnet.get('routes', []): | ||
735 | 242 | if _is_default_route(route): | ||
736 | 243 | if route_cfg.has_set_default: | ||
737 | 244 | raise ValueError("Duplicate declaration of default" | ||
738 | 245 | " route found for interface '%s'" | ||
739 | 246 | % (iface_cfg.name)) | ||
740 | 247 | # NOTE(harlowja): ipv6 and ipv4 default gateways | ||
741 | 248 | gw_key = 'GATEWAY0' | ||
742 | 249 | nm_key = 'NETMASK0' | ||
743 | 250 | addr_key = 'ADDRESS0' | ||
744 | 251 | # The owning interface provides the default route. | ||
745 | 252 | # | ||
746 | 253 | # TODO(harlowja): add validation that no other iface has | ||
747 | 254 | # also provided the default route? | ||
748 | 255 | iface_cfg['DEFROUTE'] = True | ||
749 | 256 | if 'gateway' in route: | ||
750 | 257 | iface_cfg['GATEWAY'] = route['gateway'] | ||
751 | 258 | route_cfg.has_set_default = True | ||
752 | 259 | else: | ||
753 | 260 | gw_key = 'GATEWAY%s' % route_cfg.last_idx | ||
754 | 261 | nm_key = 'NETMASK%s' % route_cfg.last_idx | ||
755 | 262 | addr_key = 'ADDRESS%s' % route_cfg.last_idx | ||
756 | 263 | route_cfg.last_idx += 1 | ||
757 | 264 | for (old_key, new_key) in [('gateway', gw_key), | ||
758 | 265 | ('netmask', nm_key), | ||
759 | 266 | ('network', addr_key)]: | ||
760 | 267 | if old_key in route: | ||
761 | 268 | route_cfg[new_key] = route[old_key] | ||
762 | 269 | |||
763 | 270 | @classmethod | ||
764 | 271 | def _render_bonding_opts(cls, iface_cfg, iface): | ||
765 | 272 | bond_opts = [] | ||
766 | 273 | for (bond_key, value_tpl) in cls.bond_tpl_opts: | ||
767 | 274 | # Seems like either dash or underscore is possible? | ||
768 | 275 | bond_keys = [bond_key, bond_key.replace("_", "-")] | ||
769 | 276 | for bond_key in bond_keys: | ||
770 | 277 | if bond_key in iface: | ||
771 | 278 | bond_value = iface[bond_key] | ||
772 | 279 | if isinstance(bond_value, (tuple, list)): | ||
773 | 280 | bond_value = " ".join(bond_value) | ||
774 | 281 | bond_opts.append(value_tpl % (bond_value)) | ||
775 | 282 | break | ||
776 | 283 | if bond_opts: | ||
777 | 284 | iface_cfg['BONDING_OPTS'] = " ".join(bond_opts) | ||
778 | 285 | |||
779 | 286 | @classmethod | ||
780 | 287 | def _render_physical_interfaces(cls, network_state, iface_contents): | ||
781 | 288 | physical_filter = renderer.filter_by_physical | ||
782 | 289 | for iface in network_state.iter_interfaces(physical_filter): | ||
783 | 290 | iface_name = iface['name'] | ||
784 | 291 | iface_subnets = iface.get("subnets", []) | ||
785 | 292 | iface_cfg = iface_contents[iface_name] | ||
786 | 293 | route_cfg = iface_cfg.routes | ||
787 | 294 | if len(iface_subnets) == 1: | ||
788 | 295 | cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0]) | ||
789 | 296 | elif len(iface_subnets) > 1: | ||
790 | 297 | for i, iface_subnet in enumerate(iface_subnets, | ||
791 | 298 | start=len(iface.children)): | ||
792 | 299 | iface_sub_cfg = iface_cfg.copy() | ||
793 | 300 | iface_sub_cfg.name = "%s:%s" % (iface_name, i) | ||
794 | 301 | iface.children.append(iface_sub_cfg) | ||
795 | 302 | cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet) | ||
796 | 303 | |||
797 | 304 | @classmethod | ||
798 | 305 | def _render_bond_interfaces(cls, network_state, iface_contents): | ||
799 | 306 | bond_filter = renderer.filter_by_type('bond') | ||
800 | 307 | for iface in network_state.iter_interfaces(bond_filter): | ||
801 | 308 | iface_name = iface['name'] | ||
802 | 309 | iface_cfg = iface_contents[iface_name] | ||
803 | 310 | cls._render_bonding_opts(iface_cfg, iface) | ||
804 | 311 | iface_master_name = iface['bond-master'] | ||
805 | 312 | iface_cfg['MASTER'] = iface_master_name | ||
806 | 313 | iface_cfg['SLAVE'] = True | ||
807 | 314 | # Ensure that the master interface (and any of its children) | ||
808 | 315 | # are actually marked as being bond types... | ||
809 | 316 | master_cfg = iface_contents[iface_master_name] | ||
810 | 317 | master_cfgs = [master_cfg] | ||
811 | 318 | master_cfgs.extend(master_cfg.children) | ||
812 | 319 | for master_cfg in master_cfgs: | ||
813 | 320 | master_cfg['BONDING_MASTER'] = True | ||
814 | 321 | master_cfg.kind = 'bond' | ||
815 | 322 | |||
816 | 323 | @staticmethod | ||
817 | 324 | def _render_vlan_interfaces(network_state, iface_contents): | ||
818 | 325 | vlan_filter = renderer.filter_by_type('vlan') | ||
819 | 326 | for iface in network_state.iter_interfaces(vlan_filter): | ||
820 | 327 | iface_name = iface['name'] | ||
821 | 328 | iface_cfg = iface_contents[iface_name] | ||
822 | 329 | iface_cfg['VLAN'] = True | ||
823 | 330 | iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')] | ||
824 | 331 | |||
825 | 332 | @staticmethod | ||
826 | 333 | def _render_dns(network_state, existing_dns_path=None): | ||
827 | 334 | content = resolv_conf.ResolvConf("") | ||
828 | 335 | if existing_dns_path and os.path.isfile(existing_dns_path): | ||
829 | 336 | content = resolv_conf.ResolvConf(util.load_file(existing_dns_path)) | ||
830 | 337 | for nameserver in network_state.dns_nameservers: | ||
831 | 338 | content.add_nameserver(nameserver) | ||
832 | 339 | for searchdomain in network_state.dns_searchdomains: | ||
833 | 340 | content.add_search_domain(searchdomain) | ||
834 | 341 | return "\n".join([_make_header(';'), str(content)]) | ||
835 | 342 | |||
836 | 343 | @classmethod | ||
837 | 344 | def _render_bridge_interfaces(cls, network_state, iface_contents): | ||
838 | 345 | bridge_filter = renderer.filter_by_type('bridge') | ||
839 | 346 | for iface in network_state.iter_interfaces(bridge_filter): | ||
840 | 347 | iface_name = iface['name'] | ||
841 | 348 | iface_cfg = iface_contents[iface_name] | ||
842 | 349 | iface_cfg.kind = 'bridge' | ||
843 | 350 | for old_key, new_key in cls.bridge_opts_keys: | ||
844 | 351 | if old_key in iface: | ||
845 | 352 | iface_cfg[new_key] = iface[old_key] | ||
846 | 353 | # Is this the right key to get all the connected interfaces? | ||
847 | 354 | for bridged_iface_name in iface.get('bridge_ports', []): | ||
848 | 355 | # Ensure all bridged interfaces are correctly tagged | ||
849 | 356 | # as being bridged to this interface. | ||
850 | 357 | bridged_cfg = iface_contents[bridged_iface_name] | ||
851 | 358 | bridged_cfgs = [bridged_cfg] | ||
852 | 359 | bridged_cfgs.extend(bridged_cfg.children) | ||
853 | 360 | for bridge_cfg in bridged_cfgs: | ||
854 | 361 | bridge_cfg['BRIDGE'] = iface_name | ||
855 | 362 | |||
856 | 363 | @classmethod | ||
857 | 364 | def _render_sysconfig(cls, base_sysconf_dir, network_state): | ||
858 | 365 | '''Given state, return /etc/sysconfig files + contents''' | ||
859 | 366 | iface_contents = {} | ||
860 | 367 | for iface in network_state.iter_interfaces(): | ||
861 | 368 | iface_name = iface['name'] | ||
862 | 369 | iface_cfg = NetInterface(iface_name, base_sysconf_dir) | ||
863 | 370 | cls._render_iface_shared(iface, iface_cfg) | ||
864 | 371 | iface_contents[iface_name] = iface_cfg | ||
865 | 372 | cls._render_physical_interfaces(network_state, iface_contents) | ||
866 | 373 | cls._render_bond_interfaces(network_state, iface_contents) | ||
867 | 374 | cls._render_vlan_interfaces(network_state, iface_contents) | ||
868 | 375 | cls._render_bridge_interfaces(network_state, iface_contents) | ||
869 | 376 | contents = {} | ||
870 | 377 | for iface_name, iface_cfg in iface_contents.items(): | ||
871 | 378 | if iface_cfg or iface_cfg.children: | ||
872 | 379 | contents[iface_cfg.path] = iface_cfg.to_string() | ||
873 | 380 | for iface_cfg in iface_cfg.children: | ||
874 | 381 | if iface_cfg: | ||
875 | 382 | contents[iface_cfg.path] = iface_cfg.to_string() | ||
876 | 383 | if iface_cfg.routes: | ||
877 | 384 | contents[iface_cfg.routes.path] = iface_cfg.routes.to_string() | ||
878 | 385 | return contents | ||
879 | 386 | |||
880 | 387 | def render_network_state(self, target, network_state): | ||
881 | 388 | base_sysconf_dir = os.path.join(target, self.sysconf_dir) | ||
882 | 389 | for path, data in self._render_sysconfig(base_sysconf_dir, | ||
883 | 390 | network_state).items(): | ||
884 | 391 | util.write_file(path, data) | ||
885 | 392 | if self.dns_path: | ||
886 | 393 | dns_path = os.path.join(target, self.dns_path) | ||
887 | 394 | resolv_content = self._render_dns(network_state, | ||
888 | 395 | existing_dns_path=dns_path) | ||
889 | 396 | util.write_file(dns_path, resolv_content) | ||
890 | 397 | if self.netrules_path: | ||
891 | 398 | netrules_content = self._render_persistent_net(network_state) | ||
892 | 399 | netrules_path = os.path.join(target, self.netrules_path) | ||
893 | 400 | util.write_file(netrules_path, netrules_content) | ||
894 | 0 | 401 | ||
895 | === modified file 'tests/unittests/test_net.py' | |||
896 | --- tests/unittests/test_net.py 2016-05-19 22:33:15 +0000 | |||
897 | +++ tests/unittests/test_net.py 2016-06-15 23:16:02 +0000 | |||
898 | @@ -2,6 +2,8 @@ | |||
899 | 2 | from cloudinit.net import cmdline | 2 | from cloudinit.net import cmdline |
900 | 3 | from cloudinit.net import eni | 3 | from cloudinit.net import eni |
901 | 4 | from cloudinit.net import network_state | 4 | from cloudinit.net import network_state |
902 | 5 | from cloudinit.net import sysconfig | ||
903 | 6 | from cloudinit.sources.helpers import openstack | ||
904 | 5 | from cloudinit import util | 7 | from cloudinit import util |
905 | 6 | 8 | ||
906 | 7 | from .helpers import mock | 9 | from .helpers import mock |
907 | @@ -74,6 +76,157 @@ | |||
908 | 74 | 'dns_nameservers': ['10.0.1.1']}], | 76 | 'dns_nameservers': ['10.0.1.1']}], |
909 | 75 | } | 77 | } |
910 | 76 | 78 | ||
911 | 79 | # Examples (and expected outputs for various renderers). | ||
912 | 80 | OS_SAMPLES = [ | ||
913 | 81 | { | ||
914 | 82 | 'in_data': { | ||
915 | 83 | "services": [{"type": "dns", "address": "172.19.0.12"}], | ||
916 | 84 | "networks": [{ | ||
917 | 85 | "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4", | ||
918 | 86 | "type": "ipv4", "netmask": "255.255.252.0", | ||
919 | 87 | "link": "tap1a81968a-79", | ||
920 | 88 | "routes": [{ | ||
921 | 89 | "netmask": "0.0.0.0", | ||
922 | 90 | "network": "0.0.0.0", | ||
923 | 91 | "gateway": "172.19.3.254", | ||
924 | 92 | }], | ||
925 | 93 | "ip_address": "172.19.1.34", "id": "network0" | ||
926 | 94 | }], | ||
927 | 95 | "links": [ | ||
928 | 96 | { | ||
929 | 97 | "ethernet_mac_address": "fa:16:3e:ed:9a:59", | ||
930 | 98 | "mtu": None, "type": "bridge", "id": | ||
931 | 99 | "tap1a81968a-79", | ||
932 | 100 | "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f" | ||
933 | 101 | }, | ||
934 | 102 | ], | ||
935 | 103 | }, | ||
936 | 104 | 'in_macs': { | ||
937 | 105 | 'fa:16:3e:ed:9a:59': 'eth0', | ||
938 | 106 | }, | ||
939 | 107 | 'out_sysconfig': [ | ||
940 | 108 | ('etc/sysconfig/network-scripts/ifcfg-eth0', | ||
941 | 109 | """ | ||
942 | 110 | # Created by cloud-init on instance boot automatically, do not edit. | ||
943 | 111 | # | ||
944 | 112 | BOOTPROTO=static | ||
945 | 113 | DEFROUTE=yes | ||
946 | 114 | DEVICE=eth0 | ||
947 | 115 | GATEWAY=172.19.3.254 | ||
948 | 116 | HWADDR=fa:16:3e:ed:9a:59 | ||
949 | 117 | IPADDR=172.19.1.34 | ||
950 | 118 | NETMASK=255.255.252.0 | ||
951 | 119 | NM_CONTROLLED=no | ||
952 | 120 | ONBOOT=yes | ||
953 | 121 | TYPE=Ethernet | ||
954 | 122 | USERCTL=no | ||
955 | 123 | """.lstrip()), | ||
956 | 124 | ('etc/sysconfig/network-scripts/route-eth0', | ||
957 | 125 | """ | ||
958 | 126 | # Created by cloud-init on instance boot automatically, do not edit. | ||
959 | 127 | # | ||
960 | 128 | ADDRESS0=0.0.0.0 | ||
961 | 129 | GATEWAY0=172.19.3.254 | ||
962 | 130 | NETMASK0=0.0.0.0 | ||
963 | 131 | """.lstrip()), | ||
964 | 132 | ('etc/resolv.conf', | ||
965 | 133 | """ | ||
966 | 134 | ; Created by cloud-init on instance boot automatically, do not edit. | ||
967 | 135 | ; | ||
968 | 136 | nameserver 172.19.0.12 | ||
969 | 137 | """.lstrip()), | ||
970 | 138 | ('etc/udev/rules.d/70-persistent-net.rules', | ||
971 | 139 | "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', | ||
972 | 140 | 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] | ||
973 | 141 | } | ||
974 | 142 | ] | ||
975 | 143 | |||
976 | 144 | |||
977 | 145 | def _setup_test(tmp_dir, mock_get_devicelist, mock_sys_netdev_info, | ||
978 | 146 | mock_sys_dev_path): | ||
979 | 147 | mock_get_devicelist.return_value = ['eth1000'] | ||
980 | 148 | dev_characteristics = { | ||
981 | 149 | 'eth1000': { | ||
982 | 150 | "bridge": False, | ||
983 | 151 | "carrier": False, | ||
984 | 152 | "dormant": False, | ||
985 | 153 | "operstate": "down", | ||
986 | 154 | "address": "07-1C-C6-75-A4-BE", | ||
987 | 155 | } | ||
988 | 156 | } | ||
989 | 157 | |||
990 | 158 | def netdev_info(name, field): | ||
991 | 159 | return dev_characteristics[name][field] | ||
992 | 160 | |||
993 | 161 | mock_sys_netdev_info.side_effect = netdev_info | ||
994 | 162 | |||
995 | 163 | def sys_dev_path(devname, path=""): | ||
996 | 164 | return tmp_dir + devname + "/" + path | ||
997 | 165 | |||
998 | 166 | for dev in dev_characteristics: | ||
999 | 167 | os.makedirs(os.path.join(tmp_dir, dev)) | ||
1000 | 168 | with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh: | ||
1001 | 169 | fh.write("down") | ||
1002 | 170 | |||
1003 | 171 | mock_sys_dev_path.side_effect = sys_dev_path | ||
1004 | 172 | |||
1005 | 173 | |||
1006 | 174 | class TestSysConfigRendering(TestCase): | ||
1007 | 175 | |||
1008 | 176 | @mock.patch("cloudinit.net.sys_dev_path") | ||
1009 | 177 | @mock.patch("cloudinit.net.sys_netdev_info") | ||
1010 | 178 | @mock.patch("cloudinit.net.get_devicelist") | ||
1011 | 179 | def test_default_generation(self, mock_get_devicelist, | ||
1012 | 180 | mock_sys_netdev_info, | ||
1013 | 181 | mock_sys_dev_path): | ||
1014 | 182 | tmp_dir = tempfile.mkdtemp() | ||
1015 | 183 | self.addCleanup(shutil.rmtree, tmp_dir) | ||
1016 | 184 | _setup_test(tmp_dir, mock_get_devicelist, | ||
1017 | 185 | mock_sys_netdev_info, mock_sys_dev_path) | ||
1018 | 186 | |||
1019 | 187 | network_cfg = net.generate_fallback_config() | ||
1020 | 188 | ns = network_state.parse_net_config_data(network_cfg, | ||
1021 | 189 | skip_broken=False) | ||
1022 | 190 | |||
1023 | 191 | render_dir = os.path.join(tmp_dir, "render") | ||
1024 | 192 | os.makedirs(render_dir) | ||
1025 | 193 | |||
1026 | 194 | renderer = sysconfig.Renderer() | ||
1027 | 195 | renderer.render_network_state(render_dir, ns) | ||
1028 | 196 | |||
1029 | 197 | render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000' | ||
1030 | 198 | with open(os.path.join(render_dir, render_file)) as fh: | ||
1031 | 199 | content = fh.read() | ||
1032 | 200 | expected_content = """ | ||
1033 | 201 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1034 | 202 | # | ||
1035 | 203 | BOOTPROTO=dhcp | ||
1036 | 204 | DEVICE=eth1000 | ||
1037 | 205 | HWADDR=07-1C-C6-75-A4-BE | ||
1038 | 206 | NM_CONTROLLED=no | ||
1039 | 207 | ONBOOT=yes | ||
1040 | 208 | TYPE=Ethernet | ||
1041 | 209 | USERCTL=no | ||
1042 | 210 | """.lstrip() | ||
1043 | 211 | self.assertEqual(expected_content, content) | ||
1044 | 212 | |||
1045 | 213 | def test_openstack_rendering_samples(self): | ||
1046 | 214 | tmp_dir = tempfile.mkdtemp() | ||
1047 | 215 | self.addCleanup(shutil.rmtree, tmp_dir) | ||
1048 | 216 | render_dir = os.path.join(tmp_dir, "render") | ||
1049 | 217 | for os_sample in OS_SAMPLES: | ||
1050 | 218 | ex_input = os_sample['in_data'] | ||
1051 | 219 | ex_mac_addrs = os_sample['in_macs'] | ||
1052 | 220 | network_cfg = openstack.convert_net_json( | ||
1053 | 221 | ex_input, known_macs=ex_mac_addrs) | ||
1054 | 222 | ns = network_state.parse_net_config_data(network_cfg, | ||
1055 | 223 | skip_broken=False) | ||
1056 | 224 | renderer = sysconfig.Renderer() | ||
1057 | 225 | renderer.render_network_state(render_dir, ns) | ||
1058 | 226 | for fn, expected_content in os_sample.get('out_sysconfig', []): | ||
1059 | 227 | with open(os.path.join(render_dir, fn)) as fh: | ||
1060 | 228 | self.assertEqual(expected_content, fh.read()) | ||
1061 | 229 | |||
1062 | 77 | 230 | ||
1063 | 78 | class TestEniNetRendering(TestCase): | 231 | class TestEniNetRendering(TestCase): |
1064 | 79 | 232 | ||
1065 | @@ -83,35 +236,10 @@ | |||
1066 | 83 | def test_default_generation(self, mock_get_devicelist, | 236 | def test_default_generation(self, mock_get_devicelist, |
1067 | 84 | mock_sys_netdev_info, | 237 | mock_sys_netdev_info, |
1068 | 85 | mock_sys_dev_path): | 238 | mock_sys_dev_path): |
1069 | 86 | mock_get_devicelist.return_value = ['eth1000', 'lo'] | ||
1070 | 87 | |||
1071 | 88 | dev_characteristics = { | ||
1072 | 89 | 'eth1000': { | ||
1073 | 90 | "bridge": False, | ||
1074 | 91 | "carrier": False, | ||
1075 | 92 | "dormant": False, | ||
1076 | 93 | "operstate": "down", | ||
1077 | 94 | "address": "07-1C-C6-75-A4-BE", | ||
1078 | 95 | } | ||
1079 | 96 | } | ||
1080 | 97 | |||
1081 | 98 | def netdev_info(name, field): | ||
1082 | 99 | return dev_characteristics[name][field] | ||
1083 | 100 | |||
1084 | 101 | mock_sys_netdev_info.side_effect = netdev_info | ||
1085 | 102 | |||
1086 | 103 | tmp_dir = tempfile.mkdtemp() | 239 | tmp_dir = tempfile.mkdtemp() |
1087 | 104 | self.addCleanup(shutil.rmtree, tmp_dir) | 240 | self.addCleanup(shutil.rmtree, tmp_dir) |
1098 | 105 | 241 | _setup_test(tmp_dir, mock_get_devicelist, | |
1099 | 106 | def sys_dev_path(devname, path=""): | 242 | mock_sys_netdev_info, mock_sys_dev_path) |
1090 | 107 | return tmp_dir + devname + "/" + path | ||
1091 | 108 | |||
1092 | 109 | for dev in dev_characteristics: | ||
1093 | 110 | os.makedirs(os.path.join(tmp_dir, dev)) | ||
1094 | 111 | with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh: | ||
1095 | 112 | fh.write("down") | ||
1096 | 113 | |||
1097 | 114 | mock_sys_dev_path.side_effect = sys_dev_path | ||
1100 | 115 | 243 | ||
1101 | 116 | network_cfg = net.generate_fallback_config() | 244 | network_cfg = net.generate_fallback_config() |
1102 | 117 | ns = network_state.parse_net_config_data(network_cfg, | 245 | ns = network_state.parse_net_config_data(network_cfg, |
1103 | @@ -120,11 +248,11 @@ | |||
1104 | 120 | render_dir = os.path.join(tmp_dir, "render") | 248 | render_dir = os.path.join(tmp_dir, "render") |
1105 | 121 | os.makedirs(render_dir) | 249 | os.makedirs(render_dir) |
1106 | 122 | 250 | ||
1112 | 123 | renderer = eni.Renderer() | 251 | renderer = eni.Renderer( |
1113 | 124 | renderer.render_network_state(render_dir, ns, | 252 | {'links_path_prefix': None, |
1114 | 125 | eni="interfaces", | 253 | 'eni_path': 'interfaces', 'netrules_path': None, |
1115 | 126 | links_prefix=None, | 254 | }) |
1116 | 127 | netrules=None) | 255 | renderer.render_network_state(render_dir, ns) |
1117 | 128 | 256 | ||
1118 | 129 | self.assertTrue(os.path.exists(os.path.join(render_dir, | 257 | self.assertTrue(os.path.exists(os.path.join(render_dir, |
1119 | 130 | 'interfaces'))) | 258 | 'interfaces'))) |
remove your print statements.
i guess go ahead and fix the TODO that you added.
running 'tox' fails for me, always good to fix that .
the rest of it looks reasonable.