#!/usr/bin/python """Ansible module wrapping id-configurator. Applies an id-configurator configuration directly to this host's live root filesystem, exactly like the `id_configurator` role does, but as a native Ansible module: it imports id-configurator's own Python API in-process (instead of shelling out to the CLI through a temporary file), which also lets it honor Ansible's check mode - reporting which modules would be applied without writing anything. This module is a thin, additive wrapper: it does not change anything about id-configurator's own behavior (the `id_configurator` Python package, including its module registry and every individual module, is used exactly as-is - the same `Configuration`, `Context` and module registry the CLI itself uses). """ from __future__ import annotations from pathlib import Path from ansible.module_utils.basic import AnsibleModule DOCUMENTATION = r""" module: id_configurator short_description: Apply an id-configurator configuration to this host. description: - Assembles the given options into an id-configurator configuration and applies it directly to this host's live root filesystem, by importing id-configurator's own Python API in-process (no CLI subprocess, no temporary file). - The option names and structure mirror id_configurator.config.Configuration and id_configurator.config.ConfigurationsBlock one for one. The `id_configurator` Python package must already be installed on the target host, using the same Python interpreter Ansible uses there. - Supports check mode - reports which modules would be applied without writing anything. author: - InterDiode options: uuid: type: str required: true description: Configuration.uuid - a UUID identifying this configuration. organisation: type: str required: true description: Configuration.organisation. description: type: str default: "" description: Configuration.description. certificates: type: list elements: dict default: [] description: ConfigurationsBlock.certificates - root certificates to install. suboptions: name: type: str required: true description: A short, unique name for this entry. content: type: str required: true description: PEM-encoded certificate content. apt: type: dict default: {} description: ConfigurationsBlock.apt. suboptions: sources: type: list elements: dict default: [] description: DEB822 APT sources (AptSource). suboptions: name: type: str required: true description: A short, unique name for this source. types: type: list elements: str default: [deb] description: DEB822 "Types" field. uris: type: list elements: str required: true description: DEB822 "URIs" field. suites: type: list elements: str required: true description: DEB822 "Suites" field. components: type: list elements: str default: [] description: DEB822 "Components" field. architectures: type: list elements: str default: [] description: DEB822 "Architectures" field. languages: type: list elements: str default: [] description: DEB822 "Languages" field. targets: type: list elements: str default: [] description: DEB822 "Targets" field. enabled: type: bool default: true description: DEB822 "Enabled" field. trusted: type: bool description: DEB822 "Trusted" field. signed_by: type: str description: - ASCII-armored content of a GPG public key to trust this source with. Folded inline as a multi-line Signed-By field (no separate keyring file or gpg binary needed). Takes precedence over signed_by_file if both are set. signed_by_file: type: str description: - Path to a keyring already expected to exist on the target system, used verbatim as the rendered Signed-By field. Ignored if signed_by is also set. pdiffs: type: bool description: DEB822 "PDiffs" field. by_hash: type: bool description: DEB822 "By-Hash" field. allow_insecure: type: bool description: DEB822 "Allow-Insecure" field. allow_weak: type: bool description: DEB822 "Allow-Weak" field. allow_downgrade_to_insecure: type: bool description: DEB822 "Allow-Downgrade-To-Insecure" field. check_valid_until: type: bool description: DEB822 "Check-Valid-Until" field. valid_until_min: type: str description: DEB822 "Valid-Until-Min" field. valid_until_max: type: str description: DEB822 "Valid-Until-Max" field. check_date: type: bool description: DEB822 "Check-Date" field. date_max_future: type: str description: DEB822 "Date-Max-Future" field. description: type: str description: DEB822 "Description" field. apk: type: dict default: {} description: ConfigurationsBlock.apk. suboptions: repositories: type: list elements: dict default: [] description: Alpine APK repositories (ApkRepository). suboptions: url: type: str required: true description: Repository URL, e.g. ending in /main or /community. key: type: str description: The signing key's name, if any (see NamedContent). yum: type: dict default: {} description: ConfigurationsBlock.yum. suboptions: repositories: type: list elements: dict default: [] description: YUM/DNF repositories (YumRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. index_url: type: str required: true description: The repository's baseurl. gpg_url: type: str description: The repository's gpgkey URL. keys: type: list elements: dict default: [] description: GPG keys imported for the repositories above (NamedContent). suboptions: name: type: str required: true description: A short, unique name for this entry. content: type: str required: true description: PEM-encoded/ASCII-armored key content. python: type: dict default: {} description: ConfigurationsBlock.python. suboptions: repositories: type: list elements: dict default: [] description: Python package indexes (PythonRepository). suboptions: name: type: str required: true description: A short, unique name for this index. index_url: type: str required: true description: The package index's URL. java: type: dict default: {} description: ConfigurationsBlock.java. suboptions: repositories: type: list elements: dict default: [] description: Maven-compatible repositories (JavaRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. php: type: dict default: {} description: ConfigurationsBlock.php. suboptions: repositories: type: list elements: dict default: [] description: Composer repositories (PhpRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. npm: type: dict default: {} description: ConfigurationsBlock.npm. suboptions: repositories: type: list elements: dict default: [] description: npm registries (NpmRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. rust: type: dict default: {} description: ConfigurationsBlock.rust. suboptions: repositories: type: list elements: dict default: [] description: Cargo registries (RustRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. go: type: dict default: {} description: ConfigurationsBlock.go. suboptions: repositories: type: list elements: dict default: [] description: Go module proxies (GoRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. ruby: type: dict default: {} description: ConfigurationsBlock.ruby. suboptions: repositories: type: list elements: dict default: [] description: RubyGems sources (RubyRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. nuget: type: dict default: {} description: ConfigurationsBlock.nuget. suboptions: repositories: type: list elements: dict default: [] description: NuGet package sources (NuGetRepository). suboptions: name: type: str required: true description: A short, unique name for this repository. url: type: str required: true description: The repository's URL. """ EXAMPLES = r""" - name: Apply an id-configurator configuration to this host id_configurator: uuid: 12345678-1234-5678-1234-567812345678 organisation: InterDiode certificates: - name: Company Root CA content: "{{ lookup('file', 'files/company-root-ca.pem') }}" apt: sources: - name: internal uris: ["https://apt.internal.example/"] suites: ["stable"] """ RETURN = r""" applied_modules: description: Names of the id-configurator modules that were (or, in check mode, would be) applied, in registration order. returned: always type: list elements: str sample: [certificates, apt] """ # Every key here must match one of ConfigurationsBlock's own field names # (id_configurator/config.py) - see the module docstring above. _CONFIGURATIONS_BLOCK_FIELDS = ( "certificates", "apt", "apk", "yum", "python", "java", "php", "npm", "rust", "go", "ruby", "nuget", ) _NAMED_CONTENT_OPTIONS = { "name": {"type": "str", "required": True}, "content": {"type": "str", "required": True}, } _NAME_URL_OPTIONS = { "name": {"type": "str", "required": True}, "url": {"type": "str", "required": True}, } def _name_url_repository_config() -> dict: """Argument spec for a `{repositories: [{name, url}]}` config block, shared by java/php/npm/rust/go/ruby/nuget.""" return { "type": "dict", "default": {}, "options": { "repositories": { "type": "list", "elements": "dict", "default": [], "options": dict(_NAME_URL_OPTIONS), }, }, } ARGUMENT_SPEC = { "uuid": {"type": "str", "required": True}, "organisation": {"type": "str", "required": True}, "description": {"type": "str", "default": ""}, "certificates": { "type": "list", "elements": "dict", "default": [], "options": dict(_NAMED_CONTENT_OPTIONS), }, "apt": { "type": "dict", "default": {}, "options": { "sources": { "type": "list", "elements": "dict", "default": [], "options": { "name": {"type": "str", "required": True}, "types": {"type": "list", "elements": "str", "default": ["deb"]}, "uris": {"type": "list", "elements": "str", "required": True}, "suites": {"type": "list", "elements": "str", "required": True}, "components": {"type": "list", "elements": "str", "default": []}, "architectures": { "type": "list", "elements": "str", "default": [], }, "languages": {"type": "list", "elements": "str", "default": []}, "targets": {"type": "list", "elements": "str", "default": []}, "enabled": {"type": "bool", "default": True}, "trusted": {"type": "bool"}, "signed_by": {"type": "str"}, "signed_by_file": {"type": "str"}, "pdiffs": {"type": "bool"}, "by_hash": {"type": "bool"}, "allow_insecure": {"type": "bool"}, "allow_weak": {"type": "bool"}, "allow_downgrade_to_insecure": {"type": "bool"}, "check_valid_until": {"type": "bool"}, "valid_until_min": {"type": "str"}, "valid_until_max": {"type": "str"}, "check_date": {"type": "bool"}, "date_max_future": {"type": "str"}, "description": {"type": "str"}, }, }, }, }, "apk": { "type": "dict", "default": {}, "options": { "repositories": { "type": "list", "elements": "dict", "default": [], "options": { "url": {"type": "str", "required": True}, "key": {"type": "str"}, }, }, }, }, "yum": { "type": "dict", "default": {}, "options": { "repositories": { "type": "list", "elements": "dict", "default": [], "options": { "name": {"type": "str", "required": True}, "index_url": {"type": "str", "required": True}, "gpg_url": {"type": "str"}, }, }, "keys": { "type": "list", "elements": "dict", "default": [], "options": dict(_NAMED_CONTENT_OPTIONS), }, }, }, "python": { "type": "dict", "default": {}, "options": { "repositories": { "type": "list", "elements": "dict", "default": [], "options": { "name": {"type": "str", "required": True}, "index_url": {"type": "str", "required": True}, }, }, }, }, "java": _name_url_repository_config(), "php": _name_url_repository_config(), "npm": _name_url_repository_config(), "rust": _name_url_repository_config(), "go": _name_url_repository_config(), "ruby": _name_url_repository_config(), "nuget": _name_url_repository_config(), } try: import id_configurator.modules # noqa: F401 (registers the built-in modules) from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.base import all_modules HAS_ID_CONFIGURATOR = True ID_CONFIGURATOR_IMPORT_ERROR = None except ImportError as exc: HAS_ID_CONFIGURATOR = False ID_CONFIGURATOR_IMPORT_ERROR = exc def _build_configuration_data(params: dict) -> dict: return { "uuid": params["uuid"], "organisation": params["organisation"], "description": params["description"], "configurations": { field: params[field] for field in _CONFIGURATIONS_BLOCK_FIELDS }, } def _apply_configuration(configuration, check_mode: bool) -> list[str]: """Apply `configuration` to this host's live root filesystem, returning the names of every module that was (or, in check mode, would be) applied. Uses id-configurator's own module registry and `Context` unchanged - this is a thin caller, not a reimplementation.""" root = Path("/") context = Context(config=configuration, dst=root, src=root) applied_modules = [] for module_cls in all_modules(): config_module = module_cls() if config_module.is_relevant(context): applied_modules.append(config_module.name) if not check_mode: config_module.apply(context) return applied_modules def main() -> None: module = AnsibleModule(argument_spec=ARGUMENT_SPEC, supports_check_mode=True) if not HAS_ID_CONFIGURATOR: module.fail_json( msg=( "id_configurator is not importable with this host's Python " f"interpreter: {ID_CONFIGURATOR_IMPORT_ERROR}" ) ) return try: configuration = Configuration.model_validate( _build_configuration_data(module.params) ) except Exception as exc: # pydantic.ValidationError, kept generic on purpose module.fail_json(msg=f"invalid id-configurator configuration: {exc}") return applied_modules = _apply_configuration(configuration, module.check_mode) module.exit_json(changed=bool(applied_modules), applied_modules=applied_modules) if __name__ == "__main__": main()