mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
204d648477
Signed-off-by: d9pouces <github@19pouces.net>
654 lines
20 KiB
Python
654 lines
20 KiB
Python
#!/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:
|
|
version:
|
|
type: str
|
|
default: "1.0"
|
|
description: Configuration.version - the configuration format's own schema version.
|
|
uuid:
|
|
type: str
|
|
description: Configuration.uuid - a UUID identifying this configuration, optional.
|
|
organisation:
|
|
type: str
|
|
description: Configuration.organisation, optional.
|
|
description:
|
|
type: str
|
|
description: Configuration.description, optional.
|
|
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.
|
|
uri_to_replace:
|
|
type: str
|
|
description:
|
|
- fnmatch pattern checked against the URIs already present in
|
|
the machine's existing apt configuration. If set, this
|
|
source is only applied when the pattern matches at least
|
|
one of them; otherwise it is skipped entirely.
|
|
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).
|
|
url_to_replace:
|
|
type: str
|
|
description:
|
|
- fnmatch pattern checked against the URLs already active in
|
|
the machine's existing /etc/apk/repositories. If set, this
|
|
repository is only applied when the pattern matches at
|
|
least one of them; otherwise it is skipped entirely.
|
|
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.
|
|
index_url_to_replace:
|
|
type: str
|
|
description:
|
|
- fnmatch pattern checked against the baseurl of every
|
|
section of every existing .repo file. If set, this
|
|
repository is only applied when the pattern matches at
|
|
least one of them; otherwise it is skipped entirely.
|
|
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 = {
|
|
"version": {"type": "str", "default": "1.0"},
|
|
"uuid": {"type": "str"},
|
|
"organisation": {"type": "str"},
|
|
"description": {"type": "str"},
|
|
"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"},
|
|
"uri_to_replace": {"type": "str"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"apk": {
|
|
"type": "dict",
|
|
"default": {},
|
|
"options": {
|
|
"repositories": {
|
|
"type": "list",
|
|
"elements": "dict",
|
|
"default": [],
|
|
"options": {
|
|
"url": {"type": "str", "required": True},
|
|
"key": {"type": "str"},
|
|
"url_to_replace": {"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"},
|
|
"index_url_to_replace": {"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 {
|
|
"version": params["version"],
|
|
"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()
|