Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 83 additions & 40 deletions cloudinit/cmd/devel/hotplug_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import abc
import argparse
import os
import sys
import time

from cloudinit import log
Expand All @@ -12,7 +13,7 @@
from cloudinit.net.network_state import parse_net_config_data
from cloudinit.reporting import events
from cloudinit.stages import Init
from cloudinit.sources import DataSource
from cloudinit.sources import DataSource, DataSourceNotFoundException


LOG = log.getLogger(__name__)
Expand All @@ -31,15 +32,35 @@ def get_parser(parser=None):
parser = argparse.ArgumentParser(prog=NAME, description=__doc__)

parser.description = __doc__
parser.add_argument("-d", "--devpath", required=True,
metavar="PATH",
help="sysfs path to hotplugged device")
parser.add_argument("-s", "--subsystem", required=True,
help="subsystem to act on",
choices=['net'])
parser.add_argument("-u", "--udevaction", required=True,
help="action to take",
choices=['add', 'remove'])
parser.add_argument(
"-s", "--subsystem", required=True,
help="subsystem to act on",
choices=['net']
)

subparsers = parser.add_subparsers(
title='Hotplug Action',
dest='hotplug_action'
)
subparsers.required = True

Comment thread
TheRealFalcon marked this conversation as resolved.
subparsers.add_parser(
'query',
help='query if hotplug is enabled for given subsystem'
)

parser_handle = subparsers.add_parser(
'handle', help='handle the hotplug event')
parser_handle.add_argument(
"-d", "--devpath", required=True,
metavar="PATH",
help="sysfs path to hotplugged device"
)
parser_handle.add_argument(
"-u", "--udevaction", required=True,
help="action to take",
choices=['add', 'remove']
)

return parser

Expand Down Expand Up @@ -133,27 +154,42 @@ def device_detected(self) -> bool:
}


def handle_hotplug(
hotplug_init: Init, devpath, subsystem, udevaction
):
handler_cls, event_scope = SUBSYSTEM_PROPERTES_MAP.get(
subsystem, (None, None)
)
if handler_cls is None:
def is_enabled(hotplug_init, subsystem):
try:
scope = SUBSYSTEM_PROPERTES_MAP[subsystem][1]
except KeyError as e:
raise Exception(
'hotplug-hook: cannot handle events for subsystem: {}'.format(
subsystem))
subsystem)
) from e

return hotplug_init.update_event_enabled(
event_source_type=EventType.HOTPLUG,
scope=scope
)


def initialize_datasource(hotplug_init, subsystem):
LOG.debug('Fetching datasource')
datasource = hotplug_init.fetch(existing="trust")

if not hotplug_init.update_event_enabled(
event_source_type=EventType.HOTPLUG,
scope=EventScope.NETWORK
):
LOG.debug('hotplug not enabled for event of type %s', event_scope)
if not datasource.get_supported_events([EventType.HOTPLUG]):
LOG.debug('hotplug not supported for event of type %s', subsystem)
return

if not is_enabled(hotplug_init, subsystem):
LOG.debug('hotplug not enabled for event of type %s', subsystem)
return
return datasource


def handle_hotplug(
hotplug_init: Init, devpath, subsystem, udevaction
):
datasource = initialize_datasource(hotplug_init, subsystem)
if not datasource:
return
handler_cls = SUBSYSTEM_PROPERTES_MAP[subsystem][0]
LOG.debug('Creating %s event handler', subsystem)
event_handler = handler_cls(
datasource=datasource,
Expand Down Expand Up @@ -200,29 +236,36 @@ def handle_args(name, args):
log.setupLogging(hotplug_init.cfg)
if 'reporting' in hotplug_init.cfg:
reporting.update_configuration(hotplug_init.cfg.get('reporting'))

# Logging isn't going to be setup until now
LOG.debug(
'%s called with the following arguments: {udevaction: %s, '
'subsystem: %s, devpath: %s}',
name, args.udevaction, args.subsystem, args.devpath
)
LOG.debug(
'%s called with the following arguments:\n'
'udevaction: %s\n'
'subsystem: %s\n'
'devpath: %s',
name, args.udevaction, args.subsystem, args.devpath
'%s called with the following arguments: {'
'hotplug_action: %s, subsystem: %s, udevaction: %s, devpath: %s}',
name,
args.hotplug_action,
args.subsystem,
args.udevaction if 'udevaction' in args else None,
args.devpath if 'devpath' in args else None,
)

with hotplug_reporter:
try:
handle_hotplug(
hotplug_init=hotplug_init,
devpath=args.devpath,
subsystem=args.subsystem,
udevaction=args.udevaction,
)
if args.hotplug_action == 'query':
try:
datasource = initialize_datasource(
hotplug_init, args.subsystem)
except DataSourceNotFoundException:
print(
"Unable to determine hotplug state. No datasource "
"detected")
sys.exit(1)
print('enabled' if datasource else 'disabled')
else:
handle_hotplug(
hotplug_init=hotplug_init,
devpath=args.devpath,
subsystem=args.subsystem,
udevaction=args.udevaction,
)
except Exception:
LOG.exception('Received fatal exception handling hotplug!')
raise
Expand Down
18 changes: 11 additions & 7 deletions cloudinit/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,16 @@ def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False):
def get_package_mirror_info(self):
return self.distro.get_package_mirror_info(data_source=self)

