mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 04:08:15 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			04deeef385
			...
			e5e394d470
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e5e394d470 | |||
| d796158c61 | 
@@ -79,11 +79,13 @@ activate_all_timers:                       false   # Activates all timers, indep
 | 
			
		||||
# You SHOULD NOT enable this on production servers
 | 
			
		||||
enable_debug:                             false
 | 
			
		||||
 | 
			
		||||
dns_provider:                             cloudflare              # The DNS Provider\Registrar for the domain
 | 
			
		||||
 | 
			
		||||
# Which ACME method to use: webroot, cloudflare, or hetzner
 | 
			
		||||
certbot_acme_challenge_method:            "cloudflare"
 | 
			
		||||
certbot_credentials_dir:                  /etc/certbot
 | 
			
		||||
certbot_credentials_file:                 "{{ certbot_credentials_dir }}/{{ certbot_acme_challenge_method }}.ini"
 | 
			
		||||
# certbot_dns_api_token                   # Define in inventory file
 | 
			
		||||
# certbot_dns_api_token                                           # Define in inventory file
 | 
			
		||||
certbot_dns_propagation_wait_seconds:     40                      # How long should the script wait for DNS propagation before continuing
 | 
			
		||||
certbot_flavor:                           san                     # Possible options: san (recommended, with a dns flavor like cloudflare, or hetzner), wildcard(doesn't function with www redirect), deicated
 | 
			
		||||
certbot_webroot_path:                     "/var/lib/letsencrypt/" # Path used by Certbot to serve HTTP-01 ACME challenges
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								library/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								library/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										92
									
								
								library/cert_check_exists.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								library/cert_check_exists.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
#!/usr/bin/python
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import, division, print_function
 | 
			
		||||
__metaclass__ = type
 | 
			
		||||
 | 
			
		||||
DOCUMENTATION = r'''
 | 
			
		||||
---
 | 
			
		||||
module: cert_check_exists
 | 
			
		||||
short_description: Check if a SSL certificate exists for a domain
 | 
			
		||||
description:
 | 
			
		||||
  - Checks if any certificate covers the given domain.
 | 
			
		||||
options:
 | 
			
		||||
  domain:
 | 
			
		||||
    description:
 | 
			
		||||
      - Domain name to check for in the certificates.
 | 
			
		||||
    required: true
 | 
			
		||||
    type: str
 | 
			
		||||
  cert_base_path:
 | 
			
		||||
    description:
 | 
			
		||||
      - Path where certificates are stored.
 | 
			
		||||
    required: false
 | 
			
		||||
    type: str
 | 
			
		||||
    default: /etc/letsencrypt/live
 | 
			
		||||
  debug:
 | 
			
		||||
    description:
 | 
			
		||||
      - Enable verbose debug output.
 | 
			
		||||
    required: false
 | 
			
		||||
    type: bool
 | 
			
		||||
    default: false
 | 
			
		||||
author:
 | 
			
		||||
  - Kevin Veen-Birkenbach
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
EXAMPLES = r'''
 | 
			
		||||
- name: Check if cert exists
 | 
			
		||||
  cert_check_exists:
 | 
			
		||||
    domain: "matomo.cymais.cloud"
 | 
			
		||||
    cert_base_path: "/etc/letsencrypt/live"
 | 
			
		||||
  register: result
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
RETURN = r'''
 | 
			
		||||
exists:
 | 
			
		||||
  description: True if a certificate covering the domain exists, false otherwise.
 | 
			
		||||
  type: bool
 | 
			
		||||
  returned: always
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from ansible.module_utils.basic import AnsibleModule
 | 
			
		||||
from ansible.module_utils.cert_utils import CertUtils
 | 
			
		||||
 | 
			
		||||
def cert_exists(domain, cert_files, debug=False):
 | 
			
		||||
    for cert_path in cert_files:
 | 
			
		||||
        cert_text = CertUtils.run_openssl(cert_path)
 | 
			
		||||
        if not cert_text:
 | 
			
		||||
            continue
 | 
			
		||||
        sans = CertUtils.extract_sans(cert_text)
 | 
			
		||||
        if debug:
 | 
			
		||||
            print(f"Checking {cert_path}: {sans}")
 | 
			
		||||
        for entry in sans:
 | 
			
		||||
            if entry == domain or (entry.startswith('*.') and domain.endswith('.' + entry[2:])):
 | 
			
		||||
                return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
def cert_check_exists(module):
 | 
			
		||||
    domain = module.params['domain']
 | 
			
		||||
    cert_base_path = module.params['cert_base_path']
 | 
			
		||||
    debug = module.params['debug']
 | 
			
		||||
 | 
			
		||||
    cert_files = CertUtils.list_cert_files(cert_base_path)
 | 
			
		||||
 | 
			
		||||
    exists = cert_exists(domain, cert_files, debug)
 | 
			
		||||
 | 
			
		||||
    module.exit_json(exists=exists)
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    module_args = dict(
 | 
			
		||||
        domain=dict(type='str', required=True),
 | 
			
		||||
        cert_base_path=dict(type='str', required=False, default='/etc/letsencrypt/live'),
 | 
			
		||||
        debug=dict(type='bool', required=False, default=False),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    module = AnsibleModule(
 | 
			
		||||
        argument_spec=module_args,
 | 
			
		||||
        supports_check_mode=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    cert_check_exists(module)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
@@ -5,7 +5,7 @@ __metaclass__ = type
 | 
			
		||||
 | 
			
		||||
DOCUMENTATION = r'''
 | 
			
		||||
