Added new user generation script and optimized mail config

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-02 15:08:42 +02:00
parent 2ccfdf0de6
commit cb6fbba8f4
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
31 changed files with 281 additions and 144 deletions

View File

@ -1,6 +1,8 @@
ROLES_DIR := ./roles ROLES_DIR := ./roles
APPLICATIONS_OUT := ./group_vars/all/03_applications.yml APPLICATIONS_OUT := ./group_vars/all/03_applications.yml
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
USERS_OUT := ./group_vars/all/10_users.yml
USERS_SCRIPT := ./cli/generate_users.py
INCLUDES_OUT := ./tasks/utils/docker-roles.yml INCLUDES_OUT := ./tasks/utils/docker-roles.yml
INCLUDES_SCRIPT := ./cli/generate_playbook.py INCLUDES_SCRIPT := ./cli/generate_playbook.py
@ -11,6 +13,10 @@ build:
@mkdir -p $(dir $(APPLICATIONS_OUT)) @mkdir -p $(dir $(APPLICATIONS_OUT))
python3 $(APPLICATIONS_SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(APPLICATIONS_OUT) python3 $(APPLICATIONS_SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(APPLICATIONS_OUT)
@echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n" @echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n"
@echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)"
@mkdir -p $(dir $(USERS_OUT))
python3 $(USERS_SCRIPT) --roles-dir $(ROLES_DIR) --output $(USERS_OUT)
@echo "✅ Users defaults written to $(USERS_OUT)\n"
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)" @echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)"
@mkdir -p $(dir $(INCLUDES_OUT)) @mkdir -p $(dir $(INCLUDES_OUT))
python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p docker- python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p docker-

163
cli/generate_users.py Normal file
View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import yaml
import glob
from collections import OrderedDict
def build_users(defs, primary_domain, start_id, become_pwd):
"""
Build user entries with auto-incremented uid/gid and default username/email.
Args:
defs (OrderedDict): Keys are user IDs, values are dicts with optional overrides.
primary_domain (str): e.g., 'example.com'.
start_id (int): Starting uid/gid (e.g., 1001).
become_pwd (str): Password string for all users.
Returns:
OrderedDict: Merged user definitions with full fields.
"""
users = OrderedDict()
next_id = start_id
for key, overrides in defs.items():
username = overrides.get('username', key)
email = overrides.get('email', f"{username}@{primary_domain}")
uid = overrides.get('uid', next_id)
gid = overrides.get('gid', next_id)
is_admin = overrides.get('is_admin', False)
entry = {
'username': username,
'email': email,
'password': become_pwd,
'uid': uid,
'gid': gid
}
if is_admin:
entry['is_admin'] = True
users[key] = entry
next_id += 1
return users
def load_user_defs(roles_dir):
"""
Scan all roles/*/vars/configuration.yml files and extract 'users:' sections.
Raises an exception if conflicting definitions are found.
Args:
roles_dir (str): Path to the directory containing role subdirectories.
Returns:
OrderedDict: Merged user definitions.
Raises:
ValueError: On invalid format or conflicting field values.
"""
pattern = os.path.join(roles_dir, '*/vars/configuration.yml')
files = sorted(glob.glob(pattern))
merged = OrderedDict()
for filepath in files:
with open(filepath, 'r') as f:
data = yaml.safe_load(f) or {}
users = data.get('users', {})
if not isinstance(users, dict):
continue
for key, overrides in users.items():
if not isinstance(overrides, dict):
raise ValueError(f"Invalid definition for user '{key}' in {filepath}")
if key not in merged:
merged[key] = overrides.copy()
else:
existing = merged[key]
for field, value in overrides.items():
if field in existing and existing[field] != value:
raise ValueError(
f"Conflict for user '{key}': field '{field}' has existing value "
f"'{existing[field]}', tried to set '{value}' in {filepath}"
)
existing.update(overrides)
return merged
def dictify(data):
"""
Recursively convert OrderedDict to regular dict before YAML dump.
"""
if isinstance(data, OrderedDict):
return {k: dictify(v) for k, v in data.items()}
if isinstance(data, dict):
return {k: dictify(v) for k, v in data.items()}
if isinstance(data, list):
return [dictify(v) for v in data]
return data
def parse_args():
parser = argparse.ArgumentParser(
description='Generate a users.yml by merging all roles/*/vars/configuration.yml users sections.'
)
parser.add_argument(
'--roles-dir', '-r', required=True,
help='Directory containing roles (e.g., roles/*/vars/configuration.yml).'
)
parser.add_argument(
'--output', '-o', required=True,
help='Path to the output YAML file (e.g., users.yml).'
)
parser.add_argument(
'--start-id', '-s', type=int, default=1001,
help='Starting uid/gid number (default: 1001).'
)
return parser.parse_args()
def main():
args = parse_args()
primary_domain = '{{ primary_domain }}'
become_pwd = '{{ ansible_become_password }}'
try:
user_defs = load_user_defs(args.roles_dir)
except ValueError as e:
print(f"Error merging user definitions: {e}", file=sys.stderr)
sys.exit(1)
users = build_users(
defs=user_defs,
primary_domain=primary_domain,
start_id=args.start_id,
become_pwd=become_pwd
)
default_users = {'default_users': users}
plain_data = dictify(default_users)
# Ensure strings are represented without Python-specific tags
yaml.SafeDumper.add_representer(
str,
lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:str', data)
)
with open(args.output, 'w') as f:
yaml.safe_dump(
plain_data,
f,
default_flow_style=False,
sort_keys=False,
width=120
)
if __name__ == '__main__':
main()