def get_supported_events(self, source_event_types: List[EventType]):
supported_events = {} # type: Dict[EventScope, set]
for event in source_event_types:
for update_scope, update_events in self.supported_update_events.items(): # noqa: E501
if event in update_events:
if not supported_events.get(update_scope):
supported_events[update_scope] = set()
supported_events[update_scope].add(event)
return supported_events

def update_metadata_if_supported(
self, source_event_types: List[EventType]
) -> bool:
Expand All @@ -694,13 +704,7 @@ def update_metadata_if_supported(
@return True if the datasource did successfully update cached metadata
due to source_event_type.
"""
supported_events = {} # type: Dict[EventScope, set]
for event in source_event_types:
for update_scope, update_events in self.supported_update_events.items(): # noqa: E501
if event in update_events:
if not supported_events.get(update_scope):
supported_events[update_scope] = set()
supported_events[update_scope].add(event)
supported_events = self.get_supported_events(source_event_types)
for scope, matched_events in supported_events.items():
LOG.debug(
"Update datasource metadata and %s config due to events: %s",
Expand Down
14 changes: 11 additions & 3 deletions tests/integration_tests/modules/test_hotplug.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_hotplug_add_remove(client: IntegrationInstance):

# Add new NIC
added_ip = client.instance.add_network_interface()
_wait_till_hotplug_complete(client)
_wait_till_hotplug_complete(client, expected_runs=2)
ips_after_add = _get_ip_addr(client)
new_addition = [ip for ip in ips_after_add if ip.ip4 == added_ip][0]

Expand All @@ -63,7 +63,7 @@ def test_hotplug_add_remove(client: IntegrationInstance):

# Remove new NIC
client.instance.remove_network_interface(added_ip)
_wait_till_hotplug_complete(client, expected_runs=2)
_wait_till_hotplug_complete(client, expected_runs=4)
ips_after_remove = _get_ip_addr(client)
assert len(ips_after_remove) == len(ips_before)
assert added_ip not in [ip.ip4 for ip in ips_after_remove]
Expand All @@ -72,6 +72,10 @@ def test_hotplug_add_remove(client: IntegrationInstance):
config = yaml.safe_load(netplan_cfg)
assert new_addition.interface not in config['network']['ethernets']

assert 'enabled' == client.execute(
'cloud-init devel hotplug-hook -s net query'
)


@pytest.mark.openstack
def test_no_hotplug_in_userdata(client: IntegrationInstance):
Expand All @@ -83,7 +87,7 @@ def test_no_hotplug_in_userdata(client: IntegrationInstance):
client.instance.add_network_interface()
_wait_till_hotplug_complete(client)
log = client.read_from_file('/var/log/cloud-init.log')
assert 'hotplug not enabled for event of type network' in log
assert "Event Denied: scopes=['network'] EventType=hotplug" in log

ips_after_add = _get_ip_addr(client)
if len(ips_after_add) == len(ips_before) + 1:
Expand All @@ -92,3 +96,7 @@ def test_no_hotplug_in_userdata(client: IntegrationInstance):
assert new_ip.state == 'DOWN'
else:
assert len(ips_after_add) == len(ips_before)

assert 'disabled' == client.execute(
'cloud-init devel hotplug-hook -s net query'
)
9 changes: 7 additions & 2 deletions tools/hook-hotplug
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ is_finished() {
[ -e /run/cloud-init/result.json ]
}

if is_finished; then
hotplug_enabled() {
[ "$(cloud-init devel hotplug-hook -s "${SUBSYSTEM}" query)" == "enabled" ]
}

if is_finished && hotplug_enabled; then
# open cloud-init's hotplug-hook fifo rw
exec 3<>/run/cloud-init/hook-hotplug-cmd
env_params=(
--devpath="${DEVPATH}"
--subsystem="${SUBSYSTEM}"
handle
--devpath="${DEVPATH}"
--udevaction="${ACTION}"
)
# write params to cloud-init's hotplug-hook fifo
Expand Down