0 UˆÇÕ ƒuçÔÀÒ¶U"""ansible enables running on first boot either ansible-pull""" import abc import logging import os import re import sys import sysconfig from copy import deepcopy from typing import Optional from cloudinit import lifecycle, subp from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema from cloudinit.distros import ALL_DISTROS, Distro from cloudinit.settings import PER_INSTANCE from cloudinit.util import get_cfg_by_path meta: MetaSchema = { "id": "cc_ansible", "frequency": PER_INSTANCE, "distros": [ALL_DISTROS], "activate_by_schema_keys": ["ansible"], } LOG = logging.getLogger(__name__) CFG_OVERRIDE = "ansible_config" class AnsiblePull(abc.ABC): def __init__(self, distro: Distro): self.cmd_pull = ["ansible-pull"] self.cmd_version = ["ansible-pull", "--version"] self.distro = distro self.env = {} self.run_user: Optional[str] = None # some ansible modules directly reference os.environ["HOME"] # and cloud-init might not have that set, default: /root self.env["HOME"] = os.environ.get("HOME", "/root") def get_version(self) -> Optional[lifecycle.Version]: stdout, _ = self.do_as(self.cmd_version) first_line = stdout.splitlines().pop(0) matches = re.search(r"([\d\.]+)", first_line) if matches: version = matches.group(0) return lifecycle.Version.from_str(version) return None def pull(self, *args) -> str: stdout, _ = self.do_as([*self.cmd_pull, *args]) return stdout def check_deps(self): if not self.is_installed(): raise ValueError("command: ansible is not installed") def do_as(self, command: list, **kwargs): if not self.run_user: return self.subp(command, **kwargs) return self.distro.do_as(command, self.run_user, **kwargs) def subp(self, command, **kwargs): return subp.subp(command, update_env=self.env, **kwargs) @abc.abstractmethod def is_installed(self): pass @abc.abstractmethod def install(self, pkg_name: str): pass class AnsiblePullPip(AnsiblePull): def __init__(self, distro: Distro, user: Optional[str]): super().__init__(distro) self.run_user = user # Add pip install site to PATH user_base, _ = self.do_as( [sys.executable, "-c", "'import site; print(site.getuserbase())'"] ) ansible_path = f"{user_base}/bin/" old_path = self.env.get("PATH") if old_path: self.env["PATH"] = ":".join([old_path, ansible_path]) else: self.env["PATH"] = ansible_path def install(self, pkg_name: str): """should cloud-init grow an interface for non-distro package managers? this seems reusable """ if not self.is_installed(): # bootstrap pip if required try: import pip # noqa: F401 except ImportError: self.distro.install_packages([self.distro.pip_package_name]) cmd = [ sys.executable, "-m", "pip", "install", ] if os.path.exists( os.path.join( sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED" ) ): cmd.append("--break-system-packages") if self.run_user: cmd.append("--user") self.do_as([*cmd, "--upgrade", "pip"]) self.do_as([*cmd, pkg_name]) def is_installed(self) -> bool: stdout, _ = self.do_as([sys.executable, "-m", "pip", "list"]) return "ansible" in stdout class AnsiblePullDistro(AnsiblePull): def install(self, pkg_name: str): if not self.is_installed(): self.distro.install_packages([pkg_name]) def is_installed(self) -> bool: return bool(subp.which("ansible")) def handle(name