Merge "Add plugin entry point sorting mechanism in OSC"
This commit is contained in:
commit
55297417a5
@ -0,0 +1,327 @@
|
||||
From 98f5f373e068032ae89d205edb12ab9b62e39d3e Mon Sep 17 00:00:00 2001
|
||||
From: Luan Nunes Utimura <LuanNunes.Utimura@windriver.com>
|
||||
Date: Fri, 24 Feb 2023 08:47:48 -0300
|
||||
Subject: [PATCH] Add plugin entry point sorting mechanism
|
||||
|
||||
On CentOS, with `python-openstackclient` on version 4.0.0
|
||||
(stable/train), the plugin entry point discovery was done by using
|
||||
a built-in library called `pkg_resources` ([1], [2], [3]).
|
||||
|
||||
On Debian, with `python-openstackclient` on version 5.4.0-4
|
||||
(stable/victoria), the discovery process is now performed by using the
|
||||
`stevedore` library ([4], [5], [6]).
|
||||
|
||||
The problem with this replacement is that, with `stevedore`, there's no
|
||||
guarantee that the plugin entry point discovery list will be the same as
|
||||
it was with `pkg_resources`. That is, the fetching order of entry points
|
||||
may vary.
|
||||
|
||||
For most plugins that only add commands to the existing OpenStackClient
|
||||
(OSC) CLI, this is fine, as the loading order doesn't matter.
|
||||
|
||||
However, for those who also override existing entry points configured by
|
||||
other plugins, this may become a problem, because they need to be loaded
|
||||
after the original plugins that define the entry points, otherwise the
|
||||
overrides will have no effect.
|
||||
|
||||
Therefore, this change aims to provide a plugin entry point sorting
|
||||
mechanism to keep the discovery process more consistent.
|
||||
|
||||
By reading plugin-specific options such as `load_first` or `load_last`
|
||||
from a configuration file - that can be specified through command-line
|
||||
argument (--os-osc-config-file, defaults to
|
||||
/etc/openstackclient/openstackclient.conf) - the plugin entry point
|
||||
sorting mechanism can decide where to insert the newly discovered
|
||||
plugin: at the beginning, at the end, or where it would be inserted by
|
||||
default in the list.
|
||||
|
||||
[1] https://opendev.org/starlingx/upstream/src/branch/master/openstack/python-openstackclient/centos/python-openstackclient.spec#L19
|
||||
[2] https://opendev.org/openstack/python-openstackclient/src/branch/stable/train/openstackclient/common/clientmanager.py#L146
|
||||
[3] https://opendev.org/openstack/cliff/src/branch/stable/train/cliff/commandmanager.py#L61
|
||||
[4] https://opendev.org/starlingx/upstream/src/branch/master/openstack/python-openstackclient/debian/meta_data.yaml#L5
|
||||
[5] https://opendev.org/openstack/python-openstackclient/src/branch/stable/victoria/openstackclient/common/clientmanager.py#L147
|
||||
[6] https://opendev.org/openstack/cliff/src/branch/stable/victoria/cliff/commandmanager.py#L75
|
||||
|
||||
Signed-off-by: Luan Nunes Utimura <LuanNunes.Utimura@windriver.com>
|
||||
---
|
||||
openstackclient/common/clientmanager.py | 173 +++++++++++++++++++++++-
|
||||
openstackclient/shell.py | 33 ++++-
|
||||
2 files changed, 202 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
|
||||
index 36c3ce26..3b859c67 100644
|
||||
--- a/openstackclient/common/clientmanager.py
|
||||
+++ b/openstackclient/common/clientmanager.py
|
||||
@@ -19,14 +19,17 @@ import importlib
|
||||
import logging
|
||||
import sys
|
||||
|
||||
+from osc_lib.i18n import _
|
||||
from osc_lib import clientmanager
|
||||
from osc_lib import shell
|
||||
+from oslo_config import cfg
|
||||
import stevedore
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
PLUGIN_MODULES = []
|
||||
+PLUGIN_OPTIONS = {}
|
||||
|
||||
USER_AGENT = 'python-openstackclient'
|
||||
|
||||
@@ -141,11 +144,177 @@ class ClientManager(clientmanager.ClientManager):
|
||||
|
||||
# Plugin Support
|
||||
|
||||
+def _get_config_file_sections(config):
|
||||
+ """Get sections from a configuration file.
|
||||
+
|
||||
+ Using oslo.config's cfg.ConfigParser, parses a single configuration file
|
||||
+ into a dictionary containing sections and their respective options.
|
||||
+
|
||||
+ The resulting dictionary has the following structure:
|
||||
+
|
||||
+ {
|
||||
+ "<section>": {
|
||||
+ "<key>": [<value>, ...],
|
||||
+ ...
|
||||
+ },
|
||||
+ ...
|
||||
+ }
|
||||
+
|
||||
+ :param config: the config file
|
||||
+ :returns: dict -- the dictionary of config file sections
|
||||
+ """
|
||||
+
|
||||
+ sections = {}
|
||||
+
|
||||
+ try:
|
||||
+ cfg.ConfigParser(config, sections).parse()
|
||||
+ except cfg.ParseError as e:
|
||||
+ msg = _(
|
||||
+ 'Error while parsing the configuration file `{}`.\n'
|
||||
+ 'Location: {}'
|
||||
+ ).format(config, e)
|
||||
+
|
||||
+ LOG.error(msg)
|
||||
+ except FileNotFoundError:
|
||||
+ msg = _(
|
||||
+ 'No custom config file found for OpenStackClient (OSC). '
|
||||
+ 'Using default configurations.'
|
||||
+ ).format(config)
|
||||
+ LOG.debug(msg)
|
||||
+
|
||||
+ return sections
|
||||
+
|
||||
+
|
||||
+def _standardize_list_type_options(options, list_opts):
|
||||
+ """Standardize list-type options.
|
||||
+
|
||||
+ With oslo.config's cfg.ConfigParser, depending on how the options are
|
||||
+ specified, their values can be parsed differently. For example:
|
||||
+
|
||||
+ load_first = A,B
|
||||
+
|
||||
+ Becomes:
|
||||
+
|
||||
+ {"load_first": ["A,B"]}
|
||||
+
|
||||
+ While:
|
||||
+
|
||||
+ load_first = A
|
||||
+ load_first = B
|
||||
+
|
||||
+ Becomes:
|
||||
+
|
||||
+ {"load_first": ["A", "B"]}
|
||||
+
|
||||
+ Therefore, this function standardizes the values of list-type options
|
||||
+ so that they are always presented in the latter format.
|
||||
+
|
||||
+ :param options: the options dictionary
|
||||
+ :param list_opts: the list of options to be standardized
|
||||
+ """
|
||||
+
|
||||
+ for list_opt in list_opts:
|
||||
+ if list_opt in options:
|
||||
+ values = [
|
||||
+ value.strip()
|
||||
+ for value in ','.join(options[list_opt]).split(',')
|
||||
+ if value.strip()
|
||||
+ ]
|
||||
+ options[list_opt] = values
|
||||
+
|
||||
+
|
||||
+def process_plugin_options(options):
|
||||
+ """Process plugin-related options.
|
||||
+
|
||||
+ :param options: the dictionary of options
|
||||
+ """
|
||||
+
|
||||
+ global PLUGIN_OPTIONS
|
||||
+
|
||||
+ # If no configuration file was specified,
|
||||
+ # there are no plugin options to process.
|
||||
+ if options.osc_config_file in ['', None]:
|
||||
+ return
|
||||
+
|
||||
+ PLUGIN_OPTIONS = _get_config_file_sections(options.osc_config_file).get(
|
||||
+ 'plugins', {}
|
||||
+ )
|
||||
+
|
||||
+ # Standardizes list-type options' values.
|
||||
+ LIST_OPTS = [
|
||||
+ 'load_first',
|
||||
+ 'load_last',
|
||||
+ ]
|
||||
+
|
||||
+ _standardize_list_type_options(PLUGIN_OPTIONS, LIST_OPTS)
|
||||
+
|
||||
+
|
||||
+def sort_plugin_entry_points(group):
|
||||
+ """Sort plugin entry points.
|
||||
+
|
||||
+ Given a configuration file specified through command-line argument
|
||||
+ (--os-osc-config-file) or environment variable (OS_OSC_CONFIG_FILE),
|
||||
+ this function sorts the plugin entry points based on the `load_first`
|
||||
+ and `load_last` options (if present).
|
||||
+
|
||||
+ By setting:
|
||||
+
|
||||
+ [plugins]
|
||||
+ load_first = A
|
||||
+
|
||||
+ This function will ensure that the plugin "A" is loaded before any other
|
||||
+ plugin. Similarly, by setting:
|
||||
+
|
||||
+ [plugins]
|
||||
+ load_last = A
|
||||
+
|
||||
+ It will ensure that the plugin "A" is loaded after any other plugin.
|
||||
+
|
||||
+ Multiples plugins are supported (as long as they're separated by commas).
|
||||
+
|
||||
+ :param group: the group name for the entry points
|
||||
+ :returns: list -- list of entry points
|
||||
+
|
||||
+ """
|
||||
+
|
||||
+ LOAD_FIRST_PLUGINS = PLUGIN_OPTIONS.get('load_first', [])
|
||||
+ LOAD_LAST_PLUGINS = PLUGIN_OPTIONS.get('load_last', [])
|
||||
+
|
||||
+ before_entry_points = []
|
||||
+ after_entry_points = []
|
||||
+ entry_points = []
|
||||
+
|
||||
+ mgr = stevedore.ExtensionManager(group)
|
||||
+ for ep in mgr:
|
||||
+ # Different versions of stevedore use different
|
||||
+ # implementations of EntryPoint from other libraries, which
|
||||
+ # are not API-compatible.
|
||||
+ try:
|
||||
+ module_name = ep.entry_point.module_name
|
||||
+ except AttributeError:
|
||||
+ try:
|
||||
+ module_name = ep.entry_point.module
|
||||
+ except AttributeError:
|
||||
+ module_name = ep.entry_point.value
|
||||
+
|
||||
+ # If the module name matches at least one plugin specified in
|
||||
+ # `load_first` or `load_last`, append the current entry point
|
||||
+ # to the correspondent list.
|
||||
+ if any([module_name.startswith(cpm) for cpm in LOAD_FIRST_PLUGINS]):
|
||||
+ before_entry_points.append(ep)
|
||||
+ elif any([module_name.startswith(cpm) for cpm in LOAD_LAST_PLUGINS]):
|
||||
+ after_entry_points.append(ep)
|
||||
+ else:
|
||||
+ entry_points.append(ep)
|
||||
+
|
||||
+ return before_entry_points + entry_points + after_entry_points
|
||||
+
|
||||
+
|
||||
def get_plugin_modules(group):
|
||||
"""Find plugin entry points"""
|
||||
mod_list = []
|
||||
- mgr = stevedore.ExtensionManager(group)
|
||||
- for ep in mgr:
|
||||
+
|
||||
+ for ep in sort_plugin_entry_points(group):
|
||||
LOG.debug('Found plugin %s', ep.name)
|
||||
|
||||
# Different versions of stevedore use different
|
||||
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
|
||||
index 755af24d..52d50256 100644
|
||||
--- a/openstackclient/shell.py
|
||||
+++ b/openstackclient/shell.py
|
||||
@@ -21,7 +21,9 @@ import sys
|
||||
|
||||
from osc_lib.api import auth
|
||||
from osc_lib.command import commandmanager
|
||||
+from osc_lib.i18n import _
|
||||
from osc_lib import shell
|
||||
+from osc_lib import utils
|
||||
import six
|
||||
|
||||
import openstackclient
|
||||
@@ -31,14 +33,26 @@ from openstackclient.common import clientmanager
|
||||
DEFAULT_DOMAIN = 'default'
|
||||
|
||||
|
||||
-class OpenStackShell(shell.OpenStackShell):
|
||||
+class CommandManager(commandmanager.CommandManager):
|
||||
+ def load_commands(self, namespace):
|
||||
+ """Load all commands from an entry point."""
|
||||
+
|
||||
+ self.group_list.append(namespace)
|
||||
+ for ep in clientmanager.sort_plugin_entry_points(namespace):
|
||||
+ cmd_name = (ep.name.replace('_', ' ')
|
||||
+ if self.convert_underscores
|
||||
+ else ep.name)
|
||||
+ self.commands[cmd_name] = ep.entry_point
|
||||
+ return
|
||||
|
||||
+
|
||||
+class OpenStackShell(shell.OpenStackShell):
|
||||
def __init__(self):
|
||||
|
||||
super(OpenStackShell, self).__init__(
|
||||
description=__doc__.strip(),
|
||||
version=openstackclient.__version__,
|
||||
- command_manager=commandmanager.CommandManager('openstack.cli'),
|
||||
+ command_manager=CommandManager('openstack.cli'),
|
||||
deferred_help=True)
|
||||
|
||||
self.api_version = {}
|
||||
@@ -52,6 +66,18 @@ class OpenStackShell(shell.OpenStackShell):
|
||||
version)
|
||||
parser = clientmanager.build_plugin_option_parser(parser)
|
||||
parser = auth.build_auth_plugins_option_parser(parser)
|
||||
+
|
||||
+ parser.add_argument(
|
||||
+ '--os-osc-config-file',
|
||||
+ metavar='<osc-config-file>',
|
||||
+ dest='osc_config_file',
|
||||
+ default='/etc/openstackclient/openstackclient.conf',
|
||||
+ help=_(
|
||||
+ 'OpenStackClient (OSC) configuration file, '
|
||||
+ 'default=/etc/openstackclient/openstackclient.conf'
|
||||
+ )
|
||||
+ )
|
||||
+
|
||||
return parser
|
||||
|
||||
def _final_defaults(self):
|
||||
@@ -64,6 +90,9 @@ class OpenStackShell(shell.OpenStackShell):
|
||||
else:
|
||||
self._auth_type = 'password'
|
||||
|
||||
+ # Process plugin-related options.
|
||||
+ clientmanager.process_plugin_options(self.options)
|
||||
+
|
||||
def _load_plugins(self):
|
||||
"""Load plugins via stevedore
|
||||
|
||||
--
|
||||
2.25.1
|
||||
|
1
openstack/python-openstackclient/debian/patches/series
Normal file
1
openstack/python-openstackclient/debian/patches/series
Normal file
@ -0,0 +1 @@
|
||||
0001-Add-plugin-entry-point-sorting-mechanism.patch
|
Loading…
Reference in New Issue
Block a user