diff --git a/src/ansible.cfg b/src/ansible.cfg index e27074e0c..3d1ae08f1 100644 --- a/src/ansible.cfg +++ b/src/ansible.cfg @@ -2,4 +2,5 @@ host_key_checking = False roles_path = ./roles filter_plugins = ./filter_plugins +callback_plugins = ./callback_plugins callback_result_format = yaml diff --git a/src/callback_plugins/foremanctl_features.py b/src/callback_plugins/foremanctl_features.py new file mode 100644 index 000000000..cefc55d15 --- /dev/null +++ b/src/callback_plugins/foremanctl_features.py @@ -0,0 +1,80 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.callback import CallbackBase +import pathlib +import yaml +import os + +BASE_DIR = pathlib.Path(__file__).parent.parent +STATE_DIR = pathlib.Path(os.environ.get('OBSAH_STATE', '.var/lib/foremanctl')) + +def load_yaml(path: pathlib.Path) -> dict: + try: + with path.open() as f: + return yaml.safe_load(f) or {} + except FileNotFoundError: + return {} + + +class CallbackModule(CallbackBase): + """Features listing callback.""" + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'foremanctl_features' + + def __init__(self): + super().__init__() + self.feature_metadata = self._load_features_metadata() + + def _load_features_metadata(self): + features_yaml = BASE_DIR / 'features.yaml' + return load_yaml(features_yaml) + + def v2_runner_on_ok(self, result): + if result._task.action in ('ansible.builtin.debug', 'debug'): + self._display_features() + + def _display_features(self): + params_yaml = STATE_DIR / "parameters.yaml" + params = load_yaml(params_yaml) + + flavor = params.get('flavor') + added_features = params.get('features', []) + + flavor_file = BASE_DIR / f"vars/flavors/{flavor}.yml" + flavor_config = load_yaml(flavor_file) + flavor_features = flavor_config.get('flavor_features', []) + + enabled_features = flavor_features + added_features + + enabled_list = [] + available_list = [] + + for name in sorted(self.feature_metadata.keys()): + meta = self.feature_metadata.get(name, {}) or {} + + if meta.get('internal', False): + continue + + description = meta.get('description', '') + + if name in enabled_features: + enabled_list.append((name, 'enabled', description)) + else: + available_list.append((name, 'available', description)) + + self._display.display(f"{'FEATURE':<25} {'STATE':<12} DESCRIPTION") + + for name, state, description in enabled_list: + self._display.display(f"{name:<25} {state:<12} {description}") + + for name, state, description in available_list: + self._display.display(f"{name:<25} {state:<12} {description}") + + total = len(enabled_list) + len(available_list) + self._display.display("") + self._display.display( + f"{total} features listed ({len(enabled_list)} enabled, {len(available_list)} available)." + ) diff --git a/src/features.py b/src/features.py new file mode 100644 index 000000000..f0d3e9211 --- /dev/null +++ b/src/features.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Display enabled and available features for Foreman deployment.""" +import yaml +import pathlib +import os + +BASE_DIR = pathlib.Path(__file__).resolve().parent +STATE_DIR = pathlib.Path(os.environ.get('OBSAH_STATE', '.var/lib/foremanctl')) + + +def load_yaml(path: pathlib.Path) -> dict: + """Load YAML file, return empty dict if not found.""" + try: + with path.open() as f: + return yaml.safe_load(f) or {} + except FileNotFoundError: + return {} + + +def main(): + features_yaml = BASE_DIR / "features.yaml" + all_features = load_yaml(features_yaml) + + # Load persisted parameters + params_yaml = STATE_DIR / "parameters.yaml" + params = load_yaml(params_yaml) + + flavor = params.get('flavor', 'katello') + added_features = params.get('features', []) + + # Load flavor configuration + flavor_file = BASE_DIR / f"vars/flavors/{flavor}.yml" + flavor_config = load_yaml(flavor_file) + flavor_features = flavor_config.get('flavor_features', []) + + enabled_features = flavor_features + added_features + + print(f"{'FEATURE':<25} {'STATE':<12} DESCRIPTION") + + # Separate enabled and available + enabled_list = [] + available_list = [] + + for name in sorted(all_features.keys()): + meta = all_features[name] or {} + + # Skip internal features + if meta.get('internal', False): + continue + + description = meta.get('description', '') + + if name in enabled_features: + enabled_list.append((name, 'enabled', description)) + else: + available_list.append((name, 'available', description)) + + for name, state, description in enabled_list: + print(f"{name:<25} {state:<12} {description}") + + for name, state, description in available_list: + print(f"{name:<25} {state:<12} {description}") + + print() + enabled_count = len(enabled_list) + total_count = len(enabled_list) + len(available_list) + print(f"{total_count} features listed ({enabled_count} enabled, {len(available_list)} available).") + + +if __name__ == "__main__": + main() diff --git a/src/playbooks/features/features.yaml b/src/playbooks/features/features.yaml new file mode 100644 index 000000000..8f43dcd5c --- /dev/null +++ b/src/playbooks/features/features.yaml @@ -0,0 +1,9 @@ +--- +- name: List features + hosts: quadlet + gather_facts: false + + tasks: + - name: List Enabled features + ansible.builtin.debug: + msg: "" diff --git a/src/playbooks/features/metadata.obsah.yaml b/src/playbooks/features/metadata.obsah.yaml new file mode 100644 index 000000000..cd8366444 --- /dev/null +++ b/src/playbooks/features/metadata.obsah.yaml @@ -0,0 +1,5 @@ +--- +help: | + List all enabled and available features + +stdout_callback: foremanctl_features