diff options
| author | zsdc <taras@vyos.io> | 2022-03-25 20:58:01 +0200 | 
|---|---|---|
| committer | zsdc <taras@vyos.io> | 2022-03-25 21:42:00 +0200 | 
| commit | 31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba (patch) | |
| tree | 349631a02467dae0158f6f663cc8aa8537974a97 /cloudinit/cmd/devel/hotplug_hook.py | |
| parent | 5c4b3943343a85fbe517e5ec1fc670b3a8566b4b (diff) | |
| parent | 8537237d80a48c8f0cbf8e66aa4826bbc882b022 (diff) | |
| download | vyos-cloud-init-31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba.tar.gz vyos-cloud-init-31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba.zip  | |
T2117: Cloud-init updated to 22.1
Merged with 22.1 tag from the upstream Cloud-init repository.
Our modules were slightly modified for compatibility with the new
version.
Diffstat (limited to 'cloudinit/cmd/devel/hotplug_hook.py')
| -rw-r--r-- | cloudinit/cmd/devel/hotplug_hook.py | 291 | 
1 files changed, 291 insertions, 0 deletions
diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py new file mode 100644 index 00000000..a9be0379 --- /dev/null +++ b/cloudinit/cmd/devel/hotplug_hook.py @@ -0,0 +1,291 @@ +# This file is part of cloud-init. See LICENSE file for license information. +"""Handle reconfiguration on hotplug events""" +import abc +import argparse +import os +import sys +import time + +from cloudinit import log, reporting, stages +from cloudinit.event import EventScope, EventType +from cloudinit.net import activators, read_sys_net_safe +from cloudinit.net.network_state import parse_net_config_data +from cloudinit.reporting import events +from cloudinit.sources import DataSource  # noqa: F401 +from cloudinit.sources import DataSourceNotFoundException +from cloudinit.stages import Init + +LOG = log.getLogger(__name__) +NAME = "hotplug-hook" + + +def get_parser(parser=None): +    """Build or extend an 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.description = __doc__ +    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 + +    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 + + +class UeventHandler(abc.ABC): +    def __init__(self, id, datasource, devpath, action, success_fn): +        self.id = id +        self.datasource = datasource  # type: DataSource +        self.devpath = devpath +        self.action = action +        self.success_fn = success_fn + +    @abc.abstractmethod +    def apply(self): +        raise NotImplementedError() + +    @property +    @abc.abstractmethod +    def config(self): +        raise NotImplementedError() + +    @abc.abstractmethod +    def device_detected(self) -> bool: +        raise NotImplementedError() + +    def detect_hotplugged_device(self): +        detect_presence = None +        if self.action == "add": +            detect_presence = True +        elif self.action == "remove": +            detect_presence = False +        else: +            raise ValueError("Unknown action: %s" % self.action) + +        if detect_presence != self.device_detected(): +            raise RuntimeError( +                "Failed to detect %s in updated metadata" % self.id +            ) + +    def success(self): +        return self.success_fn() + +    def update_metadata(self): +        result = self.datasource.update_metadata_if_supported( +            [EventType.HOTPLUG] +        ) +        if not result: +            raise RuntimeError( +                "Datasource %s not updated for event %s" +                % (self.datasource, EventType.HOTPLUG) +            ) +        return result + + +class NetHandler(UeventHandler): +    def __init__(self, datasource, devpath, action, success_fn): +        # convert devpath to mac address +        id = read_sys_net_safe(os.path.basename(devpath), "address") +        super().__init__(id, datasource, devpath, action, success_fn) + +    def apply(self): +        self.datasource.distro.apply_network_config( +            self.config, +            bring_up=False, +        ) +        interface_name = os.path.basename(self.devpath) +        activator = activators.select_activator() +        if self.action == "add": +            if not activator.bring_up_interface(interface_name): +                raise RuntimeError( +                    "Failed to bring up device: {}".format(self.devpath) +                ) +        elif self.action == "remove": +            if not activator.bring_down_interface(interface_name): +                raise RuntimeError( +                    "Failed to bring down device: {}".format(self.devpath) +                ) + +    @property +    def config(self): +        return self.datasource.network_config + +    def device_detected(self) -> bool: +        netstate = parse_net_config_data(self.config) +        found = [ +            iface +            for iface in netstate.iter_interfaces() +            if iface.get("mac_address") == self.id +        ] +        LOG.debug("Ifaces with ID=%s : %s", self.id, found) +        return len(found) > 0 + + +SUBSYSTEM_PROPERTES_MAP = { +    "net": (NetHandler, EventScope.NETWORK), +} + + +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 +            ) +        ) from e + +    return stages.update_event_enabled( +        datasource=hotplug_init.datasource, +        cfg=hotplug_init.cfg, +        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 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, +        devpath=devpath, +        action=udevaction, +        success_fn=hotplug_init._write_to_cache, +    )  # type: UeventHandler +    wait_times = [1, 3, 5, 10, 30] +    for attempt, wait in enumerate(wait_times): +        LOG.debug( +            "subsystem=%s update attempt %s/%s", +            subsystem, +            attempt, +            len(wait_times), +        ) +        try: +            LOG.debug("Refreshing metadata") +            event_handler.update_metadata() +            LOG.debug("Detecting device in updated metadata") +            event_handler.detect_hotplugged_device() +            LOG.debug("Applying config change") +            event_handler.apply() +            LOG.debug("Updating cache") +            event_handler.success() +            break +        except Exception as e: +            LOG.debug("Exception while processing hotplug event. %s", e) +            time.sleep(wait) +            last_exception = e +    else: +        raise last_exception  # type: ignore + + +def handle_args(name, args): +    # Note that if an exception happens between now and when logging is +    # setup, we'll only see it in the journal +    hotplug_reporter = events.ReportEventStack( +        name, __doc__, reporting_enabled=True +    ) + +    hotplug_init = Init(ds_deps=[], reporter=hotplug_reporter) +    hotplug_init.read_cfg() + +    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: {" +        "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: +            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 + +    LOG.debug("Exiting hotplug handler") +    reporting.flush_events() + + +if __name__ == "__main__": +    args = get_parser().parse_args() +    handle_args(NAME, args)  | 