View File

@ -1 +1,2 @@
*_applications.yml *_applications.yml
*_users.yml

View File

@ -3,7 +3,7 @@ default_system_email:
domain: "{{primary_domain}}" domain: "{{primary_domain}}"
host: "mail.{{primary_domain}}" host: "mail.{{primary_domain}}"
port: 465 port: 465
tls: true tls: true # true for TLS and false for SSL
start_tls: false start_tls: false
smtp: true smtp: true
# password: # Needs to be defined in inventory file # password: # Needs to be defined in inventory file

View File

@ -1,110 +1,50 @@
# Helper Variables
# Helper Variables for administrator
_users_administrator_username: "{{ users.administrator.username | default('administrator') }}"
_users_administrator_email: "{{ users.administrator.email | default(_users_administrator_username ~ '@' ~ primary_domain) }}"
# Helper Variables for bounce
_users_bounce_username: "{{ users.bounce.username | default('bounce') }}"
_users_bounce_email: "{{ users.bounce.email | default(_users_bounce_username ~ '@' ~ primary_domain) }}"
# Helper Variables for no-reply
_users_no_reply_username: "{{ users['no-reply'].username | default('no-reply') }}"
_users_no_reply_email: "{{ users['no-reply'].email | default(_users_no_reply_username ~ '@' ~ primary_domain) }}"
# Helper Variables for blackhole
_users_blackhole_username: "{{ users.blackhole.username | default('no-reply') }}"
_users_blackhole_email: "{{ users.blackhole.email | default(_users_blackhole_username ~ '@' ~ primary_domain) }}"
# Helper Variables for contact user
_users_contact_username: "{{ users.contact.username | default('contact') }}"
_users_contact_email: "{{ users.contact.email | default(_users_contact_username ~ '@' ~ primary_domain) }}"
# Helper Variables for support
_users_support_username: "{{ users.support.username | default('support') }}"
_users_support_email: "{{ users.support.email | default(_users_support_username ~ '@' ~ primary_domain) }}"
# Helper Variables for helpdesk
_users_helpdesk_username: "{{ users.helpdesk.username | default('helpdesk') }}"
_users_helpdesk_email: "{{ users.helpdesk.email | default(_users_helpdesk_username ~ '@' ~ primary_domain) }}"
# Extract SLD and TLD from primary_domain
_users_sld_username: "{{ primary_domain.split('.')[0] }}"
_users_sld_email: "{{ _users_sld_username ~ '@' ~ primary_domain }}"
_users_tld_username: "{{ primary_domain.split('.')[-1] }}"
_users_tld_email: "{{ _users_tld_username ~ '@' ~ primary_domain }}"
# Administrator
default_users: default_users:
# Credentials will be used as administration credentials for all applications and the system
administrator: administrator:
username: "{{_users_administrator_username}}" # Username of the administrator username: administrator
email: "{{_users_administrator_email}}" # Email of the administrator email: administrator@{{ primary_domain }}
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file password: '{{ ansible_become_password }}'
uid: 1001 # Posix User ID uid: 1001
gid: 1001 # Posix Group ID gid: 1001
is_admin: true # Define as admin user is_admin: true
# Account for Newsletter bouncing
bounce:
username: "{{ _users_bounce_username }}" # Bounce-handler account username
email: "{{ _users_bounce_email }}" # Email address for handling bounces
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1002 # Posix User ID for bounce
gid: 1002 # Posix Group ID for bounce
# User to send System Emails from
no-reply:
username: "{{ _users_no_reply_username }}" # No-reply account username
email: "{{ _users_no_reply_email }}" # Email address for outgoing no-reply mails
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
uid: 1003 # Posix User ID for no-reply
gid: 1003 # Posix Group ID for no-reply
# Emails etc, what you send to this user will be forgetten
blackhole: blackhole:
username: "{{ _users_blackhole_username }}" # Blackhole account username username: blackhole
email: "{{ _users_blackhole_email }}" # Email address to which emails can be send which well be forgetten email: blackhole@{{ primary_domain }}
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file password: '{{ ansible_become_password }}'
uid: 1004 # Posix User ID for bounce uid: 1002
gid: 1004 # Posix Group ID for bounce gid: 1002
crm:
# The contact user account which clients and plattform users can contact username: contact
contact: email: contact@{{ primary_domain }}
username: "{{ _users_contact_username }}" # Contact account username password: '{{ ansible_become_password }}'
email: "{{ _users_contact_email }}" # Email address to which initial contacct emails can be send uid: 1003
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file gid: 1003
uid: 1005 # Posix User ID for bounce bounce:
gid: 1005 # Posix Group ID for bounce username: bounce
email: bounce@{{ primary_domain }}
# Support and Helpdesk accounts password: '{{ ansible_become_password }}'
support: uid: 1004
username: "{{ _users_support_username }}" # Support account username gid: 1004
email: "{{ _users_support_email }}" # Email for customer and platform support communication newsletter:
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file username: newsletter
uid: 1006 # Posix User ID for support email: newsletter@{{ primary_domain }}
gid: 1006 # Posix Group ID for support password: '{{ ansible_become_password }}'
uid: 1005
helpdesk: gid: 1005
username: "{{ _users_helpdesk_username }}" # Helpdesk account username no-reply:
email: "{{ _users_helpdesk_email }}" # Email for internal technical helpdesk communication username: no-reply
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file email: no-reply@{{ primary_domain }}
uid: 1007 # Posix User ID for helpdesk password: '{{ ansible_become_password }}'
gid: 1007 # Posix Group ID for helpdesk uid: 1006
gid: 1006
sld_user: sld:
username: "{{ _users_sld_username }}" # Username based on SLD of the primary domain username: '{{ primary_domain.split(''.'')[0] }}'
email: "{{ _users_sld_email }}" # Email address with SLD username email: '{{ primary_domain.split(''.'')[0] }}@{{ primary_domain }}'
password: "{{ ansible_become_password }}" # Init password from inventory password: '{{ ansible_become_password }}'
uid: 1007
gid: 1007
tld:
username: '{{ primary_domain.split(''.'')[1] }}'
email: '{{ primary_domain.split(''.'')[1] }}@{{ primary_domain }}'
password: '{{ ansible_become_password }}'
uid: 1008 uid: 1008
gid: 1008 gid: 1008
tld_user:
username: "{{ _users_tld_username }}" # Username based on TLD of the primary domain
email: "{{ _users_tld_email }}" # Email address with TLD username
password: "{{ ansible_become_password }}" # Init password from inventory
uid: 1009
gid: 1009

