From a2243cf59edb145bb3341b9ad56b14428bc90197 Mon Sep 17 00:00:00 2001 From: Darko Poljak Date: Sat, 22 Jun 2019 12:55:56 +0200 Subject: [PATCH] Add support for python type defined argument parser --- cdist/conf/type/__file_py/__init__.py | 14 ++++++- cdist/core/code.py | 25 ++--------- cdist/core/manifest.py | 25 ++--------- cdist/core/pytypes.py | 60 ++++++++++++++++++++------- cdist/emulator.py | 34 ++++++++++++--- docs/src/cdist-type.rst | 3 ++ 6 files changed, 96 insertions(+), 65 deletions(-) diff --git a/cdist/conf/type/__file_py/__init__.py b/cdist/conf/type/__file_py/__init__.py index 1212f8fd..3397b417 100644 --- a/cdist/conf/type/__file_py/__init__.py +++ b/cdist/conf/type/__file_py/__init__.py @@ -1,7 +1,7 @@ import os import re -import sys from cdist.core.pytypes import * +import argparse class FileType(PythonType): @@ -101,3 +101,15 @@ class FileType(PythonType): self.die('Unknown state {}'.format(state_should)) return "\n".join(code) + + def get_args_parser(self): + parser = argparse.ArgumentParser(add_help=False, + argument_default=argparse.SUPPRESS) + parser.add_argument('--state', dest='state', action='store', + required=False, default='present') + for param in ('group', 'mode', 'owner', 'source'): + parser.add_argument('--' + param, dest=param, action='store', + required=False, default=None) + + parser.add_argument("object_id", nargs=1) + return parser diff --git a/cdist/core/code.py b/cdist/core/code.py index 34d5bd97..ae1fddf2 100644 --- a/cdist/core/code.py +++ b/cdist/core/code.py @@ -22,10 +22,8 @@ # import os -import importlib.util import inspect -import cdist -from cdist.core.pytypes import PythonType +from cdist.core.pytypes import get_pytype_class from . import util @@ -122,25 +120,8 @@ class Code(object): def run_py(self, cdist_object): cdist_type = cdist_object.cdist_type - module_name = cdist_type.name - file_path = os.path.join(cdist_type.absolute_path, '__init__.py') - - if os.path.isfile(file_path): - spec = importlib.util.spec_from_file_location(module_name, - file_path) - m = importlib.util.module_from_spec(spec) - spec.loader.exec_module(m) - classes = inspect.getmembers(m, inspect.isclass) - type_class = None - for _, cl in classes: - if cl != PythonType and issubclass(cl, PythonType): - if type_class: - raise cdist.Error("Only one python type class is " - "supported, but at least two " - "found: {}".format((type_class, - cl, ))) - else: - type_class = cl + type_class = get_pytype_class(cdist_type) + if type_class is not None: env = os.environ.copy() env.update(self.env) message_prefix = cdist_object.name diff --git a/cdist/core/manifest.py b/cdist/core/manifest.py index 546fba25..16dec465 100644 --- a/cdist/core/manifest.py +++ b/cdist/core/manifest.py @@ -22,12 +22,11 @@ import logging import os -import importlib.util import inspect import cdist import cdist.emulator from . import util -from cdist.core.pytypes import PythonType, Command +from cdist.core.pytypes import Command, get_pytype_class ''' @@ -248,27 +247,11 @@ class Manifest(object): def run_py_type_manifest(self, cdist_object): cdist_type = cdist_object.cdist_type - module_name = cdist_type.name - file_path = os.path.join(cdist_type.absolute_path, '__init__.py') - message_prefix = cdist_object.name - if os.path.isfile(file_path): + type_class = get_pytype_class(cdist_type) + if type_class is not None: self.log.verbose("Running python type manifest for object %s", cdist_object.name) - spec = importlib.util.spec_from_file_location(module_name, - file_path) - m = importlib.util.module_from_spec(spec) - spec.loader.exec_module(m) - classes = inspect.getmembers(m, inspect.isclass) - type_class = None - for _, cl in classes: - if cl != PythonType and issubclass(cl, PythonType): - if type_class: - raise cdist.Error("Only one python type class is " - "supported, but at least two " - "found: {}".format((type_class, - cl, ))) - else: - type_class = cl + message_prefix = cdist_object.name env = self.env_py_type_manifest(cdist_object) type_obj = type_class(env=env, cdist_object=cdist_object, local=self.local, remote=None, diff --git a/cdist/core/pytypes.py b/cdist/core/pytypes.py index ae4164ec..9dfc8df9 100644 --- a/cdist/core/pytypes.py +++ b/cdist/core/pytypes.py @@ -4,6 +4,9 @@ import io import sys import re from cdist import message, Error +import importlib.util +import inspect +import cdist __all__ = ["PythonType", "Command", "command"] @@ -13,18 +16,20 @@ class PythonType: def __init__(self, env, cdist_object, local, remote, message_prefix=None): self.env = env self.cdist_object = cdist_object - self.object_id = cdist_object.object_id - self.object_name = cdist_object.name - self.cdist_type = cdist_object.cdist_type self.local = local self.remote = remote - self.object_path = cdist_object.absolute_path - self.type_path = cdist_object.cdist_type.absolute_path - self.explorer_path = os.path.join(self.object_path, 'explorer') - self.parameters = cdist_object.parameters - self.stdin_path = os.path.join(self.object_path, 'stdin') - self.log = logging.getLogger( - self.local.target_host[0] + ':' + self.object_name) + if self.cdist_object: + self.object_id = cdist_object.object_id + self.object_name = cdist_object.name + self.cdist_type = cdist_object.cdist_type + self.object_path = cdist_object.absolute_path + self.explorer_path = os.path.join(self.object_path, 'explorer') + self.type_path = cdist_object.cdist_type.absolute_path + self.parameters = cdist_object.parameters + self.stdin_path = os.path.join(self.object_path, 'stdin') + if self.local: + self.log = logging.getLogger( + self.local.target_host[0] + ':' + self.object_name) self.message_prefix = message_prefix self.message = None @@ -62,12 +67,6 @@ class PythonType: def die(self, msg): raise Error("{}: {}".format(self.cdist_object, msg)) - def type_manifest(self): - pass - - def type_gencode(self): - pass - def manifest(self, stdout=None, stderr=None): try: if self.message_prefix: @@ -123,6 +122,15 @@ class PythonType: return match return None + def get_args_parser(self): + pass + + def type_manifest(self): + pass + + def type_gencode(self): + pass + class Command: def __init__(self, name, *args, **kwargs): @@ -160,3 +168,23 @@ class Command: def command(name, *args, **kwargs): return Command(name, *args, **kwargs) + + +def get_pytype_class(cdist_type): + module_name = cdist_type.name + file_path = os.path.join(cdist_type.absolute_path, '__init__.py') + type_class = None + if os.path.isfile(file_path): + spec = importlib.util.spec_from_file_location(module_name, file_path) + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + classes = inspect.getmembers(m, inspect.isclass) + for _, cl in classes: + if cl != PythonType and issubclass(cl, PythonType): + if type_class: + raise cdist.Error( + "Only one python type class is supported, but at least" + " two found: {}".format((type_class, cl, ))) + else: + type_class = cl + return type_class diff --git a/cdist/emulator.py b/cdist/emulator.py index 4800e2a3..7dc24a83 100644 --- a/cdist/emulator.py +++ b/cdist/emulator.py @@ -30,6 +30,9 @@ import cdist from cdist import core from cdist import flock from cdist.core.manifest import Manifest +import cdist.util.python_type_util as pytype_util +from cdist.core.pytypes import get_pytype_class +import inspect class MissingRequiredEnvironmentVariableError(cdist.Error): @@ -100,7 +103,28 @@ class Emulator(object): def run(self): """Emulate type commands (i.e. __file and co)""" - self.commandline() + args_parser = None + if pytype_util.is_python_type(self.cdist_type): + type_class = get_pytype_class(self.cdist_type) + if type_class is not None: + # We only need to call parse_args so we need plain instance. + type_obj = type_class(env=None, cdist_object=None, local=None, + remote=None) + if (hasattr(type_obj, 'get_args_parser') and + inspect.ismethod(type_obj.get_args_parser)): + args_parser = type_obj.get_args_parser() + self.log.trace("Using python type argument parser") + print("Using python type argument parser") + if args_parser is None: + # Fallback to classic way. + args_parser = self.get_args_parser() + self.log.trace("Fallback to classic argument parser") + print("Fallback to classic argument parser") + else: + args_parser = self.get_args_parser() + self.log.trace("Using emulator classic argument parser") + print("Using emulator classic argument parser") + self.commandline(args_parser) self.init_object() # locking for parallel execution @@ -131,9 +155,7 @@ class Emulator(object): self.log = logging.getLogger(self.target_host[0]) - def commandline(self): - """Parse command line""" - + def get_args_parser(self): parser = argparse.ArgumentParser(add_help=False, argument_default=argparse.SUPPRESS) @@ -165,8 +187,10 @@ class Emulator(object): # If not singleton support one positional parameter if not self.cdist_type.is_singleton: parser.add_argument("object_id", nargs=1) + return parser - # And finally parse/verify parameter + def commandline(self, parser): + """Parse command line""" self.args = parser.parse_args(self.argv[1:]) self.log.trace('Args: %s' % self.args) diff --git a/docs/src/cdist-type.rst b/docs/src/cdist-type.rst index 3a4a0b13..388c3caf 100644 --- a/docs/src/cdist-type.rst +++ b/docs/src/cdist-type.rst @@ -542,6 +542,9 @@ in shell, since this is the code that is directly executed at target host. When writing python type you can extend **cdist.core.pytypes.PythonType** class. You need to implement the following methods: +* **get_args_parser**: implementation should return **argparse.ArgumentParser** and if + it is undefined or returned None then cdist falls back to classic type parameter + definition and argument parsing * **type_manifest**: implementation should yield **cdist.core.pytypes.** attribute function call result, or **yield from ()** if type does not use other types * **type_gencode**: implementation should return a string consisting of lines