---
 | 
			
		||||
module: find_cert_folder
 | 
			
		||||
module: cert_folder_find
 | 
			
		||||
short_description: Find SSL certificate folder covering a given domain
 | 
			
		||||
description:
 | 
			
		||||
  - Searches through certificates to find a folder that covers the given domain.
 | 
			
		||||
@@ -38,7 +38,7 @@ author:
 | 
			
		||||
 | 
			
		||||
EXAMPLES = r'''
 | 
			
		||||
- name: Find cert folder for matomo.cymais.cloud
 | 
			
		||||
  find_cert_folder:
 | 
			
		||||
  cert_folder_find:
 | 
			
		||||
    domain: "matomo.cymais.cloud"
 | 
			
		||||
    certbot_flavor: "san"
 | 
			
		||||
    cert_base_path: "/etc/letsencrypt/live"
 | 
			
		||||
@@ -53,42 +53,18 @@ folder:
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
from ansible.module_utils.basic import AnsibleModule
 | 
			
		||||
 | 
			
		||||
def run_openssl(cert_path):
 | 
			
		||||
    try:
 | 
			
		||||
        output = subprocess.check_output(
 | 
			
		||||
            ['openssl', 'x509', '-in', cert_path, '-noout', '-text'],
 | 
			
		||||
            universal_newlines=True
 | 
			
		||||
        )
 | 
			
		||||
        return output
 | 
			
		||||
    except subprocess.CalledProcessError:
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
def extract_sans(cert_text):
 | 
			
		||||
    dns_entries = []
 | 
			
		||||
    in_san = False
 | 
			
		||||
    for line in cert_text.splitlines():
 | 
			
		||||
        line = line.strip()
 | 
			
		||||
        if 'X509v3 Subject Alternative Name:' in line:
 | 
			
		||||
            in_san = True
 | 
			
		||||
            continue
 | 
			
		||||
        if in_san:
 | 
			
		||||
            if not line:
 | 
			
		||||
                break
 | 
			
		||||
            dns_entries += [e.strip().replace('DNS:', '') for e in line.split(',') if e.strip()]
 | 
			
		||||
    return dns_entries
 | 
			
		||||
from ansible.module_utils.cert_utils import CertUtils  # IMPORT
 | 
			
		||||
 | 
			
		||||
def find_matching_folders(domain, cert_files, flavor, debug):
 | 
			
		||||
    exact_matches = []
 | 
			
		||||
    wildcard_matches = []
 | 
			
		||||
 | 
			
		||||
    for cert_path in cert_files:
 | 
			
		||||
        cert_text = run_openssl(cert_path)
 | 
			
		||||
        cert_text = CertUtils.run_openssl(cert_path)
 | 
			
		||||
        if not cert_text:
 | 
			
		||||
            continue
 | 
			
		||||
        sans = extract_sans(cert_text)
 | 
			
		||||
        sans = CertUtils.extract_sans(cert_text)
 | 
			
		||||
        if debug:
 | 
			
		||||
            print(f"Checking {cert_path}: {sans}")
 | 
			
		||||
        for entry in sans:
 | 
			
		||||
@@ -106,16 +82,13 @@ def find_matching_folders(domain, cert_files, flavor, debug):
 | 
			
		||||
    else:
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
def find_cert_folder(module):
 | 
			
		||||
def cert_folder_find(module):
 | 
			
		||||
    domain = module.params['domain']
 | 
			
		||||
    certbot_flavor = module.params['certbot_flavor']
 | 
			
		||||
    cert_base_path = module.params['cert_base_path']
 | 
			
		||||
    debug = module.params['debug']
 | 
			
		||||
 | 
			
		||||
    cert_files = []
 | 
			
		||||
    for root, dirs, files in os.walk(cert_base_path):
 | 
			
		||||
        if 'cert.pem' in files:
 | 
			
		||||
            cert_files.append(os.path.join(root, 'cert.pem'))
 | 
			
		||||
    cert_files = CertUtils.list_cert_files(cert_base_path)
 | 
			
		||||
 | 
			
		||||
    if debug:
 | 
			
		||||
        print(f"Found {len(cert_files)} cert.pem files under {cert_base_path}")
 | 
			
		||||
@@ -126,7 +99,7 @@ def find_cert_folder(module):
 | 
			
		||||
        if debug:
 | 
			
		||||
            print("Fallback: searching SAN matches without SAN structure parsing")
 | 
			
		||||
        for cert_path in cert_files:
 | 
			
		||||
            cert_text = run_openssl(cert_path)
 | 
			
		||||
            cert_text = CertUtils.run_openssl(cert_path)
 | 
			
		||||
            if f"DNS:{domain}" in cert_text:
 | 
			
		||||
                preferred.append(os.path.dirname(cert_path))
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +124,7 @@ def main():
 | 
			
		||||
        supports_check_mode=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    find_cert_folder(module)
 | 
			
		||||
    cert_folder_find(module)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										39
									
								
								module_utils/cert_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								module_utils/cert_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
#!/usr/bin/python
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
class CertUtils:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def run_openssl(cert_path):
 | 
			
		||||
        try:
 | 
			
		||||
            output = subprocess.check_output(
 | 
			
		||||
                ['openssl', 'x509', '-in', cert_path, '-noout', '-text'],
 | 
			
		||||
                universal_newlines=True
 | 
			
		||||
            )
 | 
			
		||||
            return output
 | 
			
		||||
        except subprocess.CalledProcessError:
 | 
			
		||||
            return ""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def extract_sans(cert_text):
 | 
			
		||||
        dns_entries = []
 | 
			
		||||
        in_san = False
 | 
			
		||||
        for line in cert_text.splitlines():
 | 
			
		||||
            line = line.strip()
 | 
			
		||||
            if 'X509v3 Subject Alternative Name:' in line:
 | 
			
		||||
                in_san = True
 | 
			
		||||
                continue
 | 
			
		||||
            if in_san:
 | 
			
		||||
                if not line:
 | 
			
		||||
                    break
 | 
			
		||||
                dns_entries += [e.strip().replace('DNS:', '') for e in line.split(',') if e.strip()]
 | 
			
		||||
        return dns_entries
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def list_cert_files(cert_base_path):
 | 
			
		||||
        cert_files = []
 | 
			
		||||
        for root, dirs, files in os.walk(cert_base_path):
 | 
			
		||||
            if 'cert.pem' in files:
 | 
			
		||||
                cert_files.append(os.path.join(root, 'cert.pem'))
 | 
			
		||||
        return cert_files
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
collections:
 | 
			
		||||
  - name: kewlfft.aur
 | 
			
		||||
  - name: community.general
 | 
			
		||||
pacman:
 | 
			
		||||
  - ansible
 | 
			
		||||
  - python-passlib
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								roles/dns-records-cloudflare/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								roles/dns-records-cloudflare/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
# Cloudflare DNS Records
 | 
			
		||||
 | 
			
		||||
## Description
 | 
			
		||||
 | 
			
		||||
This Ansible role automates the management of DNS A-records in Cloudflare zones. It uses the [community.general.cloudflare_dns](https://docs.ansible.com/ansible/latest/collections/community/general/cloudflare_dns_module.html) module to create or update A-records for a list of domains, automatically detects the correct zone for each record, and supports configurable proxy settings.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
Looping over a provided list of domains (`cloudflare_domains`), this role:
 | 
			
		||||
- Determines the zone name by extracting the last two labels of each domain.
 | 
			
		||||
- Ensures an A-record for each domain points to the specified IP (`cloudflare_target_ip`).
 | 
			
		||||
- Honors the `proxied` flag to switch between DNS-only and Cloudflare-proxied modes.
 | 
			
		||||
- Provides an optional debug task (`enable_debug`) to output the domain list before changes.
 | 
			
		||||
 | 
			
		||||
Ideal for environments where bulk or dynamic DNS updates are needed, this role abstracts away the complexity of Cloudflare’s zone and record API.
 | 
			
		||||
 | 
			
		||||
## Purpose
 | 
			
		||||
 | 
			
		||||
Cloudflare DNS Records delivers an idempotent, scalable solution for managing A-records across multiple Cloudflare zones. Whether you need to onboard hundreds of domains or toggle proxy settings in CI/CD pipelines, this role handles the orchestration and ensures consistency.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- **Automatic Zone Detection:** Parses each domain to derive its zone (`example.com`) without manual intervention.  
 | 
			
		||||
- **Bulk Record Management:** Creates or updates A-records for all entries in `cloudflare_domains`.  
 | 
			
		||||
- **Proxy Toggle:** Configure `proxied: true` or `false` per record to switch between DNS-only and proxied modes.  
 | 
			
		||||
- **Debug Support:** Enable `enable_debug` to print the domain list for validation before execution.  
 | 
			
		||||
- **Flexible Authentication:** Supports both API token (`api_token`) and Global API key + email.  
 | 
			
		||||
- **Low-TTL Option:** Use `ttl: 1` for rapid DNS propagation during dynamic updates.
 | 
			
		||||
 | 
			
		||||
## Author
 | 
			
		||||
 | 
			
		||||
Kevin Veen-Birkenbach
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
CyMaIS NonCommercial License (CNCL)  
 | 
			
		||||
<https://s.veen.world/cncl>
 | 
			
		||||
							
								
								
									
										26
									
								
								roles/dns-records-cloudflare/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								roles/dns-records-cloudflare/meta/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
---
 | 
			
		||||
galaxy_info:
 | 
			
		||||
  author: "Kevin Veen-Birkenbach"
 | 
			
		||||
  description: "Manages DNS A-records in Cloudflare zones."
 | 
			
		||||
  license: "CyMaIS NonCommercial License (CNCL)"
 | 
			
		||||
  license_url: "https://s.veen.world/cncl"
 | 
			
		||||
  company: |
 | 
			
		||||
    Kevin Veen-Birkenbach
 | 
			
		||||
    Consulting & Coaching Solutions
 | 
			
		||||
    https://www.veen.world
 | 
			
		||||
  min_ansible_version: "2.9"
 | 
			
		||||
  platforms:
 | 
			
		||||
    - name: "All"
 | 
			
		||||
      versions:
 | 
			
		||||
        - "all"
 | 
			
		||||
  galaxy_tags:
 | 
			
		||||
    - "cloudflare"
 | 
			
		||||
    - "dns"
 | 
			
		||||
    - "records"
 | 
			
		||||
    - "ansible"
 | 
			
		||||
    - "network"
 | 
			
		||||
    - "automation"
 | 
			
		||||
  repository: "https://s.veen.world/cymais"
 | 
			
		||||
  issue_tracker_url: "https://s.veen.world/cymaisissues"
 | 
			
		||||
  documentation: "https://s.veen.world/cymais"
 | 
			
		||||
dependencies: []
 | 
			
		||||
							
								
								
									
										18
									
								
								roles/dns-records-cloudflare/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								roles/dns-records-cloudflare/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
- name: "Debug: cloudflare_domains"
 | 
			
		||||
  debug:
 | 
			
		||||
    var: cloudflare_domains
 | 
			
		||||
  when: enable_debug
 | 
			
		||||
 | 
			
		||||
- name: Create or update Cloudflare A-record for {{ item }}
 | 
			
		||||
  community.general.cloudflare_dns:
 | 
			
		||||
    api_token: "{{ cloudflare_api_token }}"
 | 
			
		||||
    zone: "{{ item.split('.')[-2:] | join('.') }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    type: A
 | 
			
		||||
    name: "{{ item }}"
 | 
			
		||||
    content: "{{ cloudflare_target_ip }}"
 | 
			
		||||
    ttl: 1
 | 
			
		||||
    proxied: "{{ cloudflare_proxied | int }}"
 | 
			
		||||
  loop: "{{ cloudflare_domains }}"
 | 
			
		||||
  loop_control:
 | 
			
		||||
    label: "{{ item }}"
 | 
			
		||||
@@ -28,4 +28,4 @@ galaxy_info:
 | 
			
		||||
  issue_tracker_url: https://s.veen.world/cymaisissues
 | 
			
		||||
  documentation: https://s.veen.world/cymais
 | 
			
		||||
  logo:
 | 
			
		||||
    class: "fa-solid fa-chalkboard-teacher"
 | 
			
		||||
    class: "fa-solid fa-chalkboard-teacher"
 | 
			
		||||
@@ -1,12 +1,21 @@
 | 
			
		||||
---
 | 
			
		||||
- name: "include role nginx-domain-setup for {{application_id}}"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: nginx-domain-setup
 | 
			
		||||
 | 
			
		||||
# Docker Central Database Role can't be used here
 | 
			
		||||
- name: "include docker-compose role"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: docker-compose
 | 
			
		||||
 | 
			
		||||
- name:                 "Seed BigBlueButton Database for Backup"
 | 
			
		||||
  include_tasks:        "{{ playbook_dir }}/roles/backup-docker-to-local/tasks/seed-database-to-backup.yml"
 | 
			
		||||
  vars:
 | 
			
		||||
    database_instance:  "{{ application_id }}"
 | 
			
		||||
    database_password:  "{{ applications[application_id].credentials.postgresql_secret }}"
 | 
			
		||||
    database_username:  "postgres"
 | 
			
		||||
    database_name:      ""                              # Multiple databases
 | 
			
		||||
 | 
			
		||||
- name: "include role nginx-domain-setup for {{application_id}}"
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: nginx-domain-setup
 | 
			
		||||
 | 
			
		||||
- name: pull docker repository
 | 
			
		||||
  git:
 | 
			
		||||
    repo: "https://github.com/bigbluebutton/docker.git"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
ENABLE_COTURN=true
 | 
			
		||||
COTURN_TLS_CERT_PATH={{ certbot_cert_path }}/{{ ssl_cert_folder }}/fullchain.pem
 | 
			
		||||
COTURN_TLS_KEY_PATH={{ certbot_cert_path }}/{{ ssl_cert_folder }}/privkey.pem
 | 
			
		||||
ENABLE_GREENLIGHT={{applications.bigbluebutton.enable_greenlight}}
 | 
			
		||||
ENABLE_GREENLIGHT={{applications[application_id].enable_greenlight}}
 | 
			
		||||
 | 
			
		||||
# Enable Webhooks
 | 
			
		||||
# used by some integrations
 | 
			
		||||
@@ -27,13 +27,11 @@ RECORDING_MAX_AGE_DAYS=365
 | 
			
		||||
# SECRETS
 | 
			
		||||
# ====================================
 | 
			
		||||
# important! change these to any random values
 | 
			
		||||
SHARED_SECRET={{applications.bigbluebutton.credentials.shared_secret}}
 | 
			
		||||
ETHERPAD_API_KEY={{applications.bigbluebutton.credentials.etherpad_api_key}}
 | 
			
		||||
RAILS_SECRET={{applications.bigbluebutton.credentials.rails_secret}}
 | 
			
		||||
POSTGRESQL_SECRET={{applications.bigbluebutton.credentials.postgresql_secret}}
 | 
			
		||||
FSESL_PASSWORD={{applications.bigbluebutton.credentials.fsesl_password}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SHARED_SECRET={{applications[application_id].credentials.shared_secret}}
 | 
			
		||||
ETHERPAD_API_KEY={{applications[application_id].credentials.etherpad_api_key}}
 | 
			
		||||
RAILS_SECRET={{applications[application_id].credentials.rails_secret}}
 | 
			
		||||
POSTGRESQL_SECRET={{applications[application_id].credentials.postgresql_secret}}
 | 
			
		||||
FSESL_PASSWORD={{applications[application_id].credentials.fsesl_password}}
 | 
			
		||||
 | 
			
		||||
# ====================================
 | 
			
		||||
# CONNECTION
 | 
			
		||||
@@ -53,7 +51,7 @@ STUN_PORT={{ ports.public.stun[application_id] }}
 | 
			
		||||
# TURN SERVER
 | 
			
		||||
# uncomment and adjust following two lines to add an external TURN server
 | 
			
		||||
TURN_SERVER=turns:{{domains[application_id]}}:{{ ports.public.turn[application_id] }}?transport=tcp
 | 
			
		||||
TURN_SECRET={{applications.bigbluebutton.credentials.turn_secret}}
 | 
			
		||||
TURN_SECRET={{applications[application_id].credentials.turn_secret}}
 | 
			
		||||
 | 
			
		||||
# Allowed SIP IPs
 | 
			
		||||
# due to high traffic caused by bots, by default the SIP port is blocked.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,13 @@
 | 
			
		||||
application_id:               "bigbluebutton"
 | 
			
		||||
bbb_repository_directory:     "{{ docker_compose.directories.services }}"
 | 
			
		||||
docker_compose_file_origine:  "{{ docker_compose.directories.services }}docker-compose.yml"
 | 
			
		||||
docker_compose_file_final:    "{{ docker_compose.directories.instance }}docker-compose.yml"
 | 
			
		||||
application_id:                 "bigbluebutton"
 | 
			
		||||
bbb_repository_directory:       "{{ docker_compose.directories.services }}"
 | 
			
		||||
docker_compose_file_origine:    "{{ docker_compose.directories.services }}docker-compose.yml"
 | 
			
		||||
docker_compose_file_final:      "{{ docker_compose.directories.instance }}docker-compose.yml"
 | 
			
		||||
 | 
			
		||||
# Database configuration
 | 
			
		||||
database_instance:            "bigbluebutton"
 | 
			
		||||
database_name:                "multiple_databases"
 | 
			
		||||
database_username:            "postgres"
 | 
			
		||||
database_password:            "{{ applications.bigbluebutton.credentials.postgresql_secret }}"
 | 
			
		||||
database_type:                  "postgres"
 | 
			
		||||
database_password:              "{{ applications.bigbluebutton.credentials.postgresql_secret }}"
 | 
			
		||||
 | 
			
		||||
domain: 	                    "{{ domains[application_id] }}"
 | 
			
		||||
http_port: 	                  "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
bbb_env_file_link:            "{{ docker_compose.directories.instance }}.env"    
 | 
			
		||||
bbb_env_file_origine:         "{{ bbb_repository_directory }}.env"
 | 
			
		||||
domain: 	                      "{{ domains[application_id] }}"
 | 
			
		||||
http_port: 	                    "{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
bbb_env_file_link:              "{{ docker_compose.directories.instance }}.env"    
 | 
			
		||||
bbb_env_file_origine:           "{{ bbb_repository_directory }}.env"
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
database_instance:  "{{ 'central-' + database_type if applications[application_id].features.database | bool else application_id }}"
 | 
			
		||||
database_host:      "{{ 'central-' + database_type if applications[application_id].features.database | bool else 'database' }}"
 | 
			
		||||
database_name:      "{{ application_id }}"
 | 
			
		||||
database_username:  "{{ application_id }}"
 | 
			
		||||
database_name:      "{{ applications[application_id].credentials.database.name | default( application_id ) }}"      # The overwritte configuration is needed by bigbluebutton
 | 
			
		||||
database_username:  "{{ applications[application_id].credentials.database.username | default( application_id )}}"   # The overwritte configuration is needed by bigbluebutton
 | 
			
		||||
database_port:      "{{ 3306 if database_type == 'mariadb' else 5432 }}"
 | 
			
		||||
database_env:       "{{docker_compose.directories.env}}{{database_type}}.env"
 | 
			
		||||
database_url_jdbc:  "jdbc:{{ database_type if database_type == 'mariadb' else 'postgresql' }}://{{ database_host }}:{{ database_port }}/{{ database_name }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
server {
 | 
			
		||||
    listen {{ports.public.ldaps.ldap}}ssl;
 | 
			
		||||
    proxy_pass 127.0.0.1:{{ports.localhost.ldap.ldap}};
 | 
			
		||||
 | 
			
		||||
    # SSL Configuration for LDAPS
 | 
			
		||||
    
 | 
			
		||||
    {% include 'roles/letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
    ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
    ssl_ciphers HIGH:!aNULL:!MD5;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
listen 443 ssl;
 | 
			
		||||
listen [::]:443 ssl;
 | 
			
		||||
http2 on;
 | 
			
		||||
listen 443 ssl http2;
 | 
			
		||||
listen [::]:443 ssl http2;
 | 
			
		||||
 | 
			
		||||
ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
ssl_ecdh_curve X25519:P-256;
 | 
			
		||||
ssl_prefer_server_ciphers on;
 | 
			
		||||
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
 | 
			
		||||
 | 
			
		||||
ssl_session_timeout 1d;
 | 
			
		||||
ssl_session_cache shared:SSL:50m;
 | 
			
		||||
ssl_session_tickets on;
 | 
			
		||||
ssl_prefer_server_ciphers on;
 | 
			
		||||
add_header Strict-Transport-Security max-age=15768000;
 | 
			
		||||
ssl_stapling on;
 | 
			
		||||
ssl_stapling_verify on;
 | 
			
		||||
{% include 'roles/letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
 | 
			
		||||
{% include 'roles/letsencrypt/templates/ssl_credentials.j2' %}
 | 
			
		||||
@@ -1,52 +1,50 @@
 | 
			
		||||
---
 | 
			
		||||
- name: "Remove Nginx configuration for deprecated domains"
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    cmd: >-
 | 
			
		||||
      rm -fv /etc/nginx/conf.d/http/servers/*.{{ item }}.conf;
 | 
			
		||||
      rm -fv /etc/nginx/conf.d/http/servers/{{ item }}.conf
 | 
			
		||||
- name: Include task to remove deprecated nginx configs
 | 
			
		||||
  include_tasks: remove_deprecated_nginx_configs.yml
 | 
			
		||||
  loop: "{{ deprecated_domains }}"
 | 
			
		||||
  loop_control:
 | 
			
		||||
    label: "{{ item }}"
 | 
			
		||||
  notify: restart nginx
 | 
			
		||||
  vars:
 | 
			
		||||
    domain: "{{ item }}"
 | 
			
		||||
  when:
 | 
			
		||||
    - mode_cleanup | bool
 | 
			
		||||
    - run_once_nginx_domains_cleanup is not defined
 | 
			
		||||
 | 
			
		||||
# The revoking just works for the base domain
 | 
			
		||||
- name: "Revoke Certbot certificate for {{ item }}"
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    cmd: "certbot revoke -n --cert-name {{ item }}"
 | 
			
		||||
  become: true
 | 
			
		||||
  loop: "{{ deprecated_domains }}"
 | 
			
		||||
  loop_control:
 | 
			
		||||
    label: "{{ item }}"
 | 
			
		||||
  when:
 | 
			
		||||
    - mode_cleanup | bool
 | 
			
		||||
    - run_once_nginx_domains_cleanup is not defined
 | 
			
		||||
  register: certbot_revoke_result
 | 
			
		||||
  failed_when: >
 | 
			
		||||
    certbot_revoke_result.rc != 0 and
 | 
			
		||||
    'No certificate found with name' not in certbot_revoke_result.stderr
 | 
			
		||||
  changed_when: >
 | 
			
		||||
    certbot_revoke_result.rc == 0
 | 
			
		||||
 | 
			
		||||
# The deleting just works for the base domain
 | 
			
		||||
- name: "Delete Certbot certificate for {{ item }}"
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    cmd: "certbot delete -n --cert-name {{ item }}"
 | 
			
		||||
  become: true
 | 
			
		||||
  loop: "{{ deprecated_domains }}"
 | 
			
		||||
  loop_control:
 | 
			
		||||
    label: "{{ item }}"
 | 
			
		||||
  when:
 | 
			
		||||
    - mode_cleanup | bool
 | 
			
		||||
    - run_once_nginx_domains_cleanup is not defined
 | 
			
		||||
  register: certbot_delete_result
 | 
			
		||||
  failed_when: >
 | 
			
		||||
    certbot_delete_result.rc != 0 and
 | 
			
		||||
    'No certificate found with name' not in certbot_delete_result.stderr
 | 
			
		||||
  changed_when: >
 | 
			
		||||
    certbot_delete_result.rc == 0
 | 
			
		||||
## The revoking just works for the base domain
 | 
			
		||||
#- name: "Revoke Certbot certificate for {{ item }}"
 | 
			
		||||
#  ansible.builtin.command:
 | 
			
		||||
#    cmd: "certbot revoke -n --cert-name {{ item }} --non-interactive"
 | 
			
		||||
#  become: true
 | 
			
		||||
#  loop: "{{ deprecated_domains }}"
 | 
			
		||||
#  loop_control:
 | 
			
		||||
#    label: "{{ item }}"
 | 
			
		||||
#  when:
 | 
			
		||||
#    - mode_cleanup | bool
 | 
			
		||||
#    - run_once_nginx_domains_cleanup is not defined
 | 
			
		||||
#  register: certbot_revoke_result
 | 
			
		||||
#  failed_when: >
 | 
			
		||||
#    certbot_revoke_result.rc != 0 and
 | 
			
		||||
#    'No certificate found with name' not in certbot_revoke_result.stderr
 | 
			
		||||
#  changed_when: >
 | 
			
		||||
#    certbot_revoke_result.rc == 0
 | 
			
		||||
#
 | 
			
		||||
## The deleting just works for the base domain
 | 
			
		||||
#- name: "Delete Certbot certificate for {{ item }}"
 | 
			
		||||
#  ansible.builtin.command:
 | 
			
		||||
#    cmd: "certbot delete -n --cert-name {{ item }} --non-interactive"
 | 
			
		||||
#  become: true
 | 
			
		||||
#  loop: "{{ deprecated_domains }}"
 | 
			
		||||
#  loop_control:
 | 
			
		||||
#    label: "{{ item }}"
 | 
			
		||||
#  when:
 | 
			
		||||
#    - mode_cleanup | bool
 | 
			
		||||
#    - run_once_nginx_domains_cleanup is not defined
 | 
			
		||||
#  register: certbot_delete_result
 | 
			
		||||
#  failed_when: >
 | 
			
		||||
#    certbot_delete_result.rc != 0 and
 | 
			
		||||
#    'No certificate found with name' not in certbot_delete_result.stderr
 | 
			
		||||
#  changed_when: >
 | 
			
		||||
#    certbot_delete_result.rc == 0
 | 
			
		||||
 | 
			
		||||
- name: run the nginx_domains_cleanup role once
 | 
			
		||||
  set_fact:
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
---
 | 
			
		||||
- name: Find matching nginx configs for {{ domain }}
 | 
			
		||||
  ansible.builtin.find:
 | 
			
		||||
    paths: /etc/nginx/conf.d/http/servers
 | 
			
		||||
    patterns: "*.{{ domain }}.conf"
 | 
			
		||||
  register: find_result
 | 
			
		||||
 | 
			
		||||
- name: Remove wildcard nginx configs for {{ domain }}
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "{{ item.path }}"
 | 
			
		||||
    state: absent
 | 
			
		||||
  loop: "{{ find_result.files | default([]) }}"
 | 
			
		||||
  when: item is defined
 | 
			
		||||
  notify: restart nginx
 | 
			
		||||
 | 
			
		||||
- name: Remove exact nginx config for {{ domain }}
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "/etc/nginx/conf.d/http/servers/{{ domain }}.conf"
 | 
			
		||||
    state: absent
 | 
			
		||||
  notify: restart nginx
 | 
			
		||||
@@ -1,3 +1,9 @@
 | 
			
		||||
- name: "Check if certificate already exists for {{ domain }}"
 | 
			
		||||
  cert_check_exists:
 | 
			
		||||
    domain: "{{ domain }}"
 | 
			
		||||
    cert_base_path: "{{ certbot_cert_path }}"
 | 
			
		||||
  register: cert_check
 | 
			
		||||
 | 
			
		||||
- name: "receive certificate for {{ domain }}"
 | 
			
		||||
  command: >-
 | 
			
		||||
    certbot certonly 
 | 
			
		||||
@@ -21,3 +27,4 @@
 | 
			
		||||
    {{ '--test-cert' if mode_test | bool else '' }}
 | 
			
		||||
  register: certbot_result
 | 
			
		||||
  changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout"
 | 
			
		||||
  when: not cert_check.exists
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
    --domains "{{ all_domains | join(',') }}"
 | 
			
		||||
    --certbot-email "{{ users.administrator.email }}"
 | 
			
		||||
    --certbot-acme-challenge-method "{{ certbot_acme_challenge_method }}"
 | 
			
		||||
    --chunk-size 100
 | 
			
		||||
    {% if certbot_acme_challenge_method != 'webroot' %}
 | 
			
		||||
    --certbot-credentials-file "{{ certbot_credentials_file }}"
 | 
			
		||||
    --certbot-dns-propagation-seconds "{{ certbot_dns_propagation_wait_seconds }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,24 @@
 | 
			
		||||
- name: "Include flavor"
 | 
			
		||||
  include_tasks: "{{ role_path }}/tasks/flavors/{{ certbot_flavor }}.yml"
 | 
			
		||||
 | 
			
		||||
- name: "Cleanup dedicated cert for {{ domain }}"
 | 
			
		||||
  command: >-
 | 
			
		||||
    certbot delete --cert-name {{ domain }} --non-interactive
 | 
			
		||||
  when: 
 | 
			
		||||
    - mode_cleanup | bool
 | 
			
		||||
      # Cleanup mode is enabled
 | 
			
		||||
    - certbot_flavor != 'dedicated'
 | 
			
		||||
      # Wildcard certificate is enabled
 | 
			
		||||
    - domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain)
 | 
			
		||||
      # AND: The domain is a direct first-level subdomain of the primary domain
 | 
			
		||||
    - domain != primary_domain  
 | 
			
		||||
      # The domain is not the primary domain
 | 
			
		||||
  register: certbot_result
 | 
			
		||||
  failed_when: certbot_result.rc != 0 and ("No certificate found with name" not in certbot_result.stderr)
 | 
			
		||||
  changed_when: certbot_result.rc == 0 and ("No certificate found with name" not in certbot_result.stderr)
 | 
			
		||||
#- name: "Cleanup dedicated cert for {{ domain }}"
 | 
			
		||||
#  command: >-
 | 
			
		||||
#    certbot delete --cert-name {{ domain }} --non-interactive
 | 
			
		||||
#  when: 
 | 
			
		||||
#    - mode_cleanup | bool
 | 
			
		||||
#      # Cleanup mode is enabled
 | 
			
		||||
#    - certbot_flavor != 'dedicated'
 | 
			
		||||
#      # Wildcard certificate is enabled
 | 
			
		||||
#    - domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain)
 | 
			
		||||
#      # AND: The domain is a direct first-level subdomain of the primary domain
 | 
			
		||||
#    - domain != primary_domain  
 | 
			
		||||
#      # The domain is not the primary domain
 | 
			
		||||
#  register: certbot_result
 | 
			
		||||
#  failed_when: certbot_result.rc != 0 and ("No certificate found with name" not in certbot_result.stderr)
 | 
			
		||||
#  changed_when: certbot_result.rc == 0 and ("No certificate found with name" not in certbot_result.stderr)
 | 
			
		||||
 | 
			
		||||
- name: Find SSL cert folder for domain
 | 
			
		||||
  find_cert_folder:
 | 
			
		||||
  cert_folder_find:
 | 
			
		||||
    domain: "{{ domain }}"
 | 
			
		||||
    certbot_flavor: "{{ certbot_flavor }}"
 | 
			
		||||
    cert_base_path: "{{ certbot_cert_path }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,31 @@
 | 
			
		||||
---
 | 
			
		||||
# 1. Filter all domains with the “www.” prefix
 | 
			
		||||
- name: "Debug: all_domains"
 | 
			
		||||
  debug:
 | 
			
		||||
    var: all_domains
 | 
			
		||||
  when: enable_debug
 | 
			
		||||
 | 
			
		||||
- name: Filter www-prefixed domains from all_domains
 | 
			
		||||
  set_fact:
 | 
			
		||||
    www_domains: "{{ all_domains | select('match', '^www\\.') | list }}"
 | 
			
		||||
 | 
			
		||||
# 2. Build redirect mappings (www.domain → domain)
 | 
			
		||||
- name: Build redirect mappings for www domains
 | 
			
		||||
  set_fact:
 | 
			
		||||
    domain_mappings: >-
 | 
			
		||||
      {{ www_domains
 | 
			
		||||
         | map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }')
 | 
			
		||||
         | map('from_yaml')
 | 
			
		||||
         | list
 | 
			
		||||
      }}
 | 
			
		||||
 | 
			
		||||
# 3. Include the nginx-redirect-domain role to apply these mappings
 | 
			
		||||
- name: Include nginx-redirect-domain role for www-to-bare redirects
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: nginx-redirect-domain
 | 
			
		||||
  vars:
 | 
			
		||||
    domain_mappings: "{{ domain_mappings }}"
 | 
			
		||||
  when: certbot_flavor == 'dedicated'
 | 
			
		||||
    domain_mappings: "{{ www_domains
 | 
			
		||||
       | map('regex_replace',
 | 
			
		||||
             '^www\\.(.+)$',
 | 
			
		||||
             '{ source: \"www.\\1\", target: \"\\1\" }')
 | 
			
		||||
       | map('from_yaml')
 | 
			
		||||
       | list
 | 
			
		||||
    }}"
 | 
			
		||||
 | 
			
		||||
- name: Include DNS role to set redirects
 | 
			
		||||
  include_role:
 | 
			
		||||
    name: dns-records-cloudflare
 | 
			
		||||
  vars:
 | 
			
		||||
    cloudflare_api_token:     "{{ certbot_dns_api_token }}"
 | 
			
		||||
    cloudflare_domains:       "{{ www_domains }}"
 | 
			
		||||
    cloudflare_target_ip:     "{{ networks.internet.ip4 }}"
 | 
			
		||||
    cloudflare_proxied:       false
 | 
			
		||||
  when: dns_provider == 'cloudflare'
 | 
			
		||||
@@ -62,22 +62,28 @@
 | 
			
		||||
    set_fact:
 | 
			
		||||
      service_provider: "{{ defaults_service_provider | combine(service_provider | default({}, true), recursive=True) }}"
 | 
			
		||||
 | 
			
		||||
  - name: Collect all domains (domains, redirect sources + www)
 | 
			
		||||
  - name: Gather base domains (without www)
 | 
			
		||||
    set_fact:
 | 
			
		||||
      all_domains: >-
 | 
			
		||||
      base_domains: >-
 | 
			
		||||
        {{
 | 
			
		||||
          (
 | 
			
		||||
            (
 | 
			
		||||
              domains.values() | flatten
 | 
			
		||||
              + (redirect_domain_mappings | map(attribute='source') | list)
 | 
			
		||||
            )
 | 
			
		||||
            + (
 | 
			
		||||
              domains.values() | flatten
 | 
			
		||||
              + (redirect_domain_mappings | map(attribute='source') | list)
 | 
			
		||||
            ) | map('regex_replace', '^(.*)$', 'www.\\1') | list
 | 
			
		||||
          ) | unique | sort
 | 
			
		||||
          domains.values()
 | 
			
		||||
          | flatten
 | 
			
		||||
          + (redirect_domain_mappings | map(attribute='source') | list)
 | 
			
		||||
        }}
 | 
			
		||||
 | 
			
		||||
  - name: Initialise all_domains as empty list
 | 
			
		||||
    set_fact:
 | 
			
		||||
      all_domains: []
 | 
			
		||||
 | 
			
		||||
  - name: Build all_domains with base + www via loop
 | 
			
		||||
    set_fact:
 | 
			
		||||
      all_domains: "{{ all_domains + [ item, 'www.' ~ item ] }}"
 | 
			
		||||
    loop: "{{ base_domains }}"
 | 
			
		||||
 | 
			
		||||
  - name: Deduplicate and sort all_domains
 | 
			
		||||
    set_fact:
 | 
			
		||||
      all_domains: "{{ all_domains | unique | sort }}"
 | 
			
		||||
 | 
			
		||||
  - name: "Merged Variables"
 | 
			
		||||
    # Add new merged variables here
 | 
			
		||||
    debug:
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,8 @@
 | 
			
		||||
    name: cleanup-docker-anonymous-volumes
 | 
			
		||||
  when: mode_cleanup | bool
 | 
			
		||||
 | 
			
		||||
- name: "Show User Configuration (Important when mailu tokens are created automatic)"
 | 
			
		||||
- name: Show all facts
 | 
			
		||||
  debug:
 | 
			
		||||
    msg:
 | 
			
		||||
      users: "{{users}}"
 | 
			
		||||
    var: ansible_facts
 | 
			
		||||
  when: enable_debug | bool
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ defaults_applications:
 | 
			
		||||
  'iframe':   true,
 | 
			
		||||
  'ldap':     false,
 | 
			
		||||
  'oidc':     true,
 | 
			
		||||
  'database': true,
 | 
			
		||||
  'database': false,
 | 
			
		||||
}) }}{% raw %}
 | 
			
		||||
    credentials:
 | 
			
		||||
#      shared_secret:                                                       # Needs to be defined in inventory file
 | 
			
		||||
@@ -75,6 +75,9 @@ defaults_applications:
 | 
			
		||||
#      postgresql_secret:                                                   # Needs to be defined in inventory file   
 | 
			
		||||
#      fsesl_password:                                                      # Needs to be defined in inventory file
 | 
			
		||||
#      turn_secret:                                                         # Needs to be defined in inventory file  
 | 
			
		||||
      database:
 | 
			
		||||
        name:      "multiple_databases"
 | 
			
		||||
        username:  "postgres2"
 | 
			
		||||
    urls:
 | 
			
		||||
      api:                        "{{ web_protocol }}://{{domains.bigbluebutton}}/bigbluebutton/"  # API Address used by Nextcloud Integration
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user