View File

@ -1,6 +1,6 @@
users: users:
administrator: administrator:
email: "{{users.administrator.email}}" email: "administrator@{{ primary_domain }}"
images: images:
pds: "ghcr.io/bluesky-social/pds:latest" pds: "ghcr.io/bluesky-social/pds:latest"
pds: pds:

View File

@ -0,0 +1,4 @@
users:
blackhole:
description: "Everything what will be send to this user will disapear"
username: "blackhole"

View File

View File

@ -21,3 +21,4 @@ galaxy_info:
class: "fa-solid fa-phone" class: "fa-solid fa-phone"
run_after: run_after:
- docker-keycloak - docker-keycloak
- docker-mailu

View File

@ -50,12 +50,12 @@ ESPOCRM_CONFIG_LOGGER_ROTATION=false
# ------------------------------------------------ # ------------------------------------------------
ESPOCRM_CONFIG_SMTP_SERVER={{ system_email.host }} ESPOCRM_CONFIG_SMTP_SERVER={{ system_email.host }}
ESPOCRM_CONFIG_SMTP_PORT={{ system_email.port }} ESPOCRM_CONFIG_SMTP_PORT={{ system_email.port }}
ESPOCRM_CONFIG_SMTP_SECURITY=TLS ESPOCRM_CONFIG_SMTP_SECURITY={{ "TLS" if system_email.start_tls else "SSL"}}
ESPOCRM_CONFIG_SMTP_AUTH=true ESPOCRM_CONFIG_SMTP_AUTH=true
ESPOCRM_CONFIG_SMTP_USERNAME={{ users['no-reply'].email }} ESPOCRM_CONFIG_SMTP_USERNAME={{ users['contact'].email }}
ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['no-reply'].mailu_token }} ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['contact'].mailu_token }}
ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_NAME={{ service_provider.company.titel }} - CRM ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_NAME={{ service_provider.company.titel }} - CRM
ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_ADDRESS={{ users['no-reply'].email }} ESPOCRM_CONFIG_OUTBOUND_EMAIL_FROM_ADDRESS={{ users['contact'].email }}
# ------------------------------------------------ # ------------------------------------------------
# LDAP settings (optional) # LDAP settings (optional)

