Big restructuring

This commit is contained in:
2025-05-20 00:13:45 +02:00
parent efe994a4c5
commit f748f9cef1
44 changed files with 697 additions and 469 deletions

View File

@@ -0,0 +1,80 @@
from ansible.errors import AnsibleFilterError
import os
import sys
import yaml
class FilterModule(object):
def filters(self):
return {
'applications_if_group_and_deps': self.applications_if_group_and_deps,
}
def applications_if_group_and_deps(self, applications, group_names):
"""
Return only those applications whose key is either:
1) directly in group_names, or
2) the application_id of any role reachable (recursively)
from any group in group_names via meta/dependencies.
Expects:
- applications: dict mapping application_id → config
- group_names: list of active role names
"""
# validate inputs
if not isinstance(applications, dict):
raise AnsibleFilterError(f"Expected applications as dict, got {type(applications).__name__}")
if not isinstance(group_names, (list, tuple)):
raise AnsibleFilterError(f"Expected group_names as list/tuple, got {type(group_names).__name__}")
# locate roles directory (assume plugin sits in filter_plugins/)
plugin_dir = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
roles_dir = os.path.join(project_root, 'roles')
# recursively collect all roles reachable from the given groups
def collect_roles(role, seen):
if role in seen:
return
seen.add(role)
meta_file = os.path.join(roles_dir, role, 'meta', 'main.yml')
if not os.path.isfile(meta_file):
return
try:
with open(meta_file) as f:
meta = yaml.safe_load(f) or {}
except Exception:
return
for dep in meta.get('dependencies', []):
if isinstance(dep, str):
dep_name = dep
elif isinstance(dep, dict):
dep_name = dep.get('role') or dep.get('name')
else:
continue
collect_roles(dep_name, seen)
included_roles = set()
for grp in group_names:
collect_roles(grp, included_roles)
# gather application_ids from those roles
included_app_ids = set()
for role in included_roles:
vars_file = os.path.join(roles_dir, role, 'vars', 'main.yml')
if not os.path.isfile(vars_file):
continue
try:
with open(vars_file) as f:
vars_data = yaml.safe_load(f) or {}
except Exception:
continue
app_id = vars_data.get('application_id')
if isinstance(app_id, str) and app_id:
included_app_ids.add(app_id)
# build filtered result: include any application whose key is in group_names or in included_app_ids
result = {}
for app_key, cfg in applications.items():
if app_key in group_names or app_key in included_app_ids:
result[app_key] = cfg
return result

View File

@@ -5,33 +5,40 @@ class FilterModule(object):
def filters(self):
return {'generate_base_sld_domains': self.generate_base_sld_domains}
def generate_base_sld_domains(self, domains_dict, redirect_mappings):
def generate_base_sld_domains(self, domains_list):
"""
Flatten domains_dict und redirect_mappings, extrahiere SLDs (z.B. example.com),
dedupe und sortiere.
Given a list of hostnames, extract the second-level domain (SLD.TLD) for any hostname
with two or more labels, return single-label hostnames as-is, and reject IPs,
empty or malformed strings, and non-strings. Deduplicate and sort.
"""
def _flatten(domains):
flat = []
for v in (domains or {}).values():
if isinstance(v, str):
flat.append(v)
elif isinstance(v, list):
flat.extend(v)
elif isinstance(v, dict):
flat.extend(v.values())
return flat
if not isinstance(domains_list, list):
raise AnsibleFilterError(
f"generate_base_sld_domains expected a list, got {type(domains_list).__name__}"
)
try:
flat = _flatten(domains_dict)
for mapping in redirect_mappings or []:
src = mapping.get('source')
if isinstance(src, str):
flat.append(src)
elif isinstance(src, list):
flat.extend(src)
ip_pattern = re.compile(r'^\d{1,3}(?:\.\d{1,3}){3}$')
results = set()
pattern = re.compile(r'^(?:.*\.)?([^.]+\.[^.]+)$')
slds = {m.group(1) for d in flat if (m := pattern.match(d))}
return sorted(slds)
except Exception as exc:
raise AnsibleFilterError(f"generate_base_sld_domains failed: {exc}")
for hostname in domains_list:
# type check
if not isinstance(hostname, str):
raise AnsibleFilterError(f"Invalid domain entry (not a string): {hostname!r}")
# malformed or empty
if not hostname or hostname.startswith('.') or hostname.endswith('.') or '..' in hostname:
raise AnsibleFilterError(f"Invalid domain entry (malformed): {hostname!r}")
# IP addresses disallowed
if ip_pattern.match(hostname):
raise AnsibleFilterError(f"IP addresses not allowed: {hostname!r}")
# single-label hostnames
labels = hostname.split('.')
if len(labels) == 1:
results.add(hostname)
else:
# always keep only the last two labels (SLD.TLD)
sld = ".".join(labels[-2:])
results.add(sld)
return sorted(results)

