From ba4e05f8f3b74fd3f0c96140244ce13101efe451 Mon Sep 17 00:00:00 2001 From: Guillaume Fieni Date: Tue, 2 Jun 2026 16:51:38 +0200 Subject: [PATCH] refactor(cli): Cleanup CLI arguments parsers --- .../cli/common_cli_parsing_manager.py | 578 ++++++++++-------- 1 file changed, 335 insertions(+), 243 deletions(-) diff --git a/src/powerapi/cli/common_cli_parsing_manager.py b/src/powerapi/cli/common_cli_parsing_manager.py index b792265c..51a9c5a8 100644 --- a/src/powerapi/cli/common_cli_parsing_manager.py +++ b/src/powerapi/cli/common_cli_parsing_manager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, INRIA +# Copyright (c) 2021, Inria # Copyright (c) 2021, University of Lille # All rights reserved. # @@ -27,353 +27,445 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import sys - -from powerapi.cli.parsing_manager import RootConfigParsingManager, SubgroupConfigParsingManager from powerapi.cli.config_parser import store_true -from powerapi.cli.config_parser import MissingValueException -from powerapi.exception import BadTypeException, BadContextException, UnknownArgException +from powerapi.cli.parsing_manager import RootConfigParsingManager, SubgroupConfigParsingManager -POWERAPI_ENVIRONMENT_VARIABLE_PREFIX = 'POWERAPI_' -POWERAPI_OUTPUT_ENVIRONMENT_VARIABLE_PREFIX = POWERAPI_ENVIRONMENT_VARIABLE_PREFIX + 'OUTPUT_' -POWERAPI_INPUT_ENVIRONMENT_VARIABLE_PREFIX = POWERAPI_ENVIRONMENT_VARIABLE_PREFIX + 'INPUT_' -POWERAPI_PRE_PROCESSOR_ENVIRONMENT_VARIABLE_PREFIX = POWERAPI_ENVIRONMENT_VARIABLE_PREFIX + 'PRE_PROCESSOR_' -POWERAPI_POST_PROCESSOR_ENVIRONMENT_VARIABLE_PREFIX = POWERAPI_ENVIRONMENT_VARIABLE_PREFIX + 'POST_PROCESSOR' + +def generate_env_prefix(*components: str, root_prefix: str = 'POWERAPI') -> str: + """ + Generate the environment variable prefix from the given components. + :param components: Additional prefix components. + :param root_prefix: Root namespace for the prefix. + :return: The normalized environment variable prefix. + """ + return '_'.join( + normalized_part.upper() for part in (root_prefix, *components) if (normalized_part := part.strip()) + ) + '_' -class CommonCLIParsingManager(RootConfigParsingManager): +class PullerConfigParsingManager(SubgroupConfigParsingManager): """ - PowerAPI basic config parser + Subgroup parser with arguments shared by every puller input. """ - def __init__(self): - RootConfigParsingManager.__init__(self) + def __init__(self, name: str) -> None: + """ + Initialize a puller parser with common actor name and report model arguments. + """ + super().__init__(name) + + self.add_argument( + 'n', 'name', + help_text='Name assigned to this puller actor', + is_mandatory=True + ) + self.add_argument( + 'm', 'model', + help_text='Report type produced by this input source', + default_value='HWPCReport' + ) - # Environment variables prefix - self.add_argument_prefix(argument_prefix=POWERAPI_ENVIRONMENT_VARIABLE_PREFIX) - # Subgroups - self.add_subgroup(name='input', - prefix=POWERAPI_INPUT_ENVIRONMENT_VARIABLE_PREFIX, - help_text="specify a database input : --input database_name ARG1 ARG2 ... ") +class PusherConfigParsingManager(SubgroupConfigParsingManager): + """ + Subgroup parser with arguments shared by every pusher output. + """ + + def __init__(self, name: str) -> None: + """ + Initialize a pusher parser with common actor name and report model arguments. + """ + super().__init__(name) - self.add_subgroup(name='output', - prefix=POWERAPI_OUTPUT_ENVIRONMENT_VARIABLE_PREFIX, - help_text="specify a database output : --output database_name ARG1 ARG2 ...") + self.add_argument( + 'n', 'name', + help_text='Name assigned to this pusher actor', + is_mandatory=True + ) + self.add_argument( + 'm', 'model', + help_text='Report type consumed by this output destination', + default_value='PowerReport' + ) - self.add_subgroup(name='pre-processor', - prefix=POWERAPI_PRE_PROCESSOR_ENVIRONMENT_VARIABLE_PREFIX, - help_text="specify a pre-processor : --pre-processor pre_processor_name ARG1 ARG2 ...") - self.add_subgroup(name='post-processor', - prefix=POWERAPI_POST_PROCESSOR_ENVIRONMENT_VARIABLE_PREFIX, - help_text="specify a post-processor : --post-processor post_processor_name ARG1 ARG2 ...") +class PreProcessorConfigParsingManager(SubgroupConfigParsingManager): + """ + Subgroup parser with arguments shared by every pre-processor. + """ - # Parsers + def __init__(self, name: str) -> None: + """ + Initialize a pre-processor parser with common actor name and puller binding arguments. + """ + super().__init__(name) self.add_argument( - "v", - "verbose", + 'n', 'name', + help_text='Name assigned to this pre-processor actor', + is_mandatory=True + ) + self.add_argument( + 'p', 'puller', + help_text='Name of the puller actor this pre-processor receives reports from', + is_mandatory=True, + ) + + +class CommonCLIParsingManager(RootConfigParsingManager): + """ + Root parser that registers PowerAPI's built-in CLI component options. + """ + + def __init__(self) -> None: + """ + Initialize the root parser and register all built-in component parsers. + """ + super().__init__() + + self._register_environment_prefixes() + self._register_subgroups() + self._register_root_arguments() + self._register_input_parsers() + self._register_output_parsers() + self._register_pre_processor_parsers() + + def _register_environment_prefixes(self) -> None: + """ + Register environment variable prefixes accepted by the root parser. + """ + self.add_argument_prefix(generate_env_prefix()) + + def _register_subgroups(self) -> None: + """ + Register top-level component groups accepted by the CLI. + """ + self.add_subgroup( + name='input', + prefix=generate_env_prefix('INPUT'), + help_text='Configure an input source: --input TYPE OPTIONS' + ) + self.add_subgroup( + name='output', + prefix=generate_env_prefix('OUTPUT'), + help_text='Configure an output destination: --output TYPE OPTIONS' + ) + self.add_subgroup( + name='pre-processor', + prefix=generate_env_prefix('PRE_PROCESSOR'), + help_text='Configure a pre-processor: --pre-processor TYPE OPTIONS' + ) + self.add_subgroup( + name='post-processor', + prefix=generate_env_prefix('POST_PROCESSOR'), + help_text='Configure a post-processor: --post-processor TYPE OPTIONS' + ) + + def _register_root_arguments(self) -> None: + """ + Register root-level options that apply to the whole PowerAPI process. + """ + self.add_argument( + 'v', 'verbose', is_flag=True, action=store_true, default_value=False, - help_text="enable verbose mode", + help_text='Enable verbose logging', ) self.add_argument( - "s", - "stream", + 's', 'stream', is_flag=True, action=store_true, default_value=False, - help_text="enable stream mode", + help_text='Enable stream processing mode', ) - subparser_mongo_input = SubgroupConfigParsingManager("mongodb") - subparser_mongo_input.add_argument("u", "uri", help_text="specify MongoDB uri") - subparser_mongo_input.add_argument( - "d", - "db", - help_text="specify MongoDB database name", - ) + def _register_input_parsers(self): + """ + Register all built-in input source parsers. + """ + self._register_mongodb_input_parser() + self._register_socket_input_parser() + self._register_csv_input_parser() + self._register_json_input_parser() + + def _register_mongodb_input_parser(self): + """ + Register the MongoDB input parser. + """ + subparser_mongo_input = PullerConfigParsingManager('mongodb') + subparser_mongo_input.add_argument( - "c", "collection", help_text="specify MongoDB database collection" + 'u', 'uri', + help_text='MongoDB connection URI', + is_mandatory=True ) subparser_mongo_input.add_argument( - "n", "name", help_text="specify puller name", default_value="puller_mongodb" + 'd', 'db', + help_text='MongoDB database name', + is_mandatory=True ) subparser_mongo_input.add_argument( - "m", - "model", - help_text="specify data type that will be stored in the database", - default_value="HWPCReport", - ) - self.add_subgroup_parser( - subgroup_name="input", - subgroup_parser=subparser_mongo_input + 'c', 'collection', + help_text='MongoDB collection name', + is_mandatory=True ) - subparser_socket_input = SubgroupConfigParsingManager("socket") - subparser_socket_input.add_argument( - "h", "host", help_text="Specify the host the socket should listen on", default_value='127.0.0.1' - ) - subparser_socket_input.add_argument( - "p", "port", help_text="Specify the port the socket should listen on", argument_type=int, default_value=9080, - ) + self.add_subgroup_parser('input', subparser_mongo_input) + + def _register_socket_input_parser(self): + """ + Register the Socket input parser. + """ + subparser_socket_input = PullerConfigParsingManager('socket') + subparser_socket_input.add_argument( - "n", "name", help_text="specify puller name", default_value="puller_socket" + 'h', 'host', + help_text='Host address the socket listens on', + default_value='localhost' ) subparser_socket_input.add_argument( - "m", - "model", - help_text="specify data type that will be sent through the socket", - default_value="HWPCReport", - ) - self.add_subgroup_parser( - subgroup_name="input", - subgroup_parser=subparser_socket_input + 'p', 'port', + help_text="Port number the socket listens on", + argument_type=int, + default_value=9080, ) - subparser_csv_input = SubgroupConfigParsingManager("csv") + self.add_subgroup_parser('input', subparser_socket_input) + + def _register_csv_input_parser(self): + """ + Register the CSV input parser. + """ + subparser_csv_input = PullerConfigParsingManager('csv') + subparser_csv_input.add_argument( - "f", - "files", - help_text="specify input csv files with this format : file1,file2,file3", + 'f', 'files', + help_text='Comma-separated list of CSV input files', argument_type=list, is_mandatory=True ) - subparser_csv_input.add_argument( - "m", - "model", - help_text="specify data type that will be stored in the database", - default_value="HWPCReport", - ) - subparser_csv_input.add_argument( - "n", "name", help_text="specify puller name", default_value="puller_csv" - ) - self.add_subgroup_parser( - subgroup_name="input", - subgroup_parser=subparser_csv_input - ) - subparser_json_input = SubgroupConfigParsingManager("json") - subparser_json_input.add_argument("n", "name", help_text="Name of the puller", default_value="puller_json") - subparser_json_input.add_argument("m", "model", help_text="Expected report type") - subparser_json_input.add_argument("f", "filepath", help_text="Input file path") - subparser_json_input.add_argument("c", "compression", default_value='auto', help_text="Compression type") - self.add_subgroup_parser("input", subparser_json_input) + self.add_subgroup_parser('input', subparser_csv_input) - subparser_mongo_output = SubgroupConfigParsingManager("mongodb") - subparser_mongo_output.add_argument("u", "uri", help_text="specify MongoDB uri") - subparser_mongo_output.add_argument( - "d", "db", help_text="specify MongoDB database name" + def _register_json_input_parser(self): + """ + Register the JSON input parser. + """ + subparser_json_input = PullerConfigParsingManager('json') + + subparser_json_input.add_argument( + 'f', 'filepath', + help_text='Path to the JSON input file', + is_mandatory=True ) - subparser_mongo_output.add_argument( - "c", "collection", help_text="specify MongoDB database collection" + subparser_json_input.add_argument( + 'c', 'compression', + help_text='Input compression format: auto, gzip, lzma, or none', + default_value='auto' ) + self.add_subgroup_parser('input', subparser_json_input) + + def _register_output_parsers(self): + """ + Register all built-in output destination parsers. + """ + self._register_mongodb_output_parser() + self._register_prometheus_output_parser() + self._register_csv_output_parser() + self._register_json_output_parser() + self._register_opentsdb_output_parser() + self._register_influxdb2_output_parser() + + def _register_mongodb_output_parser(self): + """ + Register the MongoDB output parser. + """ + subparser_mongo_output = PusherConfigParsingManager('mongodb') + subparser_mongo_output.add_argument( - "m", - "model", - help_text="specify data type that will be stored in the database", - default_value="PowerReport", + 'u', 'uri', + help_text='MongoDB connection URI', + is_mandatory=True ) subparser_mongo_output.add_argument( - "n", "name", help_text="specify pusher name", default_value="pusher_mongodb" + 'd', 'db', + help_text='MongoDB database name', + is_mandatory=True ) - self.add_subgroup_parser( - subgroup_name="output", - subgroup_parser=subparser_mongo_output + subparser_mongo_output.add_argument( + 'c', 'collection', + help_text='MongoDB collection name', + is_mandatory=True ) - subparser_prometheus_output = SubgroupConfigParsingManager("prometheus") - subparser_prometheus_output.add_argument( - "n", "name", - help_text="Name of the pusher", - default_value="pusher_prometheus" - ) - subparser_prometheus_output.add_argument( - "m", "model", - help_text="Input report type", - default_value="PowerReport" - ) + self.add_subgroup_parser('output', subparser_mongo_output) + + def _register_prometheus_output_parser(self): + """ + Register the Prometheus output parser. + """ + subparser_prometheus_output = PusherConfigParsingManager('prometheus') + subparser_prometheus_output.add_argument( - "u", "addr", - help_text="Address the HTTP server should listen on", - default_value="localhost" + 'u', 'addr', + help_text='Host address the Prometheus HTTP server listens on', + default_value='localhost' ) subparser_prometheus_output.add_argument( - "p", "port", - help_text="Port number the HTTP server should listen on", + 'p', 'port', + help_text='Port number the Prometheus HTTP server listens on', argument_type=int, default_value=8000 ) subparser_prometheus_output.add_argument( - "M", "metric-name", - help_text="Exposed metric name", - default_value="power_estimation_watts" + 'M', 'metric-name', + help_text='Prometheus metric name to expose', + default_value='power_estimation_watts' ) subparser_prometheus_output.add_argument( - "d", "metric-description", - help_text="Set the exposed metric short description", - default_value="Estimated power consumption of the target" + 'd', 'metric-description', + help_text='Prometheus metric description', + default_value='Estimated power consumption of the target' ) subparser_prometheus_output.add_argument( - "t", "tags", - help_text="List of metadata tags that will be exposed with the metrics", + 't', 'tags', + help_text='Comma-separated list of report metadata fields exposed as metric labels', argument_type=list ) - self.add_subgroup_parser( - subgroup_name="output", - subgroup_parser=subparser_prometheus_output - ) - subparser_csv_output = SubgroupConfigParsingManager("csv") + self.add_subgroup_parser('output', subparser_prometheus_output) + + def _register_csv_output_parser(self): + """ + Register the CSV output parser. + """ + subparser_csv_output = PusherConfigParsingManager('csv') + subparser_csv_output.add_argument( - "d", - "directory", - help_text="specify directory where where output csv files will be writen", + 'd', 'directory', + help_text='Directory where CSV output files are written', is_mandatory=True ) - subparser_csv_output.add_argument( - "m", - "model", - help_text="specify data type that will be stored in the database", - default_value="PowerReport", - ) - subparser_csv_output.add_argument( - "n", "name", help_text="specify pusher name", default_value="pusher_csv" - ) - self.add_subgroup_parser( - subgroup_name="output", - subgroup_parser=subparser_csv_output - ) + self.add_subgroup_parser('output', subparser_csv_output) - subparser_json_output = SubgroupConfigParsingManager("json") - subparser_json_output.add_argument("n", "name", help_text="Name of the pusher", default_value="pusher_json") - subparser_json_output.add_argument("m", "model", help_text="Report type to be exported") - subparser_json_output.add_argument("f", "filepath", help_text="Output file path") - subparser_json_output.add_argument("c", "compression", default_value="auto", help_text="Compression type") - self.add_subgroup_parser("output", subparser_json_output) + def _register_json_output_parser(self): + """ + Register the JSON output parser. + """ + subparser_json_output = PusherConfigParsingManager('json') - subparser_opentsdb_output = SubgroupConfigParsingManager("opentsdb") - subparser_opentsdb_output.add_argument("u", "uri", help_text="specify openTSDB host") - subparser_opentsdb_output.add_argument( - "p", "port", help_text="specify openTSDB connection port", argument_type=int + subparser_json_output.add_argument( + 'f', 'filepath', + help_text='Path to the JSON output file', + is_mandatory=True ) - subparser_opentsdb_output.add_argument( - "metric-name", help_text="specify metric name" + subparser_json_output.add_argument( + 'c', 'compression', + help_text='Output compression format: auto, gzip, lzma, or none', + default_value='auto' ) + self.add_subgroup_parser('output', subparser_json_output) + + def _register_opentsdb_output_parser(self): + """ + Register the OpenTSDB output parser. + """ + subparser_opentsdb_output = PusherConfigParsingManager('opentsdb') + subparser_opentsdb_output.add_argument( - "m", - "model", - help_text="specify data type that will be stored in the database", - default_value="PowerReport", + 'u', 'uri', + help_text='OpenTSDB host address', + is_mandatory=True ) subparser_opentsdb_output.add_argument( - "n", "name", help_text="specify pusher name", default_value="pusher_opentsdb" + 'p', 'port', + help_text='OpenTSDB connection port', + argument_type=int, + default_value=4242 ) - self.add_subgroup_parser( - subgroup_name="output", - subgroup_parser=subparser_opentsdb_output + subparser_opentsdb_output.add_argument( + 'metric-name', + help_text='OpenTSDB metric name to write', + is_mandatory=True ) - subparser_influx2_output = SubgroupConfigParsingManager("influxdb2") - subparser_influx2_output.add_argument("u", "uri", help_text="specify InfluxDB uri") - subparser_influx2_output.add_argument("k", "token", - help_text="specify token for accessing the database") - subparser_influx2_output.add_argument("g", "org", - help_text="specify organisation for accessing the database") + self.add_subgroup_parser('output', subparser_opentsdb_output) + + def _register_influxdb2_output_parser(self): + """ + Register the InfluxDB 2 output parser. + """ + subparser_influx2_output = PusherConfigParsingManager('influxdb2') subparser_influx2_output.add_argument( - "b", "bucket", help_text="specify InfluxDB database name" + 'u', 'uri', + help_text='InfluxDB server URI', + is_mandatory=True ) subparser_influx2_output.add_argument( - "p", "port", help_text="specify InfluxDB connection port", argument_type=int + 'k', 'token', + help_text='InfluxDB API token', + is_mandatory=True ) subparser_influx2_output.add_argument( - "m", - "model", - help_text="specify data type that will be store in the database", - default_value="PowerReport", + 'g', 'org', + help_text='InfluxDB organization name', + is_mandatory=True ) subparser_influx2_output.add_argument( - "n", "name", help_text="specify pusher name", default_value="pusher_influxdb2" + 'b', 'bucket', + help_text='InfluxDB bucket name', + is_mandatory=True ) - self.add_subgroup_parser( - subgroup_name="output", - subgroup_parser=subparser_influx2_output - ) + self.add_subgroup_parser('output', subparser_influx2_output) - subparser_k8s_pre_processor = SubgroupConfigParsingManager("k8s") - subparser_k8s_pre_processor.add_argument( - "a", - "api-mode", - help_text="Kubernetes API mode (local, manual or cluster)", - default_value='cluster' - ) + def _register_pre_processor_parsers(self): + """ + Register all built-in pre-processor parsers. + """ + self._register_k8s_pre_processor_parser() + self._register_openstack_pre_processor_parser() - subparser_k8s_pre_processor.add_argument( - "k", - "api-key", - help_text="Kubernetes Bearer Token (only for manual API mode)", - ) + def _register_k8s_pre_processor_parser(self): + """ + Register the Kubernetes pre-processor parser. + """ + subparser_k8s_pre_processor = PreProcessorConfigParsingManager('k8s') subparser_k8s_pre_processor.add_argument( - "h", - "api-host", - help_text="Kubernetes API host (only for manual API mode)", + 'a', 'api-mode', + help_text='Kubernetes API access mode: local, manual, or cluster', + default_value='cluster' ) subparser_k8s_pre_processor.add_argument( - "p", - "puller", - help_text="Name of the puller to attach the pre-processor to", + 'k', 'api-key', + help_text='Kubernetes bearer token for manual API mode', ) subparser_k8s_pre_processor.add_argument( - "n", - "name", - help_text="Name of the pre-processor" - ) - - self.add_subgroup_parser( - subgroup_name="pre-processor", - subgroup_parser=subparser_k8s_pre_processor + 'h', 'api-host', + help_text='Kubernetes API host for manual API mode', ) - subparser_openstack_pre_processor = SubgroupConfigParsingManager("openstack") - subparser_openstack_pre_processor.add_argument("p", "puller", help_text="Name of the puller to attach the pre-processor to", is_mandatory=True) - subparser_openstack_pre_processor.add_argument("n", "name", help_text="Name of the pre-processor", default_value='preprocessor_openstack') - subparser_openstack_pre_processor.add_argument('i', "polling-interval", help_text="OpenStack API polling interval (in seconds)", argument_type=float, default_value=10.0) - self.add_subgroup_parser("pre-processor", subparser_openstack_pre_processor) + self.add_subgroup_parser('pre-processor', subparser_k8s_pre_processor) - def parse_argv(self): + def _register_openstack_pre_processor_parser(self): """ - Parse command line arguments. + Register the OpenStack pre-processor parser. """ - try: - return self.parse(sys.argv[1:]) + subparser_openstack_pre_processor = PreProcessorConfigParsingManager('openstack') - except MissingValueException as exn: - msg = "CLI error : argument " + exn.argument_name + " : expect a value" - print(msg, file=sys.stderr) - - except BadTypeException as exn: - msg = "Configuration error : " + exn.msg - print(msg, file=sys.stderr) - - except UnknownArgException as exn: - msg = "CLI error : unknown argument " + exn.argument_name - print(msg, file=sys.stderr) - - except BadContextException as exn: - msg = "CLI error : argument " + exn.argument_name - msg += " not used in the correct context\nUse it with the following arguments :" - for main_arg_name, context_name in exn.context_list: - msg += "\n --" + main_arg_name + " " + context_name - print(msg, file=sys.stderr) + subparser_openstack_pre_processor.add_argument( + 'i', "polling-interval", + help_text='OpenStack API polling interval in seconds', + argument_type=float, + default_value=10.0 + ) - sys.exit() + self.add_subgroup_parser('pre-processor', subparser_openstack_pre_processor)