View File

@ -2,10 +2,10 @@ images:
espocrm: "espocrm/espocrm:latest" espocrm: "espocrm/espocrm:latest"
users: users:
administrator: administrator:
username: "{{ users.administrator.username }}" username: "administrator"
email: "{{ users.administrator.email }}" crm:
description: "General contact account"
credentials: username: "contact"
features: features:
matomo: true matomo: true
css: false css: false
@ -26,6 +26,8 @@ csp:
connect-src: connect-src:
- wss://espocrm.{{ primary_domain }} - wss://espocrm.{{ primary_domain }}
- "data:" - "data:"
frame-src:
- https://s.espocrm.com/
domains: domains:
aliases: aliases:
- "crm.{{ primary_domain }}" - "crm.{{ primary_domain }}"

View File

@ -2,7 +2,7 @@ images:
keycloak: "quay.io/keycloak/keycloak:latest" keycloak: "quay.io/keycloak/keycloak:latest"
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" # Administrator Username for Keycloak username: "administrator"
import_realm: True # If True realm will be imported. If false skip. import_realm: True # If True realm will be imported. If false skip.
credentials: credentials:
features: features:

View File

@ -8,7 +8,7 @@ hostname: "ldap" # Hostname of the LDAP Ser
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" # Administrator username username: "administrator"
credentials: credentials:
features: features:
ldap: true ldap: true

View File

@ -2,7 +2,11 @@ images:
listmonk: "listmonk/listmonk:latest" listmonk: "listmonk/listmonk:latest"
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" # Listmonk administrator account username username: "administrator"
bounce:
username: "bounce"
newsletter:
username: "newsletter"
public_api_activated: False # Security hole. Can be used for spaming public_api_activated: False # Security hole. Can be used for spaming
version: "latest" # Docker Image version version: "latest" # Docker Image version
features: features:

View File

