Merge ~raharper/cloud-init:feature/cloud-init-hotplug-handler into cloud-init:master
- Git
- lp:~raharper/cloud-init
- feature/cloud-init-hotplug-handler
- Merge into master
Status: | Work in progress |
---|---|
Proposed branch: | ~raharper/cloud-init:feature/cloud-init-hotplug-handler |
Merge into: | cloud-init:master |
Diff against target: |
1122 lines (+709/-45) 26 files modified
bash_completion/cloud-init (+4/-1) cloudinit/cmd/devel/hotplug_hook.py (+210/-0) cloudinit/cmd/devel/parser.py (+3/-1) cloudinit/distros/__init__.py (+4/-3) cloudinit/distros/debian.py (+16/-7) cloudinit/event.py (+59/-1) cloudinit/net/eni.py (+2/-0) cloudinit/net/netplan.py (+2/-0) cloudinit/net/renderer.py (+8/-3) cloudinit/net/sysconfig.py (+2/-0) cloudinit/sources/DataSourceOpenStack.py (+15/-2) cloudinit/sources/__init__.py (+19/-9) cloudinit/stages.py (+45/-11) cloudinit/tests/test_event.py (+75/-0) cloudinit/tests/test_stages.py (+26/-5) cloudinit/version.py (+2/-0) config/cloud.cfg.d/10_updates_policy.cfg (+6/-0) doc/rtd/index.rst (+1/-0) doc/rtd/topics/capabilities.rst (+6/-0) doc/rtd/topics/datasources/smartos.rst (+2/-2) doc/rtd/topics/events.rst (+150/-0) setup.py (+2/-0) systemd/cloud-init-hotplugd.service (+11/-0) systemd/cloud-init-hotplugd.socket (+8/-0) tools/hook-hotplug (+26/-0) udev/10-cloud-init-hook-hotplug.rules (+5/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | review-wip | Pending | |
Review via email: mp+356152@code.launchpad.net |
Commit message
wip fixme
Description of the change
- 725b992... by Ryan Harper
-
Add unittests, remove hard coded scope, fix tox
- abaa5b1... by Ryan Harper
-
Add tests for event module
Scott Moser (smoser) : | # |
Ryan Harper (raharper) wrote : | # |
Thanks for the review. I've pushed some of your changes already. Doing another round now.
- a2eb198... by Ryan Harper
-
Address review comments
hotplug:
- Capture update result, log on error
- Add more error details when cloud-init is not configured for hotplug
- Use RuntimeError instead of Exception, add more detail in message
tools/hook-hotplug
- Drop trailing slash
- Quote environment variables when creating array - 97dc6e6... by Ryan Harper
-
EventType.UDEV -> EventType.HOTPLUG and udev -> hotplug
- 2d16ce8... by Ryan Harper
-
Drop extra || return 1
- 5b0d4cf... by Ryan Harper
-
Fix logic thinko if not allowed return
- 94d37e1... by Ryan Harper
-
docs: add documentation for events and hotplug support
Si-Wei Liu (siwliu) wrote : | # |
Any plan to merge this into master soon before the next quarterly cloud-init release? I see no progress had been made since November last year, wondering what happened to this enhancement? Oracle is looking forward to adopting and leveraging this feature in its Ubuntu cloud images.
Unmerged commits
- 94d37e1... by Ryan Harper
-
docs: add documentation for events and hotplug support
- 5b0d4cf... by Ryan Harper
-
Fix logic thinko if not allowed return
- 2d16ce8... by Ryan Harper
-
Drop extra || return 1
- 97dc6e6... by Ryan Harper
-
EventType.UDEV -> EventType.HOTPLUG and udev -> hotplug
- a2eb198... by Ryan Harper
-
Address review comments
hotplug:
- Capture update result, log on error
- Add more error details when cloud-init is not configured for hotplug
- Use RuntimeError instead of Exception, add more detail in message
tools/hook-hotplug
- Drop trailing slash
- Quote environment variables when creating array - abaa5b1... by Ryan Harper
-
Add tests for event module
- 725b992... by Ryan Harper
-
Add unittests, remove hard coded scope, fix tox
- af4ca37... by Ryan Harper
-
Add debug, copy dictionaries before merging
- 82287a4... by Ryan Harper
-
Reorder updates config merging with datasource capabilities
System config needs to filter datasource capabilities.
- 809bb1a... by Ryan Harper
-
Retain apply_network_
config, apply names but not config in some cases
Preview Diff
1 | diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init |
2 | index 8c25032..0eab40c 100644 |
3 | --- a/bash_completion/cloud-init |
4 | +++ b/bash_completion/cloud-init |
5 | @@ -28,7 +28,7 @@ _cloudinit_complete() |
6 | COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word)) |
7 | ;; |
8 | devel) |
9 | - COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word)) |
10 | + COMPREPLY=($(compgen -W "--help hotplug-hook net-convert schema" -- $cur_word)) |
11 | ;; |
12 | dhclient-hook|features) |
13 | COMPREPLY=($(compgen -W "--help" -- $cur_word)) |
14 | @@ -61,6 +61,9 @@ _cloudinit_complete() |
15 | --frequency) |
16 | COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word)) |
17 | ;; |
18 | + hotplug-hook) |
19 | + COMPREPLY=($(compgen -W "--help" -- $cur_word)) |
20 | + ;; |
21 | net-convert) |
22 | COMPREPLY=($(compgen -W "--help --network-data --kind --directory --output-kind" -- $cur_word)) |
23 | ;; |
24 | diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py |
25 | new file mode 100644 |
26 | index 0000000..ae23d07 |
27 | --- /dev/null |
28 | +++ b/cloudinit/cmd/devel/hotplug_hook.py |
29 | @@ -0,0 +1,210 @@ |
30 | +# This file is part of cloud-init. See LICENSE file for license information. |
31 | + |
32 | +"""Handle reconfiguration on hotplug events""" |
33 | +import abc |
34 | +import argparse |
35 | +import os |
36 | +import six |
37 | +import sys |
38 | +import time |
39 | + |
40 | +from cloudinit.event import EventType |
41 | +from cloudinit import log |
42 | +from cloudinit import reporting |
43 | +from cloudinit.reporting import events |
44 | +from cloudinit import sources |
45 | +from cloudinit.stages import Init |
46 | +from cloudinit.net import read_sys_net_safe |
47 | +from cloudinit.net.network_state import parse_net_config_data |
48 | + |
49 | + |
50 | +LOG = log.getLogger(__name__) |
51 | +NAME = 'hotplug-hook' |
52 | + |
53 | + |
54 | +def get_parser(parser=None): |
55 | + """Build or extend and arg parser for hotplug-hook utility. |
56 | + |
57 | + @param parser: Optional existing ArgumentParser instance representing the |
58 | + subcommand which will be extended to support the args of this utility. |
59 | + |
60 | + @returns: ArgumentParser with proper argument configuration. |
61 | + """ |
62 | + if not parser: |
63 | + parser = argparse.ArgumentParser(prog=NAME, description=__doc__) |
64 | + |
65 | + parser.add_argument("-d", "--devpath", |
66 | + metavar="PATH", |
67 | + help="sysfs path to hotplugged device") |
68 | + parser.add_argument("--hotplug-debug", action='store_true', |
69 | + help='enable debug logging to stderr.') |
70 | + parser.add_argument("-s", "--subsystem", |
71 | + choices=['net', 'block']) |
72 | + parser.add_argument("-u", "--udevaction", |
73 | + choices=['add', 'change', 'remove']) |
74 | + |
75 | + return parser |
76 | + |
77 | + |
78 | +def log_console(msg): |
79 | + """Log messages to stderr console and configured logging.""" |
80 | + sys.stderr.write(msg + '\n') |
81 | + sys.stderr.flush() |
82 | + LOG.debug(msg) |
83 | + |
84 | + |
85 | +def devpath_to_macaddr(devpath): |
86 | + macaddr = read_sys_net_safe(os.path.basename(devpath), 'address') |
87 | + return macaddr |
88 | + |
89 | + |
90 | +def in_netconfig(unique_id, netconfig): |
91 | + netstate = parse_net_config_data(netconfig) |
92 | + found = [iface |
93 | + for iface in netstate.iter_interfaces() |
94 | + if iface.get('mac_address') == unique_id] |
95 | + log_console('Ifaces with ID=%s : %s' % (unique_id, found)) |
96 | + return len(found) > 0 |
97 | + |
98 | + |
99 | +@six.add_metaclass(abc.ABCMeta) |
100 | +class UeventHandler(object): |
101 | + def __init__(self, ds, devpath, success_fn): |
102 | + self.datasource = ds |
103 | + self.devpath = devpath |
104 | + self.success_fn = success_fn |
105 | + |
106 | + @abc.abstractmethod |
107 | + def apply(self): |
108 | + raise NotImplementedError() |
109 | + |
110 | + @property |
111 | + @abc.abstractmethod |
112 | + def config(self): |
113 | + raise NotImplementedError() |
114 | + |
115 | + @abc.abstractmethod |
116 | + def detect(self, action): |
117 | + raise NotImplementedError() |
118 | + |
119 | + def success(self): |
120 | + return self.success_fn() |
121 | + |
122 | + def update(self): |
123 | + result = self.datasource.update_metadata([EventType.HOTPLUG]) |
124 | + if not result: |
125 | + log_console('Datasource %s not updated for ' |
126 | + ' event %s' % (self.datasource, EventType.HOTPLUG)) |
127 | + return result |
128 | + |
129 | + |
130 | +class NetHandler(UeventHandler): |
131 | + def __init__(self, ds, devpath, success_fn): |
132 | + super(NetHandler, self).__init__(ds, devpath, success_fn) |
133 | + self.id = devpath_to_macaddr(self.devpath) |
134 | + |
135 | + def apply(self): |
136 | + return self.datasource.distro.apply_network_config(self.config, |
137 | + bring_up=True) |
138 | + |
139 | + @property |
140 | + def config(self): |
141 | + return self.datasource.network_config |
142 | + |
143 | + def detect(self, action): |
144 | + detect_presence = None |
145 | + if action == 'add': |
146 | + detect_presence = True |
147 | + elif action == 'remove': |
148 | + detect_presence = False |
149 | + else: |
150 | + raise ValueError('Cannot detect unknown action: %s' % action) |
151 | + |
152 | + return detect_presence == in_netconfig(self.id, self.config) |
153 | + |
154 | + |
155 | +UEVENT_HANDLERS = { |
156 | + 'net': NetHandler, |
157 | +} |
158 | + |
159 | +SUBSYSTEM_TO_EVENT = { |
160 | + 'net': 'network', |
161 | + 'block': 'storage', |
162 | +} |
163 | + |
164 | + |
165 | +def handle_args(name, args): |
166 | + log_console('%s called with args=%s' % (NAME, args)) |
167 | + hotplug_reporter = events.ReportEventStack(NAME, __doc__, |
168 | + reporting_enabled=True) |
169 | + with hotplug_reporter: |
170 | + # only handling net udev events for now |
171 | + event_handler_cls = UEVENT_HANDLERS.get(args.subsystem) |
172 | + if not event_handler_cls: |
173 | + log_console('hotplug-hook: cannot handle events for subsystem: ' |
174 | + '"%s"' % args.subsystem) |
175 | + return 1 |
176 | + |
177 | + log_console('Reading cloud-init configation') |
178 | + hotplug_init = Init(ds_deps=[], reporter=hotplug_reporter) |
179 | + hotplug_init.read_cfg() |
180 | + |
181 | + log_console('Configuring logging') |
182 | + log.setupLogging(hotplug_init.cfg) |
183 | + if 'reporting' in hotplug_init.cfg: |
184 | + reporting.update_configuration(hotplug_init.cfg.get('reporting')) |
185 | + |
186 | + log_console('Fetching datasource') |
187 | + try: |
188 | + ds = hotplug_init.fetch(existing="trust") |
189 | + except sources.DataSourceNotFoundException: |
190 | + log_console('No Ds found') |
191 | + return 1 |
192 | + |
193 | + subevent = SUBSYSTEM_TO_EVENT.get(args.subsystem) |
194 | + if not hotplug_init.update_event_allowed(EventType.HOTPLUG, |
195 | + scope=subevent): |
196 | + log_console('cloud-init not configured to handle hotplug event' |
197 | + ' of type %s' % subevent) |
198 | + return |
199 | + |
200 | + log_console('Creating %s event handler' % args.subsystem) |
201 | + event_handler = event_handler_cls(ds, args.devpath, |
202 | + hotplug_init._write_to_cache) |
203 | + retries = [1, 1, 1, 3, 5] |
204 | + for attempt, wait in enumerate(retries): |
205 | + log_console('subsystem=%s update attempt %s/%s' % (args.subsystem, |
206 | + attempt, |
207 | + len(retries))) |
208 | + try: |
209 | + log_console('Refreshing metadata') |
210 | + result = event_handler.update() |
211 | + if not result: |
212 | + raise RuntimeError( |
213 | + 'Event handler %s update failed' % event_handler_cls) |
214 | + |
215 | + if event_handler.detect(action=args.udevaction): |
216 | + log_console('Detected update, apply config change') |
217 | + event_handler.apply() |
218 | + log_console('Updating cache') |
219 | + event_handler.success() |
220 | + break |
221 | + else: |
222 | + raise RuntimeError('Failed to detect %s in updated ' |
223 | + 'metadata' % event_handler.id) |
224 | + |
225 | + except Exception as e: |
226 | + if attempt + 1 >= len(retries): |
227 | + raise |
228 | + log_console('Exception while processing hotplug event. %s' % e) |
229 | + time.sleep(wait) |
230 | + |
231 | + log_console('Exiting hotplug handler') |
232 | + reporting.flush_events() |
233 | + |
234 | + |
235 | +if __name__ == '__main__': |
236 | + args = get_parser().parse_args() |
237 | + handle_args(NAME, args) |
238 | + |
239 | +# vi: ts=4 expandtab |
240 | diff --git a/cloudinit/cmd/devel/parser.py b/cloudinit/cmd/devel/parser.py |
241 | index 99a234c..3ad09b3 100644 |
242 | --- a/cloudinit/cmd/devel/parser.py |
243 | +++ b/cloudinit/cmd/devel/parser.py |
244 | @@ -6,7 +6,7 @@ |
245 | |
246 | import argparse |
247 | from cloudinit.config import schema |
248 | - |
249 | +from . import hotplug_hook |
250 | from . import net_convert |
251 | from . import render |
252 | |
253 | @@ -20,6 +20,8 @@ def get_parser(parser=None): |
254 | subparsers.required = True |
255 | |
256 | subcmds = [ |
257 | + (hotplug_hook.NAME, hotplug_hook.__doc__, |
258 | + hotplug_hook.get_parser, hotplug_hook.handle_args), |
259 | ('schema', 'Validate cloud-config files for document schema', |
260 | schema.get_parser, schema.handle_schema_args), |
261 | (net_convert.NAME, net_convert.__doc__, |
262 | diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py |
263 | index ef618c2..92285f5 100644 |
264 | --- a/cloudinit/distros/__init__.py |
265 | +++ b/cloudinit/distros/__init__.py |
266 | @@ -69,6 +69,7 @@ class Distro(object): |
267 | self._paths = paths |
268 | self._cfg = cfg |
269 | self.name = name |
270 | + self.net_renderer = None |
271 | |
272 | @abc.abstractmethod |
273 | def install_packages(self, pkglist): |
274 | @@ -89,9 +90,8 @@ class Distro(object): |
275 | name, render_cls = renderers.select(priority=priority) |
276 | LOG.debug("Selected renderer '%s' from priority list: %s", |
277 | name, priority) |
278 | - renderer = render_cls(config=self.renderer_configs.get(name)) |
279 | - renderer.render_network_config(network_config) |
280 | - return [] |
281 | + self.net_renderer = render_cls(config=self.renderer_configs.get(name)) |
282 | + return self.net_renderer.render_network_config(network_config) |
283 | |
284 | def _find_tz_file(self, tz): |
285 | tz_file = os.path.join(self.tz_zone_dir, str(tz)) |
286 | @@ -176,6 +176,7 @@ class Distro(object): |
287 | # a much less complete network config format (interfaces(5)). |
288 | try: |
289 | dev_names = self._write_network_config(netconfig) |
290 | + LOG.debug('Network config found dev names: %s', dev_names) |
291 | except NotImplementedError: |
292 | # backwards compat until all distros have apply_network_config |
293 | return self._apply_network_from_network_config( |
294 | diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py |
295 | index d517fb8..4f1e6a9 100644 |
296 | --- a/cloudinit/distros/debian.py |
297 | +++ b/cloudinit/distros/debian.py |
298 | @@ -114,14 +114,23 @@ class Distro(distros.Distro): |
299 | return self._supported_write_network_config(netconfig) |
300 | |
301 | def _bring_up_interfaces(self, device_names): |
302 | - use_all = False |
303 | - for d in device_names: |
304 | - if d == 'all': |
305 | - use_all = True |
306 | - if use_all: |
307 | - return distros.Distro._bring_up_interface(self, '--all') |
308 | + render_name = self.net_renderer.name |
309 | + if render_name == 'eni': |
310 | + LOG.debug('Bringing up interfaces with eni/ifup') |
311 | + use_all = False |
312 | + for d in device_names: |
313 | + if d == 'all': |
314 | + use_all = True |
315 | + if use_all: |
316 | + return distros.Distro._bring_up_interface(self, '--all') |
317 | + else: |
318 | + return distros.Distro._bring_up_interfaces(self, device_names) |
319 | + elif render_name == 'netplan': |
320 | + LOG.debug('Bringing up interfaces with netplan apply') |
321 | + util.subp(['netplan', 'apply']) |
322 | else: |
323 | - return distros.Distro._bring_up_interfaces(self, device_names) |
324 | + LOG.warning('Cannot bring up interfaces, unknown renderer: "%s"', |
325 | + render_name) |
326 | |
327 | def _write_hostname(self, your_hostname, out_fn): |
328 | conf = None |
329 | diff --git a/cloudinit/event.py b/cloudinit/event.py |
330 | index f7b311f..3a18ef9 100644 |
331 | --- a/cloudinit/event.py |
332 | +++ b/cloudinit/event.py |
333 | @@ -2,16 +2,74 @@ |
334 | |
335 | """Classes and functions related to event handling.""" |
336 | |
337 | +import copy |
338 | + |
339 | +from cloudinit import log as logging |
340 | +from cloudinit import util |
341 | + |
342 | + |
343 | +LOG = logging.getLogger(__name__) |
344 | + |
345 | |
346 | # Event types which can generate maintenance requests for cloud-init. |
347 | class EventType(object): |
348 | BOOT = "System boot" |
349 | BOOT_NEW_INSTANCE = "New instance first boot" |
350 | + HOTPLUG = "Hotplug add event" |
351 | |
352 | # TODO: Cloud-init will grow support for the follow event types: |
353 | - # UDEV |
354 | # METADATA_CHANGE |
355 | # USER_REQUEST |
356 | |
357 | |
358 | +EventTypeMap = { |
359 | + 'boot': EventType.BOOT, |
360 | + 'boot-new-instance': EventType.BOOT_NEW_INSTANCE, |
361 | + 'hotplug': EventType.HOTPLUG, |
362 | +} |
363 | + |
364 | + |
365 | +# inverted mapping |
366 | +EventNameMap = {v: k for k, v in EventTypeMap.items()} |
367 | + |
368 | + |
369 | +def get_allowed_events(sys_events, ds_events): |
370 | + '''Merge datasource capabilties with system config to determine which |
371 | + update events are allowed.''' |
372 | + |
373 | + # updates: |
374 | + # policy-version: 1 |
375 | + # network: |
376 | + # when: [boot-new-instance, boot, hotplug] |
377 | + # storage: |
378 | + # when: [boot-new-instance, hotplug] |
379 | + # watch: http://169.254.169.254/metadata/storage_config/ |
380 | + |
381 | + LOG.debug('updates: system cfg: %s', sys_events) |
382 | + LOG.debug('updates: datasrc caps: %s', ds_events) |
383 | + |
384 | + updates = util.mergemanydict([copy.deepcopy(sys_events), |
385 | + copy.deepcopy(ds_events)]) |
386 | + LOG.debug('updates: merged cfg: %s', updates) |
387 | + |
388 | + events = {} |
389 | + for etype in [scope for scope, val in sys_events.items() |
390 | + if type(val) == dict and 'when' in val]: |
391 | + events[etype] = ( |
392 | + set([EventTypeMap.get(evt) |
393 | + for evt in updates.get(etype, {}).get('when', []) |
394 | + if evt in EventTypeMap])) |
395 | + |
396 | + LOG.debug('updates: allowed events: %s', events) |
397 | + return events |
398 | + |
399 | + |
400 | +def get_update_events_config(update_events): |
401 | + '''Return a dictionary of updates config''' |
402 | + evt_cfg = {'policy-version': 1} |
403 | + for scope, events in update_events.items(): |
404 | + evt_cfg[scope] = {'when': [EventNameMap[evt] for evt in events]} |
405 | + |
406 | + return evt_cfg |
407 | + |
408 | # vi: ts=4 expandtab |
409 | diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py |
410 | index c6f631a..3d8dcfb 100644 |
411 | --- a/cloudinit/net/eni.py |
412 | +++ b/cloudinit/net/eni.py |
413 | @@ -338,6 +338,8 @@ def _ifaces_to_net_config_data(ifaces): |
414 | class Renderer(renderer.Renderer): |
415 | """Renders network information in a /etc/network/interfaces format.""" |
416 | |
417 | + name = 'eni' |
418 | + |
419 | def __init__(self, config=None): |
420 | if not config: |
421 | config = {} |
422 | diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py |
423 | index bc1087f..08c9d05 100644 |
424 | --- a/cloudinit/net/netplan.py |
425 | +++ b/cloudinit/net/netplan.py |
426 | @@ -178,6 +178,8 @@ def _clean_default(target=None): |
427 | class Renderer(renderer.Renderer): |
428 | """Renders network information in a /etc/netplan/network.yaml format.""" |
429 | |
430 | + name = 'netplan' |
431 | + |
432 | NETPLAN_GENERATE = ['netplan', 'generate'] |
433 | |
434 | def __init__(self, config=None): |
435 | diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py |
436 | index 5f32e90..88a1221 100644 |
437 | --- a/cloudinit/net/renderer.py |
438 | +++ b/cloudinit/net/renderer.py |
439 | @@ -44,6 +44,10 @@ class Renderer(object): |
440 | driver=driver)) |
441 | return content.getvalue() |
442 | |
443 | + @staticmethod |
444 | + def get_interface_names(network_state): |
445 | + return [cfg.get('name') for cfg in network_state.iter_interfaces()] |
446 | + |
447 | @abc.abstractmethod |
448 | def render_network_state(self, network_state, templates=None, |
449 | target=None): |
450 | @@ -51,8 +55,9 @@ class Renderer(object): |
451 | |
452 | def render_network_config(self, network_config, templates=None, |
453 | target=None): |
454 | - return self.render_network_state( |
455 | - network_state=parse_net_config_data(network_config), |
456 | - templates=templates, target=target) |
457 | + network_state = parse_net_config_data(network_config) |
458 | + self.render_network_state(network_state=network_state, |
459 | + templates=templates, target=target) |
460 | + return self.get_interface_names(network_state) |
461 | |
462 | # vi: ts=4 expandtab |
463 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py |
464 | index 9c16d3a..d502268 100644 |
465 | --- a/cloudinit/net/sysconfig.py |
466 | +++ b/cloudinit/net/sysconfig.py |
467 | @@ -232,6 +232,8 @@ class NetInterface(ConfigMap): |
468 | class Renderer(renderer.Renderer): |
469 | """Renders network information in a /etc/sysconfig format.""" |
470 | |
471 | + name = 'sysconfig' |
472 | + |
473 | # See: https://access.redhat.com/documentation/en-US/\ |
474 | # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\ |
475 | # s1-networkscripts-interfaces.html (or other docs for |
476 | diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py |
477 | index 4a01524..b777c65 100644 |
478 | --- a/cloudinit/sources/DataSourceOpenStack.py |
479 | +++ b/cloudinit/sources/DataSourceOpenStack.py |
480 | @@ -7,7 +7,9 @@ |
481 | import time |
482 | |
483 | from cloudinit import log as logging |
484 | +from cloudinit.event import EventType |
485 | from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError |
486 | +from cloudinit.net import is_up |
487 | from cloudinit import sources |
488 | from cloudinit import url_helper |
489 | from cloudinit import util |
490 | @@ -93,6 +95,15 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): |
491 | return sources.instance_id_matches_system_uuid(self.get_instance_id()) |
492 | |
493 | @property |
494 | + def update_events(self): |
495 | + events = {'network': set([EventType.BOOT_NEW_INSTANCE, |
496 | + EventType.BOOT, |
497 | + EventType.HOTPLUG]), |
498 | + 'storage': set([])} |
499 | + LOG.debug('OpenStack update events: %s', events) |
500 | + return events |
501 | + |
502 | + @property |
503 | def network_config(self): |
504 | """Return a network config dict for rendering ENI or netplan files.""" |
505 | if self._network_config != sources.UNSET: |
506 | @@ -122,11 +133,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): |
507 | False when unable to contact metadata service or when metadata |
508 | format is invalid or disabled. |
509 | """ |
510 | - oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list') |
511 | + oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list', |
512 | + {}) |
513 | if not detect_openstack(accept_oracle=not oracle_considered): |
514 | return False |
515 | |
516 | - if self.perform_dhcp_setup: # Setup networking in init-local stage. |
517 | + if self.perform_dhcp_setup and not is_up(self.fallback_interface): |
518 | + # Setup networking in init-local stage. |
519 | try: |
520 | with EphemeralDHCPv4(self.fallback_interface): |
521 | results = util.log_time( |
522 | diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py |
523 | index e6966b3..00b88d4 100644 |
524 | --- a/cloudinit/sources/__init__.py |
525 | +++ b/cloudinit/sources/__init__.py |
526 | @@ -158,15 +158,6 @@ class DataSource(object): |
527 | url_timeout = 10 # timeout for each metadata url read attempt |
528 | url_retries = 5 # number of times to retry url upon 404 |
529 | |
530 | - # The datasource defines a set of supported EventTypes during which |
531 | - # the datasource can react to changes in metadata and regenerate |
532 | - # network configuration on metadata changes. |
533 | - # A datasource which supports writing network config on each system boot |
534 | - # would call update_events['network'].add(EventType.BOOT). |
535 | - |
536 | - # Default: generate network config on new instance id (first boot). |
537 | - update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])} |
538 | - |
539 | # N-tuple listing default values for any metadata-related class |
540 | # attributes cached on an instance by a process_data runs. These attribute |
541 | # values are reset via clear_cached_attrs during any update_metadata call. |
542 | @@ -176,6 +167,7 @@ class DataSource(object): |
543 | ('vendordata', None), ('vendordata_raw', None)) |
544 | |
545 | _dirty_cache = False |
546 | + _update_events = {} |
547 | |
548 | # N-tuple of keypaths or keynames redact from instance-data.json for |
549 | # non-root users |
550 | @@ -589,6 +581,24 @@ class DataSource(object): |
551 | def get_package_mirror_info(self): |
552 | return self.distro.get_package_mirror_info(data_source=self) |
553 | |
554 | + # The datasource defines a set of supported EventTypes during which |
555 | + # the datasource can react to changes in metadata and regenerate |
556 | + # network configuration on metadata changes. |
557 | + # A datasource which supports writing network config on each system boot |
558 | + # would call update_events['network'].add(EventType.BOOT). |
559 | + |
560 | + # Default: generate network config on new instance id (first boot). |
561 | + @property |
562 | + def update_events(self): |
563 | + if not self._update_events: |
564 | + self._update_events = {'network': |
565 | + set([EventType.BOOT_NEW_INSTANCE])} |
566 | + return self._update_events |
567 | + |
568 | + @update_events.setter |
569 | + def update_events(self, events): |
570 | + self._update_events.update(events) |
571 | + |
572 | def update_metadata(self, source_event_types): |
573 | """Refresh cached metadata if the datasource supports this event. |
574 | |
575 | diff --git a/cloudinit/stages.py b/cloudinit/stages.py |
576 | index 8a06412..90c5c76 100644 |
577 | --- a/cloudinit/stages.py |
578 | +++ b/cloudinit/stages.py |
579 | @@ -22,9 +22,8 @@ from cloudinit.handlers.cloud_config import CloudConfigPartHandler |
580 | from cloudinit.handlers.jinja_template import JinjaTemplatePartHandler |
581 | from cloudinit.handlers.shell_script import ShellScriptPartHandler |
582 | from cloudinit.handlers.upstart_job import UpstartJobPartHandler |
583 | - |
584 | -from cloudinit.event import EventType |
585 | - |
586 | +from cloudinit.event import ( |
587 | + EventType, get_allowed_events, get_update_events_config) |
588 | from cloudinit import cloud |
589 | from cloudinit import config |
590 | from cloudinit import distros |
591 | @@ -644,7 +643,48 @@ class Init(object): |
592 | return (ncfg, loc) |
593 | return (self.distro.generate_fallback_config(), "fallback") |
594 | |
595 | + def update_event_allowed(self, event_source_type, scope=None): |
596 | + # convert ds events to config |
597 | + ds_config = get_update_events_config(self.datasource.update_events) |
598 | + LOG.debug('Datasource updates cfg: %s', ds_config) |
599 | + |
600 | + sys_config = self.cfg.get('updates', {}) |
601 | + LOG.debug('System updates cfg: %s', sys_config) |
602 | + |
603 | + allowed = get_allowed_events(sys_config, ds_config) |
604 | + LOG.debug('Allowable update events: %s', allowed) |
605 | + |
606 | + if not scope: |
607 | + scopes = [allowed.keys()] |
608 | + else: |
609 | + scopes = [scope] |
610 | + LOG.debug('Possible scopes for this event: %s', scopes) |
611 | + |
612 | + for evt_scope in scopes: |
613 | + if event_source_type in allowed.get(evt_scope, []): |
614 | + LOG.debug('Event Allowed: scope=%s EventType=%s', |
615 | + evt_scope, event_source_type) |
616 | + return True |
617 | + |
618 | + LOG.debug('Event Denied: scopes=%s EventType=%s', |
619 | + scopes, event_source_type) |
620 | + return False |
621 | + |
622 | def apply_network_config(self, bring_up): |
623 | + apply_network = True |
624 | + if self.datasource is not NULL_DATA_SOURCE: |
625 | + if not self.is_new_instance(): |
626 | + if self.update_event_allowed(EventType.BOOT, scope='network'): |
627 | + if not self.datasource.update_metadata([EventType.BOOT]): |
628 | + LOG.debug( |
629 | + "No network config applied. Datasource failed" |
630 | + " update metadata on '%s' event", EventType.BOOT) |
631 | + apply_network = False |
632 | + else: |
633 | + LOG.debug("No network config applied. " |
634 | + "'%s' event not allowed", EventType.BOOT) |
635 | + apply_network = False |
636 | + |
637 | netcfg, src = self._find_networking_config() |
638 | if netcfg is None: |
639 | LOG.info("network config is disabled by %s", src) |
640 | @@ -656,14 +696,8 @@ class Init(object): |
641 | except Exception as e: |
642 | LOG.warning("Failed to rename devices: %s", e) |
643 | |
644 | - if self.datasource is not NULL_DATA_SOURCE: |
645 | - if not self.is_new_instance(): |
646 | - if not self.datasource.update_metadata([EventType.BOOT]): |
647 | - LOG.debug( |
648 | - "No network config applied. Neither a new instance" |
649 | - " nor datasource network update on '%s' event", |
650 | - EventType.BOOT) |
651 | - return |
652 | + if not apply_network: |
653 | + return |
654 | |
655 | LOG.info("Applying network configuration from %s bringup=%s: %s", |
656 | src, bring_up, netcfg) |
657 | diff --git a/cloudinit/tests/test_event.py b/cloudinit/tests/test_event.py |
658 | new file mode 100644 |
659 | index 0000000..ad81deb |
660 | --- /dev/null |
661 | +++ b/cloudinit/tests/test_event.py |
662 | @@ -0,0 +1,75 @@ |
663 | +# This file is part of cloud-init. See LICENSE file for license information. |
664 | + |
665 | +"""Tests related to cloudinit.event module.""" |
666 | + |
667 | +import copy |
668 | +import random |
669 | +import string |
670 | + |
671 | +from cloudinit.event import (EventType, |
672 | + EventNameMap, |
673 | + get_allowed_events, |
674 | + get_update_events_config) |
675 | + |
676 | +from cloudinit.tests.helpers import CiTestCase |
677 | + |
678 | + |
679 | +def rand_event_names(): |
680 | + return [random.choice(list(EventNameMap.keys())) |
681 | + for x in range(len(EventNameMap.keys()))] |
682 | + |
683 | + |
684 | +def rand_string(size=6, chars=string.ascii_lowercase): |
685 | + return ''.join(random.choice(chars) for x in range(size)) |
686 | + |
687 | + |
688 | +class TestEvent(CiTestCase): |
689 | + with_logs = True |
690 | + |
691 | + DEFAULT_UPDATE_EVENTS = {'network': set([EventType.BOOT_NEW_INSTANCE]), |
692 | + 'storage': set([])} |
693 | + DEFAULT_UPDATES_CONFIG = {'policy-version': 1, |
694 | + 'network': {'when': ['boot-new-instance']}, |
695 | + 'storage': {'when': []}} |
696 | + |
697 | + def test_events_to_config(self): |
698 | + """validate default update_events dictionary maps to default policy""" |
699 | + events = copy.deepcopy(self.DEFAULT_UPDATE_EVENTS) |
700 | + config = get_update_events_config(events) |
701 | + |
702 | + for scope, events in events.items(): |
703 | + self.assertIn(scope, config) |
704 | + for evt in events: |
705 | + self.assertIn(evt, EventNameMap) |
706 | + self.assertIn(EventNameMap.get(evt), |
707 | + config.get(scope).get('when')) |
708 | + |
709 | + self.assertEqual(sorted(config), |
710 | + sorted(self.DEFAULT_UPDATES_CONFIG)) |
711 | + |
712 | + def test_get_allowed_events_defaults_filter_datasource(self): |
713 | + ds_config = { |
714 | + 'policy-version': 1, |
715 | + 'network': {'when': rand_event_names()}, |
716 | + 'storage': {'when': rand_event_names()}, |
717 | + } |
718 | + allowed = get_allowed_events(self.DEFAULT_UPDATES_CONFIG, ds_config) |
719 | + |
720 | + # system config filters out ds capabilities |
721 | + self.assertEqual(sorted(allowed), sorted(self.DEFAULT_UPDATE_EVENTS)) |
722 | + |
723 | + def test_get_allowed_events_uses_system_config_scopes(self): |
724 | + ds_config = { |
725 | + 'policy-version': 1, |
726 | + 'network': {'when': rand_event_names()}, |
727 | + 'storage': {'when': rand_event_names()}, |
728 | + } |
729 | + rand_scope = rand_string() |
730 | + rand_events = rand_event_names() |
731 | + sys_config = {'policy-version': 1, rand_scope: {'when': rand_events}} |
732 | + |
733 | + self.assertNotIn(rand_scope, ds_config) |
734 | + allowed = get_allowed_events(sys_config, ds_config) |
735 | + self.assertIn(rand_scope, allowed) |
736 | + |
737 | +# vi: ts=4 expandtab |
738 | diff --git a/cloudinit/tests/test_stages.py b/cloudinit/tests/test_stages.py |
739 | index 94b6b25..037ca2f 100644 |
740 | --- a/cloudinit/tests/test_stages.py |
741 | +++ b/cloudinit/tests/test_stages.py |
742 | @@ -47,6 +47,10 @@ class TestInit(CiTestCase): |
743 | 'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir, |
744 | 'run_dir': self.tmpdir}}} |
745 | self.init.datasource = FakeDataSource(paths=self.init.paths) |
746 | + self.init.datasource.update_events = { |
747 | + 'network': set([EventType.BOOT_NEW_INSTANCE])} |
748 | + self.add_patch('cloudinit.stages.get_allowed_events', 'mock_allowed', |
749 | + return_value=self.init.datasource.update_events) |
750 | |
751 | def test_wb__find_networking_config_disabled(self): |
752 | """find_networking_config returns no config when disabled.""" |
753 | @@ -200,11 +204,10 @@ class TestInit(CiTestCase): |
754 | self.init._find_networking_config = fake_network_config |
755 | self.init.apply_network_config(True) |
756 | self.init.distro.apply_network_config_names.assert_called_with(net_cfg) |
757 | + self.assertIn("No network config applied. " |
758 | + "'%s' event not allowed" % EventType.BOOT, |
759 | + self.logs.getvalue()) |
760 | self.init.distro.apply_network_config.assert_not_called() |
761 | - self.assertIn( |
762 | - 'No network config applied. Neither a new instance' |
763 | - " nor datasource network update on '%s' event" % EventType.BOOT, |
764 | - self.logs.getvalue()) |
765 | |
766 | @mock.patch('cloudinit.distros.ubuntu.Distro') |
767 | def test_apply_network_on_datasource_allowed_event(self, m_ubuntu): |
768 | @@ -222,10 +225,28 @@ class TestInit(CiTestCase): |
769 | |
770 | self.init._find_networking_config = fake_network_config |
771 | self.init.datasource = FakeDataSource(paths=self.init.paths) |
772 | - self.init.datasource.update_events = {'network': [EventType.BOOT]} |
773 | + self.init.datasource.update_events = {'network': set([EventType.BOOT])} |
774 | self.init.apply_network_config(True) |
775 | self.init.distro.apply_network_config_names.assert_called_with(net_cfg) |
776 | self.init.distro.apply_network_config.assert_called_with( |
777 | net_cfg, bring_up=True) |
778 | |
779 | + def test_update_events_allowed_true_on_supported_event(self): |
780 | + """Update Event Allowed True if source_event supported""" |
781 | + self.assertTrue( |
782 | + self.init.update_event_allowed(EventType.BOOT_NEW_INSTANCE, |
783 | + scope='network')) |
784 | + |
785 | + def test_update_events_allowed_false_on_unsupported_event(self): |
786 | + """Update Event Allowed False if source_event supported""" |
787 | + self.assertFalse( |
788 | + self.init.update_event_allowed(EventType.BOOT, |
789 | + scope='network')) |
790 | + |
791 | + def test_update_events_allowed_false_on_unknown_scope(self): |
792 | + """Update Event Allowed False if source_event supported""" |
793 | + self.assertFalse( |
794 | + self.init.update_event_allowed(EventType.BOOT, scope='xkcd')) |
795 | + |
796 | + |
797 | # vi: ts=4 expandtab |
798 | diff --git a/cloudinit/version.py b/cloudinit/version.py |
799 | index 844a02e..1993983 100644 |
800 | --- a/cloudinit/version.py |
801 | +++ b/cloudinit/version.py |
802 | @@ -12,6 +12,8 @@ FEATURES = [ |
803 | 'NETWORK_CONFIG_V1', |
804 | # supports network config version 2 (netplan) |
805 | 'NETWORK_CONFIG_V2', |
806 | + # supports network config update via hotplug |
807 | + 'NETWORK_HOTPLUG', |
808 | ] |
809 | |
810 | |
811 | diff --git a/config/cloud.cfg.d/10_updates_policy.cfg b/config/cloud.cfg.d/10_updates_policy.cfg |
812 | new file mode 100644 |
813 | index 0000000..245a2d8 |
814 | --- /dev/null |
815 | +++ b/config/cloud.cfg.d/10_updates_policy.cfg |
816 | @@ -0,0 +1,6 @@ |
817 | +# default policy for cloud-init for when to update system config |
818 | +# such as network and storage configurations |
819 | +updates: |
820 | + policy-version: 1 |
821 | + network: |
822 | + when: ['boot-new-instance'] |
823 | diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst |
824 | index 20a99a3..23b848f 100644 |
825 | --- a/doc/rtd/index.rst |
826 | +++ b/doc/rtd/index.rst |
827 | @@ -36,6 +36,7 @@ initialization of a cloud instance. |
828 | topics/examples.rst |
829 | topics/boot.rst |
830 | topics/datasources.rst |
831 | + topics/events.rst |
832 | topics/logging.rst |
833 | topics/modules.rst |
834 | topics/merging.rst |
835 | diff --git a/doc/rtd/topics/capabilities.rst b/doc/rtd/topics/capabilities.rst |
836 | index 0d8b894..a40c998 100644 |
837 | --- a/doc/rtd/topics/capabilities.rst |
838 | +++ b/doc/rtd/topics/capabilities.rst |
839 | @@ -41,6 +41,8 @@ Currently defined feature names include: |
840 | see :ref:`network_config_v1` documentation for examples. |
841 | - ``NETWORK_CONFIG_V2`` support for v2 networking configuration, |
842 | see :ref:`network_config_v2` documentation for examples. |
843 | + - ``NETWORK_HOTPLUG`` support for configuring network on hotplug, |
844 | + see :ref:`events` documentation for more information. |
845 | |
846 | |
847 | CLI Interface |
848 | @@ -237,6 +239,10 @@ likely be promoted to top-level subcommands when stable. |
849 | containing the jinja template header ``## template: jinja`` and renders |
850 | that content with any instance-data.json variables present. |
851 | |
852 | + * ``cloud-init devel hotplug-hook``: Command called to handle when a new |
853 | + device has been added to the system dynamically. Typically called from |
854 | + a Linux udev rule which provides device specific values which are passed |
855 | + as command line options. |
856 | |
857 | .. _cli_clean: |
858 | |
859 | diff --git a/doc/rtd/topics/datasources/smartos.rst b/doc/rtd/topics/datasources/smartos.rst |
860 | index cb9a128..42841f6 100644 |
861 | --- a/doc/rtd/topics/datasources/smartos.rst |
862 | +++ b/doc/rtd/topics/datasources/smartos.rst |
863 | @@ -1,7 +1,7 @@ |
864 | .. _datasource_smartos: |
865 | |
866 | -SmartOS Datasource |
867 | -================== |
868 | +SmartOS |
869 | +======= |
870 | |
871 | This datasource finds metadata and user-data from the SmartOS virtualization |
872 | platform (i.e. Joyent). |
873 | diff --git a/doc/rtd/topics/events.rst b/doc/rtd/topics/events.rst |
874 | new file mode 100644 |
875 | index 0000000..db8f657 |
876 | --- /dev/null |
877 | +++ b/doc/rtd/topics/events.rst |
878 | @@ -0,0 +1,150 @@ |
879 | +****************** |
880 | +Events and Updates |
881 | +****************** |
882 | + |
883 | +- Events |
884 | +- Datasource Event Support |
885 | +- Configuring Event Updates |
886 | +- Examples |
887 | + |
888 | +.. _events: |
889 | + |
890 | +Events |
891 | +====== |
892 | + |
893 | +`Cloud-init`_ 's will fetch and apply cloud and user data configuration |
894 | +upon serveral event types. The two most common events for `Cloud-init`_ |
895 | +are when an instance first boots and any subsequent boot thereafter (reboot). |
896 | +In addition to boot events, `Cloud-init`_ users and vendors are interested |
897 | +in when devices are added. `Cloud-init`_ currently supports the following |
898 | +event types: |
899 | + |
900 | +- **BOOT_NEW_INSTANCE**: ``New instance first boot`` |
901 | +- **BOOT**: ``Any system boot other than 'BOOT_NEW_INSTANCE'`` |
902 | +- **HOTPLUG**: ``Dynamic add of a system device`` |
903 | + |
904 | +Future work will include infrastructure and support for the following |
905 | +events: |
906 | + |
907 | +- **METADATA_CHANGE**: ``An instance's metadata has change`` |
908 | +- **USER_REQUEST**: ``Directed request to update`` |
909 | + |
910 | +Datasource Event Support |
911 | +======================== |
912 | + |
913 | +All :ref:`datasources` by default support the ``BOOT_NEW_INSTANCE`` event. |
914 | +Each Datasource will provide a set of events that it is capable of handling. |
915 | +Datasources may not support all event types. In some cases a system |
916 | +may be configured to allow a particular event but may be running on |
917 | +a platform who's datasource cannot support the event. |
918 | + |
919 | +.. table:: |
920 | + :widths: auto |
921 | + |
922 | + +-----------------------------+------------------+ |
923 | + | Datasource | Supported Events | |
924 | + +=============================+==================+ |
925 | + | :ref:`datasource_azure` | BOOT | |
926 | + +-----------------------------+------------------+ |
927 | + | :ref:`datasource_openstack` | BOOT, HOTPLUG | |
928 | + +-----------------------------+------------------+ |
929 | + | :ref:`datasource_smartos` | BOOT | |
930 | + +-----------------------------+------------------+ |
931 | + |
932 | + |
933 | +Configuring Event Updates |
934 | +========================= |
935 | + |
936 | +`Cloud-init`_ has a default updates policy to handle new instance |
937 | +events always. Vendors may want an instance to handle additional |
938 | +events. Users have the final say and may provide update configuration |
939 | +which can be used to enable or disable handling of specific events. |
940 | + |
941 | +updates |
942 | +~~~~~~~ |
943 | +Specify update policy configuration for cloud-init to define which |
944 | +events are allowed to be handled. This is separate from whether a |
945 | +particular platform or datasource has the capability for such events. |
946 | + |
947 | +**policy-version**: *<Latest policy version, currently 1>* |
948 | + |
949 | +The ``policy-version`` value specifies the updates configuration |
950 | +version number. Current version is 1, future versions may modify |
951 | +the configuation structure. |
952 | + |
953 | +**scope**: *<name of the scope for event policy>* |
954 | + |
955 | +The ``scope`` value is a string which defines under which domain do the |
956 | +event occur. Currently there are two known scopes: ``network`` and |
957 | +``storage``. Scopes are defined by convention but arbitrary values |
958 | +can be used. |
959 | + |
960 | +**when**: *<list of events to handle for a particular scope>* |
961 | + |
962 | +Each ``scope`` requires a ``when`` element to specify which events |
963 | +are to allowed to be handled. |
964 | + |
965 | + |
966 | +Examples |
967 | +======== |
968 | + |
969 | +default |
970 | +~~~~~~~ |
971 | + |
972 | +The default policy for handling new instances is found in |
973 | +/etc/cloud/cloud.cfg.d/10_updates_policy.cfg |
974 | + |
975 | +.. code-block:: shell-session |
976 | + |
977 | + # default policy for cloud-init for when to update system config |
978 | + # such as network and storage configurations |
979 | + updates: |
980 | + policy-version: 1 |
981 | + network: |
982 | + when: ['boot-new-instance'] |
983 | + |
984 | +This default policy indicates that whenever cloud-init generates a |
985 | +``BOOT_NEW_INSTANCE`` event that the ``network`` scope will be updated. |
986 | +This results in cloud-init applying network configuration when booting |
987 | +a new instance. |
988 | + |
989 | +.. note:: |
990 | + Removing 'boot-new-instance' from the policy will cause issues when |
991 | + capturing images and booting them else where as the network config |
992 | + will remain static. |
993 | + |
994 | +apply network config every boot |
995 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
996 | +On each firsboot and every boot cloud-init will apply network configuration |
997 | +found in the datasource. |
998 | + |
999 | +.. code-block:: shell-session |
1000 | + |
1001 | + # apply network config on every boot |
1002 | + updates: |
1003 | + policy-version: 1 |
1004 | + network: |
1005 | + when: ['boot-new-instance', 'boot'] |
1006 | + |
1007 | +apply network config on hotplug |
1008 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1009 | +Apply network configuration from the datasource on first boot, each boot |
1010 | +thereafter and when new network devices are dynamically added. |
1011 | + |
1012 | + |
1013 | +.. code-block:: shell-session |
1014 | + |
1015 | + # apply network config on every boot and hotplug |
1016 | + updates: |
1017 | + policy-version: 1 |
1018 | + network: |
1019 | + when: ['boot-new-instance', 'boot', 'hotplug'] |
1020 | + |
1021 | +.. note:: |
1022 | + When enabling hotplug, it's best practice to also enable the boot event. |
1023 | + In the case of a device removal, the network configuration will be |
1024 | + reconfigure on the very next boot. |
1025 | + |
1026 | + |
1027 | +.. _Cloud-init: https://launchpad.net/cloud-init |
1028 | +.. vi: textwidth=78 |
1029 | diff --git a/setup.py b/setup.py |
1030 | index ea37efc..7b76bc4 100755 |
1031 | --- a/setup.py |
1032 | +++ b/setup.py |
1033 | @@ -138,6 +138,7 @@ INITSYS_FILES = { |
1034 | 'systemd': [render_tmpl(f) |
1035 | for f in (glob('systemd/*.tmpl') + |
1036 | glob('systemd/*.service') + |
1037 | + glob('systemd/*.socket') + |
1038 | glob('systemd/*.target')) if is_f(f)], |
1039 | 'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)], |
1040 | 'upstart': [f for f in glob('upstart/*') if is_f(f)], |
1041 | @@ -243,6 +244,7 @@ data_files = [ |
1042 | (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')), |
1043 | (ETC + '/cloud/templates', glob('templates/*')), |
1044 | (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify', |
1045 | + 'tools/hook-hotplug', |
1046 | 'tools/uncloud-init', |
1047 | 'tools/write-ssh-key-fingerprints']), |
1048 | (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]), |
1049 | diff --git a/systemd/cloud-init-hotplugd.service b/systemd/cloud-init-hotplugd.service |
1050 | new file mode 100644 |
1051 | index 0000000..6f231cd |
1052 | --- /dev/null |
1053 | +++ b/systemd/cloud-init-hotplugd.service |
1054 | @@ -0,0 +1,11 @@ |
1055 | +[Unit] |
1056 | +Description=cloud-init hotplug hook daemon |
1057 | +After=cloud-init-hotplugd.socket |
1058 | + |
1059 | +[Service] |
1060 | +Type=simple |
1061 | +ExecStart=/bin/bash -c 'read args <&3; echo "args=$args"; \ |
1062 | + exec /usr/bin/cloud-init devel hotplug-hook $args; \ |
1063 | + exit 0' |
1064 | +SyslogIdentifier=cloud-init-hotplugd |
1065 | +TimeoutStopSec=5 |
1066 | diff --git a/systemd/cloud-init-hotplugd.socket b/systemd/cloud-init-hotplugd.socket |
1067 | new file mode 100644 |
1068 | index 0000000..f8f1048 |
1069 | --- /dev/null |
1070 | +++ b/systemd/cloud-init-hotplugd.socket |
1071 | @@ -0,0 +1,8 @@ |
1072 | +[Unit] |
1073 | +Description=cloud-init hotplug hook socket |
1074 | + |
1075 | +[Socket] |
1076 | +ListenFIFO=/run/cloud-init/hook-hotplug-cmd |
1077 | + |
1078 | +[Install] |
1079 | +WantedBy=cloud-init.target |
1080 | diff --git a/tools/hook-hotplug b/tools/hook-hotplug |
1081 | new file mode 100755 |
1082 | index 0000000..469c45b |
1083 | --- /dev/null |
1084 | +++ b/tools/hook-hotplug |
1085 | @@ -0,0 +1,26 @@ |
1086 | +#!/bin/bash |
1087 | +# This file is part of cloud-init. See LICENSE file for license information. |
1088 | + |
1089 | +# This script checks if cloud-init has hotplug hooked and if |
1090 | +# cloud-init has finished; if so invoke cloud-init hotplug-hook |
1091 | + |
1092 | +is_finished() { |
1093 | + [ -e /run/cloud-init/result.json ] |
1094 | +} |
1095 | + |
1096 | +if is_finished; then |
1097 | + # only hook pci devices at this time |
1098 | + case "${DEVPATH}" in |
1099 | + /devices/pci*) |
1100 | + # open cloud-init's hotplug-hook fifo rw |
1101 | + exec 3<>/run/cloud-init/hook-hotplug-cmd |
1102 | + env_params=( |
1103 | + --devpath="${DEVPATH}" |
1104 | + --subsystem="${SUBSYSTEM}" |
1105 | + --udevaction="${ACTION}" |
1106 | + ) |
1107 | + # write params to cloud-init's hotplug-hook fifo |
1108 | + echo "--hotplug-debug ${env_params[@]}" >&3 |
1109 | + ;; |
1110 | + esac |
1111 | +fi |
1112 | diff --git a/udev/10-cloud-init-hook-hotplug.rules b/udev/10-cloud-init-hook-hotplug.rules |
1113 | new file mode 100644 |
1114 | index 0000000..74324f4 |
1115 | --- /dev/null |
1116 | +++ b/udev/10-cloud-init-hook-hotplug.rules |
1117 | @@ -0,0 +1,5 @@ |
1118 | +# Handle device adds only |
1119 | +ACTION!="add", GOTO="cloudinit_end" |
1120 | +LABEL="cloudinit_hook" |
1121 | +SUBSYSTEM=="net|block", RUN+="/usr/lib/cloud-init/hook-hotplug" |
1122 | +LABEL="cloudinit_end" |
i'm not all the way through, but here are some comments.