mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Big restructuring
This commit is contained in:
80
filter_plugins/applications_if_group_and_deps.py
Normal file
80
filter_plugins/applications_if_group_and_deps.py
Normal 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
|
@@ -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)
|
@@ -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}")
|
122
filter_plugins/load_configuration.py
Normal file
122
filter_plugins/load_configuration.py
Normal 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}
|
42
filter_plugins/merge_mapping.py
Normal file
42
filter_plugins/merge_mapping.py
Normal 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,
|
||||
}
|
@@ -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 don’t 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}")
|
Reference in New Issue
Block a user