@ -1,7 +1,7 @@
version: "2024.06" # Docker Image Version version: "2024.06" # Docker Image Version
users: users:
administrator: administrator:
email: "{{users.administrator.email}}" # Administrator Email for DNS Records email: "administrator@{{ primary_domain }}" # Administrator Email for DNS Records
oidc: oidc:
email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used
enable_user_creation: true # Users will be created if not existing enable_user_creation: true # Users will be created if not existing

View File

@ -1,10 +1,9 @@
images: images:
synapse: "matrixdotorg/synapse:latest" synapse: "matrixdotorg/synapse:latest"
element: "vectorim/element-web:latest" element: "vectorim/element-web:latest"
# Set bridges
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" # Accountname of the matrix admin username: "administrator"
playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start playbook_tags: "setup-all,start" # For the initial update use: install-all,ensure-matrix-users-created,start
server_name: "{{primary_domain}}" # Adress for the account names etc. server_name: "{{primary_domain}}" # Adress for the account names etc.
synapse: synapse:

View File

@ -1,8 +1,7 @@
site_titel: "Academy on {{primary_domain}}" site_titel: "Academy on {{primary_domain}}"
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" username: "administrator"
email: "{{users.administrator.email}}"
version: "4.5" # Latest LTS - Necessary for OIDC version: "4.5" # Latest LTS - Necessary for OIDC
features: features:
matomo: true matomo: true

View File

@ -20,7 +20,7 @@ SMTP_NAME= {{ users['no-reply'].email }}
SMTP_PASSWORD= {{ users['no-reply'].mailu_token }} SMTP_PASSWORD= {{ users['no-reply'].mailu_token }}
# Email from configuration # Email from configuration
MAIL_FROM_ADDRESS= "no-reply" MAIL_FROM_ADDRESS= "{{ users['no-reply'].username }}"
MAIL_DOMAIN= "{{system_email.domain}}" MAIL_DOMAIN= "{{system_email.domain}}"
# Initial Admin Data # Initial Admin Data

View File

@ -29,7 +29,9 @@ features:
central_database: true central_database: true
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" username: "administrator"
no-reply:
username: "no-reply"
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes) default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
legacy_login_mask: legacy_login_mask:
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO enabled: False # If true, then legacy login mask is shown. Otherwise just SSO

View File

@ -3,7 +3,7 @@ server_mode: False # If true then the p
master_password_required: True # Master password is required. Recommended True. False is a security risk. master_password_required: True # Master password is required. Recommended True. False is a security risk.
users: users:
administrator: administrator:
email: "{{ users.administrator.email }}" # Initial login email address email: "administrator@{{ primary_domain }}"
oauth2_proxy: oauth2_proxy:
application: "application" application: "application"
port: "80" port: "80"

View File

@ -1,8 +1,8 @@
title: "Blog" # Wordpress titel title: "Blog" # Wordpress titel
users: # Credentials users: # Credentials
administrator: # Wordpress administrator administrator: # Wordpress administrator
username: "{{users.administrator.username}}" # Username of the wordpress administrator username: "administrator"
email: "{{users.administrator.email}}" # Email of the wordpress adminsitrator email: "administrator@{{ primary_domain }}"
plugins: plugins:
wp-discourse: wp-discourse:
enabled: "{{ 'discourse' in group_names | lower }}" enabled: "{{ 'discourse' in group_names | lower }}"

View File

@ -1,6 +1,6 @@
users: users:
administrator: administrator:
username: "{{users.administrator.username}}" username: "administrator"
version: "latest" version: "latest"
oauth2_proxy: oauth2_proxy:
application: "application" application: "application"

View File

@ -0,0 +1,7 @@
users:
sld:
description: "Auto Generated Account to reserve the SLD"
username: "{{ primary_domain.split('.')[0] }}"
tld:
description: "Auto Generated Account to reserve the TLD"
username: "{{ primary_domain.split('.')[1] }}"

View File

@ -0,0 +1,9 @@
users:
administrator:
description: "System Administrator"
username: "administrator"
email: "administrator@{{ primary_domain }}"
password: "{{ ansible_become_password }}"
uid: 1001
gid: 1001
is_admin: true