View File

@@ -1,97 +0,0 @@
from ansible.errors import AnsibleFilterError
import sys
import os
import yaml
class FilterModule(object):
def filters(self):
return {
"add_domain_if_group": self.add_domain_if_group,
}
@staticmethod
def add_domain_if_group(domains_dict, domain_key, domain_value, group_names):
"""
Add {domain_key: domain_value} to domains_dict if either:
1) domain_key is in group_names (direct inclusion), or
2) domain_key is among collected application_id values of roles
reachable from any group in group_names via recursive dependencies.
Parameters:
domains_dict: existing dict of domains
domain_key: name of the application to check
domain_value: domain or dict/list of domains to assign
group_names: list of active group (role/application) names
"""
try:
result = dict(domains_dict)
# Direct group match: if the application name itself is in group_names
if domain_key in group_names:
result[domain_key] = domain_value
return result
# Determine plugin directory based on filter plugin module if available
plugin_dir = None
for module in sys.modules.values():
fm = getattr(module, 'FilterModule', None)
if fm is not None:
try:
# Access staticmethod, compare underlying function
if getattr(fm, 'add_domain_if_group') is DomainFilterUtil.add_domain_if_group:
plugin_dir = os.path.dirname(module.__file__)
break
except Exception:
continue
if plugin_dir:
# The plugin_dir is the filter_plugins directory; project_root is one level up
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
else:
# Fallback: locate project root relative to this utility file
plugin_dir = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(plugin_dir, '..'))
roles_dir = os.path.join(project_root, 'roles')
# Collect all roles reachable from the active groups
def collect_roles(role_name, collected):
if role_name in collected:
return
collected.add(role_name)
meta_path = os.path.join(roles_dir, role_name, 'meta', 'main.yml')
if os.path.isfile(meta_path):
with open(meta_path) as f:
meta = yaml.safe_load(f) or {}
for dep in meta.get('dependencies', []):
if isinstance(dep, str):
dep_name = dep
elif isinstance(dep, dict):
dep_name = dep.get('role') or dep.get('name')
else:
continue
collect_roles(dep_name, collected)
included_roles = set()
for grp in group_names:
collect_roles(grp, included_roles)
# Gather application_ids from each included role
app_ids = set()
for role in included_roles:
vars_main = os.path.join(roles_dir, role, 'vars', 'main.yml')
if os.path.isfile(vars_main):
with open(vars_main) as f:
vars_data = yaml.safe_load(f) or {}
app_id = vars_data.get('application_id')
if app_id:
app_ids.add(app_id)
# Indirect inclusion: match by application_id
if domain_key in app_ids:
result[domain_key] = domain_value
return result
except Exception as exc:
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")

View File

