Merge ~raharper/cloud-init:feature/cloud-init-hotplug-handler into cloud-init:master

Proposed by Scott Moser
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)
Reviewer Review Type Date Requested Status
cloud-init Commiters review-wip Pending
Review via email: mp+356152@code.launchpad.net

Commit message

wip fixme

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

i'm not all the way through, but here are some comments.

725b992... by Ryan Harper

Add unittests, remove hard coded scope, fix tox

abaa5b1... by Ryan Harper

Add tests for event module

Revision history for this message
Scott Moser (smoser) :
Revision history for this message
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

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
index 8c25032..0eab40c 100644
--- a/bash_completion/cloud-init
+++ b/bash_completion/cloud-init
@@ -28,7 +28,7 @@ _cloudinit_complete()
28 COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))28 COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))
29 ;;29 ;;
30 devel)30 devel)
31 COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word))31 COMPREPLY=($(compgen -W "--help hotplug-hook net-convert schema" -- $cur_word))
32 ;;32 ;;
33 dhclient-hook|features)33 dhclient-hook|features)
34 COMPREPLY=($(compgen -W "--help" -- $cur_word))34 COMPREPLY=($(compgen -W "--help" -- $cur_word))
@@ -61,6 +61,9 @@ _cloudinit_complete()
61 --frequency)61 --frequency)
62 COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))62 COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))
63 ;;63 ;;
64 hotplug-hook)
65 COMPREPLY=($(compgen -W "--help" -- $cur_word))
66 ;;
64 net-convert)67 net-convert)
65 COMPREPLY=($(compgen -W "--help --network-data --kind --directory --output-kind" -- $cur_word))68 COMPREPLY=($(compgen -W "--help --network-data --kind --directory --output-kind" -- $cur_word))
66 ;;69 ;;
diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py
67new file mode 10064470new file mode 100644
index 0000000..ae23d07
--- /dev/null
+++ b/cloudinit/cmd/devel/hotplug_hook.py
@@ -0,0 +1,210 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3"""Handle reconfiguration on hotplug events"""
4import abc
5import argparse
6import os
7import six
8import sys
9import time
10
11from cloudinit.event import EventType
12from cloudinit import log
13from cloudinit import reporting
14from cloudinit.reporting import events
15from cloudinit import sources
16from cloudinit.stages import Init
17from cloudinit.net import read_sys_net_safe
18from cloudinit.net.network_state import parse_net_config_data
19
20
21LOG = log.getLogger(__name__)
22NAME = 'hotplug-hook'
23
24
25def get_parser(parser=None):
26 """Build or extend and arg parser for hotplug-hook utility.
27
28 @param parser: Optional existing ArgumentParser instance representing the
29 subcommand which will be extended to support the args of this utility.
30
31 @returns: ArgumentParser with proper argument configuration.
32 """
33 if not parser:
34 parser = argparse.ArgumentParser(prog=NAME, description=__doc__)
35
36 parser.add_argument("-d", "--devpath",
37 metavar="PATH",
38 help="sysfs path to hotplugged device")
39 parser.add_argument("--hotplug-debug", action='store_true',
40 help='enable debug logging to stderr.')
41 parser.add_argument("-s", "--subsystem",
42 choices=['net', 'block'])
43 parser.add_argument("-u", "--udevaction",
44 choices=['add', 'change', 'remove'])
45
46 return parser
47
48
49def log_console(msg):
50 """Log messages to stderr console and configured logging."""
51 sys.stderr.write(msg + '\n')
52 sys.stderr.flush()
53 LOG.debug(msg)
54
55
56def devpath_to_macaddr(devpath):
57 macaddr = read_sys_net_safe(os.path.basename(devpath), 'address')
58 return macaddr
59
60
61def in_netconfig(unique_id, netconfig):
62 netstate = parse_net_config_data(netconfig)
63 found = [iface
64 for iface in netstate.iter_interfaces()
65 if iface.get('mac_address') == unique_id]
66 log_console('Ifaces with ID=%s : %s' % (unique_id, found))
67 return len(found) > 0
68
69
70@six.add_metaclass(abc.ABCMeta)
71class UeventHandler(object):
72 def __init__(self, ds, devpath, success_fn):
73 self.datasource = ds
74 self.devpath = devpath
75 self.success_fn = success_fn
76
77 @abc.abstractmethod
78 def apply(self):
79 raise NotImplementedError()
80
81 @property
82 @abc.abstractmethod
83 def config(self):
84 raise NotImplementedError()
85
86 @abc.abstractmethod
87 def detect(self, action):
88 raise NotImplementedError()
89
90 def success(self):
91 return self.success_fn()
92
93 def update(self):
94 result = self.datasource.update_metadata([EventType.HOTPLUG])
95 if not result:
96 log_console('Datasource %s not updated for '
97 ' event %s' % (self.datasource, EventType.HOTPLUG))
98 return result
99
100
101class NetHandler(UeventHandler):
102 def __init__(self, ds, devpath, success_fn):
103 super(NetHandler, self).__init__(ds, devpath, success_fn)
104 self.id = devpath_to_macaddr(self.devpath)
105
106 def apply(self):
107 return self.datasource.distro.apply_network_config(self.config,
108 bring_up=True)
109
110 @property
111 def config(self):
112 return self.datasource.network_config
113
114 def detect(self, action):
115 detect_presence = None
116 if action == 'add':
117 detect_presence = True
118 elif action == 'remove':
119 detect_presence = False
120 else:
121 raise ValueError('Cannot detect unknown action: %s' % action)
122
123 return detect_presence == in_netconfig(self.id, self.config)
124
125
126UEVENT_HANDLERS = {
127 'net': NetHandler,
128}
129
130SUBSYSTEM_TO_EVENT = {
131 'net': 'network',
132 'block': 'storage',
133}
134
135
136def handle_args(name, args):
137 log_console('%s called with args=%s' % (NAME, args))
138 hotplug_reporter = events.ReportEventStack(NAME, __doc__,
139 reporting_enabled=True)
140 with hotplug_reporter:
141 # only handling net udev events for now
142 event_handler_cls = UEVENT_HANDLERS.get(args.subsystem)
143 if not event_handler_cls:
144 log_console('hotplug-hook: cannot handle events for subsystem: '
145 '"%s"' % args.subsystem)
146 return 1
147
148 log_console('Reading cloud-init configation')
149 hotplug_init = Init(ds_deps=[], reporter=hotplug_reporter)
150 hotplug_init.read_cfg()
151
152 log_console('Configuring logging')
153 log.setupLogging(hotplug_init.cfg)
154 if 'reporting' in hotplug_init.cfg:
155 reporting.update_configuration(hotplug_init.cfg.get('reporting'))
156
157 log_console('Fetching datasource')
158 try:
159 ds = hotplug_init.fetch(existing="trust")
160 except sources.DataSourceNotFoundException:
161 log_console('No Ds found')
162 return 1
163
164 subevent = SUBSYSTEM_TO_EVENT.get(args.subsystem)
165 if not hotplug_init.update_event_allowed(EventType.HOTPLUG,
166 scope=subevent):
167 log_console('cloud-init not configured to handle hotplug event'
168 ' of type %s' % subevent)
169 return
170
171 log_console('Creating %s event handler' % args.subsystem)
172 event_handler = event_handler_cls(ds, args.devpath,
173 hotplug_init._write_to_cache)
174 retries = [1, 1, 1, 3, 5]
175 for attempt, wait in enumerate(retries):
176 log_console('subsystem=%s update attempt %s/%s' % (args.subsystem,
177 attempt,
178 len(retries)))
179 try:
180 log_console('Refreshing metadata')
181 result = event_handler.update()
182 if not result:
183 raise RuntimeError(
184 'Event handler %s update failed' % event_handler_cls)
185
186 if event_handler.detect(action=args.udevaction):
187 log_console('Detected update, apply config change')
188 event_handler.apply()
189 log_console('Updating cache')
190 event_handler.success()
191 break
192 else:
193 raise RuntimeError('Failed to detect %s in updated '
194 'metadata' % event_handler.id)
195
196 except Exception as e:
197 if attempt + 1 >= len(retries):
198 raise
199 log_console('Exception while processing hotplug event. %s' % e)
200 time.sleep(wait)
201
202 log_console('Exiting hotplug handler')
203 reporting.flush_events()
204
205
206if __name__ == '__main__':
207 args = get_parser().parse_args()
208 handle_args(NAME, args)
209
210# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/devel/parser.py b/cloudinit/cmd/devel/parser.py
index 99a234c..3ad09b3 100644
--- a/cloudinit/cmd/devel/parser.py
+++ b/cloudinit/cmd/devel/parser.py
@@ -6,7 +6,7 @@
66
7import argparse7import argparse
8from cloudinit.config import schema8from cloudinit.config import schema
99from . import hotplug_hook
10from . import net_convert10from . import net_convert
11from . import render11from . import render
1212
@@ -20,6 +20,8 @@ def get_parser(parser=None):
20 subparsers.required = True20 subparsers.required = True
2121
22 subcmds = [22 subcmds = [
23 (hotplug_hook.NAME, hotplug_hook.__doc__,
24 hotplug_hook.get_parser, hotplug_hook.handle_args),
23 ('schema', 'Validate cloud-config files for document schema',25 ('schema', 'Validate cloud-config files for document schema',
24 schema.get_parser, schema.handle_schema_args),26 schema.get_parser, schema.handle_schema_args),
25 (net_convert.NAME, net_convert.__doc__,27 (net_convert.NAME, net_convert.__doc__,
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ef618c2..92285f5 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -69,6 +69,7 @@ class Distro(object):
69 self._paths = paths69 self._paths = paths
70 self._cfg = cfg70 self._cfg = cfg
71 self.name = name71 self.name = name
72 self.net_renderer = None
7273
73 @abc.abstractmethod74 @abc.abstractmethod
74 def install_packages(self, pkglist):75 def install_packages(self, pkglist):
@@ -89,9 +90,8 @@ class Distro(object):
89 name, render_cls = renderers.select(priority=priority)90 name, render_cls = renderers.select(priority=priority)
90 LOG.debug("Selected renderer '%s' from priority list: %s",91 LOG.debug("Selected renderer '%s' from priority list: %s",
91 name, priority)92 name, priority)
92 renderer = render_cls(config=self.renderer_configs.get(name))93 self.net_renderer = render_cls(config=self.renderer_configs.get(name))
93 renderer.render_network_config(network_config)94 return self.net_renderer.render_network_config(network_config)
94 return []
9595
96 def _find_tz_file(self, tz):96 def _find_tz_file(self, tz):
97 tz_file = os.path.join(self.tz_zone_dir, str(tz))97 tz_file = os.path.join(self.tz_zone_dir, str(tz))
@@ -176,6 +176,7 @@ class Distro(object):
176 # a much less complete network config format (interfaces(5)).176 # a much less complete network config format (interfaces(5)).
177 try:177 try:
178 dev_names = self._write_network_config(netconfig)178 dev_names = self._write_network_config(netconfig)
179 LOG.debug('Network config found dev names: %s', dev_names)
179 except NotImplementedError:180 except NotImplementedError:
180 # backwards compat until all distros have apply_network_config181 # backwards compat until all distros have apply_network_config
181 return self._apply_network_from_network_config(182 return self._apply_network_from_network_config(
diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index d517fb8..4f1e6a9 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -114,14 +114,23 @@ class Distro(distros.Distro):
114 return self._supported_write_network_config(netconfig)114 return self._supported_write_network_config(netconfig)
115115
116 def _bring_up_interfaces(self, device_names):116 def _bring_up_interfaces(self, device_names):
117 use_all = False117 render_name = self.net_renderer.name
118 for d in device_names:118 if render_name == 'eni':
119 if d == 'all':119 LOG.debug('Bringing up interfaces with eni/ifup')
120 use_all = True120 use_all = False
121 if use_all:121 for d in device_names:
122 return distros.Distro._bring_up_interface(self, '--all')122 if d == 'all':
123 use_all = True
124 if use_all:
125 return distros.Distro._bring_up_interface(self, '--all')
126 else:
127 return distros.Distro._bring_up_interfaces(self, device_names)
128 elif render_name == 'netplan':
129 LOG.debug('Bringing up interfaces with netplan apply')
130 util.subp(['netplan', 'apply'])
123 else:131 else:
124 return distros.Distro._bring_up_interfaces(self, device_names)132 LOG.warning('Cannot bring up interfaces, unknown renderer: "%s"',
133 render_name)
125134
126 def _write_hostname(self, your_hostname, out_fn):135 def _write_hostname(self, your_hostname, out_fn):
127 conf = None136 conf = None
diff --git a/cloudinit/event.py b/cloudinit/event.py
index f7b311f..3a18ef9 100644
--- a/cloudinit/event.py
+++ b/cloudinit/event.py
@@ -2,16 +2,74 @@
22
3"""Classes and functions related to event handling."""3"""Classes and functions related to event handling."""
44
5import copy
6
7from cloudinit import log as logging
8from cloudinit import util
9
10
11LOG = logging.getLogger(__name__)
12
513
6# Event types which can generate maintenance requests for cloud-init.14# Event types which can generate maintenance requests for cloud-init.
7class EventType(object):15class EventType(object):
8 BOOT = "System boot"16 BOOT = "System boot"
9 BOOT_NEW_INSTANCE = "New instance first boot"17 BOOT_NEW_INSTANCE = "New instance first boot"
18 HOTPLUG = "Hotplug add event"
1019
11 # TODO: Cloud-init will grow support for the follow event types:20 # TODO: Cloud-init will grow support for the follow event types:
12 # UDEV
13 # METADATA_CHANGE21 # METADATA_CHANGE
14 # USER_REQUEST22 # USER_REQUEST
1523
1624
25EventTypeMap = {
26 'boot': EventType.BOOT,
27 'boot-new-instance': EventType.BOOT_NEW_INSTANCE,
28 'hotplug': EventType.HOTPLUG,
29}
30
31
32# inverted mapping
33EventNameMap = {v: k for k, v in EventTypeMap.items()}
34
35
36def get_allowed_events(sys_events, ds_events):
37 '''Merge datasource capabilties with system config to determine which
38 update events are allowed.'''
39
40 # updates:
41 # policy-version: 1
42 # network:
43 # when: [boot-new-instance, boot, hotplug]
44 # storage:
45 # when: [boot-new-instance, hotplug]
46 # watch: http://169.254.169.254/metadata/storage_config/
47
48 LOG.debug('updates: system cfg: %s', sys_events)
49 LOG.debug('updates: datasrc caps: %s', ds_events)
50
51 updates = util.mergemanydict([copy.deepcopy(sys_events),
52 copy.deepcopy(ds_events)])
53 LOG.debug('updates: merged cfg: %s', updates)
54
55 events = {}
56 for etype in [scope for scope, val in sys_events.items()
57 if type(val) == dict and 'when' in val]:
58 events[etype] = (
59 set([EventTypeMap.get(evt)
60 for evt in updates.get(etype, {}).get('when', [])
61 if evt in EventTypeMap]))
62
63 LOG.debug('updates: allowed events: %s', events)
64 return events
65
66
67def get_update_events_config(update_events):
68 '''Return a dictionary of updates config'''
69 evt_cfg = {'policy-version': 1}
70 for scope, events in update_events.items():
71 evt_cfg[scope] = {'when': [EventNameMap[evt] for evt in events]}
72
73 return evt_cfg
74
17# vi: ts=4 expandtab75# vi: ts=4 expandtab
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index c6f631a..3d8dcfb 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -338,6 +338,8 @@ def _ifaces_to_net_config_data(ifaces):
338class Renderer(renderer.Renderer):338class Renderer(renderer.Renderer):
339 """Renders network information in a /etc/network/interfaces format."""339 """Renders network information in a /etc/network/interfaces format."""
340340
341 name = 'eni'
342
341 def __init__(self, config=None):343 def __init__(self, config=None):
342 if not config:344 if not config:
343 config = {}345 config = {}
diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py
index bc1087f..08c9d05 100644
--- a/cloudinit/net/netplan.py
+++ b/cloudinit/net/netplan.py
@@ -178,6 +178,8 @@ def _clean_default(target=None):
178class Renderer(renderer.Renderer):178class Renderer(renderer.Renderer):
179 """Renders network information in a /etc/netplan/network.yaml format."""179 """Renders network information in a /etc/netplan/network.yaml format."""
180180
181 name = 'netplan'
182
181 NETPLAN_GENERATE = ['netplan', 'generate']183 NETPLAN_GENERATE = ['netplan', 'generate']
182184
183 def __init__(self, config=None):185 def __init__(self, config=None):
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index 5f32e90..88a1221 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -44,6 +44,10 @@ class Renderer(object):
44 driver=driver))44 driver=driver))
45 return content.getvalue()45 return content.getvalue()
4646
47 @staticmethod
48 def get_interface_names(network_state):
49 return [cfg.get('name') for cfg in network_state.iter_interfaces()]
50
47 @abc.abstractmethod51 @abc.abstractmethod
48 def render_network_state(self, network_state, templates=None,52 def render_network_state(self, network_state, templates=None,
49 target=None):53 target=None):
@@ -51,8 +55,9 @@ class Renderer(object):
5155
52 def render_network_config(self, network_config, templates=None,56 def render_network_config(self, network_config, templates=None,
53 target=None):57 target=None):
54 return self.render_network_state(58 network_state = parse_net_config_data(network_config)
55 network_state=parse_net_config_data(network_config),59 self.render_network_state(network_state=network_state,
56 templates=templates, target=target)60 templates=templates, target=target)
61 return self.get_interface_names(network_state)
5762
58# vi: ts=4 expandtab63# vi: ts=4 expandtab
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 9c16d3a..d502268 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -232,6 +232,8 @@ class NetInterface(ConfigMap):
232class Renderer(renderer.Renderer):232class Renderer(renderer.Renderer):
233 """Renders network information in a /etc/sysconfig format."""233 """Renders network information in a /etc/sysconfig format."""
234234
235 name = 'sysconfig'
236
235 # See: https://access.redhat.com/documentation/en-US/\237 # See: https://access.redhat.com/documentation/en-US/\
236 # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\238 # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
237 # s1-networkscripts-interfaces.html (or other docs for239 # s1-networkscripts-interfaces.html (or other docs for
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index 4a01524..b777c65 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -7,7 +7,9 @@
7import time7import time
88
9from cloudinit import log as logging9from cloudinit import log as logging
10from cloudinit.event import EventType
10from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError11from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
12from cloudinit.net import is_up
11from cloudinit import sources13from cloudinit import sources
12from cloudinit import url_helper14from cloudinit import url_helper
13from cloudinit import util15from cloudinit import util
@@ -93,6 +95,15 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
93 return sources.instance_id_matches_system_uuid(self.get_instance_id())95 return sources.instance_id_matches_system_uuid(self.get_instance_id())
9496
95 @property97 @property
98 def update_events(self):
99 events = {'network': set([EventType.BOOT_NEW_INSTANCE,
100 EventType.BOOT,
101 EventType.HOTPLUG]),
102 'storage': set([])}
103 LOG.debug('OpenStack update events: %s', events)
104 return events
105
106 @property
96 def network_config(self):107 def network_config(self):
97 """Return a network config dict for rendering ENI or netplan files."""108 """Return a network config dict for rendering ENI or netplan files."""
98 if self._network_config != sources.UNSET:109 if self._network_config != sources.UNSET:
@@ -122,11 +133,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
122 False when unable to contact metadata service or when metadata133 False when unable to contact metadata service or when metadata
123 format is invalid or disabled.134 format is invalid or disabled.
124 """135 """
125 oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list')136 oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list',
137 {})
126 if not detect_openstack(accept_oracle=not oracle_considered):138 if not detect_openstack(accept_oracle=not oracle_considered):
127 return False139 return False
128140
129 if self.perform_dhcp_setup: # Setup networking in init-local stage.141 if self.perform_dhcp_setup and not is_up(self.fallback_interface):
142 # Setup networking in init-local stage.
130 try:143 try:
131 with EphemeralDHCPv4(self.fallback_interface):144 with EphemeralDHCPv4(self.fallback_interface):
132 results = util.log_time(145 results = util.log_time(
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index e6966b3..00b88d4 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -158,15 +158,6 @@ class DataSource(object):
158 url_timeout = 10 # timeout for each metadata url read attempt158 url_timeout = 10 # timeout for each metadata url read attempt
159 url_retries = 5 # number of times to retry url upon 404159 url_retries = 5 # number of times to retry url upon 404
160160
161 # The datasource defines a set of supported EventTypes during which
162 # the datasource can react to changes in metadata and regenerate
163 # network configuration on metadata changes.
164 # A datasource which supports writing network config on each system boot
165 # would call update_events['network'].add(EventType.BOOT).
166
167 # Default: generate network config on new instance id (first boot).
168 update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])}
169
170 # N-tuple listing default values for any metadata-related class161 # N-tuple listing default values for any metadata-related class
171 # attributes cached on an instance by a process_data runs. These attribute162 # attributes cached on an instance by a process_data runs. These attribute
172 # values are reset via clear_cached_attrs during any update_metadata call.163 # values are reset via clear_cached_attrs during any update_metadata call.
@@ -176,6 +167,7 @@ class DataSource(object):
176 ('vendordata', None), ('vendordata_raw', None))167 ('vendordata', None), ('vendordata_raw', None))
177168
178 _dirty_cache = False169 _dirty_cache = False
170 _update_events = {}
179171
180 # N-tuple of keypaths or keynames redact from instance-data.json for172 # N-tuple of keypaths or keynames redact from instance-data.json for
181 # non-root users173 # non-root users
@@ -589,6 +581,24 @@ class DataSource(object):
589 def get_package_mirror_info(self):581 def get_package_mirror_info(self):
590 return self.distro.get_package_mirror_info(data_source=self)582 return self.distro.get_package_mirror_info(data_source=self)
591583
584 # The datasource defines a set of supported EventTypes during which
585 # the datasource can react to changes in metadata and regenerate
586 # network configuration on metadata changes.
587 # A datasource which supports writing network config on each system boot
588 # would call update_events['network'].add(EventType.BOOT).
589
590 # Default: generate network config on new instance id (first boot).
591 @property
592 def update_events(self):
593 if not self._update_events:
594 self._update_events = {'network':
595 set([EventType.BOOT_NEW_INSTANCE])}
596 return self._update_events
597
598 @update_events.setter
599 def update_events(self, events):
600 self._update_events.update(events)
601
592 def update_metadata(self, source_event_types):602 def update_metadata(self, source_event_types):
593 """Refresh cached metadata if the datasource supports this event.603 """Refresh cached metadata if the datasource supports this event.
594604
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 8a06412..90c5c76 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -22,9 +22,8 @@ from cloudinit.handlers.cloud_config import CloudConfigPartHandler
22from cloudinit.handlers.jinja_template import JinjaTemplatePartHandler22from cloudinit.handlers.jinja_template import JinjaTemplatePartHandler
23from cloudinit.handlers.shell_script import ShellScriptPartHandler23from cloudinit.handlers.shell_script import ShellScriptPartHandler
24from cloudinit.handlers.upstart_job import UpstartJobPartHandler24from cloudinit.handlers.upstart_job import UpstartJobPartHandler
2525from cloudinit.event import (
26from cloudinit.event import EventType26 EventType, get_allowed_events, get_update_events_config)
27
28from cloudinit import cloud27from cloudinit import cloud
29from cloudinit import config28from cloudinit import config
30from cloudinit import distros29from cloudinit import distros
@@ -644,7 +643,48 @@ class Init(object):
644 return (ncfg, loc)643 return (ncfg, loc)
645 return (self.distro.generate_fallback_config(), "fallback")644 return (self.distro.generate_fallback_config(), "fallback")
646645
646 def update_event_allowed(self, event_source_type, scope=None):
647 # convert ds events to config
648 ds_config = get_update_events_config(self.datasource.update_events)
649 LOG.debug('Datasource updates cfg: %s', ds_config)
650
651 sys_config = self.cfg.get('updates', {})
652 LOG.debug('System updates cfg: %s', sys_config)
653
654 allowed = get_allowed_events(sys_config, ds_config)
655 LOG.debug('Allowable update events: %s', allowed)
656
657 if not scope:
658 scopes = [allowed.keys()]
659 else:
660 scopes = [scope]
661 LOG.debug('Possible scopes for this event: %s', scopes)
662
663 for evt_scope in scopes:
664 if event_source_type in allowed.get(evt_scope, []):
665 LOG.debug('Event Allowed: scope=%s EventType=%s',
666 evt_scope, event_source_type)
667 return True
668
669 LOG.debug('Event Denied: scopes=%s EventType=%s',
670 scopes, event_source_type)
671 return False
672
647 def apply_network_config(self, bring_up):673 def apply_network_config(self, bring_up):
674 apply_network = True
675 if self.datasource is not NULL_DATA_SOURCE:
676 if not self.is_new_instance():
677 if self.update_event_allowed(EventType.BOOT, scope='network'):
678 if not self.datasource.update_metadata([EventType.BOOT]):
679 LOG.debug(
680 "No network config applied. Datasource failed"
681 " update metadata on '%s' event", EventType.BOOT)
682 apply_network = False
683 else:
684 LOG.debug("No network config applied. "
685 "'%s' event not allowed", EventType.BOOT)
686 apply_network = False
687
648 netcfg, src = self._find_networking_config()688 netcfg, src = self._find_networking_config()
649 if netcfg is None:689 if netcfg is None:
650 LOG.info("network config is disabled by %s", src)690 LOG.info("network config is disabled by %s", src)
@@ -656,14 +696,8 @@ class Init(object):
656 except Exception as e:696 except Exception as e:
657 LOG.warning("Failed to rename devices: %s", e)697 LOG.warning("Failed to rename devices: %s", e)
658698
659 if self.datasource is not NULL_DATA_SOURCE:699 if not apply_network:
660 if not self.is_new_instance():700 return
661 if not self.datasource.update_metadata([EventType.BOOT]):
662 LOG.debug(
663 "No network config applied. Neither a new instance"
664 " nor datasource network update on '%s' event",
665 EventType.BOOT)
666 return
667701
668 LOG.info("Applying network configuration from %s bringup=%s: %s",702 LOG.info("Applying network configuration from %s bringup=%s: %s",
669 src, bring_up, netcfg)703 src, bring_up, netcfg)
diff --git a/cloudinit/tests/test_event.py b/cloudinit/tests/test_event.py
670new file mode 100644704new file mode 100644
index 0000000..ad81deb
--- /dev/null
+++ b/cloudinit/tests/test_event.py
@@ -0,0 +1,75 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3"""Tests related to cloudinit.event module."""
4
5import copy
6import random
7import string
8
9from cloudinit.event import (EventType,
10 EventNameMap,
11 get_allowed_events,
12 get_update_events_config)
13
14from cloudinit.tests.helpers import CiTestCase
15
16
17def rand_event_names():
18 return [random.choice(list(EventNameMap.keys()))
19 for x in range(len(EventNameMap.keys()))]
20
21
22def rand_string(size=6, chars=string.ascii_lowercase):
23 return ''.join(random.choice(chars) for x in range(size))
24
25
26class TestEvent(CiTestCase):
27 with_logs = True
28
29 DEFAULT_UPDATE_EVENTS = {'network': set([EventType.BOOT_NEW_INSTANCE]),
30 'storage': set([])}
31 DEFAULT_UPDATES_CONFIG = {'policy-version': 1,
32 'network': {'when': ['boot-new-instance']},
33 'storage': {'when': []}}
34
35 def test_events_to_config(self):
36 """validate default update_events dictionary maps to default policy"""
37 events = copy.deepcopy(self.DEFAULT_UPDATE_EVENTS)
38 config = get_update_events_config(events)
39
40 for scope, events in events.items():
41 self.assertIn(scope, config)
42 for evt in events:
43 self.assertIn(evt, EventNameMap)
44 self.assertIn(EventNameMap.get(evt),
45 config.get(scope).get('when'))
46
47 self.assertEqual(sorted(config),
48 sorted(self.DEFAULT_UPDATES_CONFIG))
49
50 def test_get_allowed_events_defaults_filter_datasource(self):
51 ds_config = {
52 'policy-version': 1,
53 'network': {'when': rand_event_names()},
54 'storage': {'when': rand_event_names()},
55 }
56 allowed = get_allowed_events(self.DEFAULT_UPDATES_CONFIG, ds_config)
57
58 # system config filters out ds capabilities
59 self.assertEqual(sorted(allowed), sorted(self.DEFAULT_UPDATE_EVENTS))
60
61 def test_get_allowed_events_uses_system_config_scopes(self):
62 ds_config = {
63 'policy-version': 1,
64 'network': {'when': rand_event_names()},
65 'storage': {'when': rand_event_names()},
66 }
67 rand_scope = rand_string()
68 rand_events = rand_event_names()
69 sys_config = {'policy-version': 1, rand_scope: {'when': rand_events}}
70
71 self.assertNotIn(rand_scope, ds_config)
72 allowed = get_allowed_events(sys_config, ds_config)
73 self.assertIn(rand_scope, allowed)
74
75# vi: ts=4 expandtab
diff --git a/cloudinit/tests/test_stages.py b/cloudinit/tests/test_stages.py
index 94b6b25..037ca2f 100644
--- a/cloudinit/tests/test_stages.py
+++ b/cloudinit/tests/test_stages.py
@@ -47,6 +47,10 @@ class TestInit(CiTestCase):
47 'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir,47 'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir,
48 'run_dir': self.tmpdir}}}48 'run_dir': self.tmpdir}}}
49 self.init.datasource = FakeDataSource(paths=self.init.paths)49 self.init.datasource = FakeDataSource(paths=self.init.paths)
50 self.init.datasource.update_events = {
51 'network': set([EventType.BOOT_NEW_INSTANCE])}
52 self.add_patch('cloudinit.stages.get_allowed_events', 'mock_allowed',
53 return_value=self.init.datasource.update_events)
5054
51 def test_wb__find_networking_config_disabled(self):55 def test_wb__find_networking_config_disabled(self):
52 """find_networking_config returns no config when disabled."""56 """find_networking_config returns no config when disabled."""
@@ -200,11 +204,10 @@ class TestInit(CiTestCase):
200 self.init._find_networking_config = fake_network_config204 self.init._find_networking_config = fake_network_config
201 self.init.apply_network_config(True)205 self.init.apply_network_config(True)
202 self.init.distro.apply_network_config_names.assert_called_with(net_cfg)206 self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
207 self.assertIn("No network config applied. "
208 "'%s' event not allowed" % EventType.BOOT,
209 self.logs.getvalue())
203 self.init.distro.apply_network_config.assert_not_called()210 self.init.distro.apply_network_config.assert_not_called()
204 self.assertIn(
205 'No network config applied. Neither a new instance'
206 " nor datasource network update on '%s' event" % EventType.BOOT,
207 self.logs.getvalue())
208211
209 @mock.patch('cloudinit.distros.ubuntu.Distro')212 @mock.patch('cloudinit.distros.ubuntu.Distro')
210 def test_apply_network_on_datasource_allowed_event(self, m_ubuntu):213 def test_apply_network_on_datasource_allowed_event(self, m_ubuntu):
@@ -222,10 +225,28 @@ class TestInit(CiTestCase):
222225
223 self.init._find_networking_config = fake_network_config226 self.init._find_networking_config = fake_network_config
224 self.init.datasource = FakeDataSource(paths=self.init.paths)227 self.init.datasource = FakeDataSource(paths=self.init.paths)
225 self.init.datasource.update_events = {'network': [EventType.BOOT]}228 self.init.datasource.update_events = {'network': set([EventType.BOOT])}
226 self.init.apply_network_config(True)229 self.init.apply_network_config(True)
227 self.init.distro.apply_network_config_names.assert_called_with(net_cfg)230 self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
228 self.init.distro.apply_network_config.assert_called_with(231 self.init.distro.apply_network_config.assert_called_with(
229 net_cfg, bring_up=True)232 net_cfg, bring_up=True)
230233
234 def test_update_events_allowed_true_on_supported_event(self):
235 """Update Event Allowed True if source_event supported"""
236 self.assertTrue(
237 self.init.update_event_allowed(EventType.BOOT_NEW_INSTANCE,
238 scope='network'))
239
240 def test_update_events_allowed_false_on_unsupported_event(self):
241 """Update Event Allowed False if source_event supported"""
242 self.assertFalse(
243 self.init.update_event_allowed(EventType.BOOT,
244 scope='network'))
245
246 def test_update_events_allowed_false_on_unknown_scope(self):
247 """Update Event Allowed False if source_event supported"""
248 self.assertFalse(
249 self.init.update_event_allowed(EventType.BOOT, scope='xkcd'))
250
251
231# vi: ts=4 expandtab252# vi: ts=4 expandtab
diff --git a/cloudinit/version.py b/cloudinit/version.py
index 844a02e..1993983 100644
--- a/cloudinit/version.py
+++ b/cloudinit/version.py
@@ -12,6 +12,8 @@ FEATURES = [
12 'NETWORK_CONFIG_V1',12 'NETWORK_CONFIG_V1',
13 # supports network config version 2 (netplan)13 # supports network config version 2 (netplan)
14 'NETWORK_CONFIG_V2',14 'NETWORK_CONFIG_V2',
15 # supports network config update via hotplug
16 'NETWORK_HOTPLUG',
15]17]
1618
1719
diff --git a/config/cloud.cfg.d/10_updates_policy.cfg b/config/cloud.cfg.d/10_updates_policy.cfg
18new file mode 10064420new file mode 100644
index 0000000..245a2d8
--- /dev/null
+++ b/config/cloud.cfg.d/10_updates_policy.cfg
@@ -0,0 +1,6 @@
1# default policy for cloud-init for when to update system config
2# such as network and storage configurations
3updates:
4 policy-version: 1
5 network:
6 when: ['boot-new-instance']
diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst
index 20a99a3..23b848f 100644
--- a/doc/rtd/index.rst
+++ b/doc/rtd/index.rst
@@ -36,6 +36,7 @@ initialization of a cloud instance.
36 topics/examples.rst36 topics/examples.rst
37 topics/boot.rst37 topics/boot.rst
38 topics/datasources.rst38 topics/datasources.rst
39 topics/events.rst
39 topics/logging.rst40 topics/logging.rst
40 topics/modules.rst41 topics/modules.rst
41 topics/merging.rst42 topics/merging.rst
diff --git a/doc/rtd/topics/capabilities.rst b/doc/rtd/topics/capabilities.rst
index 0d8b894..a40c998 100644
--- a/doc/rtd/topics/capabilities.rst
+++ b/doc/rtd/topics/capabilities.rst
@@ -41,6 +41,8 @@ Currently defined feature names include:
41 see :ref:`network_config_v1` documentation for examples.41 see :ref:`network_config_v1` documentation for examples.
42 - ``NETWORK_CONFIG_V2`` support for v2 networking configuration,42 - ``NETWORK_CONFIG_V2`` support for v2 networking configuration,
43 see :ref:`network_config_v2` documentation for examples.43 see :ref:`network_config_v2` documentation for examples.
44 - ``NETWORK_HOTPLUG`` support for configuring network on hotplug,
45 see :ref:`events` documentation for more information.
4446
4547
46CLI Interface48CLI Interface
@@ -237,6 +239,10 @@ likely be promoted to top-level subcommands when stable.
237 containing the jinja template header ``## template: jinja`` and renders239 containing the jinja template header ``## template: jinja`` and renders
238 that content with any instance-data.json variables present.240 that content with any instance-data.json variables present.
239241
242 * ``cloud-init devel hotplug-hook``: Command called to handle when a new
243 device has been added to the system dynamically. Typically called from
244 a Linux udev rule which provides device specific values which are passed
245 as command line options.
240246
241.. _cli_clean:247.. _cli_clean:
242248
diff --git a/doc/rtd/topics/datasources/smartos.rst b/doc/rtd/topics/datasources/smartos.rst
index cb9a128..42841f6 100644
--- a/doc/rtd/topics/datasources/smartos.rst
+++ b/doc/rtd/topics/datasources/smartos.rst
@@ -1,7 +1,7 @@
1.. _datasource_smartos:1.. _datasource_smartos:
22
3SmartOS Datasource3SmartOS
4==================4=======
55
6This datasource finds metadata and user-data from the SmartOS virtualization6This datasource finds metadata and user-data from the SmartOS virtualization
7platform (i.e. Joyent).7platform (i.e. Joyent).
diff --git a/doc/rtd/topics/events.rst b/doc/rtd/topics/events.rst
8new file mode 1006448new file mode 100644
index 0000000..db8f657
--- /dev/null
+++ b/doc/rtd/topics/events.rst
@@ -0,0 +1,150 @@
1******************
2Events and Updates
3******************
4
5- Events
6- Datasource Event Support
7- Configuring Event Updates
8- Examples
9
10.. _events:
11
12Events
13======
14
15`Cloud-init`_ 's will fetch and apply cloud and user data configuration
16upon serveral event types. The two most common events for `Cloud-init`_
17are when an instance first boots and any subsequent boot thereafter (reboot).
18In addition to boot events, `Cloud-init`_ users and vendors are interested
19in when devices are added. `Cloud-init`_ currently supports the following
20event types:
21
22- **BOOT_NEW_INSTANCE**: ``New instance first boot``
23- **BOOT**: ``Any system boot other than 'BOOT_NEW_INSTANCE'``
24- **HOTPLUG**: ``Dynamic add of a system device``
25
26Future work will include infrastructure and support for the following
27events:
28
29- **METADATA_CHANGE**: ``An instance's metadata has change``
30- **USER_REQUEST**: ``Directed request to update``
31
32Datasource Event Support
33========================
34
35All :ref:`datasources` by default support the ``BOOT_NEW_INSTANCE`` event.
36Each Datasource will provide a set of events that it is capable of handling.
37Datasources may not support all event types. In some cases a system
38may be configured to allow a particular event but may be running on
39a platform who's datasource cannot support the event.
40
41.. table::
42 :widths: auto
43
44 +-----------------------------+------------------+
45 | Datasource | Supported Events |
46 +=============================+==================+
47 | :ref:`datasource_azure` | BOOT |
48 +-----------------------------+------------------+
49 | :ref:`datasource_openstack` | BOOT, HOTPLUG |
50 +-----------------------------+------------------+
51 | :ref:`datasource_smartos` | BOOT |
52 +-----------------------------+------------------+
53
54
55Configuring Event Updates
56=========================
57
58`Cloud-init`_ has a default updates policy to handle new instance
59events always. Vendors may want an instance to handle additional
60events. Users have the final say and may provide update configuration
61which can be used to enable or disable handling of specific events.
62
63updates
64~~~~~~~
65Specify update policy configuration for cloud-init to define which
66events are allowed to be handled. This is separate from whether a
67particular platform or datasource has the capability for such events.
68
69**policy-version**: *<Latest policy version, currently 1>*
70
71The ``policy-version`` value specifies the updates configuration
72version number. Current version is 1, future versions may modify
73the configuation structure.
74
75**scope**: *<name of the scope for event policy>*
76
77The ``scope`` value is a string which defines under which domain do the
78event occur. Currently there are two known scopes: ``network`` and
79``storage``. Scopes are defined by convention but arbitrary values
80can be used.
81
82**when**: *<list of events to handle for a particular scope>*
83
84Each ``scope`` requires a ``when`` element to specify which events
85are to allowed to be handled.
86
87
88Examples
89========
90
91default
92~~~~~~~
93
94The default policy for handling new instances is found in
95/etc/cloud/cloud.cfg.d/10_updates_policy.cfg
96
97.. code-block:: shell-session
98
99 # default policy for cloud-init for when to update system config
100 # such as network and storage configurations
101 updates:
102 policy-version: 1
103 network:
104 when: ['boot-new-instance']
105
106This default policy indicates that whenever cloud-init generates a
107``BOOT_NEW_INSTANCE`` event that the ``network`` scope will be updated.
108This results in cloud-init applying network configuration when booting
109a new instance.
110
111.. note::
112 Removing 'boot-new-instance' from the policy will cause issues when
113 capturing images and booting them else where as the network config
114 will remain static.
115
116apply network config every boot
117~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118On each firsboot and every boot cloud-init will apply network configuration
119found in the datasource.
120
121.. code-block:: shell-session
122
123 # apply network config on every boot
124 updates:
125 policy-version: 1
126 network:
127 when: ['boot-new-instance', 'boot']
128
129apply network config on hotplug
130~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
131Apply network configuration from the datasource on first boot, each boot
132thereafter and when new network devices are dynamically added.
133
134
135.. code-block:: shell-session
136
137 # apply network config on every boot and hotplug
138 updates:
139 policy-version: 1
140 network:
141 when: ['boot-new-instance', 'boot', 'hotplug']
142
143.. note::
144 When enabling hotplug, it's best practice to also enable the boot event.
145 In the case of a device removal, the network configuration will be
146 reconfigure on the very next boot.
147
148
149.. _Cloud-init: https://launchpad.net/cloud-init
150.. vi: textwidth=78
diff --git a/setup.py b/setup.py
index ea37efc..7b76bc4 100755
--- a/setup.py
+++ b/setup.py
@@ -138,6 +138,7 @@ INITSYS_FILES = {
138 'systemd': [render_tmpl(f)138 'systemd': [render_tmpl(f)
139 for f in (glob('systemd/*.tmpl') +139 for f in (glob('systemd/*.tmpl') +
140 glob('systemd/*.service') +140 glob('systemd/*.service') +
141 glob('systemd/*.socket') +
141 glob('systemd/*.target')) if is_f(f)],142 glob('systemd/*.target')) if is_f(f)],
142 'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)],143 'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)],
143 'upstart': [f for f in glob('upstart/*') if is_f(f)],144 'upstart': [f for f in glob('upstart/*') if is_f(f)],
@@ -243,6 +244,7 @@ data_files = [
243 (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),244 (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
244 (ETC + '/cloud/templates', glob('templates/*')),245 (ETC + '/cloud/templates', glob('templates/*')),
245 (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify',246 (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify',
247 'tools/hook-hotplug',
246 'tools/uncloud-init',248 'tools/uncloud-init',
247 'tools/write-ssh-key-fingerprints']),249 'tools/write-ssh-key-fingerprints']),
248 (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]),250 (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]),
diff --git a/systemd/cloud-init-hotplugd.service b/systemd/cloud-init-hotplugd.service
249new file mode 100644251new file mode 100644
index 0000000..6f231cd
--- /dev/null
+++ b/systemd/cloud-init-hotplugd.service
@@ -0,0 +1,11 @@
1[Unit]
2Description=cloud-init hotplug hook daemon
3After=cloud-init-hotplugd.socket
4
5[Service]
6Type=simple
7ExecStart=/bin/bash -c 'read args <&3; echo "args=$args"; \
8 exec /usr/bin/cloud-init devel hotplug-hook $args; \
9 exit 0'
10SyslogIdentifier=cloud-init-hotplugd
11TimeoutStopSec=5
diff --git a/systemd/cloud-init-hotplugd.socket b/systemd/cloud-init-hotplugd.socket
0new file mode 10064412new file mode 100644
index 0000000..f8f1048
--- /dev/null
+++ b/systemd/cloud-init-hotplugd.socket
@@ -0,0 +1,8 @@
1[Unit]
2Description=cloud-init hotplug hook socket
3
4[Socket]
5ListenFIFO=/run/cloud-init/hook-hotplug-cmd
6
7[Install]
8WantedBy=cloud-init.target
diff --git a/tools/hook-hotplug b/tools/hook-hotplug
0new file mode 1007559new file mode 100755
index 0000000..469c45b
--- /dev/null
+++ b/tools/hook-hotplug
@@ -0,0 +1,26 @@
1#!/bin/bash
2# This file is part of cloud-init. See LICENSE file for license information.
3
4# This script checks if cloud-init has hotplug hooked and if
5# cloud-init has finished; if so invoke cloud-init hotplug-hook
6
7is_finished() {
8 [ -e /run/cloud-init/result.json ]
9}
10
11if is_finished; then
12 # only hook pci devices at this time
13 case "${DEVPATH}" in
14 /devices/pci*)
15 # open cloud-init's hotplug-hook fifo rw
16 exec 3<>/run/cloud-init/hook-hotplug-cmd
17 env_params=(
18 --devpath="${DEVPATH}"
19 --subsystem="${SUBSYSTEM}"
20 --udevaction="${ACTION}"
21 )
22 # write params to cloud-init's hotplug-hook fifo
23 echo "--hotplug-debug ${env_params[@]}" >&3
24 ;;
25 esac
26fi
diff --git a/udev/10-cloud-init-hook-hotplug.rules b/udev/10-cloud-init-hook-hotplug.rules
0new file mode 10064427new file mode 100644
index 0000000..74324f4
--- /dev/null
+++ b/udev/10-cloud-init-hook-hotplug.rules
@@ -0,0 +1,5 @@
1# Handle device adds only
2ACTION!="add", GOTO="cloudinit_end"
3LABEL="cloudinit_hook"
4SUBSYSTEM=="net|block", RUN+="/usr/lib/cloud-init/hook-hotplug"
5LABEL="cloudinit_end"

Subscribers

People subscribed via source and target branches