commit 4c4556c6604ecca7dd52522a2fe29c3f982d4a08 Author: Daniel Berteaud Date: Wed Dec 1 19:13:34 2021 +0100 Update to 2021-12-01 19:13 diff --git a/README.md b/README.md new file mode 100644 index 0000000..82c1a54 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Ansible roles + +I use Ansible. And I use it **a lot**. Like, there's now nearly nothing I deploy manually, without it. As such I've written a lot of roles, to deploy and manage various applications. This include : + +* Basic system configuration +* Authentication (eg, configure LDAP auth, or join an AD domain automatically) +* Plumber layers (like deploy a MySQL server, a PHP stack etc.) +* Authentication services (Samba4 in AD DC mode, Lemonldap::NG etc.) +* Collaborative apps (like Zimbra, Matrix, Etherpad, Seafile, OnlyOffice, Jitsi etc.) +* Monitoring tools (deploy Zabbix agent, proxy and server, Fusion Inventory agent, Graylog server) +* Web applications (GLPI, Ampache, Kanboard, Wordpress, Dolibarr, Matomo, Framadate, Dokuwiki etc.) +* Dev tools (Gitea) +* Security tools (OpenXPKI, Vaultwarden, manage SSH keys etc.) +* A lot more :-) + +Most of my roles are RHEL centric (tested on AlmaLinux now that CentOS Linux is dead), and are made to be deployed on AlmaLinux 8 servers. Basic roles (like basic system configuration, postfix etc.) also support Debian/Ubuntu systems, but are less tested. + +My roles are often dependent on other roles. For example, if you deploy glpi, it'll first pull all the required web and PHP stack. + +Most of the web application roles are made to run behind a reverse proxy. You can use for this the nginx (recommended) or the httpd_front role. + +## how to use this + +Here're the steps to make use of this. Note that this is not a complete ansible how-to, just a quick guide to use my roles. For example, it'll not explain how to make use of ansible-vault to protect sensitive informations. + +* Clone the repo +``` +git clone https://git.lapiole.org/fws/ansible-roles.git +cd ansible-roles +``` + +* Create a few directories +``` +mkdir {inventories,host_vars,group_vars,ssh,config} +``` + +* Create your SSH key. It's advised to set a passphrase to protect it +``` +ssh-keygen -t rsa -b 4096 -f ssh/id_rsa +``` + +* Create the ansible user account on the hosts you want to manage. This can be done manually or can be automated with tools like kickstart (you can have a look at https://ks.lapiole.org/alma8.ks for example). The ansible user must have elevated privileges with sudo (so you have to ensure sudo is installed) +``` +useradd -m ansible +mkdir ~ansible/.ssh +cat <<_EOF > ~ansible/.ssh/authorized_keys +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCwnPxF7vmJA8Jr7I2q6BNRxQIcnlFaA3O58x8532qXIox8fUdYJo0KkjpEl6pBSWGlF4ObTB04/Nks5rhv9Ew+EHO5GvavzVp5L3u8T+PP+idlLlwIERL2R632TBWVbxqvhtc813ozpaMRI7nCabgiIp8rFf4hqYJIn/RMpRdPSQaHrPHQpFEW9uHPbFYZ9+dywY88WXY+VJI1rkIU3NlOAw3GKjEd6iqiOboDl8Ld4qqc+NpqDFPeidYbk5xjKv3l/Y804tdwqO1UYC+psr983rs1Kq91jI/5xSjSQFM51W3HCpZMTzSIt4Swy+m+eqUIrInxMmw72HF2CL+PePHgmusMUBYPdBfqHIxEHEbvPuO67hLAhqH1dUDBp+0oiRSM/J/DX7K+I+jNO43/UtcvnrBjNjzAiiJEG3WRAcBAUpccOu3JHcRN5CLRB26yfLXpFRzUNCnajmdZF7qc0G5gJuy8KpUZ49VTmZmJ0Uzx1rZLaytSjHpf4e5X6F8iTQ1QmORxvCdfdsqoeod7jK384NXq+UD24Y/tEgq/eT7pl3yLCpQo4qKd/aCEBqc2bnLggVRr+WX94ojMdK35qYbdXtLsN5y6L20yde8tGtWY+nmbJzLnqVJ4TKxXKMl7q9Sdj1t7BrqQQIK3H9kP7SZRhWNP6tvNKBgKFgc/k01ldw== ansible@fws.fr +_EOF +chown -R ansible:ansible ~ansible/.ssh/ +chmod 700 ~ansible/.ssh/ +chmod 600 ~ansible/.ssh/authorized_keys +cat <<_EOF > /etc/sudoers.d/ansible +Defaults:ansible !requiretty +ansible ALL=(ALL) NOPASSWD: ALL +_EOF +chmod 600 /etc/sudoers.d/ansible +``` + +* Create your inventory file. For example, inventories/acme.ini +``` +[infra] +db.acme.com +proxyin.acme.com +``` +This will create a single group **infra** with two hosts in it. + +* Create your main playbook. This is the file describing what to deploy on which host. You can store it at in the root dir, for example, acme.yml : +``` +- name: Deploy common profiles + hosts: infra + roles: + - common + - backup + +- name: Deploy databases servers + hosts: db.acme.com + roles: + - mysql_server + - postgresql_server + +- name: Deploy reverse proxy + hosts: proxyin.acme.com + roles: + - nginx + - letsencrypt + - lemonldap_ng +``` +It's pretty self-explanatory. First, roles **common** and **backup** will be deployed on every hosts in the infra group. Then, **mysql_server** and **postgresql_server** will be deployed on **db.acme.com**. And roles **nginx**, **letsencrypt** and **lemonldap_ng** will be deployed on host **proxyin.acme.com** + +* Now, it's time to configure a few things. Configuration is done be assigning values to varibles, and can be done at several levels. + * group_vars/all/vars.yml : variables here will be inherited by every hosts +``` +ansible_become: True +trusted_ip: + - 1.2.3.4 + - 192.168.47.0/24 +zabbix_ip: + - 10.11.12.13 + +system_admin_groups: + - 'admins' +system_admin_users: + - 'dani' +system_admin_email: servers@example.com + +zabbix_agent_encryption: psk +zabbix_agent_servers: "{{ zabbix_ip }}" +zabbix_proxy_encryption: psk +zabbix_proxy_server: 'zabbix.example.com' +``` + * group_vars/infra/vars.yml : variables here will be inherited by hosts in the **infra** group +``` +sshd_src_ip: "{{ trusted_ip }}" +postfix_relay_host: '[smtp.example.com]:587' +postfix_relay_user: smtp +postfix_relay_pass: "S3cretP@ssw0rd" + +ssh_users: + - name: ansible + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCwnPxF7vmJA8Jr7I2q6BNRxQIcnlFaA3O58x8532qXIox8fUdYJo0KkjpEl6pBSWGlF4ObTB04/Nks5rhv9Ew+EHO5GvavzVp5L3u8T+PP+idlLlwIERL2R632TBWVbxqvhtc813ozpaMRI7nCabgiIp8rFf4hqYJIn/RMpRdPSQaHrPHQpFEW9uHPbFYZ9+dywY88WXY+VJI1rkIU3NlOAw3GKjEd6iqiOboDl8Ld4qqc+NpqDFPeidYbk5xjKv3l/Y804tdwqO1UYC+psr983rs1Kq91jI/5xSjSQFM51W3HCpZMTzSIt4Swy+m+eqUIrInxMmw72HF2CL+PePHgmusMUBYPdBfqHIxEHEbvPuO67hLAhqH1dUDBp+0oiRSM/J/DX7K+I+jNO43/UtcvnrBjNjzAiiJEG3WRAcBAUpccOu3JHcRN5CLRB26yfLXpFRzUNCnajmdZF7qc0G5gJuy8KpUZ49VTmZmJ0Uzx1rZLaytSjHpf4e5X6F8iTQ1QmORxvCdfdsqoeod7jK384NXq+UD24Y/tEgq/eT7pl3yLCpQo4qKd/aCEBqc2bnLggVRr+WX94ojMdK35qYbdXtLsN5y6L20yde8tGtWY+nmbJzLnqVJ4TKxXKMl7q9Sdj1t7BrqQQIK3H9kP7SZRhWNP6tvNKBgKFgc/k01ldw== ansible@fws.fr + - name: dani + allow_forwarding: True + ssh_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCwnPxF7vmJA8Jr7I2q6BNRxQIcnlFaA3O58x8532qXIox8fUdYJo0KkjpEl6pBSWGlF4ObTB04/Nks5rhv9Ew+EHO5GvavzVp5L3u8T+PP+idlLlwIERL2R632TBWVbxqvhtc813ozpaMRI7nCabgiIp8rFf4hqYJIn/RMpRdPSQaHrPHQpFEW9uHPbFYZ9+ +dywY88WXY+VJI1rkIU3NlOAw3GKjEd6iqiOboDl8Ld4qqc+NpqDFPeidYbk5xjKv3l/Y804tdwqO1UYC+psr983rs1Kq91jI/5xSjSQFM51W3HCpZMTzSIt4Swy+m+eqUIrInxMmw72HF2CL+PePHgmusMUBYPdBfqHIxEHEbvPuO67hLAhqH1dUDBp+0oiRSM/J/DX7K+I+jNO43/UtcvnrBjNjzAiiJEG3WRAcBAUpccOu3JHcRN5CLRB26yfLXpFRzUNCnajmdZF7qc0G5gJuy8KpUZ49VTmZmJ0Uzx1rZLaytSjHpf4e5X6F8iTQ1QmORxvCdfdsqoeod7jK384NXq+UD24Y/tEgq/eT7pl3yLCpQo4qKd/aCEBqc2bnLggVRr+WX94ojMdK35qYbdXtLsN5y6L20yde8tGtWY+nmbJzLnqVJ4TKxXKMl7q9Sdj1t7BrqQQIK3H9kP7SZRhWNP6tvNKBgKFgc/k01ldw== dani@fws.fr + +# Default database server +mysql_server: db.acme.com +mysql_admin_pass: "r00tP@ss" +pg_server: db.acme.com +pg_admin_pass: "{{ mysql_admin_pass }}" + +letsencrypt_challenge: dns +letsencrypt_dns_provider: gandi +letsencrypt_dns_provider_options: '--api-protocol=rest' +letsencrypt_dns_auth_token: "G7BL9RzkZdUI" +``` + * host_vars/proxyin.acme.com/vars.yml : variables here will be inherited only by the host **proxyin.acme.com** +``` +nginx_auto_letsencrypt_cert: True + +# Default vhost settings +nginx_default_vhost_extra: + auth: llng + csp: >- + default-src 'self' 'unsafe-inline' blob:; + style-src-elem 'self' 'unsafe-inline' data:; + img-src 'self' data: blob: https://stats.fws.fr; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://stats.acme.com blob:; + font-src 'self' data: + proxy: + cache: True + backend: http://web1.acme.com + +nginx_vhosts: + + - name: mail-filter.example.com + proxy: + backend: https://10.64.2.10:8006 + allowed_methods: [GET,HEAD,POST,PUT,DELETE] + src_ip: "{{ trusted_ip }}" + auth: False + + - name: graphes.acme.com + proxy: + backend: http://10.64.3.15:3000 + allowed_methods: [GET,HEAD,POST,PUT,DELETE] + +``` + +## How to check available variables + +Every role has default variables set in the defaults sub folder. You can have a look at it to see which variables are available and what default value they have. + +## Contact + +You can contact me at ansible AT lapiole DOT org if needed diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..5fdbbb5 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,15 @@ +[defaults] +remote_user = ansible +private_key_file = ssh/id_rsa +ansible_managed = Managed by ansible, manual modifications will be lost +ask_vault_pass = True +remote_tmp = /tmp/.ansible-${USER}/tmp +timeout = 30 + +[privilege_escalation] +become=True + +[ssh_connection] +ssh_args = -F ssh/config +control_path = /tmp/ans-ssh-%%C +pipelining = True diff --git a/library/iptables_raw.py b/library/iptables_raw.py new file mode 100644 index 0000000..fd1c863 --- /dev/null +++ b/library/iptables_raw.py @@ -0,0 +1,1089 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +""" +(c) 2016, Strahinja Kustudic +(c) 2016, Damir Markovic + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: iptables_raw +short_description: Manage iptables rules +version_added: "2.5" +description: + - Add/remove iptables rules while keeping state. +options: + backup: + description: + - Create a backup of the iptables state file before overwriting it. + required: false + choices: ["yes", "no"] + default: "no" + ipversion: + description: + - Target the IP version this rule is for. + required: false + default: "4" + choices: ["4", "6"] + keep_unmanaged: + description: + - If set to C(yes) keeps active iptables (unmanaged) rules for the target + C(table) and gives them C(weight=90). This means these rules will be + ordered after most of the rules, since default priority is 40, so they + shouldn't be able to block any allow rules. If set to C(no) deletes all + rules which are not set by this module. + - "WARNING: Be very careful when running C(keep_unmanaged=no) for the + first time, since if you don't specify correct rules, you can block + yourself out of the managed host." + required: false + choices: ["yes", "no"] + default: "yes" + name: + description: + - Name that will be used as an identifier for these rules. It can contain + alphanumeric characters, underscore, hyphen, dot, or a space; has to be + UNIQUE for a specified C(table). You can also pass C(name=*) with + C(state=absent) to flush all rules in the selected table, or even all + tables with C(table=*). + required: true + rules: + description: + - The rules that we want to add. Accepts multiline values. + - "Note: You can only use C(-A)/C(--append), C(-N)/C(--new-chain), and + C(-P)/C(--policy) to specify rules." + required: false + state: + description: + - The state this rules fragment should be in. + choices: ["present", "absent"] + required: false + default: present + table: + description: + - The table this rule applies to. You can specify C(table=*) only with + with C(name=*) and C(state=absent) to flush all rules in all tables. + choices: ["filter", "nat", "mangle", "raw", "security", "*"] + required: false + default: filter + weight: + description: + - Determines the order of the rules. Lower C(weight) means higher + priority. Supported range is C(0 - 99) + choices: ["0 - 99"] + required: false + default: 40 +notes: + - Requires C(iptables) package. Debian-based distributions additionally + require C(iptables-persistent). + - "Depending on the distribution, iptables rules are saved in different + locations, so that they can be loaded on boot. Red Hat distributions (RHEL, + CentOS, etc): C(/etc/sysconfig/iptables) and C(/etc/sysconfig/ip6tables); + Debian distributions (Debian, Ubuntu, etc): C(/etc/iptables/rules.v4) and + C(/etc/iptables/rules.v6); other distributions: C(/etc/sysconfig/iptables) + and C(/etc/sysconfig/ip6tables)." + - This module saves state in C(/etc/ansible-iptables) directory, so don't + modify this directory! +author: + - "Strahinja Kustudic (@kustodian)" + - "Damir Markovic (@damirda)" +''' + +EXAMPLES = ''' +# Allow all IPv4 traffic coming in on port 80 (http) +- iptables_raw: + name: allow_tcp_80 + rules: '-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT' + +# Set default rules with weight 10 and disregard all unmanaged rules +- iptables_raw: + name: default_rules + weight: 10 + keep_unmanaged: no + rules: | + -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + -A INPUT -i lo -j ACCEPT + -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT + -P INPUT DROP + -P FORWARD DROP + -P OUTPUT ACCEPT + +# Allow all IPv6 traffic coming in on port 443 (https) with weight 50 +- iptables_raw: + ipversion: 6 + weight: 50 + name: allow_tcp_443 + rules: '-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT' + +# Remove the above rule +- iptables_raw: + state: absent + ipversion: 6 + name: allow_tcp_443 + +# Define rules with a custom chain +- iptables_raw: + name: custom1_rules + rules: | + -N CUSTOM1 + -A CUSTOM1 -s 192.168.0.0/24 -j ACCEPT + +# Reset all IPv4 iptables rules in all tables and allow all traffic +- iptables_raw: + name: '*' + table: '*' + state: absent +''' + +RETURN = ''' +state: + description: state of the rules + returned: success + type: string + sample: present +name: + description: name of the rules + returned: success + type: string + sample: open_tcp_80 +weight: + description: weight of the rules + returned: success + type: int + sample: 40 +ipversion: + description: IP version of iptables used + returned: success + type: int + sample: 6 +rules: + description: passed rules + returned: success + type: string + sample: "-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT" +table: + description: iptables table used + returned: success + type: string + sample: filter +backup: + description: if the iptables file should backed up + returned: success + type: boolean + sample: False +keep_unmanaged: + description: if it should keep unmanaged rules + returned: success + type: boolean + sample: True +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import json + +import time +import fcntl +import re +import shlex +import os +import tempfile + +try: + from collections import defaultdict +except ImportError: + # This is a workaround for Python 2.4 which doesn't have defaultdict. + class defaultdict(dict): + def __init__(self, default_factory, *args, **kwargs): + super(defaultdict, self).__init__(*args, **kwargs) + self.default_factory = default_factory + + def __getitem__(self, key): + try: + return super(defaultdict, self).__getitem__(key) + except KeyError: + return self.__missing__(key) + + def __missing__(self, key): + try: + self[key] = self.default_factory() + except TypeError: + raise KeyError("Missing key %s" % (key, )) + else: + return self[key] + + +# Genereates a diff dictionary from an old and new table dump. +def generate_diff(dump_old, dump_new): + diff = dict() + if dump_old != dump_new: + diff['before'] = dump_old + diff['after'] = dump_new + return diff + + +def compare_dictionaries(dict1, dict2): + if dict1 is None or dict2 is None: + return False + if not (isinstance(dict1, dict) and isinstance(dict2, dict)): + return False + shared_keys = set(dict2.keys()) & set(dict2.keys()) + if not (len(shared_keys) == len(dict1.keys()) and len(shared_keys) == len(dict2.keys())): + return False + dicts_are_equal = True + for key in dict1.keys(): + if isinstance(dict1[key], dict): + dicts_are_equal = dicts_are_equal and compare_dictionaries(dict1[key], dict2[key]) + else: + dicts_are_equal = dicts_are_equal and (dict1[key] == dict2[key]) + if not dicts_are_equal: + break + return dicts_are_equal + + +class Iptables: + + # Default chains for each table + DEFAULT_CHAINS = { + 'filter': ['INPUT', 'FORWARD', 'OUTPUT'], + 'raw': ['PREROUTING', 'OUTPUT'], + 'nat': ['PREROUTING', 'INPUT', 'OUTPUT', 'POSTROUTING'], + 'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT', 'POSTROUTING'], + 'security': ['INPUT', 'FORWARD', 'OUTPUT'] + } + + # List of tables + TABLES = list(DEFAULT_CHAINS.copy().keys()) + + # Directory which will store the state file. + STATE_DIR = '/etc/ansible-iptables' + + # Key used for unmanaged rules + UNMANAGED_RULES_KEY_NAME = '$unmanaged_rules$' + + # Only allow alphanumeric characters, underscore, hyphen, dots, or a space for + # now. We don't want to have problems while parsing comments using regular + # expressions. + RULE_NAME_ALLOWED_CHARS = 'a-zA-Z0-9_ .-' + + module = None + + def __init__(self, module, ipversion): + # Create directory for json files. + if not os.path.exists(self.STATE_DIR): + os.makedirs(self.STATE_DIR) + if Iptables.module is None: + Iptables.module = module + self.state_save_path = self._get_state_save_path(ipversion) + self.system_save_path = self._get_system_save_path(ipversion) + self.state_dict = self._read_state_file() + self.bins = self._get_bins(ipversion) + self.iptables_names_file = self._get_iptables_names_file(ipversion) + # Check if we have a required iptables version. + self._check_compatibility() + # Save active iptables rules for all tables, so that we don't + # need to fetch them every time using 'iptables-save' command. + self._active_rules = {} + self._refresh_active_rules(table='*') + + def __eq__(self, other): + return (isinstance(other, self.__class__) and compare_dictionaries(other.state_dict, self.state_dict)) + + def __ne__(self, other): + return not self.__eq__(other) + + def _get_bins(self, ipversion): + if ipversion == '4': + return {'iptables': Iptables.module.get_bin_path('iptables'), + 'iptables-save': Iptables.module.get_bin_path('iptables-save'), + 'iptables-restore': Iptables.module.get_bin_path('iptables-restore')} + else: + return {'iptables': Iptables.module.get_bin_path('ip6tables'), + 'iptables-save': Iptables.module.get_bin_path('ip6tables-save'), + 'iptables-restore': Iptables.module.get_bin_path('ip6tables-restore')} + + def _get_iptables_names_file(self, ipversion): + if ipversion == '4': + return '/proc/net/ip_tables_names' + else: + return '/proc/net/ip6_tables_names' + + # Return a list of active iptables tables + def _get_list_of_active_tables(self): + if os.path.isfile(self.iptables_names_file): + table_names = "filter\nnat\nmangle" + open(self.iptables_names_file, 'r').read() + list_set = set(table_names.splitlines()) + unique_list = (list(list_set)) + return unique_list + else: + return [] + + # If /etc/debian_version exist, this means this is a debian based OS (Ubuntu, Mint, etc...) + def _is_debian(self): + return os.path.isfile('/etc/debian_version') + # If /etc/alpine-release exist, this means this is AlpineLinux OS + def _is_alpine(self): + return os.path.isfile('/etc/alpine-release') + + # If /etc/arch-release exist, this means this is an ArchLinux OS + def _is_arch_linux(self): + return os.path.isfile('/etc/arch-release') + + # If /etc/gentoo-release exist, this means this is Gentoo + def _is_gentoo(self): + return os.path.isfile('/etc/gentoo-release') + + # Get the iptables system save path. + # Supports RHEL/CentOS '/etc/sysconfig/' location. + # Supports Debian/Ubuntu/Mint, '/etc/iptables/' location. + # Supports Gentoo, '/var/lib/iptables/' location. + def _get_system_save_path(self, ipversion): + # distro detection, path setting should be added + if self._is_debian(): + # Check if iptables-persistent packages is installed + if not os.path.isdir('/etc/iptables'): + Iptables.module.fail_json(msg="This module requires 'iptables-persistent' package!") + if ipversion == '4': + return '/etc/iptables/rules.v4' + else: + return '/etc/iptables/rules.v6' + elif self._is_arch_linux(): + if ipversion == '4': + return '/etc/iptables/iptables.rules' + else: + return '/etc/iptables/ip6tables.rules' + elif self._is_gentoo(): + if ipversion == '4': + return '/var/lib/iptables/rules-save' + else: + return '/var/lib/ip6tables/rules-save' + + elif self._is_alpine(): + if ipversion == '4': + return '/etc/iptables/rules-save' + else: + return '/etc/iptables/rules6-save' + else: + if ipversion == '4': + return '/etc/sysconfig/iptables' + else: + return '/etc/sysconfig/ip6tables' + + # Return path to json state file. + def _get_state_save_path(self, ipversion): + if ipversion == '4': + return self.STATE_DIR + '/iptables.json' + else: + return self.STATE_DIR + '/ip6tables.json' + + # Checks if iptables is installed and if we have a correct version. + def _check_compatibility(self): + from distutils.version import StrictVersion + cmd = [self.bins['iptables'], '--version'] + rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False) + if rc == 0: + result = re.search(r'^ip6tables\s+v(\d+\.\d+)\.\d+$', stdout) + if result: + version = result.group(1) + # CentOS 5 ip6tables (v1.3.x) doesn't support comments, + # which means it cannot be used with this module. + if StrictVersion(version) < StrictVersion('1.4'): + Iptables.module.fail_json(msg="This module isn't compatible with ip6tables versions older than 1.4.x") + else: + Iptables.module.fail_json(msg="Could not fetch iptables version! Is iptables installed?") + + # Read rules from the json state file and return a dict. + def _read_state_file(self): + json_str = '{}' + if os.path.isfile(self.state_save_path): + try: + json_str = open(self.state_save_path, 'r').read() + except: + Iptables.module.fail_json(msg="Could not read the state file '%s'!" % self.state_save_path) + try: + read_dict = defaultdict(lambda: dict(dump='', rules_dict={}), json.loads(json_str)) + except: + Iptables.module.fail_json(msg="Could not parse the state file '%s'! Please manually delete it to continue." % self.state_save_path) + return read_dict + + # Checks if a table exists in the state_dict. + def _has_table(self, tbl): + return tbl in self.state_dict + + # Deletes table from the state_dict. + def _delete_table(self, tbl): + if self._has_table(tbl): + del self.state_dict[tbl] + + # Acquires lock or exits after wait_for_seconds if it cannot be acquired. + def acquire_lock_or_exit(self, wait_for_seconds=10): + lock_file = self.STATE_DIR + '/.iptables.lock' + i = 0 + f = open(lock_file, 'w+') + while i < wait_for_seconds: + try: + fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) + return + except IOError: + i += 1 + time.sleep(1) + Iptables.module.fail_json(msg="Could not acquire lock to continue execution! " + "Probably another instance of this module is running.") + + # Check if a table has anything to flush (to check all tables pass table='*'). + def table_needs_flush(self, table): + needs_flush = False + if table == '*': + for tbl in Iptables.TABLES: + # If the table exists or if it needs to be flushed that means will make changes. + if self._has_table(tbl) or self._single_table_needs_flush(tbl): + needs_flush = True + break + # Only flush the specified table + else: + if self._has_table(table) or self._single_table_needs_flush(table): + needs_flush = True + return needs_flush + + # Check if a passed table needs to be flushed. + def _single_table_needs_flush(self, table): + needs_flush = False + active_rules = self._get_active_rules(table) + if active_rules: + policies = self._filter_default_chain_policies(active_rules, table) + chains = self._filter_custom_chains(active_rules, table) + rules = self._filter_rules(active_rules, table) + # Go over default policies and check if they are all ACCEPT. + for line in policies.splitlines(): + if not re.search(r'\bACCEPT\b', line): + needs_flush = True + break + # If there is at least one rule or custom chain, that means we need flush. + if len(chains) > 0 or len(rules) > 0: + needs_flush = True + return needs_flush + + # Returns a copy of the rules dict of a passed table. + def _get_table_rules_dict(self, table): + return self.state_dict[table]['rules_dict'].copy() + + # Returns saved table dump. + def get_saved_table_dump(self, table): + return self.state_dict[table]['dump'] + + # Sets saved table dump. + def _set_saved_table_dump(self, table, dump): + self.state_dict[table]['dump'] = dump + + # Updates saved table dump from the active rules. + def refresh_saved_table_dump(self, table): + active_rules = self._get_active_rules(table) + self._set_saved_table_dump(table, active_rules) + + # Sets active rules of the passed table. + def _set_active_rules(self, table, rules): + self._active_rules[table] = rules + + # Return active rules of the passed table. + def _get_active_rules(self, table, clean=True): + active_rules = '' + if table == '*': + all_rules = [] + for tbl in Iptables.TABLES: + if tbl in self._active_rules: + all_rules.append(self._active_rules[tbl]) + active_rules = '\n'.join(all_rules) + else: + active_rules = self._active_rules[table] + if clean: + return self._clean_save_dump(active_rules) + else: + return active_rules + + # Refresh active rules of a table ('*' for all tables). + def _refresh_active_rules(self, table): + if table == '*': + for tbl in Iptables.TABLES: + self._set_active_rules(tbl, self._get_system_active_rules(tbl)) + else: + self._set_active_rules(table, self._get_system_active_rules(table)) + + # Get iptables-save dump of active rules of one or all tables (pass '*') and return it as a string. + def _get_system_active_rules(self, table): + active_tables = self._get_list_of_active_tables() + if table == '*': + cmd = [self.bins['iptables-save']] + # If there are no active tables, that means there are no rules + if not active_tables: + return "" + else: + cmd = [self.bins['iptables-save'], '-t', table] + # If the table is not active, that means it has no rules + if table not in active_tables: + return "" + rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=True) + return stdout + + # Splits a rule into tokens + def _split_rule_into_tokens(self, rule): + try: + return shlex.split(rule, comments=True) + except: + msg = "Could not parse the iptables rule:\n%s" % rule + Iptables.module.fail_json(msg=msg) + + # Removes comment lines and empty lines from rules. + @staticmethod + def clean_up_rules(rules): + cleaned_rules = [] + for line in rules.splitlines(): + # Remove lines with comments and empty lines. + if not (Iptables.is_comment(line) or Iptables.is_empty_line(line)): + cleaned_rules.append(line) + return '\n'.join(cleaned_rules) + + # Checks if the line is a custom chain in specific iptables table. + @staticmethod + def is_custom_chain(line, table): + default_chains = Iptables.DEFAULT_CHAINS[table] + if re.match(r'\s*(:|(-N|--new-chain)\s+)[^\s]+', line) \ + and not re.match(r'\s*(:|(-N|--new-chain)\s+)\b(' + '|'.join(default_chains) + r')\b', line): + return True + else: + return False + + # Checks if the line is a default chain of an iptables table. + @staticmethod + def is_default_chain(line, table): + default_chains = Iptables.DEFAULT_CHAINS[table] + if re.match(r'\s*(:|(-P|--policy)\s+)\b(' + '|'.join(default_chains) + r')\b\s+(ACCEPT|DROP)', line): + return True + else: + return False + + # Checks if a line is an iptables rule. + @staticmethod + def is_rule(line): + # We should only allow adding rules with '-A/--append', since others don't make any sense. + if re.match(r'\s*(-A|--append)\s+[^\s]+', line): + return True + else: + return False + + # Checks if a line starts with '#'. + @staticmethod + def is_comment(line): + if re.match(r'\s*#', line): + return True + else: + return False + + # Checks if a line is empty. + @staticmethod + def is_empty_line(line): + if re.match(r'^$', line.strip()): + return True + else: + return False + + # Return name of custom chain from the rule. + def _get_custom_chain_name(self, line, table): + if Iptables.is_custom_chain(line, table): + return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3) + else: + return '' + + # Return name of default chain from the rule. + def _get_default_chain_name(self, line, table): + if Iptables.is_default_chain(line, table): + return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3) + else: + return '' + + # Return target of the default chain from the rule. + def _get_default_chain_target(self, line, table): + if Iptables.is_default_chain(line, table): + return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)\s+([A-Z]+)', line).group(4) + else: + return '' + + # Removes duplicate custom chains from the table rules. + def _remove_duplicate_custom_chains(self, rules, table): + all_rules = [] + custom_chain_names = [] + for line in rules.splitlines(): + # Extract custom chains. + if Iptables.is_custom_chain(line, table): + chain_name = self._get_custom_chain_name(line, table) + if chain_name not in custom_chain_names: + custom_chain_names.append(chain_name) + all_rules.append(line) + else: + all_rules.append(line) + return '\n'.join(all_rules) + + # Returns current iptables-save dump cleaned from comments and packet/byte counters. + def _clean_save_dump(self, simple_rules): + cleaned_dump = [] + for line in simple_rules.splitlines(): + # Ignore comments. + if Iptables.is_comment(line): + continue + # Reset counters for chains (begin with ':'), for easier comparing later on. + if re.match(r'\s*:', line): + cleaned_dump.append(re.sub(r'\[([0-9]+):([0-9]+)\]', '[0:0]', line)) + else: + cleaned_dump.append(line) + cleaned_dump.append('\n') + return '\n'.join(cleaned_dump) + + # Returns lines with default chain policies. + def _filter_default_chain_policies(self, rules, table): + chains = [] + for line in rules.splitlines(): + if Iptables.is_default_chain(line, table): + chains.append(line) + return '\n'.join(chains) + + # Returns lines with iptables rules from an iptables-save table dump + # (removes chain policies, custom chains, comments and everything else). By + # default returns all rules, if 'only_unmanged=True' returns rules which + # are not managed by Ansible. + def _filter_rules(self, rules, table, only_unmanaged=False): + filtered_rules = [] + for line in rules.splitlines(): + if Iptables.is_rule(line): + if only_unmanaged: + tokens = self._split_rule_into_tokens(line) + # We need to check if a rule has a comment which starts with 'ansible[name]' + if '--comment' in tokens: + comment_index = tokens.index('--comment') + 1 + if comment_index < len(tokens): + # Fetch the comment + comment = tokens[comment_index] + # Skip the rule if the comment starts with 'ansible[name]' + if not re.match(r'ansible\[[' + Iptables.RULE_NAME_ALLOWED_CHARS + r']+\]', comment): + filtered_rules.append(line) + else: + # Fail if there is no comment after the --comment parameter + msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line + Iptables.module.fail_json(msg=msg) + # If it doesn't have comment, this means it is not managed by Ansible and we should append it. + else: + filtered_rules.append(line) + else: + filtered_rules.append(line) + return '\n'.join(filtered_rules) + + # Same as _filter_rules(), but returns custom chains + def _filter_custom_chains(self, rules, table, only_unmanaged=False): + filtered_chains = [] + # Get list of managed custom chains, which is needed to detect unmanaged custom chains + managed_custom_chains_list = self._get_custom_chains_list(table) + for line in rules.splitlines(): + if Iptables.is_custom_chain(line, table): + if only_unmanaged: + # The chain is not managed by this module if it's not in the list of managed custom chains. + chain_name = self._get_custom_chain_name(line, table) + if chain_name not in managed_custom_chains_list: + filtered_chains.append(line) + else: + filtered_chains.append(line) + return '\n'.join(filtered_chains) + + # Returns list of custom chains of a table. + def _get_custom_chains_list(self, table): + custom_chains_list = [] + for key, value in self._get_table_rules_dict(table).items(): + # Ignore UNMANAGED_RULES_KEY_NAME key, since we only want managed custom chains. + if key != Iptables.UNMANAGED_RULES_KEY_NAME: + for line in value['rules'].splitlines(): + if Iptables.is_custom_chain(line, table): + chain_name = self._get_custom_chain_name(line, table) + if chain_name not in custom_chains_list: + custom_chains_list.append(chain_name) + return custom_chains_list + + # Prepends 'ansible[name]: ' to iptables rule '--comment' argument, + # or adds 'ansible[name]' as a comment if there is no comment. + def _prepend_ansible_comment(self, rules, name): + commented_lines = [] + for line in rules.splitlines(): + # Extract rules only since we cannot add comments to custom chains. + if Iptables.is_rule(line): + tokens = self._split_rule_into_tokens(line) + if '--comment' in tokens: + # If there is a comment parameter, we need to prepand 'ansible[name]: '. + comment_index = tokens.index('--comment') + 1 + if comment_index < len(tokens): + # We need to remove double quotes from comments, since there + # is an incompatiblity with older iptables versions + comment_text = tokens[comment_index].replace('"', '') + tokens[comment_index] = 'ansible[' + name + ']: ' + comment_text + else: + # Fail if there is no comment after the --comment parameter + msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line + Iptables.module.fail_json(msg=msg) + else: + # If comment doesn't exist, we add a comment 'ansible[name]' + tokens += ['-m', 'comment', '--comment', 'ansible[' + name + ']'] + # Escape and quote tokens in case they have spaces + tokens = [self._escape_and_quote_string(x) for x in tokens] + commented_lines.append(" ".join(tokens)) + # Otherwise it's a chain, and we should just return it. + else: + commented_lines.append(line) + return '\n'.join(commented_lines) + + # Double quote a string if it contains a space and escape double quotes. + def _escape_and_quote_string(self, s): + escaped = s.replace('"', r'\"') + if re.search(r'\s', escaped): + return '"' + escaped + '"' + else: + return escaped + + # Add table rule to the state_dict. + def add_table_rule(self, table, name, weight, rules, prepend_ansible_comment=True): + self._fail_on_bad_rules(rules, table) + if prepend_ansible_comment: + self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': self._prepend_ansible_comment(rules, name)} + else: + self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': rules} + + # Remove table rule from the state_dict. + def remove_table_rule(self, table, name): + if name in self.state_dict[table]['rules_dict']: + del self.state_dict[table]['rules_dict'][name] + + # TODO: Add sorting of rules so that diffs in check_mode look nicer and easier to follow. + # Sorting would be done from top to bottom like this: + # * default chain policies + # * custom chains + # * rules + # + # Converts rules from a state_dict to an iptables-save readable format. + def get_table_rules(self, table): + generated_rules = '' + # We first add a header e.g. '*filter'. + generated_rules += '*' + table + '\n' + rules_list = [] + custom_chains_list = [] + default_chain_policies = [] + dict_rules = self._get_table_rules_dict(table) + # Return list of rule names sorted by ('weight', 'rules') tuple. + for rule_name in sorted(dict_rules, key=lambda x: (dict_rules[x]['weight'], dict_rules[x]['rules'])): + rules = dict_rules[rule_name]['rules'] + # Fail if some of the rules are bad + self._fail_on_bad_rules(rules, table) + rules_list.append(self._filter_rules(rules, table)) + custom_chains_list.append(self._filter_custom_chains(rules, table)) + default_chain_policies.append(self._filter_default_chain_policies(rules, table)) + # Clean up empty strings from these two lists. + rules_list = list(filter(None, rules_list)) + custom_chains_list = list(filter(None, custom_chains_list)) + default_chain_policies = list(filter(None, default_chain_policies)) + if default_chain_policies: + # Since iptables-restore applies the last chain policy it reads, we + # have to reverse the order of chain policies so that those with + # the lowest weight (higher priority) are read last. + generated_rules += '\n'.join(reversed(default_chain_policies)) + '\n' + if custom_chains_list: + # We remove duplicate custom chains so that iptables-restore + # doesn't fail because of that. + generated_rules += self._remove_duplicate_custom_chains('\n'.join(sorted(custom_chains_list)), table) + '\n' + if rules_list: + generated_rules += '\n'.join(rules_list) + '\n' + generated_rules += 'COMMIT\n' + return generated_rules + + # Sets unmanaged rules for the passed table in the state_dict. + def _set_unmanaged_rules(self, table, rules): + self.add_table_rule(table, Iptables.UNMANAGED_RULES_KEY_NAME, 90, rules, prepend_ansible_comment=False) + + # Clears unmanaged rules of a table. + def clear_unmanaged_rules(self, table): + self._set_unmanaged_rules(table, '') + + # Updates unmanaged rules of a table from the active rules. + def refresh_unmanaged_rules(self, table): + # Get active iptables rules and clean them up. + active_rules = self._get_active_rules(table) + unmanaged_chains_and_rules = [] + unmanaged_chains_and_rules.append(self._filter_custom_chains(active_rules, table, only_unmanaged=True)) + unmanaged_chains_and_rules.append(self._filter_rules(active_rules, table, only_unmanaged=True)) + # Clean items which are empty strings + unmanaged_chains_and_rules = list(filter(None, unmanaged_chains_and_rules)) + self._set_unmanaged_rules(table, '\n'.join(unmanaged_chains_and_rules)) + + # Check if there are bad lines in the specified rules. + def _fail_on_bad_rules(self, rules, table): + for line in rules.splitlines(): + tokens = self._split_rule_into_tokens(line) + if '-t' in tokens or '--table' in tokens: + msg = ("Iptables rules cannot contain '-t/--table' parameter. " + "You should use the 'table' parameter of the module to set rules " + "for a specific table.") + Iptables.module.fail_json(msg=msg) + # Fail if the parameter --comment doesn't have a comment after + if '--comment' in tokens and len(tokens) <= tokens.index('--comment') + 1: + msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line + Iptables.module.fail_json(msg=msg) + if not (Iptables.is_rule(line) or + Iptables.is_custom_chain(line, table) or + Iptables.is_default_chain(line, table) or + Iptables.is_comment(line)): + msg = ("Bad iptables rule '%s'! You can only use -A/--append, -N/--new-chain " + "and -P/--policy to specify rules." % line) + Iptables.module.fail_json(msg=msg) + + # Write rules to dest path. + def _write_rules_to_file(self, rules, dest): + tmp_path = self._write_to_temp_file(rules) + Iptables.module.atomic_move(tmp_path, dest) + + # Write text to a temp file and return path to that file. + def _write_to_temp_file(self, text): + fd, path = tempfile.mkstemp() + Iptables.module.add_cleanup_file(path) # add file for cleanup later + tmp = os.fdopen(fd, 'w') + tmp.write(text) + tmp.close() + return path + + # + # Public and private methods which make changes on the system + # are named 'system_*' and '_system_*', respectively. + # + + # Flush all rules in a passed table. + def _system_flush_single_table_rules(self, table): + # Set all default chain policies to ACCEPT. + for chain in Iptables.DEFAULT_CHAINS[table]: + cmd = [self.bins['iptables'], '-t', table, '-P', chain, 'ACCEPT'] + Iptables.module.run_command(cmd, check_rc=True) + # Then flush all rules. + cmd = [self.bins['iptables'], '-t', table, '-F'] + Iptables.module.run_command(cmd, check_rc=True) + # And delete custom chains. + cmd = [self.bins['iptables'], '-t', table, '-X'] + Iptables.module.run_command(cmd, check_rc=True) + # Update active rules in the object. + self._refresh_active_rules(table) + + # Save active iptables rules to the system path. + def _system_save_active(self, backup=False): + # Backup if needed + if backup: + Iptables.module.backup_local(self.system_save_path) + # Get iptables-save dump of all tables + all_active_rules = self._get_active_rules(table='*', clean=False) + # Move iptables-save dump of all tables to the iptables_save_path + self._write_rules_to_file(all_active_rules, self.system_save_path) + + # Apply table dict rules to the system. + def system_apply_table_rules(self, table, test=False): + dump_path = self._write_to_temp_file(self.get_table_rules(table)) + if test: + cmd = [self.bins['iptables-restore'], '-t', dump_path] + else: + cmd = [self.bins['iptables-restore'], dump_path] + rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False) + if rc != 0: + if test: + dump_contents_file = open(dump_path, 'r') + dump_contents = dump_contents_file.read() + dump_contents_file.close() + msg = "There is a problem with the iptables rules:" \ + + '\n\nError message:\n' \ + + stderr \ + + '\nGenerated rules:\n#######\n' \ + + dump_contents + '#####' + else: + msg = "Could not load iptables rules:\n\n" + stderr + Iptables.module.fail_json(msg=msg) + self._refresh_active_rules(table) + + # Flush one or all tables (to flush all tables pass table='*'). + def system_flush_table_rules(self, table): + if table == '*': + for tbl in Iptables.TABLES: + self._delete_table(tbl) + if self._single_table_needs_flush(tbl): + self._system_flush_single_table_rules(tbl) + # Only flush the specified table. + else: + self._delete_table(table) + if self._single_table_needs_flush(table): + self._system_flush_single_table_rules(table) + + # Saves state file and system iptables rules. + def system_save(self, backup=False): + self._system_save_active(backup=backup) + rules = json.dumps(self.state_dict, sort_keys=True, indent=4, separators=(',', ': ')) + self._write_rules_to_file(rules, self.state_save_path) + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + ipversion=dict(required=False, choices=["4", "6"], type='str', default="4"), + state=dict(required=False, choices=['present', 'absent'], default='present', type='str'), + weight=dict(required=False, type='int', default=40), + name=dict(required=True, type='str'), + table=dict(required=False, choices=Iptables.TABLES + ['*'], default="filter", type='str'), + rules=dict(required=False, type='str', default=""), + backup=dict(required=False, type='bool', default=False), + keep_unmanaged=dict(required=False, type='bool', default=True), + ), + supports_check_mode=True, + ) + + check_mode = module.check_mode + changed = False + ipversion = module.params['ipversion'] + state = module.params['state'] + weight = module.params['weight'] + name = module.params['name'] + table = module.params['table'] + rules = module.params['rules'] + backup = module.params['backup'] + keep_unmanaged = module.params['keep_unmanaged'] + + kw = dict(state=state, name=name, rules=rules, weight=weight, ipversion=ipversion, + table=table, backup=backup, keep_unmanaged=keep_unmanaged) + + iptables = Iptables(module, ipversion) + + # Acquire lock so that only one instance of this object can exist. + # Fail if the lock cannot be acquired within 10 seconds. + iptables.acquire_lock_or_exit(wait_for_seconds=10) + + # Clean up rules of comments and empty lines. + rules = Iptables.clean_up_rules(rules) + + # Check additional parameter requirements + if state == 'present' and name == '*': + module.fail_json(msg="Parameter 'name' can only be '*' if 'state=absent'") + if state == 'present' and table == '*': + module.fail_json(msg="Parameter 'table' can only be '*' if 'name=*' and 'state=absent'") + if state == 'present' and not name: + module.fail_json(msg="Parameter 'name' cannot be empty") + if state == 'present' and not re.match('^[' + Iptables.RULE_NAME_ALLOWED_CHARS + ']+$', name): + module.fail_json(msg="Parameter 'name' not valid! It can only contain alphanumeric characters, " + "underscore, hyphen, or a space, got: '%s'" % name) + if weight < 0 or weight > 99: + module.fail_json(msg="Parameter 'weight' can be 0-99, got: %d" % weight) + if state == 'present' and rules == '': + module.fail_json(msg="Parameter 'rules' cannot be empty when 'state=present'") + + # Flush rules of one or all tables + if state == 'absent' and name == '*': + # Check if table(s) need to be flushed + if iptables.table_needs_flush(table): + changed = True + if not check_mode: + # Flush table(s) + iptables.system_flush_table_rules(table) + # Save state and system iptables rules + iptables.system_save(backup=backup) + # Exit since there is nothing else to do + kw['changed'] = changed + module.exit_json(**kw) + + # Initialize new iptables object which will store new rules + iptables_new = Iptables(module, ipversion) + + if state == 'present': + iptables_new.add_table_rule(table, name, weight, rules) + else: + iptables_new.remove_table_rule(table, name) + + if keep_unmanaged: + iptables_new.refresh_unmanaged_rules(table) + else: + iptables_new.clear_unmanaged_rules(table) + + # Refresh saved table dump with active iptables rules + iptables_new.refresh_saved_table_dump(table) + + # Check if there are changes in iptables, and if yes load new rules + if iptables != iptables_new: + + changed = True + + # Test generated rules + iptables_new.system_apply_table_rules(table, test=True) + + if check_mode: + # Create a predicted diff for check_mode. + # Diff will be created from rules generated from the state dictionary. + if hasattr(module, '_diff') and module._diff: + # Update unmanaged rules in the old object so the generated diff + # from the rules dictionaries is more accurate. + iptables.refresh_unmanaged_rules(table) + # Generate table rules from rules dictionaries. + table_rules_old = iptables.get_table_rules(table) + table_rules_new = iptables_new.get_table_rules(table) + # If rules generated from dicts are not equal, we generate a diff from them. + if table_rules_old != table_rules_new: + kw['diff'] = generate_diff(table_rules_old, table_rules_new) + else: + # TODO: Update this comment to be better. + kw['diff'] = {'prepared': "System rules were not changed (e.g. rule " + "weight changed, redundant rule, etc)"} + else: + # We need to fetch active table dump before we apply new rules + # since we will need them to generate a diff. + table_active_rules = iptables_new.get_saved_table_dump(table) + + # Apply generated rules. + iptables_new.system_apply_table_rules(table) + + # Refresh saved table dump with active iptables rules. + iptables_new.refresh_saved_table_dump(table) + + # Save state and system iptables rules. + iptables_new.system_save(backup=backup) + + # Generate a diff. + if hasattr(module, '_diff') and module._diff: + table_active_rules_new = iptables_new.get_saved_table_dump(table) + if table_active_rules != table_active_rules_new: + kw['diff'] = generate_diff(table_active_rules, table_active_rules_new) + else: + # TODO: Update this comment to be better. + kw['diff'] = {'prepared': "System rules were not changed (e.g. rule " + "weight changed, redundant rule, etc)"} + + kw['changed'] = changed + module.exit_json(**kw) + + +if __name__ == '__main__': + main() diff --git a/playbooks/update_all.yml b/playbooks/update_all.yml new file mode 100644 index 0000000..e21024f --- /dev/null +++ b/playbooks/update_all.yml @@ -0,0 +1,9 @@ +--- +- name: Update everything + hosts: '*' + tasks: + - yum: name='*' state=latest + when: ansible_os_family == 'RedHat' + - apt: name='*' state=latest + when: ansible_os_family == 'Debian' + diff --git a/playbooks/update_cacertificates.yml b/playbooks/update_cacertificates.yml new file mode 100644 index 0000000..d79cf4d --- /dev/null +++ b/playbooks/update_cacertificates.yml @@ -0,0 +1,7 @@ +--- + +- name: Update ca-certificates + hosts: '*' + tasks: + - name: Update ca-certificates + package: name=ca-certificates state=latest diff --git a/playbooks/update_zabbix.yml b/playbooks/update_zabbix.yml new file mode 100644 index 0000000..3b07314 --- /dev/null +++ b/playbooks/update_zabbix.yml @@ -0,0 +1,42 @@ +--- +- name: Update Zabbix + hosts: '*' + tasks: + - yum: + name: + - zabbix-agent + - zabbix-agent-addons + state: latest + when: ansible_os_family == 'RedHat' + notify: restart zabbix-agent + - apt: + name: + - zabbix-agent + update_cache: True + state: latest + when: ansible_os_family == 'Debian' + notify: restart zabbix-agent + - git: + repo: https://git.fws.fr/fws/zabbix-agent-addons.git + dest: /var/lib/zabbix/addons + register: zabbix_agent_addons_git + when: ansible_os_family == 'Debian' + notify: restart zabbix-agent + - shell: cp -af /var/lib/zabbix/addons/{{ item.src }}/* {{ item.dest }}/ + with_items: + - { src: zabbix_conf, dest: /etc/zabbix/zabbix_agentd.conf.d } + - { src: zabbix_scripts, dest: /var/lib/zabbix/bin } + - { src: lib, dest: /usr/local/lib/site_perl } + when: + - zabbix_agent_addons_git.changed + - ansible_os_family == 'Debian' + - shell: chmod +x /var/lib/zabbix/bin/* + args: + warn: False + when: + - zabbix_agent_addons_git.changed + - ansible_os_family == 'Debian' + + handlers: + - name: restart zabbix-agent + service: name=zabbix-agent state=restarted diff --git a/roles/akeneo_pim/README.md b/roles/akeneo_pim/README.md new file mode 100644 index 0000000..e4f140b --- /dev/null +++ b/roles/akeneo_pim/README.md @@ -0,0 +1,34 @@ +# Akeneo PIM + +[Akeneo PIM](https://www.akeneo.com/) A Product Information Management (PIM) solution is aimed to centralize all the marketing data + +## Settings + +Akeneo requires a few settings at the host level. Something like this +``` +# This should be defined on the server which will host the database +# It's not mandatory to be on the same host as the PIM itself. But the important thing is that AKeneo PIM +# requires MySQL. It'll not work with MariaDB +mysql_engine: mysql + +# Prevent an error when checking system requirements. Note that this is only for the CLI +# as web access will use it's own FPM pool +php_conf_memory_limit: 512M + +# We need Elasticsearch 7. Same foir MySQL, it's not required to be on the same host +es_major_version: 7 + +# Define a vhost to expose the PIM. Note that this is a minimal example +# And you will most likely want to put a reverse proxy (look at the nginx role) in front of it +httpd_ansible_vhosts: + - name: pim.example.org + document_root: /opt/pim_1/app/public + +``` + +## Installation +Installation should be fully automatic + +## Upgrade +Major upgrades might require some manual steps, as detailed on https://docs.akeneo.com/5.0/migrate_pim/upgrade_major_version.html + diff --git a/roles/akeneo_pim/defaults/main.yml b/roles/akeneo_pim/defaults/main.yml new file mode 100644 index 0000000..70c5e48 --- /dev/null +++ b/roles/akeneo_pim/defaults/main.yml @@ -0,0 +1,36 @@ +--- + +# Version to deploy +pim_version: 5.0.43 +# User under which the PIM will run +pim_user: php-pim_{{ pim_id }} +# If you install several pim instance on the same host, you should change the ID for each of them +pim_id: 1 +# Root directory of the installation +pim_root_dir: /opt/pim_{{ pim_id }} +# Should anisble handle upgrades or just initial install +pim_manage_upgrade: True + +# PHP version to use +pim_php_version: 74 + +# Database settings +pim_db_server: "{{ mysql_server | default('localhost') }}" +pim_db_port: 3306 +pim_db_name: akeneopim_{{ pim_id }} +pim_db_user: akeneopim_{{ pim_id }} +# A random pass will be generated and stored in {{ pim_root_dir }}/meta/ansible_dbpass if not defined +# pim_db_pass: S3cr3t. + +# A secret used to sign cookies. A random one will be generated and stored in {{ pim_root_dir }}/meta/ansible_secret if not defined +# pim_secret: ChangeMe + +# Elasticsearch host +pim_es_server: localhost:9200 + +# Public URL used to reach AKeneo. Note that you'll have to define a vhost for Akeneo PIM to be reachable +pim_public_url: http://pim.{{ inventory_hostname }}/ + +# Define the initial admin password. If not defined, a random one will be generated ans stored under {{ pim_root_dir }}/meta/ansible_admin_pass +# Note that this is only used on initial install, and will be ignored for upgrades +# pim_admin_pass: p@ssw0rd diff --git a/roles/akeneo_pim/handlers/main.yml b/roles/akeneo_pim/handlers/main.yml new file mode 100644 index 0000000..0d79c93 --- /dev/null +++ b/roles/akeneo_pim/handlers/main.yml @@ -0,0 +1,7 @@ +--- + +- name: restart akeneo-pim + service: name={{ item }} state=restarted + loop: + - akeneo-pim_{{ pim_id }}-jobs + - akeneo-pim_{{ pim_id }}-events-api diff --git a/roles/akeneo_pim/meta/main.yml b/roles/akeneo_pim/meta/main.yml new file mode 100644 index 0000000..c5eb88a --- /dev/null +++ b/roles/akeneo_pim/meta/main.yml @@ -0,0 +1,12 @@ +--- + +allow_duplicates: True +dependencies: + - role: mkdir + - role: composer + - role: mysql_server + when: pim_db_server in ['localhost','127.0.0.1'] + - role: httpd_php + - role: nodejs + - role: elasticsearch + when: pim_es_server | regex_replace('(.*):\d+','\\1') in ['localhost','127.0.0.1'] diff --git a/roles/akeneo_pim/tasks/archive_post.yml b/roles/akeneo_pim/tasks/archive_post.yml new file mode 100644 index 0000000..0e1d306 --- /dev/null +++ b/roles/akeneo_pim/tasks/archive_post.yml @@ -0,0 +1,10 @@ +--- + +- name: Compress previous version + command: tar cf {{ pim_root_dir }}/archives/{{ pim_current_version }}.tar.zst ./ --use-compress-program=zstd + args: + chdir: "{{ pim_root_dir }}/archives/{{ pim_current_version }}" + warn: False + environment: + ZSTD_CLEVEL: 10 + tags: pim diff --git a/roles/akeneo_pim/tasks/archive_pre.yml b/roles/akeneo_pim/tasks/archive_pre.yml new file mode 100644 index 0000000..82d59af --- /dev/null +++ b/roles/akeneo_pim/tasks/archive_pre.yml @@ -0,0 +1,40 @@ +--- + +- name: Create the archive dir + file: path={{ pim_root_dir }}/archives/{{ pim_current_version }} state=directory + tags: pim + +- name: Stop jobs and event API services + service: name={{ item }} state=stopped + loop: + - akeneo-pim_{{ pim_id }}-jobs + - akeneo-pim_{{ pim_id }}-events-api + tags: pim + +- name: Disable cron jobs + file: path=/etc/cron.d/akeneopim_{{ pim_id }} state=absent + tags: pim + +- name: Archive current version + synchronize: + src: "{{ pim_root_dir }}/app" + dest: "{{ pim_root_dir }}/archives/{{ pim_current_version }}/" + compress: False + delete: True + delegate_to: "{{ inventory_hostname }}" + tags: pim + +- name: Dump the database + mysql_db: + state: dump + name: "{{ pim_db_name }}" + target: "{{ pim_root_dir }}/archives/{{ pim_current_version }}/{{ pim_db_name }}.sql.xz" + login_host: "{{ pim_db_server }}" + login_port: "{{ pim_db_port }}" + login_user: "{{ pim_db_user }}" + login_password: "{{ pim_db_pass }}" + quick: True + single_transaction: True + environment: + XZ_OPT: -T0 + tags: pim diff --git a/roles/akeneo_pim/tasks/cleanup.yml b/roles/akeneo_pim/tasks/cleanup.yml new file mode 100644 index 0000000..bcded67 --- /dev/null +++ b/roles/akeneo_pim/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove tmp and obsolete files + file: path={{ item }} state=absent + loop: + - "{{ pim_root_dir }}/archives/{{ pim_current_version }}" + tags: pim + diff --git a/roles/akeneo_pim/tasks/conf.yml b/roles/akeneo_pim/tasks/conf.yml new file mode 100644 index 0000000..5097311 --- /dev/null +++ b/roles/akeneo_pim/tasks/conf.yml @@ -0,0 +1,117 @@ +--- + +- name: Deploy configuration + template: src=env.j2 dest={{ pim_root_dir }}/app/.env.local group={{ pim_user }} mode=640 + tags: pim + +- import_tasks: ../includes/webapps_webconf.yml + vars: + - app_id: pim_{{ pim_id }} + - php_version: "{{ pim_php_version }}" + - php_fpm_pool: "{{ pim_php_fpm_pool | default('') }}" + tags: pim + +- name: Build and update frontend components + command: scl enable php{{ pim_php_version }} -- make upgrade-front + args: + chdir: "{{ pim_root_dir }}/app" + environment: + NO_DOCKER: true + APP_ENV: prod + become_user: "{{ pim_user }}" + when: pim_install_mode != 'none' + tags: pim + +- name: Initialize the database + command: scl enable php{{ pim_php_version }} -- make database O="--catalog vendor/akeneo/pim-community-dev/src/Akeneo/Platform/Bundle/InstallerBundle/Resources/fixtures/minimal" + args: + chdir: "{{ pim_root_dir }}/app" + environment: + NO_DOCKER: true + APP_ENV: prod + become_user: "{{ pim_user }}" + when: pim_install_mode == 'install' + tags: pim + +- name: Upgrade database + command: /bin/php{{ pim_php_version }} {{ pim_root_dir }}/app/bin/console doctrine:migrations:migrate --no-interaction + args: + chdir: "{{ pim_root_dir }}/app" + become_user: "{{ pim_user }}" + when: pim_install_mode == 'upgrade' + tags: pim + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ pim_root_dir }}/perms.sh mode=755 + register: pim_perm_script + tags: pim + +- name: Apply permissions + command: "{{ pim_root_dir }}/perms.sh" + when: pim_perm_script.changed or pim_install_mode != 'none' + tags: pim + +- name: Setup cron jobs + cron: + cron_file: akeneopim_{{ pim_id }} + user: "{{ pim_user }}" + name: "{{ item.name }}" + job: /bin/php{{ pim_php_version }} {{ pim_root_dir }}/app/bin/console {{ item.job }} + minute: "{{ item.minute | default('*') }}" + hour: "{{ item.hour | default('*') }}" + weekday: "{{ item.weekday | default('*') }}" + day: "{{ item.day | default('*') }}" + month: "{{ item.month | default('*') }}" + loop: + - name: refresh + job: pim:versioning:refresh + minute: 30 + hour: 1 + - name: purge + job: pim:versioning:purge --more-than-days 90 --no-interaction --force + minute: 30 + hour: 2 + - name: update-data + job: akeneo:connectivity-audit:update-data + minute: 1 + - name: purge-errors + job: akeneo:connectivity-connection:purge-error + minute: 10 + - name: purge-job-execution + job: akeneo:batch:purge-job-execution + minute: 20 + hour: 0 + day: 1 + - name: purge-error-count + job: akeneo:connectivity-audit:purge-error-count + minute: 40 + hour: 0 + - name: aggregate + job: pim:volume:aggregate + minute: 30 + hour: 4 + - name: schedule-periodic-tasks + job: pim:data-quality-insights:schedule-periodic-tasks + minute: 15 + hour: 0 + - name: prepare-evaluations + job: pim:data-quality-insights:prepare-evaluations + minute: '*/10' + - name: evaluations + job: pim:data-quality-insights:evaluations + minute: '*/30' + - name: purge-messages + job: akeneo:messenger:doctrine:purge-messages messenger_messages default + minute: 0 + hour: '*/2' + tags: pim + +- name: Create the admin user + command: /bin/php{{ pim_php_version }} {{ pim_root_dir }}/app/bin/console pim:user:create --admin -n -- admin {{ pim_admin_pass | quote }} admin@example.org Admin Admin fr_FR + when: pim_install_mode == 'install' + become_user: "{{ pim_user }}" + tags: pim + +- name: Deploy logrotate conf + template: src=logrotate.conf.j2 dest=/etc/logrotate.d/akeneopim_{{ pim_id }} + tags: pim diff --git a/roles/akeneo_pim/tasks/directories.yml b/roles/akeneo_pim/tasks/directories.yml new file mode 100644 index 0000000..906ef60 --- /dev/null +++ b/roles/akeneo_pim/tasks/directories.yml @@ -0,0 +1,30 @@ +--- + +- name: Create nedded directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ pim_root_dir }}/meta" + mode: 700 + - dir: "{{ pim_root_dir }}/archives" + mode: 700 + - dir: "{{ pim_root_dir }}/backup" + mode: 700 + - dir: "{{ pim_root_dir }}/data" + owner: "{{ pim_user }}" + mode: 700 + - dir: "{{ pim_root_dir }}/app" + owner: "{{ pim_user }}" + group: "{{ pim_user }}" + - dir: "{{ pim_root_dir }}/tmp" + owner: "{{ pim_user }}" + group: "{{ pim_user }}" + mode: 700 + - dir: "{{ pim_root_dir }}/sessions" + owner: "{{ pim_user }}" + group: "{{ pim_user }}" + mode: 700 + tags: pim + +- name: Link the var directory to the data dir + file: src={{ pim_root_dir }}/data dest={{ pim_root_dir }}/app/var state=link + tags: pim diff --git a/roles/akeneo_pim/tasks/facts.yml b/roles/akeneo_pim/tasks/facts.yml new file mode 100644 index 0000000..ee7b06a --- /dev/null +++ b/roles/akeneo_pim/tasks/facts.yml @@ -0,0 +1,38 @@ +--- + +# Detect installed version (if any) +- block: + - import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ pim_root_dir }}" + - version: "{{ pim_version }}" + - set_fact: pim_install_mode={{ (install_mode == 'upgrade' and not pim_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: pim_current_version={{ current_version | default('') }} + tags: pim + +# Create a random pass for the DB if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ pim_root_dir }}/meta/ansible_dbpass" + - set_fact: pim_db_pass={{ rand_pass }} + when: pim_db_pass is not defined + tags: pim + +# Create a random secret if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ pim_root_dir }}/meta/ansible_secret" + - set_fact: pim_secret={{ rand_pass }} + when: pim_secret is not defined + tags: pim + +# Create a random admin pass if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ pim_root_dir }}/meta/ansible_admin_pass" + - set_fact: pim_admin_pass={{ rand_pass }} + when: pim_admin_pass is not defined + tags: pim diff --git a/roles/akeneo_pim/tasks/install.yml b/roles/akeneo_pim/tasks/install.yml new file mode 100644 index 0000000..7ee7281 --- /dev/null +++ b/roles/akeneo_pim/tasks/install.yml @@ -0,0 +1,95 @@ +--- + +- name: Install needed tools + package: + name: + - make + - ghostscript + - aspell + tags: pim + +- when: pim_install_mode == 'upgrade' + block: + - name: Wipe install on upgrades + file: path={{ pim_root_dir }}/app state=absent + + - name: Create app subdir + file: path={{ pim_root_dir }}/app state=directory owner={{ pim_user }} group={{ pim_user }} + + - name: Link the var directory + file: src={{ pim_root_dir }}/data dest={{ pim_root_dir }}/app/var state=link + + tags: pim + +- when: pim_install_mode != 'none' + block: + - name: Deploy composer.json + template: src=composer.json.j2 dest={{ pim_root_dir }}/app/composer.json owner={{ pim_user }} + become_user: root + + - name: Install Akeneo with Composer + composer: + working_dir: "{{ pim_root_dir }}/app" + executable: /bin/php{{ pim_php_version }} + command: install + become_user: "{{ pim_user }}" + + - name: Install yarn globaly + npm: + name: yarn + path: "{{ pim_root_dir }}/app" + global: True + state: latest + + - name: Install typescript globaly + npm: + name: typescript + path: "{{ pim_root_dir }}/app" + global: True + state: latest + + tags: pim + + # the PIM makefile has /usr/local/bin/composer hardcoded +- name: Link composer in /usr/local/bin + file: src=/bin/composer dest=/usr/local/bin/composer state=link + tags: pim + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ pim_db_name }}" + - db_user: "{{ pim_db_user }}" + - db_server: "{{ pim_db_server }}" + - db_pass: "{{ pim_db_pass }}" + tags: pim + +- name: Set correct SELinux context + sefcontext: + target: "{{ pim_root_dir }}(/.*)?" + setype: httpd_sys_content_t + state: present + when: ansible_selinux.status == 'enabled' + tags: pim + +- name: Install pre/post backup hooks + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/pim_{{ pim_id }} mode=700 + loop: + - pre + - post + tags: pim + +- name: Install job consumer and events api service units + template: src={{ item.src }} dest=/etc/systemd/system/{{ item.dest }} + loop: + - src: akeneo-pim-jobs.service.j2 + dest: akeneo-pim_{{ pim_id }}-jobs.service + - src: akeneo-pim-events-api.service.j2 + dest: akeneo-pim_{{ pim_id }}-events-api.service + register: pim_job_unit + notify: restart akeneo-pim + tags: pim + +- name: Reload systemd + systemd: daemon_reload=True + when: pim_job_unit.results | selectattr('changed','equalto',True) | list | length > 0 + tags: pim diff --git a/roles/akeneo_pim/tasks/main.yml b/roles/akeneo_pim/tasks/main.yml new file mode 100644 index 0000000..a541d76 --- /dev/null +++ b/roles/akeneo_pim/tasks/main.yml @@ -0,0 +1,13 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: pim_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: write_version.yml +- include: archive_post.yml + when: pim_install_mode == 'upgrade' +- include: cleanup.yml diff --git a/roles/akeneo_pim/tasks/services.yml b/roles/akeneo_pim/tasks/services.yml new file mode 100644 index 0000000..5970910 --- /dev/null +++ b/roles/akeneo_pim/tasks/services.yml @@ -0,0 +1,8 @@ +--- + +- name: Start services + service: name={{ item }} state=started enabled=True + loop: + - akeneo-pim_{{ pim_id }}-jobs + - akeneo-pim_{{ pim_id }}-events-api + tags: pim diff --git a/roles/akeneo_pim/tasks/user.yml b/roles/akeneo_pim/tasks/user.yml new file mode 100644 index 0000000..1dbe66f --- /dev/null +++ b/roles/akeneo_pim/tasks/user.yml @@ -0,0 +1,9 @@ +--- + +- name: Create user + user: + name: "{{ pim_user }}" + system: True + home: "{{ pim_root_dir }}" + shell: /sbin/nologin + tags: pim diff --git a/roles/akeneo_pim/tasks/write_version.yml b/roles/akeneo_pim/tasks/write_version.yml new file mode 100644 index 0000000..8f74ea6 --- /dev/null +++ b/roles/akeneo_pim/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write current installed version + copy: content={{ pim_version }} dest={{ pim_root_dir }}/meta/ansible_version + tags: pim diff --git a/roles/akeneo_pim/templates/akeneo-pim-events-api.service.j2 b/roles/akeneo_pim/templates/akeneo-pim-events-api.service.j2 new file mode 100644 index 0000000..77ed1eb --- /dev/null +++ b/roles/akeneo_pim/templates/akeneo-pim-events-api.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Akeneo Events API worker for PIM {{ pim_id }} + +[Service] +User={{ pim_user }} +Group={{ pim_user }} +WorkingDirectory={{ pim_root_dir }}/app +ExecStart=/bin/php{{ pim_php_version }} bin/console messenger:consume webhook --env=prod +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=akeneo-pim_{{ pim_id }}-events-api +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target + diff --git a/roles/akeneo_pim/templates/akeneo-pim-jobs.service.j2 b/roles/akeneo_pim/templates/akeneo-pim-jobs.service.j2 new file mode 100644 index 0000000..c5fd495 --- /dev/null +++ b/roles/akeneo_pim/templates/akeneo-pim-jobs.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Akeneo jobs worker for PIM {{ pim_id }} + +[Service] +User={{ pim_user }} +Group={{ pim_user }} +WorkingDirectory={{ pim_root_dir }}/app +ExecStart=/bin/php{{ pim_php_version }} bin/console akeneo:batch:job-queue-consumer-daemon --env=prod +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=akeneo-pim_{{ pim_id }}-jobs +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target + diff --git a/roles/akeneo_pim/templates/composer.json.j2 b/roles/akeneo_pim/templates/composer.json.j2 new file mode 100644 index 0000000..0435746 --- /dev/null +++ b/roles/akeneo_pim/templates/composer.json.j2 @@ -0,0 +1,44 @@ +{ + "name": "akeneo/pim-community-standard", + "description": "The \"Akeneo Community Standard Edition\" distribution", + "license": "OSL-3.0", + "type": "project", + "authors": [ + { + "name": "Akeneo", + "homepage": "http://www.akeneo.com" + } + ], + "autoload": { + "psr-0": { + "": "src/" + }, + "psr-4": { + "Pim\\Upgrade\\": "upgrades/" + }, + "exclude-from-classmap": [ + "vendor/akeneo/pim-community-dev/src/Kernel.php" + ] + }, + "require": { + "akeneo/pim-community-dev": "^{{ pim_version }}" + }, + "require-dev": { + "doctrine/doctrine-migrations-bundle": "1.3.2", + "symfony/debug-bundle": "^4.4.7", + "symfony/web-profiler-bundle": "^4.4.7", + "symfony/web-server-bundle": "^4.4.7" + }, + "scripts": { + "post-update-cmd": [ + "bash vendor/akeneo/pim-community-dev/std-build/install-required-files.sh" + ], + "post-install-cmd": [ + "bash vendor/akeneo/pim-community-dev/std-build/install-required-files.sh" + ], + "post-create-project-cmd": [ + "bash vendor/akeneo/pim-community-dev/std-build/install-required-files.sh" + ] + }, + "minimum-stability": "stable" +} diff --git a/roles/akeneo_pim/templates/env.j2 b/roles/akeneo_pim/templates/env.j2 new file mode 100644 index 0000000..c0142bd --- /dev/null +++ b/roles/akeneo_pim/templates/env.j2 @@ -0,0 +1,17 @@ +APP_ENV=prod +APP_DEBUG=0 +APP_DATABASE_HOST={{ pim_db_server }} +APP_DATABASE_PORT={{ pim_db_port }} +APP_DATABASE_NAME={{ pim_db_name }} +APP_DATABASE_USER={{ pim_db_user }} +APP_DATABASE_PASSWORD={{ pim_db_pass | quote }} +APP_DEFAULT_LOCALE=en +APP_SECRET={{ pim_secret | quote }} +APP_INDEX_HOSTS={{ pim_es_server }} +APP_PRODUCT_AND_PRODUCT_MODEL_INDEX_NAME=akeneo_pim_product_and_product_model +APP_CONNECTION_ERROR_INDEX_NAME=akeneo_connectivity_connection_error +MAILER_URL=null://localhost&sender_address=no-reply@{{ ansible_domain }} +AKENEO_PIM_URL={{ pim_public_url }} +LOGGING_LEVEL=NOTICE +APP_EVENTS_API_DEBUG_INDEX_NAME=akeneo_connectivity_connection_events_api_debug +APP_PRODUCT_AND_PRODUCT_MODEL_INDEX_NAME=akeneo_pim_product_and_product_model diff --git a/roles/akeneo_pim/templates/httpd.conf.j2 b/roles/akeneo_pim/templates/httpd.conf.j2 new file mode 100644 index 0000000..ed08363 --- /dev/null +++ b/roles/akeneo_pim/templates/httpd.conf.j2 @@ -0,0 +1,31 @@ + + AllowOverride All + Options FollowSymLinks +{% if pim_src_ip is defined and pim_src_ip | length > 0 %} + Require ip {{ pim_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ pim_php_fpm_pool | default('pim_' + pim_id | string) }}.sock|fcgi://localhost" + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + + + Require all denied + + + + + + RewriteEngine Off + diff --git a/roles/akeneo_pim/templates/logrotate.conf.j2 b/roles/akeneo_pim/templates/logrotate.conf.j2 new file mode 100644 index 0000000..360d394 --- /dev/null +++ b/roles/akeneo_pim/templates/logrotate.conf.j2 @@ -0,0 +1,6 @@ +{{ pim_root_dir }}/data/logs/*.log { + daily + rotate 90 + compress + missingok +} diff --git a/roles/akeneo_pim/templates/perms.sh.j2 b/roles/akeneo_pim/templates/perms.sh.j2 new file mode 100644 index 0000000..4cfedea --- /dev/null +++ b/roles/akeneo_pim/templates/perms.sh.j2 @@ -0,0 +1,11 @@ +#!/bin/bash + +restorecon -R {{ pim_root_dir }} +chown root:root {{ pim_root_dir }} +chmod 700 {{ pim_root_dir }} +setfacl -R -k -b {{ pim_root_dir }} +setfacl -m u:{{ pim_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:x {{ pim_root_dir }} +find {{ pim_root_dir }}/app -type f -exec chmod 644 "{}" \; +find {{ pim_root_dir }}/app -type d -exec chmod 755 "{}" \; +chown -R {{ pim_user }}:{{ pim_user }} {{ pim_root_dir }}/app + diff --git a/roles/akeneo_pim/templates/php.conf.j2 b/roles/akeneo_pim/templates/php.conf.j2 new file mode 100644 index 0000000..9ec5d98 --- /dev/null +++ b/roles/akeneo_pim/templates/php.conf.j2 @@ -0,0 +1,35 @@ +[pim_{{ pim_id }}] + +listen.owner = root +listen.group = apache +listen.mode = 0660 +listen = /run/php-fpm/pim_{{ pim_id }}.sock +user = {{ pim_user }} +group = {{ pim_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 5m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 1024M +php_admin_value[session.save_path] = {{ pim_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ pim_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ pim_root_dir }}/tmp +php_admin_value[post_max_size] = 200M +php_admin_value[upload_max_filesize] = 200M +php_admin_value[disable_functions] = system, show_source, symlink, exec, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ pim_root_dir }}:/usr/share/pear/:/usr/share/php/ +php_admin_value[max_execution_time] = 1200 +php_admin_value[max_input_time] = 1200 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = off +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/akeneo_pim/templates/post-backup.j2 b/roles/akeneo_pim/templates/post-backup.j2 new file mode 100644 index 0000000..c21cfe3 --- /dev/null +++ b/roles/akeneo_pim/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ pim_root_dir }}/backup/*.sql.zst diff --git a/roles/akeneo_pim/templates/pre-backup.j2 b/roles/akeneo_pim/templates/pre-backup.j2 new file mode 100644 index 0000000..d2d1c40 --- /dev/null +++ b/roles/akeneo_pim/templates/pre-backup.j2 @@ -0,0 +1,14 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump \ +{% if pim_db_server not in ['localhost','127.0.0.1'] %} + --user={{ pim_db_user | quote }} \ + --password={{ pim_db_pass | quote }} \ + --host={{ pim_db_server | quote }} \ + --port={{ pim_db_port | quote }} \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ pim_db_name | quote }} | zstd -c > {{ pim_root_dir }}/backup/{{ pim_db_name }}.sql.zst + diff --git a/roles/ampache/defaults/main.yml b/roles/ampache/defaults/main.yml new file mode 100644 index 0000000..0150dba --- /dev/null +++ b/roles/ampache/defaults/main.yml @@ -0,0 +1,95 @@ +--- + +ampache_id: "1" +ampache_manage_upgrade: True + +ampache_version: '5.1.1' +ampache_config_version: 58 +ampache_zip_url: https://github.com/ampache/ampache/releases/download/{{ ampache_version }}/ampache-{{ ampache_version }}_all.zip +ampache_zip_sha1: a5347181297ab188fe95b3875f75b7838d581974 + +ampache_root_dir: /opt/ampache_{{ ampache_id }} + +ampache_php_user: php-ampache_{{ ampache_id }} +ampache_php_version: 74 + +# If you prefer using a custom PHP FPM pool, set it's name. +# You might need to adjust ampache_php_user +# ampache_php_fpm_pool: php56 + + +ampache_mysql_server: "{{ mysql_server | default('localhost') }}" +# ampache_mysql_port: 3306 +ampache_mysql_db: ampache_{{ ampache_id }} +ampache_mysql_user: ampache_{{ ampache_id }} +# If not defined, a random pass will be generated and stored in the meta directory +# ampache_mysql_pass: ampache + +# ampache_alias: ampache +# ampache_allowed_ip: +# - 192.168.7.0/24 +# - 10.2.0.0/24 + +ampache_local_web_path: "http://ampache.{{ ansible_domain }}/" +ampache_auth_methods: + - mysql + +ampache_ldap_url: "{{ ad_auth | default(False) | ternary('ldap://' + ad_realm | default(samba_realm) | lower,ldap_uri) }}" +ampache_ldap_starttls: True +ampache_ldap_search_dn: "{{ ad_auth | default(False) | ternary((ad_ldap_user_search_base is defined) | ternary(ad_ldap_user_search_base,'DC=' + ad_realm | default(samba_realm) | regex_replace('\\.',',DC=')), ldap_base) }}" +ampache_ldap_username: "" +ampache_ldap_password: "" +ampache_ldap_objectclass: "{{ ad_auth | default(False) | ternary('user','inetOrgPerson') }}" +ampache_ldap_filter: "{{ ad_auth | default(False) | ternary('(&(objectCategory=person)(objectClass=user)(primaryGroupId=513)(sAMAccountName=%v))','(uid=%v)') }}" +ampache_ldap_email_field: mail +ampache_ldap_name_field: cn + +ampache_admin_users: + - admin + +#ampache_logout_redirect: https://sso.domain.org + +ampache_metadata_order: 'getID3,filename' + +ampache_lastfm_api_key: 697bad201ee93391630d845c7b3f9610 +ampache_lastfm_api_secret: 5f5fe59aa2f9c60220f04e94aa59c209 + +ampache_max_bit_rate: 192 +ampache_min_bit_rate: 64 + +# allowed, required or false +ampache_transcode_m4a: required +ampache_transcode_flac: required +ampache_transcode_mpc: required +ampache_transcode_ogg: required +ampache_transcode_oga: required +ampache_transcode_wav: required +ampache_transcode_wma: required +ampache_transcode_aif: required +ampache_transcode_aiff: required +ampache_transcode_ape: required +ampache_transcode_shn: required +ampache_transcode_mp3: allowed +ampache_transcode_avi: required +ampache_transcode_mkv: required +ampache_transcode_mpg: required +ampache_transcode_mpeg: required +ampache_transcode_m4v: required +ampache_transcode_mp4: required +ampache_transcode_mov: required +ampache_transcode_wmv: required +ampache_transcode_ogv: required +ampache_transcode_divx: required +ampache_transcode_m2ts: required +ampache_transcode_webm: required +ampache_transcode_flv: allowed +ampache_transcode_player_api_mp3: required +ampache_encode_player_api_target: mp3 +ampache_encode_player_webplayer: mp3 +ampache_encode_target: mp3 +ampache_encode_video_target: webm + +# If defined, will be printed on the login page. HTML can be used, eg +# ampache_motd: 'Use central authentication' + +... diff --git a/roles/ampache/handlers/main.yml b/roles/ampache/handlers/main.yml new file mode 100644 index 0000000..ea83645 --- /dev/null +++ b/roles/ampache/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- include: ../httpd_common/handlers/main.yml +- include: ../httpd_php/handlers/main.yml +... diff --git a/roles/ampache/meta/main.yml b/roles/ampache/meta/main.yml new file mode 100644 index 0000000..9fed297 --- /dev/null +++ b/roles/ampache/meta/main.yml @@ -0,0 +1,6 @@ +--- +allow_duplicates: true +dependencies: + - role: httpd_php + - role: repo_rpmfusion +... diff --git a/roles/ampache/tasks/main.yml b/roles/ampache/tasks/main.yml new file mode 100644 index 0000000..289950b --- /dev/null +++ b/roles/ampache/tasks/main.yml @@ -0,0 +1,213 @@ +--- + +- name: Install needed tools + yum: + name: + - unzip + - acl + - git + - ffmpeg + - mariadb + tags: ampache + +- import_tasks: ../includes/create_system_user.yml + vars: + - user: "{{ ampache_php_user }}" + - comment: "PHP FPM for ampache {{ ampache_id }}" + tags: ampache + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ ampache_root_dir }}" + - version: "{{ ampache_version }}" + tags: ampache +- set_fact: ampache_install_mode={{ (install_mode == 'upgrade' and not ampache_manage_upgrade) | ternary('none',install_mode) }} + tags: ampache +- set_fact: ampache_current_version={{ current_version | default('') }} + tags: ampache + +- import_tasks: ../includes/webapps_archive.yml + vars: + - root_dir: "{{ ampache_root_dir }}" + - version: "{{ ampache_current_version }}" + - db_name: "{{ ampache_mysql_db }}" + when: ampache_install_mode == 'upgrade' + tags: ampache + +- name: Create directory structure + file: path={{ item }} state=directory + with_items: + - "{{ ampache_root_dir }}" + - "{{ ampache_root_dir }}/web" + - "{{ ampache_root_dir }}/tmp" + - "{{ ampache_root_dir }}/sessions" + - "{{ ampache_root_dir }}/meta" + - "{{ ampache_root_dir }}/logs" + - "{{ ampache_root_dir }}/data" + - "{{ ampache_root_dir }}/data/metadata" + - "{{ ampache_root_dir }}/data/music" + - "{{ ampache_root_dir }}/data/video" + - "{{ ampache_root_dir }}/backup" + failed_when: False # Don't fail when a fuse FS is mount on /music for example + tags: ampache + +- when: ampache_install_mode != 'none' + block: + - name: Create tmp dir + file: path={{ ampache_root_dir }}/tmp/ampache state=directory + + - name: Download Ampache + get_url: + url: "{{ ampache_zip_url }}" + dest: "{{ ampache_root_dir }}/tmp/" + checksum: "sha1:{{ ampache_zip_sha1 }}" + + - name: Extract ampache archive + unarchive: + src: "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}_all.zip" + dest: "{{ ampache_root_dir }}/tmp/ampache" + remote_src: yes + + - name: Move files to the correct directory + synchronize: + src: "{{ ampache_root_dir }}/tmp/ampache/" + dest: "{{ ampache_root_dir }}/web/" + delete: True + compress: False + delegate_to: "{{ inventory_hostname }}" + tags: ampache + +- name: Check if htaccess files needs to be moved + stat: path={{ ampache_root_dir }}/web/public/{{ item }}/.htaccess.dist + with_items: + - channel + - play + - rest + register: htaccess + tags: ampache + +- name: Rename htaccess files + command: mv -f {{ ampache_root_dir }}/web/public/{{ item.item }}/.htaccess.dist {{ ampache_root_dir }}/web/public/{{ item.item }}/.htaccess + with_items: "{{ htaccess.results }}" + when: item.stat.exists + tags: ampache + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ ampache_root_dir }}/meta/key.txt" + tags: ampache +- set_fact: ampache_key={{ rand_pass }} + tags: ampache + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ampache_root_dir }}/meta/ansible_dbpass" + when: ampache_mysql_pass is not defined + tags: ampache +- set_fact: ampache_mysql_pass={{ rand_pass }} + when: ampache_mysql_pass is not defined + tags: ampache + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ ampache_mysql_db }}" + - db_user: "{{ ampache_mysql_user }}" + - db_server: "{{ ampache_mysql_server }}" + - db_pass: "{{ ampache_mysql_pass }}" + tags: ampache + +- name: Inject SQL structure + mysql_db: + name: "{{ ampache_mysql_db }}" + state: import + target: "{{ ampache_root_dir }}/web/sql/ampache.sql" + login_host: "{{ ampache_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + when: ampache_install_mode == 'install' + tags: ampache + +- name: Deploy ampache configuration + template: src=ampache.cfg.php.j2 dest={{ ampache_root_dir }}/web/config/ampache.cfg.php group={{ ampache_php_user }} mode=640 + tags: ampache + +#- name: Upgrade SQL database +# command: php{{ ampache_php_version }} {{ ampache_root_dir }}/web/bin/cli admin:updateDatabase +# become_user: "{{ ampache_php_user }}" +# when: ampache_install_mode == 'upgrade' +# tags: ampache + +- name: Grant admin privileges + command: mysql --host={{ ampache_mysql_server }} --user=sqladmin --password={{ mysql_admin_pass }} {{ ampache_mysql_db }} -e "UPDATE `user` SET `access`='100' WHERE `username`='{{ item }}'" + changed_when: False + become_user: "{{ ampache_php_user }}" + with_items: "{{ ampache_admin_users }}" + tags: ampache + +- import_tasks: ../includes/webapps_webconf.yml + vars: + - app_id: ampache_{{ ampache_id }} + - php_version: "{{ ampache_php_version }}" + - php_fpm_pool: "{{ ampache_php_fpm_pool | default('') }}" + tags: ampache + +- name: Deploy motd + template: src=motd.php.j2 dest={{ ampache_root_dir }}/web/config/motd.php + when: ampache_motd is defined + tags: ampache + +- name: Remove motd + file: path={{ ampache_root_dir }}/web/config/motd.php state=absent + when: ampache_motd is not defined + tags: ampache + +- name: Deploy cron scripts + template: src={{ item }}.j2 dest={{ ampache_root_dir }}/web/bin/{{ item }} + with_items: + - cron.sh + tags: ampache + +- name: Enable cronjob + cron: + name: ampache_{{ ampache_id }} + special_time: daily + user: "{{ ampache_php_user }}" + job: "/bin/sh {{ ampache_root_dir }}/web/bin/cron.sh" + cron_file: ampache_{{ ampache_id }} + tags: ampache + +- name: Deploy sso script + template: src=sso.php.j2 dest={{ ampache_root_dir }}/web/sso.php + tags: ampache + +- name: Deploy backup scripts + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/ampache_{{ ampache_id }} mode=750 + loop: + - pre + - post + tags: ampache + +- import_tasks: ../includes/webapps_compress_archive.yml + vars: + - root_dir: "{{ ampache_root_dir }}" + - version: "{{ ampache_current_version }}" + when: ampache_install_mode == 'upgrade' + tags: ampache + +- import_tasks: ../includes/webapps_post.yml + vars: + - root_dir: "{{ ampache_root_dir }}" + - version: "{{ ampache_version }}" + tags: ampache + +- name: Remove temp and obsolete files + file: path={{ item }} state=absent + with_items: + - "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}_all.zip" + - "{{ ampache_root_dir }}/tmp/ampache/" + - "{{ ampache_root_dir }}/db_dumps" + - /etc/backup/pre.d/ampache_{{ ampache_id }}_dump_db + - /etc/backup/post.d/ampache_{{ ampache_id }}_rm_dump + tags: ampache + +... diff --git a/roles/ampache/templates/ampache.cfg.php.j2 b/roles/ampache/templates/ampache.cfg.php.j2 new file mode 100644 index 0000000..394dcc4 --- /dev/null +++ b/roles/ampache/templates/ampache.cfg.php.j2 @@ -0,0 +1,137 @@ +config_version = {{ ampache_config_version }} +{% if ampache_local_web_path is defined %} +local_web_path = "{{ ampache_local_web_path }}" +{% endif %} +database_hostname = {{ ampache_mysql_server }} +{% if ampache_mysql_port is defined %} +database_port = "{{ ampache_mysql_port }}" +{% endif %} +database_name = "{{ ampache_mysql_db }}" +database_username = "{{ ampache_mysql_user }}" +database_password = "{{ ampache_mysql_pass }}" +secret_key = "{{ ampache_key }}" +session_length = 3600 +stream_length = 7200 +remember_length = 604800 +session_name = ampache +session_cookielife = 0 +auth_methods = "{{ ampache_auth_methods | join(',') }}" +{% if 'ldap' in ampache_auth_methods %} +ldap_url = "{{ ampache_ldap_url }}" +ldap_username = "{{ ampache_ldap_username }}" +ldap_password = "{{ ampache_ldap_password }}" +ldap_start_tls = "{{ ampache_ldap_starttls | ternary('true','false') }}" +ldap_search_dn = "{{ ampache_ldap_search_dn }}" +ldap_objectclass = "{{ ampache_ldap_objectclass }}" +ldap_filter = "{{ ampache_ldap_filter }}" +ldap_email_field = "{{ ampache_ldap_email_field }}" +ldap_name_field = "{{ ampache_ldap_name_field }}" +external_auto_update = "true" +{% endif %} +{% if ampache_logout_redirect is defined %} +logout_redirect = "{{ ampache_logout_redirect }}" +{% endif %} +access_control = "true" +require_session = "true" +require_localnet_session = "true" +metadata_order = "{{ ampache_metadata_order }}" +getid3_tag_order = "id3v2,id3v1,vorbiscomment,quicktime,matroska,ape,asf,avi,mpeg,riff" +deferred_ext_metadata = "false" +additional_genre_delimiters = "[/]{2}|[/\\\\|,;]" +catalog_file_pattern = "mp3|mpc|m4p|m4a|aac|ogg|oga|wav|aif|aiff|rm|wma|asf|flac|opus|spx|ra|ape|shn|wv" +catalog_video_pattern = "avi|mpg|mpeg|flv|m4v|mp4|webm|mkv|wmv|ogv|mov|divx|m2ts" +catalog_playlist_pattern = "m3u|m3u8|pls|asx|xspf" +catalog_prefix_pattern = "The|An|A|Das|Ein|Eine|Les|Le|La" +track_user_ip = "true" +allow_zip_download = "true" +allow_zip_types = "album" +use_auth = "true" +ratings = "false" +userflags = "true" +directplay = "true" +sociable = "false" +licensing = "false" +memory_cache = "true" +album_art_store_disk = "true" +local_metadata_dir = "{{ ampache_root_dir }}/data/metadata" +max_upload_size = 1048576 +resize_images = "false" +art_order = "db,tags,folder,musicbrainz,lastfm,google" +lastfm_api_key = "{{ ampache_lastfm_api_key }}" +lastfm_api_secret = "{{ ampache_lastfm_api_secret }}" +channel = "false" +live_stream = "false" +refresh_limit = "60" +show_footer_statistics = "false" +debug = "true" +debug_level = 5 +log_path = "{{ ampache_root_dir }}/logs/" +log_filename = "%name.%Y%m%d.log" +site_charset = "UTF-8" +{% if 'ldap' in ampache_auth_methods or 'http' in ampache_auth_methods %} +auto_create = "true" +auto_user = "user" +{% endif %} +allow_public_registration = "false" +generate_video_preview = "true" +max_bit_rate = {{ ampache_max_bit_rate }} +min_bit_rate = {{ ampache_min_bit_rate }} +transcode_m4a = {{ ampache_transcode_m4a }} +transcode_flac = {{ ampache_transcode_flac }} +transcode_mpc = {{ ampache_transcode_mpc }} +transcode_ogg = {{ ampache_transcode_ogg }} +transcode_oga = {{ ampache_transcode_oga }} +transcode_wav = {{ ampache_transcode_wav }} +transcode_wma = {{ ampache_transcode_wma }} +transcode_aif = {{ ampache_transcode_aif }} +transcode_aiff = {{ ampache_transcode_aiff }} +transcode_ape = {{ ampache_transcode_ape }} +transcode_shn = {{ ampache_transcode_shn }} +transcode_mp3 = {{ ampache_transcode_mp3 }} +transcode_avi = {{ ampache_transcode_avi }} +transcode_mkv = {{ ampache_transcode_mkv }} +transcode_mpg = {{ ampache_transcode_mpg }} +transcode_mpeg = {{ ampache_transcode_mpeg }} +transcode_m4v = {{ ampache_transcode_m4v }} +transcode_mp4 = {{ ampache_transcode_mp4 }} +transcode_mov = {{ ampache_transcode_mov }} +transcode_wmv = {{ ampache_transcode_wmv }} +transcode_ogv = {{ ampache_transcode_ogv }} +transcode_divx = {{ ampache_transcode_divx }} +transcode_m2ts = {{ ampache_transcode_m2ts }} +transcode_webm = {{ ampache_transcode_webm }} +transcode_flv = {{ ampache_transcode_flv }} +encode_target = {{ ampache_encode_target }} +encode_player_webplayer_target = {{ ampache_encode_player_webplayer }} +transcode_player_api_mp3 = {{ ampache_transcode_player_api_mp3 }} +encode_video_target = {{ ampache_encode_video_target }} +transcode_player_customize = "true" +transcode_cmd = "/bin/ffmpeg" +transcode_input = "-i %FILE%" +encode_args_mp3 = "-vn -b:a %BITRATE%K -c:a libmp3lame -f mp3 pipe:1" +encode_args_ogg = "-vn -b:a %BITRATE%K -c:a libvorbis -f ogg pipe:1" +encode_args_m4a = "-vn -b:a %BITRATE%K -c:a libfdk_aac -f adts pipe:1" +encode_args_wav = "-vn -b:a %BITRATE%K -c:a pcm_s16le -f wav pipe:1" +encode_args_opus = "-vn -b:a %BITRATE%K -c:a libopus -compression_level 10 -vsync 2 -f ogg pipe:1" +encode_args_flv = "-b:a %BITRATE%K -ar 44100 -ac 2 -v 0 -f flv -c:v libx264 -preset superfast -threads 0 pipe:1" +encode_args_webm = "-q %QUALITY% -f webm -c:v libvpx -maxrate %MAXBITRATE%k -preset superfast -threads 0 pipe:1" +encode_args_ts = "-q %QUALITY% -s %RESOLUTION% -f mpegts -c:v libx264 -c:a libmp3lame -maxrate %MAXBITRATE%k -preset superfast -threads 0 pipe:1" +encode_get_image = "-ss %TIME% -f image2 -vframes 1 pipe:1" +encode_srt = "-vf \"subtitles='%SRTFILE%'\"" +encode_ss_frame = "-ss %TIME%" +encode_ss_duration = "-t %DURATION%" +force_ssl = "true" +common_abbr = "divx,xvid,dvdrip,hdtv,lol,axxo,repack,xor,pdtv,real,vtv,caph,2hd,proper,fqm,uncut,topaz,tvt,notv,fpn,fov,orenji,0tv,omicron,dsr,ws,sys,crimson,wat,hiqt,internal,brrip,boheme,vost,vostfr,fastsub,addiction,x264,LOL,720p,1080p,YIFY,evolve,fihtv,first,bokutox,bluray,tvboom,info" +mail_enable = "true" +mail_type = "sendmail" +mail_domain = "{{ ansible_domain }}" +{% if system_proxy is defined and system_proxy != '' %} +proxy_host = "{{ system_proxy | urlsplit('hostname') }}" +proxy_port = "{{ system_proxy | urlsplit('port') }}" +proxy_user = "{{ system_proxy | urlsplit('username') }}" +proxy_pass = "{{ system_proxy | urlsplit('password') }}" +{% endif %} +metadata_order_video = "filename,getID3" +registration_display_fields = "fullname,website" +registration_mandatory_fields = "fullnamep" +allow_upload_scripts = "false" diff --git a/roles/ampache/templates/cron.sh.j2 b/roles/ampache/templates/cron.sh.j2 new file mode 100644 index 0000000..6ee85c8 --- /dev/null +++ b/roles/ampache/templates/cron.sh.j2 @@ -0,0 +1,31 @@ +#!/bin/sh + +# Rotate logs +find {{ ampache_root_dir }}/logs -type f -mtime +7 -exec rm -f "{}" \; +find {{ ampache_root_dir }}/logs -type f -mtime +1 -exec xz -T0 "{}" \; + +# Do we have a previous filelist to compare against ? +PREV_HASH=$(cat {{ ampache_root_dir }}/tmp/data_hash.txt || echo 'none') + +# Now, compute a hash of the filelist +NEW_HASH=$(find {{ ampache_root_dir }}/data/{music,video} | sha1sum | cut -d' ' -f1) + +# Write new hash so we can compare next time +echo -n $NEW_HASH > {{ ampache_root_dir }}/tmp/data_hash.txt + +# If file list has changed since last time, then update the catalog +if [ "$PREV_HASH" != "$NEW_HASH" ]; then + # Clean (remove files which doesn't exists anymore) + /bin/php{{ ampache_php_version }} {{ ampache_root_dir }}/web/bin/cli run:updateCatalog -c > /dev/null 2>&1 + # Add (files added) + /bin/php{{ ampache_php_version }} {{ ampache_root_dir }}/web/bin/cli run:updateCatalog -a > /dev/null 2>&1 + # Update graphics + /bin/php{{ ampache_php_version }} {{ ampache_root_dir }}/web/bin/cli run:updateCatalog -g > /dev/null 2>&1 +fi + +# Now check if files have changed recently. We can have the same file list, but metadata updates +NEW_FILES=$(find {{ ampache_root_dir }}/data/{music,video} -type f -mtime -1 | wc -l) +if [ "$NEW_FILES" -gt "0" ]; then + # Verify (update metadata) + /bin/php{{ ampache_php_version }} {{ ampache_root_dir }}/web/bin/cli run:updateCatalog -e > /dev/null 2>&1 +fi diff --git a/roles/ampache/templates/httpd.conf.j2 b/roles/ampache/templates/httpd.conf.j2 new file mode 100644 index 0000000..c2e2beb --- /dev/null +++ b/roles/ampache/templates/httpd.conf.j2 @@ -0,0 +1,27 @@ +{% if ampache_alias is defined %} +Alias /{{ ampache_alias }} {{ ampache_root_dir }}/web/public +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + +RewriteEngine On + + AllowOverride All + Options FollowSymLinks +{% if ampache_allowed_ip is defined %} + Require ip {{ ampache_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ ampache_php_fpm_pool | default('ampache_' + ampache_id | string) }}.sock|fcgi://localhost" + + + Require all denied + + + + + Require all denied + + diff --git a/roles/ampache/templates/motd.php.j2 b/roles/ampache/templates/motd.php.j2 new file mode 100644 index 0000000..5dcd184 --- /dev/null +++ b/roles/ampache/templates/motd.php.j2 @@ -0,0 +1,3 @@ +{{ ampache_motd }}'; diff --git a/roles/ampache/templates/perms.sh.j2 b/roles/ampache/templates/perms.sh.j2 new file mode 100644 index 0000000..6a7e4e3 --- /dev/null +++ b/roles/ampache/templates/perms.sh.j2 @@ -0,0 +1,15 @@ +#!/bin/sh + +restorecon -R {{ ampache_root_dir }} +chown root:root {{ ampache_root_dir }} +chmod 700 {{ ampache_root_dir }} +setfacl -k -b {{ ampache_root_dir }} +setfacl -m u:{{ ampache_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:rx {{ ampache_root_dir }} +chown -R root:root {{ ampache_root_dir }}/web +chown {{ ampache_php_user }} {{ ampache_root_dir }}/data +chown -R {{ ampache_php_user }} {{ ampache_root_dir }}/{tmp,sessions,logs,data/metadata} +chmod 700 {{ ampache_root_dir }}/{tmp,sessions,logs,data} +find {{ ampache_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ ampache_root_dir }}/web -type d -exec chmod 755 "{}" \; +chown :{{ ampache_php_user }} {{ ampache_root_dir }}/web/config/ampache.cfg.php +chmod 640 {{ ampache_root_dir }}/web/config/ampache.cfg.php diff --git a/roles/ampache/templates/php.conf.j2 b/roles/ampache/templates/php.conf.j2 new file mode 100644 index 0000000..6c17756 --- /dev/null +++ b/roles/ampache/templates/php.conf.j2 @@ -0,0 +1,37 @@ +; {{ ansible_managed }} + +[ampache_{{ ampache_id }}] + +listen.owner = root +listen.group = {{ httpd_user | default('apache') }} +listen.mode = 0660 +listen = /run/php-fpm/ampache_{{ ampache_id }}.sock +user = {{ ampache_php_user }} +group = {{ ampache_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 60m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 512M +php_admin_value[session.save_path] = {{ ampache_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ ampache_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ ampache_root_dir }}/tmp +php_admin_value[post_max_size] = 5M +php_admin_value[upload_max_filesize] = 5M +php_admin_value[disable_functions] = system, show_source, symlink, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ ampache_root_dir }} +php_admin_value[max_execution_time] = 1800 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = on +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/ampache/templates/post-backup.j2 b/roles/ampache/templates/post-backup.j2 new file mode 100644 index 0000000..545c87c --- /dev/null +++ b/roles/ampache/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ ampache_root_dir }}/backup/* diff --git a/roles/ampache/templates/pre-backup.j2 b/roles/ampache/templates/pre-backup.j2 new file mode 100644 index 0000000..16ef6cd --- /dev/null +++ b/roles/ampache/templates/pre-backup.j2 @@ -0,0 +1,9 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump --user={{ ampache_mysql_user | quote }} \ + --password={{ ampache_mysql_pass | quote }} \ + --host={{ ampache_mysql_server | quote }} \ + --quick --single-transaction \ + --add-drop-table {{ ampache_mysql_db | quote }} | zstd -c > {{ ampache_root_dir }}/backup/{{ ampache_mysql_db }}.sql.zst diff --git a/roles/ampache/templates/sso.php.j2 b/roles/ampache/templates/sso.php.j2 new file mode 100644 index 0000000..1f7c064 --- /dev/null +++ b/roles/ampache/templates/sso.php.j2 @@ -0,0 +1,6 @@ + diff --git a/roles/appsmith/defaults/main.yml b/roles/appsmith/defaults/main.yml new file mode 100644 index 0000000..460980c --- /dev/null +++ b/roles/appsmith/defaults/main.yml @@ -0,0 +1,53 @@ +--- + +# Version to deploy +appsmith_version: 1.5.25 +# URL of the source archive +appsmith_archive_url: https://github.com/appsmithorg/appsmith/archive/v{{ appsmith_version }}.tar.gz +# sha1sum of the archive +appsmith_archive_sha1: dceebde21c7b0a989aa7fb96bac044df4f2ddf50 + +# Root directory where appsmith will be installed +appsmith_root_dir: /opt/appsmith +# Should ansible handle upgrades (True) or only initial install (False) +appsmith_manage_upgrade: True + +# User account under which appsmith will run +appsmith_user: appsmith + +# appsmith needs a redis server and a mongodb one +appsmith_redis_url: redis://localhost:6379 +# A random one will be created and stored in the meta directory if not defined here +appsmith_mongo_user: appsmith +# appsmith_mongo_pass: S3cr3t. +# Note: if appsmith_mongo_pass is defined, it'll be used with appsmith_mongo_user to connect, even if not indicated in appsmith_mongo_url +# Else, anonymous connection is made. By default, if you do not set appsmith_mongo_pass, a random one will be created +# If you insist on using anonymous connections, you should set appsmith_mongo_pass to False +appsmith_mongo_url: mongodb://localhost/appsmith?retryWrites=true + +# appsmith server component +appsmith_server_port: 8088 +# List of IP/CIDR having access to appsmith_server_port +appsmith_server_src_ip: [] + +# Email settings +appsmith_email_from: noreply@{{ ansible_domain }} +appsmith_email_server: localhost +appsmith_email_port: 25 +appsmith_email_tls: "{{ (appsmith_email_port == 587) | ternary(True,False) }}" +# appsmith_email_user: account +# appsmith_email_pass: S3Cr3T4m@1l + +# Encryption settings. If not defined, random values will be created and used +# appsmith_encryption_pass: p@ssw0rd +# appsmith_encryption_salt: Salt + +# Public URL used to access appsmith +appsmith_public_url: http://{{ inventory_hostname }} + +# User signup can be disabled +appsmith_user_signup: True +# If signup is enabled, you can restrict which domains are allowed to signup (an empty list means no restriction) +appsmith_signup_whitelist: [] +# If signup is disabled, you can set a list of whitelisted email which will be allowed +appsmith_admin_emails: [] diff --git a/roles/appsmith/handlers/main.yml b/roles/appsmith/handlers/main.yml new file mode 100644 index 0000000..e7b75f6 --- /dev/null +++ b/roles/appsmith/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart appsmith-server + service: name=appsmith-server state=restarted diff --git a/roles/appsmith/meta/main.yml b/roles/appsmith/meta/main.yml new file mode 100644 index 0000000..cb8d365 --- /dev/null +++ b/roles/appsmith/meta/main.yml @@ -0,0 +1,11 @@ +--- + +dependencies: + - role: mkdir + - role: maven + - role: repo_mongodb + - role: redis_server + when: appsmith_redis_url | urlsplit('hostname') in ['localhost','127.0.0.1'] + - role: mongodb_server + when: appsmith_mongo_url | urlsplit('hostname') in ['localhost','127.0.0.1'] + - role: nginx diff --git a/roles/appsmith/tasks/archive_post.yml b/roles/appsmith/tasks/archive_post.yml new file mode 100644 index 0000000..85039f6 --- /dev/null +++ b/roles/appsmith/tasks/archive_post.yml @@ -0,0 +1,10 @@ +--- + +- name: Compress previous version + command: tar cf {{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}.tar.zst --use-compress-program=zstd ./ + environment: + ZST_CLEVEL: 10 + args: + chdir: "{{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}" + warn: False + tags: appsmith diff --git a/roles/appsmith/tasks/archive_pre.yml b/roles/appsmith/tasks/archive_pre.yml new file mode 100644 index 0000000..5f93423 --- /dev/null +++ b/roles/appsmith/tasks/archive_pre.yml @@ -0,0 +1,33 @@ +--- + +- name: Create the archive dir + file: + path: "{{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}" + state: directory + tags: appsmith + +- name: Archive previous version + synchronize: + src: "{{ appsmith_root_dir }}/{{ item }}" + dest: "{{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}" + recursive: True + delete: True + loop: + - server + - client + - etc + - meta + delegate_to: "{{ inventory_hostname }}" + tags: appsmith + +- name: Dump mongo database + shell: | + mongodump --quiet \ + --out {{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}/ \ + --uri \ + {% if appsmith_mongo_pass is defined and appsmith_mongo_pass != False %} + {{ appsmith_mongo_url | urlsplit('scheme') }}://{{ appsmith_mongo_user }}:{{ appsmith_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ appsmith_mongo_url | urlsplit('hostname') }}{% if appsmith_mongo_url | urlsplit('port') %}:{{ appsmith_mongo_url | urlsplit('port') }}{% endif %}{{ appsmith_mongo_url | urlsplit('path') }}?{{ appsmith_mongo_url | urlsplit('query') }} + {% else %} + {{ appsmith_mongo_url }} + {% endif %} + tags: appsmith diff --git a/roles/appsmith/tasks/cleanup.yml b/roles/appsmith/tasks/cleanup.yml new file mode 100644 index 0000000..7524f1b --- /dev/null +++ b/roles/appsmith/tasks/cleanup.yml @@ -0,0 +1,9 @@ +--- + +- name: Remove tmp and unused files + file: path={{ item }} state=absent + loop: + - "{{ appsmith_root_dir }}/archives/{{ appsmith_current_version }}" + - "{{ appsmith_root_dir }}/tmp/appsmith-{{ appsmith_version }}" + - "{{ appsmith_root_dir }}/tmp/appsmith-{{ appsmith_version }}.tar.gz" + tags: appsmith diff --git a/roles/appsmith/tasks/conf.yml b/roles/appsmith/tasks/conf.yml new file mode 100644 index 0000000..ea6c5f0 --- /dev/null +++ b/roles/appsmith/tasks/conf.yml @@ -0,0 +1,30 @@ +--- + +- name: Deploy appsmith server conf + template: src={{ item }}.j2 dest={{ appsmith_root_dir }}/etc/{{ item }} group={{ appsmith_user }} mode=640 + loop: + - env + notify: restart appsmith-server + tags: appsmith + +- name: Deploy nginx conf + template: src=nginx.conf.j2 dest=/etc/nginx/ansible_conf.d/appsmith.conf + notify: reload nginx + tags: appsmith + +- name: Create the mongodb user + mongodb_user: + database: "{{ appsmith_mongo_url | urlsplit('path') | regex_replace('^\\/', '') }}" + name: "{{ appsmith_mongo_user }}" + password: "{{ appsmith_mongo_pass }}" + login_database: admin + login_host: "{{ appsmith_mongo_url | urlsplit('hostname') }}" + login_port: "{{ appsmith_mongo_url | urlsplit('port') | ternary(appsmith_mongo_url | urlsplit('port'),omit) }}" + login_user: mongoadmin + login_password: "{{ mongo_admin_pass }}" + roles: + - readWrite + when: + - appsmith_mongo_pass is defined + - appsmith_mongo_pass != False + tags: appsmith diff --git a/roles/appsmith/tasks/directories.yml b/roles/appsmith/tasks/directories.yml new file mode 100644 index 0000000..0c9ae84 --- /dev/null +++ b/roles/appsmith/tasks/directories.yml @@ -0,0 +1,28 @@ +--- + +- name: Create directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ appsmith_root_dir }}" + mode: 755 + - dir: "{{ appsmith_root_dir }}/archives" + mode: 700 + - dir: "{{ appsmith_root_dir }}/backup" + mode: 700 + - dir: "{{ appsmith_root_dir }}/tmp" + owner: "{{ appsmith_user }}" + mode: 700 + - dir: "{{ appsmith_root_dir }}/src" + owner: "{{ appsmith_user }}" + - dir: "{{ appsmith_root_dir }}/server" + owner: "{{ appsmith_user }}" + - dir: "{{ appsmith_root_dir }}/server/plugins" + owner: "{{ appsmith_user }}" + - dir: "{{ appsmith_root_dir }}/client" + - dir: "{{ appsmith_root_dir }}/meta" + mode: 700 + - dir: "{{ appsmith_root_dir }}/etc" + group: "{{ appsmith_user }}" + mode: 750 + - dir: "{{ appsmith_root_dir }}/bin" + tags: appsmith diff --git a/roles/appsmith/tasks/facts.yml b/roles/appsmith/tasks/facts.yml new file mode 100644 index 0000000..45ad89d --- /dev/null +++ b/roles/appsmith/tasks/facts.yml @@ -0,0 +1,61 @@ +--- + +# Detect installed version (if any) +- block: + - import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ appsmith_root_dir }}" + - version: "{{ appsmith_version }}" + - set_fact: appsmith_install_mode={{ (install_mode == 'upgrade' and not appsmith_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: appsmith_current_version={{ current_version | default('') }} + tags: appsmith + +# Create a random encryption password +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ appsmith_root_dir }}/meta/ansible_encryption_pass" + - set_fact: appsmith_encryption_pass={{ rand_pass }} + when: appsmith_encryption_pass is not defined + tags: appsmith + +# Create a random encryption salt +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ appsmith_root_dir }}/meta/ansible_encryption_salt" + - complex: False + - pass_size: 10 + - set_fact: appsmith_encryption_salt={{ rand_pass }} + when: appsmith_encryption_salt is not defined + tags: appsmith + +- set_fact: appsmith_mongo_pass={{ appsmith_mongo_url | urlsplit('password') | urldecode }} + when: + - appsmith_mongo_pass is not defined + - appsmith_mongo_url | urlsplit('password') is string + tags: mongo + +# Create a random password for mongo +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ appsmith_root_dir }}/meta/ansible_mongo_pass" + - set_fact: appsmith_mongo_pass={{ rand_pass }} + when: appsmith_mongo_pass is not defined + tags: appsmith + +# Try to read mongo admin pass +- name: Check if mongo pass file exists + stat: path=/root/.mongo.pw + register: appsmith_mongo_pw + tags: appsmith +- when: appsmith_mongo_pw.stat.exists and mongo_admin_pass is not defined + block: + - slurp: src=/root/.mongo.pw + register: appsmith_mongo_admin_pass + - set_fact: mongo_admin_pass={{ appsmith_mongo_admin_pass.content | b64decode | trim }} + tags: appsmith +- fail: msg='mongo_admin_pass must be provided' + when: not appsmith_mongo_pw.stat.exists and mongo_admin_pass is not defined + tags: appsmith diff --git a/roles/appsmith/tasks/install.yml b/roles/appsmith/tasks/install.yml new file mode 100644 index 0000000..f39a6d1 --- /dev/null +++ b/roles/appsmith/tasks/install.yml @@ -0,0 +1,141 @@ +--- + +- name: Install dependencies + yum: + name: + - nodejs + - java-11-openjdk + - java-11-openjdk-devel + - mongodb-org-tools + - make + - gcc-c++ + tags: appsmith + +- name: Detect exact JRE version + command: rpm -q java-11-openjdk + args: + warn: False + changed_when: False + register: appsmith_jre11_version + tags: appsmith + +- name: Select JRE 11 as default version + alternatives: + name: "{{ item.name }}" + link: "{{ item.link }}" + path: "{{ item.path }}" + loop: + - name: java + link: /usr/bin/java + path: /usr/lib/jvm/{{ appsmith_jre11_version.stdout | trim }}/bin/java + - name: javac + link: /usr/bin/javac + path: /usr/lib/jvm/{{ appsmith_jre11_version.stdout | trim }}/bin/javac + - name: jre_openjdk + link: /usr/lib/jvm/jre-openjdk + path: /usr/lib/jvm/{{ appsmith_jre11_version.stdout | trim }} + - name: java_sdk_openjdk + link: /usr/lib/jvm/java-openjdk + path: /usr/lib/jvm/{{ appsmith_jre11_version.stdout | trim }} + tags: appsmith + +- name: Stop the service during upgrade + service: name=appsmith-server state=stopped + when: appsmith_install_mode == 'upgrade' + tags: appsmith + +- when: appsmith_install_mode != 'none' + block: + + - name: Download appsmith + get_url: + url: "{{ appsmith_archive_url }}" + dest: "{{ appsmith_root_dir }}/tmp" + checksum: sha1:{{ appsmith_archive_sha1 }} + + - name: Extract appsmith archive + unarchive: + src: "{{ appsmith_root_dir }}/tmp/appsmith-{{ appsmith_version }}.tar.gz" + dest: "{{ appsmith_root_dir }}/tmp" + remote_src: True + + - name: Move sources + synchronize: + src: "{{ appsmith_root_dir }}/tmp/appsmith-{{ appsmith_version }}/" + dest: "{{ appsmith_root_dir }}/src/" + compress: False + delete: True + delegate_to: "{{ inventory_hostname }}" + + - name: Compile the server + command: /opt/maven/apache-maven/bin/mvn -DskipTests clean package + args: + chdir: "{{ appsmith_root_dir }}/src/app/server" + + - name: Remove previous server version + shell: find {{ appsmith_root_dir }}/server -name \*.jar -exec rm -f "{}" \; + + - name: Copy server jar + copy: src={{ appsmith_root_dir }}/src/app/server/appsmith-server/target/server-1.0-SNAPSHOT.jar dest={{ appsmith_root_dir }}/server/ remote_src=True + notify: restart appsmith-server + + - name: List plugins + shell: find {{ appsmith_root_dir }}/src/app/server/appsmith-*/*/target -maxdepth 1 -name \*.jar \! -name original\* + register: appsmith_plugins_jar + + - name: Install plugins jar + copy: src={{ item }} dest={{ appsmith_root_dir }}/server/plugins/ remote_src=True + loop: "{{ appsmith_plugins_jar.stdout_lines }}" + + - name: Install yarn + npm: + name: yarn + path: "{{ appsmith_root_dir }}/src/app/client" + + - name: Install NodeJS dependencies + command: ./node_modules/yarn/bin/yarn install --ignore-engines + args: + chdir: "{{ appsmith_root_dir }}/src/app/client" + + # Not sure why but yarn installs webpack 4.46.0 while appsmith wants 4.44.2 + - name: Install correct webpack version + command: ./node_modules/yarn/bin/yarn add webpack@4.44.2 --ignore-engines + args: + chdir: "{{ appsmith_root_dir }}/src/app/client" + + - name: Build the client + command: ./node_modules/.bin/craco --max-old-space-size=3072 build --config craco.build.config.js + args: + chdir: "{{ appsmith_root_dir }}/src/app/client" + + # Note : the client will be deployed in {{ appsmith_root_dir }}/client + # with a ExecStartPre hook of the server, which will take care of replacing + # placeholders with current settings. So no need to do it here + + become_user: "{{ appsmith_user }}" + tags: appsmith + +- name: Deploy systemd unit + template: src={{ item }}.j2 dest=/etc/systemd/system/{{ item }} + loop: + - appsmith-server.service + register: appsmith_units + notify: restart appsmith-server + tags: appsmith + +- name: Reload systemd + systemd: daemon_reload=True + when: appsmith_units.results | selectattr('changed','equalto',True) | list | length > 0 + tags: appsmith + +- name: Install pre-start script + template: src=pre-start.sh.j2 dest={{ appsmith_root_dir }}/bin/pre-start mode=755 + notify: restart appsmith-server + tags: appsmith + +- name: Install pre/post backup hoooks + template: src={{ item }}-backup.sh.j2 dest=/etc/backup/{{ item }}.d/appsmith mode=700 + loop: + - pre + - post + tags: appsmith diff --git a/roles/appsmith/tasks/iptables.yml b/roles/appsmith/tasks/iptables.yml new file mode 100644 index 0000000..2e689fd --- /dev/null +++ b/roles/appsmith/tasks/iptables.yml @@ -0,0 +1,12 @@ +--- + +- name: Handle appsmith ports in the firewall + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ item.port }} -s {{ item.src_ip | join(',') }} -j ACCEPT" + loop: + - name: appsmith_server_port + port: "{{ appsmith_server_port }}" + src_ip: "{{ appsmith_server_src_ip }}" + tags: firewall,appsmith diff --git a/roles/appsmith/tasks/main.yml b/roles/appsmith/tasks/main.yml new file mode 100644 index 0000000..1208952 --- /dev/null +++ b/roles/appsmith/tasks/main.yml @@ -0,0 +1,17 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: appsmith_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: write_version.yml +- include: archive_post.yml + when: appsmith_install_mode == 'upgrade' +- include: cleanup.yml + diff --git a/roles/appsmith/tasks/services.yml b/roles/appsmith/tasks/services.yml new file mode 100644 index 0000000..30847a4 --- /dev/null +++ b/roles/appsmith/tasks/services.yml @@ -0,0 +1,7 @@ +--- + +- name: Start and enable the services + service: name={{ item }} state=started enabled=True + loop: + - appsmith-server + tags: appsmith diff --git a/roles/appsmith/tasks/user.yml b/roles/appsmith/tasks/user.yml new file mode 100644 index 0000000..8c773cb --- /dev/null +++ b/roles/appsmith/tasks/user.yml @@ -0,0 +1,8 @@ +--- + +- name: Create appsmith user + user: + name: "{{ appsmith_user }}" + home: "{{ appsmith_root_dir }}" + system: True + tags: appsmith diff --git a/roles/appsmith/tasks/write_version.yml b/roles/appsmith/tasks/write_version.yml new file mode 100644 index 0000000..8cbcf5e --- /dev/null +++ b/roles/appsmith/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write installed version + copy: content={{ appsmith_version }} dest={{ appsmith_root_dir }}/meta/ansible_version + tags: appsmith diff --git a/roles/appsmith/templates/appsmith-server.service.j2 b/roles/appsmith/templates/appsmith-server.service.j2 new file mode 100644 index 0000000..3af1456 --- /dev/null +++ b/roles/appsmith/templates/appsmith-server.service.j2 @@ -0,0 +1,35 @@ +[Unit] +Description=Opensource framework to build app and workflows +After=syslog.target network.target mongodb.service redis.service + +[Service] +Type=simple +User={{ appsmith_user }} +Group={{ appsmith_user }} +EnvironmentFile={{ appsmith_root_dir }}/etc/env +WorkingDirectory={{ appsmith_root_dir }}/server +PermissionsStartOnly=yes +ExecStartPre={{ appsmith_root_dir }}/bin/pre-start +ExecStart=/bin/java -Djava.net.preferIPv4Stack=true \ + -Dserver.port={{ appsmith_server_port }} \ + -Djava.security.egd="file:/dev/./urandom" \ +{% if system_proxy is defined and system_proxy != '' %} + -Dhttp.proxyHost={{ system_proxy | urlsplit('hostname') }} \ + -Dhttp.proxyPort={{ system_proxy | urlsplit('port') }} \ + -Dhttps.proxyHost={{ system_proxy | urlsplit('hostname') }} \ + -Dhttps.proxyPort={{ system_proxy | urlsplit('port') }} \ +{% endif %} + -jar server-1.0-SNAPSHOT.jar +PrivateTmp=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=4096M +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 +SyslogIdentifier=appsmith-server + +[Install] +WantedBy=multi-user.target + diff --git a/roles/appsmith/templates/env.j2 b/roles/appsmith/templates/env.j2 new file mode 100644 index 0000000..62c416b --- /dev/null +++ b/roles/appsmith/templates/env.j2 @@ -0,0 +1,25 @@ +APPSMITH_MAIL_ENABLED=true +APPSMITH_MAIL_FROM={{ appsmith_email_from }} +APPSMITH_MAIL_HOST={{ appsmith_email_server }} +APPSMITH_MAIL_PORT={{ appsmith_email_port }} +APPSMITH_MAIL_SMTP_TLS_ENABLED={{ appsmith_email_tls | ternary('true','false') }} +{% if appsmith_email_user is defined and appsmith_email_pass is defined %} +APPSMITH_MAIL_SMTP_AUTH=true +APPSMITH_MAIL_USERNAME={{ appsmith_email_user }} +APPSMITH_MAIL_PASSWORD={{ appsmith_email_pass }} +{% endif %} +APPSMITH_REDIS_URL={{ appsmith_redis_url }} +{% if appsmith_mongo_user is defined and appsmith_mongo_pass is defined and appsmith_mongo_pass != False %} +{% set appsmith_mongo_url_obj = appsmith_mongo_url | urlsplit %} +APPSMITH_MONGODB_URI={{ appsmith_mongo_url_obj['scheme'] }}://{{ appsmith_mongo_user }}:{{ appsmith_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ appsmith_mongo_url_obj['hostname'] }}{% if appsmith_mongo_url_obj['port'] %}:{{ appsmith_mongo_url_obj['port'] }}{% endif %}{{ appsmith_mongo_url_obj['path'] }}?{{ appsmith_mongo_url_obj['query'] }} +{% else %} +APPSMITH_MONGODB_URI={{ appsmith_mongo_url }} +{% endif %} +APPSMITH_DISABLE_TELEMETRY=true +APPSMITH_ENCRYPTION_PASSWORD={{ appsmith_encryption_pass }} +APPSMITH_ENCRYPTION_SALT={{ appsmith_encryption_salt }} +APPSMITH_SIGNUP_DISABLED={{ appsmith_user_signup | ternary('false','true') }} +{% if appsmith_signup_whitelist | length > 0 and appsmith_user_signup %} +APPSMITH_SIGNUP_ALLOWED_DOMAINS={{ appsmith_signup_whitelist | join(',') }} +{% endif %} +APPSMITH_ADMIN_EMAILS={{ appsmith_admin_emails | join(',') }} diff --git a/roles/appsmith/templates/nginx.conf.j2 b/roles/appsmith/templates/nginx.conf.j2 new file mode 100644 index 0000000..627b447 --- /dev/null +++ b/roles/appsmith/templates/nginx.conf.j2 @@ -0,0 +1,34 @@ +server { + listen 80; + server_name {{ appsmith_public_url | urlsplit('hostname') }}; + include /etc/nginx/ansible_conf.d/acme.inc; + root {{ appsmith_root_dir }}/client; + client_max_body_size 10M; + + if ($request_method !~ ^(GET|POST|HEAD|PUT|DELETE|PATCH)$ ) { + return 405; + } + + # Send info about the original request to the backend + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Real-IP "$remote_addr"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$host"; + proxy_set_header Host "$host"; + + location / { + try_files $uri /index.html =404; + } + location /f { + proxy_pass https://cdn.optimizely.com/; + } + location /api { + proxy_pass http://127.0.0.1:{{ appsmith_server_port }}; + } + location /oauth2 { + proxy_pass http://127.0.0.1:{{ appsmith_server_port }}; + } + location /login { + proxy_pass http://127.0.0.1:{{ appsmith_server_port }}; + } +} diff --git a/roles/appsmith/templates/post-backup.sh.j2 b/roles/appsmith/templates/post-backup.sh.j2 new file mode 100644 index 0000000..8ac3f4d --- /dev/null +++ b/roles/appsmith/templates/post-backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -rf {{ appsmith_root_dir }}/backup/* diff --git a/roles/appsmith/templates/pre-backup.sh.j2 b/roles/appsmith/templates/pre-backup.sh.j2 new file mode 100644 index 0000000..ff5e338 --- /dev/null +++ b/roles/appsmith/templates/pre-backup.sh.j2 @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eo pipefail + +mongodump \ +{% if appsmith_mongo_pass is defined and appsmith_mongo_pass != False %} +{% set appsmith_mongo_url_obj = appsmith_mongo_url | urlsplit %} + --uri {{ appsmith_mongo_url_obj['scheme'] }}://{{ appsmith_mongo_user }}:{{ appsmith_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ appsmith_mongo_url_obj['hostname'] }}{% if appsmith_mongo_url_obj['port'] %}:{{ appsmith_mongo_url_obj['port'] }}{% endif %}{{ appsmith_mongo_url_obj['path'] }}?{{ appsmith_mongo_url_obj['query'] }} \ +{% else %} + --uri {{ appsmith_mongo_url }} \ +{% endif %} + --out {{ appsmith_root_dir }}/backup diff --git a/roles/appsmith/templates/pre-start.sh.j2 b/roles/appsmith/templates/pre-start.sh.j2 new file mode 100644 index 0000000..47bc1c4 --- /dev/null +++ b/roles/appsmith/templates/pre-start.sh.j2 @@ -0,0 +1,19 @@ +#!/bin/bash -e + +# If the conf changed since the last client deployement, or if the client build is newer than the one deployed, then re-deploy +if [ {{ appsmith_root_dir }}/etc/env -nt {{ appsmith_root_dir }}/client/ -o {{ appsmith_root_dir }}/src/app/client/build/ -nt {{ appsmith_root_dir }}/client/ ]; then + rsync -a --delete {{ appsmith_root_dir }}/src/app/client/build/ {{ appsmith_root_dir }}/client/ + find {{ appsmith_root_dir }}/client/ -type f | xargs \ + sed -i \ +{% for var in [ + "APPSMITH_SENTRY_DSN","APPSMITH_SMART_LOOK_ID","APPSMITH_OAUTH2_GOOGLE_CLIENT_ID", + "APPSMITH_OAUTH2_GITHUB_CLIENT_ID","APPSMITH_MARKETPLACE_ENABLED", + "APPSMITH_SEGMENT_KEY","APPSMITH_OPTIMIZELY_KEY","APPSMITH_ALGOLIA_API_ID", + "APPSMITH_ALGOLIA_SEARCH_INDEX_NAME","APPSMITH_ALGOLIA_API_KEY","APPSMITH_CLIENT_LOG_LEVEL", + "APPSMITH_GOOGLE_MAPS_API_KEY","APPSMITH_TNC_PP","APPSMITH_VERSION_ID", + "APPSMITH_VERSION_RELEASE_DATE","APPSMITH_INTERCOM_APP_ID","APPSMITH_MAIL_ENABLED","APPSMITH_DISABLE_TELEMETRY"] %} + -e "s/__{{ var }}__/${{ '{' ~ var ~ '}' }}/g"{% if not loop.last %} \{% endif %} + +{% endfor %} + +fi diff --git a/roles/backup/defaults/main.yml b/roles/backup/defaults/main.yml new file mode 100644 index 0000000..5b1614e --- /dev/null +++ b/roles/backup/defaults/main.yml @@ -0,0 +1,36 @@ +--- + +# The shell of the lbkp account +backup_shell: '/bin/bash' + +# List of commands lbkp will be allowed to run as root, with sudo +backup_sudo_base_commands: + - /usr/bin/rsync + - /usr/local/bin/pre-backup + - /usr/local/bin/post-backup + - /bin/tar + - /bin/gtar +backup_sudo_extra_commands: [] +backup_sudo_commands: "{{ backup_sudo_base_commands + backup_sudo_extra_commands }}" + +# List of ssh public keys to deploy +backup_ssh_keys: [] + +# Options to set for the ssh keys, to restrict what they can do +backup_ssh_keys_options: + - no-X11-forwarding + - no-agent-forwarding + - no-pty + +# List of IP address allowed to use the ssh keys +# Empty list means no restriction +backup_src_ip: [] + +# Custom pre / post script +backup_pre_script: | + #!/bin/bash -e + # Nothing to do +backup_post_script: | + #!/bin/bash -e + # Nothing to do +... diff --git a/roles/backup/files/dump-megaraid-cfg b/roles/backup/files/dump-megaraid-cfg new file mode 100644 index 0000000..9cef7bf --- /dev/null +++ b/roles/backup/files/dump-megaraid-cfg @@ -0,0 +1,57 @@ +#!/usr/bin/perl -w + +# This script will backup the config of MegaRAID based +# RAID controllers. The saved config can be restored with +# MegaCli -CfgRestore -f /home/lbkp/mega_0.bin for example +# It also create a backup of the config as text, so you can +# manually check how things were configured at a certain point in time + +# If MegaCli is not installed, then the script does nothing + +use strict; + +my $megacli = undef; + +if (-x '/opt/MegaRAID/MegaCli/MegaCli64'){ + $megacli = '/opt/MegaRAID/MegaCli/MegaCli64'; +} elsif (-x '/opt/MegaRAID/MegaCli/MegaCli'){ + $megacli = '/opt/MegaRAID/MegaCli/MegaCli'; +} + +if (!$megacli){ + print "MegaCli not installed, nothing to do\n"; + exit 0; +} + +my $adapters = 0; +foreach (qx($megacli -adpCount -NoLog)) { + if ( m/Controller Count:\s*(\d+)/ ) { + $adapters = $1; + last; + } +} + +foreach my $adp (0..$adapters-1){ + my $hba = 0; + my $failgrouplist = 0; + foreach my $line (qx($megacli -CfgDsply -a$adp -NoLog)) { + if ( $line =~ m/Failed to get Disk Group list/ ) { + $failgrouplist = 1; + } elsif ( $line =~ m/Product Name:.*(JBOD|HBA)/ ) { + $hba = 1; + } + } + # Skip adapter if in HBA mode + next if ($hba && $failgrouplist); + + # Save the config in binary format + print "Saving config for adapter $adp\n"; + qx($megacli -CfgSave -f /home/lbkp/megaraid/cfg_$adp.bin -a$adp -NoLog); + die "Failed to backup conf for adapter $adp\n" unless ($? == 0); + + # Now also save in text representation + open TXT, ">/home/lbkp/megaraid/cfg_$adp.txt"; + print TXT foreach qx($megacli -CfgDsply -a$adp -NoLog); + die "Failed to backup Cfg text description for adapter $adp\n" unless ($? == 0); + close TXT; +} diff --git a/roles/backup/files/dump-rpms-list b/roles/backup/files/dump-rpms-list new file mode 100644 index 0000000..a0fdc70 --- /dev/null +++ b/roles/backup/files/dump-rpms-list @@ -0,0 +1,3 @@ +#!/bin/sh + +/bin/rpm -qa --qf "%{NAME}\t%{VERSION}\t%{RELEASE}\n" | grep -v gpg-pubkey | sort > /home/lbkp/rpms.list diff --git a/roles/backup/files/post-backup b/roles/backup/files/post-backup new file mode 100644 index 0000000..4b55acc --- /dev/null +++ b/roles/backup/files/post-backup @@ -0,0 +1,15 @@ +#!/bin/bash + +if [ -d "/etc/backup/post.d" ]; then + for H in $(find /etc/backup/post.d -type f -o -type l | sort); do + if [ -x $H ]; then + echo "Running hook $H" + $H "$@" + echo "Finished hook $H" + else + echo "Skiping hook $H as it's not executable" + fi + done +fi +# Remove the lock +rm -f /var/lock/bkp.lock diff --git a/roles/backup/files/pre-backup b/roles/backup/files/pre-backup new file mode 100644 index 0000000..78a8969 --- /dev/null +++ b/roles/backup/files/pre-backup @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +# 2 locks are needed. The first one ensure we don't run +# The pre-backup script twice. It's an atomic lock. +# Then we need a second lock which will last until the post-backup ran +# This one doesn't need to be atomic (as we already checked this) +PRELOCKFILE="/var/lock/pre-bkp.lock" +exec 200>$PRELOCKFILE +flock -n 200 || ( echo "Couldn't aquire pre-backup lock" && exit 1 ) +PID=$$ +echo $PID 1>&200 + +if [ -e /var/lock/bkp.lock ]; then + # Consider the lock to be stale if it's older than 8 hours + if [ "$(( $(date +"%s") - $(stat -c "%Y" /var/lock/bkp.lock) ))" -gt "28800" ]; then + rm /var/lock/bkp.lock + else + echo "Another backup is running" + exit 1 + fi +fi +touch /var/lock/bkp.lock +if [ -d "/etc/backup/pre.d" ]; then + for H in $(find /etc/backup/pre.d -type f -o -type l | sort); do + if [ -x $H ]; then + echo "Running hook $H" + $H "$@" + echo "Finished hook $H" + else + echo "Skiping hook $H as it's not executable" + fi + done +fi diff --git a/roles/backup/files/rm-megaraid-cfg b/roles/backup/files/rm-megaraid-cfg new file mode 100644 index 0000000..17a1243 --- /dev/null +++ b/roles/backup/files/rm-megaraid-cfg @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f /home/lbkp/megaraid/* diff --git a/roles/backup/tasks/main.yml b/roles/backup/tasks/main.yml new file mode 100644 index 0000000..cb54730 --- /dev/null +++ b/roles/backup/tasks/main.yml @@ -0,0 +1,94 @@ +--- + +- name: Install backup tools + yum: name=rsync + when: ansible_os_family == 'RedHat' + +- name: Install backup tools + apt: name=rsync + when: ansible_os_family == 'Debian' + +- name: Create a local backup user account + user: name=lbkp comment="Local backup account" system=yes shell={{ backup_shell }} + tags: backup + +- name: Deploy sudo configuration + template: src=sudo.j2 dest=/etc/sudoers.d/backup mode=400 + tags: backup + +- name: Deploy SSH keys for the backup account + authorized_key: + user: lbkp + key: "{{ backup_ssh_keys | join(\"\n\") }}" + key_options: "{{ backup_ssh_keys_options | join(',') }}" + exclusive: yes + when: backup_src_ip is not defined or backup_src_ip | length < 1 + tags: backup + +- name: Deploy SSH keys for the backup account (with source IP restriction) + authorized_key: + user: lbkp + key: "{{ backup_ssh_keys | join(\"\n\") }}" + key_options: "from=\"{{ backup_src_ip | join(',') }}\",{{ backup_ssh_keys_options | join(',') }}" + exclusive: yes + when: + - backup_src_ip is defined + - backup_src_ip | length > 0 + tags: backup + +- name: Create pre and post backup hook dir + file: path={{ item }} state=directory mode=750 + with_items: + - /etc/backup/pre.d + - /etc/backup/post.d + tags: backup + +- name: Deploy default pre/post backup hooks + copy: + content: "{{ item.content }}" + dest: /etc/backup/{{ item.type }}.d/default + mode: 0755 + loop: + - type: pre + content: "{{ backup_pre_script }}" + - type: post + content: "{{ backup_post_script }}" + tags: backup + +- name: Copy pre-backup script + copy: src={{ item }} dest=/usr/local/bin/{{ item }} mode=750 group=lbkp + with_items: + - pre-backup + - post-backup + tags: backup + +- name: Deploy rpm dump list script + copy: src=dump-rpms-list dest=/etc/backup/pre.d/dump-rpms-list mode=755 + when: ansible_os_family == 'RedHat' + tags: backup + +- name: Create megaraid dump dir + file: path=/home/lbkp/megaraid state=directory + tags: backup + +- name: Deploy MegaCli backup scripts + copy: src={{ item.script }} dest=/etc/backup/{{ item.type }}.d/{{ item.script }} mode=750 + with_items: + - script: dump-megaraid-cfg + type: pre + - script: rm-megaraid-cfg + type: post + when: lsi_controllers | default([]) | length > 0 + tags: backup + +- name: Excludes for proxmox backup client + copy: + dest: /.pxarexclude + content: | + var/log/lastlog + when: + - ansible_virtualization_role == 'guest' + - ansible_virtualization_type == 'lxc' or ansible_virtualization_type == 'systemd-nspawn' + tags: backup + +... diff --git a/roles/backup/templates/sudo.j2 b/roles/backup/templates/sudo.j2 new file mode 100644 index 0000000..c272361 --- /dev/null +++ b/roles/backup/templates/sudo.j2 @@ -0,0 +1,2 @@ +Defaults:lbkp !requiretty +lbkp ALL=(root) NOPASSWD: {{ backup_sudo_commands | join(',') }} diff --git a/roles/backuppc/defaults/main.yml b/roles/backuppc/defaults/main.yml new file mode 100644 index 0000000..94af17f --- /dev/null +++ b/roles/backuppc/defaults/main.yml @@ -0,0 +1,19 @@ +--- + +# You can choose either 3 or 4 +bpc_major_version: 3 + +# Auth to access BackupPC. Can be basic, lemonldap, lemonldap2 or none +bpc_auth: basic + +# List of IP address allowed +bpc_src_ip: [] + +# Should backuppc be started on boot ? +# You might want to turn this off if for example you must unlock +# the device on which you have your backup, and manually start backuppc after that +bpc_enabled: True + +# Should /BackupPC aliases be added on the main vhost ? +# You might want to, but you can also disable this and grant access only through a dedicated vhost +bpc_alias_on_main_vhost: True diff --git a/roles/backuppc/handlers/main.yml b/roles/backuppc/handlers/main.yml new file mode 100644 index 0000000..dc1bfa2 --- /dev/null +++ b/roles/backuppc/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- include: ../httpd_common/handlers/main.yml + +... diff --git a/roles/backuppc/meta/main.yml b/roles/backuppc/meta/main.yml new file mode 100644 index 0000000..c8aca60 --- /dev/null +++ b/roles/backuppc/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - { role: httpd_front } diff --git a/roles/backuppc/tasks/main.yml b/roles/backuppc/tasks/main.yml new file mode 100644 index 0000000..bfb5dc3 --- /dev/null +++ b/roles/backuppc/tasks/main.yml @@ -0,0 +1,53 @@ +--- + +- name: Install BackupPC 4 + yum: + name: + - BackupPC4 + - fuse-backuppcfs4 + when: bpc_major_version == 4 + tags: bpc + +- name: Install BackupPC 3 + yum: + name: + - BackupPC + - fuse-backuppcfs + when: bpc_major_version != 4 + tags: bpc + +- name: Install tools + yum: + name: + - rsync + - tar + - samba-client + - openssh-clients + - BackupPC-server-scripts + - fuse-chunkfs + tags: bpc + +- name: Deploy httpd conf + template: src=httpd.conf.j2 dest=/etc/httpd/ansible_conf.d/40-BackupPC.conf + notify: reload httpd + tags: bpc + +- name: Deploy sudo config + template: src=sudoers.j2 dest=/etc/sudoers.d/backuppc mode=0400 + tags: bpc + +- name: Create SSH Key + user: + name: backuppc + generate_ssh_key: yes + ssh_key_bits: 4096 + tags: bpc + +- name: Start the service + service: name=backuppc state=started + when: bpc_enabled + tags: bpc + +- name: Handle backuppc service status + service: name=backuppc enabled={{ bpc_enabled }} + tags: bpc diff --git a/roles/backuppc/templates/httpd.conf.j2 b/roles/backuppc/templates/httpd.conf.j2 new file mode 100644 index 0000000..0df46f8 --- /dev/null +++ b/roles/backuppc/templates/httpd.conf.j2 @@ -0,0 +1,25 @@ + + SSLRequireSSL on +{% if bpc_auth == "lemonldap" %} + PerlHeaderParserHandler Lemonldap::NG::Handler +{% elif bpc_auth == "lemonldap2" %} + PerlHeaderParserHandler Lemonldap::NG::Handler::ApacheMP2 +{% elif bpc_auth == "basic" %} + AuthType Basic + AuthUserFile /etc/BackupPC/apache.users + AuthName "BackupPC" + Require valid-user +{% endif %} + +{% if bpc_src_ip | length < 1 %} + Require all denied +{% else %} + Require ip {{ bpc_src_ip | join(' ') }} +{% endif %} + + +{% if bpc_auth != False and bpc_auth != 'none' and bpc_alias_on_main_vhost == True %} +Alias /BackupPC/images /usr/share/BackupPC/html/ +ScriptAlias /BackupPC /usr/share/BackupPC/sbin/BackupPC_Admin +ScriptAlias /backuppc /usr/share/BackupPC/sbin/BackupPC_Admin +{% endif %} diff --git a/roles/backuppc/templates/sudoers.j2 b/roles/backuppc/templates/sudoers.j2 new file mode 100644 index 0000000..664f505 --- /dev/null +++ b/roles/backuppc/templates/sudoers.j2 @@ -0,0 +1,3 @@ +Defaults:backuppc !requiretty +Cmnd_Alias BACKUPPC = /usr/bin/rsync, /bin/tar, /bin/gtar, /usr/local/bin/pre-backup, /usr/local/bin/post-backup, /usr/bin/virt-backup +backuppc ALL=(root) NOPASSWD: BACKUPPC diff --git a/roles/bookstack/defaults/main.yml b/roles/bookstack/defaults/main.yml new file mode 100644 index 0000000..1a4f930 --- /dev/null +++ b/roles/bookstack/defaults/main.yml @@ -0,0 +1,78 @@ +--- + +# Version to deploy +bookstack_version: '21.11.2' +# URL of the arhive +bookstack_archive_url: https://github.com/BookStackApp/BookStack/archive/v{{ bookstack_version }}.tar.gz +# Expected sha1 of the archive +bookstack_archive_sha1: c9e8a0da936f7a2840c416dde70451f046e2b7f3 + +# Should ansible handle bookstack upgrades or just the inintial install +bookstack_manage_upgrade: True + +# We can deploy several bookstack instance on a single host +# each one can have a different ID which can be a simple number +# or a short string +bookstack_id: 1 +# Where to install bookstack +bookstack_root_dir: /opt/bookstack_{{ bookstack_id }} +# User under which the app will be executed +bookstack_php_user: php-bookstack_{{ bookstack_id }} +# Version of PHP used +bookstack_php_version: 80 +# Or you can specify here the name of a custom PHP FPM pool. See the httpd_php role +# bookstack_php_fpm_pool: custom_bookstack + +# If defined, an alias will be added in httpd's config to access bookstack +# Else, you'll have to defined a vhost to make bookstack accessible. See httpd_common role +bookstack_web_alias: /bookstack_{{ bookstack_id }} + +# You can restrict access to bookstack. If not defined or empty, +# no restriction will be made +bookstack_src_ip: "{{ httpd_ssl_src_ip | default(httpd_src_ip) | default([]) }}" + +# List of trusted proxies from which we can trust the X-Forwarded-For header +# Useful to get real client IP when BookStack is running behind a reverse proxy +# bookstack_trusted_proxies: +# - 10.99.2.10 +# The default value is to use the same as bookstack_src_ip if it's not empty and doesn't contain 0.0.0.0/0 +bookstack_trusted_proxies: "{{ (bookstack_src_ip | length > 0 and '0.0.0.0/0' not in bookstack_src_ip) | ternary(bookstack_src_ip, []) }}" + +# MySQL Database +bookstack_db_server: "{{ mysql_server | default('locaclhost') }}" +bookstack_db_port: 3306 +bookstack_db_user: bookstack_{{ bookstack_id }} +bookstack_db_name: bookstack_{{ bookstack_id }} +# If no pass is defined, a random one will be created and stored in meta/ansible_dbpass +# bookstack_db_pass: S3cr3t. + +# Application key. If not defined, a random one will be generated and store in meta/ansible_app_key +# bookstack_app_key: base64:H/zDPBqtK2BjOkgCrMMGGH+sSjOBrBs/ibcD4ozQc90= + +# Public URL of the app +bookstack_public_url: http://{{ inventory_hostname }}/bookstack_{{ bookstack_id }} + +# Email settings. Default will use local postfix installation +bookstack_email_name: BookStack +bookstack_email_from: no-reply@{{ ansible_domain }} +bookstack_email_server: localhost +bookstack_email_port: 25 +# You can set user and pass if needed +# bookstack_email_user: user@example.org +# bookstack_email_pass: S3cR3t. +# Encryption can be tls, ssl or null +bookstack_email_encryption: 'null' + +# Default lang +bookstack_default_lang: fr + +# Session lifetime, in minutes +bookstack_session_lifetime: 480 + +# You can set custom directive with this: +# bookstack_settings: +# AUTH_METHOD: saml2 +# SAML2_NAME: SSO +# SAML2_EMAIL_ATTRIBUTE: email +bookstack_settings: {} + diff --git a/roles/bookstack/meta/main.yml b/roles/bookstack/meta/main.yml new file mode 100644 index 0000000..9391075 --- /dev/null +++ b/roles/bookstack/meta/main.yml @@ -0,0 +1,8 @@ +--- + +allow_duplicates: True +dependencies: + - role: mkdir + - role: mysql_server + when: bookstack_db_server in ['localhost','127.0.0.1'] + - role: composer diff --git a/roles/bookstack/tasks/archive_post.yml b/roles/bookstack/tasks/archive_post.yml new file mode 100644 index 0000000..a4e7284 --- /dev/null +++ b/roles/bookstack/tasks/archive_post.yml @@ -0,0 +1,10 @@ +--- + +- name: Compress previous version + command: tar cf {{ bookstack_root_dir }}/archives/{{ bookstack_current_version }}.tar.zst ./ --use-compress-program=zstd + args: + chdir: "{{ bookstack_root_dir }}/archives/{{ bookstack_current_version }}" + warn: False + environment: + ZSTD_CLEVEL: 10 + tags: bookstack diff --git a/roles/bookstack/tasks/archive_pre.yml b/roles/bookstack/tasks/archive_pre.yml new file mode 100644 index 0000000..b8d4a64 --- /dev/null +++ b/roles/bookstack/tasks/archive_pre.yml @@ -0,0 +1,31 @@ +--- + +- name: Create the archive dir + file: path={{ bookstack_root_dir }}/archives/{{ bookstack_current_version }} state=directory + tags: bookstack + +- name: Archive current version + synchronize: + src: "{{ bookstack_root_dir }}/app" + dest: "{{ bookstack_root_dir }}/archives/{{ bookstack_current_version }}/" + compress: False + delete: True + rsync_opts: + - '--exclude=/storage/' + delegate_to: "{{ inventory_hostname }}" + tags: bookstack + +- name: Dump the database + mysql_db: + state: dump + name: "{{ bookstack_db_name }}" + target: "{{ bookstack_root_dir }}/archives/{{ bookstack_current_version }}/{{ bookstack_db_name }}.sql.xz" + login_host: "{{ bookstack_db_server }}" + login_user: "{{ bookstack_db_user }}" + login_password: "{{ bookstack_db_pass }}" + quick: True + single_transaction: True + environment: + XZ_OPT: -T0 + tags: bookstack + diff --git a/roles/bookstack/tasks/cleanup.yml b/roles/bookstack/tasks/cleanup.yml new file mode 100644 index 0000000..63642a9 --- /dev/null +++ b/roles/bookstack/tasks/cleanup.yml @@ -0,0 +1,9 @@ +--- + +- name: Remove tmp and obsolete files + file: path={{ item }} state=absent + loop: + - "{{ bookstack_root_dir }}/archives/{{ bookstack_current_version }}" + - "{{ bookstack_root_dir }}/tmp/BookStack-{{ bookstack_version }}" + - "{{ bookstack_root_dir }}/tmp/BookStack-{{ bookstack_version }}.tar.gz" + tags: bookstack diff --git a/roles/bookstack/tasks/conf.yml b/roles/bookstack/tasks/conf.yml new file mode 100644 index 0000000..74fd9eb --- /dev/null +++ b/roles/bookstack/tasks/conf.yml @@ -0,0 +1,54 @@ +--- + +- import_tasks: ../includes/webapps_webconf.yml + vars: + - app_id: bookstack_{{ bookstack_id }} + - php_version: "{{ bookstack_php_version }}" + - php_fpm_pool: "{{ bookstack_php_fpm_pool | default('') }}" + tags: bookstack + +- when: bookstack_app_key is not defined + block: + - name: Generate a uniq application key + shell: /bin/php{{ bookstack_php_version }} {{ bookstack_root_dir }}/app/artisan key:generate --show > {{ bookstack_root_dir }}/meta/ansible_app_key + args: + creates: "{{ bookstack_root_dir }}/meta/ansible_app_key" + + - name: Read application key + slurp: src={{ bookstack_root_dir }}/meta/ansible_app_key + register: bookstack_rand_app_key + + - set_fact: bookstack_app_key={{ bookstack_rand_app_key.content | b64decode | trim }} + + tags: bookstack + +- name: Deploy BookStack configuration + template: src=env.j2 dest={{ bookstack_root_dir }}/app/.env group={{ bookstack_php_user }} mode=640 + tags: bookstack + +- when: bookstack_install_mode != 'none' + block: + - name: Migrate the database + shell: echo yes | /bin/php{{ bookstack_php_version }} {{ bookstack_root_dir }}/app/artisan migrate + + - name: Clear cache + command: /bin/php{{ bookstack_php_version }} {{ bookstack_root_dir }}/app/artisan cache:clear + + - name: Clear views + command: /bin/php{{ bookstack_php_version }} {{ bookstack_root_dir }}/app/artisan view:clear + + - name: Regenerate search + command: /bin/php{{ bookstack_php_version }} {{ bookstack_root_dir }}/app/artisan bookstack:regenerate-search + + become_user: "{{ bookstack_php_user }}" + tags: bookstack + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ bookstack_root_dir }}/perms.sh mode=755 + register: bookstack_perm_script + tags: bookstack + +- name: Apply permissions + command: "{{ bookstack_root_dir }}/perms.sh" + when: bookstack_perm_script.changed or bookstack_install_mode != 'none' + tags: bookstack diff --git a/roles/bookstack/tasks/directories.yml b/roles/bookstack/tasks/directories.yml new file mode 100644 index 0000000..c3b6fa1 --- /dev/null +++ b/roles/bookstack/tasks/directories.yml @@ -0,0 +1,23 @@ +--- + +- name: Create required directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ bookstack_root_dir }}" + - dir: "{{ bookstack_root_dir }}/meta" + mode: 700 + - dir: "{{ bookstack_root_dir }}/backup" + mode: 700 + - dir: "{{ bookstack_root_dir }}/archives" + mode: 700 + - dir: "{{ bookstack_root_dir }}/app" + - dir: "{{ bookstack_root_dir }}/sessions" + group: "{{ bookstack_php_user }}" + mode: 770 + - dir: "{{ bookstack_root_dir }}/tmp" + group: "{{ bookstack_php_user }}" + mode: 770 + - dir: "{{ bookstack_root_dir }}/data" + group: "{{ bookstack_php_user }}" + mod: 700 + tags: bookstack diff --git a/roles/bookstack/tasks/facts.yml b/roles/bookstack/tasks/facts.yml new file mode 100644 index 0000000..cb53ed8 --- /dev/null +++ b/roles/bookstack/tasks/facts.yml @@ -0,0 +1,20 @@ +--- + +# Detect installed version (if any) +- block: + - import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ bookstack_root_dir }}" + - version: "{{ bookstack_version }}" + - set_fact: bookstack_install_mode={{ (install_mode == 'upgrade' and not bookstack_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: bookstack_current_version={{ current_version | default('') }} + tags: bookstack + +# Create a random pass for the DB if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ bookstack_root_dir }}/meta/ansible_dbpass" + - set_fact: bookstack_db_pass={{ rand_pass }} + when: bookstack_db_pass is not defined + tags: bookstack diff --git a/roles/bookstack/tasks/install.yml b/roles/bookstack/tasks/install.yml new file mode 100644 index 0000000..792b978 --- /dev/null +++ b/roles/bookstack/tasks/install.yml @@ -0,0 +1,86 @@ +--- + +- name: Install needed tools + package: + name: + - acl + - tar + - zstd + - mariadb + tags: bookstack + +- when: bookstack_install_mode != 'none' + block: + - name: Download bookstack + get_url: + url: "{{ bookstack_archive_url }}" + dest: "{{ bookstack_root_dir }}/tmp" + checksum: sha1:{{ bookstack_archive_sha1 }} + + - name: Extract the archive + unarchive: + src: "{{ bookstack_root_dir }}/tmp/BookStack-{{ bookstack_version }}.tar.gz" + dest: "{{ bookstack_root_dir }}/tmp" + remote_src: True + + - name: Move BookStack to its final dir + synchronize: + src: "{{ bookstack_root_dir }}/tmp/BookStack-{{ bookstack_version }}/" + dest: "{{ bookstack_root_dir }}/app/" + delete: True + compress: False + rsync_opts: + - '--exclude=/storage/' + - '--exclude=/public/uploads/' + delegate_to: "{{ inventory_hostname }}" + + - name: Populate data directories + synchronize: + src: "{{ bookstack_root_dir }}/tmp/BookStack-{{ bookstack_version }}/{{ item }}" + dest: "{{ bookstack_root_dir }}/data/" + compress: False + delegate_to: "{{ inventory_hostname }}" + loop: + - storage + - public/uploads + + - name: Link data directories + file: src={{ item.src }} dest={{ item.dest }} state=link + loop: + - src: "{{ bookstack_root_dir }}/data/storage" + dest: "{{ bookstack_root_dir }}/app/storage" + - src: "{{ bookstack_root_dir }}/data/uploads" + dest: "{{ bookstack_root_dir }}/app/public/uploads" + + - name: Install PHP libs with composer + composer: + command: install + working_dir: "{{ bookstack_root_dir }}/app" + executable: /bin/php{{ bookstack_php_version }} + environment: + php: /bin/php{{ bookstack_php_version }} + + tags: bookstack + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ bookstack_db_name }}" + - db_user: "{{ bookstack_db_user }}" + - db_server: "{{ bookstack_db_server }}" + - db_pass: "{{ bookstack_db_pass }}" + tags: bookstack + +- name: Set correct SELinux context + sefcontext: + target: "{{ bookstack_root_dir }}(/.*)?" + setype: httpd_sys_content_t + state: present + when: ansible_selinux.status == 'enabled' + tags: bookstack + +- name: Install pre/post backup hooks + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/bookstack_{{ bookstack_id }} mode=700 + loop: + - pre + - post + tags: bookstack diff --git a/roles/bookstack/tasks/main.yml b/roles/bookstack/tasks/main.yml new file mode 100644 index 0000000..57f4c85 --- /dev/null +++ b/roles/bookstack/tasks/main.yml @@ -0,0 +1,13 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: bookstack_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: write_version.yml +- include: archive_post.yml + when: bookstack_install_mode == 'upgrade' +- include: cleanup.yml diff --git a/roles/bookstack/tasks/user.yml b/roles/bookstack/tasks/user.yml new file mode 100644 index 0000000..ccec17e --- /dev/null +++ b/roles/bookstack/tasks/user.yml @@ -0,0 +1,5 @@ +--- + +- name: Create user account + user: name={{ bookstack_php_user }} system=True shell=/sbin/nologin home={{ bookstack_root_dir }} + tags: bookstack diff --git a/roles/bookstack/tasks/write_version.yml b/roles/bookstack/tasks/write_version.yml new file mode 100644 index 0000000..d7e8744 --- /dev/null +++ b/roles/bookstack/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write current version + copy: content={{ bookstack_version }} dest={{ bookstack_root_dir }}/meta/ansible_version + tags: bookstack diff --git a/roles/bookstack/templates/env.j2 b/roles/bookstack/templates/env.j2 new file mode 100644 index 0000000..9fdbdb8 --- /dev/null +++ b/roles/bookstack/templates/env.j2 @@ -0,0 +1,28 @@ +APP_KEY={{ bookstack_app_key }} +APP_URL={{ bookstack_public_url }} +DB_HOST={{ bookstack_db_server }} +DB_DATABASE={{ bookstack_db_name }} +DB_USERNAME={{ bookstack_db_user }} +DB_PASSWORD={{ bookstack_db_pass | quote }} +MAIL_DRIVER=smtp +MAIL_FROM_NAME="{{ bookstack_email_name }}" +MAIL_FROM={{ bookstack_email_from }} +MAIL_HOST={{ bookstack_email_server }} +MAIL_PORT={{ bookstack_email_port }} +{% if bookstack_email_user is defined and bookstack_email_pass is defined %} +MAIL_USERNAME={{ bookstack_email_user }} +MAIL_PASSWORD={{ bookstack_email_pass | quote }} +{% endif %} +MAIL_ENCRYPTION={{ bookstack_email_encryption }} +APP_TIMEZONE={{ system_tz | default('UTC') }} +APP_LANG={{ bookstack_default_lang }} +SESSION_SECURE_COOKIE={{ (bookstack_public_url | urlsplit('scheme') == 'https') | ternary('true','false') }} +SESSION_COOKIE_NAME=bookstack_{{ bookstack_id }}_session +SESSION_LIFETIME={{ bookstack_session_lifetime }} +CACHE_PREFIX=bookstack_{{ bookstack_id }} +{% if bookstack_trusted_proxies | length > 0 %} +APP_PROXIES={{ bookstack_trusted_proxies | join(',') }} +{% endif %} +{% for key in bookstack_settings.keys() | list %} +{{ key }}="{{ bookstack_settings[key] }}" +{% endfor %} diff --git a/roles/bookstack/templates/httpd.conf.j2 b/roles/bookstack/templates/httpd.conf.j2 new file mode 100644 index 0000000..d42fa33 --- /dev/null +++ b/roles/bookstack/templates/httpd.conf.j2 @@ -0,0 +1,39 @@ +{% if bookstack_web_alias is defined and bookstack_web_alias != False %} +Alias /{{ bookstack_web_alias | regex_replace('^/','') }} {{ bookstack_root_dir }}/app/public +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + + + AllowOverride All + Options FollowSymLinks +{% if bookstack_src_ip is defined and bookstack_src_ip | length > 0 %} + Require ip {{ bookstack_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ bookstack_php_fpm_pool | default('bookstack_' + bookstack_id | string) }}.sock|fcgi://localhost" + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + + + Require all denied + + + diff --git a/roles/bookstack/templates/perms.sh.j2 b/roles/bookstack/templates/perms.sh.j2 new file mode 100644 index 0000000..441ce84 --- /dev/null +++ b/roles/bookstack/templates/perms.sh.j2 @@ -0,0 +1,19 @@ +#!/bin/bash + +restorecon -R {{ bookstack_root_dir }} +chown root:root {{ bookstack_root_dir }} +chmod 700 {{ bookstack_root_dir }} +setfacl -R -k -b {{ bookstack_root_dir }} +setfacl -m u:{{ bookstack_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:x {{ bookstack_root_dir }} +find {{ bookstack_root_dir }}/app -type f -exec chmod 644 "{}" \; +find {{ bookstack_root_dir }}/app -type d -exec chmod 755 "{}" \; +chown root:{{ bookstack_php_user }} {{ bookstack_root_dir }}/app/.env +chmod 640 {{ bookstack_root_dir }}/app/.env +chown -R {{ bookstack_php_user }} {{ bookstack_root_dir }}/app/bootstrap/cache +chmod 700 {{ bookstack_root_dir }}/app/bootstrap/cache +chown -R {{ bookstack_php_user }} {{ bookstack_root_dir }}/data +chmod 700 {{ bookstack_root_dir }}/data +setfacl -R -m u:{{ httpd_user | default('apache') }}:rx {{ bookstack_root_dir }}/app/public +setfacl -m u:{{ httpd_user | default('apache') }}:x {{ bookstack_root_dir }}/data/ +setfacl -R -m u:{{ httpd_user | default('apache') }}:rx {{ bookstack_root_dir }}/data/uploads +find {{ bookstack_root_dir }} -name .htaccess -exec chmod 644 "{}" \; diff --git a/roles/bookstack/templates/php.conf.j2 b/roles/bookstack/templates/php.conf.j2 new file mode 100644 index 0000000..51a3b4b --- /dev/null +++ b/roles/bookstack/templates/php.conf.j2 @@ -0,0 +1,35 @@ +[bookstack_{{ bookstack_id }}] + +listen.owner = root +listen.group = apache +listen.mode = 0660 +listen = /run/php-fpm/bookstack_{{ bookstack_id }}.sock +user = {{ bookstack_php_user }} +group = {{ bookstack_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 5m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 256M +php_admin_value[session.save_path] = {{ bookstack_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ bookstack_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ bookstack_root_dir }}/tmp +php_admin_value[post_max_size] = 100M +php_admin_value[upload_max_filesize] = 100M +php_admin_value[disable_functions] = system, show_source, symlink, exec, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ bookstack_root_dir }}:/usr/share/pear/:/usr/share/php/ +php_admin_value[max_execution_time] = 60 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = off +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/bookstack/templates/post-backup.j2 b/roles/bookstack/templates/post-backup.j2 new file mode 100644 index 0000000..4813cc1 --- /dev/null +++ b/roles/bookstack/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ bookstack_root_dir }}/backup/*.sql.zst diff --git a/roles/bookstack/templates/pre-backup.j2 b/roles/bookstack/templates/pre-backup.j2 new file mode 100644 index 0000000..6611527 --- /dev/null +++ b/roles/bookstack/templates/pre-backup.j2 @@ -0,0 +1,13 @@ +#!/bin/sh +set -eo pipefail + +/usr/bin/mysqldump \ +{% if bookstack_db_server not in ['localhost','127.0.0.1'] %} + --user={{ bookstack_db_user | quote }} \ + --password={{ bookstack_db_pass | quote }} \ + --host={{ bookstack_db_server | quote }} \ + --port={{ bookstack_db_port | quote }} \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ bookstack_db_name | quote }} | zstd -c > {{ bookstack_root_dir }}/backup/{{ bookstack_db_name }}.sql.zst + diff --git a/roles/clamav/defaults/main.yml b/roles/clamav/defaults/main.yml new file mode 100644 index 0000000..388103a --- /dev/null +++ b/roles/clamav/defaults/main.yml @@ -0,0 +1,16 @@ +--- +clam_mirror: database.clamav.net +clam_user: clamav +clam_group: clamav +clam_enable_clamd: False +clam_custom_db_url: [] +clam_safebrowsing: True +clam_listen_port: 3310 +clam_ports: "{{ [clam_listen_port] + [clam_stream_port_min + ':' + clam_stream_port_max] }}" +clam_listen_ip: 127.0.0.1 +clam_src_ip: [] +# Max stream size, in MB +clam_stream_max_size: 50 +clam_stream_port_min: 30000 +clam_stream_port_max: 32000 + diff --git a/roles/clamav/handlers/main.yml b/roles/clamav/handlers/main.yml new file mode 100644 index 0000000..eb97d88 --- /dev/null +++ b/roles/clamav/handlers/main.yml @@ -0,0 +1,9 @@ +--- + +- include: ../common/handlers/main.yml + +- name: restart freshclam + service: name=freshclam state=restarted + +- name: restart clamd + service: name=clamd state={{ clam_enable_clamd | ternary('restarted','stopped') }} diff --git a/roles/clamav/tasks/main.yml b/roles/clamav/tasks/main.yml new file mode 100644 index 0000000..598587d --- /dev/null +++ b/roles/clamav/tasks/main.yml @@ -0,0 +1,57 @@ +--- + +- name: Install packages + yum: + name: + - clamav + - clamav-data-empty + - clamav-server-systemd + - clamav-update + +- name: Create clamav user account + user: + name: clamav + system: True + shell: /sbin/nologin + comment: "ClamAV antivirus user account" + +- name: Set SELinux + seboolean: name={{ item }} state=True persistent=True + with_items: + - clamd_use_jit + - antivirus_can_scan_system + when: ansible_selinux.status == 'enabled' + +- name: Deploy freshclam configuration + template: src=freshclam.conf.j2 dest=/etc/freshclam.conf mode=644 + notify: restart freshclam + +- name: Deploy clamd configuration + template: src=clamd.conf.j2 dest=/etc/clamd.conf + notify: restart clamd + +- name: Deploy systemd units + template: src={{ item }}.j2 dest=/etc/systemd/system/{{ item }} + with_items: + - freshclam.service + - clamd.service + notify: + - restart freshclam + - restart clamd + register: clamav_units + +- name: Deploy tmpfiles.d fragment + copy: + content: 'd /run/clamav 755 {{ clam_user }} {{ clam_group }}' + dest: /etc/tmpfiles.d/clamav.conf + notify: systemd-tmpfiles + +- name: Reload systemd + command: systemctl daemon-reload + when: clamav_units.changed + +- name: Start and enable freshclam + service: name=freshclam state=started enabled=True + +- name: Handle clamd service + service: name=clamd state={{ clam_enable_clamd | ternary('started','stopped') }} enabled={{ clam_enable_clamd }} diff --git a/roles/clamav/templates/clamd.conf.j2 b/roles/clamav/templates/clamd.conf.j2 new file mode 100644 index 0000000..5ec1c65 --- /dev/null +++ b/roles/clamav/templates/clamd.conf.j2 @@ -0,0 +1,12 @@ +LogSyslog yes +LogVerbose yes +ExtendedDetectionInfo yes +LocalSocket /var/run/clamav/clamd.sock +LocalSocketMode 666 +TCPSocket {{ clam_listen_port }} +TCPAddr {{ clam_listen_ip }} +StreamMinPort {{ clam_stream_port_min }} +StreamMaxPort {{ clam_stream_port_max }} +StreamMaxLength {{ clam_stream_max_size }}M +ExitOnOOM yes +Foreground yes diff --git a/roles/clamav/templates/clamd.service.j2 b/roles/clamav/templates/clamd.service.j2 new file mode 100644 index 0000000..4845593 --- /dev/null +++ b/roles/clamav/templates/clamd.service.j2 @@ -0,0 +1,13 @@ +[Unit] +Description=ClamAV antivirus daemon +After=syslog.target network.target + +[Service] +Type=simple +ExecStart=/usr/sbin/clamd -c /etc/clamd.conf +User={{ clam_user }} +Group={{ clam_group }} +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/roles/clamav/templates/freshclam.conf.j2 b/roles/clamav/templates/freshclam.conf.j2 new file mode 100644 index 0000000..f072f62 --- /dev/null +++ b/roles/clamav/templates/freshclam.conf.j2 @@ -0,0 +1,12 @@ +DatabaseDirectory /var/lib/clamav +LogVerbose yes +LogSyslog yes +Checks {{ clam_safebrowsing | ternary('48','12') }} +DatabaseOwner clamupdate +DatabaseMirror {{ clam_mirror }} +{% for custom in clam_custom_db_url %} +DatabaseCustomURL={{ custom }} +{% endfor %} +NotifyClamd /etc/clamd.conf +Foreground yes +SafeBrowsing {{ clam_safebrowsing | ternary('yes','no') }} diff --git a/roles/clamav/templates/freshclam.service.j2 b/roles/clamav/templates/freshclam.service.j2 new file mode 100644 index 0000000..19b7b6a --- /dev/null +++ b/roles/clamav/templates/freshclam.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=ClamAV signature updater +After=network.target + +[Service] +Type=simple +User=clamupdate +Group=clamupdate +ExecStart=/usr/bin/freshclam --stdout --daemon +Restart=on-failure +PrivateTmp=true + +[Install] +WantedBy=multi-user.target + diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 0000000..069bac4 --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,112 @@ +--- + +# List of UNIX group which will have full root access, using sudo +system_admin_groups: ['admins','Domain\ Admins'] + +# Email address of the admin (will receive root email) +# system_admin_email: admin@domain.net + +# List of basic system utilisties to install +# (Common list for EL and Debian based distro) +system_utils: + - htop + - screen + - iftop + - tcpdump + - bzip2 + - pbzip2 + - lzop + - vim + - bash-completion + - rsync + - lsof + - net-tools + - sysstat + - pciutils + - strace + - wget + - man-db + - unzip + - openssl + - pv + - less + - nano + - tree + - mc + - tar + +# Kernel modules to load +system_kmods: [] + +# List of extra package to install +system_extra_pkgs: [] + +# MegaCLI tool version +megacli_version: 8.07.14-1 + +# List of FS to mount +fstab: [] +# fstab: +# - name: /mnt/data +# src: files.domain.org:/data +# opts: noatime +# fstype: nfs +# state: present +# boot: yes + +# Various SELinux booleans +sebool: [] +# sebool: +# - name: httpd_use_fusefs +# state: True +# persistent: True + +system_swappiness: 10 +system_sysctl: {} +# system_sysctl: +# vm.vfs_cache_pressure: 500 +# vm.dirty_ratio: 10 +# vm.dirty_background_ratio: 5 + +# Disable traditional rsyslog daemon +system_disable_syslog: False + +# Send journald logs to a remote server using systemd-journal-upload +# system_journal_remote_uri: http://logs.example.com:19532 + +# Max disk space used by the Journal. Default is 10% of the available space. But must be exressed as an absolute value in the conf +# We can specify the max amount of space used, and the min amount of space left free. The smallest limit will apply +system_journal_max_use: 3G +system_journal_keep_free: 2G + +# System Timezone +system_tz: 'Europe/Paris' + +# Tuned profile to apply. If undefined, virt-host and virt-guest are applied automatically when needed +# system_tuned_profile: enterprise-storage + +# Frquency of the fstrim cron job. Can be daily, weekly or monthly +system_fstrim_freq: weekly + +system_base_bash_aliases: + ls: 'ls $LS_OPTIONS' + ll: 'ls $LS_OPTIONS -l' + l: 'ls $LS_OPTIONS -lA' + rm: 'rm -i' + cp: 'cp -i' + mv: 'mv -i' + +system_extra_bash_aliases: {} +system_bash_aliases: "{{ system_base_bash_aliases | combine(system_extra_bash_aliases, recursive=True) }}" + +# shell scriplet to exec on boot +system_rc_local_base_cmd: [] +system_rc_local_extra_cmd: [] +system_rc_local_cmd: "{{ system_rc_local_base_cmd + system_rc_local_extra_cmd }}" + +# shell scriplet to exec on shutdown +system_rc_local_shutdown_base_cmd: [] +system_rc_local_shutdown_extra_cmd: [] +system_rc_local_shutdown_cmd: "{{ system_rc_local_shutdown_base_cmd + system_rc_local_shutdown_extra_cmd }}" + +... diff --git a/roles/common/files/MegaCli-8.07.14-1.noarch.rpm b/roles/common/files/MegaCli-8.07.14-1.noarch.rpm new file mode 100644 index 0000000..b79499e Binary files /dev/null and b/roles/common/files/MegaCli-8.07.14-1.noarch.rpm differ diff --git a/roles/common/files/bash_aliases.sh b/roles/common/files/bash_aliases.sh new file mode 100644 index 0000000..ef01ba2 --- /dev/null +++ b/roles/common/files/bash_aliases.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +export LS_OPTIONS='--color=auto' +eval "`dircolors`" +alias ls='ls $LS_OPTIONS' +alias ll='ls $LS_OPTIONS -l' +alias l='ls $LS_OPTIONS -lA' +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' diff --git a/roles/common/files/crond b/roles/common/files/crond new file mode 100644 index 0000000..159869b --- /dev/null +++ b/roles/common/files/crond @@ -0,0 +1 @@ +CRONDARGS="-s" diff --git a/roles/common/files/fstrim_all b/roles/common/files/fstrim_all new file mode 100644 index 0000000..ba0a43e --- /dev/null +++ b/roles/common/files/fstrim_all @@ -0,0 +1,10 @@ +#!/bin/bash + +/sbin/fstrim -v --all + +# Proxmox container support +if [ -x /usr/sbin/pct ]; then + for CONTAINER in $(/usr/sbin/pct list | awk '/^[0-9]/ {print $1}'); do + /sbin/fstrim -v /proc/$(lxc-info -n $CONTAINER -p | awk '{print $2}')/root + done +fi diff --git a/roles/common/files/megacli_8.07.14-1_all.deb b/roles/common/files/megacli_8.07.14-1_all.deb new file mode 100644 index 0000000..03327d3 Binary files /dev/null and b/roles/common/files/megacli_8.07.14-1_all.deb differ diff --git a/roles/common/files/vimrc.local_Debian b/roles/common/files/vimrc.local_Debian new file mode 100644 index 0000000..34eca07 --- /dev/null +++ b/roles/common/files/vimrc.local_Debian @@ -0,0 +1,4 @@ +let g:skip_defaults_vim=1 +set mouse-=a +set background=dark +syntax on diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 0000000..d416f7a --- /dev/null +++ b/roles/common/handlers/main.yml @@ -0,0 +1,33 @@ +--- +- name: rehash postfix + command: "postmap /etc/postfix/{{ item }}" + with_items: + - relay_auth + +- name: restart postfix + service: name=postfix state=restarted + +- name: newaliases + command: newaliases + +- name: restart journald + service: name=systemd-journald state=restarted + +- name: systemd-tmpfiles + command: systemd-tmpfiles --create + +- name: reload systemd + command: systemctl daemon-reload + +- name: restart crond + service: name=crond state=restarted + +- name: restart journal-upload + service: name=systemd-journal-upload state=restarted + when: remote_journal is defined + +- name: restart journald + service: name=systemd-journald state=restarted + +- name: load kmods + service: name=systemd-modules-load state=restarted diff --git a/roles/common/meta/main.yml b/roles/common/meta/main.yml new file mode 100644 index 0000000..34dcd11 --- /dev/null +++ b/roles/common/meta/main.yml @@ -0,0 +1,28 @@ +--- +allow_duplicates: no +dependencies: + - role: mkdir + - role: system_proxy + - role: repo_base + when: ansible_os_family == 'RedHat' + - role: network + - role: iptables + when: iptables_manage | default(True) + - role: zabbix_agent + - role: fusioninventory_agent + - role: sssd_ldap_auth + when: ldap_auth | default(False) + - role: sssd_ad_auth + when: ad_auth | default(False) + - role: ntp_client + when: ansible_virtualization_role == 'host' or (ansible_virtualization_type != 'lxc' and ansible_virtualization_type != 'systemd-nspawn') + - role: sudo + - role: ssh + - role: patrix + when: + - patrix_enabled | default(True) + - patrix_server is defined + - (patrix_user is defined and patrix_pass is defined) or patrix_token is defined + - role: postfix + when: system_postfix | default(True) + - role: timers diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml new file mode 100644 index 0000000..6ab12be --- /dev/null +++ b/roles/common/tasks/facts.yml @@ -0,0 +1,5 @@ +--- + +- name: Check if tailf command exists + stat: path=/bin/tailf + register: system_tailf diff --git a/roles/common/tasks/guest.yml b/roles/common/tasks/guest.yml new file mode 100644 index 0000000..6ee1b41 --- /dev/null +++ b/roles/common/tasks/guest.yml @@ -0,0 +1,16 @@ +--- + +- name: Check if qemu agent channel is available + stat: path=/dev/virtio-ports/org.qemu.guest_agent.0 + register: qemu_ga_dev + +- include: guest_{{ ansible_os_family }}.yml + when: + - qemu_ga_dev.stat.exists + - ansible_virtualization_type == 'kvm' + +- name: Start and enable qemu guest agent + service: name=qemu-guest-agent state=started enabled=yes + when: + - qemu_ga_dev.stat.exists + - ansible_virtualization_type == 'kvm' diff --git a/roles/common/tasks/guest_Debian.yml b/roles/common/tasks/guest_Debian.yml new file mode 100644 index 0000000..100b660 --- /dev/null +++ b/roles/common/tasks/guest_Debian.yml @@ -0,0 +1,4 @@ +--- + +- name: Install qemu guest agent + apt: name=qemu-guest-agent state=present diff --git a/roles/common/tasks/guest_RedHat.yml b/roles/common/tasks/guest_RedHat.yml new file mode 100644 index 0000000..a279e07 --- /dev/null +++ b/roles/common/tasks/guest_RedHat.yml @@ -0,0 +1,5 @@ +--- + +- name: Install qemu guest agent + yum: name=qemu-guest-agent state=present + diff --git a/roles/common/tasks/hardware.yml b/roles/common/tasks/hardware.yml new file mode 100644 index 0000000..c1474f8 --- /dev/null +++ b/roles/common/tasks/hardware.yml @@ -0,0 +1,18 @@ +--- + +- set_fact: + controllers: "{{ controllers | default([]) + [ ansible_devices[item].host ] }}" + with_items: "{{ ansible_devices.keys() | list }}" + +- set_fact: + lsi_controllers: "{{ controllers | select('match', '(?i).*(lsi|megaraid).*') | list | unique }}" + +- include_tasks: hardware_{{ ansible_os_family }}.yml + +- name: Remove MegaCli package + file: path=/tmp/{{ megacli }} state=absent + when: + - lsi_controllers | length > 0 + - megacli_installed_version.stdout != megacli_version + +... diff --git a/roles/common/tasks/hardware_Debian.yml b/roles/common/tasks/hardware_Debian.yml new file mode 100644 index 0000000..5ced547 --- /dev/null +++ b/roles/common/tasks/hardware_Debian.yml @@ -0,0 +1,30 @@ +--- + +- set_fact: megacli=megacli_{{ megacli_version }}_all.deb + +- name: Install libncurses + apt: + name: + - libncurses5 + +- name: Check if MegaCLi is installed (Debian) + shell: dpkg -s megacli | grep Version | awk '{ print $2 }' 2>/dev/null + args: + warn: False + register: megacli_installed_version + failed_when: False + changed_when: False + when: lsi_controllers | length > 0 + +- name: Copy MegaCli package + copy: src={{ megacli }} dest=/tmp + when: + - lsi_controllers | length > 0 + - megacli_installed_version.stdout != megacli_version + +- name: Install MegaCli (Debian) + apt: deb=/tmp/{{ megacli }} allow_unauthenticated=yes + when: + - lsi_controllers | length > 0 + - megacli_installed_version.stdout != megacli_version + diff --git a/roles/common/tasks/hardware_RedHat.yml b/roles/common/tasks/hardware_RedHat.yml new file mode 100644 index 0000000..350123c --- /dev/null +++ b/roles/common/tasks/hardware_RedHat.yml @@ -0,0 +1,24 @@ +--- + +- set_fact: + megacli: MegaCli-{{ megacli_version }}.noarch.rpm + +- name: Check if MegaCLi is installed + shell: rpm -q --qf "%{VERSION}-%{RELEASE}" MegaCli 2>/dev/null + register: megacli_installed_version + changed_when: False + failed_when: False + when: lsi_controllers | length > 0 + +- name: Copy MegaCli package + copy: src={{ megacli }} dest=/tmp + when: + - lsi_controllers | length > 0 + - megacli_installed_version.stdout != megacli_version + +- name: Install MegaCli + yum: name=/tmp/{{ megacli }} state=present + when: + - lsi_controllers | length > 0 + - megacli_installed_version.stdout != megacli_version + diff --git a/roles/common/tasks/hostname.yml b/roles/common/tasks/hostname.yml new file mode 100644 index 0000000..629afb7 --- /dev/null +++ b/roles/common/tasks/hostname.yml @@ -0,0 +1,11 @@ +--- + +- name: Set system hostname + hostname: name={{ system_hostname | default(inventory_hostname | regex_replace('^([^\.]+)\..*','\\1')) }} + +- name: Prevent PVE from changing /etc/hostname + copy: content='' dest=/etc/.pve-ignore.hostname + when: ansible_virtualization_type == 'lxc' + +... + diff --git a/roles/common/tasks/mail.yml b/roles/common/tasks/mail.yml new file mode 100644 index 0000000..084107d --- /dev/null +++ b/roles/common/tasks/mail.yml @@ -0,0 +1,15 @@ +--- + +- when: system_admin_email is defined + block: + - name: Install postfix + package: name=postfix + + - name: Configure root email forward + lineinfile: + dest: /etc/aliases + regexp: "^root:.*" + line: "root: {{ system_admin_email }}" + notify: newaliases + +... diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..7257859 --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,26 @@ +--- + +- include_vars: "{{ item }}" + with_first_found: + - vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_distribution }}.yml + - vars/{{ ansible_os_family }}.yml + +- include: facts.yml +- include_tasks: utils.yml +- include_tasks: hostname.yml +- include_tasks: tz.yml +- include_tasks: tuned.yml + when: + - ansible_virtualization_role == 'host' or ansible_virtualization_type != 'lxc' + - ansible_os_family == 'RedHat' +- include_tasks: mail.yml +- include_tasks: system.yml +- include_tasks: hardware.yml + when: ansible_virtualization_role == 'host' +- include_tasks: guest.yml + when: + - ansible_virtualization_role == 'guest' + +... diff --git a/roles/common/tasks/system.yml b/roles/common/tasks/system.yml new file mode 100644 index 0000000..6114845 --- /dev/null +++ b/roles/common/tasks/system.yml @@ -0,0 +1,153 @@ +--- + +- name: Deploy journald.conf + template: src=journald.conf.j2 dest=/etc/systemd/journald.conf + when: ansible_service_mgr == 'systemd' + notify: restart journald + +- name: Allow userspace to trigger kernel autoload of modules + seboolean: name=domain_kernel_load_modules state=yes persistent=yes + when: ansible_selinux.status == 'enabled' + tags: selinux + +- name: Configure kmod to load + copy: content={{ system_kmods | join("\n") }} dest=/etc/modules-load.d/system.conf + register: system_kmods_file + +- name: Load needed kmods + service: name=systemd-modules-load state=restarted + when: system_kmods_file.changed + +- name: Set SELinux booleans + seboolean: name={{ item.name }} state={{ item.state }} persistent={{ item.persistent | default(True) }} + when: ansible_selinux.status == 'enabled' + with_items: "{{ sebool }}" + +- name: Set logrotate_t to permissive mode + selinux_permissive: name=logrotate_t permissive=True + when: ansible_selinux.status == 'enabled' + +- name: Create mount points directories + file: path={{ item.name }} state=directory + with_items: "{{ fstab }}" + ignore_errors: True # needed for some fuse mount points + +- name: Configure mount points + mount: + name: "{{ item.name }}" + src: "{{ item.src }}" + fstype: "{{ item.fstype | default(omit) }}" + opts: "{{ item.opts | default(omit) }}" + boot: "{{ item.boot | default(omit) }}" + state: "{{ item.state | default('mounted') }}" + with_items: "{{ fstab }}" + +- name: Set swappiness + sysctl: + name: vm.swappiness + value: "{{ system_swappiness }}" + sysctl_file: /etc/sysctl.d/ansible.conf + state: present + when: ansible_virtualization_role == 'host' or (ansible_virtualization_type != 'lxc' and ansible_virtualization_type != 'systemd-nspawn') + +- name: Set sysctl values + sysctl: + name: "{{ item }}" + value: "{{ system_sysctl[item] }}" + sysctl_file: /etc/sysctl.d/ansible.conf + state: present + when: ansible_virtualization_role == 'host' or ansible_virtualization_type != 'lxc' + loop: "{{ system_sysctl.keys() | list }}" + +- name: Create symlink for restricted bash + file: + src: /bin/bash + dest: /bin/rbash + state: link + +- name: Set bash as default shell + file: + src: /bin/bash + dest: /bin/sh + state: link + +- name: Configure logrotate compression + blockinfile: + dest: /etc/logrotate.conf + insertbefore: BOF + block: | + compress + compressoptions -T0 + compresscmd /usr/bin/xz + compressext .xz + uncompresscmd /usr/bin/unxz + +- name: Configure crond to send cron's log to syslog + copy: src=crond dest=/etc/sysconfig/crond mode=600 + notify: restart crond + when: ansible_os_family == 'RedHat' + +- name: Deploy fstrim script + copy: src=fstrim_all dest=/usr/local/bin/fstrim_all mode=755 + +- name: Add a cron task to run fstrim + cron: + name: fstrim + special_time: "{{ system_fstrim_freq }}" + user: root + job: 'sleep $(( 3600 + 1$(/bin/date +\%N) \% 7200 )); /usr/bin/systemd-cat /usr/local/bin/fstrim_all' + cron_file: fstrim + state: "{{ (ansible_virtualization_role == 'guest' and ansible_virtualization_type == 'lxc') | ternary('absent','present') }}" + +- name: Deploy global vimrc + copy: src=vimrc.local_{{ ansible_os_family }} dest=/etc/vim/vimrc.local + when: ansible_os_family == 'Debian' + +- name: Configure vim for dark background + lineinfile: path=/etc/vimrc regexp='^set\sbackground=' line='set background=dark' + when: ansible_os_family == 'RedHat' + +- name: Configure screen to use login shell + lineinfile: path=/etc/screenrc regexp='^shell\s.*' line='shell -/bin/sh' + when: ansible_os_family == 'Debian' + +- name: Handle syslog daemon + service: + name: rsyslog + state: "{{ (system_disable_syslog | default(False)) | ternary('stopped','started') }}" + enabled: "{{ (system_disable_syslog | default(False)) | ternary(False,True) }}" + +- name: Remove old bash aliases script + file: path=/etc/profile.d/bash_aliases.sh state=absent + +- name: Deploy bash aliases + template: src=bash_aliases.sh.j2 dest=/etc/profile.d/ansible_aliases.sh mode=755 + +- name: Ensure /etc/rc.d exists + file: path=/etc/rc.d state=directory + +- name: Deploy rc.local script + template: src=rc.local.j2 dest=/etc/rc.d/rc.local mode=755 + +- name: Deploy rc.local.shutdown script + template: src=rc.local.shutdown.j2 dest=/etc/rc.d/rc.local.shutdown mode=755 + + # Debian is using /etc/rc.local while RHEL is using /etc/rc.d/rc.local +- name: Link /etc/rc.local to /etc/rc.d/rc.local + file: src=/etc/rc.d/rc.local path=/etc/rc.local state=link force=True + +- name: Link /etc/rc.local.shutdown to /etc/rc.d/rc.local.shutdown + file: src=/etc/rc.d/rc.local.shutdown path=/etc/rc.local.shutdown state=link force=True + +- name: Deploy rc-local-shutdown systemd unit + template: src=rc-local-shutdown.service.j2 dest=/etc/systemd/system/rc-local-shutdown.service + register: system_rc_local_shutdown_unit + +- name: Reload systemd + systemd: daemon_reload=True + when: system_rc_local_shutdown_unit.changed + +- name: Enable rc-local-shutdown service + service: name=rc-local-shutdown enabled=True + +... diff --git a/roles/common/tasks/tuned.yml b/roles/common/tasks/tuned.yml new file mode 100644 index 0000000..83b1590 --- /dev/null +++ b/roles/common/tasks/tuned.yml @@ -0,0 +1,35 @@ +--- + +- name: Install tuned service + yum: name=tuned state=present + +- name: Enabling tuned + service: name=tuned state=started enabled=yes + +- name: Check actual tuned profile + shell: "tuned-adm active | awk -F': ' '{print $2}'" + register: tuned_profile + changed_when: False + ignore_errors: True + +- name: Applying custom tuned profile + command: tuned-adm profile {{ system_tuned_profile }} + when: + - system_tuned_profile is defined + - tuned_profile.stdout != system_tuned_profile + +- name: Applying virtual guest tuned profile + command: tuned-adm profile virtual-guest + when: + - ansible_virtualization_role == "guest" + - tuned_profile.stdout != "virtual-guest" + - system_tuned_profile is not defined + +- name: Applying virtual host tuned profile + command: tuned-adm profile virtual-host + when: + - ansible_virtualization_role == "host" + - tuned_profile.stdout != "virtual-host" + - system_tuned_profile is not defined + +... diff --git a/roles/common/tasks/tz.yml b/roles/common/tasks/tz.yml new file mode 100644 index 0000000..28df4f9 --- /dev/null +++ b/roles/common/tasks/tz.yml @@ -0,0 +1,5 @@ +--- + +- name: Set system TZ + timezone: name={{ system_tz }} + when: system_tz is defined diff --git a/roles/common/tasks/utils.yml b/roles/common/tasks/utils.yml new file mode 100644 index 0000000..34a7b30 --- /dev/null +++ b/roles/common/tasks/utils.yml @@ -0,0 +1,19 @@ +--- + +- name: Install common utilities + package: + name: "{{ system_utils + system_distro_utils }}" + +- name: Install extra softwares + package: + name: "{{ system_extra_pkgs }}" + + # Screendump is not used, and prevent using tab to use screen quickly, so remove it +- name: Check if screendump is present + stat: path=/usr/bin/screendump + register: system_screendump + +- name: Rename screendump + command: mv -f /usr/bin/screendump /usr/bin/_screendump + when: system_screendump.stat.exists +... diff --git a/roles/common/templates/bash_aliases.sh.j2 b/roles/common/templates/bash_aliases.sh.j2 new file mode 100644 index 0000000..9a7a9b6 --- /dev/null +++ b/roles/common/templates/bash_aliases.sh.j2 @@ -0,0 +1,13 @@ +#!/bin/bash -e + +# {{ ansible_managed }} + +export LS_OPTIONS='--color=auto' +eval "`dircolors`" + +{% for alias in system_bash_aliases.keys() | list %} +alias {{ alias }}='{{ system_bash_aliases[alias] }}' +{% endfor %} +{% if not system_tailf.stat.exists %} +alias tailf='tail -f' +{% endif %} diff --git a/roles/common/templates/journal-upload.conf.j2 b/roles/common/templates/journal-upload.conf.j2 new file mode 100644 index 0000000..4b545bd --- /dev/null +++ b/roles/common/templates/journal-upload.conf.j2 @@ -0,0 +1,7 @@ +[Upload] +{% if system_journal_remote_uri is defined and system_journal_remote_uri | regex_search('^https?://') %} +URL={{ system_journal_remote_uri }} +{% if ansible_os_family == 'RedHat' %} +TrustedCertificateFile=/etc/pki/tls/cert.pem +{% endif %} +{% endif %} diff --git a/roles/common/templates/journald.conf.j2 b/roles/common/templates/journald.conf.j2 new file mode 100644 index 0000000..59fa20d --- /dev/null +++ b/roles/common/templates/journald.conf.j2 @@ -0,0 +1,4 @@ +[Journal] +SystemMaxFileSize=100M +SystemMaxUse={{ system_journal_max_use }} +SystemKeepFree={{ system_journal_keep_free }} diff --git a/roles/common/templates/rc-local-shutdown.service.j2 b/roles/common/templates/rc-local-shutdown.service.j2 new file mode 100644 index 0000000..724906b --- /dev/null +++ b/roles/common/templates/rc-local-shutdown.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=/etc/rc.d/rc.local.shutdown compatibility +ConditionFileIsExecutable=/etc/rc.d/rc.local.shutdown +DefaultDependencies=no +After=rc-local.service basic.target +Before=shutdown.target + +[Service] +Type=oneshot +ExecStart=/etc/rc.d/rc.local.shutdown +StandardInput=tty +RemainAfterExit=yes + +[Install] +WantedBy=shutdown.target diff --git a/roles/common/templates/rc.local.j2 b/roles/common/templates/rc.local.j2 new file mode 100644 index 0000000..b7c1d86 --- /dev/null +++ b/roles/common/templates/rc.local.j2 @@ -0,0 +1,9 @@ +#!/bin/bash + +# {{ ansible_managed }} + +{% for cmd in system_rc_local_cmd %} +{{ cmd }} +{% endfor %} + +exit 0 diff --git a/roles/common/templates/rc.local.shutdown.j2 b/roles/common/templates/rc.local.shutdown.j2 new file mode 100644 index 0000000..69003fe --- /dev/null +++ b/roles/common/templates/rc.local.shutdown.j2 @@ -0,0 +1,9 @@ +#!/bin/bash + +# {{ ansible_managed }} + +{% for cmd in system_rc_local_shutdown_cmd %} +{{ cmd }} +{% endfor %} + +exit 0 diff --git a/roles/common/templates/systemd-journal-upload.service.j2 b/roles/common/templates/systemd-journal-upload.service.j2 new file mode 100644 index 0000000..dfc712a --- /dev/null +++ b/roles/common/templates/systemd-journal-upload.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Journal Remote Upload Service +After=network.target + +[Service] +ExecStart=/lib/systemd/systemd-journal-upload \ + --save-state +User=systemd-journal-upload +PrivateTmp=yes +PrivateDevices=yes +WatchdogSec=20min +Restart=always +RestartSec=10min +TimeoutStopSec=10 + +# If there are many split up journal files we need a lot of fds to +# access them all and combine +LimitNOFILE=16384 + +[Install] +WantedBy=multi-user.target + diff --git a/roles/common/vars/Debian-10.yml b/roles/common/vars/Debian-10.yml new file mode 100644 index 0000000..11709c8 --- /dev/null +++ b/roles/common/vars/Debian-10.yml @@ -0,0 +1,10 @@ +--- + +system_distro_utils: + - apt-transport-https + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs + - zstd diff --git a/roles/common/vars/Debian-11.yml b/roles/common/vars/Debian-11.yml new file mode 100644 index 0000000..11709c8 --- /dev/null +++ b/roles/common/vars/Debian-11.yml @@ -0,0 +1,10 @@ +--- + +system_distro_utils: + - apt-transport-https + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs + - zstd diff --git a/roles/common/vars/Debian-8.yml b/roles/common/vars/Debian-8.yml new file mode 100644 index 0000000..c5a47f2 --- /dev/null +++ b/roles/common/vars/Debian-8.yml @@ -0,0 +1,9 @@ +--- + +system_distro_utils: + - apt-transport-https + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs diff --git a/roles/common/vars/Debian-9.yml b/roles/common/vars/Debian-9.yml new file mode 100644 index 0000000..11709c8 --- /dev/null +++ b/roles/common/vars/Debian-9.yml @@ -0,0 +1,10 @@ +--- + +system_distro_utils: + - apt-transport-https + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs + - zstd diff --git a/roles/common/vars/RedHat-7.yml b/roles/common/vars/RedHat-7.yml new file mode 100644 index 0000000..99b3376 --- /dev/null +++ b/roles/common/vars/RedHat-7.yml @@ -0,0 +1,13 @@ +--- + +system_distro_utils: + - openssh-clients + - nc + - xz + - lz4 + - yum-utils + - fuse-sshfs + - policycoreutils-python + - MySQL-python + - python-psycopg2 + - zstd diff --git a/roles/common/vars/RedHat-8.yml b/roles/common/vars/RedHat-8.yml new file mode 100644 index 0000000..b3bb35e --- /dev/null +++ b/roles/common/vars/RedHat-8.yml @@ -0,0 +1,13 @@ +--- + +system_distro_utils: + - openssh-clients + - nc + - xz + - lz4 + - yum-utils + - fuse-sshfs + - policycoreutils-python-utils + - python3-mysql + - python3-psycopg2 + - zstd diff --git a/roles/common/vars/Ubuntu-20.yml b/roles/common/vars/Ubuntu-20.yml new file mode 100644 index 0000000..11709c8 --- /dev/null +++ b/roles/common/vars/Ubuntu-20.yml @@ -0,0 +1,10 @@ +--- + +system_distro_utils: + - apt-transport-https + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs + - zstd diff --git a/roles/composer/meta/main.yml b/roles/composer/meta/main.yml new file mode 100644 index 0000000..52d815d --- /dev/null +++ b/roles/composer/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: httpd_php diff --git a/roles/composer/tasks/cleanup.yml b/roles/composer/tasks/cleanup.yml new file mode 100644 index 0000000..6fdbdc1 --- /dev/null +++ b/roles/composer/tasks/cleanup.yml @@ -0,0 +1,11 @@ +--- + +- name: Check if composer exists in /usr/local/bin + stat: path=/usr/local/bin/composer + register: composer_local + tags: web + +- name: Remove manually installed composer + file: path=/usr/local/bin/composer state=absent + when: composer_local.stat.exists and not composer_local.stat.islnk + tags: web diff --git a/roles/composer/tasks/install.yml b/roles/composer/tasks/install.yml new file mode 100644 index 0000000..4a80a1d --- /dev/null +++ b/roles/composer/tasks/install.yml @@ -0,0 +1,7 @@ +--- + +- name: Install composer + package: + name: + - composer + tags: web diff --git a/roles/composer/tasks/main.yml b/roles/composer/tasks/main.yml new file mode 100644 index 0000000..6392c89 --- /dev/null +++ b/roles/composer/tasks/main.yml @@ -0,0 +1,4 @@ +--- + +- include: install.yml +- include: cleanup.yml diff --git a/roles/coturn/defaults/main.yml b/roles/coturn/defaults/main.yml new file mode 100644 index 0000000..1d1f534 --- /dev/null +++ b/roles/coturn/defaults/main.yml @@ -0,0 +1,38 @@ +--- + +# Set turn realm. Default to the domain name if unset +# turn_realm: turn.example.com + +# The static, shared auth secret. If not set, will use long term auth. +# See turn_lt_users +# turn_auth_secret: + +# Long term users +turn_lt_users: [] +# - name: asterisk +# pass: S3cr3t. + + +turn_listen_ip: + - 0.0.0.0 + +# If defined, restrict who can access the service +turn_src_ip: + - 0.0.0.0/0 + +turn_port: 3478 +turn_tls_port: 5349 + +# Allow non TLS relay +turn_allow_non_tls: True + +# Turn on TLS listener. If true, certificate must be present +turn_tls: False +# turn_tls_cert: +# turn_tls_key: +# Or alternatively, set the name of a Let's Encrypt cert +# turn_letsencrypt_cert: turn.example.org + +# If behind a NAT, you must set the public IP +# turn_external_ip: 12.13.14.15 + diff --git a/roles/coturn/handlers/main.yml b/roles/coturn/handlers/main.yml new file mode 100644 index 0000000..8288105 --- /dev/null +++ b/roles/coturn/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart coturn + service: name=coturn state=restarted enabled=yes diff --git a/roles/coturn/meta/main.yml b/roles/coturn/meta/main.yml new file mode 100644 index 0000000..dc58dfa --- /dev/null +++ b/roles/coturn/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: mkdir diff --git a/roles/coturn/tasks/main.yml b/roles/coturn/tasks/main.yml new file mode 100644 index 0000000..3422aa5 --- /dev/null +++ b/roles/coturn/tasks/main.yml @@ -0,0 +1,122 @@ +--- + +- name: Check if turnserver is installed + stat: path=/lib/systemd/system/turnserver.service + register: turn_turnserver + tags: turn + + # Migrate from the turnserver package/role +- when: turn_turnserver.stat.exists + block: + - name: Stop and disable turnserver + service: name=turnserver state=stopped enabled=False + + - name: Remove turnserver package + yum: name=turnserver state=absent + + - name: Remove turnserver dehydrated hook + file: path=/etc/dehydrated/hooks_deploy_cert.d/20turnserver.sh state=absent + tags: turn + +- name: Install Coturn + yum: name=coturn state=present + register: turn_installed + tags: turn + +- name: Create tmpfiles + command: systemd-tmpfiles --create + when: turn_installed.changed + tags: turn + +- name: Deploy main configuration + template: src=turnserver.conf.j2 dest=/etc/coturn/turnserver.conf group=coturn mode=640 + notify: restart coturn + tags: turn + +- name: Create the ssl dir + file: path=/etc/coturn/ssl state=directory group=coturn mode=750 + tags: turn + + # Create a self signed cert. This is needed even if a cert is later obtained with dehydrated as + # turnserver must be started before that +- import_tasks: ../includes/create_selfsigned_cert.yml + vars: + - cert_path: /etc/coturn/ssl/cert.pem + - cert_key_path: /etc/coturn/ssl/key.pem + - cert_user: coturn + tags: turn + +- name: Deploy dehydrated hook + template: src=dehydrated_deploy_hook.j2 dest=/etc/dehydrated/hooks_deploy_cert.d/20coturn.sh mode=755 + tags: turn + +- name: Remove turnserver rules + iptables_raw: + name: turnserver_ports + state: absent + when: iptables_manage | default(True) + tags: turn,firewall + +- name: Handle coturn ports + iptables_raw: + name: coturn_ports + state: "{{ (turn_src_ip | length > 0) | ternary('present','absent') }}" + rules: | + -A INPUT -m state --state NEW -p tcp -m multiport --dports {{ [turn_port,turn_tls_port] | join(',') }} -s {{ turn_src_ip | join(',') }} -j ACCEPT + -A INPUT -p udp -m multiport --dports {{ [turn_port,turn_tls_port] | join(',') }} -s {{ turn_src_ip | join(',') }} -j ACCEPT + -A INPUT -p tcp --dport 49152:65535 -s {{ turn_src_ip | join(',') }} -j ACCEPT + -A INPUT -p udp --dport 49152:65535 -s {{ turn_src_ip | join(',') }} -j ACCEPT + when: iptables_manage | default(True) + tags: turn,firewall + +- name: Create systemd unit snippet dir + file: path=/etc/systemd/system/coturn.service.d state=directory + tags: turn + +- name: Customize systemd unit + copy: + content: | + [Service] + # Allow binding on privileged ports + CapabilityBoundingSet=CAP_NET_BIND_SERVICE + AmbientCapabilities=CAP_NET_BIND_SERVICE + dest: /etc/systemd/system/coturn.service.d/99-ansible.conf + register: turn_unit + tags: turn + +- name: Reload systemd + systemd: daemon_reload=True + when: turn_unit.changed + tags: turn + +- name: Start and enable the service + service: name=coturn state=started enabled=True + tags: turn + +- name: Add long term users + command: turnadmin --add --user={{ item.name }} --password={{ item.pass | quote }} --realm={{ turn_realm | default(ansible_domain) }} + loop: "{{ turn_lt_users }}" + tags: turn + +- name: Remove users with unknown realm + shell: | + for U in $(turnadmin --list | grep -vP '^0:\s+:\s+(log file opened|SQLite connection)'); do + user=$(echo $U | cut -d'[' -f1) + realm=$(echo $U | perl -pe 's/.*\[(.*)\]/$1/') + [ "$realm" == "{{ turn_realm | default(ansible_domain) }}" ] || turnadmin --delete --user=$user --realm=$realm + done + changed_when: False + tags: turn + +- name: List long term users + shell: turnadmin --list | grep -vP '^0:\s+:\s+(log file opened|SQLite connection)' | cut -d'[' -f1 + register: turn_lt_existing_users + changed_when: False + tags: turn + +- name: Remove unmanaged long term users + command: turnadmin --delete --user={{ item }} --realm={{ turn_realm | default(ansible_domain) }} + when: item not in turn_lt_users | map(attribute='name') | list + loop: "{{ turn_lt_existing_users.stdout_lines }}" + tags: turn + diff --git a/roles/coturn/templates/dehydrated_deploy_hook.j2 b/roles/coturn/templates/dehydrated_deploy_hook.j2 new file mode 100644 index 0000000..f956062 --- /dev/null +++ b/roles/coturn/templates/dehydrated_deploy_hook.j2 @@ -0,0 +1,13 @@ +#!/bin/sh + +{% if turn_letsencrypt_cert is defined %} +if [ $1 == "{{ turn_letsencrypt_cert }}" ]; then + cat /var/lib/dehydrated/certificates/certs/{{ turn_letsencrypt_cert }}/privkey.pem > /etc/coturn/ssl/key.pem + cat /var/lib/dehydrated/certificates/certs/{{ turn_letsencrypt_cert }}/fullchain.pem > /etc/coturn/ssl/cert.pem + chown root:coturn /etc/coturn/ssl/* + chmod 644 /etc/coturn/ssl/cert.pem + chmod 640 /etc/coturn/ssl/key.pem + + /bin/systemctl restart coturn +fi +{% endif %} diff --git a/roles/coturn/templates/turnserver.conf.j2 b/roles/coturn/templates/turnserver.conf.j2 new file mode 100644 index 0000000..7b1eda6 --- /dev/null +++ b/roles/coturn/templates/turnserver.conf.j2 @@ -0,0 +1,43 @@ +pidfile="/var/run/coturn/coturn.pid" +verbose +fingerprint +{% if turn_auth_secret is defined %} +use-auth-secret +static-auth-secret {{ turn_auth_secret }} +{% else %} +lt-cred-mech +{% endif %} +no-sslv2 +no-sslv3 +no-loopback-peers +no-multicast-peers +realm {{ turn_realm | default(ansible_domain) }} +proc-user coturn +proc-group coturn +syslog + +{% for ip in turn_listen_ip %} +listening-ip {{ ip }} +{% endfor %} + +{% if not turn_allow_non_tls %} +no-tcp +no-udp +{% endif %} + +listening-port {{ turn_port }} + +{% if turn_tls %} +tls-listening-port {{ turn_tls_port }} +{% if turn_letsencrypt_cert is defined %} +cert /etc/coturn/ssl/cert.pem +pkey /etc/coturn/ssl/key.pem +{% else %} +cert {{ turn_tls_cert }} +pkey {{ turn_tls_key }} +{% endif %} +{% endif %} + +{% if turn_external_ip is defined %} +external-ip {{ turn_external_ip }} +{% endif %} diff --git a/roles/crowdsec/defaults/main.yml b/roles/crowdsec/defaults/main.yml new file mode 100644 index 0000000..ac3c59e --- /dev/null +++ b/roles/crowdsec/defaults/main.yml @@ -0,0 +1,97 @@ +--- + +# Version to install +cs_version: 1.1.1 +# URL of the archive +cs_archive_url: https://github.com/crowdsecurity/crowdsec/releases/download/v{{ cs_version }}/crowdsec-release.tgz +# Expected sha1 of the archive +cs_archive_sha1: e128534e1fc5529441512451753ecb79c2cdcb85 + +# Crowdsec usually should run as root to be able to access all your logs +# but in some situations, when all your logs are readable by a less privileged user, you can run +# crowdsec as another user account, for better security +cs_user: root + +# Directory where data will be stored +cs_root_dir: /opt/crowdsec + +# Can be sqlite or mysql +cs_db_engine: sqlite +# This is for mysql backend +cs_db_server: "{{ mysql_server | default('localhost') }}" +cs_db_port: 3306 +cs_db_name: crowdsec +cs_db_user: crowdsec +# If not defined, a random one will be generated and store in /etc/crowdsec/meta/ansible_dbpass +# cs_db_pass: S3cr3t. + +# You can disable the Local API, if using a remote one for example +cs_lapi_enabled: True +# Set to true if Local API is enabled, and you intend to use it through a trusted reverse proxy +cs_use_forwarded_headers: False +# Port on which the Local API will listen +cs_lapi_port: 8080 +# List of IP/CIDR allowed to access cs_lapi_port +cs_lapi_src_ip: [] + +# Address of the Local API server +# The default config will make it standalone +cs_lapi_url: http://localhost:{{ cs_lapi_port }}/ +cs_lapi_user: "{{ inventory_hostname }}" +# On installation, ansible will register this host on the Local API +# And will then validate the registration on the following server. +# So set it to your own Local API server so ansible will delegate the task +cs_lapi_server: "{{ inventory_hostname }}" + +# Use the central API, to share your banned IP, and received list of IP to ban +# Requires cs_lapi_enabled to be true too +cs_capi_enabled: False +# You can either register manuelly and the the user/pass with those variable +# Else, ansible will register and configure the credentials +# cs_capi_user: 123456789 +# cs_capi_pass: azertyuiop + +# Port on which the prometheus metric endpoint will bind to +cs_prometheus_port: 6060 +# List of IP/CIDR allowed to access the prometheus port +cs_prometheus_src_ip: [] + +# Default duration of a ban +cs_trusted_countries: + - FR +# Duration of bans for attacks from trusted countries +cs_ban_trusted_duration: 15m +# Default duration of a ban +cs_ban_duration: 2h + +# List of parsers to install from the hub +cs_parsers: + - crowdsecurity/syslog-logs + - crowdsecurity/geoip-enrich + - crowdsecurity/dateparse-enrich + - crowdsecurity/whitelists + - crowdsecurity/sshd-logs + - crowdsecurity/iptables-logs +# List of scenarios to install from the hub +cs_scenarios: + - crowdsecurity/ban-defcon-drop_range + - crowdsecurity/ssh-bf +# List of postoverflows to install from the hub +cs_postoverflows: + - crowdsecurity/cdn-whitelist + - crowdsecurity/rdns + - crowdsecurity/seo-bots-whitelist + +# If not set, crowdsec will look for yaml files in /etc/crowdsec/acquis/ +# The default will only read syslog using journalctl +# If defined, only acquisition set by ansible will be used +# cs_aquis: +# - journalctl_filter: +# - '_SYSTEMD_UNIT=sshd.service' +# labels: +# type: syslog +# +# - filename: +# - /var/log/nginx/access.log +# labels: +# type: nginx diff --git a/roles/crowdsec/handlers/main.yml b/roles/crowdsec/handlers/main.yml new file mode 100644 index 0000000..e2e79b8 --- /dev/null +++ b/roles/crowdsec/handlers/main.yml @@ -0,0 +1,7 @@ +--- + +- name: restart crowdsec + service: name=crowdsec state=restarted + +- name: reload crowdsec + service: name=crowdsec state=reloaded diff --git a/roles/crowdsec/meta/main.yml b/roles/crowdsec/meta/main.yml new file mode 100644 index 0000000..e397f4e --- /dev/null +++ b/roles/crowdsec/meta/main.yml @@ -0,0 +1,6 @@ +--- + +dependencies: + - role: mkdir + - role: mysql_server + when: cs_db_server in ['localhost','127.0.0.1'] diff --git a/roles/crowdsec/tasks/cleanup.yml b/roles/crowdsec/tasks/cleanup.yml new file mode 100644 index 0000000..300acdf --- /dev/null +++ b/roles/crowdsec/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove temp and obsolete files + file: path={{ item }} state=absent + loop: + - /tmp/crowdsec-release.tgz + - /tmp/crowdsec-v{{ cs_version }} + tags: cs diff --git a/roles/crowdsec/tasks/conf.yml b/roles/crowdsec/tasks/conf.yml new file mode 100644 index 0000000..deaf5dd --- /dev/null +++ b/roles/crowdsec/tasks/conf.yml @@ -0,0 +1,126 @@ +--- + +- name: Deploy configuration + template: src={{ item }}.j2 dest=/etc/crowdsec/{{ item }} + loop: + - config.yaml + - acquis.yaml + - simulation.yaml + - profiles.yaml + - parsers/s02-enrich/trusted_ip.yaml + - dev.yaml + notify: reload crowdsec + tags: cs + +# Create the database +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ cs_db_name }}" + - db_user: "{{ cs_db_user }}" + - db_server: "{{ cs_db_server }}" + - db_pass: "{{ cs_db_pass }}" + when: + - cs_db_engine == 'mysql' + - cs_lapi_enabled + tags: cs + +- when: cs_lapi_pass is not defined + block: + - name: Declare on the local API + command: cscli machines add {{ cs_lapi_user }} --auto --force --file /dev/stdout --output raw + register: cs_lapi_credentials + delegate_to: "{{ cs_lapi_server }}" + - set_fact: cs_lapi_credentials_yaml={{ cs_lapi_credentials.stdout | from_yaml }} + - copy: content={{ cs_lapi_credentials_yaml.password }} dest=/etc/crowdsec/meta/lapi_pass mode=600 + - set_fact: cs_lapi_pass={{ cs_lapi_credentials_yaml.password }} + tags: cs + +- when: + - cs_lapi_enabled + - cs_capi_enabled + - cs_capi_user is not defined or cs_capi_pass is not defined + block: + - name: Register on the central API + command: cscli capi register -o raw -f /dev/stdout + register: cs_capi_credentials + - set_fact: cs_capi_credentials_yaml={{ cs_capi_credentials.stdout | from_yaml }} + - copy: content={{ cs_capi_credentials_yaml.login }} dest=/etc/crowdsec/meta/capi_user mode=600 + - copy: content={{ cs_capi_credentials_yaml.password }} dest=/etc/crowdsec/meta/capi_pass mode=600 + - set_fact: cs_capi_user={{ cs_capi_credentials_yaml.login }} + - set_fact: cs_capi_pass={{ cs_capi_credentials_yaml.password }} + tags: cs + +- name: Deploy credentials config + template: src={{ item }}_api_credentials.yaml.j2 dest=/etc/crowdsec/{{ item }}_api_credentials.yaml mode=600 + loop: + - online + - local + notify: restart crowdsec + tags: cs + +- name: List installed parsers + shell: cscli parsers list -o json + register: cs_installed_parsers + changed_when: False + tags: cs + +- name: Install parsers + command: cscli parsers install {{ item }} + when: item not in cs_installed_parsers.stdout | from_json | map(attribute='name') | list + loop: "{{ cs_parsers }}" + notify: reload crowdsec + tags: cs + +- name: Upgrade parsers + command: cscli parsers upgrade {{ item }} + loop: "{{ cs_parsers }}" + when: cs_install_mode == 'upgrade' + notify: reload crowdsec + tags: cs + +- name: List installed scenarios + command: cscli scenarios list -o json + register: cs_installed_scenarios + changed_when: False + tags: cs + +- name: Install scenarios + command: cscli scenarios install {{ item }} + when: item not in cs_installed_scenarios.stdout | from_json | map(attribute='name') | list + loop: "{{ cs_scenarios }}" + notify: reload crowdsec + tags: cs + +- name: Upgrade scenarios + command: cscli scenarios upgrade {{ item }} + loop: "{{ cs_scenarios }}" + when: cs_install_mode == 'upgrade' + notify: reload crowdsec + tags: cs + +- name: List installed postoverflows + command: cscli postoverflows list -o json + register: cs_installed_postoverflows + changed_when: False + tags: cs + +- name: Install postoverflows + command: cscli postoverflows install {{ item }} + when: item not in cs_installed_postoverflows.stdout | from_json | map(attribute='name') | list + loop: "{{ cs_postoverflows }}" + notify: reload crowdsec + tags: cs + +- name: Upgrade postoverflows + command: cscli postoverflows upgrade {{ item }} + loop: "{{ cs_postoverflows }}" + when: cs_install_mode == 'upgrade' + notify: reload crowdsec + tags: cs + +- name: Set permissions on conf and data directories + file: path={{ item }} owner={{ cs_user }} group={{ cs_user }} recurse=True + loop: + - /etc/crowdsec + - "{{ cs_root_dir }}/data" + tags: cs diff --git a/roles/crowdsec/tasks/directories.yml b/roles/crowdsec/tasks/directories.yml new file mode 100644 index 0000000..2c3bbbd --- /dev/null +++ b/roles/crowdsec/tasks/directories.yml @@ -0,0 +1,21 @@ +--- + +- name: Create required directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: /etc/crowdsec + mode: 755 + - dir: "{{ cs_root_dir }}" + - dir: "{{ cs_root_dir }}/backup" + mode: 700 + - dir: "{{ cs_root_dir }}/data" + - dir: /etc/crowdsec/parsers/s00-raw + - dir: /etc/crowdsec/parsers/s01-parse + - dir: /etc/crowdsec/parsers/s02-enrich + - dir: /etc/crowdsec/scenarios + - dir: /etc/crowdsec/postoverflows/s00-enrich + - dir: /etc/crowdsec/postoverflows/s01-whitelist + - dir: /etc/crowdsec/acquis + - dir: /etc/crowdsec/meta + mode: 700 + tags: cs diff --git a/roles/crowdsec/tasks/facts.yml b/roles/crowdsec/tasks/facts.yml new file mode 100644 index 0000000..119ee78 --- /dev/null +++ b/roles/crowdsec/tasks/facts.yml @@ -0,0 +1,84 @@ +--- + +- name: Set initial facts + block: + - set_fact: cs_install_mode='none' + - set_fact: cs_current_version='' + tags: cs + +- name: Check if crowdsec is installed + stat: path=/usr/local/bin/crowdsec + register: cs_bin + tags: cs + +- name: Check installed version + shell: | + crowdsec -version 2>&1 | perl -ne 'm/version: v(\d+(\.\d+)*)/ && print $1' + register: cs_current_version + changed_when: False + when: cs_bin.stat.exists + tags: cs + +- name: Set install mode + set_fact: cs_install_mode='install' + when: not cs_bin.stat.exists + tags: cs + +- name: Set upgrade mode + set_fact: cs_install_mode='upgrade' + when: + - cs_bin.stat.exists + - cs_current_version.stdout != cs_version + tags: cs + +# Create a random db password if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "/etc/crowdsec/meta/ansible_db_pass" + - complex: False + - set_fact: cs_db_pass={{ rand_pass }} + when: + - cs_db_pass is not defined + - cs_lapi_enabled + tags: cs + +# Check if local API credentials are available in the meta dir +- name: Check local API credential files + stat: path=/etc/crowdsec/meta/lapi_pass + register: cs_lapi_pass_file + tags: cs + +- name: Read the local API pass + block: + - slurp: src=/etc/crowdsec/meta/lapi_pass + register: cs_lapi_pass_meta + - set_fact: cs_lapi_pass={{ cs_lapi_pass_meta.content | b64decode | trim }} + when: cs_lapi_pass is not defined and cs_lapi_pass_file.stat.exists + tags: cs + +# Check if central API credentials are available in the meta dir +- name: Check central API credential files + block: + - stat: path=/etc/crowdsec/meta/capi_user + register: cs_capi_user_file + - stat: path=/etc/crowdsec/meta/capi_pass + register: cs_capi_pass_file + tags: cs + +- name: Read the central API user + block: + - slurp: src=/etc/crowdsec/meta/capi_user + register: cs_capi_user_meta + - set_fact: cs_capi_user={{ cs_capi_user_meta.content | b64decode | trim }} + when: cs_capi_user is not defined and cs_capi_user_file.stat.exists + tags: cs + +- name: Read the central API pass + block: + - slurp: src=/etc/crowdsec/meta/capi_pass + register: cs_capi_pass_meta + - set_fact: cs_capi_pass={{ cs_capi_pass_meta.content | b64decode | trim }} + when: cs_capi_pass is not defined and cs_capi_pass_file.stat.exists + tags: cs + diff --git a/roles/crowdsec/tasks/install.yml b/roles/crowdsec/tasks/install.yml new file mode 100644 index 0000000..ec8fe06 --- /dev/null +++ b/roles/crowdsec/tasks/install.yml @@ -0,0 +1,74 @@ +--- + +- name: Install needed tools + package: + name: + - tar + - zstd + tags: cs + +- when: cs_install_mode != 'none' + block: + - name: Download crowdsec + get_url: + url: "{{ cs_archive_url }}" + dest: /tmp/ + checksum: sha1:{{ cs_archive_sha1 }} + + - name: Extract crowdsec + unarchive: + src: /tmp/crowdsec-release.tgz + dest: /tmp/ + remote_src: True + + - name: Install or upgrade crowdsec + command: ./wizard.sh --bin{{ cs_install_mode }} --force + args: + chdir: /tmp/crowdsec-v{{ cs_version }}/ + notify: restart crowdsec + + tags: cs + +- name: Update crowdsec hub + command: cscli hub update + changed_when: False + tags: cs + +- name: Create the systemd unit snippet dir + file: path=/etc/systemd/system/crowdsec.service.d state=directory + tags: cs + +- name: Make the service restart on failure + copy: + content: | + [Service] + Restart=on-failure + StartLimitInterval=0 + RestartSec=30 + dest: /etc/systemd/system/crowdsec.service.d/restart.conf + register: crodwsec_unit_restart + notify: restart crowdsec + tags: cs + +- name: Set user account which runs the service + copy: + content: | + [Service] + User={{ cs_user }} + Group={{ cs_user }} + dest: /etc/systemd/system/crowdsec.service.d/user.conf + register: crodwsec_unit_user + notify: restart crowdsec + tags: cs + +- name: Reload systemd + systemd: daemon_reload=True + when: crodwsec_unit_restart.changed or crodwsec_unit_user.changed + tags: cs + +- name: Install pre and post backup hooks + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/crowdsec mode=700 + loop: + - pre + - post + tags: cs diff --git a/roles/crowdsec/tasks/iptables.yml b/roles/crowdsec/tasks/iptables.yml new file mode 100644 index 0000000..82fd517 --- /dev/null +++ b/roles/crowdsec/tasks/iptables.yml @@ -0,0 +1,15 @@ +--- + +- name: Handle crowdsec port in the firewall + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ item.port }} -s {{ item.src_ip | join(',') }} -j ACCEPT" + loop: + - name: cs_lapi_port + port: "{{ cs_lapi_port }}" + src_ip: "{{ cs_lapi_src_ip }}" + - name: cs_prometheus_port + port: "{{ cs_prometheus_port }}" + src_ip: "{{ cs_prometheus_src_ip }}" + tags: firewall,cs diff --git a/roles/crowdsec/tasks/main.yml b/roles/crowdsec/tasks/main.yml new file mode 100644 index 0000000..23c1263 --- /dev/null +++ b/roles/crowdsec/tasks/main.yml @@ -0,0 +1,11 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: install.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: cleanup.yml diff --git a/roles/crowdsec/tasks/services.yml b/roles/crowdsec/tasks/services.yml new file mode 100644 index 0000000..e778afb --- /dev/null +++ b/roles/crowdsec/tasks/services.yml @@ -0,0 +1,5 @@ +--- + +- name: Start and enable the service + service: name=crowdsec state=started enabled=True + tags: cs diff --git a/roles/crowdsec/tasks/user.yml b/roles/crowdsec/tasks/user.yml new file mode 100644 index 0000000..bab2a0f --- /dev/null +++ b/roles/crowdsec/tasks/user.yml @@ -0,0 +1,6 @@ +--- + +- name: Create crowdsec user + user: name={{ cs_user }} system=True shell=/sbin/nologin + when: cs_user != 'root' + tags: cs diff --git a/roles/crowdsec/templates/acquis.yaml.j2 b/roles/crowdsec/templates/acquis.yaml.j2 new file mode 100644 index 0000000..152bda7 --- /dev/null +++ b/roles/crowdsec/templates/acquis.yaml.j2 @@ -0,0 +1,6 @@ +{% if cs_acquis is defined and cs_acquis | length > 0%} +{% for acquis in cs_acquis %} +--- +{{ acquis | to_nice_yaml }} +{% endfor %} +{% endif %} diff --git a/roles/crowdsec/templates/acquis/system.yaml.j2 b/roles/crowdsec/templates/acquis/system.yaml.j2 new file mode 100644 index 0000000..b8b8149 --- /dev/null +++ b/roles/crowdsec/templates/acquis/system.yaml.j2 @@ -0,0 +1,5 @@ +--- +journalctl_filter: + - "" +labels: + type: syslog diff --git a/roles/crowdsec/templates/config.yaml.j2 b/roles/crowdsec/templates/config.yaml.j2 new file mode 100644 index 0000000..fa42a33 --- /dev/null +++ b/roles/crowdsec/templates/config.yaml.j2 @@ -0,0 +1,65 @@ +common: + daemonize: true + pid_dir: /var/run/ + log_media: stdout + log_level: info + working_dir: . + +config_paths: + config_dir: /etc/crowdsec/ + data_dir: {{ cs_root_dir }}/data/ + simulation_path: /etc/crowdsec/simulation.yaml + hub_dir: /etc/crowdsec/hub/ + index_path: /etc/crowdsec/hub/.index.json + +crowdsec_service: +{% if cs_acquis is defined %} + acquisition_path: /etc/crowdsec/acquis.yaml +{% else %} + acquisition_dir: /etc/crowdsec/acquis/ +{% endif %} + parser_routines: 1 + +cscli: + output: human + hub_branch: master + +db_config: + log_level: info +{% if cs_db_engine == 'mysql' %} + type: mysql + user: {{ cs_db_user }} + password: {{ cs_db_pass | quote }} + db_name: {{ cs_db_name }} + host: {{ cs_db_server }} + port: {{ cs_db_port }} +{% else %} + type: sqlite + db_path: {{ cs_root_dir }}/data/crowdsec.db +{% endif %} + flush: + max_items: 100000 + max_age: 730d + +api: + client: + insecure_skip_verify: false + credentials_path: /etc/crowdsec/local_api_credentials.yaml + +{% if cs_lapi_enabled %} + server: + log_level: info + listen_uri: 0.0.0.0:{{ cs_lapi_port }} + profiles_path: /etc/crowdsec/profiles.yaml +{% if cs_capi_enabled %} + online_client: + credentials_path: /etc/crowdsec/online_api_credentials.yaml +{% endif %} +{% endif %} + +prometheus: + enabled: true + level: full + listen_addr: {{ (cs_prometheus_src_ip | length > 0) | ternary(ansible_all_ipv4_addresses[0],'127.0.0.1') }} + listen_port: {{ cs_prometheus_port }} + diff --git a/roles/crowdsec/templates/dev.yaml.j2 b/roles/crowdsec/templates/dev.yaml.j2 new file mode 100644 index 0000000..5d6f302 --- /dev/null +++ b/roles/crowdsec/templates/dev.yaml.j2 @@ -0,0 +1,39 @@ +common: + daemonize: false + log_media: stdout + log_level: info + working_dir: . + +config_paths: + config_dir: /etc/crowdsec/ + data_dir: {{ cs_root_dir }}/data/ + simulation_path: /etc/crowdsec/simulation.yaml + hub_dir: /etc/crowdsec/hub/ + index_path: /etc/crowdsec/hub/.index.json + +crowdsec_service: + acquisition_path: /etc/crowdsec/acquis.yaml + parser_routines: 1 + +cscli: + output: human + hub_branch: master + +db_config: + log_level: info + type: sqlite + db_path: {{ cs_root_dir }}/data/dev.db + flush: + max_items: 1000 + max_age: 30d + +api: + client: + insecure_skip_verify: false + credentials_path: /etc/crowdsec/local_api_credentials.yaml + server: + profiles_path: /etc/crowdsec/profiles.yaml + +prometheus: + enabled: false + diff --git a/roles/crowdsec/templates/local_api_credentials.yaml.j2 b/roles/crowdsec/templates/local_api_credentials.yaml.j2 new file mode 100644 index 0000000..2b8d193 --- /dev/null +++ b/roles/crowdsec/templates/local_api_credentials.yaml.j2 @@ -0,0 +1,3 @@ +url: {{ cs_lapi_enabled | ternary('http://127.0.0.1:' ~ cs_lapi_port,(cs_lapi_url is search('/$')) | ternary(cs_lapi_url, cs_lapi_url ~ '/')) }} +login: {{ cs_lapi_user }} +password: {{ cs_lapi_pass }} diff --git a/roles/crowdsec/templates/online_api_credentials.yaml.j2 b/roles/crowdsec/templates/online_api_credentials.yaml.j2 new file mode 100644 index 0000000..ae7b3c6 --- /dev/null +++ b/roles/crowdsec/templates/online_api_credentials.yaml.j2 @@ -0,0 +1,7 @@ +url: https://api.crowdsec.net/ +{% if cs_capi_user is defined %} +login: {{ cs_capi_user }} +{% endif %} +{% if cs_capi_pass is defined %} +password: {{ cs_capi_pass }} +{% endif %} diff --git a/roles/crowdsec/templates/parsers/s02-enrich/trusted_ip.yaml.j2 b/roles/crowdsec/templates/parsers/s02-enrich/trusted_ip.yaml.j2 new file mode 100644 index 0000000..173f25e --- /dev/null +++ b/roles/crowdsec/templates/parsers/s02-enrich/trusted_ip.yaml.j2 @@ -0,0 +1,16 @@ +name: fws/trusted_ip +description: "Whitelist events from trusted ip" +whitelist: + reason: "trusted ip" + ip: +{% for ip in trusted_ip | default([]) %} +{% if ip is not search('/\d+$') %} + - "{{ ip }}" +{% endif %} +{% endfor %} + cidr: +{% for ip in trusted_ip | default([]) %} +{% if ip is search('/\d+$') %} + - "{{ ip }}" +{% endif %} +{% endfor %} diff --git a/roles/crowdsec/templates/post-backup.j2 b/roles/crowdsec/templates/post-backup.j2 new file mode 100644 index 0000000..8a75c62 --- /dev/null +++ b/roles/crowdsec/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ cs_root_dir }}/backup/* diff --git a/roles/crowdsec/templates/pre-backup.j2 b/roles/crowdsec/templates/pre-backup.j2 new file mode 100644 index 0000000..ece55ab --- /dev/null +++ b/roles/crowdsec/templates/pre-backup.j2 @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eo pipefail + +{% if cs_lapi_enabled %} +{% if cs_db_engine == 'mysql' %} +/usr/bin/mysqldump \ +{% if cs_db_server not in ['localhost','127.0.0.1'] %} + --user={{ cs_db_user | quote }} \ + --password={{ cs_db_pass | quote }} \ + --host={{ cs_db_server | quote }} \ + --port={{ cs_db_port | quote }} \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ cs_db_name | quote }} | zstd -c > {{ cs_root_dir }}/backup/{{ cs_db_name }}.sql.zst +{% else %} +sqlite3 {{ cs_root_dir }}/data/crowdsec.db .dump | zstd -c > {{ cs_root_dir }}/backup/crowdsec.sql.zst +{% endif %} +{% endif %} diff --git a/roles/crowdsec/templates/profiles.yaml.j2 b/roles/crowdsec/templates/profiles.yaml.j2 new file mode 100644 index 0000000..3f28bc6 --- /dev/null +++ b/roles/crowdsec/templates/profiles.yaml.j2 @@ -0,0 +1,33 @@ +{% if cs_trusted_countries | length > 0 %} +name: trusted_countries_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.Source.Cn in ["{{ cs_trusted_countries | join('","') }}"] +decisions: + - type: ban + duration: {{ cs_ban_trusted_duration }} +on_success: break +--- +name: trusted_countries_range_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" && Alert.Source.Cn in ["{{ cs_trusted_countries | join('","') }}"] +decisions: + - type: ban + duration: {{ cs_ban_trusted_duration }} +on_success: break +--- +{% endif %} +name: default_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" +decisions: + - type: ban + duration: {{ cs_ban_duration }} +on_success: break +--- +name: default_range_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" +decisions: + - type: ban + duration: {{ cs_ban_duration }} +on_success: break diff --git a/roles/crowdsec/templates/simulation.yaml.j2 b/roles/crowdsec/templates/simulation.yaml.j2 new file mode 100644 index 0000000..94adcc5 --- /dev/null +++ b/roles/crowdsec/templates/simulation.yaml.j2 @@ -0,0 +1 @@ +simulation: off diff --git a/roles/crowdsec_firewall_bouncer/defaults/main.yml b/roles/crowdsec_firewall_bouncer/defaults/main.yml new file mode 100644 index 0000000..d8d5c91 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/defaults/main.yml @@ -0,0 +1,15 @@ +--- + +# Version of the firewall bouncer to install +cs_fw_version: 0.0.10 +# URL of the firewall bouncer archive +cs_fw_archive_url: https://github.com/crowdsecurity/cs-firewall-bouncer/releases/download/v{{ cs_fw_version }}/cs-firewall-bouncer.tgz +# Expected sha1 of the archive +cs_fw_archive_sha1: 46863e95bdc8f48434583f55e89b7720fce5736d + +# API on which the bouncer should listen for alerts +cs_fw_lapi_url: "{{ cs_lapi_url | default('http://localhost:8080/') }}" +# If not defined, ansible will try to register the bouncer on the Local API server +# cs_lapi_server must be defined in this case +# cs_fw_lapi_key: aaabbbccc + diff --git a/roles/crowdsec_firewall_bouncer/handlers/main.yml b/roles/crowdsec_firewall_bouncer/handlers/main.yml new file mode 100644 index 0000000..c8c6d1e --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart cs-firewall-bouncer + service: name=cs-firewall-bouncer state=restarted diff --git a/roles/crowdsec_firewall_bouncer/tasks/cleanup.yml b/roles/crowdsec_firewall_bouncer/tasks/cleanup.yml new file mode 100644 index 0000000..59e3129 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove temp and obsolete files + file: path={{ item }} state=absent + loop: + - /tmp/cs-firewall-bouncer.tgz + - /tmp/cs-firewall-bouncer-v{{ cs_fw_version }} + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/tasks/conf.yml b/roles/crowdsec_firewall_bouncer/tasks/conf.yml new file mode 100644 index 0000000..b1485ec --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/conf.yml @@ -0,0 +1,6 @@ +--- + +- name: Deploy configuration + template: src=cs-firewall-bouncer.yaml.j2 dest=/etc/crowdsec/cs-firewall-bouncer/cs-firewall-bouncer.yaml mode=600 + notify: restart cs-firewall-bouncer + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/tasks/directories.yml b/roles/crowdsec_firewall_bouncer/tasks/directories.yml new file mode 100644 index 0000000..36ba46e --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/directories.yml @@ -0,0 +1,9 @@ +--- + +- name: Create needed directories + file: path={{ item.dir }} state=directory mode={{ item.mode | default(omit) }} + loop: + - dir: /etc/crowdsec/cs-firewall-bouncer + - dir: /etc/crowdsec/meta + mode: 700 + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/tasks/facts.yml b/roles/crowdsec_firewall_bouncer/tasks/facts.yml new file mode 100644 index 0000000..f963a0b --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/facts.yml @@ -0,0 +1,73 @@ +--- + +- include_vars: "{{ item }}" + with_first_found: + - vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_distribution }}.yml + - vars/{{ ansible_os_family }}.yml + tags: cs + +- name: Check if API key is available + stat: path=/etc/crowdsec/meta/bouncer_fw_api_key + register: cs_fw_lapi_key_file + tags: cs + +- when: cs_fw_lapi_key is not defined and (not cs_fw_lapi_key_file.stat.exists or cs_fw_lapi_key_file.stat.size == 0) + block: + - name: Register the bouncer + shell: | + cscli bouncers list -o raw | grep -q -P '^{{ inventory_hostname }}-firewall' && cscli bouncers delete {{ inventory_hostname }}-firewall + cscli bouncers add {{ inventory_hostname }}-firewall -o raw + register: cs_bouncer_add + failed_when: cs_bouncer_add.rc not in [0,1] + changed_when: cs_bouncer_add.rc == 0 + delegate_to: "{{ cs_lapi_server | default(inventory_hostname) }}" + + - name: Record the API key for later use + copy: content={{ cs_bouncer_add.stdout }} dest=/etc/crowdsec/meta/bouncer_fw_api_key mode=600 + + tags: cs + +- when: cs_fw_lapi_key is not defined + block: + - name: Read the API key + slurp: src=/etc/crowdsec/meta/bouncer_fw_api_key + register: cs_fw_lapi_generated_key + - set_fact: cs_fw_lapi_key={{ cs_fw_lapi_generated_key.content | b64decode | trim }} + tags: cs + +- name: Set initial facts + block: + - set_fact: cs_fw_current_version='' + - set_fact: cs_fw_install_mode='none' + tags: cs + +- name: Check if the bouncer is installed + stat: path=/usr/local/bin/cs-firewall-bouncer + register: cs_fw_bin + tags: cs + +- when: cs_fw_bin.stat.exists + block: + - name: Detect installed version + shell: | + cs-firewall-bouncer -c /dev/null 2>&1 | perl -ne 'm/cs-firewall-bouncer v(\d+(\.\d+)*)/ && print $1' + register: cs_fw_current_version + changed_when: False + + - set_fact: cs_fw_current_version={{ cs_fw_current_version.stdout }} + tags: cs + +- name: Set install mode + set_fact: cs_fw_install_mode='install' + when: not cs_fw_bin.stat.exists + tags: cs + +- name: Set upgrade mode + set_fact: cs_fw_install_mode='upgrade' + when: + - cs_fw_bin.stat.exists + - cs_fw_current_version != cs_fw_version + tags: cs + diff --git a/roles/crowdsec_firewall_bouncer/tasks/install.yml b/roles/crowdsec_firewall_bouncer/tasks/install.yml new file mode 100644 index 0000000..b9c2ee1 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/install.yml @@ -0,0 +1,70 @@ +--- + +- name: Install needed tools + package: + name: + - ipset + tags: cs + +- when: cs_fw_install_mode != 'none' + block: + + - name: Download the bouncer + get_url: + url: "{{ cs_fw_archive_url }}" + dest: /tmp + checksum: sha1:{{ cs_fw_archive_sha1 }} + + - name: Extract the archive + unarchive: + src: /tmp/cs-firewall-bouncer.tgz + dest: /tmp + remote_src: True + + - name: Install or upgrade + command: ./{{ cs_fw_install_mode }}.sh + args: + chdir: /tmp/cs-firewall-bouncer-v{{ cs_fw_version }} + notify: restart cs-firewall-bouncer + + tags: cs + +- name: Create systemd unit snippet dir + file: path=/etc/systemd/system/cs-firewall-bouncer.service.d state=directory + tags: cs + +- name: Create iptables snippet dir + file: path=/etc/systemd/system/{{ cs_iptables_service }}.service.d state=directory + tags: cs + +- name: Create ipsets before iptables starts + copy: + content: | + [Service] + ExecStartPre=/usr/sbin/ipset -exist create crowdsec-blacklists nethash timeout 300 + ExecStartPre=/usr/sbin/ipset -exist create crowdsec6-blacklists nethash timeout 300 family inet6 + dest: /etc/systemd/system/{{ cs_iptables_service }}.service.d/cs-ipset.conf + register: cs_iptable_unit + tags: cs + +- name: Tune cs-firewall-bouncer service + copy: + content: | + [Unit] + # The bouncer should start after crowdsec to be able to register on the API + After=crowdsec.service + + [Service] + # Restart on failure + Restart=on-failure + StartLimitInterval=0 + RestartSec=30 + dest: /etc/systemd/system/cs-firewall-bouncer.service.d/ansible.conf + register: crodwsec_fw_unit + notify: restart cs-firewall-bouncer + tags: cs + +- name: Reload systemd + systemd: daemon_reload=True + when: crodwsec_fw_unit.changed or cs_iptable_unit.changed + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/tasks/iptables.yml b/roles/crowdsec_firewall_bouncer/tasks/iptables.yml new file mode 100644 index 0000000..14d27f0 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/iptables.yml @@ -0,0 +1,17 @@ +--- + +- name: Ensure ipsets exist + shell: | + ipset list crowdsec-blacklists || ipset create crowdsec-blacklists nethash timeout 300 + ipset list crowdsec6-blacklists || ipset create crowdsec6-blacklists nethash timeout 300 family inet6 + changed_when: False + tags: cs + +- name: Add DROP rules + iptables_raw: + name: cs_blacklist + weight: 9 + rules: | + -A INPUT -m set --match-set crowdsec-blacklists src -j DROP + -A FORWARD -m set --match-set crowdsec-blacklists src -j DROP + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/tasks/main.yml b/roles/crowdsec_firewall_bouncer/tasks/main.yml new file mode 100644 index 0000000..9575cea --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/main.yml @@ -0,0 +1,10 @@ +--- + +- include: directories.yml +- include: facts.yml +- include: install.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: cleanup.yml diff --git a/roles/crowdsec_firewall_bouncer/tasks/services.yml b/roles/crowdsec_firewall_bouncer/tasks/services.yml new file mode 100644 index 0000000..dc01f70 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/tasks/services.yml @@ -0,0 +1,5 @@ +--- + +- name: Start and enable the service + service: name=cs-firewall-bouncer state=started enabled=True + tags: cs diff --git a/roles/crowdsec_firewall_bouncer/templates/cs-firewall-bouncer.yaml.j2 b/roles/crowdsec_firewall_bouncer/templates/cs-firewall-bouncer.yaml.j2 new file mode 100644 index 0000000..7d60022 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/templates/cs-firewall-bouncer.yaml.j2 @@ -0,0 +1,12 @@ +--- + +mode: iptables +piddir: /var/run/ +update_frequency: 10s +daemonize: true +log_mode: stdout +log_level: info +api_url: {{ (cs_fw_lapi_url is search('/$')) | ternary(cs_fw_lapi_url,cs_fw_lapi_url ~ '/') }} +api_key: {{ cs_fw_lapi_key }} +disable_ipv6: false + diff --git a/roles/crowdsec_firewall_bouncer/vars/Debian.yml b/roles/crowdsec_firewall_bouncer/vars/Debian.yml new file mode 100644 index 0000000..c77c3eb --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/vars/Debian.yml @@ -0,0 +1,3 @@ +--- + +cs_iptables_service: netfilter-persistent diff --git a/roles/crowdsec_firewall_bouncer/vars/RedHat.yml b/roles/crowdsec_firewall_bouncer/vars/RedHat.yml new file mode 100644 index 0000000..3201c17 --- /dev/null +++ b/roles/crowdsec_firewall_bouncer/vars/RedHat.yml @@ -0,0 +1,3 @@ +--- + +cs_iptables_service: iptables diff --git a/roles/diagrams/defaults/main.yml b/roles/diagrams/defaults/main.yml new file mode 100644 index 0000000..2d417bc --- /dev/null +++ b/roles/diagrams/defaults/main.yml @@ -0,0 +1,17 @@ +--- + +# Veresion of diagrams to deploy +diagrams_version: 15.7.4 +# URL of the WAR file to deploy +diagrams_war_url: https://github.com/jgraph/drawio/releases/download/v{{ diagrams_version }}/draw.war +# Expected sha1 of the WAR file +diagrams_war_sha1: 6ec509cba9c0cb7ed5a6872bcfc3711f6bfb1933 +# root directory of the installation +diagrams_root_dir: /opt/diagrams +# Should ansible manage upgrades, or just initial install ? +diagrams_manage_upgrade: True +# Port on which the tomcat instance will listen. +# Note that it'll also use this port +1 for shutdown requests, but only on 127.0.0.1 +diagrams_port: 8182 +# List of IP addresses (or CIDR) allowed to access tomcat port +diagrams_src_ip: [] diff --git a/roles/diagrams/handlers/main.yml b/roles/diagrams/handlers/main.yml new file mode 100644 index 0000000..684b4b1 --- /dev/null +++ b/roles/diagrams/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart diagrams + service: name=tomcat@diagrams state=restarted diff --git a/roles/diagrams/meta/main.yml b/roles/diagrams/meta/main.yml new file mode 100644 index 0000000..5a1ad60 --- /dev/null +++ b/roles/diagrams/meta/main.yml @@ -0,0 +1,7 @@ +--- + +dependencies: + - role: repo_lux # EL8 doesn't have tomcat anymore + when: + - ansible_os_family == 'RedHat' + - ansible_distribution_major_version is version('8','>=') diff --git a/roles/diagrams/tasks/archive_post.yml b/roles/diagrams/tasks/archive_post.yml new file mode 100644 index 0000000..1443872 --- /dev/null +++ b/roles/diagrams/tasks/archive_post.yml @@ -0,0 +1,14 @@ +--- + +- name: Compress previous version + command: tar cf {{ diagrams_root_dir }}/archives/{{ diagrams_current_version }}.tar.zst --use-compress-program=zstd ./ + environment: + ZST_CLEVEL: 10 + args: + chdir: "{{ diagrams_root_dir }}/archives/{{ diagrams_current_version }}" + warn: False + tags: diagrams + +- name: Remove the arachive directory + file: path={{ diagrams_root_dir }}/archives/{{ diagrams_current_version }} state=absent + tags: diagrams diff --git a/roles/diagrams/tasks/archive_pre.yml b/roles/diagrams/tasks/archive_pre.yml new file mode 100644 index 0000000..c9d14a9 --- /dev/null +++ b/roles/diagrams/tasks/archive_pre.yml @@ -0,0 +1,9 @@ +--- + +- name: Create the archive dir + file: path={{ diagrams_root_dir }}/archives/{{ diagrams_current_version }} state=directory + tags: diagrams + +- name: Copy the war archive + copy: src={{ diagrams_root_dir }}/webapps/draw.war dest={{ diagrams_root_dir }}/archives/{{ diagrams_current_version }} remote_src=True + tags: diagrams diff --git a/roles/diagrams/tasks/cleanup.yml b/roles/diagrams/tasks/cleanup.yml new file mode 100644 index 0000000..6b73d1d --- /dev/null +++ b/roles/diagrams/tasks/cleanup.yml @@ -0,0 +1,7 @@ +--- + +- name: Remove tmp and obsolete files + file: path={{ item }} state=absent + loop: + - "{{ diagrams_root_dir }}/tmp/draw.war" + tags: diagrams diff --git a/roles/diagrams/tasks/conf.yml b/roles/diagrams/tasks/conf.yml new file mode 100644 index 0000000..0f3b117 --- /dev/null +++ b/roles/diagrams/tasks/conf.yml @@ -0,0 +1,21 @@ +--- + +- name: Deploy sysconfig + template: src=sysconfig.j2 dest=/etc/sysconfig/tomcat@diagrams + notify: restart diagrams + tags: diagrams + +- name: Deploy tomcat configuration + template: src={{ item }}.j2 dest={{ diagrams_root_dir }}/conf/{{ item }} group=tomcat mode=640 + loop: + - server.xml + notify: restart diagrams + tags: diagrams + +- name: Link configuration files + file: state=link src=/etc/tomcat/{{ item }} dest={{ diagrams_root_dir }}/conf/{{ item }} + loop: + - web.xml + - logging.properties + notify: restart diagrams + tags: diagrams diff --git a/roles/diagrams/tasks/directories.yml b/roles/diagrams/tasks/directories.yml new file mode 100644 index 0000000..6d78392 --- /dev/null +++ b/roles/diagrams/tasks/directories.yml @@ -0,0 +1,38 @@ +--- + +- name: Create directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ diagrams_root_dir }}/" + group: tomcat + - dir: "{{ diagrams_root_dir }}/webapps" + group: tomcat + mode: 770 + - dir: "{{ diagrams_root_dir }}/conf" + group: tomcat + - dir: "{{ diagrams_root_dir }}/conf/Catalina" + owner: tomcat + mode: 700 + - dir: "{{ diagrams_root_dir }}/tmp" + group: tomcat + mode: 770 + - dir: "{{ diagrams_root_dir }}/logs" + owner: tomcat + mode: 700 + - dir: "{{ diagrams_root_dir }}/work" + owner: tomcat + mode: 700 + - dir: "{{ diagrams_root_dir }}/meta" + mode: 700 + - dir: "{{ diagrams_root_dir }}/archives" + mode: 700 + tags: diagrams + +- name: Create symlinks + file: state=link src={{ item.src }} dest={{ item.dest }} + loop: + - src: /usr/share/tomcat/bin/ + dest: "{{ diagrams_root_dir }}/bin" + - src: /usr/share/java/tomcat + dest: "{{ diagrams_root_dir }}/lib" + tags: diagrams diff --git a/roles/diagrams/tasks/facts.yml b/roles/diagrams/tasks/facts.yml new file mode 100644 index 0000000..12c2714 --- /dev/null +++ b/roles/diagrams/tasks/facts.yml @@ -0,0 +1,12 @@ +--- + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ diagrams_root_dir }}" + - version: "{{ diagrams_version }}" + tags: diagrams + +- block: + - set_fact: diagrams_install_mode={{ (install_mode == 'upgrade' and not diagrams_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: diagrams_current_version={{ current_version | default('') }} + tags: diagrams diff --git a/roles/diagrams/tasks/install.yml b/roles/diagrams/tasks/install.yml new file mode 100644 index 0000000..0f6bf06 --- /dev/null +++ b/roles/diagrams/tasks/install.yml @@ -0,0 +1,15 @@ +--- + +- when: diagrams_install_mode != 'none' + block: + - name: Download diagrams WAR + get_url: + url: "{{ diagrams_war_url }}" + dest: "{{ diagrams_root_dir }}/tmp/draw.war" + checksum: sha1:{{ diagrams_war_sha1 }} + + - name: Move WAR to the webapp dir + copy: src={{ diagrams_root_dir }}/tmp/draw.war dest={{ diagrams_root_dir }}/webapps/draw.war remote_src=True + notify: restart diagrams + + tags: diagrams diff --git a/roles/diagrams/tasks/iptables.yml b/roles/diagrams/tasks/iptables.yml new file mode 100644 index 0000000..a4c6924 --- /dev/null +++ b/roles/diagrams/tasks/iptables.yml @@ -0,0 +1,9 @@ +--- + +- name: Handle diagrams port in the firewall + iptables_raw: + name: diagrams_port + state: "{{ (diagrams_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ diagrams_port }} -s {{ diagrams_src_ip | join(',') }} -j ACCEPT" + tags: firewall,diagrams + diff --git a/roles/diagrams/tasks/main.yml b/roles/diagrams/tasks/main.yml new file mode 100644 index 0000000..c74213c --- /dev/null +++ b/roles/diagrams/tasks/main.yml @@ -0,0 +1,23 @@ +--- + +- name: Install tomcat + yum: + name: + - tomcat + tags: diagrams + +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: diagrams_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: selinux.yml + when: ansible_selinux.status == 'enabled' +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: write_version.yml +- include: archive_post.yml + when: diagrams_install_mode == 'upgrade' +- include: cleanup.yml diff --git a/roles/diagrams/tasks/selinux.yml b/roles/diagrams/tasks/selinux.yml new file mode 100644 index 0000000..691db5c --- /dev/null +++ b/roles/diagrams/tasks/selinux.yml @@ -0,0 +1,25 @@ +--- + +- name: Allow tomcat to bind on diagrams' port + seport: ports={{ diagrams_port }},{{ diagrams_port + 1 }} proto=tcp setype=http_port_t state=present + tags: diagrams + +- name: Set SELinux context + sefcontext: + target: "{{ item.target }}" + setype: "{{ item.type }}" + state: present + loop: + - target: "{{ diagrams_root_dir }}/webapps(/.*)?" + type: tomcat_var_lib_t + - target: "{{ diagrams_root_dir }}/(work|tmp)(/.*)?" + type: tomcat_cache_t + - target: "{{ diagrams_root_dir }}/logs(/.*)?" + type: tomcat_log_t + register: diagrams_sefcontext + tags: diagrams + +- name: Restore file contexts + command: restorecon -R {{ diagrams_root_dir }} + when: diagrams_sefcontext.results | selectattr('changed','equalto',True) | list | length > 0 + tags: diagrams diff --git a/roles/diagrams/tasks/services.yml b/roles/diagrams/tasks/services.yml new file mode 100644 index 0000000..98bf198 --- /dev/null +++ b/roles/diagrams/tasks/services.yml @@ -0,0 +1,5 @@ +--- + +- name: start and enable diagrams + service: name=tomcat@diagrams state=started enabled=True + tags: diagrams diff --git a/roles/diagrams/tasks/write_version.yml b/roles/diagrams/tasks/write_version.yml new file mode 100644 index 0000000..06669f1 --- /dev/null +++ b/roles/diagrams/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write installed version + copy: content={{ diagrams_version }} dest={{ diagrams_root_dir }}/meta/ansible_version + tags: diagrams diff --git a/roles/diagrams/templates/server.xml.j2 b/roles/diagrams/templates/server.xml.j2 new file mode 100644 index 0000000..7a98284 --- /dev/null +++ b/roles/diagrams/templates/server.xml.j2 @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/roles/diagrams/templates/sysconfig.j2 b/roles/diagrams/templates/sysconfig.j2 new file mode 100644 index 0000000..5a15e03 --- /dev/null +++ b/roles/diagrams/templates/sysconfig.j2 @@ -0,0 +1,3 @@ +CATALINA_BASE="{{ diagrams_root_dir }}" +CATALINA_HOME="{{ diagrams_root_dir }}" +CATALINA_TMPDIR="{{ diagrams_root_dir }}/tmp" diff --git a/roles/dnscache/defaults/main.yml b/roles/dnscache/defaults/main.yml new file mode 100644 index 0000000..2ee472e --- /dev/null +++ b/roles/dnscache/defaults/main.yml @@ -0,0 +1,71 @@ +--- + +# IP allowed in the firewall +dnscache_src_ip: [] + +# IP on which we bind +dnscache_ip: 127.0.0.1 + +# If we want to delegate only some zones +#dnscache_forwarded_zones: +# - zone: firewall-services.com +# servers: +# - 192.168.133.254 +# - zone: 133.168.192.in-addr.arpa +# servers: +# - 192.168.133.254 + +dnscache_forwarded_zones: + - zone: letsencrypt.org + servers: + - 80.67.169.12 + - 80.67.169.40 + - zone: api.letsencrypt.org + servers: + - 80.67.169.12 + - 80.67.169.40 + - zone: edgekey.net + servers: + - 80.67.169.12 + - 80.67.169.40 + - zone: akamaiedge.net + servers: + - 80.67.169.12 + - 80.67.169.40 + - zone: akamaized.net + servers: + - 80.67.169.12 + - 80.67.169.40 + - zone: akamai.net + servers: + - 80.67.169.12 + - 80.67.169.40 + +# Root server list. If dnscache_forward_only is True, should be a list +# of server to which we forward queries instead of root servers +dnscache_roots: + - 128.63.2.53 + - 192.112.36.4 + - 192.203.230.10 + - 192.228.79.201 + - 192.33.4.12 + - 192.36.148.17 + - 192.5.5.241 + - 192.58.128.30 + - 193.0.14.129 + - 198.41.0.4 + - 199.7.83.42 + - 199.7.91.13 + - 202.12.27.33 + +# Do we act as a resolver or a simple forwarder +dnscache_forward_only: False + +# Data and Cache sizes. Cache should not exceed data +dnscache_data_limit: 12000000 +dnscache_cache_size: 10000000 + +# Account under which we run. Default to daemons +dnscache_uid: 2 +dnscache_gid: 2 + diff --git a/roles/dnscache/handlers/main.yml b/roles/dnscache/handlers/main.yml new file mode 100644 index 0000000..12df842 --- /dev/null +++ b/roles/dnscache/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: restart dnscache + service: name=dnscache state=restarted enabled=yes +... diff --git a/roles/dnscache/tasks/main.yml b/roles/dnscache/tasks/main.yml new file mode 100644 index 0000000..74bc836 --- /dev/null +++ b/roles/dnscache/tasks/main.yml @@ -0,0 +1,53 @@ +--- + +- name: Install packages + yum: + name: + - ndjbdns + +- name: Deploy dnscache config + template: src={{ item.src }} dest={{ item.dest }} + with_items: + - { src: dnscache.conf.j2, dest: /etc/ndjbdns/dnscache.conf } + - { src: roots.j2, dest: /etc/ndjbdns/servers/roots } + notify: restart dnscache + +- name: Handle DNS port + iptables_raw: + name=dnscache_ports + state={{ (dnscache_src_ip | length > 0) | ternary('present','absent') }} + rules='-A INPUT -m state --state NEW -p udp -m multiport --dports 53 -s {{ dnscache_src_ip | join(',') }} -j ACCEPT' + when: iptables_manage | default(True) + +- name: Allow queries + copy: + content: "" + dest: /etc/ndjbdns/ip/0 + force: no + group: root + owner: root + mode: 0644 + notify: restart dnscache + +- name: List forwarded zones + shell: ls -1 /etc/ndjbdns/servers/ | xargs -n1 basename | grep -vP '^roots$' | cat + register: dnscache_fwd_zones + changed_when: False + +- name: Remove unmanaged forwarded zones + file: path=/etc/ndjbdns/servers/{{ item }} state=absent + with_items: "{{ dnscache_fwd_zones.stdout_lines | default([]) }}" + when: item not in dnscache_forwarded_zones | map(attribute='zone') + +- name: Deploy forwarded zones + copy: + content: "{{ item.servers | default([]) | join(\"\n\") }}" + dest: /etc/ndjbdns/servers/{{ item.zone }} + with_items: "{{ dnscache_forwarded_zones }}" + when: dnscache_forwarded_zones is defined and dnscache_forwarded_zones | length > 0 + notify: restart dnscache + +- name: Start and enable the service + service: name=dnscache state=started enabled=yes + +... diff --git a/roles/dnscache/templates/dnscache.conf.j2 b/roles/dnscache/templates/dnscache.conf.j2 new file mode 100644 index 0000000..6159ba2 --- /dev/null +++ b/roles/dnscache/templates/dnscache.conf.j2 @@ -0,0 +1,10 @@ +DATALIMIT={{ dnscache_data_limit }} +CACHESIZE={{ dnscache_cache_size }} +IP={{ dnscache_ip }} +IPSEND=0.0.0.0 +UID={{ dnscache_uid }} +GID={{ dnscache_gid }} +ROOT=/etc/ndjbdns +HIDETTL= +FORWARDONLY={{ dnscache_forward_only | ternary('1','') }} +DEBUG_LEVEL=1 diff --git a/roles/dnscache/templates/roots.j2 b/roles/dnscache/templates/roots.j2 new file mode 100644 index 0000000..11bfc97 --- /dev/null +++ b/roles/dnscache/templates/roots.j2 @@ -0,0 +1,3 @@ +{% for server in dnscache_roots %} +{{ server }} +{% endfor %} diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 0000000..2021395 --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,19 @@ +--- + +docker_data_dir: /opt/docker +docker_log_driver: journald + +docker_base_conf: + data-root: /opt/docker + log-driver: journald + storage-driver: overlay2 + storage-opts: + - 'overlay2.override_kernel_check=true' +docker_extra_conf: {} +# docker_extra_conf: +# log-opts: +# max-size: 100m +# max-file: 5 + +docker_conf: "{{ docker_base_conf | combine(docker_extra_conf, recursive=True) }}" + diff --git a/roles/docker/handlers/main.yml b/roles/docker/handlers/main.yml new file mode 100644 index 0000000..8321a6a --- /dev/null +++ b/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- name: restart docker + service: name=docker state=restarted + when: not docker_start.changed diff --git a/roles/docker/meta/main.yml b/roles/docker/meta/main.yml new file mode 100644 index 0000000..b7fa454 --- /dev/null +++ b/roles/docker/meta/main.yml @@ -0,0 +1,5 @@ +--- + +dependencies: + - role: repo_docker + - role: docker_compose diff --git a/roles/docker/tasks/conf.yml b/roles/docker/tasks/conf.yml new file mode 100644 index 0000000..e96d715 --- /dev/null +++ b/roles/docker/tasks/conf.yml @@ -0,0 +1,67 @@ +--- + +- name: Deploy docker daemon configuration + template: src=daemon.json.j2 dest=/etc/docker/daemon.json mode=600 + notify: restart docker + tags: docker + +- name: Create systemd snippet dir + file: path=/etc/systemd/system/docker.{{ item }}.d state=directory + loop: + - service + - socket + tags: docker + +- name: Create systemd service snippet dir + file: path=/etc/systemd/system/docker.service.d state=directory + tags: docker + +- name: Configure Docker to restart on failure + copy: + content: | + [Unit] + After=sssd.service + + [Service] + Restart=on-failure + StartLimitInterval=0 + RestartSec=30 + dest: /etc/systemd/system/docker.service.d/99-ansible.conf + register: docker_service_unit + tags: docker + +- name: Override docker socket configuration + copy: + content: | + [Unit] + After=sssd.service + DefaultDependencies=no + + [Socket] + SocketGroup={{ docker_conf.group }} + dest: /etc/systemd/system/docker.socket.d/99-ansible.conf + when: docker_conf.group is defined + register: docker_socket_unit + notify: restart docker + tags: docker + +- name: Remove obsolete conf + file: path=/etc/systemd/system/docker.socket.d/group.conf state=absent + register: docker_old_unit + tags: docker + +- name: Disable docker.socket to ensure the socket is pulled by the service + systemd: name=docker.socket enabled=False + tags: docker + +- name: Reload systemd + systemd: daemon_reload=True + when: docker_socket_unit.changed or docker_service_unit.changed or docker_old_unit.changed + tags: docker + +- name: Fix the dockremap UID namespace + lineinfile: path=/etc/{{ item }} regexp='^dockremap\s.*' line='dockremap:100000:65536' + loop: + - subuid + - subgid + tags: docker diff --git a/roles/docker/tasks/directories.yml b/roles/docker/tasks/directories.yml new file mode 100644 index 0000000..9c836d1 --- /dev/null +++ b/roles/docker/tasks/directories.yml @@ -0,0 +1,8 @@ +--- + +- name: Create directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ docker_conf['data-root'] }}" + - dir: /etc/docker + tags: docker diff --git a/roles/docker/tasks/facts.yml b/roles/docker/tasks/facts.yml new file mode 100644 index 0000000..09101b6 --- /dev/null +++ b/roles/docker/tasks/facts.yml @@ -0,0 +1,8 @@ +--- + +- set_fact: sysconfdir=/etc/sysconfig + when: ansible_os_family == 'RedHat' + tags: docker + +- set_fact: sysconfdir=/etc/default + when: ansible_os_family == 'Debian' diff --git a/roles/docker/tasks/install.yml b/roles/docker/tasks/install.yml new file mode 100644 index 0000000..cae3cfe --- /dev/null +++ b/roles/docker/tasks/install.yml @@ -0,0 +1,4 @@ +--- + +- include: install_{{ ansible_os_family }}.yml + diff --git a/roles/docker/tasks/install_RedHat.yml b/roles/docker/tasks/install_RedHat.yml new file mode 100644 index 0000000..3adc559 --- /dev/null +++ b/roles/docker/tasks/install_RedHat.yml @@ -0,0 +1,19 @@ +--- + +- name: Install packages + yum: + name: + - docker-ce + - docker-ce-cli + - device-mapper-persistent-data + - lvm2 + state: present + tags: docker + +- name: Remove packaged docker-compose + yum: + name: + - docker-compose + state: absent + tags: docker + diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..8da754b --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,7 @@ +--- + +- include: facts.yml +- include: directories.yml +- include: install.yml +- include: conf.yml +- include: service.yml diff --git a/roles/docker/tasks/service.yml b/roles/docker/tasks/service.yml new file mode 100644 index 0000000..cec4202 --- /dev/null +++ b/roles/docker/tasks/service.yml @@ -0,0 +1,6 @@ +--- + +- name: Start and enable dockerd + service: name=docker state=started enabled=True + register: docker_start + tags: docker diff --git a/roles/docker/templates/daemon.json.j2 b/roles/docker/templates/daemon.json.j2 new file mode 100644 index 0000000..939fa62 --- /dev/null +++ b/roles/docker/templates/daemon.json.j2 @@ -0,0 +1 @@ +{{ docker_conf | to_nice_json(indent=4) }} diff --git a/roles/docker/templates/docker-service-ansible.conf.j2 b/roles/docker/templates/docker-service-ansible.conf.j2 new file mode 100644 index 0000000..b42eb3f --- /dev/null +++ b/roles/docker/templates/docker-service-ansible.conf.j2 @@ -0,0 +1,5 @@ +[Unit] +After=local-fs.target +{% if docker_sssd.stat.exists %} +After=sssd.service +{% endif %} diff --git a/roles/docker_compose/defaults/main.yml b/roles/docker_compose/defaults/main.yml new file mode 100644 index 0000000..ebc12b8 --- /dev/null +++ b/roles/docker_compose/defaults/main.yml @@ -0,0 +1,4 @@ +--- + +docker_compose_version: 1.29.2 +docker_compose_bin_sha256: f3f10cf3dbb8107e9ba2ea5f23c1d2159ff7321d16f0a23051d68d8e2547b323 diff --git a/roles/docker_compose/tasks/main.yml b/roles/docker_compose/tasks/main.yml new file mode 100644 index 0000000..712db5b --- /dev/null +++ b/roles/docker_compose/tasks/main.yml @@ -0,0 +1,28 @@ +--- + +- name: Check if docker-compose is installed + stat: path=/usr/local/bin/docker-compose + register: docker_compose_bin + tags: docker + +- name: Detect docker-compose version + shell: docker-compose -v | perl -ne '/version (\d+(\.\d+)+),/ && print "$1\n"' + register: docker_compose_current_version + changed_when: False + when: docker_compose_bin.stat.exists + tags: docker + +- name: Remove docker-compose + file: path=/usr/local/bin/docker-compose state=absent + when: docker_compose_bin.stat.exists and docker_compose_current_version.stdout != docker_compose_version + tags: docker + +- name: Install docker-compose + get_url: + url: https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64 + dest: /usr/local/bin/docker-compose + mode: 0755 + checksum: sha256:{{ docker_compose_bin_sha256 }} + environment: + - https_proxy: "{{ system_proxy | default('') }}" + tags: docker diff --git a/roles/docker_volume_local_persist/defaults/main.yml b/roles/docker_volume_local_persist/defaults/main.yml new file mode 100644 index 0000000..28196c6 --- /dev/null +++ b/roles/docker_volume_local_persist/defaults/main.yml @@ -0,0 +1,6 @@ +--- + +docker_local_persist_version: 1.3.0 +docker_local_persist_url: https://github.com/MatchbookLab/local-persist/releases/download/v{{ docker_local_persist_version }}/local-persist-linux-amd64 +docker_local_persist_sha1: 41a2169525575da40695451f95cfc5f2c314ab6d + diff --git a/roles/docker_volume_local_persist/handlers/main.yml b/roles/docker_volume_local_persist/handlers/main.yml new file mode 100644 index 0000000..4926787 --- /dev/null +++ b/roles/docker_volume_local_persist/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- name: restart docker-volume-local-persist + service: name=docker-volume-local-persist state=restarted + when: not docker_local_persist_started.changed diff --git a/roles/docker_volume_local_persist/meta/main.yml b/roles/docker_volume_local_persist/meta/main.yml new file mode 100644 index 0000000..dc58dfa --- /dev/null +++ b/roles/docker_volume_local_persist/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: mkdir diff --git a/roles/docker_volume_local_persist/tasks/main.yml b/roles/docker_volume_local_persist/tasks/main.yml new file mode 100644 index 0000000..78d873f --- /dev/null +++ b/roles/docker_volume_local_persist/tasks/main.yml @@ -0,0 +1,40 @@ +--- + +- name: Download binary + get_url: + url: "{{ docker_local_persist_url }}" + dest: /usr/local/bin/docker-volume-local-persist + mode: 0755 + checksum: sha1:{{ docker_local_persist_sha1 }} + tags: docker + +- name: Create needed dir + file: path={{ item.dir }} state=directory mode={{ item.mode }} + loop: + - dir: /opt/docker + mode: "755" + - dir: /opt/docker/plugin-data + mode: "700" + - dir: /var/lib/docker + mode: "755" + tags: docker + +- name: Link plugin-data + file: src=/opt/docker/plugin-data dest=/var/lib/docker/plugin-data state=link + tags: docker + +- name: Install systemd unit + template: src=docker-volume-local-persist.service.j2 dest=/etc/systemd/system/docker-volume-local-persist.service + register: docker_local_persist_unit + notify: restart docker-volume-local-persist + tags: docker + +- name: Reload systemd + systemd: daemon_reload=True + when: docker_local_persist_unit.changed + tags: docker + +- name: Start and enable the service + service: name=docker-volume-local-persist state=started enabled=True + register: docker_local_persist_started + tags: docker diff --git a/roles/docker_volume_local_persist/templates/docker-volume-local-persist.service.j2 b/roles/docker_volume_local_persist/templates/docker-volume-local-persist.service.j2 new file mode 100644 index 0000000..232955e --- /dev/null +++ b/roles/docker_volume_local_persist/templates/docker-volume-local-persist.service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Create named local volumes that persist in the location(s) you want +Before=docker.service +Wants=docker.service + +[Service] +TimeoutStartSec=0 +ExecStart=/usr/local/bin/docker-volume-local-persist + +[Install] +WantedBy=multi-user.target diff --git a/roles/documize/defaults/main.yml b/roles/documize/defaults/main.yml new file mode 100644 index 0000000..0ec56e8 --- /dev/null +++ b/roles/documize/defaults/main.yml @@ -0,0 +1,35 @@ +--- + +# Version of cocumize to deploy +documize_version: 4.1.1 +# URL of the binary to install +documize_bin_url: https://github.com/documize/community/releases/download/v{{ documize_version }}/documize-community-linux-amd64 +# Expected sha1 of the binary +documize_bin_sha1: 7362cb0b0479b1315399df86fabef81aa1a43124 + +# Should documize handle upgrades or only initial install ? +documize_manage_upgrade: True + +# Root directory where documize will be installed +documize_root_dir: /opt/documize + +# User under which documize will run +documize_user: documize + +# port on which documize will listen +documize_port: 5001 + +# List of IP / CIDR allowed to access documize port +documize_src_ip: [] + +# Database settings +documize_db_engine: 'mysql' +documize_db_server: "{{ (documize_db_engine == 'postgres') | ternary(pg_server,mysql_server) | default('localhost') }}" +documize_db_port: "{{ (documize_db_engine == 'postgres') | ternary('5432','3306') }}" +documize_db_user: documize +documize_db_name: documize +# If password is not defined, a random one will be generated and stored in meta/ansible_dbpass +# documize_db_pass: S3Cr3t. + +# Salt for documize. A random one will be generated if not defined +# documize_salt: tsu3Acndky8cdTNx3 diff --git a/roles/documize/handlers/main.yml b/roles/documize/handlers/main.yml new file mode 100644 index 0000000..4ad147b --- /dev/null +++ b/roles/documize/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- name: restart documize + service: name=documize state=restarted + when: not documize_started.changed diff --git a/roles/documize/meta/main.yml b/roles/documize/meta/main.yml new file mode 100644 index 0000000..9eac46c --- /dev/null +++ b/roles/documize/meta/main.yml @@ -0,0 +1,8 @@ +--- + +allow_duplicates: True +dependencies: + - role: mysql_server + when: documize_db_engine == 'mysql' and documize_db_server in ['127.0.0.1','localhost'] + - role: postgresql_server + when: documize_db_engine == 'postgres' and documize_db_server in ['127.0.0.1','localhost'] diff --git a/roles/documize/tasks/archive_post.yml b/roles/documize/tasks/archive_post.yml new file mode 100644 index 0000000..cc23f14 --- /dev/null +++ b/roles/documize/tasks/archive_post.yml @@ -0,0 +1,10 @@ +--- + +- name: Compress previous version + command: tar cf {{ documize_root_dir }}/archives/{{ documize_current_version }}.tar.zst --use-compress-program=zstd ./ + args: + chdir: "{{ documize_root_dir }}/archives/{{ documize_current_version }}" + warn: False + environment: + ZSTD_CLEVEL: 10 + tags: documize diff --git a/roles/documize/tasks/archive_pre.yml b/roles/documize/tasks/archive_pre.yml new file mode 100644 index 0000000..6631278 --- /dev/null +++ b/roles/documize/tasks/archive_pre.yml @@ -0,0 +1,41 @@ +--- + +- name: Create the archive dir + file: path={{ documize_root_dir }}/archives/{{ documize_current_version }} state=directory + tags: documize + +- name: Backup previous version + copy: src={{ documize_root_dir }}/bin/documize dest={{ documize_root_dir }}/archives/{{ documize_current_version }}/ remote_src=True + tags: documize + +- name: Backup the database + command: > + /usr/pgsql-14/bin/pg_dump + --clean + --create + --host={{ documize_db_server }} + --port={{ documize_db_port }} + --username={{ documize_db_user }} + {{ documize_db_name }} + --file={{ documize_root_dir }}/archives/{{ documize_current_version }}/{{ documize_db_name }}.sql + environment: + - PGPASSWORD: "{{ documize_db_pass }}" + when: documize_db_engine == 'postgres' + tags: documize + +- name: Archive the database + mysql_db: + state: dump + name: "{{ documize_db_name }}" + target: "{{ documize_root_dir }}/archives/{{ documize_current_version }}/{{ documize_db_name }}.sql.xz" + login_host: "{{ documize_db_server | default(mysql_server) }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + quick: True + single_transaction: True + environment: + XZ_OPT: -T0 + when: documize_db_engine == 'mysql' + tags: documize + + diff --git a/roles/documize/tasks/cleanup.yml b/roles/documize/tasks/cleanup.yml new file mode 100644 index 0000000..5f2933e --- /dev/null +++ b/roles/documize/tasks/cleanup.yml @@ -0,0 +1,7 @@ +--- + +- name: Remove tmp and obsolete files + file: path={{ item }} state=absent + loop: + - "{{ documize_root_dir }}/archives/{{ documize_current_version }}" + tags: documize diff --git a/roles/documize/tasks/conf.yml b/roles/documize/tasks/conf.yml new file mode 100644 index 0000000..dee1305 --- /dev/null +++ b/roles/documize/tasks/conf.yml @@ -0,0 +1,6 @@ +--- + +- name: Deploy documize configuration + template: src=documize.conf.j2 dest={{ documize_root_dir }}/etc/documize.conf group={{ documize_user }} mode=640 + notify: restart documize + tags: documize diff --git a/roles/documize/tasks/directories.yml b/roles/documize/tasks/directories.yml new file mode 100644 index 0000000..887ed81 --- /dev/null +++ b/roles/documize/tasks/directories.yml @@ -0,0 +1,20 @@ +--- + +- name: Create needed directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: "{{ documize_root_dir }}" + - dir: "{{ documize_root_dir }}/tmp" + group: "{{ documize_user }}" + mode: 770 + - dir: "{{ documize_root_dir }}/bin" + - dir: "{{ documize_root_dir }}/etc" + group: "{{ documize_user }}" + mode: 750 + - dir: "{{ documize_root_dir }}/meta" + mode: 700 + - dir: "{{ documize_root_dir }}/backup" + mode: 700 + - dir: "{{ documize_root_dir }}/archives" + mode: 700 + tags: documize diff --git a/roles/documize/tasks/facts.yml b/roles/documize/tasks/facts.yml new file mode 100644 index 0000000..d4e0c93 --- /dev/null +++ b/roles/documize/tasks/facts.yml @@ -0,0 +1,33 @@ +--- + +# Detect installed version (if any) +- block: + - import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ documize_root_dir }}" + - version: "{{ documize_version }}" + - set_fact: documize_install_mode={{ (install_mode == 'upgrade' and not documize_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: documize_current_version={{ current_version | default('') }} + tags: documize + +# Create a random pass for the DB if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ documize_root_dir }}/meta/ansible_db_pass" + - complex: False + - set_fact: documize_db_pass={{ rand_pass }} + when: documize_db_pass is not defined + tags: documize + +# Create a random salt if needed +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ documize_root_dir }}/meta/ansible_salt" + - complex: False + - pass_size: 17 + - set_fact: documize_salt={{ rand_pass }} + when: documize_salt is not defined + tags: documize + diff --git a/roles/documize/tasks/install.yml b/roles/documize/tasks/install.yml new file mode 100644 index 0000000..70f9e08 --- /dev/null +++ b/roles/documize/tasks/install.yml @@ -0,0 +1,72 @@ +--- + +- name: Install needed tools + package: + name: + - tar + - zstd + - postgresql14 + tags: documize + +- name: Download documize + get_url: + url: "{{ documize_bin_url }}" + dest: "{{ documize_root_dir }}/bin/documize" + checksum: sha1:{{ documize_bin_sha1 }} + mode: 755 + when: documize_install_mode != 'none' + notify: restart documize + tags: documize + +- name: Install systemd unit + template: src=documize.service.j2 dest=/etc/systemd/system/documize.service + notify: restart documize + register: documize_unit + tags: documize + +- name: Reload systemd + systemd: daemon_reload=True + when: documize_unit.changed + tags: documize + +- when: documize_db_engine == 'postgres' + block: + - name: Create the PostgreSQL role + postgresql_user: + db: postgres + name: "{{ miniflux_db_user }}" + password: "{{ miniflux_db_pass }}" + login_host: "{{ miniflux_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + + - name: Create the PostgreSQL database + postgresql_db: + name: "{{ miniflux_db_name }}" + encoding: UTF-8 + lc_collate: C + lc_ctype: C + template: template0 + owner: "{{ miniflux_db_user }}" + login_host: "{{ miniflux_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + + tags: miniflux + + # Create MySQL database +- when: documize_db_engine == 'mysql' + import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ documize_db_name }}" + - db_user: "{{ documize_db_user }}" + - db_server: "{{ documize_db_server }}" + - db_pass: "{{ documize_db_pass }}" + tags: documize + +- name: Deploy backup hooks + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/documize mode=700 + loop: + - pre + - post + tags: documize diff --git a/roles/documize/tasks/iptables.yml b/roles/documize/tasks/iptables.yml new file mode 100644 index 0000000..16b927a --- /dev/null +++ b/roles/documize/tasks/iptables.yml @@ -0,0 +1,8 @@ +--- + +- name: Handle documize port in the firewall + iptables_raw: + name: documize_port + state: "{{ (documize_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ documize_port }} -s {{ documize_src_ip | join(',') }} -j ACCEPT" + tags: firewall,documize diff --git a/roles/documize/tasks/main.yml b/roles/documize/tasks/main.yml new file mode 100644 index 0000000..14d37ac --- /dev/null +++ b/roles/documize/tasks/main.yml @@ -0,0 +1,16 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: documize_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: write_version.yml +- include: archive_post.yml + when: documize_install_mode == 'upgrade' +- include: cleanup.yml diff --git a/roles/documize/tasks/services.yml b/roles/documize/tasks/services.yml new file mode 100644 index 0000000..9caf56f --- /dev/null +++ b/roles/documize/tasks/services.yml @@ -0,0 +1,7 @@ +--- + +- name: Start and enable the service + service: name=documize state=started enabled=True + register: documize_started + tags: documize + diff --git a/roles/documize/tasks/user.yml b/roles/documize/tasks/user.yml new file mode 100644 index 0000000..53e17ac --- /dev/null +++ b/roles/documize/tasks/user.yml @@ -0,0 +1,5 @@ +--- + +- name: Create user account + user: name={{ documize_user }} system=True shell=/sbin/nologin home={{ documize_root_dir }} + tags: documize diff --git a/roles/documize/tasks/write_version.yml b/roles/documize/tasks/write_version.yml new file mode 100644 index 0000000..0a10a51 --- /dev/null +++ b/roles/documize/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write installed version + copy: content={{ documize_version }} dest={{ documize_root_dir }}/meta/ansible_version + tags: documize diff --git a/roles/documize/templates/documize.conf.j2 b/roles/documize/templates/documize.conf.j2 new file mode 100644 index 0000000..676d0fe --- /dev/null +++ b/roles/documize/templates/documize.conf.j2 @@ -0,0 +1,15 @@ +[http] +port = {{ documize_port }} + +[database] +{% if documize_db_engine == 'mysql' %} +type = "mysql" +connection = "{{ documize_db_user }}:{{ documize_db_pass }}@tcp({{ documize_db_server }}:{{ documize_db_port }})/{{ documize_db_name }}" +{% elif documize_db_engine == 'postgres' %} +type = "postgresql" +connection = "host={{ documize_db_server }} port={{ documize_db_port }} dbname={{ documize_db_name }} user={{ documize_db_user }} password={{ documize_db_pass }} sslmode=disable" +{% endif %} +salt = "{{ documize_salt }}" + +[install] +location = "selfhost" diff --git a/roles/documize/templates/documize.service.j2 b/roles/documize/templates/documize.service.j2 new file mode 100644 index 0000000..aa2ef8c --- /dev/null +++ b/roles/documize/templates/documize.service.j2 @@ -0,0 +1,24 @@ +[Unit] +Description=Documize Documentation Manager +After=network.target postgresql.service mariadb.service + +[Service] +Type=simple +User={{ documize_user }} +ExecStart={{ documize_root_dir }}/bin/documize {{ documize_root_dir }}/etc/documize.conf +WorkingDirectory={{ documize_root_dir }}/tmp +Restart=always +NoNewPrivileges=true +PrivateDevices=true +ProtectControlGroups=true +ProtectHome=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectSystem=strict +RestrictRealtime=true +ReadWritePaths=/run +PrivateTmp=true + +[Install] +WantedBy=multi-user.target + diff --git a/roles/documize/templates/post-backup.j2 b/roles/documize/templates/post-backup.j2 new file mode 100644 index 0000000..4c1304a --- /dev/null +++ b/roles/documize/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ documize_root_dir }}/backup/* diff --git a/roles/documize/templates/pre-backup.j2 b/roles/documize/templates/pre-backup.j2 new file mode 100644 index 0000000..e82ec85 --- /dev/null +++ b/roles/documize/templates/pre-backup.j2 @@ -0,0 +1,26 @@ +#!/bin/sh + +set -eo pipefail + +{% if documize_db_engine == 'mysql' %} +/usr/bin/mysqldump \ +{% if documize_db_server not in ['127.0.0.1','localhost'] %} + --user={{ documize_db_user | quote }} \ + --password={{ documize_db_pass | quote }} \ + --host={{ documize_db_server | quote }} \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ documize_db_name | quote }} | zstd -c > "{{ documize_root_dir }}/backup/{{ documize_db_name }}.sql.zst" +{% elif documize_db_engine == 'postgres' %} +{% if documize_db_server not in ['127.0.0.1','localhost'] %} +PGPASSWORD={{ documize_db_pass | quote }} /usr/pgsql-14/bin/pg_dump \ + --clean \ + --create \ + --username={{ documize_db_user | quote }} \ + --host={{ documize_db_server | quote }} \ + {{ documize_db_name | quote }} | \ +{% else %} +su - postgres -c "/usr/pgsql-14/bin/pg_dump --clean --create {{ documize_db_name | quote }}" | \ +{% endif %} + zstd -c > "{{ documize_root_dir }}/backup/{{ documize_db_name }}.sql.zst" +{% endif %} diff --git a/roles/dokuwiki/defaults/main.yml b/roles/dokuwiki/defaults/main.yml new file mode 100644 index 0000000..db6a2bf --- /dev/null +++ b/roles/dokuwiki/defaults/main.yml @@ -0,0 +1,204 @@ +--- + +# A unique ID for this instance. You can deploy several dokuwiki instances on the same machine +dokuwiki_id: 1 + +# Version to deploy +dokuwiki_version: 2020-07-29 +# The sha1 checksum of the archive +dokuwiki_archive_sha1: 119f3875d023d15070068a6aca1e23acd7f9a19a + +# Root dir where the app will be installed. Each instance must have a different install path +dokuwiki_root_dir: /opt/dokuwiki_{{ dokuwiki_id }} + +# Should upgrades be handled by ansible +dokuwiki_manage_upgrade: True + +# The URL to download dokuwiki archive +dokuwiki_archive_url: https://download.dokuwiki.org/src/dokuwiki/dokuwiki-{{ dokuwiki_version }}.tgz + +# The user account under which PHP is executed +dokuwiki_php_user: php-dokuwiki_{{ dokuwiki_id }} + +dokuwiki_php_version: 74 + +# The name of the PHP-FPM pool to use +# dokuwiki_php_fpm_pool: php70 + +# List of default DokuWiki plugins +dokuwiki_plugins: + todo: + archive_name: dokuwiki-plugin-todo-stable.zip + url: https://github.com/leibler/dokuwiki-plugin-todo/archive/stable.zip + note: + archive_name: dokuwiki_note-master.zip + url: https://github.com/LarsGit223/dokuwiki_note/archive/master.zip + odt: + archive_name: dokuwiki-plugin-odt-master.zip + url: https://github.com/LarsGit223/dokuwiki-plugin-odt/archive/master.zip + dw2pdf: + archive_name: dokuwiki-plugin-dw2pdf-master.zip + url: https://github.com/splitbrain/dokuwiki-plugin-dw2pdf/archive/master.zip + color: + archive_name: dokuwiki_plugin_color-master.zip + url: https://github.com/leeyc0/dokuwiki_plugin_color/archive/master.zip + hidden: + archive_name: hidden-master.zip + url: https://github.com/gturri/hidden/archive/master.zip + encryptedpasswords: + archive_name: dw-plugin-encryptedpasswords-master.zip + url: https://github.com/ssahara/dw-plugin-encryptedpasswords/archive/master.zip + tag: + archive_name: plugin-tag-master.zip + url: https://github.com/dokufreaks/plugin-tag/archive/master.zip + pagelist: + archive_name: plugin-pagelist-master.zip + url: https://github.com/dokufreaks/plugin-pagelist/archive/master.zip + nspages: + archive_name: nspages-master.zip + url: https://github.com/gturri/nspages/archive/master.zip + changes: + archive_name: changes-master.zip + url: https://github.com/cosmocode/changes/archive/master.zip + pagemove: + archive_name: DokuWiki-Pagemove-Plugin-master.zip + url: https://github.com/desolat/DokuWiki-Pagemove-Plugin/archive/master.zip + loglog: + archive_name: dokuwiki-plugin-loglog-master.zip + url: https://github.com/splitbrain/dokuwiki-plugin-loglog/archive/master.zip + ckgdoku: + archive_name: ckgdoku-master.zip + url: https://github.com/turnermm/ckgdoku/archive/master.zip + ckgedit: + archive_name: ckgedit-master.zip + url: https://github.com/turnermm/ckgedit/archive/master.zip + edittable: + archive_name: edittable-master.zip + url: https://github.com/cosmocode/edittable/archive/master.zip + sortablejs: + archive_name: sortablejs-master.zip + url: https://github.com/FyiurAmron/sortablejs/archive/master.zip + howhard: + archive_name: howhard-master.zip + url: https://github.com/chtiland/howhard/archive/master.zip + indexmenu: + url: https://github.com/samuelet/indexmenu/archive/master.zip + archive_name: indexmenu-master.zip + discussion: + url: https://github.com/dokufreaks/plugin-discussion/archive/master.zip + archive_name: plugin-discussion-master.zip + piwik2: + url: https://github.com/Bravehartk2/dokuwiki-piwik2/archive/master.zip + archive_name: dokuwiki-piwik2-master.zip + authorstats: + url: https://github.com/ConX/dokuwiki-plugin-authorstats/archive/master.zip + archive_name: dokuwiki-plugin-authorstats-master.zip + gallery: + url: https://github.com/splitbrain/dokuwiki-plugin-gallery/archive/master.zip + archive_name: dokuwiki-plugin-gallery-master.zip + custombuttons: + url: https://github.com/ConX/dokuwiki-plugin-custombuttons/archive/master.zip + archive_name: dokuwiki-plugin-custombuttons-master.zip + include: + url: https://github.com/dokufreaks/plugin-include/archive/master.zip + archive_name: plugin-include-master.zip + blockquote: + url: https://github.com/dokufreaks/plugin-blockquote/archive/master.zip + archive_name: plugin-blockquote-master.zip + wrap: + url: https://github.com/selfthinker/dokuwiki_plugin_wrap/archive/master.zip + archive_name: dokuwiki_plugin_wrap-master.zip + bureaucracy: + url: https://github.com/splitbrain/dokuwiki-plugin-bureaucracy/archive/master.zip + archive_name: dokuwiki-plugin-bureaucracy-master.zip + struct: + url: https://github.com/cosmocode/dokuwiki-plugin-struct/archive/master.zip + archive_name: dokuwiki-plugin-struct-master.zip + bootstrap3: + url: https://github.com/LotarProject/dokuwiki-template-bootstrap3/archive/master.zip + archive_name: dokuwiki-template-bootstrap3-master.zip + type: tpl + material: + url: https://github.com/LeonStaufer/material-dokuwiki/archive/master.zip + archive_name: material-dokuwiki-master.zip + type: tpl + +# List of core plugins which won't be uninstalled +dokuwiki_core_plugins: + - acl + - authhttpldap + - authad + - authldap + - authmysql + - authpdo + - authpgsql + - authplain + - config + - extension + - info + - popularity + - revert + - safefnrecode + - styling + - usermanager + +# List of plugin to install +dokuwiki_base_plugins_to_install: + - edittable + - todo + - color + - hidden + - indexmenu + - odt + - dw2pdf + - loglog + - changes + - pagemove + - authorstats + - note +# An additional list, so you can just keep the default and add more if needed, in hosts_var +dokuwiki_extra_plugins_to_install: [] +dokuwiki_plugins_to_install: "{{ dokuwiki_base_plugins_to_install + dokuwiki_extra_plugins_to_install }}" + +# List of templates to install +dokuwiki_base_tpl_to_install: + - bootstrap3 + - material +dokuwiki_extra_tpl_to_install: [] +dokuwiki_tpl_to_install: "{{ dokuwiki_base_tpl_to_install + dokuwiki_extra_tpl_to_install }}" + +dokuwiki_remove_unmanaged_plugins: True +dokuwiki_remove_unmanaged_tpl: True + +# An alias for httpd config +# dokuwiki_alias: wiki + +# A list of ip address allowed to access dokuwiki +# dokuwiki_src_ip: +# - 192.168.7.0/24 +# - 10.99.0.0/16 + + +# Auth plugin. Can be authldap, authhttpldap, authplain +dokuwiki_auth: "{{ ad_auth | default(False) | ternary('authad', ldap_auth | default(False) | ternary('authhttpldap', 'authplain')) }}" + +# LDAP Auth settings +dokuwiki_ldap_uri: "{{ ldap_uri | default('ldap://ldap.' ~ ansible_domain) }}" +dokuwiki_ldap_starttls: True +dokuwiki_ldap_user_base: "{{ ldap_user_base | default('ou=Users') + ',' + ldap_base | default(ansible_domain | regex_replace('\\.',',dc=')) }}" +dokuwiki_ldap_group_base: "{{ ldap_group_base | default('ou=Groups') + ',' + ldap_base | default(ansible_domain | regex_replace('\\.',',dc=')) }}" +dokuwiki_ldap_user_filter: '(&(uid=%{user})(objectClass=inetOrgPerson))' +dokuwiki_ldap_group_filter: '(&(objectClass=posixGroup)(memberUid=%{user}))' +dokuwiki_ldap_group_key: cn +# dokuwiki_ldap_bind_dn: +# dokuwiki_ldap_bind_pass: + +# AD Settings +dokuwiki_ad_dc: "{{ ad_ldap_servers | default(ansible_domain) }}" +dokuwiki_ad_starttls: True +dokuwiki_ad_user_base: "{{ ad_ldap_user_search_base | default('DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC=')) }}" +dokuwiki_ad_domain: "{{ samba_realm | default(ansible_domain) }}" +# AD user. Do not use full DN notation, just simple login +# dokuwiki_ad_bind_user: +# dokuwiki_ad_bind_pass: +... diff --git a/roles/dokuwiki/files/authhttpldap/auth.php b/roles/dokuwiki/files/authhttpldap/auth.php new file mode 100644 index 0000000..7f48c71 --- /dev/null +++ b/roles/dokuwiki/files/authhttpldap/auth.php @@ -0,0 +1,63 @@ + + */ + +require(DOKU_PLUGIN."authldap/auth.php"); +class auth_plugin_authhttpldap extends auth_plugin_authldap { + /** + * Constructor + */ + public function __construct() { + parent::__construct(); + + // ldap extension is needed + if(!function_exists('ldap_connect')) { + $this->debug("LDAP err: PHP LDAP extension not found.", -1, __LINE__, __FILE__); + $this->success = false; + return; + } + $this->cando = array ( + 'addUser' => false, // can Users be created? + 'delUser' => false, // can Users be deleted? + 'modLogin' => false, // can login names be changed? + 'modPass' => false, // can passwords be changed? + 'modName' => false, // can real names be changed? + 'modMail' => false, // can emails be changed? + 'modGroups' => false, // can groups be changed? + 'getUsers' => true, // can a (filtered) list of users be retrieved? + 'getUserCount'=> false, // can the number of users be retrieved? + 'getGroups' => true, // can a list of available groups be retrieved? + 'external' => true, // does the module do external auth checking? + 'logout' => true, // can the user logout again? (eg. not possible with HTTP auth) + ); + } + + /** + * Check if REMOTE_USER is set + */ + function trustExternal($user,$pass,$sticky=false){ + global $USERINFO; + $success = false; + if (!isset($_SERVER['REMOTE_USER'])) return false; + $username = $_SERVER['REMOTE_USER']; + $this->debug('HTTP User Name: '.htmlspecialchars($username),0,__LINE__,__FILE__); + if (!empty($username)){ + $USERINFO = $this->getUserData($username,true); + if ($USERINFO !== false){ + $success = true; + $_SESSION[DOKU_COOKIE]['auth']['user'] = $username; + $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; + } + } + return $success; + } +} diff --git a/roles/dokuwiki/files/authhttpldap/plugin.info.txt b/roles/dokuwiki/files/authhttpldap/plugin.info.txt new file mode 100644 index 0000000..b61a020 --- /dev/null +++ b/roles/dokuwiki/files/authhttpldap/plugin.info.txt @@ -0,0 +1,7 @@ +base authhttpldap +author Daniel Berteaud +email daniel@firewall-services.com +date 2014-05-06 +name HTTP+LDAP auth plugin +desc This plugin uses a basic HTTP authentication, but LDAP to get info and authorization +url https://www.firewall-services.com diff --git a/roles/dokuwiki/handlers/main.yml b/roles/dokuwiki/handlers/main.yml new file mode 100644 index 0000000..c81cf5b --- /dev/null +++ b/roles/dokuwiki/handlers/main.yml @@ -0,0 +1,3 @@ +--- + +... diff --git a/roles/dokuwiki/meta/main.yml b/roles/dokuwiki/meta/main.yml new file mode 100644 index 0000000..5642b6d --- /dev/null +++ b/roles/dokuwiki/meta/main.yml @@ -0,0 +1,6 @@ +--- +allow_duplicates: true +dependencies: + - role: mkdir + - role: httpd_php +... diff --git a/roles/dokuwiki/tasks/filebeat.yml b/roles/dokuwiki/tasks/filebeat.yml new file mode 100644 index 0000000..51e2c8d --- /dev/null +++ b/roles/dokuwiki/tasks/filebeat.yml @@ -0,0 +1,5 @@ +--- + +- name: Deploy filebeat configuration + template: src=filebeat.yml.j2 dest=/etc/filebeat/ansible_inputs.d/dokuwiki_{{ dokuwiki_id }}.yml + tags: dokuwiki,log diff --git a/roles/dokuwiki/tasks/main.yml b/roles/dokuwiki/tasks/main.yml new file mode 100644 index 0000000..3248959 --- /dev/null +++ b/roles/dokuwiki/tasks/main.yml @@ -0,0 +1,393 @@ +--- + +- name: Set default install mode to none + set_fact: dokuwiki_install_mode="none" + tags: dokuwiki + +- name: Install dependencies + yum: + name: + - acl + tags: dokuwiki + +- name: Create PHP user acount + user: + name: "{{ dokuwiki_php_user }}" + comment: "PHP FPM for dokuwiki {{ dokuwiki_id }}" + system: yes + shell: /sbin/nologin + tags: dokuwiki + +- name: Check if dokuwiki is already installed + stat: path={{ dokuwiki_root_dir }}/meta/ansible_version + register: dokuwiki_version_file + changed_when: False + tags: dokuwiki + +- name: Check dokuwiki version + command: cat {{ dokuwiki_root_dir }}/meta/ansible_version + register: dokuwiki_current_version + changed_when: False + when: dokuwiki_version_file.stat.exists + tags: dokuwiki + +- name: Set installation process to install + set_fact: dokuwiki_install_mode='install' + when: not dokuwiki_version_file.stat.exists + tags: dokuwiki + +- name: Set installation process to upgrade + set_fact: dokuwiki_install_mode='upgrade' + when: + - dokuwiki_version_file.stat.exists + - dokuwiki_current_version.stdout | string != dokuwiki_version | string + - dokuwiki_manage_upgrade + tags: dokuwiki + +- name: Create archive dir + file: path={{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }} state=directory mode=700 + when: dokuwiki_install_mode == 'upgrade' + tags: dokuwiki + +- name: Prepare dokuwiki upgrade + synchronize: + src: "{{ dokuwiki_root_dir }}/web" + dest: "{{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: dokuwiki_install_mode == 'upgrade' + tags: dokuwiki + +- name: Create directory structure + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.groupe | default(omit) }} mode={{ item.mode | default(omit) }} + with_items: + - dir: "{{ dokuwiki_root_dir }}" + - dir: "{{ dokuwiki_root_dir }}/web" + - dir: "{{ dokuwiki_root_dir }}/tmp" + owner: "{{ dokuwiki_php_user }}" + mode: 700 + - dir: "{{ dokuwiki_root_dir }}/cache" + owner: "{{ dokuwiki_php_user }}" + mode: 700 + - dir: "{{ dokuwiki_root_dir }}/sessions" + owner: "{{ dokuwiki_php_user }}" + mode: 700 + - dir: "{{ dokuwiki_root_dir }}/data" + - dir: "{{ dokuwiki_root_dir }}/meta" + mode: 700 + - dir: "{{ dokuwiki_root_dir }}/web/conf/tpl" + group: "{{ dokuwiki_php_user }}" + mode: 770 + tags: dokuwiki + +- name: Download Dokuwiki + get_url: + url: "{{ dokuwiki_archive_url }}" + dest: "{{ dokuwiki_root_dir }}/tmp/" + checksum: "sha1:{{ dokuwiki_archive_sha1 }}" + when: dokuwiki_install_mode != 'none' + tags: dokuwiki + +- name: Extract dokuwiki archive + unarchive: + src: "{{ dokuwiki_root_dir }}/tmp/dokuwiki-{{ dokuwiki_version }}.tgz" + dest: "{{ dokuwiki_root_dir }}/tmp/" + remote_src: yes + when: dokuwiki_install_mode != 'none' + tags: dokuwiki + +- name: Move the content of dokuwiki to the correct top directory + synchronize: + src: "{{ dokuwiki_root_dir }}/tmp/dokuwiki-{{ dokuwiki_version }}/" + dest: "{{ dokuwiki_root_dir }}/web/" + recursive: True + delete: True + rsync_opts: + - '--exclude=data/' + delegate_to: "{{ inventory_hostname }}" + when: dokuwiki_install_mode != 'none' + tags: dokuwiki + +- name: Populate the data dir + synchronize: + src: "{{ dokuwiki_root_dir }}/tmp/dokuwiki-{{ dokuwiki_version }}/data/" + dest: "{{ dokuwiki_root_dir }}/data/" + recursive: True + delegate_to: "{{ inventory_hostname }}" + when: dokuwiki_install_mode != 'none' + tags: dokuwiki + +- name: Check existing conf to restore + stat: path={{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/web/{{ item }} + with_items: + - conf/local.php + - conf/acl.auth.php + - conf/users.auth.php + - conf/plugins.local.php + - conf/tpl/ + register: dokuwiki_conf_to_restore + when: dokuwiki_install_mode == 'upgrade' + tags: dokuwiki + +- name: Restore Configuration + synchronize: + src: "{{ item.stat.path }}" + dest: "{{ dokuwiki_root_dir }}/web/{{ item.item }}" + recursive: True + delegate_to: "{{ inventory_hostname }}" + with_items: "{{ dokuwiki_conf_to_restore.results }}" + when: + - dokuwiki_install_mode == 'upgrade' + - item.stat.exists + tags: dokuwiki + +- name: List previously installed plugins + shell: find {{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/web/lib/plugins -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: dokuwiki_current_plugins + when: + - dokuwiki_install_mode == 'upgrade' + - not dokuwiki_remove_unmanaged_plugins + tags: dokuwiki + +- name: Restore unmanaged previous plugins + synchronize: + src: "{{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/web/lib/plugins/{{ item }}" + dest: "{{ dokuwiki_root_dir }}/web/lib/plugins/" + recursive: True + delegate_to: "{{ inventory_hostname }}" + with_items: "{{ dokuwiki_current_plugins.stdout_lines }}" + when: + - dokuwiki_install_mode == 'upgrade' + - not dokuwiki_remove_unmanaged_plugins + tags: dokuwiki + +- name: List previously installed templates + shell: find {{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/web/lib/tpl -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: dokuwiki_current_tpl + when: + - dokuwiki_install_mode == 'upgrade' + - not dokuwiki_remove_unmanaged_tpl + tags: dokuwiki + +- name: Restore unmanaged previous templates + synchronize: + src: "{{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}/web/lib/tpl/{{ item }}" + dest: "{{ dokuwiki_root_dir }}/web/lib/tpl/" + recursive: True + delegate_to: "{{ inventory_hostname }}" + with_items: "{{ dokuwiki_current_tpl.stdout_lines }}" + when: + - dokuwiki_install_mode == 'upgrade' + - not dokuwiki_remove_unmanaged_tpl + tags: dokuwiki + +- name: Write dokuwiki version + copy: content={{ dokuwiki_version }} dest={{ dokuwiki_root_dir }}/meta/ansible_version + when: dokuwiki_install_mode != 'none' + tags: dokuwiki + +- name: Compress previous version + command: tar cJf {{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}.txz ./ + environment: + XZ_OPT: -T0 + args: + chdir: "{{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }}" + when: dokuwiki_install_mode == 'upgrade' + tags: dokuwiki + +- name: Remove archive directory + file: path={{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }} state=absent + when: dokuwiki_install_mode == 'upgrade' + tags: dokuwiki + +- name: Build a list of installed plugins + shell: find {{ dokuwiki_root_dir }}/web/lib/plugins -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: dokuwiki_installed_plugins + changed_when: False + tags: dokuwiki + +- name: Install authhttpldap plugin + copy: src=authhttpldap dest={{ dokuwiki_root_dir }}/web/lib/plugins + tags: dokuwiki + +- name: Download plugins + get_url: + url: "{{ dokuwiki_plugins[item].url }}" + dest: "{{ dokuwiki_root_dir }}/tmp/" + when: + - item not in dokuwiki_installed_plugins.stdout_lines + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'plugin' + with_items: "{{ dokuwiki_plugins_to_install }}" + tags: dokuwiki + +- name: Extract plugins + unarchive: + src: "{{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_name }}" + dest: "{{ dokuwiki_root_dir }}/tmp" + remote_src: yes + when: + - item not in dokuwiki_installed_plugins.stdout_lines + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'plugin' + with_items: "{{ dokuwiki_plugins_to_install }}" + tags: dokuwiki + +- name: Move plugins to the final dir + synchronize: + src: "{{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_dir | default(dokuwiki_plugins[item].archive_name | splitext | first) }}/" + dest: "{{ dokuwiki_root_dir }}/web/lib/plugins/{{ item }}" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: + - item not in dokuwiki_installed_plugins.stdout_lines + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'plugin' + with_items: "{{ dokuwiki_plugins_to_install }}" + tags: dokuwiki + +- name: Remove unmanaged plugins + file: path={{ dokuwiki_root_dir }}/web/lib/plugins/{{ item }} state=absent + with_items: "{{ dokuwiki_installed_plugins.stdout_lines }}" + when: + - item not in dokuwiki_plugins_to_install + - item not in dokuwiki_core_plugins + - dokuwiki_remove_unmanaged_plugins + tags: dokuwiki + +- name: Build a list of installed templates + shell: find {{ dokuwiki_root_dir }}/web/lib/tpl -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: dokuwiki_installed_tpl + changed_when: False + tags: dokuwiki + +- name: Download templates + get_url: + url: "{{ dokuwiki_plugins[item].url }}" + dest: "{{ dokuwiki_root_dir }}/tmp/" + when: + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'tpl' + - item not in dokuwiki_installed_tpl.stdout_lines | difference(['dokuwiki']) + with_items: "{{ dokuwiki_tpl_to_install }}" + tags: dokuwiki + +- name: Extract templates + unarchive: + src: "{{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_name }}" + dest: "{{ dokuwiki_root_dir }}/tmp" + remote_src: yes + when: + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'tpl' + - item not in dokuwiki_installed_tpl.stdout_lines | difference(['dokuwiki']) + with_items: "{{ dokuwiki_tpl_to_install }}" + tags: dokuwiki + +- name: Move templates to the final dir + synchronize: + src: "{{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_dir | default(dokuwiki_plugins[item].archive_name | splitext | first) }}/" + dest: "{{ dokuwiki_root_dir }}/web/lib/tpl/{{ item }}" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: + - dokuwiki_plugins[item] is defined + - dokuwiki_plugins[item].type | default('plugin') == 'tpl' + - item not in dokuwiki_installed_tpl.stdout_lines | difference(['dokuwiki']) + with_items: "{{ dokuwiki_tpl_to_install }}" + tags: dokuwiki + +- name: Remove unmanaged tpl + file: path={{ dokuwiki_root_dir }}/web/lib/tpl/{{ item }} state=absent + with_items: "{{ dokuwiki_installed_plugins.stdout_lines }}" + when: + - item not in dokuwiki_tpl_to_install + - item != 'dokuwiki' + - dokuwiki_remove_unmanaged_tpl + tags: dokuwiki + +- name: Remove temp files + file: path={{ dokuwiki_root_dir }}/tmp/{{ item }} state=absent + with_items: + - dokuwiki-{{ dokuwiki_version }} + - dokuwiki-{{ dokuwiki_version }}.tgz + tags: dokuwiki + +- name: Remove plugins archives + file: path={{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_name }} state=absent + when: dokuwiki_plugins[item] is defined + with_items: "{{ dokuwiki_plugins_to_install + dokuwiki_tpl_to_install }}" + tags: dokuwiki + +- name: Remove plugins temp files + file: path={{ dokuwiki_root_dir }}/tmp/{{ dokuwiki_plugins[item].archive_dir | default(dokuwiki_plugins[item].archive_name | splitext | first) }} state=absent + when: dokuwiki_plugins[item] is defined + with_items: "{{ dokuwiki_plugins_to_install + dokuwiki_tpl_to_install }}" + tags: dokuwiki + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ dokuwiki_root_dir }}/perms.sh mode=755 + tags: dokuwiki + +- name: Deploy httpd configuration + template: src=httpd.conf.j2 dest=/etc/httpd/ansible_conf.d/10-dokuwiki_{{ dokuwiki_id }}.conf + notify: reload httpd + tags: dokuwiki + +- name: Deploy php configuration + template: src=php.conf.j2 dest=/etc/opt/remi/php{{ dokuwiki_php_version }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf + notify: restart php-fpm + tags: dokuwiki + +- name: Remove PHP config from other versions + file: path=/etc/opt/remi/php{{ item }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf state=absent + with_items: "{{ httpd_php_versions | difference([ dokuwiki_php_version ]) }}" + notify: restart php-fpm + tags: dokuwiki + +- name: Remove PHP config (using a custom pool) + file: path=/etc/opt/remi/php{{ dokuwiki_php_version }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf state=absent + with_items: "{{ httpd_php_versions }}" + when: dokuwiki_php_fpm_pool is defined + notify: restart php-fpm + tags: dokuwiki + +- name: Deploy dokuwiki configuration + template: src={{ item }}.j2 dest={{ dokuwiki_root_dir }}/web/conf/{{ item }} owner=root group={{ dokuwiki_php_user }} mode=660 + with_items: + - local.protected.php + - plugins.protected.php + tags: dokuwiki + +- name: Check if local.php exists + stat: path={{ dokuwiki_root_dir }}/web/conf/local.php + register: dokuwiki_local_php + tags: dokuwiki + +- name: Set default values + template: src=local.php.j2 dest={{ dokuwiki_root_dir }}/web/conf/local.php + when: not dokuwiki_local_php.stat.exists + tags: dokuwiki + +- name: Deploy htaccess + template: src=htaccess.j2 dest={{ dokuwiki_root_dir }}/web/.htaccess + tags: dokuwiki + +- name: Set correct SElinux context + sefcontext: + target: "{{ dokuwiki_root_dir }}(/.*)?" + setype: httpd_sys_content_t + state: present + when: ansible_selinux.status == 'enabled' + tags: dokuwiki + +- name: Set optimal permissions + command: "{{ dokuwiki_root_dir }}/perms.sh" + changed_when: False + tags: dokuwiki + +- include: filebeat.yml +... diff --git a/roles/dokuwiki/templates/filebeat.yml.j2 b/roles/dokuwiki/templates/filebeat.yml.j2 new file mode 100644 index 0000000..73ea88c --- /dev/null +++ b/roles/dokuwiki/templates/filebeat.yml.j2 @@ -0,0 +1,7 @@ +- type: log + enabled: True + paths: + - {{ dokuwiki_root_dir }}/data/cache/loglog.log + exclude_files: + - '\.[gx]z$' + - '\d+$' diff --git a/roles/dokuwiki/templates/htaccess.j2 b/roles/dokuwiki/templates/htaccess.j2 new file mode 100644 index 0000000..fa591f6 --- /dev/null +++ b/roles/dokuwiki/templates/htaccess.j2 @@ -0,0 +1,17 @@ + + Require all denied + + + RedirectMatch 404 /\.git + + +RewriteEngine on +RewriteRule ^_media/(.*) lib/exe/fetch.php?media=$1 [QSA,L] +RewriteRule ^_detail/(.*) lib/exe/detail.php?media=$1 [QSA,L] +RewriteRule ^_export/([^/]+)/(.*) doku.php?do=export_$1&id=$2 [QSA,L] +RewriteRule ^$ doku.php [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule (.*) doku.php?id=$1 [QSA,L] +RewriteRule ^index.php$ doku.php + diff --git a/roles/dokuwiki/templates/httpd.conf.j2 b/roles/dokuwiki/templates/httpd.conf.j2 new file mode 100644 index 0000000..15a61dd --- /dev/null +++ b/roles/dokuwiki/templates/httpd.conf.j2 @@ -0,0 +1,41 @@ +{% if dokuwiki_alias is defined %} +Alias /{{ dokuwiki_alias }} {{ dokuwiki_root_dir }}/web +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + + + AllowOverride All + Options FollowSymLinks +{% if dokuwiki_src_ip is defined %} + Require ip {{ dokuwiki_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ dokuwiki_php_fpm_pool | default('dokuwiki_' + dokuwiki_id | string) }}.sock|fcgi://localhost" + + + + Require all denied + + +{% if httpd_src_ip is defined and httpd_src_ip | length > 0 and '0.0.0.0/0' not in httpd_src_ip and dokuwiki_auth == 'authhttpldap' %} + RewriteEngine On + RewriteCond %{HTTP:Auth-User} ^(\w+)$ + RewriteRule .* - [E=REMOTE_USER:%1] +{% endif %} + + + + Require all denied + + + Require all denied + + + Require all denied + + + Require all denied + diff --git a/roles/dokuwiki/templates/local.php.j2 b/roles/dokuwiki/templates/local.php.j2 new file mode 100644 index 0000000..af00a2b --- /dev/null +++ b/roles/dokuwiki/templates/local.php.j2 @@ -0,0 +1,14 @@ + diff --git a/roles/dokuwiki/templates/local.protected.php.j2 b/roles/dokuwiki/templates/local.protected.php.j2 new file mode 100644 index 0000000..d54f482 --- /dev/null +++ b/roles/dokuwiki/templates/local.protected.php.j2 @@ -0,0 +1,37 @@ + diff --git a/roles/dokuwiki/templates/perms.sh.j2 b/roles/dokuwiki/templates/perms.sh.j2 new file mode 100644 index 0000000..ef9598f --- /dev/null +++ b/roles/dokuwiki/templates/perms.sh.j2 @@ -0,0 +1,16 @@ +#!/bin/sh + +restorecon -R {{ dokuwiki_root_dir }} +chown root:root {{ dokuwiki_root_dir }} +chmod 700 {{ dokuwiki_root_dir }} +setfacl -k -b {{ dokuwiki_root_dir }} +setfacl -m u:{{ dokuwiki_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:rx {{ dokuwiki_root_dir }} +chown -R root:root {{ dokuwiki_root_dir }}/web +chown -R {{ dokuwiki_php_user | default('apache') }} {{ dokuwiki_root_dir }}/web/lib/{plugins,tpl} +chown -R {{ dokuwiki_php_user }} {{ dokuwiki_root_dir }}/{tmp,sessions,cache,data} +chmod 700 {{ dokuwiki_root_dir }}/{tmp,sessions,cache,data} +find {{ dokuwiki_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ dokuwiki_root_dir }}/web -type d -exec chmod 755 "{}" \; +chown -R :{{ dokuwiki_php_user }} {{ dokuwiki_root_dir }}/web/conf +find {{ dokuwiki_root_dir }}/web/conf -type f -exec chmod 660 "{}" \; +find {{ dokuwiki_root_dir }}/web/conf -type d -exec chmod 770 "{}" \; diff --git a/roles/dokuwiki/templates/php.conf.j2 b/roles/dokuwiki/templates/php.conf.j2 new file mode 100644 index 0000000..f3dc01b --- /dev/null +++ b/roles/dokuwiki/templates/php.conf.j2 @@ -0,0 +1,37 @@ +; {{ ansible_managed }} + +[dokuwiki_{{ dokuwiki_id }}] + +listen.owner = root +listen.group = {{ httpd_user | default('apache') }} +listen.mode = 0660 +listen = /run/php-fpm/dokuwiki_{{ dokuwiki_id }}.sock +user = {{ dokuwiki_php_user }} +group = {{ dokuwiki_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 60m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 256M +php_admin_value[session.save_path] = {{ dokuwiki_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ dokuwiki_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ dokuwiki_root_dir }}/tmp +php_admin_value[post_max_size] = 50M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[disable_functions] = system, show_source, symlink, exec, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ dokuwiki_root_dir }} +php_admin_value[max_execution_time] = 300 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = on +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/dokuwiki/templates/plugins.protected.php.j2 b/roles/dokuwiki/templates/plugins.protected.php.j2 new file mode 100644 index 0000000..a69023b --- /dev/null +++ b/roles/dokuwiki/templates/plugins.protected.php.j2 @@ -0,0 +1,3 @@ + + AllowOverride All + Options FollowSymLinks +{% if dolibarr_src_ip is defined %} + Require ip {{ dolibarr_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ dolibarr_php_fpm_pool | default('dolibarr_' + dolibarr_id | string) }}.sock|fcgi://localhost" + + diff --git a/roles/dolibarr/templates/logrotate.conf.j2 b/roles/dolibarr/templates/logrotate.conf.j2 new file mode 100644 index 0000000..dc4db04 --- /dev/null +++ b/roles/dolibarr/templates/logrotate.conf.j2 @@ -0,0 +1,7 @@ +{{ dolibarr_root_dir }}/data/*.log { + daily + rotate 90 + compress + missingok + create 640 {{ dolibarr_php_user }} {{ dolibarr_php_user }} +} diff --git a/roles/dolibarr/templates/perms.sh.j2 b/roles/dolibarr/templates/perms.sh.j2 new file mode 100644 index 0000000..37fb0bb --- /dev/null +++ b/roles/dolibarr/templates/perms.sh.j2 @@ -0,0 +1,21 @@ +#!/bin/sh + +restorecon -R {{ dolibarr_root_dir }} +chown root:root {{ dolibarr_root_dir }} +chmod 700 {{ dolibarr_root_dir }} +chown root:root {{ dolibarr_root_dir }}/{meta,db_dumps} +chmod 700 {{ dolibarr_root_dir }}/{meta,db_dumps} +setfacl -k -b {{ dolibarr_root_dir }} +setfacl -m u:{{ dolibarr_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:rx {{ dolibarr_root_dir }} +chown -R root:root {{ dolibarr_root_dir }}/web +chown -R {{ dolibarr_php_user }} {{ dolibarr_root_dir }}/{tmp,sessions,data} +chmod 700 {{ dolibarr_root_dir }}/{tmp,sessions,data} +setfacl -R -m u:{{ httpd_user | default('apache') }}:rX {{ dolibarr_root_dir }}/data +find {{ dolibarr_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ dolibarr_root_dir }}/web -type d -exec chmod 755 "{}" \; +chown -R :{{ dolibarr_php_user }} {{ dolibarr_root_dir }}/web/htdocs/{conf,custom} +chmod 770 {{ dolibarr_root_dir }}/web/htdocs/custom +setfacl -R -m u:{{ httpd_user | default('apache') }}:rX {{ dolibarr_root_dir }}/web/htdocs/custom +chmod 770 {{ dolibarr_root_dir }}/web/htdocs/conf +chmod 640 {{ dolibarr_root_dir }}/web/htdocs/conf/* +chmod 755 {{ dolibarr_root_dir }}/web/scripts/user/sync_ldap2dolibarr.sh diff --git a/roles/dolibarr/templates/php.conf.j2 b/roles/dolibarr/templates/php.conf.j2 new file mode 100644 index 0000000..ca051ff --- /dev/null +++ b/roles/dolibarr/templates/php.conf.j2 @@ -0,0 +1,37 @@ +; {{ ansible_managed }} + +[dolibarr_{{ dolibarr_id }}] + +listen.owner = root +listen.group = {{ httpd_user | default('apache') }} +listen.mode = 0660 +listen = /run/php-fpm/dolibarr_{{ dolibarr_id }}.sock +user = {{ dolibarr_php_user }} +group = {{ dolibarr_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 60m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 512M +php_admin_value[session.save_path] = {{ dolibarr_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ dolibarr_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ dolibarr_root_dir }}/tmp +php_admin_value[post_max_size] = 20M +php_admin_value[upload_max_filesize] = 20M +php_admin_value[disable_functions] = system, show_source, symlink, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ dolibarr_root_dir }} +php_admin_value[max_execution_time] = 900 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = on +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/dolibarr/templates/post-backup.j2 b/roles/dolibarr/templates/post-backup.j2 new file mode 100644 index 0000000..baa9778 --- /dev/null +++ b/roles/dolibarr/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ dolibarr_root_dir }}/db_dumps/* diff --git a/roles/dolibarr/templates/pre-backup.j2 b/roles/dolibarr/templates/pre-backup.j2 new file mode 100644 index 0000000..4c5a2f9 --- /dev/null +++ b/roles/dolibarr/templates/pre-backup.j2 @@ -0,0 +1,9 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump --user={{ dolibarr_db_user }} \ + --password={{ dolibarr_db_pass | quote }} \ + --host={{ dolibarr_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ dolibarr_db_name }} | zstd -c > {{ dolibarr_root_dir }}/db_dumps/{{ dolibarr_db_name }}.sql.zst diff --git a/roles/elasticsearch/defaults/main.yml b/roles/elasticsearch/defaults/main.yml new file mode 100644 index 0000000..6a92fce --- /dev/null +++ b/roles/elasticsearch/defaults/main.yml @@ -0,0 +1,14 @@ +--- + +# Name of the Elasticsearch cluster +es_cluster_name: elasticsearch +# Name of this ES node +es_node_name: "{{ inventory_hostname }}" +# Port on which ES will bind +es_port: 9200 +# List of IP/CIDR which will have access to es_port (if iptables_manage == True) +es_src_ip: [] +# Path where ES will store its data +es_data_dir: /opt/elasticsearch/data +# Path where ES will store snapshots for backups (created by pre-backup, removed by post-backup) +es_backup_dir: /opt/elasticsearch/dumps diff --git a/roles/elasticsearch/handlers/main.yml b/roles/elasticsearch/handlers/main.yml new file mode 100644 index 0000000..3c88137 --- /dev/null +++ b/roles/elasticsearch/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart elasticsearch + service: name=elasticsearch state=restarted diff --git a/roles/elasticsearch/meta/main.yml b/roles/elasticsearch/meta/main.yml new file mode 100644 index 0000000..b65d6f1 --- /dev/null +++ b/roles/elasticsearch/meta/main.yml @@ -0,0 +1,5 @@ +--- + +dependencies: + - role: repo_elasticsearch + - role: mkdir diff --git a/roles/elasticsearch/tasks/backup.yml b/roles/elasticsearch/tasks/backup.yml new file mode 100644 index 0000000..9f4774a --- /dev/null +++ b/roles/elasticsearch/tasks/backup.yml @@ -0,0 +1,18 @@ +--- + +- name: Declare repo in ElasticSearch + uri: + url: http://localhost:{{ es_port }}/_snapshot/lbkp + method: PUT + body: + type: fs + settings: + compress: True + location: "{{ es_backup_dir }}" + body_format: json + register: es_lbkp + until: es_lbkp.failed == False + retries: 10 + delay: 10 + tags: es + diff --git a/roles/elasticsearch/tasks/conf.yml b/roles/elasticsearch/tasks/conf.yml new file mode 100644 index 0000000..e432032 --- /dev/null +++ b/roles/elasticsearch/tasks/conf.yml @@ -0,0 +1,9 @@ +--- + +- name: Deploy configuration + template: src={{ item }}.j2 dest=/etc/elasticsearch/{{ item }} group=elasticsearch mode=660 + loop: + - elasticsearch.yml + - log4j2.properties + notify: restart elasticsearch + tags: es diff --git a/roles/elasticsearch/tasks/directories.yml b/roles/elasticsearch/tasks/directories.yml new file mode 100644 index 0000000..55449df --- /dev/null +++ b/roles/elasticsearch/tasks/directories.yml @@ -0,0 +1,14 @@ +--- + +- name: Ensure the data dir exists + file: path={{ es_data_dir }} state=directory + tags: es + + # We do it in two steps, so that parent dirs aren't created with restrictive permissions +- name: Restrict permissions on data dir + file: path={{ es_data_dir }} state=directory owner=elasticsearch group=elasticsearch mode=750 + tags: es + +- name: Create backup dir + file: path={{ es_backup_dir }} state=directory owner=elasticsearch group=elasticsearch mode=700 + tags: es diff --git a/roles/elasticsearch/tasks/install.yml b/roles/elasticsearch/tasks/install.yml new file mode 100644 index 0000000..ff7df13 --- /dev/null +++ b/roles/elasticsearch/tasks/install.yml @@ -0,0 +1,42 @@ +--- + +- name: Install needed packages + yum: + name: + - elasticsearch-oss + - java-1.8.0-openjdk-headless + tags: es + +- name: Deploy pre and post backup script + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/es mode=750 + loop: + - pre + - post + tags: es + +- name: Create systemd unit snippet dir + file: path=/etc/systemd/system/elasticsearch.service.d state=directory + tags: es + +- name: Customize systemd unit + copy: + content: | + [Service] + ProtectSystem=full + PrivateDevices=yes + ProtectHome=yes + NoNewPrivileges=yes + SyslogIdentifier=elasticsearch + Restart=on-failure + ExecStart= + ExecStart=/usr/share/elasticsearch/bin/elasticsearch -p ${PID_DIR}/elasticsearch.pid + dest: /etc/systemd/system/elasticsearch.service.d/ansible.conf + register: es_unit + notify: restart elasticsearch + tags: es + +- name: Reload systemd + systemd: daemon_reload=True + when: es_unit.changed + tags: es + diff --git a/roles/elasticsearch/tasks/iptables.yml b/roles/elasticsearch/tasks/iptables.yml new file mode 100644 index 0000000..d98afad --- /dev/null +++ b/roles/elasticsearch/tasks/iptables.yml @@ -0,0 +1,13 @@ +--- + +- name: Handle Elasticsearch port + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ item.port }} -s {{ item.src_ip | join(',') }} -j ACCEPT" + loop: + - port: "{{ es_port }}" + name: es_port + src_ip: "{{ es_src_ip }}" + tags: firewall,es + diff --git a/roles/elasticsearch/tasks/main.yml b/roles/elasticsearch/tasks/main.yml new file mode 100644 index 0000000..5959d45 --- /dev/null +++ b/roles/elasticsearch/tasks/main.yml @@ -0,0 +1,10 @@ +--- + +- include: install.yml +- include: directories.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: services.yml +- include: backup.yml + diff --git a/roles/elasticsearch/tasks/services.yml b/roles/elasticsearch/tasks/services.yml new file mode 100644 index 0000000..bc2842c --- /dev/null +++ b/roles/elasticsearch/tasks/services.yml @@ -0,0 +1,6 @@ +--- + +- name: Start and enable the service + service: name=elasticsearch state=started enabled=True + tags: es + diff --git a/roles/elasticsearch/templates/elasticsearch.yml.j2 b/roles/elasticsearch/templates/elasticsearch.yml.j2 new file mode 100644 index 0000000..8d13173 --- /dev/null +++ b/roles/elasticsearch/templates/elasticsearch.yml.j2 @@ -0,0 +1,11 @@ +cluster.name: {{ es_cluster_name }} +network.host: 0.0.0.0 +http.port: {{ es_port }} +node.name: {{ es_node_name }} +path.data: {{ es_data_dir }} +path.logs: /var/log/elasticsearch +path.repo: [ {{ es_backup_dir }} ] +action.auto_create_index: false +{% if es_major_version is defined and es_major_version is version('7','>=') %} +discovery.type: single-node +{% endif %} diff --git a/roles/elasticsearch/templates/log4j2.properties.j2 b/roles/elasticsearch/templates/log4j2.properties.j2 new file mode 100644 index 0000000..52cfcdc --- /dev/null +++ b/roles/elasticsearch/templates/log4j2.properties.j2 @@ -0,0 +1,28 @@ +status = error + +# log action execution errors for easier debugging +logger.action.name = org.elasticsearch.action +logger.action.level = debug + +appender.console.type = Console +appender.console.name = console +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%-5p][%-25c{1.}] %m%n + +rootLogger.level = info +rootLogger.appenderRef.console.ref = console + +logger.deprecation.name = org.elasticsearch.deprecation +logger.deprecation.level = warn +logger.deprecation.appenderRef.console.ref = console +logger.deprecation.additivity = false + +logger.index_search_slowlog_rolling.name = index.search.slowlog +logger.index_search_slowlog_rolling.level = trace +logger.index_search_slowlog_rolling.appenderRef.console.ref = console +logger.index_search_slowlog_rolling.additivity = false + +logger.index_indexing_slowlog.name = index.indexing.slowlog.index +logger.index_indexing_slowlog.level = trace +logger.index_indexing_slowlog.appenderRef.console.ref = console +logger.index_indexing_slowlog.additivity = false diff --git a/roles/elasticsearch/templates/post-backup.j2 b/roles/elasticsearch/templates/post-backup.j2 new file mode 100644 index 0000000..cc1bbfa --- /dev/null +++ b/roles/elasticsearch/templates/post-backup.j2 @@ -0,0 +1,5 @@ +#!/bin/bash -e + +curl -X DELETE http://localhost:{{ es_port }}/_snapshot/lbkp/lbkp +umount /home/lbkp/es +fstrim -a -v diff --git a/roles/elasticsearch/templates/pre-backup.j2 b/roles/elasticsearch/templates/pre-backup.j2 new file mode 100644 index 0000000..3e95f9c --- /dev/null +++ b/roles/elasticsearch/templates/pre-backup.j2 @@ -0,0 +1,7 @@ +#!/bin/sh + +set -eo pipefail + +mkdir -p /home/lbkp/es +mount -o bind,ro {{ es_backup_dir }} /home/lbkp/es +curl -X PUT http://localhost:{{ es_port }}/_snapshot/lbkp/lbkp?wait_for_completion=true diff --git a/roles/ethercalc/defaults/main.yml b/roles/ethercalc/defaults/main.yml new file mode 100644 index 0000000..d94e310 --- /dev/null +++ b/roles/ethercalc/defaults/main.yml @@ -0,0 +1,32 @@ +--- + +# A unique ID is needed for each ethercalc instance +# You can deploy several instances on the same machine +ethercalc_id: 1 + +# Path where ethercalc will be installed +# You also need to choose e different root_dir for each instance +ethercalc_root_dir: /opt/ethercalc_{{ ethercalc_id }} + +# User account/group under which the daemon will run. Will be created if needed +ethercalc_user: ethercalc_{{ ethercalc_id }} +ethercalc_group: "{{ ethercalc_user }}" + +# Port on which ethercalc should bind +ethercalc_port: 8000 + +# Optionally restrict source IP which will be allowed to connect +# Undefined or an empty list means no access. +ethercalc_src_ip: + - 0.0.0.0/0 + +# This is the time for which inactive calc will be kept +ethercalc_expire: 15552000 + +# URI of the git repository +ethercalc_git_uri: https://github.com/audreyt/ethercalc.git + +# Version to deploy +ethercalc_version: HEAD + +... diff --git a/roles/ethercalc/handlers/main.yml b/roles/ethercalc/handlers/main.yml new file mode 100644 index 0000000..feb9aef --- /dev/null +++ b/roles/ethercalc/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- include: ../common/handlers/main.yml +- name: restart ethercalc + service: name=ethercalc_{{ ethercalc_id }} state=restarted enabled=yes diff --git a/roles/ethercalc/meta/main.yml b/roles/ethercalc/meta/main.yml new file mode 100644 index 0000000..740c3a9 --- /dev/null +++ b/roles/ethercalc/meta/main.yml @@ -0,0 +1,4 @@ +--- +allow_duplicates: true +... + diff --git a/roles/ethercalc/tasks/main.yml b/roles/ethercalc/tasks/main.yml new file mode 100644 index 0000000..d9ba735 --- /dev/null +++ b/roles/ethercalc/tasks/main.yml @@ -0,0 +1,69 @@ +--- + +- name: Install dependencies + yum: + name: + - nodejs + - npm + - git + - gcc-c++ + - make + tags: ethercalc + +- name: Create user account + user: name={{ ethercalc_user }} home={{ ethercalc_root_dir }} shell=/bin/bash state=present + tags: ethercalc + +- name: Clone / Update Ethercalc repository + git: + repo: "{{ ethercalc_git_uri }}" + dest: "{{ ethercalc_root_dir }}/app" + version: "{{ ethercalc_version}}" + become_user: "{{ ethercalc_user }}" + register: ethercalc_git + notify: restart ethercalc + become: "{{ ethercalc_user }}" + tags: ethercalc + +- name: Install Ethercalc + command: npm i + args: + chdir: "{{ ethercalc_root_dir }}/app" + when: ethercalc_git.changed + tags: ethercalc + +- name: Install systemd unit + template: src=ethercalc.service.j2 dest=/etc/systemd/system/ethercalc_{{ ethercalc_id }}.service + notify: restart ethercalc + register: ethercalc_unit + tags: ethercalc + +- name: Reload systemd + systemd: daemon_reload=True + when: ethercalc_unit.changed + tags: ethercalc + +- name: Handle Ethercalc port + iptables_raw: + name: ethercalc_{{ ethercalc_id }}_port + state: "{{ (ethercalc_src_ip is defined and ethercalc_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ ethercalc_port }} -s {{ ethercalc_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: ethercalc,firewall + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ ethercalc_root_dir }}/key.txt" + tags: ethercalc +- set_fact: ethercalc_key={{ rand_pass }} + tags: ethercalc + +- name: Create the env file for systemd + template: src=env.j2 dest={{ ethercalc_root_dir }}/env owner={{ ethercalc_user }} mode=640 + tags: ethercalc + +- name: Start and enable the service + service: name=ethercalc_{{ ethercalc_id }} state=started enabled=True + tags: ethercalc + +... diff --git a/roles/ethercalc/templates/env.j2 b/roles/ethercalc/templates/env.j2 new file mode 100644 index 0000000..4c31862 --- /dev/null +++ b/roles/ethercalc/templates/env.j2 @@ -0,0 +1,2 @@ +KEY={{ ethercalc_key }} +NODE_ENV=production diff --git a/roles/ethercalc/templates/ethercalc.service.j2 b/roles/ethercalc/templates/ethercalc.service.j2 new file mode 100644 index 0000000..0e685e3 --- /dev/null +++ b/roles/ethercalc/templates/ethercalc.service.j2 @@ -0,0 +1,21 @@ +[Unit] +Description=Ethercalc ({{ ethercalc_id }} Instance) +After=syslog.target network.target redis.service + +[Service] +Type=simple +User={{ ethercalc_user }} +Group={{ ethercalc_group }} +ExecStart={{ ethercalc_root_dir }}/app/bin/ethercalc --host 0.0.0.0 --port {{ ethercalc_port }} --expire {{ ethercalc_expire }} --key=${KEY} +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=512M +SyslogIdentifier=ethercalc-{{ ethercalc_id }} +Restart=on-failure +EnvironmentFile={{ ethercalc_root_dir }}/env + +[Install] +WantedBy=multi-user.target diff --git a/roles/etherpad/defaults/main.yml b/roles/etherpad/defaults/main.yml new file mode 100644 index 0000000..822e2e3 --- /dev/null +++ b/roles/etherpad/defaults/main.yml @@ -0,0 +1,47 @@ +--- + +# You can install several instances of etherpad on the same server +# They should each get their own ID and port +etherpad_id: 1 +# Where etherpad will be installed +etherpad_root_dir: /opt/etherpad_{{ etherpad_id }} +# Unix account under which etherpad will run. The user will be created if it doesn't exist +etherpad_user: etherpad_{{ etherpad_id }} +# Version to deploy +etherpad_version: 1.8.16 +# URL from where the archive will be downloaded +etherpad_archive_url: https://github.com/ether/etherpad-lite/archive/{{ etherpad_version }}.tar.gz +# Expected sha1 of the archive, to check the download were OK +etherpad_archive_sha1: 048801cdcf597a1b3b14c7ef560daa839e836435 +# Port on which the service will listen +etherpad_port: 9003 +# List of IP/CIDR for which the port will be opened (if iptables_manage == True) +etherpad_src_ip: [] + +# Etherpad uses a MySQL compatible database +etherpad_db_name: etherpad_{{ etherpad_id }} +etherpad_db_user: etherpad_{{ etherpad_id }} +etherpad_db_port: 3306 +etherpad_db_server: "{{mysql_server | default('localhost') }}" +# A random one is generated if not defined, and stored under {{ etherpad_root_dir }}/meta/ansible_dbpass +# etherpad_db_pass: s3cr3t. + +# Page title +etherpad_title: Etherpad +# Default theme +etherpad_theme: colibris + +# Admin password +# A random one will be created if not defined, and stored under {{ etherpad_root_dir }}/meta/ansible_admin_pass +# etherpad_admin_pass: p@ssw0rd +# The API Key +# A random one will be created if not defined, and stored under {{ etherpad_root_dir }}/meta/ansible_api_key +# etherpad_api_key: 123456 + +# List of plugins to install +etherpad_plugins_base: + - adminpads + - delete_after_delay + - delete_empty_pads +etherpad_plugins_extra: [] +etherpad_plugins: "{{ etherpad_plugins_base + etherpad_plugins_extra }}" diff --git a/roles/etherpad/handlers/main.yml b/roles/etherpad/handlers/main.yml new file mode 100644 index 0000000..9d0cbcb --- /dev/null +++ b/roles/etherpad/handlers/main.yml @@ -0,0 +1,6 @@ +--- + +- name: restart etherpad + service: name=etherpad_{{ etherpad_id }} state=restarted + when: not etherpad_started.changed + diff --git a/roles/etherpad/meta/main.yml b/roles/etherpad/meta/main.yml new file mode 100644 index 0000000..20b5b67 --- /dev/null +++ b/roles/etherpad/meta/main.yml @@ -0,0 +1,6 @@ +--- + +dependencies: + - role: repo_nodejs + - role: mysql_server + when: etherpad_db_server in ['localhost','127.0.0.1'] diff --git a/roles/etherpad/tasks/archive_post.yml b/roles/etherpad/tasks/archive_post.yml new file mode 100644 index 0000000..bcf6302 --- /dev/null +++ b/roles/etherpad/tasks/archive_post.yml @@ -0,0 +1,9 @@ +--- + +- import_tasks: ../includes/webapps_compress_archive.yml + vars: + - root_dir: "{{ etherpad_root_dir }}" + - version: "{{ current_version }}" + when: install_mode == 'upgrade' + tags: etherpad + diff --git a/roles/etherpad/tasks/archive_pre.yml b/roles/etherpad/tasks/archive_pre.yml new file mode 100644 index 0000000..b4b9b36 --- /dev/null +++ b/roles/etherpad/tasks/archive_pre.yml @@ -0,0 +1,28 @@ +--- + +- name: Create archive dir + file: path={{ etherpad_root_dir }}/archives/{{ etherpad_current_version }} state=directory mode=700 + tags: etherpad + +- name: Archive previous version + synchronize: + src: "{{ etherpad_root_dir }}/{{ etherpad_web_dir.stat.exists | ternary('web','app') }}" # previous versions were installed in the web subdir, now in app) + dest: "{{ etherpad_root_dir }}/archives/{{ etherpad_current_version }}/" + compress: False + delete: True + delegate_to: "{{ inventory_hostname }}" + tags: etherpad + +- name: Dump the database + mysql_db: + state: dump + name: "{{ etherpad_db_name }}" + target: "{{ etherpad_root_dir }}/archives/{{ etherpad_current_version }}/{{ etherpad_db_name }}.sql.xz" + login_host: "{{ etherpad_db_server | default(mysql_server) }}" + login_user: "{{ etherpad_db_user }}" + login_password: "{{ etherpad_db_pass }}" + quick: True + single_transaction: True + environment: + XZ_OPT: -T0 + tags: etherpad diff --git a/roles/etherpad/tasks/cleanup.yml b/roles/etherpad/tasks/cleanup.yml new file mode 100644 index 0000000..370a402 --- /dev/null +++ b/roles/etherpad/tasks/cleanup.yml @@ -0,0 +1,10 @@ +--- + +- name: Remove temp and obsolete files + file: path={{ etherpad_root_dir }}/{{ item }} state=absent + loop: + - tmp/etherpad-lite-{{ etherpad_version }} + - tmp/etherpad-lite-{{ etherpad_version }}.tar.gz + - web + - db_dumps + tags: etherpad diff --git a/roles/etherpad/tasks/conf.yml b/roles/etherpad/tasks/conf.yml new file mode 100644 index 0000000..b9762f8 --- /dev/null +++ b/roles/etherpad/tasks/conf.yml @@ -0,0 +1,15 @@ +--- + +- name: Configure random keys + copy: content={{ item.value }} dest={{ etherpad_root_dir }}/app/{{ item.file }} owner={{ etherpad_user }} group={{ etherpad_user }} mode=600 + loop: + - file: SESSIONKEY.txt + value: "{{ etherpad_session_key }}" + - file: APIKEY.txt + value: "{{ etherpad_api_key }}" + tags: etherpad + +- name: Deploy service configuration + template: src=settings.json.j2 dest={{ etherpad_root_dir }}/app/settings.json + notify: restart etherpad + tags: etherpad diff --git a/roles/etherpad/tasks/directories.yml b/roles/etherpad/tasks/directories.yml new file mode 100644 index 0000000..211717e --- /dev/null +++ b/roles/etherpad/tasks/directories.yml @@ -0,0 +1,18 @@ +--- + +- name: Create directories + file: path={{ etherpad_root_dir }}/{{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: meta + mode: 700 + - dir: tmp + mode: 770 + group: "{{ etherpad_user }}" + - dir: backup + mode: 700 + - dir: archives + mode: 700 + - dir: app + owner: "{{ etherpad_user }}" + tags: etherpad + diff --git a/roles/etherpad/tasks/facts.yml b/roles/etherpad/tasks/facts.yml new file mode 100644 index 0000000..8b2fa49 --- /dev/null +++ b/roles/etherpad/tasks/facts.yml @@ -0,0 +1,46 @@ +--- + +- block: + - import_tasks: ../includes/webapps_set_install_mode.yml + vars: + root_dir: "{{ etherpad_root_dir }}" + version: "{{ etherpad_version }}" + - set_fact: etherpad_install_mode={{ install_mode }} + - set_fact: etherpad_current_version={{ current_version | default('') }} + tags: etherpad + +- when: etherpad_db_pass is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_dbpass" + - set_fact: etherpad_db_pass={{ rand_pass }} + tags: etherpad + +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_session_key" + - set_fact: etherpad_session_key={{ rand_pass }} + tags: etherpad + +- when: etherpad_api_key is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_api_key" + - set_fact: etherpad_api_key={{ rand_pass }} + tags: etherpad + +- when: etherpad_admin_pass is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_admin_pass" + - set_fact: etherpad_admin_pass={{ rand_pass }} + tags: etherpad + +- name: Check if web dir exists + stat: path={{ etherpad_root_dir }}/web + register: etherpad_web_dir + tags: etherpad diff --git a/roles/etherpad/tasks/install.yml b/roles/etherpad/tasks/install.yml new file mode 100644 index 0000000..3143eee --- /dev/null +++ b/roles/etherpad/tasks/install.yml @@ -0,0 +1,78 @@ +--- + +- name: Install dependencies + yum: + name: + - nodejs + notify: restart etherpad + tags: etherpad + +- when: etherpad_install_mode != 'none' + block: + - name: Download etherpad + get_url: + url: "{{ etherpad_archive_url }}" + dest: "{{ etherpad_root_dir }}/tmp" + checksum: "sha1:{{ etherpad_archive_sha1 }}" + + - name: Extract etherpad + unarchive: + src: "{{ etherpad_root_dir }}/tmp/etherpad-lite-{{ etherpad_version }}.tar.gz" + dest: "{{ etherpad_root_dir }}/tmp/" + remote_src: True + + - name: Move etherpad to its correct dir + synchronize: + src: "{{ etherpad_root_dir }}/tmp/etherpad-lite-{{ etherpad_version }}/" + dest: "{{ etherpad_root_dir }}/app/" + recursive: True + delete: True + compress: False + delegate_to: "{{ inventory_hostname }}" + become_user: "{{ etherpad_user }}" + + tags: etherpad + +- name: Install node modules + npm: + path: "{{ etherpad_root_dir }}/app/src" + state: "{{ (etherpad_install_mode == 'none') | ternary('present','latest') }}" + become_user: "{{ etherpad_user }}" + notify: restart etherpad + tags: etherpad + +- name: Install plugins + npm: + name: ep_{{ item }} + path: "{{ etherpad_root_dir }}/app/src" + state: "{{ (etherpad_install_mode == 'none') | ternary('present','latest') }}" + loop: "{{ etherpad_plugins }}" + become_user: "{{ etherpad_user }}" + notify: restart etherpad + tags: etherpad + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ etherpad_db_name }}" + - db_user: "{{ etherpad_db_user }}" + - db_server: "{{ etherpad_db_server }}" + - db_pass: "{{ etherpad_db_pass }}" + tags: etherpad + +- name: Deploy systemd unit + template: src=etherpad.service.j2 dest=/etc/systemd/system/etherpad_{{ etherpad_id }}.service + register: etherpad_unit + notify: restart etherpad + tags: etherpad + +- name: Reload systemd + systemd: daemon_reload=True + when: etherpad_unit.changed + tags: etherpad + +- name: Deploy pre/post backup scripts + template: src={{ item }}_backup.sh.j2 dest=/etc/backup/{{ item }}.d/etherpad_{{ etherpad_id }}.sh mode=750 + loop: + - pre + - post + tags: etherpad diff --git a/roles/etherpad/tasks/iptables.yml b/roles/etherpad/tasks/iptables.yml new file mode 100644 index 0000000..d19f7d5 --- /dev/null +++ b/roles/etherpad/tasks/iptables.yml @@ -0,0 +1,10 @@ +--- + +- name: Handle Etherpad port + iptables_raw: + name: etherpad_{{ etherpad_id }}_port + state: "{{ (etherpad_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ etherpad_port }} -s {{ etherpad_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: etherpad + diff --git a/roles/etherpad/tasks/main.yml b/roles/etherpad/tasks/main.yml new file mode 100644 index 0000000..31bf39d --- /dev/null +++ b/roles/etherpad/tasks/main.yml @@ -0,0 +1,17 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: etherpad_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: service.yml +- include: write_version.yml +- include: archive_post.yml + when: etherpad_install_mode == 'upgrade' +- include: cleanup.yml + diff --git a/roles/etherpad/tasks/service.yml b/roles/etherpad/tasks/service.yml new file mode 100644 index 0000000..7cc4a6c --- /dev/null +++ b/roles/etherpad/tasks/service.yml @@ -0,0 +1,7 @@ +--- + +- name: Start and enable the service + service: name=etherpad_{{ etherpad_id }} state=started enabled=True + register: etherpad_started + tags: etherpad + diff --git a/roles/etherpad/tasks/user.yml b/roles/etherpad/tasks/user.yml new file mode 100644 index 0000000..4387df0 --- /dev/null +++ b/roles/etherpad/tasks/user.yml @@ -0,0 +1,7 @@ +--- + +- import_tasks: ../includes/create_system_user.yml + vars: + user: "{{ etherpad_user }}" + home: "{{ etherpad_root_dir }}" + tags: etherpad diff --git a/roles/etherpad/tasks/write_version.yml b/roles/etherpad/tasks/write_version.yml new file mode 100644 index 0000000..760c8fc --- /dev/null +++ b/roles/etherpad/tasks/write_version.yml @@ -0,0 +1,8 @@ +--- + +- import_tasks: ../includes/webapps_post.yml + vars: + - root_dir: "{{ etherpad_root_dir }}" + - version: "{{ etherpad_version }}" + tags: etherpad + diff --git a/roles/etherpad/templates/etherpad.service.j2 b/roles/etherpad/templates/etherpad.service.j2 new file mode 100644 index 0000000..f0a0781 --- /dev/null +++ b/roles/etherpad/templates/etherpad.service.j2 @@ -0,0 +1,24 @@ +[Unit] +Description=Etherpad ({{ etherpad_id }} Instance) +After=syslog.target network.target + +[Service] +Type=simple +User={{ etherpad_user }} +Group={{ etherpad_user }} +WorkingDirectory={{ etherpad_root_dir }}/app +ExecStart=/usr/bin/node ./src/node/server.js +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=etherpad-{{ etherpad_id }} +Restart=always +Environment=NODE_ENV=production +StartLimitInterval=0 +RestartSec=20 + +[Install] +WantedBy=multi-user.target diff --git a/roles/etherpad/templates/perms.sh.j2 b/roles/etherpad/templates/perms.sh.j2 new file mode 100644 index 0000000..9322183 --- /dev/null +++ b/roles/etherpad/templates/perms.sh.j2 @@ -0,0 +1,7 @@ +#!/bin/bash -e + +restorecon -R {{ etherpad_root_dir }} +chown -R {{ etherpad_user }}:{{ etherpad_user }} {{ etherpad_root_dir }}/app +find {{ etherpad_root_dir }}/app -type f -exec chmod 644 "{}" \; +find {{ etherpad_root_dir }}/app -type d -exec chmod 755 "{}" \; +chmod 600 {{ etherpad_root_dir }}/app/{settings.json,SESSIONKEY.txt,APIKEY.txt} diff --git a/roles/etherpad/templates/post_backup.sh.j2 b/roles/etherpad/templates/post_backup.sh.j2 new file mode 100644 index 0000000..2f54002 --- /dev/null +++ b/roles/etherpad/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ etherpad_root_dir }}/backup/* diff --git a/roles/etherpad/templates/pre_backup.sh.j2 b/roles/etherpad/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..5a288d9 --- /dev/null +++ b/roles/etherpad/templates/pre_backup.sh.j2 @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump \ +{% if etherpad_db_server not in ['localhost', '127.0.0.1'] %} + --user={{ etherpad_db_user }} \ + --password={{ etherpad_db_pass | quote }} \ + --host={{ etherpad_db_server }} \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ etherpad_db_name }} | zstd -c > {{ etherpad_root_dir }}/backup/{{ etherpad_db_name }}.sql.zst diff --git a/roles/etherpad/templates/settings.json.j2 b/roles/etherpad/templates/settings.json.j2 new file mode 100644 index 0000000..be4d430 --- /dev/null +++ b/roles/etherpad/templates/settings.json.j2 @@ -0,0 +1,32 @@ +{ + "title" : "{{ etherpad_title }}", + "skinName" : "{{ etherpad_theme }}", + "port" : {{ etherpad_port }}, + "showSettingsInAdminPage" : false, + "dbType" : "mysql", + "dbSettings" : { + "user" : "{{ etherpad_db_user }}", + "host" : "{{ etherpad_db_server }}", + "port" : {{ etherpad_db_port }}, + "password" : "{{ etherpad_db_pass }}", + "database" : "{{ etherpad_db_name }}", + "charset" : "utf8mb4" + }, + "defaultPadText" : "", + "socketTransportProtocols" : ["websocket", "xhr-polling", "jsonp-polling", "htmlfile"], + "allowUnknownFileEnds" : false, + "trustProxy" : true, + "users": { + "admin": { + "password" : "{{ etherpad_admin_pass }}", + "is_admin" : true + } + }, + "ep_delete_after_delay": { + "delay" : 2592000, + "loop" : true, + "loopDelay" : 3600, + "deleteAtStart" : true, + "text" : "" + } +} diff --git a/roles/filebeat/defaults/main.yml b/roles/filebeat/defaults/main.yml new file mode 100644 index 0000000..4b115fd --- /dev/null +++ b/roles/filebeat/defaults/main.yml @@ -0,0 +1,12 @@ +--- +filebeat_output_type: logstash +filebeat_output_hosts: [] +# filebeat_output_hosts: +# - graylog.example.org:5044 +filebeat_output_ssl: + enabled: True + # cert_authorities: + # - /path/to/ca.crt + # client_cert: /etc/filebeat/ssl/cert.pem + # client_key: /etc/filebeat/ssl/key.pem + # client_key_passphrase: s3cr3t. diff --git a/roles/filebeat/handlers/main.yml b/roles/filebeat/handlers/main.yml new file mode 100644 index 0000000..fe44390 --- /dev/null +++ b/roles/filebeat/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: restart filebeat + service: name=filebeat state=restarted + when: filebeat_output_hosts | length > 0 + +- name: restart journalbeat + service: name=journalbeat state=restarted + when: + - filebeat_output_hosts | length > 0 + - ansible_service_mgr == 'systemd' diff --git a/roles/filebeat/meta/main.yml b/roles/filebeat/meta/main.yml new file mode 100644 index 0000000..b091a2d --- /dev/null +++ b/roles/filebeat/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: repo_filebeat diff --git a/roles/filebeat/tasks/main.yml b/roles/filebeat/tasks/main.yml new file mode 100644 index 0000000..80942fe --- /dev/null +++ b/roles/filebeat/tasks/main.yml @@ -0,0 +1,75 @@ +--- + +- name: Install filebeatbeat + package: + name: + - filebeat + tags: logs + +- name: Install journalbeat + package: + name: + - journalbeat + when: ansible_service_mgr == 'systemd' + tags: logs + +# Not useful, and prevent fast completion for journalctl +- name: Remove journalbeat shortcut + file: path={{ item }} state=absent + loop: + - /bin/journalbeat + - /usr/bin/journalbeat + when: ansible_service_mgr == 'systemd' + tags: logs + +- name: Create ansible module directories + file: path=/etc/filebeat/ansible_{{ item }}.d state=directory + loop: + - modules + - inputs + tags: logs + +- name: Deploy filebeat configuration + template: src={{ item }}.j2 dest=/etc/filebeat/{{ item }} + loop: + - filebeat.yml + - ansible_modules.d/system.yml + - ansible_modules.d/auditd.yml + - ansible_inputs.d/system_specific.yml + notify: restart filebeat + tags: logs + +- name: Deploy journalbeat configuration + template: src=journalbeat.yml.j2 dest=/etc/journalbeat/journalbeat.yml + notify: restart journalbeat + tags: logs + +- name: Override filebeat unit + template: src=filebeat.service.j2 dest=/etc/systemd/system/filebeat.service + register: filebeat_unit + tags: logs + +- name: Override journalbeat unit + template: src=journalbeat.service.j2 dest=/etc/systemd/system/journalbeat.service + register: filebeat_journalbeat_unit + when: ansible_service_mgr == 'systemd' + tags: logs + +- name: Reload systemd + systemd: daemon_reload=True + when: filebeat_unit.changed or (filebeat_journalbeat_unit is defined and filebeat_journalbeat_unit.changed) + tags: logs + +- name: Handle filebeat service + service: + name: filebeat + state: "{{ (filebeat_output_hosts | length > 0) | ternary('started','stopped') }}" + enabled: "{{ (filebeat_output_hosts | length > 0) | ternary(True,False) }}" + tags: logs + +- name: Handle journalbeat service + service: + name: journalbeat + state: "{{ (ansible_service_mgr == 'systemd' and filebeat_output_hosts | length > 0) | ternary('started','stopped') }}" + enabled: "{{ (ansible_service_mgr == 'systemd' and filebeat_output_hosts | length > 0) | ternary(True,False) }} " + tags: logs diff --git a/roles/filebeat/templates/ansible_inputs.d/system_specific.yml.j2 b/roles/filebeat/templates/ansible_inputs.d/system_specific.yml.j2 new file mode 100644 index 0000000..71ff431 --- /dev/null +++ b/roles/filebeat/templates/ansible_inputs.d/system_specific.yml.j2 @@ -0,0 +1,13 @@ +- type: log + enabled: True + paths: +{% if ansible_os_family == 'RedHat' %} + - /var/log/yum.log +{% elif ansible_os_family == 'Debian' %} + - /var/log/dpkg.log + - /var/log/apt/*.log + - /var/log/alternatives.log +{% endif %} + exclude_files: + - '\.[gx]z$' + - '\d+$' diff --git a/roles/filebeat/templates/ansible_modules.d/auditd.yml.j2 b/roles/filebeat/templates/ansible_modules.d/auditd.yml.j2 new file mode 100644 index 0000000..5a81769 --- /dev/null +++ b/roles/filebeat/templates/ansible_modules.d/auditd.yml.j2 @@ -0,0 +1,7 @@ +- module: auditd + log: + enabled: True + input: + exclude_files: + - '\.[xg]z$' + - '\d+$' diff --git a/roles/filebeat/templates/ansible_modules.d/system.yml.j2 b/roles/filebeat/templates/ansible_modules.d/system.yml.j2 new file mode 100644 index 0000000..cc9a3ce --- /dev/null +++ b/roles/filebeat/templates/ansible_modules.d/system.yml.j2 @@ -0,0 +1,9 @@ +{% if ansible_service_mgr == 'systemd' %} +# We use journalbeat on systemd based systems +{% else %} +- module: system + syslog: + enabled: True + auth: + enabled: True +{% endif %} diff --git a/roles/filebeat/templates/filebeat.service.j2 b/roles/filebeat/templates/filebeat.service.j2 new file mode 100644 index 0000000..dc3cf6e --- /dev/null +++ b/roles/filebeat/templates/filebeat.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Filebeat sends log files to Logstash or directly to Elasticsearch. +Documentation=https://www.elastic.co/products/beats/filebeat +Wants=network-online.target +After=network-online.target + +[Service] +Environment="BEAT_CONFIG_OPTS=-c /etc/filebeat/filebeat.yml" +Environment="BEAT_PATH_OPTS=-path.home /usr/share/filebeat -path.config /etc/filebeat -path.data /var/lib/filebeat -path.logs /var/log/filebeat" +ExecStart=/usr/share/filebeat/bin/filebeat $BEAT_CONFIG_OPTS $BEAT_PATH_OPTS +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/roles/filebeat/templates/filebeat.yml.j2 b/roles/filebeat/templates/filebeat.yml.j2 new file mode 100644 index 0000000..9676ce3 --- /dev/null +++ b/roles/filebeat/templates/filebeat.yml.j2 @@ -0,0 +1,41 @@ +fields: + source: {{ inventory_hostname }} +fields_under_root: True +logging.files: + rotateeverybytes: 5242880 + keepfiles: 2 +filebeat.config.inputs: + path: /etc/filebeat/ansible_inputs.d/*.yml + reload.enabled: True + reload.period: 30s +filebeat.config.modules: + path: /etc/filebeat/ansible_modules.d/*.yml + reload.enabled: True + reload.period: 30s +processors: + - add_host_metadata: ~ + - add_cloud_metadata: ~ +output.{{ filebeat_output_type }}: + hosts: +{% for host in filebeat_output_hosts %} + - {{ host }} +{% endfor %} +{% if filebeat_output_ssl is defined %} + ssl: +{% if filebeat_output_ssl.enabled is defined %} + enabled: {{ filebeat_output_ssl.enabled }} +{% endif %} +{% if filebeat_output_ssl.cert_authorities is defined %} + certificate_authorities: +{% for ca in filebeat_output_ssl.cert_authorities %} + - {{ ca }} +{% endfor %} +{% endif %} +{% if filebeat_output_ssl.client_cert is defined and filebeat_output_ssl.client_key is defined %} + certificate: {{ filebeat_output_ssl.client_cert }} + key: {{ filebeat_output_ssl.client_key }} +{% endif %} +{% if filebeat_output_ssl.client_key_passphrase is defined %} + key_passphrase: {{ filebeat_output_ssl.client_key_passphrase | quote }} +{% endif %} +{% endif %} diff --git a/roles/filebeat/templates/journalbeat.service.j2 b/roles/filebeat/templates/journalbeat.service.j2 new file mode 100644 index 0000000..403dafd --- /dev/null +++ b/roles/filebeat/templates/journalbeat.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=Journalbeat ships systemd journal entries to Elasticsearch or Logstash. +Documentation=https://www.elastic.co/products/beats/journalbeat +Wants=network-online.target +After=network-online.target + +[Service] +Environment="BEAT_CONFIG_OPTS=-c /etc/journalbeat/journalbeat.yml" +Environment="BEAT_PATH_OPTS=-path.home /usr/share/journalbeat -path.config /etc/journalbeat -path.data /var/lib/journalbeat -path.logs /var/log/journalbeat" +ExecStart=/usr/share/journalbeat/bin/journalbeat $BEAT_CONFIG_OPTS $BEAT_PATH_OPTS +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/roles/filebeat/templates/journalbeat.yml.j2 b/roles/filebeat/templates/journalbeat.yml.j2 new file mode 100644 index 0000000..cd1aad0 --- /dev/null +++ b/roles/filebeat/templates/journalbeat.yml.j2 @@ -0,0 +1,34 @@ +fields: + source: {{ inventory_hostname }} +fields_under_root: True +logging.files: + rotateeverybytes: 5242880 + keepfiles: 2 +journalbeat.inputs: + - paths: [] + seek: cursor + cursor_seek_fallback: tail +output.{{ filebeat_output_type }}: + hosts: +{% for host in filebeat_output_hosts %} + - {{ host }} +{% endfor %} +{% if filebeat_output_ssl is defined %} + ssl: +{% if filebeat_output_ssl.enabled is defined %} + enabled: {{ filebeat_output_ssl.enabled }} +{% endif %} +{% if filebeat_output_ssl.cert_authorities is defined %} + certificate_authorities: +{% for ca in filebeat_output_ssl.cert_authorities %} + - {{ ca }} +{% endfor %} +{% endif %} +{% if filebeat_output_ssl.client_cert is defined and filebeat_output_ssl.client_key is defined %} + certificate: {{ filebeat_output_ssl.client_cert }} + key: {{ filebeat_output_ssl.client_key }} +{% endif %} +{% if filebeat_output_ssl.client_key_passphrase is defined %} + key_passphrase: {{ filebeat_output_ssl.client_key_passphrase | quote }} +{% endif %} +{% endif %} diff --git a/roles/framadate/defaults/main.yml b/roles/framadate/defaults/main.yml new file mode 100644 index 0000000..873a0ec --- /dev/null +++ b/roles/framadate/defaults/main.yml @@ -0,0 +1,48 @@ +--- + +# A unique ID for this instance. You can deploy several framadate instances on the same machine +framadate_id: 1 + +# Root dir where the app will be installed. Each instance must have a different install path +framadate_root_dir: /opt/framadate_{{ framadate_id }} + +# The version to deploy +framadate_version: '1.1.17' + +# Should ansible manage upgrades, or only initial installation +framadate_manage_upgrade: True + +# The URL to download framadate archive +framadate_zip_url: https://framagit.org/framasoft/framadate/framadate/-/archive/{{ framadate_version }}/framadate-{{ framadate_version }}.zip + +# The sha1 checksum of the archive +framadate_zip_sha1: 5c0782f1db6a797df70047c3715003178956ca3d + +# The user account under which PHP is executed +framadate_php_user: php-framadate_{{ framadate_id }} + +# The version of PHP to use +framadate_php_version: 74 + +# Alternatively, use a custom php pool, which must be defined manually +#framadate_php_fpm_pool: php70 + +# Database parameters, framadate_mysql_pass must be set +framadate_mysql_server: "{{ mysql_server | default('localhost') }}" +framadate_mysql_port: 3306 +framadate_mysql_db: framadate_{{ framadate_id }} +framadate_mysql_user: framadate_{{ framadate_id }} +# If not set, a default one will be generated +# framadate_mysql_pass: framadate + +# The email of the admin +#framadate_admin_email: admin@domain.net + +# Logo URL. Can be relative the framadate_root_dir or an absolute URL +# in which case the logo will be downloaded during the installation +framadate_logo_url: images/logo-framadate.png + +# Should framadate trust the webserver authentication +framadate_proxy_auth: False + +... diff --git a/roles/framadate/files/framadate.sql b/roles/framadate/files/framadate.sql new file mode 100644 index 0000000..1da96d5 --- /dev/null +++ b/roles/framadate/files/framadate.sql @@ -0,0 +1,67 @@ +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE IF NOT EXISTS `fd_comment` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `poll_id` varchar(64) NOT NULL, + `name` varchar(64) DEFAULT NULL, + `comment` text NOT NULL, + `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `poll_id` (`poll_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE IF NOT EXISTS `fd_framadate_migration` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` text NOT NULL, + `execute_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE IF NOT EXISTS `fd_poll` ( + `id` varchar(64) NOT NULL, + `admin_id` char(24) NOT NULL, + `title` text NOT NULL, + `description` text, + `admin_name` varchar(64) DEFAULT NULL, + `admin_mail` varchar(128) DEFAULT NULL, + `creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `end_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `format` varchar(1) DEFAULT NULL, + `editable` tinyint(1) DEFAULT '0', + `receiveNewVotes` tinyint(1) DEFAULT '0', + `receiveNewComments` tinyint(1) DEFAULT '0', + `active` tinyint(1) DEFAULT '1', + `hidden` tinyint(1) NOT NULL DEFAULT '0', + `password_hash` varchar(255) DEFAULT NULL, + `results_publicly_visible` tinyint(1) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE IF NOT EXISTS `fd_slot` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `poll_id` varchar(64) NOT NULL, + `title` text, + `moments` text, + PRIMARY KEY (`id`), + KEY `poll_id` (`poll_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE IF NOT EXISTS `fd_vote` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `uniqId` char(16) NOT NULL, + `poll_id` varchar(64) NOT NULL, + `name` varchar(64) NOT NULL, + `choices` text NOT NULL, + PRIMARY KEY (`id`), + KEY `poll_id` (`poll_id`), + KEY `uniqId` (`uniqId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; diff --git a/roles/framadate/handlers/main.yml b/roles/framadate/handlers/main.yml new file mode 100644 index 0000000..5de68b6 --- /dev/null +++ b/roles/framadate/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- include: ../httpd_common/handlers/main.yml +... diff --git a/roles/framadate/meta/main.yml b/roles/framadate/meta/main.yml new file mode 100644 index 0000000..9f53129 --- /dev/null +++ b/roles/framadate/meta/main.yml @@ -0,0 +1,5 @@ +--- +allow_duplicates: true +dependencies: + - role: composer +... diff --git a/roles/framadate/tasks/main.yml b/roles/framadate/tasks/main.yml new file mode 100644 index 0000000..75242e1 --- /dev/null +++ b/roles/framadate/tasks/main.yml @@ -0,0 +1,256 @@ +--- + +- name: Set install mode + set_fact: framadate_install_mode='none' + tags: framadate + +- name: Install needed tools + yum: + name: + - unzip + - acl + - tar + tags: framadate + +- name: Create user account for PHP + user: + name: "{{ framadate_php_user }}" + comment: "PHP FPM {{ framadate_php_user }}" + system: True + shell: /sbin/nologin + tags: framadate + +- name: Check if framadate is already installed + stat: path={{ framadate_root_dir }}/meta/ansible_version + register: framadate_version_file + tags: framadate + +- name: Check framadate version + command: cat {{ framadate_root_dir }}/meta/ansible_version + register: framadate_current_version + changed_when: False + when: framadate_version_file.stat.exists + tags: framadate + +- name: Set installation process to install + set_fact: framadate_install_mode='install' + when: not framadate_version_file.stat.exists + tags: framadate + +- name: Set installation process to upgrade + set_fact: framadate_install_mode='upgrade' + when: + - framadate_version_file.stat.exists + - framadate_current_version.stdout != framadate_version + - framadate_manage_upgrade + tags: framadate + +- name: Create archive dir + file: path={{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }} state=directory mode=700 + when: framadate_install_mode == 'upgrade' + tags: framadate + +- name: Archive current version + synchronize: + src: "{{ framadate_root_dir }}/web" + dest: "{{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }}/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: framadate_install_mode == 'upgrade' + tags: framadate + +- name: Dump database + mysql_db: + state: dump + name: "{{ framadate_mysql_db }}" + target: "{{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }}/{{ framadate_mysql_db }}.sql" + login_host: "{{ framadate_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + quick: True + single_transaction: True + when: framadate_install_mode == 'upgrade' + tags: framadate + +- name: Create directory structure + file: path={{ item }} state=directory + with_items: + - "{{ framadate_root_dir }}" + - "{{ framadate_root_dir }}/web" + - "{{ framadate_root_dir }}/web/tpl_c" + - "{{ framadate_root_dir }}/tmp" + - "{{ framadate_root_dir }}/sessions" + - "{{ framadate_root_dir }}/logs" + - "{{ framadate_root_dir }}/meta" + tags: framadate + +- name: Download Framadate + get_url: + url: "{{ framadate_zip_url }}" + dest: "{{ framadate_root_dir }}/tmp/" + checksum: "sha1:{{ framadate_zip_sha1 }}" + when: framadate_install_mode != 'none' + tags: framadate + +- name: Extract framadate archive + unarchive: + src: "{{ framadate_root_dir }}/tmp/framadate-{{ framadate_version }}.zip" + dest: "{{ framadate_root_dir }}/tmp/" + remote_src: yes + when: framadate_install_mode != 'none' + tags: framadate + +- name: Move the content of framadate to the correct top directory + synchronize: + src: "{{ framadate_root_dir }}/tmp/framadate-{{ framadate_version }}/" + dest: "{{ framadate_root_dir }}/web/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: framadate_install_mode != 'none' + tags: framadate + +- name: Install libs using composer + composer: command=install working_dir={{ framadate_root_dir }}/web executable=/bin/php{{ framadate_php_version }} + environment: + php: /bin/php{{ framadate_php_version }} + tags: framadate + +- name: Download custom logo + get_url: + url: "{{ framadate_logo_url }}" + dest: "{{ framadate_root_dir }}/web/images" + when: framadate_logo_url is search('https?://') + tags: framadate + +- name: Generate a random pass for the database + shell: openssl rand -base64 45 > {{ framadate_root_dir }}/meta/ansible_dbpass + args: + creates: "{{ framadate_root_dir }}/meta/ansible_dbpass" + when: framadate_mysql_pass is not defined + tags: framadate + +- name: Read database password + command: cat {{ framadate_root_dir }}/meta/ansible_dbpass + register: framadate_rand_pass + when: framadate_mysql_pass is not defined + changed_when: False + tags: framadate + +- name: Set database pass + set_fact: framadate_mysql_pass={{ framadate_rand_pass.stdout }} + when: framadate_mysql_pass is not defined + tags: framadate + +- name: Create MySQL database + mysql_db: + name: "{{ framadate_mysql_db }}" + login_host: "{{ framadate_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + state: present + register: framadate_mysql_created + tags: framadate + +- name: Create MySQL User + mysql_user: + name: "{{ framadate_mysql_user }}" + password: "{{ framadate_mysql_pass }}" + priv: "{{ framadate_mysql_db }}.*:ALL" + host: "{{ (framadate_mysql_server == 'localhost') | ternary('localhost', item) }}" + login_host: "{{ framadate_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + state: present + with_items: "{{ ansible_all_ipv4_addresses }}" + tags: framadate + +- name: Copy SQL structure + copy: src=framadate.sql dest={{ framadate_root_dir }}/tmp/framadate.sql + when: framadate_install_mode != 'none' + tags: framadate + +- name: Inject MySQL schema + mysql_db: + name: "{{ framadate_mysql_db }}" + state: import + target: "{{ framadate_root_dir }}/tmp/framadate.sql" + login_host: "{{ framadate_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + when: framadate_install_mode == 'install' + tags: framadate + +- name: Remove temp files + file: path={{ item }} state=absent + with_items: + - "{{ framadate_root_dir }}/tmp/framadate-{{ framadate_version }}" + - "{{ framadate_root_dir }}/tmp/framadate-{{ framadate_version }}.zip" + - "{{ framadate_root_dir }}/tmp/framadate.sql" + tags: framadate + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ framadate_root_dir}}/perms.sh mode=755 + tags: framadate + +- name: Deploy httpd configuration + template: src=httpd.conf.j2 dest=/etc/httpd/ansible_conf.d/10-framadate_{{ framadate_id }}.conf + notify: reload httpd + tags: framadate + +- name: Deploy PHP configuration + template: src=php.conf.j2 dest=/etc/opt/remi/php{{ framadate_php_version }}/php-fpm.d/framadate_{{ framadate_id }}.conf + notify: restart php-fpm + tags: framadate + +- name: Remove PHP configuration from other versions + file: path=/etc/opt/remi/php{{ item }}/php-fpm.d/framadate_{{ framadate_id }}.conf state=absent + with_items: "{{ httpd_php_versions | difference([ framadate_php_version ]) }}" + notify: restart php-fpm + tags: framadate + +- name: Remove PHP configuration (using a custom pool) + file: path=/etc/opt/remi/php{{ framadate_php_version }}/php-fpm.d/framadate_{{ framadate_id }}.conf state=absent + when: framadate_php_fpm_pool is defined + notify: restart php-fpm + tags: framadate + +- name: Deploy framadate configuration + template: src=config.php.j2 dest={{ framadate_root_dir }}/web/app/inc/config.php owner=root group={{ framadate_php_user }} mode=640 + tags: framadate + +- name: Set correct SELinux context + sefcontext: + target: "{{ framadate_root_dir }}(/.*)?" + setype: httpd_sys_content_t + state: present + when: ansible_selinux.status == 'enabled' + tags: framadate + +- name: Restrict permissions + command: "{{ framadate_root_dir }}/perms.sh" + changed_when: False + tags: framadate + +- name: Compress previous version + command: tar cJf {{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }}.txz ./ + environment: + XZ_OPT: -T0 + args: + chdir: "{{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }}" + warn: False + when: framadate_install_mode == 'upgrade' + tags: framadate + +- name: Remove archive directory + file: path={{ framadate_root_dir }}/archives/{{ framadate_current_version.stdout }} state=absent + when: framadate_install_mode == 'upgrade' + tags: framadate + +- name: Write version number + copy: content={{ framadate_version }} dest={{ framadate_root_dir }}/meta/ansible_version + when: framadate_install_mode != 'none' + tags: framadate + +... diff --git a/roles/framadate/templates/config.php.j2 b/roles/framadate/templates/config.php.j2 new file mode 100644 index 0000000..2e2b358 --- /dev/null +++ b/roles/framadate/templates/config.php.j2 @@ -0,0 +1,39 @@ +'; +const DB_USER = '{{ framadate_mysql_user | default('framadate') }}'; +const DB_PASSWORD = '{{ framadate_mysql_pass }}'; +const DB_CONNECTION_STRING = 'mysql:host={{ framadate_mysql_server }};dbname={{ framadate_mysql_db }};port={{ framadate_mysql_port }}'; +const MIGRATION_TABLE = 'framadate_migration'; +const TABLENAME_PREFIX = 'fd_'; +const DEFAULT_LANGUAGE = 'fr'; +$ALLOWED_LANGUAGES = [ + 'fr' => 'Français', + 'en' => 'English', + 'oc' => 'Occitan', + 'es' => 'Español', + 'de' => 'Deutsch', + 'nl' => 'Dutch', + 'it' => 'Italiano', + 'br' => 'Brezhoneg', +]; +const IMAGE_TITRE = '/images/{{ framadate_logo_url | basename }}'; +const URL_PROPRE = true; +const USE_REMOTE_USER = {{ framadate_proxy_auth | ternary('true','false') }}; +const LOG_FILE = '../logs/stdout.log'; +const PURGE_DELAY = 60; +const MAX_SLOTS_PER_POLL = 366; +const TIME_EDIT_LINK_EMAIL = 60; +$config = [ + 'use_smtp' => true, + 'show_what_is_that' => false, + 'show_the_software' => false, + 'show_cultivate_your_garden' => false, + 'default_poll_duration' => 180, + 'user_can_add_img_or_link' => true, + 'provide_fork_awesome' => true, +]; diff --git a/roles/framadate/templates/httpd.conf.j2 b/roles/framadate/templates/httpd.conf.j2 new file mode 100644 index 0000000..720bcbc --- /dev/null +++ b/roles/framadate/templates/httpd.conf.j2 @@ -0,0 +1,45 @@ +{% if framadate_alias is defined %} +Alias /{{ framadate_alias }} {{ framadate_root_dir }}/web +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + + + AllowOverride None + Options FollowSymLinks +{% if framadate_allowed_ip is defined %} + Require ip {{ framadate_allowed_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ framadate_php_fpm_pool | default('framadate_' + framadate_id | string) }}.sock|fcgi://localhost" + +{% if framadate_proxy_auth %} + SetEnvIfNoCase Auth-User "(.*)" REMOTE_USER=$1 +{% endif %} + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} -f [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule . - [L] + + RewriteRule ^([a-zA-Z0-9-]+)$ studs.php?poll=$1 [L] + RewriteRule ^([a-zA-Z0-9-]+)/action/([a-zA-Z_-]+)/(.+)$ studs.php?poll=$1&$2=$3 + RewriteRule ^([a-zA-Z0-9-]+)/vote/([a-zA-Z0-9]{16})$ studs.php?poll=$1&vote=$2 + RewriteRule ^(action/)?([a-zA-Z0-9-]{24})/admin$ adminstuds.php?poll=$2 + RewriteRule ^([a-zA-Z0-9-]{24})/admin/vote/([a-zA-Z0-9]{16})$ adminstuds.php?poll=$1&vote=$2 + RewriteRule ^([a-zA-Z0-9-]{24})/admin/action/([a-zA-Z_-]+)(/(.+))?$ adminstuds.php?poll=$1&$2=$4 + + + + Require all denied + + + Order allow,deny + Deny from all + + + + + diff --git a/roles/framadate/templates/perms.sh.j2 b/roles/framadate/templates/perms.sh.j2 new file mode 100644 index 0000000..6dda36c --- /dev/null +++ b/roles/framadate/templates/perms.sh.j2 @@ -0,0 +1,17 @@ +#!/bin/sh + +restorecon -R {{ framadate_root_dir }} +chown root:root {{ framadate_root_dir }} +chmod 700 {{ framadate_root_dir }} +setfacl -k -b {{ framadate_root_dir }} +setfacl -m u:{{ framadate_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:rx {{ framadate_root_dir }} +chown -R root:root {{ framadate_root_dir }}/web +chown -R {{ framadate_php_user }} {{ framadate_root_dir }}/{tmp,sessions,logs} +chmod 700 {{ framadate_root_dir }}/{tmp,sessions,logs} +find {{ framadate_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ framadate_root_dir }}/web -type d -exec chmod 755 "{}" \; +chown :{{ framadate_php_user }} {{ framadate_root_dir }}/web/app/inc/config.php +chmod 640 {{ framadate_root_dir }}/web/app/inc/config.php +[ -d {{ framadate_root_dir }}/web/tpl_c ] || mkdir -p {{ framadate_root_dir }}/web/tpl_c +chown :{{ framadate_php_user }} {{ framadate_root_dir }}/web/tpl_c +chmod 775 {{ framadate_root_dir }}/web/tpl_c diff --git a/roles/framadate/templates/php.conf.j2 b/roles/framadate/templates/php.conf.j2 new file mode 100644 index 0000000..799b668 --- /dev/null +++ b/roles/framadate/templates/php.conf.j2 @@ -0,0 +1,36 @@ +[framadate_{{ framadate_id }}] + +listen.owner = root +listen.group = apache +listen.mode = 0660 +listen = /run/php-fpm/framadate_{{ framadate_id }}.sock +user = {{ framadate_php_user }} +group = {{ framadate_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 5m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 64M +php_admin_value[session.save_path] = {{ framadate_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ framadate_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ framadate_root_dir }}/tmp +php_admin_value[post_max_size] = 2M +php_admin_value[upload_max_filesize] = 2M +php_admin_value[disable_functions] = system, show_source, symlink, exec, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ framadate_root_dir }} +php_admin_value[max_execution_time] = 60 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = off +php_admin_flag[file_uploads] = off +php_admin_flag[session.cookie_httponly] = on + diff --git a/roles/freepbx/defaults/main.yml b/roles/freepbx/defaults/main.yml new file mode 100644 index 0000000..2db239c --- /dev/null +++ b/roles/freepbx/defaults/main.yml @@ -0,0 +1,52 @@ +--- + +fpbx_version: 15.0 +fpbx_archive_sha1: 42aae0f245a5d6297f8f2154281f28436663ee33 +fpbx_archive_url: https://mirror.freepbx.org/modules/packages/freepbx/freepbx-{{ fpbx_version }}-latest.tgz +fpbx_root_dir: /opt/freepbx +fpbx_manage_upgrade: True + +fpbx_db_server: localhost +fpbx_db_user: freepbx +fpbx_db_name: freepbx +fpbx_cdr_db_name: asteriskcdrdb +# fpbx_db_pass: secret + +fpbx_php_version: 56 + +# fbx_alias: /freepbx +# fpbx_src_ip: +# - 192.168.281.0/24 + +# fpbx_manager_pass: secret +# Can be set to database to use internal auth. None is used when protecting accessing with the web server +fpbx_auth_type: none + +fpbx_mgm_tcp_ports: [ 5038 ] +fpbx_mgm_udp_ports: [] +fpbx_voip_tcp_ports: + - 5060 # SIP, chan_pjsip + - 5061 # SIP, chan_sip +fpbx_voip_udp_ports: + - 5060 # SIP, chan_pjsip + - 5160 # SIP, chan_sip + - '10000:20000' # RTP + - 4520 # dundi + - 4569 # IAX2 +fpbx_prov_tcp_ports: [ 21 ] +fpbx_prov_udp_ports: [ 69 ] +fpbx_http_ports: + - 80 # Normal HTTP + - 8088 # UCP node + - 8001 # ast WS +fpbx_mgm_src_ip: [] +fpbx_voip_src_ip: [] +fpbx_http_src_ip: "{{ httpd_src_ip }}" +fpbx_prov_src_ip: "{{ fpbx_voip_src_ip }}" + +# Password used for provisioning. The user is phone +# A random one is created if not set here +# fpbx_phone_pass: s3crEt. + +# Set to your vhost if you use one +# fpbx_vhost: https://tel.domain.net diff --git a/roles/freepbx/files/agi/jitsi_conf_pin b/roles/freepbx/files/agi/jitsi_conf_pin new file mode 100644 index 0000000..91675ce --- /dev/null +++ b/roles/freepbx/files/agi/jitsi_conf_pin @@ -0,0 +1,23 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use LWP::UserAgent; +use JSON; + +my $ret = 'error'; + +my $url = $ARGV[0] . '?id=' . $ARGV[1]; +my $ua = LWP::UserAgent->new(timeout => 10); +$ua->env_proxy; + +my $response = $ua->get($url); +if ($response->is_success){ + my $json = from_json($response->content); + if (defined $json and defined $json->{conference}){ + $ret = $json->{conference}; + $ret =~ s/@.*//; + } +} + +print "SET VARIABLE JITSI_ROOM $ret\n"; diff --git a/roles/freepbx/files/patches/install_dbhost.patch b/roles/freepbx/files/patches/install_dbhost.patch new file mode 100644 index 0000000..37a5897 --- /dev/null +++ b/roles/freepbx/files/patches/install_dbhost.patch @@ -0,0 +1,32 @@ +--- ./installlib/installcommand.class.php.orig 2019-05-24 18:06:10.587719554 +0200 ++++ ./installlib/installcommand.class.php 2019-05-24 18:09:43.226443972 +0200 +@@ -17,6 +17,10 @@ + 'default' => 'mysql', + 'description' => 'Database engine' + ), ++ 'dbhost' => array( ++ 'default' => 'localhost', ++ 'description' => 'Database server' ++ ), + 'dbname' => array( + 'default' => 'asterisk', + 'description' => 'Database name' +@@ -366,6 +370,9 @@ + if (isset($answers['dbengine'])) { + $amp_conf['AMPDBENGINE'] = $answers['dbengine']; + } ++ if (isset($answers['dbhost'])) { ++ $amp_conf['AMPDBHOST'] = $answers['dbhost']; ++ } + if (isset($answers['dbname'])) { + $amp_conf['AMPDBNAME'] = $answers['dbname']; + } +@@ -415,7 +422,7 @@ + + $amp_conf['AMPDBUSER'] = $answers['dbuser']; + $amp_conf['AMPDBPASS'] = $answers['dbpass']; +- $amp_conf['AMPDBHOST'] = 'localhost'; ++ $amp_conf['AMPDBHOST'] = $answers['dbhost']; + + if($dbroot) { + $output->write("Database Root installation checking credentials and permissions.."); diff --git a/roles/freepbx/files/patches/webrtc_proxy.patch b/roles/freepbx/files/patches/webrtc_proxy.patch new file mode 100644 index 0000000..749df2e --- /dev/null +++ b/roles/freepbx/files/patches/webrtc_proxy.patch @@ -0,0 +1,21 @@ +--- /opt/freepbx/web/admin/modules/webrtc/Webrtc.class.php.orig 2019-11-12 14:47:05.904759608 +0100 ++++ /opt/freepbx/web/admin/modules/webrtc/Webrtc.class.php 2019-11-12 14:55:46.392864447 +0100 +@@ -374,13 +374,14 @@ + $prefix = $this->FreePBX->Config->get('HTTPPREFIX'); + $suffix = !empty($prefix) ? "/".$prefix."/ws" : "/ws"; + +- if($secure && !$this->FreePBX->Config->get('HTTPTLSENABLE')) { +- return array("status" => false, "message" => _("HTTPS is not enabled for Asterisk")); +- } ++ //if($secure && !$this->FreePBX->Config->get('HTTPTLSENABLE')) { ++ // return array("status" => false, "message" => _("HTTPS is not enabled for Asterisk")); ++ //} + + $type = ($this->FreePBX->Config->get('HTTPTLSENABLE') && $secure) ? 'wss' : 'ws'; + $port = ($this->FreePBX->Config->get('HTTPTLSENABLE') && $secure) ? $this->FreePBX->Config->get('HTTPTLSBINDPORT') : $this->FreePBX->Config->get('HTTPBINDPORT'); +- $results['websocket'] = !empty($results['websocket']) ? $results['websocket'] : $type.'://'.$sip_server.':'.$port.$suffix; ++ //$results['websocket'] = !empty($results['websocket']) ? $results['websocket'] : $type.'://'.$sip_server.':'.$port.$suffix; ++ $results['websocket'] = !empty($results['websocket']) ? $results['websocket'] : 'wss://'.$_SERVER['HTTP_HOST'].'/'.$this->FreePBX->Config->get('HTTPPREFIX').'/ws'; + try { + $stunaddr = $this->FreePBX->Sipsettings->getConfig("webrtcstunaddr"); + $stunaddr = !empty($stunaddr) ? $stunaddr : $this->FreePBX->Sipsettings->getConfig("stunaddr"); diff --git a/roles/freepbx/files/safe_asterisk b/roles/freepbx/files/safe_asterisk new file mode 100755 index 0000000..231af06 --- /dev/null +++ b/roles/freepbx/files/safe_asterisk @@ -0,0 +1,228 @@ +#!/bin/sh + +ASTETCDIR="/etc/asterisk" +ASTSBINDIR="/usr/sbin" +ASTVARRUNDIR="/var/run/asterisk" +ASTVARLOGDIR="/var/log/asterisk" + +CLIARGS="$*" # Grab any args passed to safe_asterisk +TTY=9 # TTY (if you want one) for Asterisk to run on +CONSOLE=yes # Whether or not you want a console +#NOTIFY=root@localhost # Who to notify about crashes +#EXEC=/path/to/somescript # Run this command if Asterisk crashes +#LOGFILE="${ASTVARLOGDIR}/safe_asterisk.log" # Where to place the normal logfile (disabled if blank) +#SYSLOG=local0 # Which syslog facility to use (disabled if blank) +MACHINE=`hostname` # To specify which machine has crashed when getting the mail +DUMPDROP="${DUMPDROP:-/tmp}" +RUNDIR="${RUNDIR:-/tmp}" +SLEEPSECS=4 +ASTPIDFILE="${ASTVARRUNDIR}/asterisk.pid" + +# comment this line out to have this script _not_ kill all mpg123 processes when +# asterisk exits +KILLALLMPG123=1 + +# run asterisk with this priority +PRIORITY=0 + +# set system filemax on supported OSes if this variable is set +# SYSMAXFILES=262144 + +# Asterisk allows full permissions by default, so set a umask, if you want +# restricted permissions. +#UMASK=022 + +# set max files open with ulimit. On linux systems, this will be automatically +# set to the system's maximum files open devided by two, if not set here. +# MAXFILES=32768 + +message() { + if test -n "$TTY" && test "$TTY" != "no"; then + echo "$1" >/dev/${TTY} + fi + if test -n "$SYSLOG"; then + logger -p "${SYSLOG}.warn" -t safe_asterisk[$$] "$1" + fi + if test -n "$LOGFILE"; then + echo "safe_asterisk[$$]: $1" >>"$LOGFILE" + fi +} + +# Check if Asterisk is already running. If it is, then bug out, because +# starting safe_asterisk when Asterisk is running is very bad. +VERSION=`"${ASTSBINDIR}/asterisk" -nrx 'core show version' 2>/dev/null` +if test "`echo $VERSION | cut -c 1-8`" = "Asterisk"; then + message "Asterisk is already running. $0 will exit now." + exit 1 +fi + +# since we're going to change priority and open files limits, we need to be +# root. if running asterisk as other users, pass that to asterisk on the command +# line. +# if we're not root, fall back to standard everything. +if test `id -u` != 0; then + echo "Oops. I'm not root. Falling back to standard prio and file max." >&2 + echo "This is NOT suitable for large systems." >&2 + PRIORITY=0 + message "safe_asterisk was started by `id -n` (uid `id -u`)." +else + if `uname -s | grep Linux >/dev/null 2>&1`; then + # maximum number of open files is set to the system maximum + # divided by two if MAXFILES is not set. + if test -z "$MAXFILES"; then + # just check if file-max is readable + if test -r /proc/sys/fs/file-max; then + MAXFILES=$((`cat /proc/sys/fs/file-max` / 2)) + # don't exceed upper limit of 2^20 for open + # files on systems where file-max is > 2^21 + if test $MAXFILES -gt 1048576; then + MAXFILES=1048576 + fi + fi + fi + SYSCTL_MAXFILES="fs.file-max" + elif `uname -s | grep Darwin /dev/null 2>&1`; then + SYSCTL_MAXFILES="kern.maxfiles" + fi + + + if test -n "$SYSMAXFILES"; then + if test -n "$SYSCTL_MAXFILES"; then + sysctl -w $SYSCTL_MAXFILES=$SYSMAXFILES + fi + fi + + # set the process's filemax to whatever set above + ulimit -n $MAXFILES + + if test ! -d "${ASTVARRUNDIR}"; then + mkdir -p "${ASTVARRUNDIR}" + chmod 770 "${ASTVARRUNDIR}" + fi + +fi + +if test -n "$UMASK"; then + umask $UMASK +fi + +# +# Let Asterisk dump core +# +ulimit -c unlimited + +# +# Don't fork when running "safely" +# +ASTARGS="" +if test -n "$TTY" && test "$TTY" != "no"; then + if test -c /dev/tty${TTY}; then + TTY=tty${TTY} + elif test -c /dev/vc/${TTY}; then + TTY=vc/${TTY} + elif test "$TTY" = "9"; then # ignore default if it was untouched + # If there is no /dev/tty9 and not /dev/vc/9 we don't + # necessarily want to die at this point. Pretend that + # TTY wasn't set. + TTY= + else + message "Cannot find specified TTY (${TTY})" + exit 1 + fi + if test -n "$TTY"; then + ASTARGS="${ASTARGS} -vvvg" + if test "$CONSOLE" != "no"; then + ASTARGS="${ASTARGS} -c" + fi + fi +fi + +if test ! -d "${RUNDIR}"; then + message "${RUNDIR} does not exist, creating" + if ! mkdir -p "${RUNDIR}"; then + message "Unable to create ${RUNDIR}" + exit 1 + fi +fi + +if test ! -w "${DUMPDROP}"; then + message "Cannot write to ${DUMPDROP}" + exit 1 +fi + +# +# Don't die if stdout/stderr can't be written to +# +trap '' PIPE + +# +# Run scripts to set any environment variables or do any other system-specific setup needed +# + +if test -d "${ASTETCDIR}/startup.d"; then + for script in "${ASTETCDIR}/startup.d/"*.sh; do + if test -r "${script}"; then + . "${script}" + fi + done +fi + +run_asterisk() +{ + while :; do + if test -n "$TTY" && test "$TTY" != "no"; then + cd "${RUNDIR}" + stty sane /dev/${TTY} 2>&1 /dev/null 2>&1 + scl enable php{{ fpbx_php_version }} -- ./install + -n --webroot={{ fpbx_root_dir }}/web --dbengine=mysql + --dbuser={{ fpbx_db_user }} --dbname={{ fpbx_db_name }} + --cdrdbname={{ fpbx_cdr_db_name }} --dbpass={{ fpbx_db_pass | quote }} + --astmoddir=/usr/lib64/asterisk/modules/ + --astagidir=/usr/share/asterisk/agi-bin/ + --ampsbin=/usr/local/bin + --ampcgibin=/opt/freepbx/cgi-bin + args: + chdir: "{{ fpbx_root_dir }}/tmp/freepbx" + when: fpbx_install_mode == 'install' + tags: fpbx + + # TODO: should be in a loop to patch easily several files, but checking for file presence in a loop + # is a pain with ansible + #- name: Check if webrtc class exist + # stat: path={{ fpbx_root_dir }}/web/admin/modules/webrtc/Webrtc.class.php + # register: fpbx_webrtc_class + # tags: fpbx + # + #- name: Patch webrtc class + # patch: src=patches/webrtc_proxy.patch dest={{ fpbx_root_dir }}/web/admin/modules/webrtc/Webrtc.class.php + # when: fpbx_webrtc_class.stat.exists + # tags: fpbx + +- name: Check for wrapper symlinks + stat: path=/usr/local/bin/{{ item }} + register: fpbx_wrapper_links + loop: + - fwconsole + - amportal + tags: fpbx + +- name: Remove symlinks + file: path=/usr/local/bin/{{ item.item }} state=absent + when: item.stat.islnk is defined and item.stat.islnk + loop: "{{ fpbx_wrapper_links.results }}" + tags: fpbx + +- name: Install wrappers + template: src={{ item }}.j2 dest=/usr/local/bin/{{ item }} mode=755 + loop: + - fwconsole + - amportal + tags: fpbx + +- name: Install safe_asterisk + copy: src=safe_asterisk dest=/usr/local/bin/safe_asterisk mode=755 + tags: fpbx + +- name: Ensure asterisk service is stopped and disabled + service: name=asterisk state=stopped enabled=False + tags: fpbx + +- name: Ensure /etc/systemd/system/ exists + file: path=/etc/systemd/system/ state=directory + tags: fpbx + +- name: Deploy FreePBX service unit + template: src=freepbx.service.j2 dest=/etc/systemd/system/freepbx.service + register: fpbx_unit + notify: restart freepbx + tags: fpbx + +- name: Reload systemd + systemd: daemon_reload=True + when: fpbx_unit.changed + tags: fpbx + +- name: Remove temp files + file: path={{ item }} state=absent + loop: + - "{{ fpbx_root_dir }}/tmp/freepbx-{{ fpbx_version }}-latest.tgz" + - "{{ fpbx_root_dir }}/tmp/freepbx" + tags: fpbx + + #- name: Update modules + # command: /usr/local/bin/fwconsole ma updateall + # changed_when: False + # tags: fpbx + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ fpbx_root_dir }}/meta/ansible_manager_pass" + - complex: False + when: fpbx_manager_pass is not defined + tags: fpbx +- set_fact: fpbx_manager_pass={{ rand_pass }} + when: fpbx_manager_pass is not defined + tags: fpbx + +- name: Deploy configuration + template: src={{ item }}.j2 dest=/etc/{{ item }} + loop: + - freepbx.conf + notify: + - reload freepbx + - fpbx chown + tags: fpbx + +- name: Configure manager.conf and extensions.conf + lineinfile: + path: "{{ item.file }}" + regexp: '^{{ item.param }}\s*=.*' + line: '{{ item.param }} = {{ item.value }}' + loop: + # - param: AMPMGRPASS + # value: "{{ fpbx_manager_pass }}" + # file: /etc/asterisk/extensions_additional.conf + #- param: AMPDBHOST + # value: "{{ fpbx_db_server }}" + # file: /etc/amportal.conf + #- param: AMPDBNAME + # value: "{{ fpbx_db_name }}" + # file: /etc/amportal.conf + #- param: AMPDBUSER + # value: "{{ fpbx_db_user }}" + # file: /etc/amportal.conf + #- param: AMPDBPASS + # value: "{{ fpbx_db_pass }}" + # file: /etc/amportal.conf + #- param: CDRDBNAME + # value: "{{ fpbx_cdr_db_name }}" + # file: /etc/amportal.conf + - param: secret + value: "{{ fpbx_manager_pass }}" + file: /etc/asterisk/manager.conf + tags: fpbx + +- name: Set amportal settings + command: /usr/local/bin/fwconsole setting {{ item.param }} {{ item.value }} + loop: + - param: AMPMGRUSER + value: admin + - param: AMPMGRPASS + value: "{{ fpbx_manager_pass }}" + - param: PROXY_ENABLED + value: "{{ (system_proxy is defined and system_proxy != '') | ternary('TRUE','FALSE') }}" + - param: PROXY_ADDRESS + value: "'{{ (system_proxy is defined and system_proxy != '') | ternary(system_proxy,'') }}'" + - param: AUTHTYPE + value: "{{ fpbx_auth_type }}" + - param: PHPTIMEZONE + value: "{{ system_tz | default('UTC') }}" + - param: HTTPENABLED + value: TRUE + - param: HTTPBINDADDRESS + value: 0.0.0.0 + - param: HTTPBINDPORT + value: 8088 + - param: HTTPPREFIX + value: asterisk + - param: NODEJSBINDADDRESS + value: 0.0.0.0 + - param: NODEJSHTTPSBINDADDRESS + value: 0.0.0.0 + - param: SIGNATURECHECK + value: FALSE # Needed since we're going to patch some module to pass through a rev proxy + changed_when: False + tags: fpbx + +- name: Set global language # TODO : this is an ugly hack + command: mysql --host={{ fpbx_db_server}} --user={{ fpbx_db_user }} --password={{ fpbx_db_pass | quote }} {{ fpbx_db_name }} -e "UPDATE `soundlang_settings` SET `value`='fr' WHERE `keyword`='language'" + changed_when: False + tags: fpbx + +- import_tasks: ../includes/webapps_webconf.yml + vars: + - app_id: freepbx + - php_version: "{{ fpbx_php_version }}" + - php_fpm_pool: "{{ fpbx_php_fpm_pool | default('') }}" + tags: fpbx + +- name: Deploy pre/post backup scripts + template: src={{ item }}_backup.sh.j2 dest=/etc/backup/{{ item }}.d/freepbx.sh mode=750 + loop: + - pre + - post + tags: fpbx + +- name: Install agi scripts + copy: src=agi/{{ item }} dest=/usr/share/asterisk/agi-bin/{{ item }} mode=750 group=asterisk + loop: + - jitsi_conf_pin + tags: fpbx + +- name: Handle FreePBX ports + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src | length > 0 and (item.tcp_ports | length > 0 or item.udp_ports | length > 0)) | ternary('present','absent') }}" + rules: "{% if item.tcp_ports is defined and item.tcp_ports | length > 0 %}-A INPUT -m state --state NEW -p tcp -m multiport --dports {{ item.tcp_ports | join(',') }} -s {{ item.src | join(',') }} -j ACCEPT\n{% endif %} + {% if item.udp_ports is defined and item.udp_ports | length > 0 %}-A INPUT -m state --state NEW -p udp -m multiport --dports {{ item.udp_ports | join(',') }} -s {{ item.src | join(',') }} -j ACCEPT{% endif %}" + when: iptables_manage | default(True) + loop: + - name: fpbx_mgm_ports + tcp_ports: "{{ fpbx_mgm_tcp_ports }}" + udp_ports: "{{ fpbx_mgm_udp_ports }}" + src: "{{ fpbx_mgm_src_ip }}" + - name: fpbx_voip_ports + tcp_ports: "{{ fpbx_voip_tcp_ports }}" + udp_ports: "{{ fpbx_voip_udp_ports }}" + src: "{{ fpbx_voip_src_ip }}" + - name: fpbx_http_ports + tcp_ports: "{{ fpbx_http_ports }}" + src: "{{ fpbx_http_src_ip }}" + - name: fpbx_prov_ports + tcp_ports: "{{ fpbx_prov_tcp_ports }}" + udp_ports: "{{ fpbx_prov_udp_ports }}" + src: "{{ fpbx_prov_src_ip }}" + tags: fpbx,firewall + +- name: Remove old iptables rules + iptables_raw: + name: "{{ item }}" + state: absent + loop: + - ast_mgm_tcp_ports + - ast_mgm_udp_ports + - ast_voip_tcp_ports + - ast_voip_udp_ports + - ast_http_ports + tags: fpbx,firewall + +- name: Install logrotate config + template: src=logrotate.conf.j2 dest=/etc/logrotate.d/asterisk + tags: fpbx + +- name: Start and enable the service + service: name=freepbx state=started enabled=True + tags: fpbx + +- import_tasks: ../includes/webapps_post.yml + vars: + - root_dir: "{{ fpbx_root_dir }}" + - version: "{{ fpbx_version }}" + tags: fpbx + +- include: filebeat.yml diff --git a/roles/freepbx/templates/amportal.j2 b/roles/freepbx/templates/amportal.j2 new file mode 100644 index 0000000..efa78e9 --- /dev/null +++ b/roles/freepbx/templates/amportal.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +scl enable php{{ fpbx_php_version }} -- /var/lib/asterisk/bin/amportal "$@" diff --git a/roles/freepbx/templates/asterisk/manager.conf.j2 b/roles/freepbx/templates/asterisk/manager.conf.j2 new file mode 100644 index 0000000..4244ca6 --- /dev/null +++ b/roles/freepbx/templates/asterisk/manager.conf.j2 @@ -0,0 +1,28 @@ +; +; AMI - Asterisk Manager interface +; +; FreePBX needs this to be enabled. Note that if you enable it on a different IP, you need +; to assure that this can't be reached from un-authorized hosts with the ACL settings (permit/deny). +; Also, remember to configure non-default port or IP-addresses in amportal.conf. +; +; The AMI connection is used both by the portal and the operator's panel in FreePBX. +; +; FreePBX assumes an AMI connection to localhost:5038 by default. +; +[general] +enabled = yes +port = 5038 +bindaddr = 0.0.0.0 +displayconnects=no ;only effects 1.6+ + +[admin] +secret = {{ fpbx_manager_pass }} +deny=0.0.0.0/0.0.0.0 +permit=127.0.0.1/255.255.255.0 +read = system,call,log,verbose,command,agent,user,config,command,dtmf,reporting,cdr,dialplan,originate,message +write = system,call,log,verbose,command,agent,user,config,command,dtmf,reporting,cdr,dialplan,originate,message +writetimeout = 5000 + +#include manager_additional.conf +#include manager_custom.conf + diff --git a/roles/freepbx/templates/filebeat.yml.j2 b/roles/freepbx/templates/filebeat.yml.j2 new file mode 100644 index 0000000..de9bc54 --- /dev/null +++ b/roles/freepbx/templates/filebeat.yml.j2 @@ -0,0 +1,9 @@ +- type: log + enabled: True + paths: + - /var/log/asterisk/full + - /var/log/asterisk/*.log + - /var/lib/asterisk/.pm2/pm2.log + exclude_files: + - '\.[xg]z$' + - '\.\d+$' diff --git a/roles/freepbx/templates/freepbx.conf.j2 b/roles/freepbx/templates/freepbx.conf.j2 new file mode 100644 index 0000000..9e659e9 --- /dev/null +++ b/roles/freepbx/templates/freepbx.conf.j2 @@ -0,0 +1,13 @@ + diff --git a/roles/freepbx/templates/freepbx.service.j2 b/roles/freepbx/templates/freepbx.service.j2 new file mode 100644 index 0000000..c2f085a --- /dev/null +++ b/roles/freepbx/templates/freepbx.service.j2 @@ -0,0 +1,19 @@ + +[Unit] +Description=FreePBX VoIP Server +{% if fpbx_db_server == 'localhost' or fpbx_db_server == '127.0.0.1' %} +Requires=mariadb.service +{% endif %} + +[Service] +Type=forking +ExecStart=/usr/local/bin/fwconsole start -q +ExecStop=/usr/local/bin/fwconsole stop -q +ExecReload=/usr/local/bin/fwconsole reload -q +SyslogIdentifier=FreePBX +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/roles/freepbx/templates/fwconsole.j2 b/roles/freepbx/templates/fwconsole.j2 new file mode 100644 index 0000000..2867424 --- /dev/null +++ b/roles/freepbx/templates/fwconsole.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +scl enable php{{ fpbx_php_version }} -- /var/lib/asterisk/bin/fwconsole "$@" diff --git a/roles/freepbx/templates/httpd.conf.j2 b/roles/freepbx/templates/httpd.conf.j2 new file mode 100644 index 0000000..21a51ed --- /dev/null +++ b/roles/freepbx/templates/httpd.conf.j2 @@ -0,0 +1,20 @@ +{% if fpbx_alias is defined %} +Alias /{{ fpbx_alias }} {{ fpbx_root_dir }}/web/ +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + +ProxyTimeout 900 +RewriteEngine On + + AllowOverride All + Options FollowSymLinks +{% if fpbx_src_ip is defined %} + Require ip {{ fpbx_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ fpbx_php_fpm_pool | default('freepbx') }}.sock|fcgi://localhost" + + diff --git a/roles/freepbx/templates/logrotate.conf.j2 b/roles/freepbx/templates/logrotate.conf.j2 new file mode 100644 index 0000000..57aa842 --- /dev/null +++ b/roles/freepbx/templates/logrotate.conf.j2 @@ -0,0 +1,27 @@ +/var/log/asterisk/messages +/var/log/asterisk/event_log +/var/log/asterisk/queue_log +/var/log/asterisk/full +/var/log/asterisk/security +/var/log/asterisk/freepbx.log +/var/log/asterisk/freepbx_security.log +/var/log/asterisk/ucp_err.log +/var/log/asterisk/ucp_out.log +/var/log/asterisk/cdr-csv/Master.csv +{ + missingok + notifempty + su asterisk asterisk + create 0640 asterisk asterisk + sharedscripts + daily + rotate 365 + compress + compressoptions -T0 + compresscmd /usr/bin/xz + compressext .xz + uncompresscmd /usr/bin/unxz + postrotate + /usr/sbin/asterisk -rx 'logger reload' >/dev/null 2>/dev/null || true + endscript +} diff --git a/roles/freepbx/templates/perms.sh.j2 b/roles/freepbx/templates/perms.sh.j2 new file mode 100644 index 0000000..675fc76 --- /dev/null +++ b/roles/freepbx/templates/perms.sh.j2 @@ -0,0 +1,18 @@ +#!/bin/sh + +restorecon -R {{ fpbx_root_dir }} +chmod 755 {{ fpbx_root_dir }} +chown root:root {{ fpbx_root_dir }}/{meta,db_dumps} +chmod 700 {{ fpbx_root_dir }}/{meta,db_dumps} +setfacl -k -b {{ fpbx_root_dir }} +setfacl -m u:asterisk:rx,u:{{ httpd_user | default('apache') }}:rx {{ fpbx_root_dir }} +chown -R root:root {{ fpbx_root_dir }}/web +chown -R asterisk:asterisk {{ fpbx_root_dir }}/{tmp,sessions,web} +chmod 755 {{ fpbx_root_dir }}/provisioning +chown -R asterisk:asterisk {{ fpbx_root_dir }}/provisioning +setfacl -m u:phone:rX {{ fpbx_root_dir }}/provisioning/* +setfacl -R -m u:phone:rwX {{ fpbx_root_dir }}/provisioning/{contacts,logs,overrides,licenses,bmp} +chmod 700 {{ fpbx_root_dir }}/{tmp,sessions} +find {{ fpbx_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ fpbx_root_dir }}/web -type d -exec chmod 755 "{}" \; +scl enable php{{ fpbx_php_version }} -- /usr/local/bin/fwconsole chown diff --git a/roles/freepbx/templates/php.conf.j2 b/roles/freepbx/templates/php.conf.j2 new file mode 100644 index 0000000..1eca03b --- /dev/null +++ b/roles/freepbx/templates/php.conf.j2 @@ -0,0 +1,45 @@ +; {{ ansible_managed }} + +[freepbx] + +listen.owner = root +listen.group = {{ httpd_user | default('apache') }} +listen.mode = 0660 +listen = /run/php-fpm/freepbx.sock +user = asterisk +group = asterisk +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 60m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 512M +php_admin_value[session.save_path] = {{ fpbx_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ fpbx_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ fpbx_root_dir }}/tmp +php_admin_value[post_max_size] = 50M +php_admin_value[upload_max_filesize] = 50M +php_admin_value[max_execution_time] = 900 +php_admin_value[max_input_time] = 900 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = on +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on + +; Needed so that the #!/usr/bin/env php shebang will point to the correct PHP version +env[PATH] = /opt/remi/php{{ fpbx_php_version }}/root/usr/bin:/opt/remi/php{{ fpbx_php_version }}/root/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin +{% if system_proxy is defined and system_proxy != '' %} +env[http_proxy] = {{ system_proxy }} +env[https_proxy] = {{ system_proxy }} +{% if system_proxy_no_proxy is defined and system_proxy_no_proxy | length > 0 %} +env[no_proxy] = {{ system_proxy_no_proxy | join(',') }} +{% endif %} +{% endif %} diff --git a/roles/freepbx/templates/post_backup.sh.j2 b/roles/freepbx/templates/post_backup.sh.j2 new file mode 100644 index 0000000..3346c11 --- /dev/null +++ b/roles/freepbx/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ fpbx_root_dir }}/backup/* diff --git a/roles/freepbx/templates/pre_backup.sh.j2 b/roles/freepbx/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..7d43f24 --- /dev/null +++ b/roles/freepbx/templates/pre_backup.sh.j2 @@ -0,0 +1,20 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump \ + --quick --single-transaction \ +{% if fpbx_db_server not in ['127.0.0.1', 'localhost'] %} + --user={{ fpbx_db_user }} \ + --password={{ fpbx_db_pass | quote }} \ + --host={{ fpbx_db_server }} \ +{% endif %} + --add-drop-table {{ fpbx_db_name }} | zstd -T0 -c > {{ fpbx_root_dir }}/backup/{{ fpbx_db_name }}.sql.zst +/usr/bin/mysqldump \ + --quick --single-transaction \ +{% if fpbx_db_server not in ['127.0.0.1', 'localhost'] %} + --user={{ fpbx_db_user }} \ + --password={{ fpbx_db_pass | quote }} \ + --host={{ fpbx_db_server }} \ +{% endif %} + --add-drop-table {{ fpbx_cdr_db_name }} | zstd -T0 -c > {{ fpbx_root_dir }}/backup/{{ fpbx_cdr_db_name }}.sql.zst diff --git a/roles/freepbx/templates/vsftpd/chroot_list.j2 b/roles/freepbx/templates/vsftpd/chroot_list.j2 new file mode 100644 index 0000000..68ab536 --- /dev/null +++ b/roles/freepbx/templates/vsftpd/chroot_list.j2 @@ -0,0 +1 @@ +phone diff --git a/roles/freepbx/templates/vsftpd/pam.j2 b/roles/freepbx/templates/vsftpd/pam.j2 new file mode 100644 index 0000000..ed819d5 --- /dev/null +++ b/roles/freepbx/templates/vsftpd/pam.j2 @@ -0,0 +1,7 @@ +#%PAM-1.0 +session optional pam_keyinit.so force revoke +auth required pam_listfile.so item=user sense=deny file=/etc/vsftpd/ftpusers onerr=succeed +auth include password-auth +account include password-auth +session required pam_loginuid.so +session include password-auth diff --git a/roles/freepbx/templates/vsftpd/user_list.j2 b/roles/freepbx/templates/vsftpd/user_list.j2 new file mode 100644 index 0000000..68ab536 --- /dev/null +++ b/roles/freepbx/templates/vsftpd/user_list.j2 @@ -0,0 +1 @@ +phone diff --git a/roles/freepbx/templates/vsftpd/vsftpd.conf.j2 b/roles/freepbx/templates/vsftpd/vsftpd.conf.j2 new file mode 100644 index 0000000..3d39a48 --- /dev/null +++ b/roles/freepbx/templates/vsftpd/vsftpd.conf.j2 @@ -0,0 +1,15 @@ +anonymous_enable=NO +local_enable=YES +write_enable=YES +local_umask=007 +xferlog_enable=YES +xferlog_std_format=YES +chroot_list_enable=YES +listen=YES +pam_service_name=vsftpd +userlist_enable=YES +tcp_wrappers=YES +userlist_deny=NO +pasv_enable=YES +pasv_min_port=40000 +pasv_max_port=40100 diff --git a/roles/freepbx/vars/RedHat-7.yml b/roles/freepbx/vars/RedHat-7.yml new file mode 100644 index 0000000..280d0ed --- /dev/null +++ b/roles/freepbx/vars/RedHat-7.yml @@ -0,0 +1,32 @@ +--- + +fpbx_packages: + - asterisk + - asterisk-voicemail + - asterisk-pjsip + - asterisk-sip + - asterisk-mysql + - asterisk-ael + - asterisk-iax2 + - asterisk-dahdi + - asterisk-fax + - asterisk-ldap + - asterisk-misdn + - asterisk-mp3 + - asterisk-odbc + - mysql-connector-odbc + - mpg123 + - lame + - opus + - nmap + - nodejs + - tar + - mariadb + - MySQL-python + - acl + - gcc-c++ # needed for ucp + - icu + - libicu-devel + - patch + - vsftpd + diff --git a/roles/freepbx/vars/RedHat-8.yml b/roles/freepbx/vars/RedHat-8.yml new file mode 100644 index 0000000..2420c72 --- /dev/null +++ b/roles/freepbx/vars/RedHat-8.yml @@ -0,0 +1,31 @@ +--- + +fpbx_packages: + - asterisk + - asterisk-voicemail + - asterisk-pjsip + - asterisk-sip + - asterisk-mysql + - asterisk-ael + - asterisk-iax2 + - asterisk-dahdi + - asterisk-fax + - asterisk-ldap + - asterisk-mp3 + - asterisk-odbc + - mariadb-connector-odbc + - mpg123 +# - lame + - opus + - nmap + - nodejs + - tar + - mariadb + - python3-mysql + - acl + - gcc-c++ # needed for ucp + - icu + - libicu-devel + - patch + - vsftpd + diff --git a/roles/funkwhale/defaults/main.yml b/roles/funkwhale/defaults/main.yml new file mode 100644 index 0000000..3472180 --- /dev/null +++ b/roles/funkwhale/defaults/main.yml @@ -0,0 +1,55 @@ +--- + +funkwhale_version: 1.1.2 +funkwhale_id: 1 +# https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/{{ funkwhale_version }}/download?job=build_front +# https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/{{ funkwhale_version }}/download?job=build_api +funkwhale_base_url: https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/{{ funkwhale_version }}/download +funkwhale_archive_sha1: + api: 43c123ab0f19e81169372d79a3b322cb2e079974 + front: 6a5d2b586fd54dd433a7aeb7ef6fe166485d4a13 +funkwhale_root_dir: /opt/funkwhale_{{ funkwhale_id }} + +# Should ansible manage upgrades of funkwhale, or only initial install +funkwhale_manage_upgrade: True + +# A random one will be created if not defined +# funkwhale_secret_key: + +funkwhale_user: funkwhale_{{ funkwhale_id }} + +funkwhale_api_bind_ip: 127.0.0.1 +funkwhale_api_port: 5006 + +# Set to your public URL +funkwhale_public_url: https://{{ inventory_hostname }} + +# Database param +funkwhale_db_server: "{{ pg_server | default('localhost') }}" +funkwhale_db_port: 5432 +funkwhale_db_name: funkwhale_{{ funkwhale_id }} +funkwhale_db_user: funkwhale_{{ funkwhale_id }} +# A rand pass will be created if not defined +# funkwhale_db_pass: + +# Cache param +funkwhale_redis_url: redis://127.0.0.1:6379/0 + +# LDAP param +funkwhale_ldap_auth: False +funkwhale_ldap_url: "{{ ad_auth | default(False) | ternary('ldap://' + ad_realm | default(samba_realm) | default(ansible_domain) | lower, ldap_auth | default(False) | ternary(ldap_uri, '')) }}" +# funkwhale_bind_dn: CN=Funkwhale,OU=Apps,DC=example,DC=org +# funkwhale_bind_pass: S3cR3t. +funkwhale_ldap_user_filter: "{{ ad_auth | default(False) | ternary('(&(objectClass=user)(sAMAccountName={0}))','(&(objectClass=inetOrgPerson)(uid={0}))') }}" +funkwhale_ldap_base: "{{ ad_auth | default(False) | ternary((ad_ldap_user_search_base is defined) | ternary(ad_ldap_user_search_base,'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC=')), ldap_auth | ternary(ldap_user_base + ',' + ldap_base, '')) }}" +funkwhale_ldap_attr_map: "first_name:givenName, last_name:sn, username:{{ ad_auth | ternary('sAMAccountName', 'uid') }}, email:mail" + +# dict of library ID <-> path from which to import music +funkwhale_libraries: [] +# funkwhale_libraries: +# - id: 7b64b90c-353d-4969-8ab4-dafdf049036e +# path: /opt/funkwhale/data/music +# inplace: True + +# Increase on busy servers (but will require more memory) +funkwhale_web_workers: 1 diff --git a/roles/funkwhale/handlers/main.yml b/roles/funkwhale/handlers/main.yml new file mode 100644 index 0000000..c16ac94 --- /dev/null +++ b/roles/funkwhale/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: restart funkwhale + service: name=funkwhale_{{ funkwhale_id }}-{{ item }} state=restarted + loop: + - server + - worker + - beat diff --git a/roles/funkwhale/meta/main.yml b/roles/funkwhale/meta/main.yml new file mode 100644 index 0000000..f16e6b1 --- /dev/null +++ b/roles/funkwhale/meta/main.yml @@ -0,0 +1,13 @@ +--- +allow_duplicates: true +dependencies: + - role: repo_rpmfusion # for ffmpeg + - role: repo_xsendfile # mod_xsendfile is not available in base repo for EL8 + when: + - ansible_os_family == 'RedHat' + - ansible_distribution_major_version is version('8','>=') + - role: httpd_common + - role: redis_server + when: funkwhale_redis_url | urlsplit('hostname') == 'localhost' or funkwhale_redis_url | urlsplit('hostname') == '127.0.0.1' + - role: postgresql_server + when: funkwhale_db_server == 'localhost' or funkwhale_db_server == '127.0.0.1' diff --git a/roles/funkwhale/tasks/archive_post.yml b/roles/funkwhale/tasks/archive_post.yml new file mode 100644 index 0000000..511f786 --- /dev/null +++ b/roles/funkwhale/tasks/archive_post.yml @@ -0,0 +1,11 @@ +--- + +- name: Compress previous version + command: tar cf {{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}.tar.zst --use-compress-program=zstd ./ + args: + chdir: "{{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}" + warn: False + environment: + ZSTD_CLEVEL: 10 + tags: funkwhale + diff --git a/roles/funkwhale/tasks/archive_pre.yml b/roles/funkwhale/tasks/archive_pre.yml new file mode 100644 index 0000000..8dc0d19 --- /dev/null +++ b/roles/funkwhale/tasks/archive_pre.yml @@ -0,0 +1,32 @@ +--- + +- name: Create archive dir + file: path={{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }} state=directory + tags: funkwhale + +- name: Archive previous version + synchronize: + src: "{{ funkwhale_root_dir }}/{{ item }}" + dest: "{{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}/" + recursive: True + delete: True + loop: + - api + - front + - venv + delegate_to: "{{ inventory_hostname }}" + tags: funkwhale + +- name: Archive a database dump + command: > + /usr/pgsql-14/bin/pg_dump + --clean + --create + --host={{ funkwhale_db_server }} + --port={{ funkwhale_db_port }} + --username=sqladmin {{ funkwhale_db_name }} + --file={{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}/{{ funkwhale_db_name }}.sql + environment: + - PGPASSWORD: "{{ pg_admin_pass }}" + tags: funkwhale + diff --git a/roles/funkwhale/tasks/cleanup.yml b/roles/funkwhale/tasks/cleanup.yml new file mode 100644 index 0000000..49c0903 --- /dev/null +++ b/roles/funkwhale/tasks/cleanup.yml @@ -0,0 +1,12 @@ +--- + +- name: Remove temp files + file: path={{ funkwhale_root_dir }}/{{ item }} state=absent + loop: + - tmp/api.zip + - tmp/api + - tmp/front.zip + - tmp/front + - archives/{{ funkwhale_current_version }} + - db_dumps + tags: funkwhale diff --git a/roles/funkwhale/tasks/conf.yml b/roles/funkwhale/tasks/conf.yml new file mode 100644 index 0000000..f6f2dc5 --- /dev/null +++ b/roles/funkwhale/tasks/conf.yml @@ -0,0 +1,17 @@ +--- + +- name: Deploy permissions script + template: src=perms.sh.j2 dest={{ funkwhale_root_dir }}/perms.sh mode=755 + register: funkwhale_perms + tags: funkwhale + +- name: Set optimal permissions + command: "{{ funkwhale_root_dir }}/perms.sh" + when: funkwhale_install_mode != 'none' or funkwhale_perms.changed + tags: funkwhale + +- name: Deploy apache config + template: src=httpd.conf.j2 dest=/etc/httpd/ansible_conf.d/40-funkwhale_{{ funkwhale_id }}.conf + notify: reload httpd + tags: funkwhale + diff --git a/roles/funkwhale/tasks/directories.yml b/roles/funkwhale/tasks/directories.yml new file mode 100644 index 0000000..d97cf51 --- /dev/null +++ b/roles/funkwhale/tasks/directories.yml @@ -0,0 +1,30 @@ +--- + +- name: Create directories + file: + path: "{{ funkwhale_root_dir }}/{{ item.dir }}" + state: directory + owner: "{{ item.user | default(omit) }}" + group: "{{ item.group | default(omit) }}" + mode: "{{ item.mode | default(omit) }}" + loop: + - dir: / + - dir: api + - dir: front + - dir: data + - dir: data/media + - dir: data/music + - dir: data/static + - dir: config + group: "{{ funkwhale_user }}" + mode: 750 + - dir: archives + mode: 700 + - dir: meta + mode: 700 + - dir: tmp + mode: 700 + - dir: backup + mode: 700 + tags: funkwhale + diff --git a/roles/funkwhale/tasks/facts.yml b/roles/funkwhale/tasks/facts.yml new file mode 100644 index 0000000..2622d80 --- /dev/null +++ b/roles/funkwhale/tasks/facts.yml @@ -0,0 +1,42 @@ +--- + +- include_vars: "{{ item }}" + with_first_found: + - vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_distribution }}.yml + - vars/{{ ansible_os_family }}.yml + tags: funkwhale + +- fail: msg="pg_admin_pass must be set" + when: pg_admin_pass is not defined + tags: funkwhale + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ funkwhale_root_dir }}" + - version: "{{ funkwhale_version }}" + tags: funkwhale +- block: + - set_fact: funkwhale_install_mode={{ (install_mode == 'upgrade' and not funkwhale_manage_upgrade) | ternary('none',install_mode) }} + - set_fact: funkwhale_current_version={{ current_version | default('') }} + tags: funkwhale + + # Create a random pass for the DB if needed +- when: funkwhale_db_pass is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ funkwhale_root_dir }}/meta/ansible_dbpass" + - set_fact: funkwhale_db_pass={{ rand_pass }} + tags: funkwhale + + # Create a random django secret key +- when: funkwhale_secret_key is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ funkwhale_root_dir }}/meta/ansible_django_key" + - set_fact: funkwhale_secret_key={{ rand_pass }} + tags: funkwhale + diff --git a/roles/funkwhale/tasks/install.yml b/roles/funkwhale/tasks/install.yml new file mode 100644 index 0000000..ba786e9 --- /dev/null +++ b/roles/funkwhale/tasks/install.yml @@ -0,0 +1,183 @@ +--- + +- name: Install packages + yum: name={{ funkwhale_packages }} + tags: funkwhale + +- name: Check if mod_xsendfile is available + stat: path=/usr/lib64/httpd/modules/mod_xsendfile.so + register: funkwhale_xsendfile + tags: funkwhale + +- name: Download funkwhale frontend and api + get_url: + url: "{{ funkwhale_base_url }}?job=build_{{ item }}" + dest: "{{ funkwhale_root_dir }}/tmp/{{ item }}.zip" + checksum: sha1:{{ funkwhale_archive_sha1[item] }} + when: funkwhale_install_mode != 'none' + loop: + - front + - api + tags: funkwhale + +- name: Extract funkwhale archives + unarchive: + src: "{{ funkwhale_root_dir }}/tmp/{{ item }}.zip" + dest: "{{ funkwhale_root_dir }}/tmp/" + remote_src: True + when: funkwhale_install_mode != 'none' + loop: + - front + - api + tags: funkwhale + +- name: Move files to their final location + synchronize: + src: "{{ funkwhale_root_dir }}/tmp/{{ item }}/" + dest: "{{ funkwhale_root_dir }}/{{ item }}/" + recursive: True + delete: True + loop: + - api + - front + delegate_to: "{{ inventory_hostname }}" + when: funkwhale_install_mode != 'none' + tags: funkwhale + +- name: Create the PostgreSQL role + postgresql_user: + db: postgres + name: "{{ funkwhale_db_user }}" + password: "{{ funkwhale_db_pass }}" + login_host: "{{ funkwhale_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + tags: funkwhale + +- name: Create the PostgreSQL database + postgresql_db: + name: "{{ funkwhale_db_name }}" + encoding: UTF-8 + lc_collate: C + lc_ctype: C + template: template0 + owner: "{{ funkwhale_db_user }}" + login_host: "{{ funkwhale_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + tags: funkwhale + +- name: Enable required PostgreSQL extensions + postgresql_ext: + name: "{{ item }}" + db: "{{ funkwhale_db_name }}" + login_host: "{{ funkwhale_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + loop: + - unaccent + - citext + tags: funkwhale + +- name: Wipe the venv on upgrade + file: path={{ funkwhale_root_dir }}/venv state=absent + when: funkwhale_install_mode == 'upgrade' + tags: funkwhale + +- name: Create the venv dir + file: path={{ funkwhale_root_dir }}/venv state=directory + tags: funkwhale + +- name: Create the virtualenv + pip: + name: + - wheel + - pip + - virtualenv + - service_identity + virtualenv: "{{ funkwhale_root_dir }}/venv" + virtualenv_command: /bin/virtualenv-3 + virtualenv_python: /bin/python3 + when: funkwhale_install_mode != 'none' + notify: restart funkwhale + tags: funkwhale + +- name: Install python modules in the virtualenv + pip: + requirements: "{{ funkwhale_root_dir }}/api/requirements.txt" + virtualenv: "{{ funkwhale_root_dir }}/venv" + virtualenv_command: /bin/virtualenv-3 + virtualenv_python: /bin/python3 + when: funkwhale_install_mode != 'none' + notify: restart funkwhale + tags: funkwhale + +- name: Deploy funkwhale configuration + template: src=env.j2 dest={{ funkwhale_root_dir }}/config/.env group={{ funkwhale_user }} + notify: restart funkwhale + tags: funkwhale + +- name: Migrate database + django_manage: + command: migrate + app_path: "{{ funkwhale_root_dir }}/api" + virtualenv: "{{ funkwhale_root_dir }}/venv" + environment: + - FUNKWHALE_URL: "{{ funkwhale_public_url }}" + when: funkwhale_install_mode != 'none' + notify: restart funkwhale + tags: funkwhale + +- name: Collect static files + django_manage: + command: collectstatic + app_path: "{{ funkwhale_root_dir }}/api" + virtualenv: "{{ funkwhale_root_dir }}/venv" + when: funkwhale_install_mode != 'none' + tags: funkwhale + +- name: Deploy systemd units + template: src=funkwhale-{{ item }}.service.j2 dest=/etc/systemd/system/funkwhale_{{ funkwhale_id }}-{{ item }}.service + register: funkwhale_units + loop: + - server + - worker + - beat + notify: restart funkwhale + tags: funkwhale + +- name: Deploy library update units + template: src=funkwhale-update-media.{{ item }}.j2 dest=/etc/systemd/system/funkwhale_{{ funkwhale_id }}-update-media.{{ item }} + register: funkwhale_media_updater + loop: + - service + - timer + tags: funkwhale + +- name: Reload systemd + systemd: daemon_reload=True + when: (funkwhale_units.results + funkwhale_media_updater.results) | selectattr('changed','equalto',True) | list | length > 0 + tags: funkwhale + +- name: Deploy pre and post backup scripts + template: src={{ item }}-backup.sh.j2 dest=/etc/backup/{{ item }}.d/funkwhale_{{ funkwhale_id }}.sh mode=750 + loop: + - pre + - post + tags: funkwhale + + # When upgrading to funkwhale 1.0, we have to rebuild thumbnails +- block: + - name: Wipe the thumbnail directory + file: path={{ funkwhale_root_dir }}/data/media/__sized__ state=absent + + - name: Rebuild thumbnails + django_manage: + command: fw media generate-thumbnails + app_path: "{{ funkwhale_root_dir }}/api" + virtualenv: "{{ funkwhale_root_dir }}/venv" + + when: + - funkwhale_install_mode == 'upgrade' + - funkwhale_current_version is version('1.0', '<') + tags: funkwhale diff --git a/roles/funkwhale/tasks/main.yml b/roles/funkwhale/tasks/main.yml new file mode 100644 index 0000000..7a3ac5a --- /dev/null +++ b/roles/funkwhale/tasks/main.yml @@ -0,0 +1,14 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: funkwhale_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: service.yml +- include: write_version.yml +- include: archive_post.yml + when: funkwhale_install_mode == 'upgrade' +- include: cleanup.yml diff --git a/roles/funkwhale/tasks/service.yml b/roles/funkwhale/tasks/service.yml new file mode 100644 index 0000000..aed14bd --- /dev/null +++ b/roles/funkwhale/tasks/service.yml @@ -0,0 +1,9 @@ +--- + +- name: Start and enable funkwhale services + systemd: name=funkwhale_{{ funkwhale_id }}-{{ item }} state=started enabled=True + loop: + - server.service + - update-media.timer + tags: funkwhale + diff --git a/roles/funkwhale/tasks/user.yml b/roles/funkwhale/tasks/user.yml new file mode 100644 index 0000000..beac574 --- /dev/null +++ b/roles/funkwhale/tasks/user.yml @@ -0,0 +1,10 @@ +--- + +- name: Create a system user account + user: + name: "{{ funkwhale_user }}" + comment: "Funkwhale system user" + system: True + shell: /sbin/nologin + home: "{{ funkwhale_root_dir }}" + tags: funkwhale diff --git a/roles/funkwhale/tasks/write_version.yml b/roles/funkwhale/tasks/write_version.yml new file mode 100644 index 0000000..921a7f8 --- /dev/null +++ b/roles/funkwhale/tasks/write_version.yml @@ -0,0 +1,6 @@ +--- + +- name: Write version + copy: content={{ funkwhale_version }} dest={{ funkwhale_root_dir }}/meta/ansible_version + tags: funkwhale + diff --git a/roles/funkwhale/templates/env.j2 b/roles/funkwhale/templates/env.j2 new file mode 100644 index 0000000..3b331f1 --- /dev/null +++ b/roles/funkwhale/templates/env.j2 @@ -0,0 +1,34 @@ +FUNKWHALE_API_IP=127.0.0.1 +FUNKWHALE_API_PORT={{ funkwhale_api_port }} +FUNKWHALE_WEB_WORKERS={{ funkwhale_web_workers }} +FUNKWHALE_HOSTNAME={{ funkwhale_public_url | urlsplit('hostname') }} +FUNKWHALE_PROTOCOL={{ funkwhale_public_url | urlsplit('scheme') }} +EMAIL_CONFIG=smtp://127.0.0.1 +DEFAULT_FROM_EMAIL=funkwhale-noreply@{{ ansible_domain }} +REVERSE_PROXY_TYPE=apache2 +DATABASE_URL='postgresql://{{ funkwhale_db_user }}:{{ funkwhale_db_pass | urlencode | regex_replace('/','%2F') }}@{{ funkwhale_db_server }}:{{ funkwhale_db_port }}/{{ funkwhale_db_name }}' +CACHE_URL={{ funkwhale_redis_url }} +MEDIA_ROOT={{ funkwhale_root_dir }}/data/media +STATIC_ROOT={{ funkwhale_root_dir }}/data/static +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_SECRET_KEY='{{ funkwhale_secret_key }}' +RAVEN_ENABLED=False +RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 +MUSIC_DIRECTORY_PATH={{ funkwhale_root_dir }}/data/music +{% if funkwhale_ldap_url is defined %} +LDAP_ENABLED=True +LDAP_SERVER_URI={{ funkwhale_ldap_url }} +LDAP_START_TLS={{ (funkwhale_ldap_url | urlsplit('scheme') == 'ldaps' or funkwhale_ldap_url | urlsplit('hostname') == '127.0.0.1' or funkwhale_ldap_url | urlsplit('hostname') == 'localhost') | ternary('False', 'True') }} +{% if funkwhale_ldap_bind_dn is defined and funkwhale_ldap_bind_pass is defined %} +LDAP_BIND_DN='{{ funkwhale_ldap_bind_dn }}' +LDAP_BIND_PASSWORD='{{ funkwhale_ldap_bind_pass }}' +{% endif %} +LDAP_SEARCH_FILTER='{{ funkwhale_ldap_user_filter }}' +LDAP_ROOT_DN='{{ funkwhale_ldap_base }}' +LDAP_USER_ATTR_MAP='{{ funkwhale_ldap_attr_map }}' +{% endif %} +FUNKWHALE_FRONTEND_PATH={{ funkwhale_root_dir }}/front/dist +NGINX_MAX_BODY_SIZE=100M +MUSIC_USE_DENORMALIZATION=True +FUNKWHALE_SPA_HTML_ROOT={{ funkwhale_root_dir }}/front/dist/ +FUNKWHALE_URL={{ funkwhale_public_url }} diff --git a/roles/funkwhale/templates/funkwhale-beat.service.j2 b/roles/funkwhale/templates/funkwhale-beat.service.j2 new file mode 100644 index 0000000..6088328 --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-beat.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Funkwhale celery beat process +After=redis.service postgresql.service + +[Service] +User={{ funkwhale_user }} +WorkingDirectory={{ funkwhale_root_dir }}/api +EnvironmentFile={{ funkwhale_root_dir }}/config/.env +ExecStart={{ funkwhale_root_dir }}/venv/bin/celery -A funkwhale_api.taskapp beat -l INFO --pidfile /tmp/funkwhale-beat.pid --schedule /tmp/funkwhale-beat-schedule.db +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=funkwhale_{{ funkwhale_id }}-beat +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/roles/funkwhale/templates/funkwhale-server.service.j2 b/roles/funkwhale/templates/funkwhale-server.service.j2 new file mode 100644 index 0000000..083131e --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-server.service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=Funkwhale application server +After=redis.service postgresql.service +Wants=funkwhale_{{ funkwhale_id }}-worker.service funkwhale_{{ funkwhale_id }}-beat.service + +[Service] +User={{ funkwhale_user }} +WorkingDirectory={{ funkwhale_root_dir }}/api +EnvironmentFile={{ funkwhale_root_dir }}/config/.env +ExecStart={{ funkwhale_root_dir }}/venv/bin/gunicorn config.asgi:application -w ${FUNKWHALE_WEB_WORKERS} -k uvicorn.workers.UvicornWorker -b ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT} +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=funkwhale_{{ funkwhale_id }}-server +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/roles/funkwhale/templates/funkwhale-update-media.service.j2 b/roles/funkwhale/templates/funkwhale-update-media.service.j2 new file mode 100644 index 0000000..b101f71 --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-update-media.service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=Update funkwhale media library + +[Service] +Type=oneshot +{% for lib in funkwhale_libraries %} +ExecStart={{ funkwhale_root_dir }}/venv/bin/python \ + {{ funkwhale_root_dir }}/api/manage.py \ + import_files {{ lib.id }} \ + --no-input{% if lib.inplace %} --in-place{% endif %} \ + "{{ lib.path }}" \ + --recursive +{% endfor %} +ExecStart={{ funkwhale_root_dir }}/venv/bin/python \ + {{ funkwhale_root_dir }}/api/manage.py \ + check_inplace_files \ + --no-dry-run +ExecStart={{ funkwhale_root_dir }}/venv/bin/python \ + {{ funkwhale_root_dir }}/api/manage.py \ + prune_library \ + --tracks --albums --artists --no-dry-run +User={{ funkwhale_user }} +Group={{ funkwhale_user }} diff --git a/roles/funkwhale/templates/funkwhale-update-media.timer.j2 b/roles/funkwhale/templates/funkwhale-update-media.timer.j2 new file mode 100644 index 0000000..bea23b7 --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-update-media.timer.j2 @@ -0,0 +1,8 @@ +[Unit] +Description=Update funkwhale media library + +[Timer] +OnCalendar=daily + +[Install] +WantedBy=timers.target diff --git a/roles/funkwhale/templates/funkwhale-worker.service.j2 b/roles/funkwhale/templates/funkwhale-worker.service.j2 new file mode 100644 index 0000000..5ddfda4 --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-worker.service.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=Funkwhale celery worker +After=redis.service postgresql.service + +[Service] +User={{ funkwhale_user }} +WorkingDirectory={{ funkwhale_root_dir }}/api +EnvironmentFile={{ funkwhale_root_dir }}/config/.env +ExecStart={{ funkwhale_root_dir }}/venv/bin/celery -A funkwhale_api.taskapp worker -l INFO --pool=solo --concurrency=1 +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=1024M +SyslogIdentifier=funkwhale_{{ funkwhale_id }}-worker +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/roles/funkwhale/templates/httpd.conf.j2 b/roles/funkwhale/templates/httpd.conf.j2 new file mode 100644 index 0000000..91ddf59 --- /dev/null +++ b/roles/funkwhale/templates/httpd.conf.j2 @@ -0,0 +1,81 @@ + + ServerName {{ funkwhale_public_url | urlsplit('hostname') }} + ProxyVia On + ProxyPreserveHost On + + RemoteIPHeader X-Forwarded-For + + + AddDefaultCharset off + Order Allow,Deny + Allow from all + + + + LimitRequestBody 104857600 + ProxyPass http://127.0.0.1:{{ funkwhale_api_port }}/ + ProxyPassReverse http://127.0.0.1:{{ funkwhale_api_port }}/ + + + ProxyPass http://127.0.0.1:{{ funkwhale_api_port }}/federation + ProxyPassReverse http://127.0.0.1:{{ funkwhale_api_port }}/federation + + + + ProxyPass http://127.0.0.1:{{ funkwhale_api_port }}/api/subsonic/rest + ProxyPassReverse http://127.0.0.1:{{ funkwhale_api_port }}/api/subsonic/rest + + + + ProxyPass http://127.0.0.1:{{ funkwhale_api_port }}/.well-known/ + ProxyPassReverse http://127.0.0.1:{{ funkwhale_api_port }}/.well-known/ + + + + ProxyPass "!" + + + Alias /front {{ funkwhale_root_dir }}/front/dist/ + + + ProxyPass "!" + + Alias /media {{ funkwhale_root_dir }}/data/media/ + + + ProxyPass "!" + + Alias /staticfiles {{ funkwhale_root_dir }}/data/static + + + ProxyPass ws://127.0.0.1:{{ funkwhale_api_port }}/api/v1/activity + + + + Options FollowSymLinks + AllowOverride None + Require all granted + + + + Options FollowSymLinks + AllowOverride None + Require all granted + + + + Options FollowSymLinks + AllowOverride None + Require all granted + + +{% if funkwhale_xsendfile.stat.exists %} + LoadModule xsendfile_module modules/mod_xsendfile.so +{% endif %} + + XSendFile On + XSendFilePath {{ funkwhale_root_dir }}/data/media + XSendFilePath {{ funkwhale_root_dir }}/data/music + SetEnv MOD_X_SENDFILE_ENABLED 1 + + diff --git a/roles/funkwhale/templates/perms.sh.j2 b/roles/funkwhale/templates/perms.sh.j2 new file mode 100644 index 0000000..3220984 --- /dev/null +++ b/roles/funkwhale/templates/perms.sh.j2 @@ -0,0 +1,15 @@ +#!/bin/bash + +chown -R root:root {{ funkwhale_root_dir }}/{front,api} +chmod 755 {{ funkwhale_root_dir }} +chown {{ funkwhale_user }}:apache {{ funkwhale_root_dir }}/data +chmod 750 {{ funkwhale_root_dir }}/data +chown -R {{ funkwhale_user }}:{{ funkwhale_user }} {{ funkwhale_root_dir }}/data/{media,music} +chown -R root:root {{ funkwhale_root_dir }}/data/static +find {{ funkwhale_root_dir }}/{front,api,data/static} -type f -exec chmod 644 "{}" \; +find {{ funkwhale_root_dir }}/{front,api} -type d -exec chmod 755 "{}" \; +chmod 755 {{ funkwhale_root_dir }}/api/manage.py +chmod 700 {{ funkwhale_root_dir }}/{meta,db_dumps,archives} +chown -R root:{{ funkwhale_user }} {{ funkwhale_root_dir }}/config +chmod 750 {{ funkwhale_root_dir }}/config +chmod 640 {{ funkwhale_root_dir }}/config/.env diff --git a/roles/funkwhale/templates/post-backup.sh.j2 b/roles/funkwhale/templates/post-backup.sh.j2 new file mode 100644 index 0000000..5a86235 --- /dev/null +++ b/roles/funkwhale/templates/post-backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ funkwhale_root_dir }}/backup/{{ funkwhale_db_name }}.sql.zst diff --git a/roles/funkwhale/templates/pre-backup.sh.j2 b/roles/funkwhale/templates/pre-backup.sh.j2 new file mode 100644 index 0000000..3afe803 --- /dev/null +++ b/roles/funkwhale/templates/pre-backup.sh.j2 @@ -0,0 +1,11 @@ +#!/bin/sh + +set -eo pipefail + +PGPASSWORD={{ funkwhale_db_pass | quote }} /usr/pgsql-14/bin/pg_dump \ + --clean \ + --create \ + --username={{ funkwhale_db_user | quote }} \ + --host={{ funkwhale_db_server | quote }} \ + {{ funkwhale_db_name | quote }} | \ + zstd -c > {{ funkwhale_root_dir }}/backup/{{ funkwhale_db_name | quote }}.sql.zst diff --git a/roles/funkwhale/vars/RedHat-7.yml b/roles/funkwhale/vars/RedHat-7.yml new file mode 100644 index 0000000..afc9ca4 --- /dev/null +++ b/roles/funkwhale/vars/RedHat-7.yml @@ -0,0 +1,17 @@ +--- + +funkwhale_packages: + - gcc + - git + - postgresql14 + - postgresql-devel + - openldap-devel + - cyrus-sasl-devel + - libjpeg-turbo-devel + - python-psycopg2 + - python-setuptools + - python3-virtualenv + - python3-pip + - ffmpeg + - mod_xsendfile + diff --git a/roles/funkwhale/vars/RedHat-8.yml b/roles/funkwhale/vars/RedHat-8.yml new file mode 100644 index 0000000..6936f30 --- /dev/null +++ b/roles/funkwhale/vars/RedHat-8.yml @@ -0,0 +1,16 @@ +--- + +funkwhale_packages: + - gcc + - git + - postgresql14 + - postgresql-devel + - openldap-devel + - cyrus-sasl-devel + - libjpeg-turbo-devel + - python3-psycopg2 + - python3-setuptools + - python3-virtualenv + - python3-pip + - ffmpeg + - mod_xsendfile diff --git a/roles/fusioninventory_agent/defaults/main.yml b/roles/fusioninventory_agent/defaults/main.yml new file mode 100644 index 0000000..6b6ee1e --- /dev/null +++ b/roles/fusioninventory_agent/defaults/main.yml @@ -0,0 +1,17 @@ +--- + +fusinv_uri: [] +fusinv_user: user +fusinv_pass: secret +fusinv_disabled_tasks: + - ESX + - WakeOnLan + - NetDiscovery + - Deploy + - NetInventory + +# Not included in debian repo +# so we need to manually down and install it +fusinv_deb_version: 2.4.2-1 + +... diff --git a/roles/fusioninventory_agent/handlers/main.yml b/roles/fusioninventory_agent/handlers/main.yml new file mode 100644 index 0000000..86eaa3f --- /dev/null +++ b/roles/fusioninventory_agent/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart fusioninventory-agent + service: name=fusioninventory-agent state=restarted enabled=yes diff --git a/roles/fusioninventory_agent/tasks/install_Debian.yml b/roles/fusioninventory_agent/tasks/install_Debian.yml new file mode 100644 index 0000000..5d16452 --- /dev/null +++ b/roles/fusioninventory_agent/tasks/install_Debian.yml @@ -0,0 +1,45 @@ +--- + +- when: > + (ansible_distribution == 'Debian' and ansible_distribution_major_version is version('11', '<')) or + (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('20', '<')) + block: + - name: Install dependencies + apt: + name: + - dmidecode + - hwdata + - ucf + - hdparm + - perl + - libuniversal-require-perl + - libwww-perl + - libparse-edid-perl + - libproc-daemon-perl + - libproc-pid-file-perl + - libfile-which-perl + - libxml-treepp-perl + - libyaml-perl + - libnet-cups-perl + - libnet-ip-perl + - libdigest-sha-perl + - libsocket-getaddrinfo-perl + - libtext-template-perl + + - name: Install fusioninventory + apt: deb=http://ftp.fr.debian.org/debian/pool/main/f/fusioninventory-agent/fusioninventory-agent_{{ fusinv_deb_version }}_all.deb + environment: + - http_proxy: "{{ system_proxy | default('') }}" + + tags: inventory + +- when: > + (ansible_distribution == 'Debian' and ansible_distribution_major_version is version('11', '>=')) or + (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('20', '>=')) + block: + - name: Install FusionInventory Agent + apt: + name: + - fusioninventory-agent + tags: inventory + diff --git a/roles/fusioninventory_agent/tasks/install_RedHat.yml b/roles/fusioninventory_agent/tasks/install_RedHat.yml new file mode 100644 index 0000000..1b37977 --- /dev/null +++ b/roles/fusioninventory_agent/tasks/install_RedHat.yml @@ -0,0 +1,6 @@ +--- + +- name: Install FusionInventory Agent + yum: name=fusioninventory-agent + tags: inventory + diff --git a/roles/fusioninventory_agent/tasks/main.yml b/roles/fusioninventory_agent/tasks/main.yml new file mode 100644 index 0000000..89014f3 --- /dev/null +++ b/roles/fusioninventory_agent/tasks/main.yml @@ -0,0 +1,24 @@ +--- + +- include: install_{{ ansible_os_family }}.yml + +- name: Deploy FusionInventory Agent config + template: src=agent.cfg.j2 dest=/etc/fusioninventory/agent.cfg mode=640 + notify: restart fusioninventory-agent + tags: inventory + +- name: Check if the first inventory has been done + stat: path=/var/lib/fusioninventory-agent/FusionInventory-Agent.dump + register: first_inventory + tags: inventory + +- name: First Fusion Inventory report + command: /usr/bin/fusioninventory-agent + when: not first_inventory.stat.exists + tags: inventory + +- name: Start FusionInventory Agent + service: name=fusioninventory-agent state=started enabled=yes + tags: inventory + +... diff --git a/roles/fusioninventory_agent/templates/agent.cfg.j2 b/roles/fusioninventory_agent/templates/agent.cfg.j2 new file mode 100644 index 0000000..aede73d --- /dev/null +++ b/roles/fusioninventory_agent/templates/agent.cfg.j2 @@ -0,0 +1,7 @@ +server={{ fusinv_uri | join(',') | quote }} +user={{ fusinv_user | quote }} +password={{ fusinv_pass | quote }} +no-p2p +no-httpd +httpd-ip="127.0.0.1" +no-task={{ fusinv_disabled_tasks | join(',') | quote }} diff --git a/roles/g2cs/README.md b/roles/g2cs/README.md new file mode 100644 index 0000000..a5f3a29 --- /dev/null +++ b/roles/g2cs/README.md @@ -0,0 +1,17 @@ +# G2CS + +This is a small daemon writtent in perl to allow a bridge between Graylog and Crowdsec. +This idea is that if you collect your logs to a graylog instance, you can forward them all in a single stream from Graylog to CrowdSec, instead of collecting them all again on every hosts. + +So, this small g2cs daemon is a very simple perl utility which will listen on a port for a syslog stream. It should run a the server which will host your single crowdsec instance. + +On graylog, you have to install the syslog-output plugin, and configure it to output the streams you want to this daemon. You should choose UDP, the port on which g2cs binds, and the CEF format. + +When g2cs receive this stream of logs, it'll just make simple transformations so that your logs can be consumed by crowdsec : + + * nginx logs go to nginx/ + * httpd logs go to httpd/ + * squid logs go to squid/ + * Everything else goes to syslog.log + +Now, you can configure your acquisitions on crowdsec to just read these locations diff --git a/roles/g2cs/defaults/main.yml b/roles/g2cs/defaults/main.yml new file mode 100644 index 0000000..f2a19d7 --- /dev/null +++ b/roles/g2cs/defaults/main.yml @@ -0,0 +1,11 @@ +--- + +# Port on which g2cs will listen +g2cs_port: 3514 + +# Where log files will be created. Thos files won't grow too large as g2cs truncates them after 10000 lines +# so better to use a tmpfs +g2cs_log_dir: /run/g2cs/logs + +# List of IP/CIDR for which g2cs port will be reachable +g2cs_src_ip: [] diff --git a/roles/g2cs/files/g2cs.pl b/roles/g2cs/files/g2cs.pl new file mode 100644 index 0000000..63b1662 --- /dev/null +++ b/roles/g2cs/files/g2cs.pl @@ -0,0 +1,183 @@ +#!/usr/bin/perl -w + +use IO::Socket; +use Getopt::Long; +use File::Basename; +use File::Path qw(make_path); +use IO::Handle; + +my $maxlen = 16384; +my $port = 514; +my $maxlines = 10000; +my $logdir = '/run/cs-gelf-server/'; + +GetOptions( + "port=i" => \$port, + "maxlines=i" => \$maxlines, + "logdir=s" => \$logdir +); + +if ($port !~ /^\d+$/ or $port < 1 or $port > 65535){ + die "Invalid port $port\n"; +} +if ($maxlines !~ /^\d+/ or $maxlines < 10){ + die "Invalid max line specified\n"; +} +if (not -d $logdir){ + die "$logdir doesn't exists or is not a directory\n"; +} + +# Remove trailing / of the logdir, it's not nice in the logs when you have double / +$logdir =~ s/\/$//; + +# Create files so crowdsec can open them before any lines are written +foreach my $dir (qw(nginx httpd zimbra pveproxy)){ + if (not -d $logdir . '/' . $dir){ + make_path($logdir . '/' . $dir) + } +} +foreach my $file (qw(syslog.log nginx/access.log nginx/error.log httpd/access.log httpd/error.log zimbra/mailbox.log)){ + open(FILE, '>', $logdir . '/' . $file); + print FILE ''; + close FILE; +} + +# List of syslog_identifier we're not intersted in +my @ignored_syslog_id = qw( + c-icap + charon + unbound + sudo + zed + zimbramon + systemd + systemd-logind + CROND + ttrss_1 + turnserver + syncoid + influxd +); +# List of log files we're not interested in +my @ignored_log_files = qw( + /var/log/audit/audit.log + /var/log/squid/cache.log + /var/log/squid/access.log + /var/log/ufdbGuard/ufdbguardd.log + /opt/zimbra/log/gc.log + /var/log/samba/json/auth.log + /var/log/samba/json/dsdb.log + /var/log/samba/json/dsdb_password.log + /var/log/samba/json/dsdb_transaction.log +); + +print "Start listening on UDP port $port\n"; +$sock = IO::Socket::INET->new( + LocalPort => $port, + Proto => 'udp' + ) or die("Socket: $@"); + +my $buf; +my $cnt = {}; +my $loghandles = {}; + +while (1) { + $sock->recv($buf, $maxlen); + my ($port, $ipaddr) = sockaddr_in($sock->peername); + my $fields = {}; + + # We're not really interested in CEF headers. So let's extract + # the various fields + $buf =~ m/(?:(?:CEF:\d+\|)(?:[^=\\]+\|)+)(.*)/; + my $ext = $1; + + # Taken from https://github.com/DavidJBianco/pycef + while ($ext =~ m/([^=\s]+)=((?:[\\]=|[^=])+)(?:\s|$)/g) { + $fields->{$1} = $2; + # Unescape value string + $fields->{$1} =~ s/\\=/=/g; + } + + # Skip lines we're not interested in early. + # So crowdsec will eat less CPU parsing useless stuff + if ( + defined $fields->{syslog_identifier} and grep { $_ eq $fields->{syslog_identifier} } @ignored_syslog_id or + defined $fields->{log_file_path} and grep { $_ eq $fields->{log_file_path} } @ignored_log_files + ) { + next; + } + + # We need a timestamp, a source and a msg at least + if (not defined $fields->{timestamp} or not defined $fields->{source} or not defined $fields->{msg}){ + next; + } + + my $msg; + # Default log will be syslog + my $logfile = $logdir . '/syslog.log'; + + # But for some services, we need special handling. Eg for web access logs + if (defined $fields->{event_dataset}){ + if ($fields->{event_dataset} =~ m/^nginx\.(access|ingress_controller)/){ + $logfile = $logdir . '/nginx/access.log'; + $msg = $fields->{msg}; + } elsif ($fields->{event_dataset} =~ m/^nginx\.error/){ + $logfile = $logdir . '/nginx/error.log'; + $msg = $fields->{msg}; + } elsif ($fields->{event_dataset} =~ m/^apache\.access/){ + $logfile = $logdir . '/httpd/access.log'; + $msg = $fields->{msg}; + } elsif ($fields->{event_dataset} =~ m/^apache\.error/){ + $logfile = $logdir . '/httpd/access.log'; + $msg = $fields->{msg}; + } + } elsif (defined $fields->{log_file_path}){ + if ($fields->{log_file_path} eq '/var/log/pveproxy/access.log'){ + $logfile = $logdir . '/pveproxy/access.log'; + $msg = $fields->{msg}; + } elsif ($fields->{log_file_path} eq '/opt/zimbra/log/nginx.access.log'){ + $logfile = $logdir . '/nginx/access.log'; + $msg = $fields->{msg}; + } elsif ($fields->{log_file_path} eq '/opt/zimbra/log/mailbox.log'){ + $logfile = $logdir . '/zimbra/mailbox.log'; + $msg = $fields->{msg}; + } + } elsif (defined $fields->{application_name}){ + if ($fields->{application_name} eq 'nginx'){ + $logfile = $logdir . '/nginx/access.log'; + $msg = $fields->{msg}; + } + } + + # OK, no special handling (else $msg would be defined), so let's + # provide a syslog format + if (not defined $msg){ + $msg .= $fields->{timestamp} . ' ' . $fields->{source} . ' '; + my $id = $fields->{syslog_identifier} || $fields->{program} || $fields->{application_name} || $fields->{process_name} || 'unknown'; + # For older PfSense, which sent invalid syslog messages, we might extract + # the syslog identifier from the begining of the message + if ($id eq 'unknown' and $fields->{msg} =~ m/^(\w+(\[\d+\])?):\s(.*)/){ + $id = $1; + $fields->{msg} = $3; + } + $msg .= $id; + # Try to append the pid of the process + if ($id ne 'kernel' and $id ne 'filterlog' and $id !~ m/\[\d+\]$/){ + $msg .= '['; + $msg .= $fields->{process_pid} || $fields->{process_id} || $fields->{pid} || '0'; + $msg .= ']'; + } + $msg .= ': ' . $fields->{msg}; + } + + defined $loghandles->{$logfile} or open($loghandles->{$logfile}, ">>", $logfile); + # Truncate the file so it's not growing too large + # Crowdsec will read it in nearly real time anyway + if ($cnt->{$logfile}++ > $maxlines){ + print "Truncating $logfile\n"; + truncate $loghandles->{$logfile}, 0; + $cnt->{$logfile} = 0; + } + print { $loghandles->{$logfile} } $msg . "\n"; + $loghandles->{$logfile}->flush; +}; diff --git a/roles/g2cs/handlers/main.yml b/roles/g2cs/handlers/main.yml new file mode 100644 index 0000000..4715c8a --- /dev/null +++ b/roles/g2cs/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart g2cs + service: name=g2cs state=restarted diff --git a/roles/g2cs/tasks/install.yml b/roles/g2cs/tasks/install.yml new file mode 100644 index 0000000..692ab7d --- /dev/null +++ b/roles/g2cs/tasks/install.yml @@ -0,0 +1,38 @@ +--- + +- name: Install dependencies + yum: + name: + - perl-IO + - perl-Getopt-Long + tags: cs + +- name: Install main script + copy: src=g2cs.pl dest=/usr/local/bin/g2cs mode=755 + notify: restart g2cs + tags: cs + +- name: Deploy systemd unit + template: src=g2cs.service.j2 dest=/etc/systemd/system/g2cs.service + notify: restart g2cs + register: g2cs_unit + tags: cs + +- name: Reload systemd + systemd: daemon_reload=True + when: g2cs_unit.changed + tags: cs + +- name: Deploy tmpfiles.d config + copy: + content: | + d /run/g2cs 0755 g2cs g2cs - - + d /run/g2cs/logs 0700 g2cs g2cs - - + dest: /etc/tmpfiles.d/g2cs.conf + register: g2cs_tmpfiles + tags: cs + +- name: Create tmpfiles dir + command: systemd-tmpfiles --create + when: g2cs_tmpfiles.changed + tags: cs diff --git a/roles/g2cs/tasks/iptables.yml b/roles/g2cs/tasks/iptables.yml new file mode 100644 index 0000000..cc3aa3f --- /dev/null +++ b/roles/g2cs/tasks/iptables.yml @@ -0,0 +1,8 @@ +--- + +- name: Handle g2cs port in the firewall + iptables_raw: + name: g2cs_port + state: "{{ (g2cs_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -p udp --dport {{ g2cs_port }} -s {{ g2cs_src_ip | join(',') }} -j ACCEPT" + tags: firewall,cs diff --git a/roles/g2cs/tasks/main.yml b/roles/g2cs/tasks/main.yml new file mode 100644 index 0000000..e656205 --- /dev/null +++ b/roles/g2cs/tasks/main.yml @@ -0,0 +1,7 @@ +--- + +- include: user.yml +- include: install.yml +- include: iptables.yml + when: iptables_manage | default(True) +- include: service.yml diff --git a/roles/g2cs/tasks/service.yml b/roles/g2cs/tasks/service.yml new file mode 100644 index 0000000..731357e --- /dev/null +++ b/roles/g2cs/tasks/service.yml @@ -0,0 +1,5 @@ +--- + +- name: Start and enable the service + service: name=g2cs state=started enabled=True + tags: cs diff --git a/roles/g2cs/tasks/user.yml b/roles/g2cs/tasks/user.yml new file mode 100644 index 0000000..9e80d69 --- /dev/null +++ b/roles/g2cs/tasks/user.yml @@ -0,0 +1,5 @@ +--- + +- name: Create g2cs user account + user: name=g2cs system=True shell=/sbin/nologin + tags: cs diff --git a/roles/g2cs/templates/g2cs.service.j2 b/roles/g2cs/templates/g2cs.service.j2 new file mode 100644 index 0000000..21fc99b --- /dev/null +++ b/roles/g2cs/templates/g2cs.service.j2 @@ -0,0 +1,26 @@ +[Unit] +Description=Graylog to Crowdsec syslog daemon +After=syslog.target +Before=crowdsec.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/g2cs --port={{ g2cs_port }} --logdir={{ g2cs_log_dir }} +User=g2cs +Group=g2cs +Restart=always +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +SyslogIdentifier=g2cs + +# Allow binding on privileged ports +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE + + +[Install] +WantedBy=multi-user.target + diff --git a/roles/geoipupdate/defaults/main.yml b/roles/geoipupdate/defaults/main.yml new file mode 100644 index 0000000..4b1e733 --- /dev/null +++ b/roles/geoipupdate/defaults/main.yml @@ -0,0 +1,7 @@ +--- + +geoip_account_id: +geoip_license_key: +geoip_edition_ids: + - GeoLite2-Country + - GeoLite2-City diff --git a/roles/geoipupdate/handlers/main.yml b/roles/geoipupdate/handlers/main.yml new file mode 100644 index 0000000..7d7e277 --- /dev/null +++ b/roles/geoipupdate/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: start geoipupdate + service: name=geoipupdate state=started diff --git a/roles/geoipupdate/tasks/main.yml b/roles/geoipupdate/tasks/main.yml new file mode 100644 index 0000000..cf32c88 --- /dev/null +++ b/roles/geoipupdate/tasks/main.yml @@ -0,0 +1,32 @@ +--- + +- name: Install geoipupdate + yum: + name: + - geoipupdate + tags: geoip + +- name: Deploy configuration + template: src=GeoIP.conf.j2 dest=/etc/GeoIP.conf mode=600 + notify: start geoipupdate + tags: geoip + +- name: Deploy geoipupdate units + template: src=geoipupdate.{{ item }}.j2 dest=/etc/systemd/system/geoipupdate.{{ item }} + loop: + - timer + - service + register: geoip_units + tags: geoip + +- name: Reload systemd + systemd: daemon_reload=True + when: geoip_units.results | selectattr('changed', 'equalto', True) | list | length > 0 + tags: geoip + +- name: Handle geoip timer + systemd: + name: geoipupdate.timer + state: "{{ (geoip_account_id is defined and geoip_license_key is defined) | ternary('started', 'stopped') }}" + enabled: "{{ (geoip_account_id is defined and geoip_license_key is defined) | ternary(True, False) }}" + tags: geoip diff --git a/roles/geoipupdate/templates/GeoIP.conf.j2 b/roles/geoipupdate/templates/GeoIP.conf.j2 new file mode 100644 index 0000000..0917298 --- /dev/null +++ b/roles/geoipupdate/templates/GeoIP.conf.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +AccountID {{ geoip_account_id | default('0000000') }} +LicenseKey {{ geoip_license_key | default('00000000') }} +EditionIDs {{ geoip_edition_ids | join(' ') }} diff --git a/roles/geoipupdate/templates/geoipupdate.service.j2 b/roles/geoipupdate/templates/geoipupdate.service.j2 new file mode 100644 index 0000000..4f2cfb8 --- /dev/null +++ b/roles/geoipupdate/templates/geoipupdate.service.j2 @@ -0,0 +1,7 @@ +[Unit] +Description=Update MaxMind GeoIP databases + +[Service] +Type=oneshot +ExecStart=/usr/bin/geoipupdate +TimeoutSec=600 diff --git a/roles/geoipupdate/templates/geoipupdate.timer.j2 b/roles/geoipupdate/templates/geoipupdate.timer.j2 new file mode 100644 index 0000000..c337c98 --- /dev/null +++ b/roles/geoipupdate/templates/geoipupdate.timer.j2 @@ -0,0 +1,9 @@ +[Unit] +Description=Update MaxMind GeoIP databases + +[Timer] +OnCalendar=weekly +Persistent=yes + +[Install] +WantedBy=timers.target diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml new file mode 100644 index 0000000..31656bc --- /dev/null +++ b/roles/gitea/defaults/main.yml @@ -0,0 +1,39 @@ +--- + +# Version to install +gitea_version: 1.15.6 +# URL to the binary +gitea_bin_url: https://dl.gitea.io/gitea/{{ gitea_version }}/gitea-{{ gitea_version }}-linux-amd64 +# sha256 of the binary +gitea_bin_sha256: 1b7473b5993e07b33fec58edbc1a90f15f040759ca4647e97317c33d5dfe58be +# Handle updates. If set to false, ansible will only install +# Gitea and then won't touch an existing installation +gitea_manage_upgrade: True +# Root directory of the gitea +gitea_root_dir: /opt/gitea + +# The domain name will be used to build GIT URL in the UI +gitea_domain: "{{ inventory_hostname }}" +# Used to build ssh URL. Can be different from gitea_domain, if using a reverse proxy for example +gitea_ssh_domain: "{{ gitea_domain }}" +# Set to the public URL where gitea will be available +gitea_public_url: 'http://%(DOMAIN)s:%(HTTP_PORT)s/' +# Port of the web interface (plain text http) +gitea_web_port: 3280 +# Port for SSH access +gitea_ssh_port: 22 +# Used to restrict access to the web interface +gitea_web_src_ip: [] +# If set, will read username from the following HTTP header +# use when behind a reverse proxy +# gitea_username_header: Auth-User + +# Enable user registration +gitea_registration: False + +# Database settings +gitea_db_server: "{{ mysql_server | default('localhost') }}" +gitea_db_name: gitea +gitea_db_user: gitea +# A random pass will be created if not set here +# gitea_db_pass: xxxxx diff --git a/roles/gitea/handlers/main.yml b/roles/gitea/handlers/main.yml new file mode 100644 index 0000000..39c1c74 --- /dev/null +++ b/roles/gitea/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart gitea + service: name=gitea state=restarted diff --git a/roles/gitea/meta/main.yml b/roles/gitea/meta/main.yml new file mode 100644 index 0000000..96a7b1c --- /dev/null +++ b/roles/gitea/meta/main.yml @@ -0,0 +1,6 @@ +--- +dependencies: + - role: repo_scl + when: + - ansible_os_family == 'RedHat' + - ansible_distribution_major_version is version('8', '<') diff --git a/roles/gitea/tasks/admin_user.yml b/roles/gitea/tasks/admin_user.yml new file mode 100644 index 0000000..def0a60 --- /dev/null +++ b/roles/gitea/tasks/admin_user.yml @@ -0,0 +1,30 @@ +--- +- name: Check if admin user exists + command: "mysql --host={{ gitea_db_server }} --user={{ gitea_db_user }} --password='{{ gitea_db_pass }}' {{ gitea_db_name }} -ss -e \"select count(*) from user where lower_name='gitadmin'\"" + register: gitea_admin + changed_when: False + retries: 10 # first time gitea starts, it'll take some time to create the tables + delay: 10 + until: gitea_admin.rc == 0 + tags: gitea + + # The user table is created before the email_address. So on first run, we might have an error when creating the + # admin account. Here, we just ensure the email_address table exists before we can continue +- name: Check if the email_address table exists + command: "mysql --host={{ gitea_db_server }} --user={{ gitea_db_user }} --password='{{ gitea_db_pass }}' {{ gitea_db_name }} -ss -e \"select count(*) from email_address\"" + register: gitea_email_table + changed_when: False + retries: 10 + delay: 10 + until: gitea_email_table.rc == 0 + when: gitea_admin.stdout != "1" + tags: gitea + +- name: Create the admin account + command: "{{ gitea_root_dir }}/bin/gitea admin user create --name gitadmin --admin --password admin --email admin@example.net --config {{ gitea_root_dir }}/etc/app.ini" + args: + chdir: "{{ gitea_root_dir }}" + become_user: gitea + when: gitea_admin.stdout != "1" + tags: gitea + diff --git a/roles/gitea/tasks/archive_post.yml b/roles/gitea/tasks/archive_post.yml new file mode 100644 index 0000000..ece0450 --- /dev/null +++ b/roles/gitea/tasks/archive_post.yml @@ -0,0 +1,6 @@ +--- +- import_tasks: ../includes/webapps_compress_archive.yml + vars: + - root_dir: "{{ gitea_root_dir }}" + - version: "{{ gitea_current_version }}" + tags: gitea diff --git a/roles/gitea/tasks/archive_pre.yml b/roles/gitea/tasks/archive_pre.yml new file mode 100644 index 0000000..ed9ff05 --- /dev/null +++ b/roles/gitea/tasks/archive_pre.yml @@ -0,0 +1,23 @@ +--- +- name: Create archive directory + file: path={{ gitea_root_dir }}/archives/{{ gitea_current_version }} state=directory mode=700 + tags: gitea + +- name: Archive previous version + copy: src={{ gitea_root_dir }}/bin/gitea dest={{ gitea_root_dir }}/archives/{{ gitea_current_version }} remote_src=True + tags: gitea + +- name: Archive the database + mysql_db: + state: dump + name: "{{ gitea_db_name }}" + target: "{{ gitea_root_dir }}/archives/{{ gitea_current_version }}/{{ gitea_db_name }}.sql.xz" + login_host: "{{ gitea_db_server | default(mysql_server) }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + quick: True + single_transaction: True + environment: + XZ_OPT: -T0 + tags: gitea + diff --git a/roles/gitea/tasks/cleanup.yml b/roles/gitea/tasks/cleanup.yml new file mode 100644 index 0000000..3eb64a9 --- /dev/null +++ b/roles/gitea/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove tmp and obsolete files + file: path={{ item }} state=absent + loop: + - /etc/profile.d/git.sh + - "{{ gitea_root_dir }}/db_dumps" + tags: gitea diff --git a/roles/gitea/tasks/conf.yml b/roles/gitea/tasks/conf.yml new file mode 100644 index 0000000..f3e154b --- /dev/null +++ b/roles/gitea/tasks/conf.yml @@ -0,0 +1,34 @@ +--- + +- name: Create random tokens + shell: "{{ gitea_root_dir }}/bin/gitea generate secret {{ item }} > {{ gitea_root_dir }}/meta/ansible_{{ item }}" + args: + creates: "{{ gitea_root_dir }}/meta/ansible_{{ item }}" + with_items: + - INTERNAL_TOKEN + - LFS_JWT_SECRET + - SECRET_KEY + - JWT_SECRET + tags: gitea + +- name: Read random tokens + command: cat {{ gitea_root_dir }}/meta/ansible_{{ item }} + with_items: + - INTERNAL_TOKEN + - LFS_JWT_SECRET + - SECRET_KEY + - JWT_SECRET + changed_when: False + register: gitea_tokens + tags: gitea + +- name: Deploy gitea configuration + template: src=app.ini.j2 dest={{ gitea_root_dir }}/etc/app.ini owner=root group=gitea mode=0660 + notify: restart gitea + tags: gitea + +- name: Set optimal permissions + command: "{{ gitea_root_dir }}/perms.sh" + changed_when: False + tags: gitea + diff --git a/roles/gitea/tasks/directories.yml b/roles/gitea/tasks/directories.yml new file mode 100644 index 0000000..76a92f9 --- /dev/null +++ b/roles/gitea/tasks/directories.yml @@ -0,0 +1,28 @@ +--- +- name: Create directory structure + file: + path: "{{ gitea_root_dir }}/{{ item.dir }}" + state: directory + owner: "{{ item.owner | default('gitea') }}" + group: "{{ item.group | default('gitea') }}" + mode: "{{ item.mode | default('750') }}" + loop: + - dir: / + owner: gitea + group: gitea + - dir: data + - dir: data/repositories + - dir: custom + - dir: public + - dir: etc + - dir: tmp + - dir: bin + - dir: meta + owner: root + group: root + mode: 700 + - dir: backup + owner: root + group: root + mode: 700 + tags: gitea diff --git a/roles/gitea/tasks/facts.yml b/roles/gitea/tasks/facts.yml new file mode 100644 index 0000000..4ab0d7c --- /dev/null +++ b/roles/gitea/tasks/facts.yml @@ -0,0 +1,36 @@ +--- + +- include_vars: "{{ item }}" + with_first_found: + - vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_distribution }}.yml + - vars/{{ ansible_os_family }}.yml + tags: gitea + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ gitea_root_dir }}" + - version: "{{ gitea_version }}" + tags: gitea +- set_fact: gitea_install_mode={{ (install_mode == 'upgrade' and not gitea_manage_upgrade) | ternary('none',install_mode) }} + tags: gitea +- set_fact: gitea_current_version={{ current_version | default('') }} + tags: gitea + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ gitea_root_dir }}/meta/ansible_key" + tags: gitea +- set_fact: gitea_key={{ rand_pass }} + tags: gitea + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ gitea_root_dir }}/meta/ansible_dbpass" + when: gitea_db_pass is not defined + tags: gitea +- set_fact: gitea_db_pass={{ rand_pass }} + when: gitea_db_pass is not defined + tags: gitea + diff --git a/roles/gitea/tasks/install.yml b/roles/gitea/tasks/install.yml new file mode 100644 index 0000000..f8619ce --- /dev/null +++ b/roles/gitea/tasks/install.yml @@ -0,0 +1,61 @@ +--- +- name: Install packages + yum: name={{ gitea_packages }} + tags: gitea + +- name: Download gitea binary + get_url: + url: "{{ gitea_bin_url }}" + dest: "{{ gitea_root_dir }}/tmp/gitea" + checksum: "sha256:{{ gitea_bin_sha256 }}" + when: gitea_install_mode != 'none' + notify: restart gitea + tags: gitea + +- name: Move gitea binary + command: mv -f {{ gitea_root_dir }}/tmp/gitea {{ gitea_root_dir }}/bin/ + when: gitea_install_mode != 'none' + tags: gitea + +- name: Make gitea executable + file: path={{ gitea_root_dir }}/bin/gitea mode=0755 + tags: gitea + +- name: Deploy gitea service unit + template: src=gitea.service.j2 dest=/etc/systemd/system/gitea.service + register: gitea_unit + notify: restart gitea + tags: gitea + +- name: Reload systemd + systemd: daemon_reload=True + when: gitea_unit.changed + tags: gitea + + # Create MySQL database +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ gitea_db_name }}" + - db_user: "{{ gitea_db_user }}" + - db_server: "{{ gitea_db_server }}" + - db_pass: "{{ gitea_db_pass }}" + tags: gitea + +- name: Deploy pre/post backup scripts + template: src={{ item }}_backup.sh.j2 dest=/etc/backup/{{ item }}.d/gitea.sh mode=0750 + with_items: + - pre + - post + tags: gitea + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ gitea_root_dir }}/perms.sh mode=755 + tags: gitea + +- name: Set correct SELinux context + sefcontext: + target: "{{ gitea_root_dir }}/.ssh(/.*)?" + setype: ssh_home_t + state: present + when: ansible_selinux.status == 'enabled' + tags: gitea diff --git a/roles/gitea/tasks/iptables.yml b/roles/gitea/tasks/iptables.yml new file mode 100644 index 0000000..9816d3c --- /dev/null +++ b/roles/gitea/tasks/iptables.yml @@ -0,0 +1,14 @@ +--- + +- name: Handle gitea ports in the firewall + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ item.port }} -s {{ item.src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + with_items: + - port: "{{ gitea_web_port }}" + name: gitea_web_port + src_ip: "{{ gitea_web_src_ip }}" + tags: firewall,gitea + diff --git a/roles/gitea/tasks/main.yml b/roles/gitea/tasks/main.yml new file mode 100644 index 0000000..84c71f3 --- /dev/null +++ b/roles/gitea/tasks/main.yml @@ -0,0 +1,16 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: gitea_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml +- include: service.yml +- include: admin_user.yml +- include: archive_post.yml + when: gitea_install_mode == 'upgrade' +- include: write_version.yml +- include: cleanup.yml diff --git a/roles/gitea/tasks/service.yml b/roles/gitea/tasks/service.yml new file mode 100644 index 0000000..42e54aa --- /dev/null +++ b/roles/gitea/tasks/service.yml @@ -0,0 +1,4 @@ +--- +- name: Start and enable the service + service: name=gitea state=started enabled=True + tags: gitea diff --git a/roles/gitea/tasks/user.yml b/roles/gitea/tasks/user.yml new file mode 100644 index 0000000..22471b1 --- /dev/null +++ b/roles/gitea/tasks/user.yml @@ -0,0 +1,8 @@ +--- +- import_tasks: ../includes/create_system_user.yml + vars: + - user: gitea + - comment: GIT Repository account + - home: "{{ gitea_root_dir }}" + - shell: /bin/bash + tags: gitea diff --git a/roles/gitea/tasks/write_version.yml b/roles/gitea/tasks/write_version.yml new file mode 100644 index 0000000..dba054e --- /dev/null +++ b/roles/gitea/tasks/write_version.yml @@ -0,0 +1,6 @@ +--- + +- name: Write version + copy: content={{ gitea_version }} dest={{ gitea_root_dir }}/meta/ansible_version + tags: gitea + diff --git a/roles/gitea/templates/app.ini.j2 b/roles/gitea/templates/app.ini.j2 new file mode 100644 index 0000000..91e5def --- /dev/null +++ b/roles/gitea/templates/app.ini.j2 @@ -0,0 +1,106 @@ +APP_NAME = Gitea: Git with a cup of tea +RUN_USER = gitea +RUN_MODE = prod + +[security] +INTERNAL_TOKEN = {{ gitea_tokens.results | selectattr('item','equalto','INTERNAL_TOKEN') | map(attribute='stdout') | first | string }} +INSTALL_LOCK = true +SECRET_KEY = {{ gitea_tokens.results | selectattr('item','equalto','SECRET_KEY') | map(attribute='stdout') | first | string }} +{% if gitea_username_header is defined %} +REVERSE_PROXY_AUTHENTICATION_USER = {{ gitea_username_header }} +{% endif %} +{% if gitea_web_src_ip is defined and gitea_web_src_ip | length > 0 %} +REVERSE_PROXY_LIMIT = 1 +REVERSE_PROXY_TRUSTED_PROXIES = {{ gitea_web_src_ip | select('search','\\.\\d+$') | list | join(',') }} +REVERSE_PROXY_TRUSTED_NETWORKS = {{ gitea_web_src_ip | select('search','/\\d+$') | list | join(',') }} +{% endif %} + +[server] +LOCAL_ROOT_URL = http://localhost:{{ gitea_web_port }}/ +SSH_DOMAIN = {{ gitea_ssh_domain }} +DOMAIN = {{ gitea_domain }} +HTTP_PORT = {{ gitea_web_port }} +ROOT_URL = {{ gitea_public_url }} +DISABLE_SSH = false +SSH_PORT = {{ gitea_ssh_port }} +LFS_START_SERVER = true +LFS_CONTENT_PATH = {{ gitea_root_dir }}/data/lfs +LFS_JWT_SECRET = {{ gitea_tokens.results | selectattr('item','equalto','LFS_JWT_SECRET') | map(attribute='stdout') | first | string }} +OFFLINE_MODE = true +STATIC_ROOT_PATH = {{ gitea_root_dir }} +LANDING_PAGE = explore + +[oauth2] +JWT_SECRET = {{ gitea_tokens.results | selectattr('item','equalto','JWT_SECRET') | map(attribute='stdout') | first | string }} + +[ssh.minimum_key_sizes] +DSA = -1 + +[ui] +ISSUE_PAGING_NUM = 20 + +[repository.upload] +TEMP_PATH = tmp/uploads + +[database] +DB_TYPE = mysql +HOST = {{ gitea_db_server }} +NAME = {{ gitea_db_name }} +USER = {{ gitea_db_user }} +PASSWD = `{{ gitea_db_pass }}` +LOG_SQL = false + +[repository] +ROOT = {{ gitea_root_dir }}/data/repositories + +[mailer] +ENABLED = true +HOST = localhost:25 +FROM = gitea-no-reply@{{ ansible_domain }} +USER = +PASSWD = + +[service] +REGISTER_EMAIL_CONFIRM = true +ENABLE_NOTIFY_MAIL = true +DISABLE_REGISTRATION = {{ gitea_registration | ternary('false','true') }} +ALLOW_ONLY_EXTERNAL_REGISTRATION = false +ENABLE_CAPTCHA = false +REQUIRE_SIGNIN_VIEW = false +DEFAULT_KEEP_EMAIL_PRIVATE = true +DEFAULT_ALLOW_CREATE_ORGANIZATION = true +DEFAULT_ENABLE_TIMETRACKING = true +NO_REPLY_ADDRESS = noreply.{{ ansible_domain }} +{% if gitea_username_header is defined %} +ENABLE_REVERSE_PROXY_AUTHENTICATION = true +ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true +{% endif %} + +[picture] +DISABLE_GRAVATAR = false +ENABLE_FEDERATED_AVATAR = true + +[openid] +ENABLE_OPENID_SIGNIN = false +ENABLE_OPENID_SIGNUP = false + +[session] +PROVIDER = file + +[log] +MODE = console +LEVEL = Trace +ROOT_PATH = {{ gitea_root_dir }}/log + +[log.console] +LEVEL = Trace + +[indexer] +REPO_INDEXER_ENABLED = true +STARTUP_TIMEOUT = 300s + +[other] +SHOW_FOOTER_VERSION = false + +[migrations] +ALLOW_LOCALNETWORKS = true diff --git a/roles/gitea/templates/git.sh.j2 b/roles/gitea/templates/git.sh.j2 new file mode 100644 index 0000000..b0ec666 --- /dev/null +++ b/roles/gitea/templates/git.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash + +source scl_source enable sclo-git212 diff --git a/roles/gitea/templates/gitea.service.j2 b/roles/gitea/templates/gitea.service.j2 new file mode 100644 index 0000000..e5c9eb0 --- /dev/null +++ b/roles/gitea/templates/gitea.service.j2 @@ -0,0 +1,26 @@ +[Unit] +Description=Gitea (Git with a cup of tea) +After=syslog.target +After=network.target + +[Service] +Type=simple +User=gitea +Group=gitea +WorkingDirectory={{ gitea_root_dir }} +ExecStart={{ gitea_scl_cmd }}{{ gitea_root_dir }}/bin/gitea web -c /opt/gitea/etc/app.ini +Environment=USER=gitea HOME={{ gitea_root_dir }} GITEA_WORK_DIR={{ gitea_root_dir }} +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +MemoryLimit=4096M +LimitNOFILE=65535 +SyslogIdentifier=gitea +Restart=always +StartLimitInterval=0 +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/roles/gitea/templates/perms.sh.j2 b/roles/gitea/templates/perms.sh.j2 new file mode 100644 index 0000000..9c3f514 --- /dev/null +++ b/roles/gitea/templates/perms.sh.j2 @@ -0,0 +1,5 @@ +#!/bin/bash + +restorecon -R {{ gitea_root_dir }} +chown root:root {{ gitea_root_dir }}/bin/gitea +chmod 755 {{ gitea_root_dir }}/bin/gitea diff --git a/roles/gitea/templates/post_backup.sh.j2 b/roles/gitea/templates/post_backup.sh.j2 new file mode 100644 index 0000000..3bb1c4e --- /dev/null +++ b/roles/gitea/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ gitea_root_dir }}/backup/* diff --git a/roles/gitea/templates/pre_backup.sh.j2 b/roles/gitea/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..9a5f00f --- /dev/null +++ b/roles/gitea/templates/pre_backup.sh.j2 @@ -0,0 +1,10 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump --user={{ gitea_db_user | quote }} \ + --password={{ gitea_db_pass | quote }} \ + --host={{ gitea_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ gitea_db_name }} | \ + zstd -c > {{ gitea_root_dir }}/backup/{{ gitea_db_name }}.sql.zst diff --git a/roles/gitea/vars/RedHat-7.yml b/roles/gitea/vars/RedHat-7.yml new file mode 100644 index 0000000..5c8b2dc --- /dev/null +++ b/roles/gitea/vars/RedHat-7.yml @@ -0,0 +1,6 @@ +--- + +gitea_packages: + - sclo-git212-git + - git-lfs +gitea_scl_cmd: '/bin/scl enable sclo-git212 -- ' diff --git a/roles/gitea/vars/RedHat-8.yml b/roles/gitea/vars/RedHat-8.yml new file mode 100644 index 0000000..02daaaa --- /dev/null +++ b/roles/gitea/vars/RedHat-8.yml @@ -0,0 +1,6 @@ +--- + +gitea_packages: + - git + - git-lfs +gitea_scl_cmd: '' diff --git a/roles/glpi/defaults/main.yml b/roles/glpi/defaults/main.yml new file mode 100644 index 0000000..a8be6e1 --- /dev/null +++ b/roles/glpi/defaults/main.yml @@ -0,0 +1,96 @@ +--- + +glpi_id: 1 +glpi_manage_upgrade: True +glpi_version: 9.5.5 +glpi_zip_url: https://github.com/glpi-project/glpi/releases/download/{{ glpi_version }}/glpi-{{ glpi_version }}.tgz +glpi_zip_sha1: 4a3408d3485b3813251e5c3f567283767bd76847 +glpi_root_dir: /opt/glpi_{{ glpi_id }} +glpi_php_user: php-glpi_{{ glpi_id }} +# If set, will use the following custom PHP FPM pool, which must be created +# glpi_php_fpm_pool: php70 +glpi_php_version: 74 +glpi_mysql_server: "{{ mysql_server | default('localhost') }}" +glpi_mysql_db: glpi_{{ glpi_id }} +glpi_mysql_user: glpi_{{ glpi_id }} +# If unset, a random one will be created and stored in the meta directory +# glpi_mysql_pass: glpi + +# glpi_alias: glpi +# glpi_src_ip: +# - 192.168.7.0/24 +# - 10.2.0.0/24 + +glpi_plugins: + fusioninventory: + version: '9.5+3.0' + sha1: ecfd38bb31600d6806cb32a8a5af2db24bac8145 + url: https://github.com/fusioninventory/fusioninventory-for-glpi/releases/download/glpi9.5%2B3.0/fusioninventory-9.5+3.0.tar.bz2 + reports: + version: 1.14.0 + sha1: 0e2c8912c43360b1140f972e70e3509e4e6e3c0d + url: https://forge.glpi-project.org/attachments/download/2317/glpi-plugin-reports-1.14.0.tar.gz + pdf: + version: 2.0.0 + sha1: c1eee0ab488852265c6a920510894551424b3f3e + url: https://forge.glpi-project.org/attachments/download/2335/glpi-pdf-2.0.0.tar.gz + behaviors: + version: 2.5.0 + sha1: 972e06027835d9da1e2eb2523caff40382389f81 + url: https://forge.glpi-project.org/attachments/download/2336/glpi-behaviors-2.5.0.tar.gz + manufacturersimports: + version: 2.3.1 + sha1: a6c3fd696665221ab5f1cf4cada18dd6481a1f31 + url: https://github.com/InfotelGLPI/manufacturersimports/releases/download/2.3.1/glpi-manufacturersimports-2.3.1.tar.gz + domains: + version: 2.2.1 + sha1: 298b459f5a132a20b3e3427921485c201f68ea78 + url: https://github.com/InfotelGLPI/domains/releases/download/2.2.1/glpi-domains-2.2.1.tar.gz + formcreator: + version: 2.11.2 + sha1: f8a51ee17296602d9f17daacbc4470e1b26c743c + url: https://github.com/pluginsGLPI/formcreator/releases/download/v2.11.2/glpi-formcreator-2.11.2.tar.bz2 + tag: + version: 2.8.1 + sha1: 9e88086bd54f41c67ddbd915020be0d558d05668 + url: https://github.com/pluginsGLPI/tag/releases/download/2.8.1/glpi-tag-2.8.1.tar.bz2 + mreporting: + version: 1.7.2 + sha1: 9e9498b11dd59707b16501a1fb341839642eb4a6 + url: https://github.com/pluginsGLPI/mreporting/releases/download/1.7.2/glpi-mreporting-1.7.2.tar.bz2 + fields: + version: 1.12.4 + sha1: 6415badd849dcb5d3b51d6cb256c7cb3040aaff1 + url: https://github.com/pluginsGLPI/fields/releases/download/1.12.4/glpi-fields-1.12.4.tar.bz2 + webapplications: + version: 3.0.0 + sha1: c5fe2ce301e02469b0fe66e302dedcf0a564e1ae + url: https://github.com/InfotelGLPI/webapplications/releases/download/3.0.0/glpi-webapplications-3.0.0.tar.gz + genericobject: + version: 2.10.1 + sha1: a8a27acfb0055f5880715bcd5931134bcd690542 + url: https://github.com/pluginsGLPI/genericobject/releases/download/2.10.1/glpi-genericobject-2.10.1.tar.bz2 + mantis: + version: 4.4.0 + sha1: 1686f9a944d16e86b74eab9689e61bf64f4cf144 + url: https://github.com/pluginsGLPI/mantis/releases/download/4.4.0/glpi-mantis-4.4.0.tar.bz2 + archimap: + version: 2.2.1 + sha1: a9dfac68dfad5af7230e36b76199391a7fee0c04 + url: https://github.com/ericferon/glpi-archimap/releases/download/v2.2.1/archimap-v2.2.1.tar.gz + dashboard: + version: 1.0.2 + sha1: c98a504f18c9914b57deda0340c19dbfad08440f + url: https://forge.glpi-project.org/attachments/download/2323/glpi-dashboard-1.0.2.zip + rename_from: glpi-dashboard-1.0.2 + +glpi_plugins_to_install: [] + +# You can customize the logo, ansible will download the logo +# This one is at the top left on every page. Should be 100x55 +# glpi_logo: https://img.example.org/logos/glpi/fd_glpi.png + +# This one is on the login page. It should be 145x80 +# glpi_login_logo: https://img.example.org/logos/glpi/login_logo_glpi.png + +... diff --git a/roles/glpi/handlers/main.yml b/roles/glpi/handlers/main.yml new file mode 100644 index 0000000..ea83645 --- /dev/null +++ b/roles/glpi/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- include: ../httpd_common/handlers/main.yml +- include: ../httpd_php/handlers/main.yml +... diff --git a/roles/glpi/meta/main.yml b/roles/glpi/meta/main.yml new file mode 100644 index 0000000..ddd97c1 --- /dev/null +++ b/roles/glpi/meta/main.yml @@ -0,0 +1,8 @@ +--- +allow_duplicates: true +dependencies: + - role: mkdir + - role: httpd_php + - role: mysql_server + when: glpi_mysql_server == 'localhost' or glpi_mysql_server == '127.0.0.1' +... diff --git a/roles/glpi/tasks/archive_post.yml b/roles/glpi/tasks/archive_post.yml new file mode 100644 index 0000000..ef73b69 --- /dev/null +++ b/roles/glpi/tasks/archive_post.yml @@ -0,0 +1,8 @@ +--- + +- import_tasks: ../includes/webapps_compress_archive.yml + vars: + - root_dir: "{{ glpi_root_dir }}" + - version: "{{ glpi_current_version }}" + tags: glpi + diff --git a/roles/glpi/tasks/archive_pre.yml b/roles/glpi/tasks/archive_pre.yml new file mode 100644 index 0000000..07b1139 --- /dev/null +++ b/roles/glpi/tasks/archive_pre.yml @@ -0,0 +1,9 @@ +--- + +- import_tasks: ../includes/webapps_archive.yml + vars: + - root_dir: "{{ glpi_root_dir }}" + - version: "{{ glpi_current_version }}" + - db_name: "{{ glpi_mysql_db }}" + tags: glpi + diff --git a/roles/glpi/tasks/cleanup.yml b/roles/glpi/tasks/cleanup.yml new file mode 100644 index 0000000..4b7870a --- /dev/null +++ b/roles/glpi/tasks/cleanup.yml @@ -0,0 +1,20 @@ +--- + +- name: Remove plugins archives + file: + path: "{{ glpi_root_dir }}/tmp/{{ glpi_plugins[item].url | urlsplit('path') | basename }}" + state: absent + with_items: "{{ glpi_plugins_to_install }}" + when: glpi_plugins[item] is defined + tags: glpi + +- name: Remove temp files + file: path={{ item }} state=absent + with_items: + - "{{ glpi_root_dir }}/tmp/glpi" + - "{{ glpi_root_dir }}/tmp/glpi-{{ glpi_version }}.tgz" + - "{{ glpi_root_dir }}/db_dumps" + - /etc/backup/pre.d/glpi_{{ glpi_id }}_dump_db + - /etc/backup/post.d/glpi_{{ glpi_id }}_rm_dump + tags: glpi + diff --git a/roles/glpi/tasks/conf.yml b/roles/glpi/tasks/conf.yml new file mode 100644 index 0000000..49980fe --- /dev/null +++ b/roles/glpi/tasks/conf.yml @@ -0,0 +1,39 @@ +--- + +- import_tasks: ../includes/webapps_webconf.yml + vars: + - app_id: glpi_{{ glpi_id }} + - php_version: "{{ glpi_php_version }}" + - php_fpm_pool: "{{ glpi_php_fpm_pool | default('') }}" + tags: glpi + +- name: Deploy glpi configuration + template: src={{ item }}.j2 dest={{ glpi_root_dir }}/web/config/{{ item }} owner=root group={{ glpi_php_user }} mode=660 + with_items: + - local_define.php + - config_db.php + tags: glpi + +- name: Remove obsolete conf files + file: path={{ glpi_root_dir }}/web/config/{{ item }} state=absent + with_items: + - config_path.php + tags: glpi + +- name: Init database + command: "/bin/php{{ glpi_php_version }} {{ glpi_root_dir }}/web/bin/console -n db:install" + when: glpi_install_mode == 'install' + tags: glpi + +- name: Upgrade database + command: "/bin/php{{ glpi_php_version }} {{ glpi_root_dir }}/web/bin/console -n db:update" + when: glpi_install_mode == 'upgrade' + tags: glpi + +- name: Deploy sso.php script + template: src=sso.php.j2 dest={{ glpi_root_dir }}/web/sso.php + tags: glpi + +- name: Deploy logrotate conf + template: src=logrotate.conf.j2 dest=/etc/logrotate.d/glpi_{{ glpi_id }} + tags: glpi diff --git a/roles/glpi/tasks/directories.yml b/roles/glpi/tasks/directories.yml new file mode 100644 index 0000000..ddd6b66 --- /dev/null +++ b/roles/glpi/tasks/directories.yml @@ -0,0 +1,24 @@ +--- + +- name: Create directory structure + file: path={{ item }} state=directory + with_items: + - "{{ glpi_root_dir }}" + - "{{ glpi_root_dir }}/web" + - "{{ glpi_root_dir }}/tmp" + - "{{ glpi_root_dir }}/sessions" + - "{{ glpi_root_dir }}/meta" + - "{{ glpi_root_dir }}/backup" + - "{{ glpi_root_dir }}/data" + - "{{ glpi_root_dir }}/data/_files" + - "{{ glpi_root_dir }}/data/_cache" + - "{{ glpi_root_dir }}/data/_cron" + - "{{ glpi_root_dir }}/data/_dumps" + - "{{ glpi_root_dir }}/data/_graphs" + - "{{ glpi_root_dir }}/data/_lock" + - "{{ glpi_root_dir }}/data/_log" + - "{{ glpi_root_dir }}/data/_pictures" + - "{{ glpi_root_dir }}/data/_plugins" + - "{{ glpi_root_dir }}/data/_rss" + tags: glpi + diff --git a/roles/glpi/tasks/facts.yml b/roles/glpi/tasks/facts.yml new file mode 100644 index 0000000..435ef1c --- /dev/null +++ b/roles/glpi/tasks/facts.yml @@ -0,0 +1,21 @@ +--- + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ glpi_root_dir }}" + - version: "{{ glpi_version }}" + tags: glpi +- set_fact: glpi_install_mode={{ (install_mode == 'upgrade' and not glpi_manage_upgrade) | ternary('none',install_mode) }} + tags: glpi +- set_fact: glpi_current_version={{ current_version | default('') }} + tags: glpi + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ glpi_root_dir }}/meta/ansible_dbpass" + when: glpi_mysql_pass is not defined + tags: glpi +- set_fact: glpi_mysql_pass={{ rand_pass }} + when: glpi_mysql_pass is not defined + tags: glpi + diff --git a/roles/glpi/tasks/filebeat.yml b/roles/glpi/tasks/filebeat.yml new file mode 100644 index 0000000..534aa2b --- /dev/null +++ b/roles/glpi/tasks/filebeat.yml @@ -0,0 +1,5 @@ +--- + +- name: Deploy filebeat configuration + template: src=filebeat.yml.j2 dest=/etc/filebeat/ansible_inputs.d/glpi_{{ glpi_id }}.yml + tags: glpi,log diff --git a/roles/glpi/tasks/install.yml b/roles/glpi/tasks/install.yml new file mode 100644 index 0000000..21ea49c --- /dev/null +++ b/roles/glpi/tasks/install.yml @@ -0,0 +1,142 @@ +--- + +- name: Install needed tools + yum: + name: + - unzip + - tar + - bzip2 + - acl + - mariadb + tags: glpi + +- name: Download glpi + get_url: + url: "{{ glpi_zip_url }}" + dest: "{{ glpi_root_dir }}/tmp/" + checksum: "sha1:{{ glpi_zip_sha1 }}" + when: glpi_install_mode != "none" + tags: glpi + +- name: Extract glpi archive + unarchive: + src: "{{ glpi_root_dir }}/tmp/glpi-{{ glpi_version }}.tgz" + dest: "{{ glpi_root_dir }}/tmp/" + remote_src: yes + when: glpi_install_mode != "none" + tags: glpi + +- name: Move the content of glpi to the correct top directory + synchronize: + src: "{{ glpi_root_dir }}/tmp/glpi/" + dest: "{{ glpi_root_dir }}/web/" + recursive: True + delete: True + rsync_opts: + - '--exclude=/install/install.php' + - '--exclude=/files/' + - '--exclude=/config/glpicrypt.key' + delegate_to: "{{ inventory_hostname }}" + when: glpi_install_mode != "none" + tags: glpi + +- name: Remove unwanted files and directories + file: path={{ glpi_root_dir }}/web/{{ item }} state=absent + with_items: + - files + - install/install.php + tags: glpi + +- name: Build a list of installed plugins + shell: find {{ glpi_root_dir }}/web/plugins -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: glpi_installed_plugins + changed_when: False + tags: glpi + +- name: Download plugins + get_url: + url: "{{ glpi_plugins[item].url }}" + dest: "{{ glpi_root_dir }}/tmp/" + checksum: "sha1:{{ glpi_plugins[item].sha1 }}" + when: + - item not in glpi_installed_plugins.stdout_lines + - glpi_plugins[item] is defined + with_items: "{{ glpi_plugins_to_install }}" + tags: glpi + +- name: Extract plugins + unarchive: + src: "{{ glpi_root_dir }}/tmp/{{ glpi_plugins[item].url | urlsplit('path') | basename }}" + dest: "{{ glpi_root_dir }}/web/plugins/" + remote_src: yes + when: + - item not in glpi_installed_plugins.stdout_lines + - glpi_plugins[item] is defined + with_items: "{{ glpi_plugins_to_install }}" + tags: glpi + +# Some plugins have the directory name not matching the plugin name +# Eg, glpi-dashboard-1.0.2 instead of dashboard. So it's removed as if it was an unmanaged plugin +# If the prop rename_from is defined for the plugin, rename the dir +- name: Rename plugin dir + command: mv {{ glpi_root_dir }}/web/plugins/{{ glpi_plugins[item].rename_from }} {{ glpi_root_dir }}/web/plugins/{{ item }} + args: + creates: "{{ glpi_root_dir }}/web/plugins/{{ item }}" + when: glpi_plugins[item].rename_from is defined + loop: "{{ glpi_plugins_to_install }}" + tags: glpi + +- name: Build a list of installed plugins + shell: find {{ glpi_root_dir }}/web/plugins -maxdepth 1 -mindepth 1 -type d -exec basename "{}" \; + register: glpi_installed_plugins + changed_when: False + tags: glpi + +- name: Remove unmanaged plugins + file: path={{ glpi_root_dir }}/web/plugins/{{ item }} state=absent + with_items: "{{ glpi_installed_plugins.stdout_lines }}" + when: item not in glpi_plugins_to_install + tags: glpi + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ glpi_mysql_db }}" + - db_user: "{{ glpi_mysql_user }}" + - db_server: "{{ glpi_mysql_server }}" + - db_pass: "{{ glpi_mysql_pass }}" + tags: glpi + +- set_fact: glpi_db_created={{ db_created }} + tags: glpi + +- name: Deploy cron task + cron: + name: glpi_{{ glpi_id }} + cron_file: glpi_{{ glpi_id }} + user: "{{ glpi_php_user }}" + job: "/bin/php{{ (glpi_php_version == '54') | ternary('',glpi_php_version) }} {{ glpi_root_dir }}/web/front/cron.php" + minute: "*/5" + tags: glpi + +- name: Deploy backup scripts + template: src={{ item }}_backup.j2 dest=/etc/backup/{{ item }}.d/glpi_{{ glpi_id }} mode=750 + loop: + - pre + - post + tags: glpi + +- name: Download the logo + get_url: + url: "{{ glpi_logo }}" + dest: "{{ glpi_root_dir }}/web/pics/fd_logo.png" + force: True + when: glpi_logo is defined + tags: glpi + +- name: Download the login page logo + get_url: + url: "{{ glpi_login_logo }}" + dest: "{{ glpi_root_dir }}/web/pics/login_logo_glpi.png" + force: True + when: glpi_login_logo is defined + tags: glpi diff --git a/roles/glpi/tasks/main.yml b/roles/glpi/tasks/main.yml new file mode 100644 index 0000000..1f16396 --- /dev/null +++ b/roles/glpi/tasks/main.yml @@ -0,0 +1,14 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: glpi_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: cleanup.yml +- include: write_version.yml +- include: archive_post.yml + when: glpi_install_mode == 'upgrade' +- include: filebeat.yml diff --git a/roles/glpi/tasks/user.yml b/roles/glpi/tasks/user.yml new file mode 100644 index 0000000..8510f9f --- /dev/null +++ b/roles/glpi/tasks/user.yml @@ -0,0 +1,8 @@ +--- + +- import_tasks: ../includes/create_system_user.yml + vars: + - user: "{{ glpi_php_user }}" + - comment: "PHP FPM for glpi {{ glpi_id }}" + tags: glpi + diff --git a/roles/glpi/tasks/write_version.yml b/roles/glpi/tasks/write_version.yml new file mode 100644 index 0000000..f2e8477 --- /dev/null +++ b/roles/glpi/tasks/write_version.yml @@ -0,0 +1,17 @@ +--- + +- name: Write plugin versions + shell: echo {{ glpi_plugins[item].version }} > {{ glpi_root_dir }}/meta/glpi_plugin_{{ item }}_ansible_version + when: + - item not in glpi_installed_plugins + - glpi_plugins[item] is defined + with_items: "{{ glpi_plugins_to_install }}" + changed_when: False + tags: glpi + +- import_tasks: ../includes/webapps_post.yml + vars: + - root_dir: "{{ glpi_root_dir }}" + - version: "{{ glpi_version }}" + tags: glpi + diff --git a/roles/glpi/templates/config_db.php.j2 b/roles/glpi/templates/config_db.php.j2 new file mode 100644 index 0000000..08abe52 --- /dev/null +++ b/roles/glpi/templates/config_db.php.j2 @@ -0,0 +1,8 @@ + diff --git a/roles/glpi/templates/filebeat.yml.j2 b/roles/glpi/templates/filebeat.yml.j2 new file mode 100644 index 0000000..5242339 --- /dev/null +++ b/roles/glpi/templates/filebeat.yml.j2 @@ -0,0 +1,7 @@ +- type: log + enabled: True + paths: + - {{ glpi_root_dir }}/data/_log/*.log + exclude_files: + - '\.[gx]z$' + - '\d+$' diff --git a/roles/glpi/templates/httpd.conf.j2 b/roles/glpi/templates/httpd.conf.j2 new file mode 100644 index 0000000..b664f21 --- /dev/null +++ b/roles/glpi/templates/httpd.conf.j2 @@ -0,0 +1,29 @@ +{% if glpi_alias is defined %} +Alias /{{ glpi_alias }} {{ glpi_root_dir }}/web +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + + + AllowOverride All + Options FollowSymLinks +{% if glpi_src_ip is defined %} + Require ip {{ glpi_src_ip | join(' ') }} +{% else %} + Require all granted +{% endif %} + + SetHandler "proxy:unix:/run/php-fpm/{{ glpi_php_fpm_pool | default('glpi_' + glpi_id | string) }}.sock|fcgi://localhost" + + + + Require all denied + + + + +{% for dir in [ 'scripts', 'locales', 'config', 'inc', 'vendor', '.github', 'bin' ] %} + + Require all denied + +{% endfor %} diff --git a/roles/glpi/templates/local_define.php.j2 b/roles/glpi/templates/local_define.php.j2 new file mode 100644 index 0000000..5cb1564 --- /dev/null +++ b/roles/glpi/templates/local_define.php.j2 @@ -0,0 +1,9 @@ + diff --git a/roles/glpi/templates/logrotate.conf.j2 b/roles/glpi/templates/logrotate.conf.j2 new file mode 100644 index 0000000..cd4cdcd --- /dev/null +++ b/roles/glpi/templates/logrotate.conf.j2 @@ -0,0 +1,7 @@ +{{ glpi_root_dir }}/data/_log/*.log { + daily + rotate 90 + compress + missingok + su {{ glpi_php_user }} {{ glpi_php_user }} +} diff --git a/roles/glpi/templates/perms.sh.j2 b/roles/glpi/templates/perms.sh.j2 new file mode 100644 index 0000000..4f117c3 --- /dev/null +++ b/roles/glpi/templates/perms.sh.j2 @@ -0,0 +1,20 @@ +#!/bin/sh + +restorecon -R {{ glpi_root_dir }} +chown root:root {{ glpi_root_dir }} +chmod 700 {{ glpi_root_dir }} +chown root:root {{ glpi_root_dir }}/{meta,backup} +chmod 700 {{ glpi_root_dir }}/{meta,backup} +setfacl -k -b {{ glpi_root_dir }} +setfacl -m u:{{ glpi_php_user | default('apache') }}:rx,u:{{ httpd_user | default('apache') }}:rx {{ glpi_root_dir }} +chown -R root:root {{ glpi_root_dir }}/web +chown -R {{ glpi_php_user }} {{ glpi_root_dir }}/{tmp,sessions,data} +chmod 700 {{ glpi_root_dir }}/{tmp,sessions,data} +find {{ glpi_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ glpi_root_dir }}/web -type d -exec chmod 755 "{}" \; +chown -R :{{ glpi_php_user }} {{ glpi_root_dir }}/web/config +chown -R :{{ glpi_php_user }} {{ glpi_root_dir }}/web/marketplace +chmod 770 {{ glpi_root_dir }}/web/config +chmod 660 {{ glpi_root_dir }}/web/config/* +chmod 770 {{ glpi_root_dir }}/web/marketplace +chmod 660 {{ glpi_root_dir }}/web/marketplace/* diff --git a/roles/glpi/templates/php.conf.j2 b/roles/glpi/templates/php.conf.j2 new file mode 100644 index 0000000..8662510 --- /dev/null +++ b/roles/glpi/templates/php.conf.j2 @@ -0,0 +1,35 @@ +[glpi_{{ glpi_id }}] + +listen.owner = root +listen.group = apache +listen.mode = 0660 +listen = /run/php-fpm/glpi_{{ glpi_id }}.sock +user = {{ glpi_php_user }} +group = {{ glpi_php_user }} +catch_workers_output = yes + +pm = dynamic +pm.max_children = 15 +pm.start_servers = 3 +pm.min_spare_servers = 3 +pm.max_spare_servers = 6 +pm.max_requests = 5000 +request_terminate_timeout = 5m + +php_flag[display_errors] = off +php_admin_flag[log_errors] = on +php_admin_value[error_log] = syslog +php_admin_value[memory_limit] = 256M +php_admin_value[session.save_path] = {{ glpi_root_dir }}/sessions +php_admin_value[upload_tmp_dir] = {{ glpi_root_dir }}/tmp +php_admin_value[sys_temp_dir] = {{ glpi_root_dir }}/tmp +php_admin_value[post_max_size] = 100M +php_admin_value[upload_max_filesize] = 100M +php_admin_value[disable_functions] = system, show_source, symlink, exec, dl, shell_exec, passthru, phpinfo, escapeshellarg, escapeshellcmd +php_admin_value[open_basedir] = {{ glpi_root_dir }}:/usr/share/pear/:/usr/share/php/ +php_admin_value[max_execution_time] = 60 +php_admin_value[max_input_time] = 60 +php_admin_flag[allow_url_include] = off +php_admin_flag[allow_url_fopen] = off +php_admin_flag[file_uploads] = on +php_admin_flag[session.cookie_httponly] = on diff --git a/roles/glpi/templates/post_backup.j2 b/roles/glpi/templates/post_backup.j2 new file mode 100644 index 0000000..85a23f0 --- /dev/null +++ b/roles/glpi/templates/post_backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ glpi_root_dir }}/backup/* diff --git a/roles/glpi/templates/pre_backup.j2 b/roles/glpi/templates/pre_backup.j2 new file mode 100644 index 0000000..279141f --- /dev/null +++ b/roles/glpi/templates/pre_backup.j2 @@ -0,0 +1,11 @@ +#!/bin/sh + +set -eo pipefail + +/usr/bin/mysqldump --user={{ glpi_mysql_user | quote }} \ + --password={{ glpi_mysql_pass | quote }} \ + --host={{ glpi_mysql_server | quote }} \ + --quick --single-transaction \ + --add-drop-table {{ glpi_mysql_db | quote }} | zstd -T0 -c > {{ glpi_root_dir }}/backup/{{ glpi_mysql_db }}.sql.zst + +[ -e {{ glpi_root_dir }}/web/config/glpicrypt.key ] && cp {{ glpi_root_dir }}/web/config/glpicrypt.key {{ glpi_root_dir }}/backup/ diff --git a/roles/glpi/templates/sso.php.j2 b/roles/glpi/templates/sso.php.j2 new file mode 100644 index 0000000..1f7c064 --- /dev/null +++ b/roles/glpi/templates/sso.php.j2 @@ -0,0 +1,6 @@ + diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..fc0d2db --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,89 @@ +--- + +# On which ip we should bind. +grafana_listen_ip: 0.0.0.0 + +# Port on which we should bind +grafana_port: 3000 + +# If defined, will be the public URL of Grafana +# granafa_root_url: https://graph.example.com + +# IP allowed to access grafana port. Only relevant if listen ip is not 127.0.0.1 +grafana_src_ip: [] + +# Database settings +# Can be sqlite3, mysql or postgres +grafana_db_type: mysql + +# If mysql or postgres is used, all the following settings have to be set +# For MySQL you can also set the path to a UNIX socket +grafana_db_server: "{{ mysql_server | default('/var/lib/mysql/mysql.sock') }}" +# If using TCP for MySQL or PostgreSQL, you must provide the port +grafana_db_port: 3306 +grafana_db_name: grafana +grafana_db_user: grafana +# grafana_db_pass: secret + +# Is grafana_reporting_enabled is true. Send reports to stats.grafana.org +grafana_reporting: False + +# Automatic check for updates +grafana_check_for_updates: True + +# Log level. Can be "debug", "info", "warn", "error", "critical" +grafana_log_level: info + +# Allow user to sign up +grafana_allow_sign_up: False + +grafana_auth_base: + anonymous: + org_role: Viewer + enabled: False + proxy: + header_name: Auth-User + enabled: False + # whitelist: + # - 10.10.1.20 + # - 192.168.7.12 + ldap: + enabled: "{{ (ad_auth | default(False) or ldap_auth | default(False)) | ternary(True,False) }}" + servers: "{{ (ad_ldap_servers is defined) | ternary(ad_ldap_servers,[ldap.example.org]) }}" + port: 389 + use_ssl: True + start_tls: True + ssl_skip_verify: False + # root_ca_cert: /etc/pki/tls/certs/cert.pem + # bind_dn: + # bind_password: + search_filter: "({{ ad_auth | default(False) | ternary('samaccountname','uid') }}=%s)" + search_base_dns: + - "{{ ad_auth | default(False) | ternary('DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC='), ldap_base | default('dc=example,dc=org')) }}" + # group_search_filter: "(&(objectClass=posixGroup)(memberUid=%s))" + # group_search_base_dns: + # - ou=groups,dc=example,dc=org + # group_search_filter_user_attribute: uid + attributes: + name: givenName + surname: sn + username: "{{ ad_auth | default(False) | ternary('sAMAccountName','uid') }}" + member_of: "{{ ad_auth | default(False) | ternary('memberOf','cn') }}" + email: mail + group_mappings: + - ldap_group: "{{ ad_auth | default(False) | ternary('CN=Domain Admins,CN=Users,' + 'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC='),'admins') }}" + role: Admin + - ldap_group: "{{ ad_auth | default(False) | ternary('CN=Domain Admins,OU=Groups,' + 'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC='),'admins') }}" + role: Admin + - ldap_group: "{{ ad_auth | default(False) | ternary('CN=Domain Users,CN=Users,' + 'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC='),'shared') }}" + role: Editor + - ldap_group: "{{ ad_auth | default(False) | ternary('CN=Domain Users,OU=Groups,' + 'DC=' + ad_realm | default(samba_realm) | default(ansible_domain) | regex_replace('\\.',',DC='),'shared') }}" + role: Editor + - ldap_group: '*' + role: Viewer +grafana_auth_extra: {} +grafana_auth: "{{ grafana_auth_base | combine(grafana_auth_extra, recursive=True) }}" + +# Plugins to install +grafana_plugins: + - alexanderzobnin-zabbix-app diff --git a/roles/grafana/handlers/main.yml b/roles/grafana/handlers/main.yml new file mode 100644 index 0000000..abe6907 --- /dev/null +++ b/roles/grafana/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- include: ../common/handlers/main.yml + +- name: restart grafana + service: name=grafana-server state=restarted diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 0000000..0b6e031 --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - { role: repo_grafana } diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..e592cab --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,141 @@ +--- +- name: Install grafana + yum: name=grafana state=present + register: grafana_install + tags: grafana + +- name: Create unit snippet dir + file: path=/etc/systemd/system/grafana-server.service.d state=directory + tags: grafana + +- name: Tune to restart indefinitely + copy: + content: | + [Service] + StartLimitInterval=0 + RestartSec=20 + dest: /etc/systemd/system/grafana-server.service.d/restart.conf + register: grafana_unit + tags: grafana + +- name: Reload systemd + systemd: daemon_reload=True + when: grafana_unit.changed + tags: grafana + +- name: Handle grafana port + iptables_raw: + name: grafana_port + state: "{{ (grafana_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp --dport {{ grafana_port }} -s {{ grafana_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: grafana,firewall + +- when: grafana_db_pass is not defined + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: /etc/grafana/ansible_db_pass + - complex: False + - set_fact: grafana_db_pass={{ rand_pass }} + tags: grafana + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ grafana_db_name }}" + - db_user: "{{ grafana_db_user }}" + - db_server: "{{ grafana_db_server }}" + - db_pass: "{{ grafana_db_pass }}" + when: grafana_db_type == 'mysql' + tags: grafana + +- when: grafana_db_type == 'postgres' + block: + - name: Create the PostgreSQL role + postgresql_user: + name: "{{ grafana_db_user }}" + password: "{{ grafana_db_pass }}" + login_host: "{{ grafana_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + + - name: Create the PostgreSQL database + postgresql_db: + name: "{{ grafana_db_name }}" + encoding: UTF-8 + lc_collate: C + lc_ctype: C + template: template0 + owner: "{{ grafana_db_user }}" + login_host: "{{ grafana_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + tags: grafana + +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: /etc/grafana/ansible_secret_key + - set_fact: grafana_secret_key={{ rand_pass }} + tags: grafana + +- name: Deploy grafana configuration + template: src={{ item }}.j2 dest=/etc/grafana/{{ item }} owner=root group=grafana mode=640 + with_items: + - grafana.ini + - ldap.toml + notify: restart grafana + tags: grafana + + # Since Grafana 7.5.7, grafana-cli even when invoked as root takes action under the grafana user + # so we need to be sure permissions are OK, or plugin update/installation/removal will fail +- name: Ensure correct permissions on data dir + file: path=/var/lib/grafana owner=grafana group=grafana mode=770 recurse=True + tags: grafana + +- name: Build a list of installed plugins + shell: grafana-cli plugins ls | perl -ne '/^(\w[\-\w]+)\s\@\s\d+\./ && print "$1\n"' + register: grafana_installed_plugins + changed_when: False + tags: grafana + +- name: Remove unmanaged plugins + command: grafana-cli plugins uninstall {{ item }} + with_items: "{{ grafana_installed_plugins.stdout_lines }}" + when: item not in grafana_plugins + notify: restart grafana + tags: grafana + +- name: Install plugins + command: grafana-cli plugins install {{ item }} + with_items: "{{ grafana_plugins }}" + when: item not in grafana_installed_plugins.stdout_lines + notify: restart grafana + tags: grafana + +- name: Check installed plugins versions + shell: grafana-cli plugins ls | perl -ne '/^(\w[\-\w]+)\s\@\s(\d+[^\s]*)/ && print "$1 $2\n"' + register: grafana_installed_plugins_versions + changed_when: False + tags: grafana + +- name: Check available plugins versions + shell: grafana-cli plugins list-remote | perl -ne '/^id:\s+(\w[\-\w]+)\sversion:\s+(\d+[^\s]*)/ && print "$1 $2\n"' + register: grafana_remote_plugins_versions + changed_when: False + tags: grafana + +- name: Update grafana plugins + command: grafana-cli plugins update-all + when: grafana_installed_plugins_versions.stdout_lines is not subset(grafana_remote_plugins_versions.stdout_lines) + notify: restart grafana + tags: grafana + +- name: Start and enable the service + service: name=grafana-server state=started enabled=True + tags: grafana + +- name: Change admin password to a random one + command: grafana-cli admin reset-admin-password --homepath="/usr/share/grafana" --config /etc/grafana/grafana.ini $(openssl rand -base64 33) + when: grafana_install.changed + tags: grafana diff --git a/roles/grafana/templates/grafana.ini.j2 b/roles/grafana/templates/grafana.ini.j2 new file mode 100644 index 0000000..3ec64be --- /dev/null +++ b/roles/grafana/templates/grafana.ini.j2 @@ -0,0 +1,75 @@ +[paths] + +[server] +protocol = http +http_addr = 0.0.0.0 +http_port = {{ grafana_port }} +{% if grafana_root_url is defined %} +root_url = {{ grafana_root_url }} +{% endif %} + +[database] +type = {{ grafana_db_type }} +{% if grafana_db_type == 'sqlite3' %} +path = grafana.db +{% else %} +host = {{ grafana_db_server }}{% if grafana_db_port is defined and not grafana_db_server is match ('^/') %}:{{ grafana_db_port }}{% endif %} + +name = {{ grafana_db_name }} +user = {{ grafana_db_user }} +password = {{ grafana_db_pass }} +{% endif %} + +[session] + +[dataproxy] + +[analytics] +reporting_enabled = {{ grafana_reporting | ternary('true', 'false') }} +check_for_updates = {{ grafana_check_for_updates | ternary('true', 'false') }} + +[security] +secret_key = {{ grafana_secret_key }} + +[snapshots] + +[users] +allow_sign_up = {{ grafana_allow_sign_up | ternary('true','false') }} + +[auth] + +[auth.anonymous] +{% if grafana_auth.anonymous is defined and grafana_auth.anonymous.enabled | default(True) %} +enabled = true +{% if grafana_auth.anonymous.org_name is defined %} +org_name = {{ grafana_auth.anonymous.org_name }} +{% endif %} +{% if grafana_auth.anonymous.org_role is defined %} +org_role = {{ grafana_auth.anonymous.org_role }} +{% endif %} +{% endif %} + +[auth.proxy] +{% if grafana_auth.proxy is defined and grafana_auth.proxy.enabled | default(True) %} +enabled = true +header_name = {{ grafana_auth.proxy.header_name | default('User-Name') }} +header_property = username +auto_sign_up = true +{% if grafana_auth.proxy.whitelist is defined %} +whitelist = {{ grafana_auth.proxy.whitelist | join(',') }} +{% endif %} +{% endif %} + +[auth.basic] + +[auth.ldap] +{% if grafana_auth.ldap is defined and grafana_auth.ldap.enabled | default(True) %} +enabled = true +config_file = /etc/grafana/ldap.toml +{% endif %} + +[emails] + +[log] +mode = console +level = {{ grafana_log_level }} diff --git a/roles/grafana/templates/ldap.toml.j2 b/roles/grafana/templates/ldap.toml.j2 new file mode 100644 index 0000000..dd23b92 --- /dev/null +++ b/roles/grafana/templates/ldap.toml.j2 @@ -0,0 +1,37 @@ +[[servers]] +host = "{{ grafana_auth.ldap.servers | join(' ') }}" +port = {{ grafana_auth.ldap.port }} +use_ssl = {{ (grafana_auth.ldap.use_ssl or grafana_auth.ldap.start_tls) | ternary('true','false') }} +start_tls = {{ grafana_auth.ldap.start_tls | ternary('true','false') }} +ssl_skip_verify = {{ grafana_auth.ldap.ssl_skip_verify | ternary('true','false') }} + +{% if grafana_auth.ldap.root_ca_cert is defined %} +root_ca_cert = {{ grafana_auth.ldap.root_ca_cert }} +{% endif %} + +{% if grafana_auth.ldap.bind_dn is defined and grafana_auth.ldap.bind_password is defined %} +bind_dn = "{{ grafana_auth.ldap.bind_dn }}" +bind_password = '{{ grafana_auth.ldap.bind_password }}' +{% endif %} +search_filter = "{{ grafana_auth.ldap.search_filter }}" +search_base_dns = ["{{ grafana_auth.ldap.search_base_dns | join('","') }}"] + +{% if grafana_auth.ldap.group_search_filter is defined %} +group_search_filter = "{{ grafana_auth.ldap.group_search_filter }}" +group_search_base_dns = ["{{ grafana_auth.ldap.group_search_base_dns | join('","') }}"] +{% if grafana_auth.ldap.group_search_filter_user_attribute is defined %} +group_search_filter_user_attribute = "{{ grafana_auth.ldap.group_search_filter_user_attribute }}" +{% endif %} +{% endif %} + +[servers.attributes] +{% for attr in grafana_auth.ldap.attributes %} +{{ attr }} = "{{ grafana_auth.ldap.attributes[attr] }}" +{% endfor %} + +{% for map in grafana_auth.ldap.group_mappings %} +[[servers.group_mappings]] +group_dn = "{{ map['ldap_group'] }}" +org_role = "{{ map['role'] }}" + +{% endfor %} diff --git a/roles/graylog/defaults/main.yml b/roles/graylog/defaults/main.yml new file mode 100644 index 0000000..9b25b6b --- /dev/null +++ b/roles/graylog/defaults/main.yml @@ -0,0 +1,73 @@ +--- + +graylog_version: 4.1.6 +graylog_archive_url: https://downloads.graylog.org/releases/graylog/graylog-{{ graylog_version }}.tgz +graylog_archive_sha1: 7701118689798cb68cda2181e2a1c56a67792495 +graylog_root_dir: /opt/graylog +graylog_manage_upgrade: True + +graylog_is_master: True + +graylog_plugins: + graylog-output-syslog: + version: 3.3.0 + sha1: e18bc112cd3b5d5b07b69ed5e5c2e146dfd67677 + url: https://github.com/wizecore/graylog2-output-syslog/releases/download/3.3.0/graylog-output-syslog-3.3.0.jar + +# Plugins bundled, which should not be removed +graylog_plugins_core: + - aws + - collector + - threatintel +graylog_plugins_to_install: [] + +# A random one will be created is not defined +# graylog_pass_secret: +# graylog_admin_pass: + +# 9000 is for the web interface and api, 12201 is the default for gelf HTTP inputs +graylog_api_port: 9000 +graylog_listeners_http_ports: [12201] +graylog_http_ports: "{{ [graylog_api_port] + graylog_listeners_http_ports }}" +graylog_http_src_ip: [] + +# Must match your inputs (eg, syslog/raw) +# used to open ports in the firewall +graylog_listeners_udp_ports: [514] +graylog_listeners_tcp_ports: [514] +graylog_listeners_src_ip: [0.0.0.0/0] + +# graylog_external_uri: https://logs.domain.tld/ + +graylog_es_hosts: + - http://localhost:9200 +graylog_es_cluster_name: elasticsearch + +graylog_mongo_user: graylog +# A random one will be created if not set. To make anonymous connections, set it to False +# If you use more than 1 mongo URL, then no password will be created, mongo user must be created manually +# and configured in the url +#graylog_mongo_pass: S3cRet. +# Note: if graylog_mongo_pass is defined, it'll be used with graylog_mongo_user to connect, even if not indicated in graylog_mongo_url +# Else, anonymous connection is made. By default, if you do not set graylog_mongo_pass, a random one will be created +# If you insist on using anonymous connections, you should set graylog_mongo_pass to False +graylog_mongo_url: + - mongodb://localhost/graylog + +# Max size of Graylog journal, in GB +graylog_journal_max_size: 5 + +# If you want to obtain a cert with dehydrated +# it'll be deployed as {{ graylog_root_dir }}/ssl/cert.pem and {{ graylog_root_dir }}/ssl/key.pem +# graylog_letsencrypt_cert: graylog.domain.tls + +# If set, will populate enabled_tls_protocols +# on el7, TLSv1.3 seems to break filebeat connections, so, just enable TLSv1.2 +graylog_tls_versions: + - TLSv1.2 + +# Mem to allocate to the JVM (Xmx / Xms) +graylog_jvm_mem: 2g + +# Version of the Elasticsearch server +# graylog_es_version: 6 diff --git a/roles/graylog/handlers/main.yml b/roles/graylog/handlers/main.yml new file mode 100644 index 0000000..de1337f --- /dev/null +++ b/roles/graylog/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- name: restart graylog-server + service: name=graylog-server state=restarted + when: not graylog_started.changed diff --git a/roles/graylog/meta/main.yml b/roles/graylog/meta/main.yml new file mode 100644 index 0000000..09b0f1d --- /dev/null +++ b/roles/graylog/meta/main.yml @@ -0,0 +1,6 @@ +--- + +dependencies: + - role: mkdir + - role: repo_mongodb + - role: geoipupdate diff --git a/roles/graylog/tasks/archive_post.yml b/roles/graylog/tasks/archive_post.yml new file mode 100644 index 0000000..825ee59 --- /dev/null +++ b/roles/graylog/tasks/archive_post.yml @@ -0,0 +1,7 @@ +--- + +- import_tasks: ../includes/webapps_compress_archive.yml + vars: + - root_dir: "{{ graylog_root_dir }}" + - version: "{{ graylog_current_version }}" + tags: graylog diff --git a/roles/graylog/tasks/archive_pre.yml b/roles/graylog/tasks/archive_pre.yml new file mode 100644 index 0000000..65634e1 --- /dev/null +++ b/roles/graylog/tasks/archive_pre.yml @@ -0,0 +1,27 @@ +--- + +- name: Create archive dir + file: path={{ graylog_root_dir }}/archives/{{ graylog_current_version }}/mongo state=directory + tags: graylog + +- name: Archive current version + synchronize: + src: "{{ graylog_root_dir }}/app" + dest: "{{ graylog_root_dir }}/archives/{{ graylog_current_version }}/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + tags: graylog + +- name: Archive mongo database + shell: | + mongodump --quiet \ + --out {{ graylog_root_dir }}/archives/{{ graylog_current_version }}/mongo \ + --uri \ + {% if graylog_mongo_pass is defined and graylog_mongo_pass != False and graylog_mongo_url | length == 1 %} + {% set url = graylog_mongo_url[0] %} + {{ url | urlsplit('scheme') }}://{{ graylog_mongo_user }}:{{ graylog_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ url | urlsplit('hostname') }}{% if url | urlsplit('port') %}:{{ url | urlsplit('port') }}{% endif %}{{ url | urlsplit('path') }}?{{ url | urlsplit('query') }} + {% else %} + {{ graylog_mongo_url[0] }} + {% endif %} + tags: graylog diff --git a/roles/graylog/tasks/cleanup.yml b/roles/graylog/tasks/cleanup.yml new file mode 100644 index 0000000..295c4e9 --- /dev/null +++ b/roles/graylog/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove temp files + file: path={{ item }} state=absent + loop: + - "{{ graylog_root_dir }}/tmp/graylog-{{ graylog_version }}.tgz" + - "{{ graylog_root_dir }}/tmp/graylog-{{ graylog_version }}" + tags: graylog diff --git a/roles/graylog/tasks/conf.yml b/roles/graylog/tasks/conf.yml new file mode 100644 index 0000000..4f47127 --- /dev/null +++ b/roles/graylog/tasks/conf.yml @@ -0,0 +1,33 @@ +--- + +- name: Deploy configuration + template: src={{ item }}.j2 dest={{ graylog_root_dir }}/etc/{{ item }} group=graylog mode=640 + loop: + - server.conf + - log4j2.xml + notify: restart graylog-server + tags: graylog + +- name: Create the mongodb user + mongodb_user: + database: "{{ item | urlsplit('path') | regex_replace('^\\/', '') }}" + name: "{{ graylog_mongo_user }}" + password: "{{ graylog_mongo_pass }}" + login_database: admin + login_host: "{{ item | urlsplit('hostname') }}" + login_port: "{{ item | urlsplit('port') | ternary(item | urlsplit('port'),omit) }}" + login_user: mongoadmin + login_password: "{{ mongo_admin_pass }}" + roles: + - readWrite + loop: "{{ graylog_mongo_url }}" + changed_when: False # the module is buggy and indicates a change even if there were none + when: + - graylog_mongo_url | length == 1 + - graylog_mongo_pass is defined + - graylog_mongo_pass != False + tags: graylog + +- name: Deploy logrotate configuration + template: src=logrotate.conf.j2 dest=/etc/logrotate.d/graylog + tags: graylog diff --git a/roles/graylog/tasks/directories.yml b/roles/graylog/tasks/directories.yml new file mode 100644 index 0000000..af88a51 --- /dev/null +++ b/roles/graylog/tasks/directories.yml @@ -0,0 +1,39 @@ +--- + +- name: Create dir + file: + path: "{{ graylog_root_dir }}/{{ item.dir }}" + state: directory + owner: "{{ item.owner | default(omit) }}" + group: "{{ item.group | default(omit) }}" + mode: "{{ item.mode | default(omit) }}" + loop: + - dir: / + - dir: etc + owner: root + group: graylog + mode: 750 + - dir: app + - dir: state + owner: graylog + group: graylog + - dir: data/journal + owner: graylog + group: graylog + mode: 700 + - dir: meta + mode: 700 + - dir: ssl + owner: root + group: graylog + mode: 750 + - dir: archives + mode: 700 + - dir: tmp + - dir: logs + owner: graylog + group: graylog + mode: 700 + - dir: backup + mode: 700 + tags: graylog diff --git a/roles/graylog/tasks/facts.yml b/roles/graylog/tasks/facts.yml new file mode 100644 index 0000000..33ba5f7 --- /dev/null +++ b/roles/graylog/tasks/facts.yml @@ -0,0 +1,82 @@ +--- + +# Detect if already installed, and if an upgrade is needed +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + - root_dir: "{{ graylog_root_dir }}" + - version: "{{ graylog_version }}" + tags: graylog +- set_fact: graylog_install_mode={{ (install_mode == 'upgrade' and not graylog_manage_upgrade) | ternary('none',install_mode) }} + tags: graylog +- set_fact: graylog_current_version={{ current_version | default('') }} + tags: graylog + +# Try to read mongo admin pass +- name: Check if mongo pass file exists + stat: path=/root/.mongo.pw + register: graylog_mongo_pw + tags: graylog +- when: graylog_mongo_pw.stat.exists and mongo_admin_pass is not defined + block: + - slurp: src=/root/.mongo.pw + register: graylog_mongo_admin_pass + - set_fact: mongo_admin_pass={{ graylog_mongo_admin_pass.content | b64decode | trim }} + tags: graylog +- fail: msg='mongo_admin_pass must be provided' + when: not graylog_mongo_pw.stat.exists and mongo_admin_pass is not defined + tags: graylog + +- name: Remove randomly generated admin password + file: path={{ graylog_root_dir }}/meta/admin_pass state=absent + when: graylog_admin_pass is defined + tags: graylog + +- name: Remove randomly generated password secret + file: path={{ graylog_root_dir }}/meta/pass_secret state=absent + when: graylog_pass_secret is defined + tags: graylog + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ graylog_root_dir }}/meta/pass_secret" + when: graylog_pass_secret is not defined + tags: graylog +- set_fact: graylog_pass_secret={{ rand_pass }} + when: graylog_pass_secret is not defined + tags: graylog + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ graylog_root_dir }}/meta/admin_pass" + when: graylog_admin_pass is not defined + tags: graylog +- set_fact: graylog_admin_pass={{ rand_pass }} + when: graylog_admin_pass is not defined + tags: graylog + +# If only one mongo url is given and graylog_mongo_pass is not defined, +# parse the password from the url, or generate one +- debug: + msg: | + graylog_mongo_url is '{{ graylog_mongo_url }}' + parsed pass is "{{ graylog_mongo_url[0] | urlsplit('password') }}" + tags: graylog + +- name: Parse password from the first mongo URL + set_fact: graylog_mongo_pass={{ graylog_mongo_url[0] | urlsplit('password') | urldecode }} + when: + - graylog_mongo_url | length == 1 + - graylog_mongo_pass is not defined + - graylog_mongo_url[0] | urlsplit('password') is string + tags: mongo + +# Create a random password for mongo +- block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ graylog_root_dir }}/meta/mongo_pass" + - set_fact: graylog_mongo_pass={{ rand_pass }} + when: + - graylog_mongo_url | length == 1 + - graylog_mongo_pass is not defined + tags: graylog diff --git a/roles/graylog/tasks/filebeat.yml b/roles/graylog/tasks/filebeat.yml new file mode 100644 index 0000000..c1c1fee --- /dev/null +++ b/roles/graylog/tasks/filebeat.yml @@ -0,0 +1,5 @@ +--- + +- name: Deploy filebeat configuration + template: src=filebeat.yml.j2 dest=/etc/filebeat/ansible_inputs.d/graylog.yml + tags: graylog,log diff --git a/roles/graylog/tasks/install.yml b/roles/graylog/tasks/install.yml new file mode 100644 index 0000000..80178c1 --- /dev/null +++ b/roles/graylog/tasks/install.yml @@ -0,0 +1,100 @@ +--- + +- name: Uninstall RPM + yum: + name: + - graylog-server + state: absent + tags: graylog + +- name: Install packages + yum: + name: + - java-1.8.0-openjdk + - mongodb-org-tools + tags: graylog + +- name: Download graylog archive + get_url: + url: "{{ graylog_archive_url }}" + dest: "{{ graylog_root_dir }}/tmp/" + checksum: sha1:{{ graylog_archive_sha1 }} + when: graylog_install_mode != 'none' + tags: graylog + +- name: Extract graylog archive + unarchive: + src: "{{ graylog_root_dir }}/tmp/graylog-{{ graylog_version }}.tgz" + dest: "{{ graylog_root_dir }}/tmp" + remote_src: True + when: graylog_install_mode != 'none' + tags: graylog + +- name: Deploy graylog app + synchronize: + src: "{{ graylog_root_dir }}/tmp/graylog-{{ graylog_version }}/" + dest: "{{ graylog_root_dir }}/app/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: graylog_install_mode != 'none' + notify: restart graylog-server + tags: graylog + +- name: Install plugins + get_url: + url: "{{ graylog_plugins[item].url }}" + dest: "{{ graylog_root_dir }}/app/plugin" + checksum: sha1:{{ graylog_plugins[item].sha1 }} + when: item in graylog_plugins_to_install + loop: "{{ graylog_plugins.keys() | list }}" + notify: restart graylog-server + tags: graylog + +- name: Remove old plugins + shell: find {{ graylog_root_dir }}/app/plugin -name graylog-plugin-{{ item }}\* -a \! -name \*{{ graylog_plugins[item].version }}.jar -exec rm -f "{}" \; + when: graylog_plugins[item] is defined + changed_when: False + loop: "{{ graylog_plugins_to_install }}" + tags: graylog + +- name: List installed plugins + shell: find {{ graylog_root_dir }}/app/plugin/ -type f -name graylog-plugin-\*.jar + register: graylog_plugins_installed + changed_when: False + tags: graylog + +- name: Remove unwanted plugins + file: path={{ item }} state=absent + when: item | basename | regex_replace('graylog\-plugin\-(.+)\-\d(\.\d+)+\.jar','\\1') not in graylog_plugins_core + graylog_plugins_to_install + notify: restart graylog-server + loop: "{{ graylog_plugins_installed.stdout_lines }}" + tags: graylog + +- name: Deploy systemd service unit + template: src=graylog-server.service.j2 dest=/etc/systemd/system/graylog-server.service + register: graylog_unit + notify: restart graylog-server + tags: graylog + +- name: Reload systemd + systemd: daemon_reload=True + when: graylog_unit.changed + tags: graylog + +- name: Deploy pre/post backup scripts + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/graylog mode=750 + loop: + - pre + - post + tags: graylog + +- name: Deploy dehydrated hook + template: src=dehydrated_deploy_hook.j2 dest=/etc/dehydrated/hooks_deploy_cert.d/graylog mode=755 + when: graylog_letsencrypt_cert is defined + tags: graylog + +- name: Remove dehydrated hook + file: path=/etc/dehydrated/hooks_deploy_cert.d/graylog state=absent + when: graylog_letsencrypt_cert is not defined + tags: graylog diff --git a/roles/graylog/tasks/iptables.yml b/roles/graylog/tasks/iptables.yml new file mode 100644 index 0000000..6c9da06 --- /dev/null +++ b/roles/graylog/tasks/iptables.yml @@ -0,0 +1,20 @@ +--- + +- name: Handle graylog ports + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p {{ item.proto | default('tcp') }} -m multiport --dports {{ item.port }} -s {{ item.src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + loop: + - port: "{{ graylog_http_ports | join(',') }}" + name: graylog_http_ports + src_ip: "{{ graylog_http_src_ip }}" + - port: "{{ graylog_listeners_tcp_ports | join(',') }}" + name: graylog_listeners_tcp_ports + src_ip: "{{ graylog_listeners_src_ip }}" + - port: "{{ graylog_listeners_udp_ports | join(',') }}" + proto: udp + name: graylog_listeners_udp_ports + src_ip: "{{ graylog_listeners_src_ip }}" + tags: firewall,graylog diff --git a/roles/graylog/tasks/main.yml b/roles/graylog/tasks/main.yml new file mode 100644 index 0000000..8421709 --- /dev/null +++ b/roles/graylog/tasks/main.yml @@ -0,0 +1,16 @@ +--- + +- include: facts.yml +- include: user.yml +- include: directories.yml +- include: archive_pre.yml + when: graylog_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml +- include: service.yml +- include: write_version.yml +- include: cleanup.yml +- include: archive_post.yml + when: graylog_install_mode == 'upgrade' +- include: filebeat.yml diff --git a/roles/graylog/tasks/service.yml b/roles/graylog/tasks/service.yml new file mode 100644 index 0000000..4a7a636 --- /dev/null +++ b/roles/graylog/tasks/service.yml @@ -0,0 +1,6 @@ +--- + +- name: Start and enable the service + service: name=graylog-server state=started enabled=True + register: graylog_started + tags: graylog diff --git a/roles/graylog/tasks/user.yml b/roles/graylog/tasks/user.yml new file mode 100644 index 0000000..0bc72a0 --- /dev/null +++ b/roles/graylog/tasks/user.yml @@ -0,0 +1,9 @@ +--- + +- name: Create a system account to run graylog + user: + name: graylog + comment: "Graylog system account" + system: True + shell: /sbin/nologin + tags: graylog diff --git a/roles/graylog/tasks/write_version.yml b/roles/graylog/tasks/write_version.yml new file mode 100644 index 0000000..d62395e --- /dev/null +++ b/roles/graylog/tasks/write_version.yml @@ -0,0 +1,5 @@ +--- + +- name: Write version + copy: content={{ graylog_version }} dest={{ graylog_root_dir }}/meta/ansible_version + tags: graylog diff --git a/roles/graylog/templates/dehydrated_deploy_hook.j2 b/roles/graylog/templates/dehydrated_deploy_hook.j2 new file mode 100644 index 0000000..035238b --- /dev/null +++ b/roles/graylog/templates/dehydrated_deploy_hook.j2 @@ -0,0 +1,12 @@ +#!/bin/bash -e + +{% if graylog_letsencrypt_cert is defined %} +if [ $1 == "{{ graylog_letsencrypt_cert }}" ]; then + cat /var/lib/dehydrated/certificates/certs/{{ graylog_letsencrypt_cert }}/privkey.pem > {{ graylog_root_dir }}/ssl/key.pem + cat /var/lib/dehydrated/certificates/certs/{{ graylog_letsencrypt_cert }}/fullchain.pem > {{ graylog_root_dir }}/ssl/cert.pem + chown root:graylog {{ graylog_root_dir }}/ssl/* + chmod 644 {{ graylog_root_dir }}/ssl/cert.pem + chmod 640 {{ graylog_root_dir }}/ssl/key.pem + /bin/systemctl restart graylog-server +fi +{% endif %} diff --git a/roles/graylog/templates/filebeat.yml.j2 b/roles/graylog/templates/filebeat.yml.j2 new file mode 100644 index 0000000..a70b982 --- /dev/null +++ b/roles/graylog/templates/filebeat.yml.j2 @@ -0,0 +1,4 @@ +- type: log + enabled: True + paths: + - {{ graylog_root_dir }}/logs/server.log diff --git a/roles/graylog/templates/graylog-server.j2 b/roles/graylog/templates/graylog-server.j2 new file mode 100644 index 0000000..0cbeee9 --- /dev/null +++ b/roles/graylog/templates/graylog-server.j2 @@ -0,0 +1,29 @@ +#!/bin/sh + +set -e + +# For Debian/Ubuntu based systems. +if [ -f "/etc/default/graylog-server" ]; then + . "/etc/default/graylog-server" +fi + +# For RedHat/Fedora based systems. +if [ -f "/etc/sysconfig/graylog-server" ]; then + . "/etc/sysconfig/graylog-server" +fi + +if [ -f "/usr/share/graylog-server/installation-source.sh" ]; then + . "/usr/share/graylog-server/installation-source.sh" +fi + +# Java versions > 8 don't support UseParNewGC +if ${JAVA:=/usr/bin/java} -XX:+PrintFlagsFinal 2>&1 | grep -q UseParNewGC; then + GRAYLOG_SERVER_JAVA_OPTS="$GRAYLOG_SERVER_JAVA_OPTS -XX:+UseParNewGC" +fi + +$GRAYLOG_COMMAND_WRAPPER ${JAVA:=/usr/bin/java} $GRAYLOG_SERVER_JAVA_OPTS \ + -cp /usr/share/graylog-server/graylog.jar{% if graylog_libs.keys() | list | length > 0 %}:{% for lib in graylog_libs.keys() | list %}:{{ graylog_root_dir }}/libs/{{ lib }}-{{ graylog_libs[lib].version }}.jar{% endfor %} {% endif %} -Dlog4j.configurationFile=file://{{ graylog_root_dir }}/etc/log4j2.xml \ + -Djava.library.path=/usr/share/graylog-server/lib/sigar \ + -Dgraylog2.installation_source=${GRAYLOG_INSTALLATION_SOURCE:=unknown} \ + org.graylog2.bootstrap.Main server -f {{ graylog_root_dir }}/etc/server.conf -np \ + $GRAYLOG_SERVER_ARGS diff --git a/roles/graylog/templates/graylog-server.service.j2 b/roles/graylog/templates/graylog-server.service.j2 new file mode 100644 index 0000000..06ee0e3 --- /dev/null +++ b/roles/graylog/templates/graylog-server.service.j2 @@ -0,0 +1,37 @@ +[Unit] +Description=Graylog server +Documentation=http://docs.graylog.org/ +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=10 +User=graylog +Group=graylog +LimitNOFILE=64000 +ExecStart=/usr/bin/java \ + -Xms{{ graylog_jvm_mem }} -Xmx{{ graylog_jvm_mem }} -Djdk.tls.acknowledgeCloseNotify=true \ + -XX:NewRatio=1 -server -XX:+ResizeTLAB \ + -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \ + -XX:+CMSClassUnloadingEnabled -XX:-OmitStackTraceInFastThrow \ + -Dlog4j.configurationFile=file://{{ graylog_root_dir }}/etc/log4j2.xml \ + -Djava.library.path={{ graylog_root_dir }}/app/lib/sigar \ + -jar {{ graylog_root_dir }}/app/graylog.jar server -f {{ graylog_root_dir }}/etc/server.conf -np + +# When a JVM receives a SIGTERM signal it exits with 143. +SuccessExitStatus=143 +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes +SyslogIdentifier=graylog-server + +# Allow binding on privileged ports +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/roles/graylog/templates/log4j2.xml.j2 b/roles/graylog/templates/log4j2.xml.j2 new file mode 100644 index 0000000..2137cb1 --- /dev/null +++ b/roles/graylog/templates/log4j2.xml.j2 @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roles/graylog/templates/logrotate.conf.j2 b/roles/graylog/templates/logrotate.conf.j2 new file mode 100644 index 0000000..c1450d1 --- /dev/null +++ b/roles/graylog/templates/logrotate.conf.j2 @@ -0,0 +1,8 @@ +{{ graylog_root_dir }}/logs/*.log { + daily + rotate 180 + compress + notifempty + missingok + copytruncate +} diff --git a/roles/graylog/templates/post-backup.j2 b/roles/graylog/templates/post-backup.j2 new file mode 100644 index 0000000..bb5d786 --- /dev/null +++ b/roles/graylog/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -rf {{ graylog_root_dir }}/backup/{mongo,es}/* diff --git a/roles/graylog/templates/pre-backup.j2 b/roles/graylog/templates/pre-backup.j2 new file mode 100644 index 0000000..2ee9d42 --- /dev/null +++ b/roles/graylog/templates/pre-backup.j2 @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eo pipefail + +mongodump \ +{% if graylog_mongo_url | length == 1 and graylog_mongo_pass is defined and graylog_mongo_pass != False %} +{% set graylog_mongo = graylog_mongo_url[0] | urlsplit %} + --uri {{ graylog_mongo['scheme'] }}://{{ graylog_mongo_user }}:{{ graylog_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ graylog_mongo['hostname'] }}{% if graylog_mongo['port'] %}:{{ graylog_mongo['port'] }}{% endif %}{{ graylog_mongo['path'] }}?{{ graylog_mongo['query'] }} \ +{% else %} + --uri {{ graylog_mongo_url[0] }} \ +{% endif %} + --quiet --out {{ graylog_root_dir }}/backup/mongo diff --git a/roles/graylog/templates/server.conf.j2 b/roles/graylog/templates/server.conf.j2 new file mode 100644 index 0000000..1afdbdb --- /dev/null +++ b/roles/graylog/templates/server.conf.j2 @@ -0,0 +1,60 @@ +is_master = {{ graylog_is_master | ternary('true','false') }} +node_id_file = {{ graylog_root_dir }}/state/node-id +password_secret = {{ graylog_pass_secret }} +root_password_sha2 = {{ graylog_admin_pass | hash('sha256') }} +root_email = {{ system_admin_email | default('""') }} +root_timezone = {{ system_tz | default('UTC') }} +http_bind_address = 0.0.0.0:{{ graylog_api_port }} +{% if graylog_external_uri is defined %} +http_external_uri = {{ graylog_external_uri }}{% if not graylog_external_uri is search('/$') %}/{% endif %} + +{% endif %} +http_enable_gzip = false +{% if graylog_http_src_ip | length > 0 and '0.0.0.0/0' not in graylog_http_src_ip %} +trusted_proxies = {% for host in graylog_http_src_ip %}{{ host }}{% if not host is search('/\d+$') %}/32{% endif %}{% if not loop.last %},{% else %}{% endif %}{% endfor %} + +{% endif %} +elasticsearch_hosts = {{ graylog_es_hosts | join(',') }} +elasticsearch_cluster_name = {{ graylog_es_cluster_name | default('elasticsearch') }} +{% if graylog_mongo_pass is defined and graylog_mongo_pass != False and graylog_mongo_url | length == 1 %} +mongodb_uri = {% for url in graylog_mongo_url %}{{ url | urlsplit('scheme') }}://{{ graylog_mongo_user }}:{{ graylog_mongo_pass | urlencode | regex_replace('/','%2F') }}@{{ url | urlsplit('hostname') }}{% if url | urlsplit('port') %}:{{ url | urlsplit('port') }}{% endif %}{{ url | urlsplit('path') }}?{{ url | urlsplit('query') }}{% if not loop.last %},{% endif %} +{% endfor %} +{% else %} +mongodb_uri = {{ graylog_mongo_url | join(',') }} +{% endif %} + +message_journal_enabled = true + +transport_email_enabled = true +transport_email_hostname = localhost +transport_email_port = 25 +transport_email_use_auth = false +transport_email_from_email = graylog@{{ ansible_domain }} +{% if graylog_external_uri is defined %} +transport_email_web_interface_url = {{ graylog_external_uri }} +{% endif %} + +{% if system_proxy is defined and system_proxy != '' %} +http_proxy_uri = {{ system_proxy }} +http_non_proxy_hosts = {{ (system_proxy_no_proxy | default([]) + ansible_all_ipv4_addresses) | join(',') }} +{% endif %} + +bin_dir = {{ graylog_root_dir }}/app/bin +data_dir = {{ graylog_root_dir }}/data +plugin_dir = {{ graylog_root_dir }}/app/plugin +message_journal_dir = {{ graylog_root_dir }}/data/journal +message_journal_max_size = {{ graylog_journal_max_size }}gb + +allow_leading_wildcard_searches = true + +{% if 'dnsresolver' in graylog_plugins_to_install %} +dns_resolver_enabled = true +{% endif %} + +{% if graylog_tls_versions | length > 0 %} +enabled_tls_protocols = {{ graylog_tls_versions | join(',') }} +{% endif %} + +{% if graylog_es_version is defined %} +elasticsearch_version = {{ graylog_es_version }} +{% endif %} diff --git a/roles/httpd_common/defaults/main.yml b/roles/httpd_common/defaults/main.yml new file mode 100644 index 0000000..5646f8b --- /dev/null +++ b/roles/httpd_common/defaults/main.yml @@ -0,0 +1,67 @@ +--- +httpd_ports: ['80'] +httpd_src_ip: + - 0.0.0.0/0 +httpd_user: apache +httpd_group: apache +httpd_modules: + - unixd + - access_compat + - alias + - allowmethods + - auth_basic + - authn_core + - authn_file + - authz_core + - authz_host + - authz_user + - authnz_pam + - autoindex + - deflate + - dir + - env + - expires + - filter + - headers + - log_config + - logio + - mime_magic + - mime + - include + - remoteip + - rewrite + - setenvif + - systemd + - status + - negotiation + - fcgid + - proxy + - proxy_fcgi + - proxy_http + - proxy_wstunnel +# Optional extra module to load +# httpd_modules_extras: [] +httpd_mpm: prefork +httpd_primary_domain: 'firewall-services.com' +httpd_log_format: 'combined_virtual' +httpd_status_ip: + - '127.0.0.1' + +httpd_proxy_timeout: 90 + +httpd_ansible_vhosts: [] +httpd_ansible_directories: [] +httpd_custom_conf: | + # Custom config directives here + +httpd_htpasswd: [] +# httpd_htpasswd: +# - path: /etc/httpd/conf/customers.htpasswd +# users: +# - login: client1 +# password: s3crEt. + +# The default vhost will have the name of the server in the inventory. +# But you can overwrite it with this var +# httpd_default_vhost: +... diff --git a/roles/httpd_common/files/index_default.html b/roles/httpd_common/files/index_default.html new file mode 100644 index 0000000..e69de29 diff --git a/roles/httpd_common/files/index_maintenance.html b/roles/httpd_common/files/index_maintenance.html new file mode 100644 index 0000000..733cb5a --- /dev/null +++ b/roles/httpd_common/files/index_maintenance.html @@ -0,0 +1 @@ +Maintenance en cours... diff --git a/roles/httpd_common/handlers/main.yml b/roles/httpd_common/handlers/main.yml new file mode 100644 index 0000000..ab9e681 --- /dev/null +++ b/roles/httpd_common/handlers/main.yml @@ -0,0 +1,10 @@ +--- + +- include: ../common/handlers/main.yml + +- name: reload httpd + service: name=httpd state=reloaded + +- name: restart httpd + service: name=httpd state=restarted +... diff --git a/roles/httpd_common/meta/main.yml b/roles/httpd_common/meta/main.yml new file mode 100644 index 0000000..01dfbc8 --- /dev/null +++ b/roles/httpd_common/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: mkdir diff --git a/roles/httpd_common/tasks/filebeat.yml b/roles/httpd_common/tasks/filebeat.yml new file mode 100644 index 0000000..b7be10d --- /dev/null +++ b/roles/httpd_common/tasks/filebeat.yml @@ -0,0 +1,5 @@ +--- +- name: Deploy filebeat module + template: src=filebeat.yml.j2 dest=/etc/filebeat/ansible_modules.d/httpd.yml + tags: web,log + diff --git a/roles/httpd_common/tasks/main.yml b/roles/httpd_common/tasks/main.yml new file mode 100644 index 0000000..ddb0455 --- /dev/null +++ b/roles/httpd_common/tasks/main.yml @@ -0,0 +1,164 @@ +--- + +- include_vars: "{{ item }}" + with_first_found: + - vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_os_family }}-{{ ansible_distribution_major_version }}.yml + - vars/{{ ansible_distribution }}.yml + - vars/{{ ansible_os_family }}.yml + - vars/defaults.yml + tags: web + +- name: Install packages + yum: name={{ httpd_common_packages }} + tags: web + +- name: List httpd ports + set_fact: httpd_ports={{ httpd_ports + (httpd_ansible_vhosts | selectattr('port','defined') | map(attribute='port') | list) | unique }} + tags: [firewall,web] + +- name: Allow httpd to bind on ports + seport: ports={{ httpd_ports | join(',') }} proto=tcp setype=http_port_t state=present + when: ansible_selinux.status == 'enabled' + tags: web + +- name: Creates default root directory + file: path={{ item }} state=directory mode=755 + with_items: + - /var/www/html/default + - /var/www/html/default/cgi-bin + - /var/www/html/downtime + - /etc/httpd/ansible_conf.d + - /etc/httpd/custom_conf.d + - /etc/httpd/ansible_conf.modules.d + tags: web + +- name: Deploy an empty default index for the catch all vhost + copy: src=index_default.html dest=/var/www/html/default/index.html + tags: web + +- name: Deploy the maintenance page + copy: src=index_maintenance.html dest=/var/www/html/default/maintenance.html + tags: web + +- name: Remove obsolete configuration files + file: path={{ item }} state=absent + with_items: + - /etc/httpd/ansible_conf.d/10-welcome.conf + tags: web + +- name: Deploy mpm configuration + template: src=10-mpm.conf.j2 dest=/etc/httpd/ansible_conf.modules.d/10-mpm.conf + notify: restart httpd + tags: [conf,web] + +- name: Deploy main httpd configuration + template: src={{ item.src }} dest={{ item.dest }} + with_items: + - src: httpd.conf.j2 + dest: /etc/httpd/conf/httpd.conf + - src: common_env.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_env.inc + - src: autoindex.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-autoindex.conf + - src: status.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-status.conf + - src: errors.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-errors.conf + - src: vhost_default.conf.j2 + dest: /etc/httpd/ansible_conf.d/20-vhost_default.conf + - src: 00-base_mod.conf.j2 + dest: /etc/httpd/ansible_conf.modules.d/00-base_mod.conf + - src: 20-cgi.conf.j2 + dest: /etc/httpd/ansible_conf.modules.d/20-cgi.conf + notify: reload httpd + tags: [conf,web] + +- name: Check if common config templates are present + stat: path=/etc/httpd/ansible_conf.d/{{ item }} + with_items: + - common_perf.inc + - common_filter.inc + - common_force_ssl.inc + - common_letsencrypt.inc + - common_cache.inc + - common_mod_security2.inc + register: common_files + tags: [conf,web] + +- name: Deploy dummy config files if needed + copy: content="# Dummy config file. Use httpd_front / letsencrypt roles to get the real config" dest=/etc/httpd/ansible_conf.d/{{ item.item }} + when: not item.stat.exists + with_items: "{{ common_files.results }}" + notify: reload httpd + tags: [conf,web] + +- name: Deploy ansible vhosts configuration + template: src=vhost_ansible.conf.j2 dest=/etc/httpd/ansible_conf.d/30-vhost_ansible.conf + notify: reload httpd + tags: [conf,web] + +- name: Create ansible directories + file: path={{ item.path }} state=directory + with_items: "{{ httpd_ansible_directories }}" + tags: [conf,web] + +- name: Deploy ansible directories configuration + template: src=dir_ansible.conf.j2 dest=/etc/httpd/ansible_conf.d/10-dir_ansible.conf + notify: reload httpd + tags: [conf,web] + +- name: Deploy custom global configuration + copy: content={{ httpd_custom_conf }} dest=/etc/httpd/ansible_conf.d/10-custom_ansible.conf + notify: reload httpd + tags: [conf,web] + +- name: Configure log rotation + template: src=logrotate.conf.j2 dest=/etc/logrotate.d/httpd + tags: [conf,web] + +- name: Remove old iptables rule + iptables_raw: + name: httpd_port + state: absent + when: iptables_manage | default(True) + tags: [firewall,web] + +- name: Handle HTTP ports + iptables_raw: + name: httpd_ports + state: "{{ (httpd_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state new -p tcp -m multiport --dports {{ httpd_ports | join(',') }} -s {{ httpd_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: [firewall,web] + +- name: Start and enable the service + service: name=httpd state=started enabled=yes + tags: web + +- name: Allow network connections in SELinux + seboolean: name={{ item }} state=yes persistent=yes + with_items: + - httpd_can_connect_ldap + - httpd_unified + - httpd_can_network_connect + - httpd_mod_auth_pam + when: ansible_selinux.status == 'enabled' + tags: web + +- name: Create or update htpasswd files + htpasswd: + path: "{{ item[0].path }}" + name: "{{ item[1].login }}" + password: "{{ item[1].pass | default(omit) }}" + owner: root + group: "{{ httpd_user }}" + mode: 0640 + state: "{{ (item[1].state | default('present')) }}" + with_subelements: + - "{{ httpd_htpasswd }}" + - users + tags: web + +- include: filebeat.yml +... diff --git a/roles/httpd_common/templates/00-base_mod.conf.j2 b/roles/httpd_common/templates/00-base_mod.conf.j2 new file mode 100644 index 0000000..6debc77 --- /dev/null +++ b/roles/httpd_common/templates/00-base_mod.conf.j2 @@ -0,0 +1,6 @@ +{% for module in httpd_modules %} +LoadModule {{ module }}_module modules/mod_{{ module }}.so +{% endfor %} +{% for module in httpd_modules_extras | default([]) %} +LoadModule {{ module }}_module modules/mod_{{ module }}.so +{% endfor %} diff --git a/roles/httpd_common/templates/10-mpm.conf.j2 b/roles/httpd_common/templates/10-mpm.conf.j2 new file mode 100644 index 0000000..a7e3c18 --- /dev/null +++ b/roles/httpd_common/templates/10-mpm.conf.j2 @@ -0,0 +1 @@ +LoadModule mpm_{{ httpd_mpm }}_module modules/mod_mpm_{{ httpd_mpm }}.so diff --git a/roles/httpd_common/templates/20-cgi.conf.j2 b/roles/httpd_common/templates/20-cgi.conf.j2 new file mode 100644 index 0000000..692495e --- /dev/null +++ b/roles/httpd_common/templates/20-cgi.conf.j2 @@ -0,0 +1,5 @@ +{% if httpd_mpm == 'prefork' %} +LoadModule cgi_module modules/mod_cgi.so +{% else %} +LoadModule cgid_module modules/mod_cgid.so +{% endif %} diff --git a/roles/httpd_common/templates/autoindex.conf.j2 b/roles/httpd_common/templates/autoindex.conf.j2 new file mode 100644 index 0000000..88937b0 --- /dev/null +++ b/roles/httpd_common/templates/autoindex.conf.j2 @@ -0,0 +1,45 @@ +IndexOptions FancyIndexing HTMLTable VersionSort +Alias /icons/ "/usr/share/httpd/icons/" + + + Options Indexes MultiViews FollowSymlinks + AllowOverride None + Require all granted + + +AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip + +AddIconByType (TXT,/icons/text.gif) text/* +AddIconByType (IMG,/icons/image2.gif) image/* +AddIconByType (SND,/icons/sound2.gif) audio/* +AddIconByType (VID,/icons/movie.gif) video/* + +AddIcon /icons/binary.gif .bin .exe +AddIcon /icons/binhex.gif .hqx +AddIcon /icons/tar.gif .tar +AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv +AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip +AddIcon /icons/a.gif .ps .ai .eps +AddIcon /icons/layout.gif .html .shtml .htm .pdf +AddIcon /icons/text.gif .txt +AddIcon /icons/c.gif .c +AddIcon /icons/p.gif .pl .py +AddIcon /icons/f.gif .for +AddIcon /icons/dvi.gif .dvi +AddIcon /icons/uuencoded.gif .uu +AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl +AddIcon /icons/tex.gif .tex +AddIcon /icons/bomb.gif /core +AddIcon /icons/bomb.gif */core.* + +AddIcon /icons/back.gif .. +AddIcon /icons/hand.right.gif README +AddIcon /icons/folder.gif ^^DIRECTORY^^ +AddIcon /icons/blank.gif ^^BLANKICON^^ + +DefaultIcon /icons/unknown.gif + +ReadmeName README.html +HeaderName HEADER.html + +IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t diff --git a/roles/httpd_common/templates/common_env.inc.j2 b/roles/httpd_common/templates/common_env.inc.j2 new file mode 100644 index 0000000..4d311df --- /dev/null +++ b/roles/httpd_common/templates/common_env.inc.j2 @@ -0,0 +1,7 @@ +# Determine which protocol to use +RewriteRule .* - [E=HTTP:http] +RewriteCond %{HTTPS} =on +RewriteRule .* - [E=HTTP:https] +{% if httpd_log_format == 'combined_virtual_backend' %} +SetEnvIf X-Forwarded-Proto https HTTPS=on +{% endif %} diff --git a/roles/httpd_common/templates/dir_ansible.conf.j2 b/roles/httpd_common/templates/dir_ansible.conf.j2 new file mode 100644 index 0000000..826eb8b --- /dev/null +++ b/roles/httpd_common/templates/dir_ansible.conf.j2 @@ -0,0 +1,34 @@ + +# {{ ansible_managed }} + +{% for dir in httpd_ansible_directories | default([]) %} + +{% if dir.full_config is defined %} +{{ dir.full_config | indent(4, true) }} +{% else %} +{% if dir.custom_pre is defined %} +{{ dir.custom_pre | indent(4, true) }} +{% endif %} + AllowOverride {{ dir.allow_override | default('All') }} +{% if dir.options is defined %} + Options {{ dir.options | join(' ') }} +{% endif %} +{% if dir.allowed_ip is not defined or dir.allowed_ip == 'all' %} + Require all granted +{% elif dir.allowed_ip == 'none' %} + Require all denied +{% else %} + Require ip {{ dir.allowed_ip | join(' ') }} +{% endif %} +{% if dir.php is defined and dir.php.enabled | default(False) %} + + SetHandler "proxy:unix:/run/php-fpm/{{ dir.php.pool | default('php70') }}.sock|fcgi://localhost" + +{% endif %} +{% if dir.custom_post is defined %} +{{ dir.custom_post | indent(4, true) }} +{% endif %} +{% endif %} + + +{% endfor %} diff --git a/roles/httpd_common/templates/errors.conf.j2 b/roles/httpd_common/templates/errors.conf.j2 new file mode 100644 index 0000000..1aa0e8e --- /dev/null +++ b/roles/httpd_common/templates/errors.conf.j2 @@ -0,0 +1,30 @@ +Alias /_deferror/ "/usr/share/httpd/error/" + + + AllowOverride None + Options IncludesNoExec + AddOutputFilter Includes html + AddHandler type-map var + Require all granted + LanguagePriority en cs de es fr it ja ko nl pl pt-br ro sv tr + ForceLanguagePriority Prefer Fallback + + +ErrorDocument 400 /_deferror/HTTP_BAD_REQUEST.html.var +ErrorDocument 401 /_deferror/HTTP_UNAUTHORIZED.html.var +ErrorDocument 403 /_deferror/HTTP_FORBIDDEN.html.var +ErrorDocument 404 /_deferror/HTTP_NOT_FOUND.html.var +ErrorDocument 405 /_deferror/HTTP_METHOD_NOT_ALLOWED.html.var +ErrorDocument 408 /_deferror/HTTP_REQUEST_TIME_OUT.html.var +ErrorDocument 410 /_deferror/HTTP_GONE.html.var +ErrorDocument 411 /_deferror/HTTP_LENGTH_REQUIRED.html.var +ErrorDocument 412 /_deferror/HTTP_PRECONDITION_FAILED.html.var +ErrorDocument 413 /_deferror/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var +ErrorDocument 414 /_deferror/HTTP_REQUEST_URI_TOO_LARGE.html.var +ErrorDocument 415 /_deferror/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var +ErrorDocument 500 /_deferror/HTTP_INTERNAL_SERVER_ERROR.html.var +ErrorDocument 501 /_deferror/HTTP_NOT_IMPLEMENTED.html.var +ErrorDocument 502 /_deferror/HTTP_BAD_GATEWAY.html.var +ErrorDocument 503 /_deferror/HTTP_SERVICE_UNAVAILABLE.html.var +ErrorDocument 506 /_deferror/HTTP_VARIANT_ALSO_VARIES.html.var + diff --git a/roles/httpd_common/templates/filebeat.yml.j2 b/roles/httpd_common/templates/filebeat.yml.j2 new file mode 100644 index 0000000..6f809a4 --- /dev/null +++ b/roles/httpd_common/templates/filebeat.yml.j2 @@ -0,0 +1,15 @@ +--- +- module: apache + access: + enabled: True + input: + exclude_files: + - '\.[gx]z$' + - '\d+$' + error: + enabled: True + input: + exclude_files: + - '\.[gx]z$' + - '\d+$' + diff --git a/roles/httpd_common/templates/httpd.conf.j2 b/roles/httpd_common/templates/httpd.conf.j2 new file mode 100644 index 0000000..9b1a342 --- /dev/null +++ b/roles/httpd_common/templates/httpd.conf.j2 @@ -0,0 +1,55 @@ +ServerRoot "/etc/httpd" +{% for port in httpd_ports %} +Listen {{ port }} http +{% endfor %} +Include ansible_conf.modules.d/*.conf +User {{ httpd_user }} +Group {{ httpd_group }} +ServerAdmin root@{{ inventory_hostname }} +ServerName {{ inventory_hostname }} +ServerTokens Prod + +ProxyTimeout {{ httpd_proxy_timeout }} + + + AllowOverride none + Require all denied + +DocumentRoot "/var/www/html/default" + + AllowOverride None + Require all granted + + + DirectoryIndex index.html index.php + + + Require all denied + +ErrorLog "logs/error_log" +LogLevel warn + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" scheme=\"%{HTTP}e\"" combined + LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" scheme=\"%{HTTP}e\"" combined_virtual + LogFormat "%V %{X-Forwarded-For}i %l %{Auth-User}i %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" scheme=\"%{HTTP}e\"" combined_virtual_backend + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + CustomLog "logs/access_log" {{ httpd_log_format | default('combined_virtual') }} + + + + TypesConfig /etc/mime.types + AddType application/x-compress .Z + AddType application/x-gzip .gz .tgz + AddType text/html .shtml + AddOutputFilter INCLUDES .shtml + +AddDefaultCharset UTF-8 + + MIMEMagicFile conf/magic + +EnableSendfile on +IncludeOptional ansible_conf.d/*.conf +IncludeOptional custom_conf.d/*.conf diff --git a/roles/httpd_common/templates/logrotate.conf.j2 b/roles/httpd_common/templates/logrotate.conf.j2 new file mode 100644 index 0000000..882f60c --- /dev/null +++ b/roles/httpd_common/templates/logrotate.conf.j2 @@ -0,0 +1,11 @@ +/var/log/httpd/*log { + daily + rotate 60 + missingok + notifempty + compress + sharedscripts + postrotate + /bin/systemctl reload httpd.service > /dev/null 2>/dev/null || true + endscript +} diff --git a/roles/httpd_common/templates/status.conf.j2 b/roles/httpd_common/templates/status.conf.j2 new file mode 100644 index 0000000..b0446db --- /dev/null +++ b/roles/httpd_common/templates/status.conf.j2 @@ -0,0 +1,7 @@ +{% if httpd_status_ip is defined and httpd_status_ip | length > 0 %} + + SetHandler server-status + Require ip {{ httpd_status_ip | join(' ') }} + +ExtendedStatus On +{% endif %} diff --git a/roles/httpd_common/templates/vhost_ansible.conf.j2 b/roles/httpd_common/templates/vhost_ansible.conf.j2 new file mode 100644 index 0000000..94d3a44 --- /dev/null +++ b/roles/httpd_common/templates/vhost_ansible.conf.j2 @@ -0,0 +1,204 @@ +# {{ ansible_managed }} + +{% for vhost in httpd_ansible_vhosts | default([]) %} + +##################################### +## Plain vhost for {{ vhost.name }} +##################################### + + + ServerName {{ vhost.name }} +{% if vhost.full_config is defined %} +{{ vhost.full_config | indent(2, true) }} +{% else %} +{% if vhost.aliases is defined %} + ServerAlias {{ vhost.aliases | default([]) | join(' ') }} +{% endif %} +{% if vhost.webmaster_email is defined %} + ServerAdmin {{ vhost.webmaster_email }} +{% endif %} +{% if vhost.custom_pre is defined %} +{{ vhost.custom_pre | indent(2, true) }} +{% endif %} +{% if vhost.set_remote_user_from_header is defined %} + # Read {{ vhost.set_remote_user_from_header }} header from proxy and set REMOTE_USER + RewriteEngine On + RewriteCond %{HTTP:{{ vhost.set_remote_user_from_header }}} ^(\w+)$ + RewriteRule .* - [E=REMOTE_USER:%1] +{% endif %} + DocumentRoot {{ vhost.document_root | default('/var/www/html/default') }} +{% if vhost.maintenance | default(False) %} + Include ansible_conf.d/common_maintenance.inc +{% else %} + Alias /_deferror/ "/usr/share/httpd/error/" + Include ansible_conf.d/common_env.inc +{% if vhost.common_perf | default((httpd_log_format == 'combined_virtual_backend') | ternary(False,True)) %} + Include ansible_conf.d/common_perf.inc +{% endif %} +{% if vhost.common_filter | default((httpd_log_format == 'combined_virtual_backend') | ternary(False,True)) %} + Include ansible_conf.d/common_filter.inc +{% endif %} +{% if vhost.common_cache | default(False) %} + Include ansible_conf.d/common_cache.inc +{% endif %} +{% if vhost.ssl is defined and vhost.ssl.enabled | default((httpd_log_format == 'combined_virtual_backend') | ternary(False,True)) and vhost.ssl.forced | default((httpd_log_format == 'combined_virtual_backend') | ternary(False,True)) %} + Include ansible_conf.d/common_force_ssl.inc +{% endif %} +{% if ((vhost.common_letsencrypt is defined and vhost.common_letsencrypt) or (vhost.ssl is defined and vhost.ssl.letsencrypt_cert is defined )) | default(False) %} + Include ansible_conf.d/common_letsencrypt.inc +{% endif %} +{% if vhost.common_mod_security | default(False) == True or vhost.common_mod_security | default(False) == 'audit' %} + Include ansible_conf.d/common_mod_security2.inc +{% if vhost.common_mod_security | default(False) == 'audit' %} + SecRuleEngine DetectionOnly +{% endif %} +{% for id in vhost.mod_security_disabled_rules | default([]) %} + SecRuleRemoveById {{ id }} +{% endfor %} +{% endif %} +{% if vhost.include_conf is defined %} +{% for include in vhost.include_conf | default([]) %} + Include {{ include }} +{% endfor %} +{% endif %} +{% if vhost.proxypass is defined %} +{% if vhost.proxypass is match('^https://') %} + SSLProxyEngine On +{% endif %} + RequestHeader set X-Forwarded-Proto "http" + ProxyPass /.well-known/acme-challenge ! + ProxyPass /_deferror/ ! + ProxyPreserveHost {{ vhost.proxypreservehost | default(True) | ternary('On','Off') }} + # WebSocket proxy handling + RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC] + RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC] + RewriteRule .* {{ vhost.proxypass | regex_replace('^http','ws') }}%{REQUEST_URI} [P] + # Normal proxy + ProxyPass / {{ vhost.proxypass }} + ProxyPassReverse / {{ vhost.proxypass }} +{% endif %} +{% if vhost.src_ip is defined %} + +{% if vhost.src_ip | length < 1 %} + Require all denied +{% else %} + Require ip {{ vhost.src_ip | join(' ') }} +{% endif %} + +{% endif %} +{% if vhost.custom_post is defined %} +{{ vhost.custom_post | indent(2, true) }} +{% endif %} +{% endif %} +{% endif %} + +{% if vhost.ssl is defined and vhost.ssl.enabled | default((httpd_log_format == 'combined_virtual_backend') | ternary(False,True)) %} + +##################################### +## SSL vhost for {{ vhost.name }} +##################################### + + + + ServerName {{ vhost.name }} +{% if vhost.ssl.full_config is defined %} +{{ vhost.ssl.full_config | indent(4, true) }} +{% else %} +{% if vhost.aliases is defined %} + ServerAlias {{ vhost.aliases | default([]) | join(' ') }} +{% endif %} +{% if vhost.webmaster_email is defined %} + ServerAdmin {{ vhost.webmaster_email }} +{% endif %} +{% if vhost.custom_pre is defined %} +{{ vhost.custom_pre | indent(4, true) }} +{% endif %} +{% if vhost.set_remote_user_from_header is defined %} + # Read {{ vhost.set_remote_user_from_header }} header from proxy and set REMOTE_USER + RewriteEngine On + RewriteCond %{HTTP:{{ vhost.set_remote_user_from_header }}} ^(\w+)$ + RewriteRule .* - [E=REMOTE_USER:%1] +{% endif %} + DocumentRoot {{ vhost.document_root | default('/var/www/html/default') }} + SSLEngine On +{% if vhost.maintenance | default(False) %} + Include ansible_conf.d/common_maintenance.inc +{% else %} + Alias /_deferror/ "/usr/share/httpd/error/" +{% if vhost.ssl.cert is defined and vhost.ssl.key is defined %} + SSLCertificateFile {{ vhost.ssl.cert }} + SSLCertificateKeyFile {{ vhost.ssl.key }} +{% if vhost.ssl.cert_chain is defined %} + SSLCertificateChainFile {{ vhost.ssl.cert_chain }} +{% endif %} +{% elif vhost.ssl.letsencrypt_cert is defined %} + SSLCertificateFile /var/lib/dehydrated/certificates/certs/{{ vhost.ssl.letsencrypt_cert }}/cert.pem + SSLCertificateKeyFile /var/lib/dehydrated/certificates/certs/{{ vhost.ssl.letsencrypt_cert }}/privkey.pem + SSLCertificateChainFile /var/lib/dehydrated/certificates/certs/{{ vhost.ssl.letsencrypt_cert }}/chain.pem +{% endif %} + Include ansible_conf.d/common_env.inc +{% if vhost.common_perf | default(True) %} + Include ansible_conf.d/common_perf.inc +{% endif %} +{% if vhost.common_filter | default(True) %} + Include ansible_conf.d/common_filter.inc +{% endif %} +{% if vhost.common_cache | default(False) %} + Include ansible_conf.d/common_cache.inc +{% endif %} +{% if vhost.include_conf is defined %} +{% for include in vhost.include_conf | default([]) %} + Include {{ include }} +{% endfor %} +{% endif %} +{% if ((vhost.common_letsencrypt is defined and vhost.common_letsencrypt) or (vhost.ssl is defined and vhost.ssl.letsencrypt_cert is defined )) | default(False) %} + Include ansible_conf.d/common_letsencrypt.inc +{% endif %} +{% if vhost.common_mod_security | default(False) == True or vhost.common_mod_security | default(False) == 'audit' %} + Include ansible_conf.d/common_mod_security2.inc +{% if vhost.common_mod_security | default(False) == 'audit' %} + SecRuleEngine DetectionOnly +{% endif %} +{% for id in vhost.mod_security_disabled_rules | default([]) %} + SecRuleRemoveById {{ id }} +{% endfor %} +{% endif %} +{% if vhost.proxypass is defined %} +{% if vhost.proxypass is match('^https://') %} + SSLProxyEngine On +{% endif %} + RequestHeader set X-Forwarded-Proto "https" + ProxyPass /.well-known/acme-challenge ! + ProxyPass /_deferror/ ! + ProxyPreserveHost {{ vhost.proxypreservehost | default(True) | ternary('On','Off') }} + # WebSocket proxy handling + RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC] + RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC] + RewriteRule .* {{ vhost.proxypass | regex_replace('^http','ws') }}%{REQUEST_URI} [P] + # Normal proxy + ProxyPass / {{ vhost.proxypass }} + ProxyPassReverse / {{ vhost.proxypass }} +{% endif %} +{% if vhost.src_ip is defined %} + +{% if vhost.src_ip | length < 1 %} + Require all denied +{% else %} + Require ip {{ vhost.src_ip | join(' ') }} +{% endif %} + +{% endif %} +{% if vhost.custom_post is defined %} +{{ vhost.custom_post | indent(4, true) }} +{% endif %} +{% endif %} +{% endif %} + + +{% endif %} + +##################################### +## End of config for {{ vhost.name }} +##################################### + +{% endfor %} diff --git a/roles/httpd_common/templates/vhost_default.conf.j2 b/roles/httpd_common/templates/vhost_default.conf.j2 new file mode 100644 index 0000000..f7a8913 --- /dev/null +++ b/roles/httpd_common/templates/vhost_default.conf.j2 @@ -0,0 +1,24 @@ + + Require all granted + AllowOverride None + Options None + + + Require all granted + AllowOverride None + SetHandler cgi-script + Options ExecCGI + + + + ServerName {{ httpd_default_vhost | default(inventory_hostname) }} + DocumentRoot /var/www/html/default + Include ansible_conf.d/common_letsencrypt.inc + + + + ServerName {{ httpd_default_vhost | default(inventory_hostname) }} + SSLEngine On + DocumentRoot /var/www/html/default + + diff --git a/roles/httpd_common/vars/RedHat-7.yml b/roles/httpd_common/vars/RedHat-7.yml new file mode 100644 index 0000000..0ca26e4 --- /dev/null +++ b/roles/httpd_common/vars/RedHat-7.yml @@ -0,0 +1,8 @@ +--- + +httpd_common_packages: + - httpd + - mod_fcgid + - policycoreutils-python + - python-passlib + - mod_authnz_pam diff --git a/roles/httpd_common/vars/RedHat-8.yml b/roles/httpd_common/vars/RedHat-8.yml new file mode 100644 index 0000000..f3bf5c1 --- /dev/null +++ b/roles/httpd_common/vars/RedHat-8.yml @@ -0,0 +1,8 @@ +--- + +httpd_common_packages: + - httpd + - mod_fcgid + - python3-policycoreutils + - python3-passlib + - mod_authnz_pam diff --git a/roles/httpd_common/vars/defaults.yml b/roles/httpd_common/vars/defaults.yml new file mode 100644 index 0000000..857bd74 --- /dev/null +++ b/roles/httpd_common/vars/defaults.yml @@ -0,0 +1,4 @@ +--- + +httpd_common_packages: + - httpd diff --git a/roles/httpd_front/defaults/main.yml b/roles/httpd_front/defaults/main.yml new file mode 100644 index 0000000..b598d39 --- /dev/null +++ b/roles/httpd_front/defaults/main.yml @@ -0,0 +1,39 @@ +--- +httpd_ssl_ports: ['443'] +httpd_ssl_src_ip: + - 0.0.0.0/0 +httpd_front_modules: + - ssl + - socache_shmcb + - cache + - cache_disk + - security2 + - unique_id +httpd_cert_path: /etc/pki/tls/certs/localhost.crt +httpd_key_path: /etc/pki/tls/private/localhost.key +# httpd_chain_path: /etc/pki/tls/certs/chain.crt + +# httpd_ssl_cipher_suite: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + +# httpd_dos_page_count: 8 +# httpd_dos_site_count: 150 +# httpd_dos_page_interval: 1 +# httpd_dos_site_interval: 5 +# httpd_dos_block_time: 30 +# httpd_dos_whitelisted_ip: +# - 12.13.14.15 +# - 41.42.43.44 + +# httpd_cache_max_file_size: 1000000 +# httpd_cache_default_expire: 3600 +# httpd_cache_max_expire: 86400 +# httpd_cache_limit: 200M + +# httpd_mod_security: True | audit +# httpd_mod_security_request_body_limit: 13107200 +# httpd_mod_security_body_no_files_limit: 131072 +# httpd_mod_security_in_memory_limit: 131072 +# httpd_mod_sec_disabled_rules: +# - 960015 +# - 981203 +... diff --git a/roles/httpd_front/files/dehydrated_deploy_hook b/roles/httpd_front/files/dehydrated_deploy_hook new file mode 100644 index 0000000..71ddbd9 --- /dev/null +++ b/roles/httpd_front/files/dehydrated_deploy_hook @@ -0,0 +1,3 @@ +#!/bin/sh + +/sbin/service httpd reload diff --git a/roles/httpd_front/handlers/main.yml b/roles/httpd_front/handlers/main.yml new file mode 100644 index 0000000..a9f19d7 --- /dev/null +++ b/roles/httpd_front/handlers/main.yml @@ -0,0 +1,8 @@ +--- + +- include: ../httpd_common/handlers/main.yml + +- name: restart htcacheclean + service: name=htcacheclean state=restarted enabled=yes + +... diff --git a/roles/httpd_front/meta/main.yml b/roles/httpd_front/meta/main.yml new file mode 100644 index 0000000..6671012 --- /dev/null +++ b/roles/httpd_front/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - { role: httpd_common } +... diff --git a/roles/httpd_front/tasks/main.yml b/roles/httpd_front/tasks/main.yml new file mode 100644 index 0000000..67149d4 --- /dev/null +++ b/roles/httpd_front/tasks/main.yml @@ -0,0 +1,134 @@ +--- + +- name: Install needed packages + yum: + name: + - mod_ssl + - mod_evasive + - mod_security + - mod_security_crs + tags: [package,web] + +- name: List httpd SSL ports + set_fact: httpd_ssl_ports={{ httpd_ssl_ports + (httpd_ansible_vhosts | selectattr('ssl','defined') | selectattr('ssl.port','defined') | map(attribute='ssl.port') | list) | unique }} + tags: [firewall,web] + +- name: Allow httpd to bind on ssl ports + seport: ports={{ httpd_ssl_ports | join(',') }} proto=tcp setype=http_port_t state=present + when: ansible_selinux.status == 'enabled' + tags: [firewall,web] + +- set_fact: httpd_cert_path={{ '/var/lib/dehydrated/certificates/certs/' + httpd_letsencrypt_cert + '/cert.pem' }} + when: httpd_letsencrypt_cert is defined + tags: [cert,web,conf] +- set_fact: httpd_key_path={{ '/var/lib/dehydrated/certificates/certs/' + httpd_letsencrypt_cert + '/privkey.pem' }} + when: httpd_letsencrypt_cert is defined + tags: [cert,web,conf] +- set_fact: httpd_chain_path={{ '/var/lib/dehydrated/certificates/certs/' + httpd_letsencrypt_cert + '/chain.pem' }} + when: httpd_letsencrypt_cert is defined + tags: [cert,web,conf] + +- name: Deploy configuration fragments + template: src={{ item.src }} dest={{ item.dest }} + with_items: + - src: ssl.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-ssl.conf + - src: evasive.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-evasive.conf + - src: security.conf.j2 + dest: /etc/httpd/ansible_conf.d/10-security.conf + - src: common_filter.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_filter.inc + - src: common_perf.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_perf.inc + - src: common_cache.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_cache.inc + - src: common_force_ssl.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_force_ssl.inc + - src: common_maintenance.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_maintenance.inc + - src: common_mod_security2.inc.j2 + dest: /etc/httpd/ansible_conf.d/common_mod_security2.inc + - src: vhost_downtime.conf.j2 + dest: /etc/httpd/ansible_conf.d/21-vhost_downtime.conf + - src: 01-front.conf.j2 + dest: /etc/httpd/ansible_conf.modules.d/01-front.conf + - src: 02-evasive.conf.j2 + dest: /etc/httpd/ansible_conf.modules.d/02-evasive.conf + notify: reload httpd + tags: [conf,web] + +- name: Check if Let's Encrypt' cert exist + stat: path=/var/lib/dehydrated/certificates/certs/{{ item.ssl.letsencrypt_cert }}/cert.pem + register: httpd_letsencrypt_certs + with_items: "{{ httpd_ansible_vhosts }}" + when: + - item.ssl is defined + - item.ssl.letsencrypt_cert is defined + tags: [cert,web,conf] + +- name: Create directories for missing Let's Encrypt cert + file: path=/var/lib/dehydrated/certificates/certs/{{ item.item.ssl.letsencrypt_cert }} state=directory + with_items: "{{ httpd_letsencrypt_certs.results }}" + when: + - item.stat is defined + - not item.stat.exists + tags: [cert,web,conf] + +- name: Link missing Let's Encrypt cert to the default one + file: src={{ httpd_cert_path }} dest=/var/lib/dehydrated/certificates/certs/{{ item.item.ssl.letsencrypt_cert }}/cert.pem state=link + with_items: "{{ httpd_letsencrypt_certs.results }}" + when: + - item.stat is defined + - not item.stat.exists + tags: [cert,web,conf] + +- name: Link missing Let's Encrypt key to the default one + file: src={{ httpd_key_path }} dest=/var/lib/dehydrated/certificates/certs/{{ item.item.ssl.letsencrypt_cert }}/privkey.pem state=link + with_items: "{{ httpd_letsencrypt_certs.results }}" + when: + - item.stat is defined + - not item.stat.exists + tags: [cert,web,conf] + +- name: Link missing Let's Encrypt chain to the default cert + file: src={{ httpd_cert_path }} dest=/var/lib/dehydrated/certificates/certs/{{ item.item.ssl.letsencrypt_cert }}/chain.pem state=link + with_items: "{{ httpd_letsencrypt_certs.results }}" + when: + - item.stat is defined + - not item.stat.exists + tags: [cert,web,conf] + +- name: Create dehydrated hooks dir + file: path=/etc/dehydrated/hooks_deploy_cert.d/ state=directory + tags: [cert,web] + +- name: Deploy dehydrated hook + copy: src=dehydrated_deploy_hook dest=/etc/dehydrated/hooks_deploy_cert.d/10httpd.sh mode=755 + tags: [cert,web] + +- name: Remove old iptables rule + iptables_raw: + name: httpd_ssl_port + state: absent + when: iptables_manage | default(True) + tags: [firewall,web] + +- name: Handle HTTPS ports + iptables_raw: + name: httpd_ssl_ports + state: "{{ (httpd_ssl_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state new -p tcp -m multiport --dports {{ httpd_ssl_ports | join(',') }} -s {{ httpd_ssl_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: [firewall,web] + +- name: Deploy the Cache cleaner configuration + template: src=htcacheclean.j2 dest=/etc/sysconfig/htcacheclean + notify: restart htcacheclean + tags: [conf,web] + +- name: Enable the htcacheclean service + service: name=htcacheclean state=started enabled=yes + tags: web + +... diff --git a/roles/httpd_front/templates/01-front.conf.j2 b/roles/httpd_front/templates/01-front.conf.j2 new file mode 100644 index 0000000..2c41be3 --- /dev/null +++ b/roles/httpd_front/templates/01-front.conf.j2 @@ -0,0 +1,3 @@ +{% for module in httpd_front_modules %} +LoadModule {{ module }}_module modules/mod_{{ module }}.so +{% endfor %} diff --git a/roles/httpd_front/templates/02-evasive.conf.j2 b/roles/httpd_front/templates/02-evasive.conf.j2 new file mode 100644 index 0000000..0058a2c --- /dev/null +++ b/roles/httpd_front/templates/02-evasive.conf.j2 @@ -0,0 +1 @@ +LoadModule evasive20_module modules/mod_evasive24.so diff --git a/roles/httpd_front/templates/common_cache.inc.j2 b/roles/httpd_front/templates/common_cache.inc.j2 new file mode 100644 index 0000000..eb70019 --- /dev/null +++ b/roles/httpd_front/templates/common_cache.inc.j2 @@ -0,0 +1,15 @@ +CacheLock on +CacheLockPath /tmp/mod_cache-lock +CacheLockMaxAge 5 +CacheRoot /var/cache/httpd/proxy +CacheEnable disk / +CacheDirLevels 2 +CacheDirLength 1 +CacheIgnoreHeaders Set-Cookie +CacheMaxFileSize {{ httpd_cache_max_file_size | default('1000000') }} +CacheMinFileSize 1 +CacheIgnoreNoLastMod On +CacheIgnoreQueryString Off +CacheLastModifiedFactor 0.1 +CacheDefaultExpire {{ httpd_cache_default_expire | default('3600') }} +CacheMaxExpire {{ httpd_cache_max_expire | default('86400') }} diff --git a/roles/httpd_front/templates/common_filter.inc.j2 b/roles/httpd_front/templates/common_filter.inc.j2 new file mode 100644 index 0000000..c8a24a0 --- /dev/null +++ b/roles/httpd_front/templates/common_filter.inc.j2 @@ -0,0 +1,153 @@ +# enable rewrite engine +RewriteEngine on + +# block trace and track methods +RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) +RewriteRule .* - [F] + +# block XSS attacks (attempted to hide query string) +RewriteCond %{THE_REQUEST} \?.*\?(\ |$) +RewriteRule .* - [F] + +# block XSS attacks (http) +RewriteCond %{THE_REQUEST} (\b|%\d\d)https?(:|%3A)(/|%\d\d){2} [NC] +RewriteRule .* - [F] + +# block XSS attacks (ftp) +RewriteCond %{THE_REQUEST} (\b|%\d\d)ftp(:|%3A)(/|%\d\d){2} [NC] +RewriteRule .* - [F] + +# block hack attempts (/etc/passwd) +RewriteCond %{THE_REQUEST} (/|%2F)etc(/|%2F)passwd [NC] +RewriteRule .* - [R=404,L] + +# Block out some common exploits +# If the request query string contains /proc/self/environ (by SigSiu.net) +RewriteCond %{QUERY_STRING} proc/self/environ [OR] + +# Block out any script trying to base64_encode or base64_decode data within the URL +RewriteCond %{QUERY_STRING} base64_(en|de)code[^(]*\([^)]*\) [OR] + +# Block out any script that includes a