@@ -0,0 +1,122 @@
import os
import yaml
import re
from ansible.errors import AnsibleFilterError
# in-memory cache: application_id → (parsed_yaml, is_nested)
_cfg_cache = {}
def load_configuration(application_id, key):
if not isinstance(key, str):
raise AnsibleFilterError("Key must be a dotted-string, e.g. 'features.matomo'")
# locate roles/
here = os.path.dirname(__file__)
root = os.path.abspath(os.path.join(here, '..'))
roles_dir = os.path.join(root, 'roles')
if not os.path.isdir(roles_dir):
raise AnsibleFilterError(f"Roles directory not found at {roles_dir}")
# first time? load & cache
if application_id not in _cfg_cache:
config_path = None
# 1) primary: vars/main.yml declares it
for role in os.listdir(roles_dir):
mv = os.path.join(roles_dir, role, 'vars', 'main.yml')
if os.path.exists(mv):
try:
md = yaml.safe_load(open(mv)) or {}
except Exception:
md = {}
if md.get('application_id') == application_id:
cf = os.path.join(roles_dir, role, 'vars', 'configuration.yml')
if not os.path.exists(cf):
raise AnsibleFilterError(
f"Role '{role}' declares '{application_id}' but missing configuration.yml"
)
config_path = cf
break
# 2) fallback nested
if config_path is None:
for role in os.listdir(roles_dir):
cf = os.path.join(roles_dir, role, 'vars', 'configuration.yml')
if not os.path.exists(cf):
continue
try:
dd = yaml.safe_load(open(cf)) or {}
except Exception:
dd = {}
if isinstance(dd, dict) and application_id in dd:
config_path = cf
break
# 3) fallback flat
if config_path is None:
for role in os.listdir(roles_dir):
cf = os.path.join(roles_dir, role, 'vars', 'configuration.yml')
if not os.path.exists(cf):
continue
try:
dd = yaml.safe_load(open(cf)) or {}
except Exception:
dd = {}
# flat style: dict with all non-dict values
if isinstance(dd, dict) and not any(isinstance(v, dict) for v in dd.values()):
config_path = cf
break
if config_path is None:
return None
# parse once
try:
parsed = yaml.safe_load(open(config_path)) or {}
except Exception as e:
raise AnsibleFilterError(f"Error loading configuration.yml at {config_path}: {e}")
# detect nested vs flat
is_nested = isinstance(parsed, dict) and (application_id in parsed)
_cfg_cache[application_id] = (parsed, is_nested)
parsed, is_nested = _cfg_cache[application_id]
# pick base entry
entry = parsed[application_id] if is_nested else parsed
# resolve dotted key
key_parts = key.split('.')
for part in key_parts:
# Check if part has an index (e.g., domains.canonical[0])
match = re.match(r'([^\[]+)\[([0-9]+)\]', part)
if match:
part, index = match.groups()
index = int(index)
if isinstance(entry, dict) and part in entry:
entry = entry[part]
# Check if entry is a list and access the index
if isinstance(entry, list) and 0 <= index < len(entry):
entry = entry[index]
else:
raise AnsibleFilterError(
f"Index '{index}' out of range for key '{part}' in application '{application_id}'"
)
else:
raise AnsibleFilterError(
f"Key '{part}' not found under application '{application_id}'"
)
else:
if isinstance(entry, dict) and part in entry:
entry = entry[part]
else:
raise AnsibleFilterError(
f"Key '{part}' not found under application '{application_id}'"
)
return entry
class FilterModule(object):
def filters(self):
return {'load_configuration': load_configuration}

View File

@@ -0,0 +1,42 @@
# filter_plugins/merge_mapping.py
from ansible.errors import AnsibleFilterError
def merge_mapping(list1, list2, key_name='source'):
"""
Merge two lists of dicts on a given key.
- list1, list2: each must be a List[Dict]
- key_name: the field to match on
If both lists contain an item with the same key_name value,
their dictionaries are merged (fields from list2 overwrite or add to list1).
"""
if not isinstance(list1, list) or not isinstance(list2, list):
raise AnsibleFilterError("merge_mapping expects two lists")
merged = {}
# First, copy items from list1
for item in list1:
if key_name not in item:
raise AnsibleFilterError(f"Item {item} is missing the key '{key_name}'")
merged[item[key_name]] = item.copy()
# Then merge in items from list2
for item in list2:
if key_name not in item:
raise AnsibleFilterError(f"Item {item} is missing the key '{key_name}'")
k = item[key_name]
if k in merged:
# update will overwrite existing fields or add new ones
merged[k].update(item)
else:
merged[k] = item.copy()
# Return as a list of dicts again
return list(merged.values())
class FilterModule(object):
def filters(self):
return {
'merge_mapping': merge_mapping,
}

View File

@@ -1,37 +0,0 @@
# roles/<your-role>/filter_plugins/redirect_filters.py
from ansible.errors import AnsibleFilterError
class FilterModule(object):
"""
Custom filters for redirect domain mappings
"""
def filters(self):
return {
"add_redirect_if_group": self.add_redirect_if_group,
}
@staticmethod
def add_redirect_if_group(redirect_list, group, source, target, group_names):
"""
Append {"source": source, "target": target} to *redirect_list*
**only** if *group* is contained in *group_names*.
Usage in Jinja:
{{ redirect_list
| add_redirect_if_group('lam',
'ldap.' ~ primary_domain,
domains | get_domain('lam'),
group_names) }}
"""
try:
# Make a copy so we dont mutate the original list in place
redirects = list(redirect_list)
if group in group_names:
redirects.append({"source": source, "target": target})
return redirects
except Exception as exc:
raise AnsibleFilterError(f"add_redirect_if_group failed: {exc}")