From 08d3e559f8f1ffdce209e0c1daa60c7d4a673e1d Mon Sep 17 00:00:00 2001 From: Paul Farault Date: Thu, 18 Jul 2024 12:14:00 +0200 Subject: [PATCH] feat: lazy load ansible imports --- tdp/cli/commands/default_diff.py | 4 +- tdp/core/ansible_loader.py | 102 +++++++++++++++++++++++++++++++ tdp/core/inventory_reader.py | 34 +++-------- tdp/core/variables/variables.py | 18 ++++-- 4 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 tdp/core/ansible_loader.py diff --git a/tdp/cli/commands/default_diff.py b/tdp/cli/commands/default_diff.py index 5401bd36..c8d677e1 100644 --- a/tdp/cli/commands/default_diff.py +++ b/tdp/cli/commands/default_diff.py @@ -10,9 +10,9 @@ from typing import TYPE_CHECKING, Optional import click -from ansible.utils.vars import merge_hash from tdp.cli.params import collections_option, vars_option +from tdp.core.ansible_loader import AnsibleLoader from tdp.core.constants import DEFAULT_VARS_DIRECTORY_NAME from tdp.core.variables import ClusterVariables, Variables @@ -73,7 +73,7 @@ def service_diff(collections, service): with Variables(default_service_vars_filepath).open( "r" ) as default_variables: - default_service_varfile = merge_hash( + default_service_varfile = AnsibleLoader.load_merge_hash()( default_service_varfile, default_variables ) diff --git a/tdp/core/ansible_loader.py b/tdp/core/ansible_loader.py new file mode 100644 index 00000000..cf130f32 --- /dev/null +++ b/tdp/core/ansible_loader.py @@ -0,0 +1,102 @@ +# Copyright 2022 TOSIT.IO +# SPDX-License-Identifier: Apache-2.0 + + +class AnsibleLoader: + """Lazy loader for Ansible classes and functions. + + This class is required as ansible automatically generate a config when imported. + """ + + _merge_hash = None + _from_yaml = None + _AnsibleDumper = None + _InventoryCLI = None + _InventoryReader = None + _InventoryManager = None + _CustomInventoryCLI = None + + @classmethod + def load_merge_hash(cls): + """Load the merge_hash function from ansible.""" + if cls._merge_hash is None: + from ansible.utils.vars import merge_hash + + cls._merge_hash = merge_hash + + return cls._merge_hash + + @classmethod + def load_from_yaml(cls): + """Load the from_yaml function from ansible.""" + if cls._from_yaml is None: + from ansible.parsing.utils.yaml import from_yaml + + cls._from_yaml = from_yaml + + return cls._from_yaml + + @classmethod + def load_AnsibleDumper(cls): + """Load the AnsibleDumper class from ansible.""" + if cls._AnsibleDumper is None: + from ansible.parsing.yaml.dumper import AnsibleDumper + + cls._AnsibleDumper = AnsibleDumper + + return cls._AnsibleDumper + + @classmethod + def load_InventoryCLI(cls): + """Load the InventoryCLI class from ansible.""" + if cls._InventoryCLI is None: + from ansible.cli.inventory import InventoryCLI + + cls._InventoryCLI = InventoryCLI + + return cls._InventoryCLI + + @classmethod + def load_InventoryReader(cls): + """Load the InventoryReader class from ansible.""" + if cls._InventoryReader is None: + from tdp.core.inventory_reader import InventoryReader + + cls._InventoryReader = InventoryReader + + return cls._InventoryReader + + @classmethod + def load_InventoryManager(cls): + """Load the InventoryManager class from ansible.""" + if cls._InventoryManager is None: + from ansible.inventory.manager import InventoryManager + + cls._InventoryManager = InventoryManager + + return cls._InventoryManager + + @classmethod + def get_CustomInventoryCLI(cls): + if cls._CustomInventoryCLI is None: + + class CustomInventoryCLI(cls.load_InventoryCLI()): + """Represent a custom Ansible inventory CLI which does nothing. + This is used to load inventory files with Ansible code. + """ + + def __init__(self): + super().__init__(["program", "--list"]) + # "run" must be called from CLI (the parent of InventoryCLI), to + # initialize context (reading ansible.cfg for example). + super(cls.load_InventoryCLI(), self).run() + # Get InventoryManager instance + _, self.inventory, _ = self._play_prereqs() + + # Avoid call InventoryCLI "run", we do not need to run InventoryCLI + def run(self): + pass + + cls._CustomInventoryCLI = CustomInventoryCLI() + + return cls._CustomInventoryCLI diff --git a/tdp/core/inventory_reader.py b/tdp/core/inventory_reader.py index 7e58041c..df899818 100644 --- a/tdp/core/inventory_reader.py +++ b/tdp/core/inventory_reader.py @@ -1,11 +1,11 @@ # Copyright 2022 TOSIT.IO # SPDX-License-Identifier: Apache-2.0 -from typing import Optional, TextIO +from typing import TYPE_CHECKING, Optional, TextIO import yaml -from ansible.cli.inventory import InventoryCLI -from ansible.inventory.manager import InventoryManager + +from tdp.core.ansible_loader import AnsibleLoader try: from yaml import CLoader as Loader @@ -13,33 +13,17 @@ from yaml import Loader -# From ansible/cli/inventory.py -class _CustomInventoryCLI(InventoryCLI): - """Represent a custom Ansible inventory CLI which does nothing. - This is used to load inventory files with Ansible code. - """ - - def __init__(self): - super().__init__(["program", "--list"]) - # "run" must be called from CLI (the parent of InventoryCLI), to - # initialize context (reading ansible.cfg for example). - super(InventoryCLI, self).run() - # Get InventoryManager instance - _, self.inventory, _ = self._play_prereqs() - - # Avoid call InventoryCLI "run", we do not need to run InventoryCLI - def run(self): - pass - - -custom_inventory_cli_instance = _CustomInventoryCLI() +if TYPE_CHECKING: + from ansible.inventory.manager import InventoryManager class InventoryReader: """Represent an Ansible inventory reader.""" - def __init__(self, inventory: Optional[InventoryManager] = None): - self.inventory = inventory or custom_inventory_cli_instance.inventory + def __init__(self, inventory: Optional["InventoryManager"] = None): + if inventory is None: + inventory = AnsibleLoader.get_CustomInventoryCLI().inventory + self.inventory = inventory def get_hosts(self, *args, **kwargs) -> list[str]: """Takes a pattern or list of patterns and returns a list of matching diff --git a/tdp/core/variables/variables.py b/tdp/core/variables/variables.py index fa3ba6bb..7a6afbce 100644 --- a/tdp/core/variables/variables.py +++ b/tdp/core/variables/variables.py @@ -10,10 +10,8 @@ from weakref import proxy import yaml -from ansible.parsing.utils.yaml import from_yaml -from ansible.parsing.yaml.dumper import AnsibleDumper -from ansible.utils.vars import merge_hash +from tdp.core.ansible_loader import AnsibleLoader from tdp.core.types import PathLike @@ -90,7 +88,7 @@ def merge(self, mapping: MutableMapping) -> None: Args: mapping: Mapping to merge. """ - self._content = merge_hash(self._content, mapping) + self._content = AnsibleLoader.load_merge_hash()(self._content, mapping) def __getitem__(self, key): return self._content.__getitem__(key) @@ -131,7 +129,10 @@ def __init__(self, path: Path, mode: Optional[str] = None): self._file_path = path self._file_descriptor = open(self._file_path, mode or "r+") # Initialize the content of the variables file - super().__init__(content=from_yaml(self._file_descriptor) or {}, name=path.name) + super().__init__( + content=AnsibleLoader.load_from_yaml()(self._file_descriptor) or {}, + name=path.name, + ) def __enter__(self) -> _VariablesIOWrapper: return proxy(self) @@ -152,7 +153,12 @@ def _flush_on_disk(self) -> None: # Write the content of the variables file on disk self._file_descriptor.seek(0) self._file_descriptor.write( - yaml.dump(self._content, Dumper=AnsibleDumper, sort_keys=False, width=1000) + yaml.dump( + self._content, + Dumper=AnsibleLoader.load_AnsibleDumper(), + sort_keys=False, + width=1000, + ) ) self._file_descriptor.truncate() self._file_descriptor.flush()