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"
_______________________________________________ Mailing list: https://launchpad.net/~cloud-init-dev Post to : cloud-init-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~cloud-init-dev More help : https://help.launchpad.net/ListHelp