cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #05578
[Merge] ~raharper/cloud-init:feature/cloud-init-hotplug-handler into cloud-init:master
Scott Moser has proposed merging ~raharper/cloud-init:feature/cloud-init-hotplug-handler into cloud-init:master.
Commit message:
wip fixme
Requested reviews:
cloud-init commiters (cloud-init-dev): review-wip
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/356152
--
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:feature/cloud-init-hotplug-handler into cloud-init:master.
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()
COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word))
;;
devel)
- COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word))
+ COMPREPLY=($(compgen -W "--help hotplug-hook net-convert schema" -- $cur_word))
;;
dhclient-hook|features)
COMPREPLY=($(compgen -W "--help" -- $cur_word))
@@ -61,6 +61,9 @@ _cloudinit_complete()
--frequency)
COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word))
;;
+ hotplug-hook)
+ COMPREPLY=($(compgen -W "--help" -- $cur_word))
+ ;;
net-convert)
COMPREPLY=($(compgen -W "--help --network-data --kind --directory --output-kind" -- $cur_word))
;;
diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py
new file mode 100644
index 0000000..c24b1ff
--- /dev/null
+++ b/cloudinit/cmd/devel/hotplug_hook.py
@@ -0,0 +1,195 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Handle reconfiguration on hotplug events"""
+import argparse
+import os
+import sys
+
+from cloudinit.event import EventType
+from cloudinit import log
+from cloudinit import reporting
+from cloudinit.reporting import events
+from cloudinit import sources
+from cloudinit.stages import Init
+from cloudinit.net import read_sys_net_safe
+from cloudinit.net.network_state import parse_net_config_data
+
+
+LOG = log.getLogger(__name__)
+NAME = 'hotplug-hook'
+
+
+def get_parser(parser=None):
+ """Build or extend and arg parser for hotplug-hook utility.
+
+ @param parser: Optional existing ArgumentParser instance representing the
+ subcommand which will be extended to support the args of this utility.
+
+ @returns: ArgumentParser with proper argument configuration.
+ """
+ if not parser:
+ parser = argparse.ArgumentParser(prog=NAME, description=__doc__)
+
+ parser.add_argument("-d", "--devpath",
+ metavar="PATH",
+ help="sysfs path to hotplugged device")
+ parser.add_argument("--hotplug-debug", action='store_true',
+ help='enable debug logging to stderr.')
+ parser.add_argument("-s", "--subsystem",
+ choices=['net', 'block'])
+ parser.add_argument("-u", "--udevaction",
+ choices=['add', 'change', 'remove'])
+
+ return parser
+
+
+def log_console(msg):
+ """Log messages to stderr console and configured logging."""
+ sys.stderr.write(msg + '\n')
+ sys.stderr.flush()
+ LOG.debug(msg)
+
+
+def devpath_to_macaddr(devpath):
+ macaddr = read_sys_net_safe(os.path.basename(devpath), 'address')
+ log_console('Checking if %s in netconfig' % macaddr)
+ return macaddr
+
+
+def in_netconfig(unique_id, netconfig):
+ netstate = parse_net_config_data(netconfig)
+ found = [iface
+ for iface in netstate.iter_interfaces()
+ if iface.get('mac_address') == unique_id]
+ log_console('Ifaces with ID=%s : %s' % (unique_id, found))
+ return len(found) > 0
+
+
+class UeventHandler(object):
+ def __init__(self, ds, devpath, success_fn):
+ self.datasource = ds
+ self.devpath = devpath
+ self.success_fn = success_fn
+
+ def apply(self):
+ raise NotImplemented()
+
+ @property
+ def config(self):
+ raise NotImplemented()
+
+ def detect(self, action):
+ raise NotImplemented()
+
+ def success(self):
+ return self.success_fn()
+
+ def update(self):
+ self.datasource.update_metadata([EventType.UDEV])
+
+
+class NetHandler(UeventHandler):
+ def __init__(self, ds, devpath, success_fn):
+ super(NetHandler, self).__init__(ds, devpath, success_fn)
+ self.id = devpath_to_macaddr(self.devpath)
+
+ def apply(self):
+ return self.datasource.distro.apply_network_config(self.config,
+ bring_up=True)
+
+ @property
+ def config(self):
+ return self.datasource.network_config
+
+ def detect(self, action):
+ detect_presence = None
+ if action == 'add':
+ detect_presence = True
+ elif action == 'remove':
+ detect_presence = False
+ else:
+ raise ValueError('Cannot detect unknown action: %s' % action)
+
+ return detect_presence == in_netconfig(self.id, self.config)
+
+
+UEVENT_HANDLERS = {
+ 'net': NetHandler,
+}
+
+SUBSYSTEM_TO_EVENT = {
+ 'net': 'network',
+ 'block': 'storage',
+}
+
+
+def handle_args(name, args):
+ log_console('%s called with args=%s' % (NAME, args))
+ hotplug_reporter = events.ReportEventStack(NAME, __doc__,
+ reporting_enabled=True)
+ with hotplug_reporter:
+ # only handling net udev events for now
+ event_handler_cls = UEVENT_HANDLERS.get(args.subsystem)
+ if not event_handler_cls:
+ log_console('hotplug-hook: cannot handle events for subsystem: '
+ '"%s"' % args.subsystem)
+ return 1
+
+ log_console('Reading cloud-init configation')
+ hotplug_init = Init(ds_deps=[], reporter=hotplug_reporter)
+ hotplug_init.read_cfg()
+
+ log_console('Configuring logging')
+ log.setupLogging(hotplug_init.cfg)
+ if 'reporting' in hotplug_init.cfg:
+ reporting.update_configuration(hotplug_init.cfg.get('reporting'))
+
+ log_console('Fetching datasource')
+ try:
+ ds = hotplug_init.fetch(existing="trust")
+ except sources.DatasourceNotFoundException:
+ log_console('No Ds found')
+ return 1
+
+ subevent = SUBSYSTEM_TO_EVENT.get(args.subsystem)
+ if hotplug_init.update_event_allowed(EventType.UDEV, scope=subevent):
+ log_console('cloud-init not configured to handle udev events')
+ return
+
+ log_console('Creating %s event handler' % args.subsystem)
+ event_handler = event_handler_cls(ds, args.devpath,
+ hotplug_init._write_to_cache)
+ retries = [1, 1, 1, 3, 5]
+ for attempt, wait in enumerate(retries):
+ log_console('subsystem=%s update attempt %s/%s' % (args.subsystem,
+ attempt,
+ len(retries)))
+ try:
+ log_console('Refreshing metadata')
+ event_handler.update()
+ if event_handler.detect(action=args.udevaction):
+ log_console('Detected update, apply config change')
+ event_handler.apply()
+ log_console('Updating cache')
+ event_handler.success()
+ break
+ else:
+ raise Exception(
+ "Failed to detect device change in metadata")
+
+ except Exception as e:
+ if attempt + 1 >= len(retries):
+ raise
+ log_console('exception while processing hotplug event. %s' % e)
+
+ log_console('exiting handler')
+ reporting.flush_events()
+
+
+if __name__ == '__main__':
+ if 'TZ' not in os.environ:
+ os.environ['TZ'] = ":/etc/localtime"
+ args = get_parser().parse_args()
+ handle_args(NAME, args)
+
+# 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 @@
import argparse
from cloudinit.config import schema
-
+from . import hotplug_hook
from . import net_convert
from . import render
@@ -20,6 +20,8 @@ def get_parser(parser=None):
subparsers.required = True
subcmds = [
+ (hotplug_hook.NAME, hotplug_hook.__doc__,
+ hotplug_hook.get_parser, hotplug_hook.handle_args),
('schema', 'Validate cloud-config files for document schema',
schema.get_parser, schema.handle_schema_args),
(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):
self._paths = paths
self._cfg = cfg
self.name = name
+ self.net_renderer = None
@abc.abstractmethod
def install_packages(self, pkglist):
@@ -89,9 +90,8 @@ class Distro(object):
name, render_cls = renderers.select(priority=priority)
LOG.debug("Selected renderer '%s' from priority list: %s",
name, priority)
- renderer = render_cls(config=self.renderer_configs.get(name))
- renderer.render_network_config(network_config)
- return []
+ self.net_renderer = render_cls(config=self.renderer_configs.get(name))
+ return self.net_renderer.render_network_config(network_config)
def _find_tz_file(self, tz):
tz_file = os.path.join(self.tz_zone_dir, str(tz))
@@ -176,6 +176,7 @@ class Distro(object):
# a much less complete network config format (interfaces(5)).
try:
dev_names = self._write_network_config(netconfig)
+ LOG.debug('Network config found dev names: %s', dev_names)
except NotImplementedError:
# backwards compat until all distros have apply_network_config
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):
return self._supported_write_network_config(netconfig)
def _bring_up_interfaces(self, device_names):
- use_all = False
- for d in device_names:
- if d == 'all':
- use_all = True
- if use_all:
- return distros.Distro._bring_up_interface(self, '--all')
+ render_name = self.net_renderer.name
+ if render_name == 'eni':
+ LOG.debug('Bringing up interfaces with eni/ifup')
+ use_all = False
+ for d in device_names:
+ if d == 'all':
+ use_all = True
+ if use_all:
+ return distros.Distro._bring_up_interface(self, '--all')
+ else:
+ return distros.Distro._bring_up_interfaces(self, device_names)
+ elif render_name == 'netplan':
+ LOG.debug('Bringing up interfaces with netplan apply')
+ util.subp(['netplan', 'apply'])
else:
- return distros.Distro._bring_up_interfaces(self, device_names)
+ LOG.warning('Cannot bring up interfaces, unknown renderer: "%s"',
+ render_name)
def _write_hostname(self, your_hostname, out_fn):
conf = None
diff --git a/cloudinit/event.py b/cloudinit/event.py
index f7b311f..77ce631 100644
--- a/cloudinit/event.py
+++ b/cloudinit/event.py
@@ -2,16 +2,68 @@
"""Classes and functions related to event handling."""
+from cloudinit import log as logging
+from cloudinit import util
+
+
+LOG = logging.getLogger(__name__)
+
# Event types which can generate maintenance requests for cloud-init.
class EventType(object):
BOOT = "System boot"
BOOT_NEW_INSTANCE = "New instance first boot"
+ UDEV = "Udev add|change event on net|storage"
# TODO: Cloud-init will grow support for the follow event types:
- # UDEV
# METADATA_CHANGE
# USER_REQUEST
+EventTypeMap = {
+ 'boot': EventType.BOOT,
+ 'boot-new-instance': EventType.BOOT_NEW_INSTANCE,
+ 'udev': EventType.UDEV,
+}
+
+# inverted mapping
+EventNameMap = {v: k for k, v in EventTypeMap.items()}
+
+
+def get_allowed_events(sys_events, ds_events):
+ '''Merge datasource capabilties with system config to determine which
+ update events are allowed.'''
+
+ # updates:
+ # policy-version: 1
+ # network:
+ # when: [boot-new-instance, boot, udev]
+ # storage:
+ # when: [boot-new-instance, udev]
+ # watch: http://169.254.169.254/metadata/storage_config/
+
+ LOG.debug('updates: system cfg: %s', sys_events)
+ LOG.debug('updates: datasrc caps: %s', ds_events)
+
+ updates = util.mergemanydict([sys_events, ds_events])
+ LOG.debug('updates: merged cfg: %s', updates)
+
+ events = {}
+ for etype in ['network', 'storage']:
+ events[etype] = (
+ set([EventTypeMap.get(evt)
+ for evt in updates.get(etype, {}).get('when', [])
+ if evt in EventTypeMap]))
+
+ LOG.debug('updates: allowed events: %s', events)
+ return events
+
+
+def get_update_events_config(update_events):
+ '''Return a dictionary of updates config'''
+ evt_cfg = {'policy-version': 1}
+ for scope, events in update_events.items():
+ evt_cfg[scope] = {'when': [EventNameMap[evt] for evt in events]}
+
+ return evt_cfg
# 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):
class Renderer(renderer.Renderer):
"""Renders network information in a /etc/network/interfaces format."""
+ name = 'eni'
+
def __init__(self, config=None):
if not config:
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):
class Renderer(renderer.Renderer):
"""Renders network information in a /etc/netplan/network.yaml format."""
+ name = 'netplan'
+
NETPLAN_GENERATE = ['netplan', 'generate']
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):
driver=driver))
return content.getvalue()
+ @staticmethod
+ def get_interface_names(network_state):
+ return [cfg.get('name') for cfg in network_state.iter_interfaces()]
+
@abc.abstractmethod
def render_network_state(self, network_state, templates=None,
target=None):
@@ -51,8 +55,9 @@ class Renderer(object):
def render_network_config(self, network_config, templates=None,
target=None):
- return self.render_network_state(
- network_state=parse_net_config_data(network_config),
- templates=templates, target=target)
+ network_state = parse_net_config_data(network_config)
+ self.render_network_state(network_state=network_state,
+ templates=templates, target=target)
+ return self.get_interface_names(network_state)
# 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):
class Renderer(renderer.Renderer):
"""Renders network information in a /etc/sysconfig format."""
+ name = 'sysconfig'
+
# See: https://access.redhat.com/documentation/en-US/\
# Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
# s1-networkscripts-interfaces.html (or other docs for
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index 4a01524..5136dcb 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -7,7 +7,9 @@
import time
from cloudinit import log as logging
+from cloudinit.event import EventType
from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
+from cloudinit.net import is_up
from cloudinit import sources
from cloudinit import url_helper
from cloudinit import util
@@ -93,6 +95,15 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
return sources.instance_id_matches_system_uuid(self.get_instance_id())
@property
+ def update_events(self):
+ events = {'network': set([EventType.BOOT_NEW_INSTANCE,
+ EventType.BOOT,
+ EventType.UDEV]),
+ 'storage': set([])}
+ LOG.debug('OpenStack update events: %s', events)
+ return events
+
+ @property
def network_config(self):
"""Return a network config dict for rendering ENI or netplan files."""
if self._network_config != sources.UNSET:
@@ -122,11 +133,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
False when unable to contact metadata service or when metadata
format is invalid or disabled.
"""
- oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list')
+ oracle_considered = 'Oracle' in self.sys_cfg.get('datasource_list',
+ {})
if not detect_openstack(accept_oracle=not oracle_considered):
return False
- if self.perform_dhcp_setup: # Setup networking in init-local stage.
+ if self.perform_dhcp_setup and not is_up(self.fallback_interface):
+ # Setup networking in init-local stage.
try:
with EphemeralDHCPv4(self.fallback_interface):
results = util.log_time(
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index 5ac9882..71da091 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -141,15 +141,6 @@ class DataSource(object):
url_timeout = 10 # timeout for each metadata url read attempt
url_retries = 5 # number of times to retry url upon 404
- # The datasource defines a set of supported EventTypes during which
- # the datasource can react to changes in metadata and regenerate
- # network configuration on metadata changes.
- # A datasource which supports writing network config on each system boot
- # would call update_events['network'].add(EventType.BOOT).
-
- # Default: generate network config on new instance id (first boot).
- update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])}
-
# N-tuple listing default values for any metadata-related class
# attributes cached on an instance by a process_data runs. These attribute
# values are reset via clear_cached_attrs during any update_metadata call.
@@ -159,6 +150,7 @@ class DataSource(object):
('vendordata', None), ('vendordata_raw', None))
_dirty_cache = False
+ _update_events = {}
# N-tuple of keypaths or keynames redact from instance-data.json for
# non-root users
@@ -525,6 +517,24 @@ class DataSource(object):
def get_package_mirror_info(self):
return self.distro.get_package_mirror_info(data_source=self)
+ # The datasource defines a set of supported EventTypes during which
+ # the datasource can react to changes in metadata and regenerate
+ # network configuration on metadata changes.
+ # A datasource which supports writing network config on each system boot
+ # would call update_events['network'].add(EventType.BOOT).
+
+ # Default: generate network config on new instance id (first boot).
+ @property
+ def update_events(self):
+ if not self._update_events:
+ self._update_events = {'network':
+ set([EventType.BOOT_NEW_INSTANCE])}
+ return self._update_events
+
+ @update_events.setter
+ def update_events(self, events):
+ self._update_events.update(events)
+
def update_metadata(self, source_event_types):
"""Refresh cached metadata if the datasource supports this event.
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 8a06412..3dce084 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -22,9 +22,8 @@ from cloudinit.handlers.cloud_config import CloudConfigPartHandler
from cloudinit.handlers.jinja_template import JinjaTemplatePartHandler
from cloudinit.handlers.shell_script import ShellScriptPartHandler
from cloudinit.handlers.upstart_job import UpstartJobPartHandler
-
-from cloudinit.event import EventType
-
+from cloudinit.event import (
+ EventType, get_allowed_events, get_update_events_config)
from cloudinit import cloud
from cloudinit import config
from cloudinit import distros
@@ -644,7 +643,45 @@ class Init(object):
return (ncfg, loc)
return (self.distro.generate_fallback_config(), "fallback")
+ def update_event_allowed(self, event_source_type, scope=None):
+ # convert ds events to config
+ ds_config = get_update_events_config(self.datasource.update_events)
+ LOG.debug('Datasource updates cfg: %s', ds_config)
+
+ allowed = get_allowed_events(self.cfg.get('updates', {}), ds_config)
+ LOG.debug('Allowable update events: %s', allowed)
+
+ if not scope:
+ scopes = [allowed.keys()]
+ else:
+ scopes = [scope]
+ LOG.debug('Possible scopes for this event: %s', scopes)
+
+ for evt_scope in scopes:
+ if event_source_type in allowed[evt_scope]:
+ LOG.debug('Event Allowed: scope=%s EventType=%s',
+ evt_scope, event_source_type)
+ return True
+
+ LOG.debug('Event Denied: scopes=%s EventType=%s',
+ scopes, event_source_type)
+ return False
+
def apply_network_config(self, bring_up):
+ apply_network = True
+ if self.datasource is not NULL_DATA_SOURCE:
+ if not self.is_new_instance():
+ if self.update_event_allowed(EventType.BOOT, scope='network'):
+ if not self.datasource.update_metadata([EventType.BOOT]):
+ LOG.debug(
+ "No network config applied. Datasource failed"
+ " update metadata on '%s' event", EventType.BOOT)
+ apply_network = False
+ else:
+ LOG.debug("No network config applied. "
+ "'%s' event not allowed", EventType.BOOT)
+ apply_network = False
+
netcfg, src = self._find_networking_config()
if netcfg is None:
LOG.info("network config is disabled by %s", src)
@@ -656,14 +693,8 @@ class Init(object):
except Exception as e:
LOG.warning("Failed to rename devices: %s", e)
- if self.datasource is not NULL_DATA_SOURCE:
- if not self.is_new_instance():
- if not self.datasource.update_metadata([EventType.BOOT]):
- LOG.debug(
- "No network config applied. Neither a new instance"
- " nor datasource network update on '%s' event",
- EventType.BOOT)
- return
+ if not apply_network:
+ return
LOG.info("Applying network configuration from %s bringup=%s: %s",
src, bring_up, netcfg)
diff --git a/cloudinit/tests/test_stages.py b/cloudinit/tests/test_stages.py
index 94b6b25..6e2068a 100644
--- a/cloudinit/tests/test_stages.py
+++ b/cloudinit/tests/test_stages.py
@@ -47,6 +47,10 @@ class TestInit(CiTestCase):
'distro': 'ubuntu', 'paths': {'cloud_dir': self.tmpdir,
'run_dir': self.tmpdir}}}
self.init.datasource = FakeDataSource(paths=self.init.paths)
+ self.init.datasource.update_events = {
+ 'network': set([EventType.BOOT_NEW_INSTANCE])}
+ self.add_patch('cloudinit.stages.get_allowed_events', 'mock_allowed',
+ return_value=self.init.datasource.update_events)
def test_wb__find_networking_config_disabled(self):
"""find_networking_config returns no config when disabled."""
@@ -200,11 +204,10 @@ class TestInit(CiTestCase):
self.init._find_networking_config = fake_network_config
self.init.apply_network_config(True)
self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
+ self.assertIn("No network config applied. "
+ "'%s' event not allowed" % EventType.BOOT,
+ self.logs.getvalue())
self.init.distro.apply_network_config.assert_not_called()
- self.assertIn(
- 'No network config applied. Neither a new instance'
- " nor datasource network update on '%s' event" % EventType.BOOT,
- self.logs.getvalue())
@mock.patch('cloudinit.distros.ubuntu.Distro')
def test_apply_network_on_datasource_allowed_event(self, m_ubuntu):
@@ -222,7 +225,7 @@ class TestInit(CiTestCase):
self.init._find_networking_config = fake_network_config
self.init.datasource = FakeDataSource(paths=self.init.paths)
- self.init.datasource.update_events = {'network': [EventType.BOOT]}
+ self.init.datasource.update_events = {'network': set([EventType.BOOT])}
self.init.apply_network_config(True)
self.init.distro.apply_network_config_names.assert_called_with(net_cfg)
self.init.distro.apply_network_config.assert_called_with(
diff --git a/config/cloud.cfg.d/10_updates_policy.cfg b/config/cloud.cfg.d/10_updates_policy.cfg
new file mode 100644
index 0000000..245a2d8
--- /dev/null
+++ b/config/cloud.cfg.d/10_updates_policy.cfg
@@ -0,0 +1,6 @@
+# default policy for cloud-init for when to update system config
+# such as network and storage configurations
+updates:
+ policy-version: 1
+ network:
+ when: ['boot-new-instance']
diff --git a/setup.py b/setup.py
index 5ed8eae..5f3521e 100755
--- a/setup.py
+++ b/setup.py
@@ -138,6 +138,7 @@ INITSYS_FILES = {
'systemd': [render_tmpl(f)
for f in (glob('systemd/*.tmpl') +
glob('systemd/*.service') +
+ glob('systemd/*.socket') +
glob('systemd/*.target')) if is_f(f)],
'systemd.generators': [f for f in glob('systemd/*-generator') if is_f(f)],
'upstart': [f for f in glob('upstart/*') if is_f(f)],
@@ -243,6 +244,7 @@ data_files = [
(ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
(ETC + '/cloud/templates', glob('templates/*')),
(USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify',
+ 'tools/hook-hotplug',
'tools/uncloud-init',
'tools/write-ssh-key-fingerprints']),
(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
new file mode 100644
index 0000000..6f231cd
--- /dev/null
+++ b/systemd/cloud-init-hotplugd.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=cloud-init hotplug hook daemon
+After=cloud-init-hotplugd.socket
+
+[Service]
+Type=simple
+ExecStart=/bin/bash -c 'read args <&3; echo "args=$args"; \
+ exec /usr/bin/cloud-init devel hotplug-hook $args; \
+ exit 0'
+SyslogIdentifier=cloud-init-hotplugd
+TimeoutStopSec=5
diff --git a/systemd/cloud-init-hotplugd.socket b/systemd/cloud-init-hotplugd.socket
new file mode 100644
index 0000000..f8f1048
--- /dev/null
+++ b/systemd/cloud-init-hotplugd.socket
@@ -0,0 +1,8 @@
+[Unit]
+Description=cloud-init hotplug hook socket
+
+[Socket]
+ListenFIFO=/run/cloud-init/hook-hotplug-cmd
+
+[Install]
+WantedBy=cloud-init.target
diff --git a/tools/hook-hotplug b/tools/hook-hotplug
new file mode 100755
index 0000000..697d3ad
--- /dev/null
+++ b/tools/hook-hotplug
@@ -0,0 +1,26 @@
+#!/bin/bash
+# This file is part of cloud-init. See LICENSE file for license information.
+
+# This script checks if cloud-init has hotplug hooked and if
+# cloud-init has finished; if so invoke cloud-init hotplug-hook
+
+is_finished() {
+ [ -e /run/cloud-init/result.json ] || return 1
+}
+
+if is_finished; then
+ # only hook pci devices at this time
+ case ${DEVPATH} in
+ /devices/pci*)
+ # open cloud-init's hotplug-hook fifo rw
+ exec 3<>/run/cloud-init/hook-hotplug-cmd
+ env_params=( \
+ --devpath=${DEVPATH}
+ --subsystem=${SUBSYSTEM}
+ --udevaction=${ACTION}
+ )
+ # write params to cloud-init's hotplug-hook fifo
+ echo "--hotplug-debug ${env_params[@]}" >&3
+ ;;
+ esac
+fi
diff --git a/udev/10-cloud-init-hook-hotplug.rules b/udev/10-cloud-init-hook-hotplug.rules
new file mode 100644
index 0000000..74324f4
--- /dev/null
+++ b/udev/10-cloud-init-hook-hotplug.rules
@@ -0,0 +1,5 @@
+# Handle device adds only
+ACTION!="add", GOTO="cloudinit_end"
+LABEL="cloudinit_hook"
+SUBSYSTEM=="net|block", RUN+="/usr/lib/cloud-init/hook-hotplug"
+LABEL="cloudinit_end"
References