mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-09 11:47:14 +02:00
Compare commits
7 Commits
2ccfdf0de6
...
03db141316
Author | SHA1 | Date | |
---|---|---|---|
03db141316 | |||
9cf18cae0e | |||
e807a3e956 | |||
ef663a1356 | |||
821275ce70 | |||
9d1b44319c | |||
cb6fbba8f4 |
14
Makefile
14
Makefile
@@ -1,16 +1,26 @@
|
||||
ROLES_DIR := ./roles
|
||||
APPLICATIONS_OUT := ./group_vars/all/03_applications.yml
|
||||
APPLICATIONS_OUT := ./group_vars/all/04_applications.yml
|
||||
APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
|
||||
USERS_OUT := ./group_vars/all/03_users.yml
|
||||
USERS_SCRIPT := ./cli/generate_users.py
|
||||
INCLUDES_OUT := ./tasks/utils/docker-roles.yml
|
||||
INCLUDES_SCRIPT := ./cli/generate_playbook.py
|
||||
|
||||
EXTRA_USERS := $(shell \
|
||||
find $(ROLES_DIR) -maxdepth 1 -type d -name 'docker*' -printf '%f\n' \
|
||||
| sed -E 's/^docker[_-]?//' \
|
||||
| paste -sd, - \
|
||||
)
|
||||
|
||||
.PHONY: build install test
|
||||
|
||||
build:
|
||||
@echo "🔧 Generating applications defaults → $(APPLICATIONS_OUT) from roles in $(ROLES_DIR)…"
|
||||
@mkdir -p $(dir $(APPLICATIONS_OUT))
|
||||
python3 $(USERS_SCRIPT) --roles-dir $(ROLES_DIR) --output $(USERS_OUT) --extra-users "$(EXTRA_USERS)"
|
||||
@echo "✅ Users defaults written to $(USERS_OUT)\n"
|
||||
python3 $(APPLICATIONS_SCRIPT) --roles-dir $(ROLES_DIR) --output-file $(APPLICATIONS_OUT)
|
||||
@echo "✅ Applications defaults written to $(APPLICATIONS_OUT)\n"
|
||||
@echo "🔧 Generating users defaults → $(USERS_OUT) from roles in $(ROLES_DIR)…"
|
||||
@echo "🔧 Generating Docker role includes → $(INCLUDES_OUT)…"
|
||||
@mkdir -p $(dir $(INCLUDES_OUT))
|
||||
python3 $(INCLUDES_SCRIPT) $(ROLES_DIR) -o $(INCLUDES_OUT) -p docker-
|
||||
|
@@ -13,20 +13,31 @@ def load_yaml_file(path):
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate defaults_applications YAML from docker roles.")
|
||||
parser.add_argument("--roles-dir", default="roles", help="Path to the roles directory (default: roles)")
|
||||
parser.add_argument("--output-file", default="group_vars/all/03_applications.yml", help="Path to output YAML file")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate defaults_applications YAML from docker roles and include users meta data for each role."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--roles-dir",
|
||||
help="Path to the roles directory (default: roles)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-file",
|
||||
help="Path to output YAML file"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
cwd = Path.cwd()
|
||||
roles_dir = (cwd / args.roles_dir).resolve()
|
||||
output_file = (cwd / args.output_file).resolve()
|
||||
|
||||
# Ensure output directory exists
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Initialize result structure
|
||||
result = {"defaults_applications": {}}
|
||||
|
||||
# Process each role for application configs
|
||||
for role_dir in sorted(roles_dir.iterdir()):
|
||||
role_name = role_dir.name
|
||||
vars_main = role_dir / "vars" / "main.yml"
|
||||
@@ -40,9 +51,10 @@ def main():
|
||||
try:
|
||||
application_id = vars_data.get("application_id")
|
||||
except Exception as e:
|
||||
# print the exception message
|
||||
print(f"Warning: failed to read application_id from {vars_data} in {vars_main}.\nException: {e}", file=sys.stderr)
|
||||
# exit with status 0
|
||||
print(
|
||||
f"Warning: failed to read application_id from {vars_main}\nException: {e}",
|
||||
file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not application_id:
|
||||
@@ -56,7 +68,19 @@ def main():
|
||||
config_data = load_yaml_file(config_file)
|
||||
if config_data:
|
||||
result["defaults_applications"][application_id] = config_data
|
||||
users_meta_file = role_dir / "meta" / "users.yml"
|
||||
transformed_users = {}
|
||||
if users_meta_file.exists():
|
||||
users_meta = load_yaml_file(users_meta_file)
|
||||
users_data = users_meta.get("users", {})
|
||||
for user, role_user_attrs in users_data.items():
|
||||
transformed_users[user] = f"{{{{ users[\"{user}\"] }}}}"
|
||||
|
||||
# Attach transformed users under each application
|
||||
if transformed_users:
|
||||
result["defaults_applications"][application_id]["users"] = transformed_users
|
||||
|
||||
# Write out result YAML
|
||||
with output_file.open("w", encoding="utf-8") as f:
|
||||
yaml.dump(result, f, sort_keys=False)
|
||||
|
||||
@@ -65,5 +89,6 @@ def main():
|
||||
except ValueError:
|
||||
print(f"✅ Generated: {output_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
228
cli/generate_users.py
Normal file
228
cli/generate_users.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/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, default username/email, and optional description.
|
||||
|
||||
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.
|
||||
|
||||
Raises:
|
||||
ValueError: If duplicate override uids/gids or conflicts in generated values.
|
||||
"""
|
||||
users = OrderedDict()
|
||||
used_uids = set()
|
||||
|
||||
# Pre-collect any provided uids/gids and check for duplicates
|
||||
for key, overrides in defs.items():
|
||||
if 'uid' in overrides:
|
||||
uid = overrides['uid']
|
||||
if uid in used_uids:
|
||||
raise ValueError(f"Duplicate uid {uid} for user '{key}'")
|
||||
used_uids.add(uid)
|
||||
|
||||
next_free = start_id
|
||||
def allocate_free_id():
|
||||
nonlocal next_free
|
||||
# find next free id not in used_uids
|
||||
while next_free in used_uids:
|
||||
next_free += 1
|
||||
free = next_free
|
||||
used_uids.add(free)
|
||||
next_free += 1
|
||||
return free
|
||||
|
||||
# Build entries
|
||||
for key, overrides in defs.items():
|
||||
username = overrides.get('username', key)
|
||||
email = overrides.get('email', f"{username}@{primary_domain}")
|
||||
description = overrides.get('description')
|
||||
password = overrides.get('password',become_pwd)
|
||||
# UID assignment
|
||||
if 'uid' in overrides:
|
||||
uid = overrides['uid']
|
||||
else:
|
||||
uid = allocate_free_id()
|
||||
gid = overrides.get('gid',uid)
|
||||
|
||||
entry = {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'uid': uid,
|
||||
'gid': gid
|
||||
}
|
||||
if description is not None:
|
||||
entry['description'] = description
|
||||
if overrides.get('is_admin', False):
|
||||
entry['is_admin'] = True
|
||||
|
||||
users[key] = entry
|
||||
|
||||
# Validate uniqueness of username, email, and gid
|
||||
seen_usernames = set()
|
||||
seen_emails = set()
|
||||
seen_gids = set()
|
||||
for key, entry in users.items():
|
||||
un = entry['username']
|
||||
em = entry['email']
|
||||
gd = entry['gid']
|
||||
if un in seen_usernames:
|
||||
raise ValueError(f"Duplicate username '{un}' in merged users")
|
||||
if em in seen_emails:
|
||||
raise ValueError(f"Duplicate email '{em}' in merged users")
|
||||
seen_usernames.add(un)
|
||||
seen_emails.add(em)
|
||||
seen_gids.add(gd)
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def load_user_defs(roles_dir):
|
||||
"""
|
||||
Scan all roles/*/meta/users.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, '*/meta/users.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/*/meta/users.yml users sections.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--roles-dir', '-r', required=True,
|
||||
help='Directory containing roles (e.g., roles/*/meta/users.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).'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--extra-users', '-e',
|
||||
help='Comma-separated list of additional usernames to include.',
|
||||
default=None
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
primary_domain = '{{ primary_domain }}'
|
||||
become_pwd = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
|
||||
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)
|
||||
|
||||
# Add extra users if any
|
||||
if args.extra_users:
|
||||
for name in args.extra_users.split(','):
|
||||
user = name.strip()
|
||||
if not user:
|
||||
continue
|
||||
if user in user_defs:
|
||||
print(f"Warning: extra user '{user}' already defined; skipping.", file=sys.stderr)
|
||||
else:
|
||||
user_defs[user] = {}
|
||||
|
||||
try:
|
||||
users = build_users(
|
||||
defs=user_defs,
|
||||
primary_domain=primary_domain,
|
||||
start_id=args.start_id,
|
||||
become_pwd=become_pwd
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"Error building user entries: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
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()
|
3
group_vars/all/.gitignore
vendored
3
group_vars/all/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*_applications.yml
|
||||
*_applications.yml
|
||||
*_users.yml
|
@@ -3,7 +3,7 @@ default_system_email:
|
||||
domain: "{{primary_domain}}"
|
||||
host: "mail.{{primary_domain}}"
|
||||
port: 465
|
||||
tls: true
|
||||
tls: true # true for TLS and false for SSL
|
||||
start_tls: false
|
||||
smtp: true
|
||||
# password: # Needs to be defined in inventory file
|
@@ -1,110 +0,0 @@
|
||||
# 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:
|
||||
|
||||
# Credentials will be used as administration credentials for all applications and the system
|
||||
administrator:
|
||||
username: "{{_users_administrator_username}}" # Username of the administrator
|
||||
email: "{{_users_administrator_email}}" # Email of the administrator
|
||||
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
|
||||
uid: 1001 # Posix User ID
|
||||
gid: 1001 # Posix Group ID
|
||||
is_admin: true # Define as admin user
|
||||
|
||||
# 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:
|
||||
username: "{{ _users_blackhole_username }}" # Blackhole account username
|
||||
email: "{{ _users_blackhole_email }}" # Email address to which emails can be send which well be forgetten
|
||||
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
|
||||
uid: 1004 # Posix User ID for bounce
|
||||
gid: 1004 # Posix Group ID for bounce
|
||||
|
||||
# The contact user account which clients and plattform users can contact
|
||||
contact:
|
||||
username: "{{ _users_contact_username }}" # Contact account username
|
||||
email: "{{ _users_contact_email }}" # Email address to which initial contacct emails can be send
|
||||
password: "{{ansible_become_password}}" # Example initialisation password needs to be set in inventory file
|
||||
uid: 1005 # Posix User ID for bounce
|
||||
gid: 1005 # Posix Group ID for bounce
|
||||
|
||||
# Support and Helpdesk accounts
|
||||
support:
|
||||
username: "{{ _users_support_username }}" # Support account username
|
||||
email: "{{ _users_support_email }}" # Email for customer and platform support communication
|
||||
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file
|
||||
uid: 1006 # Posix User ID for support
|
||||
gid: 1006 # Posix Group ID for support
|
||||
|
||||
helpdesk:
|
||||
username: "{{ _users_helpdesk_username }}" # Helpdesk account username
|
||||
email: "{{ _users_helpdesk_email }}" # Email for internal technical helpdesk communication
|
||||
password: "{{ ansible_become_password }}" # Example initialisation password needs to be set in inventory file
|
||||
uid: 1007 # Posix User ID for helpdesk
|
||||
gid: 1007 # Posix Group ID for helpdesk
|
||||
|
||||
sld_user:
|
||||
username: "{{ _users_sld_username }}" # Username based on SLD of the primary domain
|
||||
email: "{{ _users_sld_email }}" # Email address with SLD username
|
||||
password: "{{ ansible_become_password }}" # Init password from inventory
|
||||
uid: 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
|
||||
|
@@ -1,8 +1,8 @@
|
||||
images:
|
||||
akaunting: "docker.io/akaunting/akaunting:latest"
|
||||
company_name: "{{primary_domain}}"
|
||||
company_email: "{{users.administrator.email}}"
|
||||
setup_admin_email: "{{users.administrator.email}}"
|
||||
company_email: "{{ users.administrator.email }}"
|
||||
setup_admin_email: "{{ users.administrator.email }}"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
|
3
roles/docker-bluesky/meta/users.yml
Normal file
3
roles/docker-bluesky/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
email: "administrator@{{ primary_domain }}"
|
@@ -1,6 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
email: "{{users.administrator.email}}"
|
||||
images:
|
||||
pds: "ghcr.io/bluesky-social/pds:latest"
|
||||
pds:
|
||||
|
4
roles/docker-compose/meta/users.yml
Normal file
4
roles/docker-compose/meta/users.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
users:
|
||||
blackhole:
|
||||
description: "Everything what will be send to this user will disapear"
|
||||
username: "blackhole"
|
0
roles/docker-compose/vars/main.yml
Normal file
0
roles/docker-compose/vars/main.yml
Normal file
@@ -48,7 +48,7 @@ env:
|
||||
#DOCKER_USE_HOSTNAME: true
|
||||
|
||||
## on initial signup example 'user1@example.com,user2@example.com'
|
||||
DISCOURSE_DEVELOPER_EMAILS: {{users.administrator.email}}
|
||||
DISCOURSE_DEVELOPER_EMAILS: {{ users.administrator.email }}
|
||||
|
||||
# Set Logo
|
||||
{% if service_provider.platform.logo | bool %}
|
||||
@@ -135,8 +135,8 @@ run:
|
||||
- exec: rails r "SiteSetting.username_change_period = 0" # Deactivate changing of username
|
||||
|
||||
# Activate Administrator User
|
||||
#- exec: printf '{{users.administrator.email}}\n{{users.administrator.password}}\n{{users.administrator.password}}\nY\n' | rake admin:create
|
||||
#- exec: rails r "User.find_by_email('{{users.administrator.email}}').update(username: '{{users.administrator.username}}')"
|
||||
#- exec: printf '{{ users.administrator.email }}\n{{users.administrator.password}}\n{{users.administrator.password}}\nY\n' | rake admin:create
|
||||
#- exec: rails r "User.find_by_email('{{ users.administrator.email }}').update(username: '{{users.administrator.username}}')"
|
||||
|
||||
# The following code is just an inspiration, how to connect with the oidc account. as long as this is not set the admini account needs to be manually connected with oidc
|
||||
# docker exec -it discourse_application rails runner "user = User.find_by_email('test@cymais.cloud'); UserAuth.create(user_id: user.id, provider: 'oidc', uid: 'eindeutige_oidc_id', info: { name: user.username, email: user.email })"
|
||||
|
@@ -20,4 +20,5 @@ galaxy_info:
|
||||
logo:
|
||||
class: "fa-solid fa-phone"
|
||||
run_after:
|
||||
- docker-keycloak
|
||||
- docker-keycloak
|
||||
- docker-mailu
|
6
roles/docker-espocrm/meta/users.yml
Normal file
6
roles/docker-espocrm/meta/users.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
contact:
|
||||
description: "General contact account"
|
||||
username: "contact"
|
@@ -50,12 +50,12 @@ ESPOCRM_CONFIG_LOGGER_ROTATION=false
|
||||
# ------------------------------------------------
|
||||
ESPOCRM_CONFIG_SMTP_SERVER={{ system_email.host }}
|
||||
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_USERNAME={{ users['no-reply'].email }}
|
||||
ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['no-reply'].mailu_token }}
|
||||
ESPOCRM_CONFIG_SMTP_USERNAME={{ users['contact'].email }}
|
||||
ESPOCRM_CONFIG_SMTP_PASSWORD={{ users['contact'].mailu_token }}
|
||||
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)
|
||||
|
@@ -1,11 +1,5 @@
|
||||
images:
|
||||
espocrm: "espocrm/espocrm:latest"
|
||||
users:
|
||||
administrator:
|
||||
username: "{{ users.administrator.username }}"
|
||||
email: "{{ users.administrator.email }}"
|
||||
|
||||
credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: false
|
||||
@@ -26,6 +20,8 @@ csp:
|
||||
connect-src:
|
||||
- wss://espocrm.{{ primary_domain }}
|
||||
- "data:"
|
||||
frame-src:
|
||||
- https://s.espocrm.com/
|
||||
domains:
|
||||
aliases:
|
||||
- "crm.{{ primary_domain }}"
|
@@ -27,5 +27,5 @@ SMTP_STARTTLS= {{ 'on' if system_email.start_tls else 'off' }}
|
||||
SMTP_FROM= no-reply
|
||||
|
||||
# Administrator Credentials
|
||||
FRIENDICA_ADMIN_MAIL= {{users.administrator.email}}
|
||||
MAILNAME= {{users.administrator.email}}
|
||||
FRIENDICA_ADMIN_MAIL= {{ users.administrator.email }}
|
||||
MAILNAME= {{ users.administrator.email }}
|
3
roles/docker-keycloak/meta/users.yml
Normal file
3
roles/docker-keycloak/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
@@ -1,8 +1,5 @@
|
||||
images:
|
||||
keycloak: "quay.io/keycloak/keycloak:latest"
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}" # Administrator Username for Keycloak
|
||||
import_realm: True # If True realm will be imported. If false skip.
|
||||
credentials:
|
||||
features:
|
||||
|
3
roles/docker-ldap/meta/users.yml
Normal file
3
roles/docker-ldap/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
@@ -6,9 +6,6 @@ network:
|
||||
public: False # Set to true in inventory file if you want to expose the LDAP port to the internet
|
||||
hostname: "ldap" # Hostname of the LDAP Server in the central_ldap network
|
||||
webinterface: "lam" # The webinterface which should be used. Possible: lam and phpldapadmin
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}" # Administrator username
|
||||
credentials:
|
||||
features:
|
||||
ldap: true
|
7
roles/docker-listmonk/meta/users.yml
Normal file
7
roles/docker-listmonk/meta/users.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
bounce:
|
||||
username: "bounce"
|
||||
newsletter:
|
||||
username: "newsletter"
|
@@ -1,8 +1,5 @@
|
||||
images:
|
||||
listmonk: "listmonk/listmonk:latest"
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}" # Listmonk administrator account username
|
||||
public_api_activated: False # Security hole. Can be used for spaming
|
||||
version: "latest" # Docker Image version
|
||||
features:
|
||||
|
3
roles/docker-mailu/meta/users.yml
Normal file
3
roles/docker-mailu/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
email: "administrator@{{ primary_domain }}" # Administrator Email for DNS Records
|
@@ -1,7 +1,4 @@
|
||||
version: "2024.06" # Docker Image Version
|
||||
users:
|
||||
administrator:
|
||||
email: "{{users.administrator.email}}" # Administrator Email for DNS Records
|
||||
oidc:
|
||||
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
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
- name: Create admin account via tootctl
|
||||
command:
|
||||
cmd: 'docker compose exec -u root web bash -c "RAILS_ENV=production bin/tootctl accounts create {{users.administrator.username}} --email {{users.administrator.email}} --confirmed --role Owner"'
|
||||
cmd: 'docker compose exec -u root web bash -c "RAILS_ENV=production bin/tootctl accounts create {{users.administrator.username}} --email {{ users.administrator.email }} --confirmed --role Owner"'
|
||||
chdir: "{{docker_compose.directories.instance}}"
|
||||
register: tootctl_create
|
||||
changed_when: tootctl_create.rc == 0
|
||||
|
@@ -46,7 +46,7 @@ devture_traefik_config_entrypoint_web_forwardedHeaders_insecure: true
|
||||
# you won't be required to define this variable (see `docs/configuring-playbook-ssl-certificates.md`).
|
||||
#
|
||||
# Example value: someone@example.com
|
||||
devture_traefik_config_certificatesResolvers_acme_email: "{{users.administrator.email}}"
|
||||
devture_traefik_config_certificatesResolvers_acme_email: "{{ users.administrator.email }}"
|
||||
|
||||
# A Postgres password to use for the superuser Postgres user (called `matrix` by default).
|
||||
#
|
||||
|
3
roles/docker-matrix/meta/users.yml
Normal file
3
roles/docker-matrix/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
@@ -28,7 +28,7 @@ web_client_location: "{{ web_protocol }}://{{domains.matrix.element}}
|
||||
public_baseurl: "{{ web_protocol }}://{{domains.matrix.synapse}}"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
admin_contact: 'mailto:{{users.administrator.email}}'
|
||||
admin_contact: 'mailto:{{ users.administrator.email }}'
|
||||
|
||||
email:
|
||||
smtp_host: "{{system_email.host}}"
|
||||
|
@@ -1,10 +1,6 @@
|
||||
images:
|
||||
synapse: "matrixdotorg/synapse:latest"
|
||||
element: "vectorim/element-web:latest"
|
||||
# Set bridges
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}" # Accountname of the matrix admin
|
||||
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.
|
||||
synapse:
|
||||
|
3
roles/docker-moodle/meta/users.yml
Normal file
3
roles/docker-moodle/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
@@ -1,8 +1,4 @@
|
||||
site_titel: "Academy on {{primary_domain}}"
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}"
|
||||
email: "{{users.administrator.email}}"
|
||||
version: "4.5" # Latest LTS - Necessary for OIDC
|
||||
features:
|
||||
matomo: true
|
||||
|
5
roles/docker-nextcloud/meta/users.yml
Normal file
5
roles/docker-nextcloud/meta/users.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
||||
no-reply:
|
||||
username: "no-reply"
|
@@ -20,7 +20,7 @@ SMTP_NAME= {{ users['no-reply'].email }}
|
||||
SMTP_PASSWORD= {{ users['no-reply'].mailu_token }}
|
||||
|
||||
# Email from configuration
|
||||
MAIL_FROM_ADDRESS= "no-reply"
|
||||
MAIL_FROM_ADDRESS= "{{ users['no-reply'].username }}"
|
||||
MAIL_DOMAIN= "{{system_email.domain}}"
|
||||
|
||||
# Initial Admin Data
|
||||
|
@@ -27,9 +27,6 @@ features:
|
||||
ldap: true
|
||||
oidc: true
|
||||
central_database: true
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}"
|
||||
default_quota: '1000000000' # Quota to assign if no quota is specified in the OIDC response (bytes)
|
||||
legacy_login_mask:
|
||||
enabled: False # If true, then legacy login mask is shown. Otherwise just SSO
|
||||
|
3
roles/docker-pgadmin/meta/users.yml
Normal file
3
roles/docker-pgadmin/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
email: "administrator@{{ primary_domain }}"
|
@@ -1,9 +1,6 @@
|
||||
version: "latest"
|
||||
server_mode: False # If true then the preconfigured database file is loaded. Recommended False. True is a security risk.
|
||||
master_password_required: True # Master password is required. Recommended True. False is a security risk.
|
||||
users:
|
||||
administrator:
|
||||
email: "{{ users.administrator.email }}" # Initial login email address
|
||||
oauth2_proxy:
|
||||
application: "application"
|
||||
port: "80"
|
||||
|
4
roles/docker-wordpress/meta/users.yml
Normal file
4
roles/docker-wordpress/meta/users.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
users: # Credentials
|
||||
administrator: # Wordpress administrator
|
||||
username: "administrator"
|
||||
email: "administrator@{{ primary_domain }}"
|
@@ -1,8 +1,4 @@
|
||||
title: "Blog" # Wordpress titel
|
||||
users: # Credentials
|
||||
administrator: # Wordpress administrator
|
||||
username: "{{users.administrator.username}}" # Username of the wordpress administrator
|
||||
email: "{{users.administrator.email}}" # Email of the wordpress adminsitrator
|
||||
plugins:
|
||||
wp-discourse:
|
||||
enabled: "{{ 'discourse' in group_names | lower }}"
|
||||
|
3
roles/docker-yourls/meta/users.yml
Normal file
3
roles/docker-yourls/meta/users.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "administrator"
|
@@ -1,6 +1,3 @@
|
||||
users:
|
||||
administrator:
|
||||
username: "{{users.administrator.username}}"
|
||||
version: "latest"
|
||||
oauth2_proxy:
|
||||
application: "application"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/bin/sendmail -t <<ERRMAIL
|
||||
To: {{users.administrator.email}}
|
||||
To: {{ users.administrator.email }}
|
||||
From: systemd <{{ users['no-reply'].email }}>
|
||||
Subject: $1
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
9
roles/user-administrator/meta/users.yml
Normal file
9
roles/user-administrator/meta/users.yml
Normal 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
|
132
roles/user/meta/users.yml
Normal file
132
roles/user/meta/users.yml
Normal file
@@ -0,0 +1,132 @@
|
||||
# Reserved usernames
|
||||
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] }}"
|
||||
root:
|
||||
username: root
|
||||
uid: 0
|
||||
gid: 0
|
||||
description: "System superuser"
|
||||
daemon:
|
||||
username: daemon
|
||||
description: "Daemon processes owner"
|
||||
bin:
|
||||
username: bin
|
||||
description: "Owner of essential binaries"
|
||||
sys:
|
||||
username: sys
|
||||
description: "System files owner"
|
||||
sync:
|
||||
username: sync
|
||||
description: "Sync user for filesystem synchronization"
|
||||
games:
|
||||
username: games
|
||||
description: "Games and educational software owner"
|
||||
man:
|
||||
username: man
|
||||
description: "Manual pages viewer"
|
||||
lp:
|
||||
username: lp
|
||||
description: "Printer spooler"
|
||||
mail:
|
||||
username: mail
|
||||
description: "Mail system"
|
||||
news:
|
||||
username: news
|
||||
description: "Network news system"
|
||||
uucp:
|
||||
username: uucp
|
||||
description: "UUCP system"
|
||||
proxy:
|
||||
username: proxy
|
||||
description: "Proxy user"
|
||||
www-data:
|
||||
username: www-data
|
||||
description: "Web server user"
|
||||
backup:
|
||||
username: backup
|
||||
description: "Backup operator"
|
||||
list:
|
||||
username: list
|
||||
description: "Mailing list manager"
|
||||
irc:
|
||||
username: irc
|
||||
description: "IRC services user"
|
||||
gnats:
|
||||
username: gnats
|
||||
description: "GNATS bug-reporting system"
|
||||
nobody:
|
||||
username: nobody
|
||||
description: "Unprivileged user"
|
||||
messagebus:
|
||||
username: messagebus
|
||||
description: "D-Bus message bus system"
|
||||
sshd:
|
||||
username: sshd
|
||||
description: "SSH daemon"
|
||||
rpc:
|
||||
username: rpc
|
||||
description: "Rpcbind daemon"
|
||||
ftp:
|
||||
username: ftp
|
||||
description: "FTP server"
|
||||
postfix:
|
||||
username: postfix
|
||||
description: "Postfix mail transfer agent"
|
||||
mysql:
|
||||
username: mysql
|
||||
description: "MySQL database server"
|
||||
mongodb:
|
||||
username: mongodb
|
||||
description: "MongoDB database server"
|
||||
admin:
|
||||
username: admin
|
||||
description: "Generic reserved username"
|
||||
administrator:
|
||||
username: administrator
|
||||
user:
|
||||
username: user
|
||||
description: "Generic reserved username"
|
||||
test:
|
||||
username: test
|
||||
description: "Generic reserved username"
|
||||
guest:
|
||||
username: guest
|
||||
description: "Generic reserved username"
|
||||
demo:
|
||||
username: demo
|
||||
description: "Generic reserved username"
|
||||
info:
|
||||
username: info
|
||||
description: "Generic reserved username"
|
||||
support:
|
||||
username: support
|
||||
description: "Generic reserved username"
|
||||
helpdesk:
|
||||
username: helpdesk
|
||||
description: "Generic reserved username"
|
||||
operator:
|
||||
username: operator
|
||||
description: "Generic reserved username"
|
||||
staff:
|
||||
username: staff
|
||||
description: "Generic reserved username"
|
||||
smtp:
|
||||
username: smtp
|
||||
description: "Generic reserved username"
|
||||
imap:
|
||||
username: imap
|
||||
description: "Generic reserved username"
|
||||
pop:
|
||||
username: pop
|
||||
description: "Generic reserved username"
|
||||
webmaster:
|
||||
username: webmaster
|
||||
description: "Generic reserved username"
|
||||
mailman:
|
||||
username: mailman
|
||||
description: "Generic reserved username"
|
@@ -12,7 +12,7 @@ class TestDomainUniqueness(unittest.TestCase):
|
||||
and assert that no domain appears more than once.
|
||||
"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
yaml_file = repo_root / 'group_vars' / 'all' / '03_applications.yml'
|
||||
yaml_file = repo_root / 'group_vars' / 'all' / '04_applications.yml'
|
||||
|
||||
# Generate the file if it doesn't exist
|
||||
if not yaml_file.exists():
|
||||
|
@@ -8,7 +8,7 @@ class TestOAuth2ProxyPorts(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
# Set up root paths and load oauth2_proxy ports mapping
|
||||
cls.ROOT = Path(__file__).parent.parent.parent.resolve()
|
||||
cls.PORTS_FILE = cls.ROOT / 'group_vars' / 'all' / '08_ports.yml'
|
||||
cls.PORTS_FILE = cls.ROOT / 'group_vars' / 'all' / '09_ports.yml'
|
||||
with cls.PORTS_FILE.open() as f:
|
||||
data = yaml.safe_load(f)
|
||||
cls.oauth2_ports = (
|
||||
@@ -50,7 +50,7 @@ class TestOAuth2ProxyPorts(unittest.TestCase):
|
||||
if app_id not in self.oauth2_ports:
|
||||
self.fail(
|
||||
f"Missing oauth2_proxy port mapping for application '{app_id}' "
|
||||
f"in group_vars/all/08_ports.yml"
|
||||
f"in group_vars/all/09_ports.yml"
|
||||
)
|
||||
|
||||
|
||||
|
36
tests/integration/test_unittest_imports.py
Normal file
36
tests/integration/test_unittest_imports.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# File: tests/integration/test_unittest_imports.py
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
class TestUnittestImports(unittest.TestCase):
|
||||
TEST_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
def test_all_test_files_import_unittest(self):
|
||||
missing = []
|
||||
|
||||
for root, dirs, files in os.walk(self.TEST_ROOT):
|
||||
for filename in files:
|
||||
if not filename.endswith('.py'):
|
||||
continue
|
||||
# only consider test files named like "test_*.py"
|
||||
if not filename.startswith('test_'):
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, filename)
|
||||
with open(filepath, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# check for either import form
|
||||
if 'import unittest' not in content and 'from unittest import' not in content:
|
||||
rel_path = os.path.relpath(filepath, os.getcwd())
|
||||
missing.append(rel_path)
|
||||
|
||||
if missing:
|
||||
self.fail(
|
||||
"The following test files do not import unittest:\n" +
|
||||
"\n".join(f"- {path}" for path in missing)
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@@ -2,49 +2,59 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Add module_utils/ to the import path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..", "module_utils")))
|
||||
sys.path.insert(
|
||||
0,
|
||||
os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../..",
|
||||
"module_utils",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
from module_utils.cert_utils import CertUtils
|
||||
|
||||
def test_matches():
|
||||
tests = [
|
||||
# Exact matches
|
||||
("example.com", "example.com", True),
|
||||
("www.example.com", "www.example.com", True),
|
||||
("api.example.com", "api.example.com", True),
|
||||
|
||||
# Wildcard matches
|
||||
("sub.example.com", "*.example.com", True),
|
||||
("www.example.com", "*.example.com", True),
|
||||
class TestCertUtilsMatches(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# prepare your test cases
|
||||
self.tests = [
|
||||
# Exact matches
|
||||
("example.com", "example.com", True),
|
||||
("www.example.com", "www.example.com", True),
|
||||
("api.example.com", "api.example.com", True),
|
||||
|
||||
# Wildcard non-matches
|
||||
("example.com", "*.example.com", False), # base domain is not covered
|
||||
("deep.sub.example.com", "*.example.com", False), # too deep
|
||||
("sub.deep.example.com", "*.deep.example.com", True), # correct: one level below
|
||||
# Wildcard matches
|
||||
("sub.example.com", "*.example.com", True),
|
||||
("www.example.com", "*.example.com", True),
|
||||
|
||||
# Special cases
|
||||
("deep.api.example.com", "*.api.example.com", True),
|
||||
("api.example.com", "*.api.example.com", False), # base not covered by wildcard
|
||||
# Wildcard non-matches
|
||||
("example.com", "*.example.com", False), # base domain is not covered
|
||||
("deep.sub.example.com", "*.example.com", False), # too deep
|
||||
("sub.deep.example.com", "*.deep.example.com", True), # correct: one level below
|
||||
|
||||
# Completely different domains
|
||||
("test.other.com", "*.example.com", False),
|
||||
]
|
||||
# Special cases
|
||||
("deep.api.example.com", "*.api.example.com", True),
|
||||
("api.example.com", "*.api.example.com", False), # base not covered by wildcard
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
# Completely different domains
|
||||
("test.other.com", "*.example.com", False),
|
||||
]
|
||||
|
||||
for domain, san, expected in tests:
|
||||
result = CertUtils.matches(domain, san)
|
||||
if result == expected:
|
||||
print(f"✅ PASS: {domain} vs {san} -> {result}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"❌ FAIL: {domain} vs {san} -> {result} (expected {expected})")
|
||||
failed += 1
|
||||
def test_matches(self):
|
||||
for domain, san, expected in self.tests:
|
||||
with self.subTest(domain=domain, san=san):
|
||||
result = CertUtils.matches(domain, san)
|
||||
self.assertEqual(
|
||||
result,
|
||||
expected,
|
||||
msg=f"CertUtils.matches({domain!r}, {san!r}) returned {result}, expected {expected}",
|
||||
)
|
||||
|
||||
print(f"\nSummary: {passed} passed, {failed} failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_matches()
|
||||
unittest.main()
|
||||
|
65
tests/unit/test_generate_applications_defaults_users.py
Normal file
65
tests/unit/test_generate_applications_defaults_users.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
class TestGenerateDefaultApplicationsUsers(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Setup temporary roles directory
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
self.roles_dir = self.temp_dir / "roles"
|
||||
self.roles_dir.mkdir()
|
||||
|
||||
# Sample role with users meta
|
||||
self.role = self.roles_dir / "docker-app-with-users"
|
||||
(self.role / "vars").mkdir(parents=True)
|
||||
(self.role / "meta").mkdir(parents=True)
|
||||
|
||||
# Write application_id and configuration
|
||||
(self.role / "vars" / "main.yml").write_text("application_id: app_with_users\n")
|
||||
(self.role / "vars" / "configuration.yml").write_text("setting: value\n")
|
||||
|
||||
# Write users meta
|
||||
users_meta = {
|
||||
'users': {
|
||||
'alice': {'uid': 2001, 'gid': 2001},
|
||||
'bob': {'uid': 2002, 'gid': 2002}
|
||||
}
|
||||
}
|
||||
with (self.role / "meta" / "users.yml").open('w', encoding='utf-8') as f:
|
||||
yaml.dump(users_meta, f)
|
||||
|
||||
# Output file path
|
||||
self.output_file = self.temp_dir / "output.yml"
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_users_injection(self):
|
||||
"""
|
||||
When a users.yml exists with defined users, the script should inject a 'users'
|
||||
mapping in the generated YAML, mapping each username to a Jinja2 reference.
|
||||
"""
|
||||
script_path = Path(__file__).resolve().parents[2] / "cli" / "generate-applications-defaults.py"
|
||||
result = subprocess.run([
|
||||
"python3", str(script_path),
|
||||
"--roles-dir", str(self.roles_dir),
|
||||
"--output-file", str(self.output_file)
|
||||
], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
data = yaml.safe_load(self.output_file.read_text())
|
||||
|
||||
apps = data.get('defaults_applications', {})
|
||||
# Only the app with users should be present
|
||||
self.assertIn('app_with_users', apps)
|
||||
|
||||
# 'users' section should be present and correct
|
||||
users_map = apps['app_with_users']['users']
|
||||
expected = {'alice': '{{ users["alice"] }}', 'bob': '{{ users["bob"] }}'}
|
||||
self.assertEqual(users_map, expected)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@@ -23,7 +23,7 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
||||
(self.sample_role / "vars" / "configuration.yml").write_text("foo: bar\nbaz: 123\n")
|
||||
|
||||
# Output file path
|
||||
self.output_file = self.temp_dir / "group_vars" / "all" / "03_applications.yml"
|
||||
self.output_file = self.temp_dir / "group_vars" / "all" / "04_applications.yml"
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
136
tests/unit/test_generate_users.py
Normal file
136
tests/unit/test_generate_users.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
import yaml
|
||||
from collections import OrderedDict
|
||||
|
||||
# Add cli/ to import path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..", "cli")))
|
||||
|
||||
import generate_users
|
||||
|
||||
class TestGenerateUsers(unittest.TestCase):
|
||||
def test_build_users_auto_increment_and_overrides(self):
|
||||
defs = {
|
||||
'alice': {},
|
||||
'bob': {'uid': 2000, 'email': 'bob@custom.com', 'description': 'Custom user'},
|
||||
'carol': {}
|
||||
}
|
||||
users = generate_users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
start_id=1001,
|
||||
become_pwd='pw'
|
||||
)
|
||||
# alice should get uid/gid 1001
|
||||
self.assertEqual(users['alice']['uid'], 1001)
|
||||
self.assertEqual(users['alice']['gid'], 1001)
|
||||
self.assertEqual(users['alice']['email'], 'alice@example.com')
|
||||
# bob overrides
|
||||
self.assertEqual(users['bob']['uid'], 2000)
|
||||
self.assertEqual(users['bob']['gid'], 2000)
|
||||
self.assertEqual(users['bob']['email'], 'bob@custom.com')
|
||||
self.assertIn('description', users['bob'])
|
||||
# carol should get next free id = 1002
|
||||
self.assertEqual(users['carol']['uid'], 1002)
|
||||
self.assertEqual(users['carol']['gid'], 1002)
|
||||
|
||||
def test_build_users_default_lookup_password(self):
|
||||
"""
|
||||
When no 'password' override is provided,
|
||||
the become_pwd lookup template string must be used as the password.
|
||||
"""
|
||||
defs = {'frank': {}}
|
||||
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
users = generate_users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
start_id=1001,
|
||||
become_pwd=lookup_template
|
||||
)
|
||||
self.assertEqual(
|
||||
users['frank']['password'],
|
||||
lookup_template,
|
||||
"The lookup template string was not correctly applied as the default password"
|
||||
)
|
||||
|
||||
def test_build_users_override_password(self):
|
||||
"""
|
||||
When a 'password' override is provided,
|
||||
that custom password must be used instead of become_pwd.
|
||||
"""
|
||||
defs = {'eva': {'password': 'custompw'}}
|
||||
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
users = generate_users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
start_id=1001,
|
||||
become_pwd=lookup_template
|
||||
)
|
||||
self.assertEqual(
|
||||
users['eva']['password'],
|
||||
'custompw',
|
||||
"The override password was not correctly applied"
|
||||
)
|
||||
|
||||
|
||||
def test_build_users_duplicate_override_uid(self):
|
||||
defs = {
|
||||
'u1': {'uid': 1001},
|
||||
'u2': {'uid': 1001}
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
generate_users.build_users(defs, 'ex.com', 1001, 'pw')
|
||||
|
||||
def test_build_users_shared_gid_allowed(self):
|
||||
# Allow two users to share the same GID when one overrides gid and the other uses that as uid
|
||||
defs = {
|
||||
'a': {'uid': 1500},
|
||||
'b': {'gid': 1500}
|
||||
}
|
||||
users = generate_users.build_users(defs, 'ex.com', 1500, 'pw')
|
||||
# Both should have gid 1500
|
||||
self.assertEqual(users['a']['gid'], 1500)
|
||||
self.assertEqual(users['b']['gid'], 1500)
|
||||
|
||||
def test_build_users_duplicate_username_email(self):
|
||||
defs = {
|
||||
'u1': {'username': 'same', 'email': 'same@ex.com'},
|
||||
'u2': {'username': 'same'}
|
||||
}
|
||||
# second user with same username should raise
|
||||
with self.assertRaises(ValueError):
|
||||
generate_users.build_users(defs, 'ex.com', 1001, 'pw')
|
||||
|
||||
def test_dictify_converts_ordereddict(self):
|
||||
od = generate_users.OrderedDict([('a', 1), ('b', {'c': 2})])
|
||||
result = generate_users.dictify(OrderedDict(od))
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result, {'a': 1, 'b': {'c': 2}})
|
||||
|
||||
def test_load_user_defs_and_conflict(self):
|
||||
# create temp roles structure
|
||||
tmp = tempfile.mkdtemp()
|
||||
try:
|
||||
os.makedirs(os.path.join(tmp, 'role1/meta'))
|
||||
os.makedirs(os.path.join(tmp, 'role2/meta'))
|
||||
# role1 defines user x
|
||||
with open(os.path.join(tmp, 'role1/meta/users.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
|
||||
# role2 defines same user x with same value
|
||||
with open(os.path.join(tmp, 'role2/meta/users.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
|
||||
defs = generate_users.load_user_defs(tmp)
|
||||
self.assertIn('x', defs)
|
||||
# now conflict definition
|
||||
with open(os.path.join(tmp, 'role2/meta/users.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@b'}}}, f)
|
||||
with self.assertRaises(ValueError):
|
||||
generate_users.load_user_defs(tmp)
|
||||
finally:
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in New Issue
Block a user