diff --git a/library/iptables_raw.py b/library/iptables_raw.py new file mode 100644 index 0000000..890b5dc --- /dev/null +++ b/library/iptables_raw.py @@ -0,0 +1,1087 @@ +#!/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 = open(self.iptables_names_file, 'r').read() + return table_names.splitlines() + 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_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/ampache/defaults/main.yml b/roles/ampache/defaults/main.yml new file mode 100644 index 0000000..8b35c6b --- /dev/null +++ b/roles/ampache/defaults/main.yml @@ -0,0 +1,94 @@ +--- + +ampache_id: "1" +ampache_manage_upgrade: True + +ampache_version: '4.1.1' +ampache_config_version: 40 +ampache_zip_url: https://github.com/ampache/ampache/archive/{{ ampache_version }}.zip +ampache_zip_sha1: 744ff90039a268579551d50650ce1502ec89daf1 + +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_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..9feb30b --- /dev/null +++ b/roles/ampache/meta/main.yml @@ -0,0 +1,6 @@ +--- +allow_duplicates: true +dependencies: + - role: repo_nux_dextop + - role: httpd_php +... diff --git a/roles/ampache/tasks/main.yml b/roles/ampache/tasks/main.yml new file mode 100644 index 0000000..14aaf70 --- /dev/null +++ b/roles/ampache/tasks/main.yml @@ -0,0 +1,218 @@ +--- + +- name: Install needed tools + yum: + name: + - unzip + - MySQL-python + - mariadb + - acl + - git + - composer + - patch + - ffmpeg + 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: Download Ampache + get_url: + url: "{{ ampache_zip_url }}" + dest: "{{ ampache_root_dir }}/tmp/" + checksum: "sha1:{{ ampache_zip_sha1 }}" + when: ampache_install_mode != 'none' + tags: ampache + +- name: Extract ampache archive + unarchive: + src: "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}.zip" + dest: "{{ ampache_root_dir }}/tmp" + remote_src: yes + when: ampache_install_mode != 'none' + 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 }}/db_dumps" + tags: ampache + + +- name: Move files to the correct directory + synchronize: + src: "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}/" + dest: "{{ ampache_root_dir }}/web/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + when: ampache_install_mode != 'none' + tags: ampache + +- name: Check if htaccess files needs to be moved + stat: path={{ ampache_root_dir }}/web/{{ item }}/.htaccess.dist + with_items: + - channel + - play + - rest + register: htaccess + tags: ampache + +- name: Rename htaccess files + command: mv -f {{ ampache_root_dir }}/web/{{ item.item }}/.htaccess.dist {{ ampache_root_dir }}/web/{{ item.item }}/.htaccess + with_items: "{{ htaccess.results }}" + when: item.stat.exists + tags: ampache + +- name: Install libs using composer + composer: command=install working_dir={{ ampache_root_dir }}/web executable={{ (ampache_php_version == '54') | ternary('/bin/php','/bin/php' ~ ampache_php_version ) }} + tags: ampache + +- name: Remove temp files + file: path={{ item }} state=absent + with_items: + - "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}.zip" + - "{{ ampache_root_dir }}/tmp/ampache-{{ ampache_version }}" + 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: Upgrade SQL database + command: php{{ (ampache_php_version == '54') | ternary('', ampache_php_version) }} {{ ampache_root_dir }}/web/bin/install/update_db.inc + 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 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: 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.script }}.j2 dest=/etc/backup/{{ item.type }}.d/ampache_{{ ampache_id }}_{{ item.script }} mode=750 + with_items: + - script: dump_db + type: pre + - script: rm_dump + type: 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 +... diff --git a/roles/ampache/templates/ampache.cfg.php.j2 b/roles/ampache/templates/ampache.cfg.php.j2 new file mode 100644 index 0000000..6df7c07 --- /dev/null +++ b/roles/ampache/templates/ampache.cfg.php.j2 @@ -0,0 +1,134 @@ +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 }} +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%" +mail_type = "sendmail" +mail_domain = "{{ ansible_domain }}" +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" +force_ssl = "true" +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 %} diff --git a/roles/ampache/templates/cron.sh.j2 b/roles/ampache/templates/cron.sh.j2 new file mode 100644 index 0000000..5b96e34 --- /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 == '54') | ternary('',ampache_php_version) }} {{ ampache_root_dir }}/web/bin/catalog_update.inc -c > /dev/null 2>&1 + # Add (files added) + /bin/php{{ (ampache_php_version == '54') | ternary('',ampache_php_version) }} {{ ampache_root_dir }}/web/bin/catalog_update.inc -a > /dev/null 2>&1 + # Update graphics + /bin/php{{ (ampache_php_version == '54') | ternary('',ampache_php_version) }} {{ ampache_root_dir }}/web/bin/catalog_update.inc -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 == '54') | ternary('',ampache_php_version) }} {{ ampache_root_dir }}/web/bin/catalog_update.inc -v > /dev/null 2>&1 +fi diff --git a/roles/ampache/templates/dump_db.j2 b/roles/ampache/templates/dump_db.j2 new file mode 100644 index 0000000..2ce18b0 --- /dev/null +++ b/roles/ampache/templates/dump_db.j2 @@ -0,0 +1,7 @@ +#!/bin/sh + +/usr/bin/mysqldump --user={{ ampache_mysql_user }} \ + --password={{ ampache_mysql_pass }} \ + --host={{ ampache_mysql_server }} \ + --quick --single-transaction \ + --add-drop-table {{ ampache_mysql_db }} | lz4 -c > {{ ampache_root_dir }}/db_dumps/{{ ampache_mysql_db }}.sql.lz4 diff --git a/roles/ampache/templates/httpd.conf.j2 b/roles/ampache/templates/httpd.conf.j2 new file mode 100644 index 0000000..af2bef6 --- /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 +{% 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..7e87668 --- /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 apache-ampache {{ 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..a7d2398 --- /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, exec, 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/rm_dump.j2 b/roles/ampache/templates/rm_dump.j2 new file mode 100644 index 0000000..0549f03 --- /dev/null +++ b/roles/ampache/templates/rm_dump.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ ampache_root_dir }}/db_dump/* 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/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..ebd0fd6 --- /dev/null +++ b/roles/backup/files/dump-megaraid-cfg @@ -0,0 +1,53 @@ +#!/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'; +} + +exit (0) unless($megacli); + +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 + 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..655ba45 --- /dev/null +++ b/roles/backup/files/post-backup @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ -d "/etc/backup/post.d" ]; then + for H in $(find /etc/backup/post.d -type f -o -type l | sort); do + [ -x $H ] && $H "$@" + 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..82a5e44 --- /dev/null +++ b/roles/backup/files/pre-backup @@ -0,0 +1,29 @@ +#!/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 + [ -x $H ] && $H "$@" + 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..a50e8ae --- /dev/null +++ b/roles/backup/tasks/main.yml @@ -0,0 +1,84 @@ +--- + +- 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: 755 + 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 + +... 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..e4c4f20 --- /dev/null +++ b/roles/backuppc/tasks/main.yml @@ -0,0 +1,48 @@ +--- + +- 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 and enable the service + service: name=backuppc state=started 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/bitwarden_rs/defaults/main.yml b/roles/bitwarden_rs/defaults/main.yml new file mode 100644 index 0000000..12e3ac6 --- /dev/null +++ b/roles/bitwarden_rs/defaults/main.yml @@ -0,0 +1,45 @@ +--- + +bitwarden_version: 1.14.2 +bitwarden_archive_url: https://github.com/dani-garcia/bitwarden_rs/archive/{{ bitwarden_version }}.tar.gz +bitwarden_archive_sha1: 1bb75b6ab11371ab60380ef19151ebd9410de4ef + +bitwarden_web_version: 2.13.2b +bitwarden_web_archive_url: https://github.com/dani-garcia/bw_web_builds/releases/download/v{{ bitwarden_web_version }}/bw_web_v{{ bitwarden_web_version }}.tar.gz +bitwarden_web_archive_sha1: df6f280731b852b31c3d938bfa1733140be9abb5 + +bitwarden_root_dir: /opt/bitwarden_rs +bitwarden_user: bitwarden_rs + +# Database : can be sqlite or mysql +bitwarden_db_engine: sqlite +bitwarden_db_server: "{{ mysql_server | default('localhost') }}" +bitwarden_db_port: 3306 +bitwarden_db_name: bitwardenrs +bitwarden_db_user: bitwardenrs +# A random one will be created if not defined +# bitwaren_db_pass: S3cr3t. + +# Port on which bitwarden will bind +bitwarden_http_port: 8000 +bitwarden_ws_port: 8001 +# List of IP addresses (can be CIDR notation) which will be able to +# access bitwarden ports +bitwarden_src_ip: [] +bitwarden_web_src_ip: [] + +# Public URL on which bitwarden will be accessible +bitwarden_public_url: http://{{ inventory_hostname }}:{{ bitwarden_http_port }} + +# Should registration be enabled +bitwarden_registration: False +# List of domain names for which registration will be accepted +# Thos domains will be accepted for registration even if bitwarden_registration is set to False +bitwarden_domains_whitelist: + - "{{ ansible_domain }}" + +# Admin Token to access /admin. A random one is created if not defined +# bitwarden_admin_token: S3cr3t. + +# Or you can just disable the admin token. But you have to protect /admin yourself (eg, on a reverse proxy) +bitwarden_disable_admin_token: False diff --git a/roles/bitwarden_rs/handlers/main.yml b/roles/bitwarden_rs/handlers/main.yml new file mode 100644 index 0000000..2794df6 --- /dev/null +++ b/roles/bitwarden_rs/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- name: restart bitwarden_rs + service: name=bitwarden_rs state=restarted + when: not bitwarden_started.changed diff --git a/roles/bitwarden_rs/meta/main.yml b/roles/bitwarden_rs/meta/main.yml new file mode 100644 index 0000000..d06f483 --- /dev/null +++ b/roles/bitwarden_rs/meta/main.yml @@ -0,0 +1,7 @@ +--- + +dependencies: + - role: rust + - role: nginx + - role: mysql_server + when: bitwarden_db_engine == 'mysql' and (bitwarden_db_server == 'localhost' or bitwarden_db_server == '127.0.0.1') diff --git a/roles/bitwarden_rs/tasks/archive_post.yml b/roles/bitwarden_rs/tasks/archive_post.yml new file mode 100644 index 0000000..0ed100d --- /dev/null +++ b/roles/bitwarden_rs/tasks/archive_post.yml @@ -0,0 +1,12 @@ +--- + +- name: Compress previous version + command: tar cJf {{ bitwarden_root_dir }}/archives/{{ bitwarden_current_version }}+{{ bitwarden_web_current_version }}.txz ./ + args: + warn: False + chdir: "{{ bitwarden_root_dir }}/archives/{{ bitwarden_current_version }}+{{ bitwarden_web_current_version }}" + tags: bitwarden + +- name: Remove archive dir + file: path={{ bitwarden_root_dir }}/archives/{{ bitwarden_current_version }}+{{ bitwarden_web_current_version }} state=absent + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/archive_pre.yml b/roles/bitwarden_rs/tasks/archive_pre.yml new file mode 100644 index 0000000..714b187 --- /dev/null +++ b/roles/bitwarden_rs/tasks/archive_pre.yml @@ -0,0 +1,23 @@ +--- + +- name: Create archive dir + file: path={{ bitwarden_root_dir }}/archives/{{ bitwarden_current_version }}+{{ bitwarden_web_current_version }} state=directory + tags: bitwarden + +- name: Stop bitwarden during upgrade + service: name=bitwarden_rs state=stopped + tags: bitwarden + +- name: Archive current version + synchronize: + src: "{{ bitwarden_root_dir }}/{{ item }}" + dest: "{{ bitwarden_root_dir }}/archives/{{ bitwarden_current_version }}+{{ bitwarden_web_current_version }}/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + loop: + - bitwarden_rs + - data + - etc + - web-vault + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/cleanup.yml b/roles/bitwarden_rs/tasks/cleanup.yml new file mode 100644 index 0000000..1d2ef93 --- /dev/null +++ b/roles/bitwarden_rs/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove temp files + files: path={{ item }} state=absent + loop: + - "{{ bitwarden_root_dir }}/tmp/bitwarden_rs-{{ bitwarden_version }}" + - "{{ bitwarden_root_dir }}/tmp/bitwarden_rs-{{ bitwarden_version }}.tar.gz" + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/conf.yml b/roles/bitwarden_rs/tasks/conf.yml new file mode 100644 index 0000000..b927011 --- /dev/null +++ b/roles/bitwarden_rs/tasks/conf.yml @@ -0,0 +1,11 @@ +--- + +- name: Deploy configuration + template: src=bitwarden_rs.conf.j2 dest={{ bitwarden_root_dir }}/etc/bitwarden_rs.conf group={{ bitwarden_user }} mode=640 + notify: restart bitwarden_rs + tags: bitwarden + +- name: Deploy nginx configuration + template: src=nginx.conf.j2 dest=/etc/nginx/ansible_conf.d/31-bitwarden.conf + notify: reload nginx + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/directories.yml b/roles/bitwarden_rs/tasks/directories.yml new file mode 100644 index 0000000..e25f096 --- /dev/null +++ b/roles/bitwarden_rs/tasks/directories.yml @@ -0,0 +1,24 @@ +--- + +- name: Create directories + file: path={{ bitwarden_root_dir }}/{{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + loop: + - dir: / + mode: 755 + - dir: etc + group: "{{ bitwarden_user }}" + mode: 750 + - dir: tmp + mode: 700 + - dir: meta + mode: 700 + - dir: archives + mode: 700 + - dir: data + owner: "{{ bitwarden_user }}" + group: "{{ bitwarden_user }}" + mode: 700 + - dir: web-vault + - dir: backup + mode: 700 + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/facts.yml b/roles/bitwarden_rs/tasks/facts.yml new file mode 100644 index 0000000..cbe95c5 --- /dev/null +++ b/roles/bitwarden_rs/tasks/facts.yml @@ -0,0 +1,67 @@ +--- + +- name: Set initial install modes + block: + - set_fact: bitwarden_install_mode='none' + - set_fact: bitwarden_current_version='' + - set_fact: bitwarden_web_install_mode='none' + - set_fact: bitwarden_web_current_version='' + tags: bitwarden + +- name: Check if server is installed + stat: path={{ bitwarden_root_dir }}/meta/ansible_version + register: bitwarden_version_file + tags: bitwarden + +- when: bitwarden_version_file.stat.exists + block: + - name: Check installed version + slurp: src={{ bitwarden_root_dir }}/meta/ansible_version + register: bitwarden_current_version + - set_fact: bitwarden_current_version={{ bitwarden_current_version.content | b64decode | trim }} + - set_fact: bitwarden_install_mode='upgrade' + when: bitwarden_current_version != bitwarden_version + tags: bitwarden + +- when: not bitwarden_version_file.stat.exists + block: + - set_fact: bitwarden_install_mode='install' + tags: bitwarden + +- name: Check if web vault is installed + stat: path={{ bitwarden_root_dir }}/meta/ansible_web_version + register: bitwarden_web_version_file + tags: bitwarden + +- when: bitwarden_web_version_file.stat.exists + block: + - name: Check installed version + slurp: src={{ bitwarden_root_dir }}/meta/ansible_web_version + register: bitwarden_web_current_version + - set_fact: bitwarden_web_current_version={{ bitwarden_web_current_version.content | b64decode | trim }} + - set_fact: bitwarden_web_install_mode='upgrade' + when: bitwarden_web_current_version != bitwarden_web_version + tags: bitwarden + +- when: not bitwarden_web_version_file.stat.exists + block: + - set_fact: bitwarden_web_install_mode='install' + tags: bitwarden + +- when: bitwarden_admin_token is not defined + name: Generate a random admin token + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ bitwarden_root_dir }}/meta/ansible_admin_token" + - set_fact: bitwarden_admin_token={{ rand_pass }} + tags: bitwarden + +- when: bitwarden_db_pass is not defined + tags: bitwarden + block: + - import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ bitwarden_root_dir }}/meta/ansible_dbpass" + - set_fact: bitwarden_db_pass={{ rand_pass }} + diff --git a/roles/bitwarden_rs/tasks/install.yml b/roles/bitwarden_rs/tasks/install.yml new file mode 100644 index 0000000..3802bb4 --- /dev/null +++ b/roles/bitwarden_rs/tasks/install.yml @@ -0,0 +1,97 @@ +--- + +- name: Install needed packages + yum: + name: + - openssl-devel + - gcc + - sqlite + tags: bitwarden + +- name: Check if MariaDB version is set + fail: msg="Need to define mysql_mariadb_version" + when: + - bitwarden_db_engine == 'mysql' + - mysql_mariadb_version is not defined or mysql_mariadb_version == 'default' + tags: bitwarden + +- name: Install MariaDB devel package + yum: + name: + - MariaDB-devel + - /usr/lib64/libmariadb.so + when: bitwarden_db_engine == 'mysql' + tags: bitwarden + +- when: bitwarden_install_mode != 'none' + tags: bitwarden + block: + - name: Download bitwarden + get_url: + url: "{{ bitwarden_archive_url }}" + dest: "{{ bitwarden_root_dir }}/tmp" + checksum: sha1:{{ bitwarden_archive_sha1 }} + + - name: Extract bitwarden archive + unarchive: + src: "{{ bitwarden_root_dir }}/tmp/bitwarden_rs-{{ bitwarden_version }}.tar.gz" + dest: "{{ bitwarden_root_dir }}/tmp" + remote_src: True + + - name: Build bitwarden + command: bash -lc 'cargo build --features={{ (bitwarden_db_engine == "mysql") | ternary("mysql","sqlite") }} --release' + args: + chdir: "{{ bitwarden_root_dir }}/tmp/bitwarden_rs-{{ bitwarden_version }}" + + - name: Install binary + copy: src={{ bitwarden_root_dir }}/tmp/bitwarden_rs-{{ bitwarden_version }}/target/release/bitwarden_rs dest="{{ bitwarden_root_dir }}/" mode=755 remote_src=True + notify: restart bitwarden_rs + +- when: bitwarden_web_install_mode != 'none' + tags: bitwarden + block: + - name: Download bitwarden web vault + get_url: + url: "{{ bitwarden_web_archive_url }}" + dest: "{{ bitwarden_root_dir }}/tmp" + checksum: sha1:{{ bitwarden_web_archive_sha1 }} + + - name: Extract the archive + unarchive: + src: "{{ bitwarden_root_dir }}/tmp/bw_web_v{{ bitwarden_web_version }}.tar.gz" + dest: "{{ bitwarden_root_dir }}/tmp" + remote_src: True + + - name: Move files to their final location + synchronize: + src: "{{ bitwarden_root_dir }}/tmp/web-vault/" + dest: "{{ bitwarden_root_dir }}/web-vault/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + +- name: Install systemd unit + template: src=bitwarden_rs.service.j2 dest=/etc/systemd/system/bitwarden_rs.service + register: bitwarden_unit + tags: bitwarden + +- name: Reload systemd + systemd: daemon_reload=True + when: bitwarden_unit.changed + tags: bitwarden + +- name: Install pre/post backup hooks + template: src={{ item }}-backup.sh.j2 dest=/etc/backup/{{ item }}.d/bitwarden_rs.sh mode=755 + loop: + - pre + - post + tags: bitwarden + +- import_tasks: ../includes/webapps_create_mysql_db.yml + vars: + - db_name: "{{ bitwarden_db_name }}" + - db_user: "{{ bitwarden_db_user }}" + - db_server: "{{ bitwarden_db_server }}" + - db_pass: "{{ bitwarden_db_pass }}" + when: bitwarden_db_engine == 'mysql' + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/iptables.yml b/roles/bitwarden_rs/tasks/iptables.yml new file mode 100644 index 0000000..d320360 --- /dev/null +++ b/roles/bitwarden_rs/tasks/iptables.yml @@ -0,0 +1,9 @@ +--- + +- name: Handle bitwarden_rs ports in the firewall + iptables_raw: + name: bitwarden_rs + state: "{{ (bitwarden_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -m multiport -p tcp --dports {{ bitwarden_http_port }},{{ bitwarden_ws_port }} -s {{ bitwarden_src_ip | join(',') }} -j ACCEPT" + when: iptables_manage | default(True) + tags: firewall,bitwarden diff --git a/roles/bitwarden_rs/tasks/main.yml b/roles/bitwarden_rs/tasks/main.yml new file mode 100644 index 0000000..77d9087 --- /dev/null +++ b/roles/bitwarden_rs/tasks/main.yml @@ -0,0 +1,14 @@ +--- + +- include: user.yml +- include: directories.yml +- include: facts.yml +- include: archive_pre.yml + when: bitwarden_install_mode == 'upgrade' or bitwarden_web_install_mode == 'upgrade' +- include: install.yml +- include: conf.yml +- include: iptables.yml +- include: service.yml +- include: write_version.yml +- include: archive_post.yml + when: bitwarden_install_mode == 'upgrade' or bitwarden_web_install_mode == 'upgrade' diff --git a/roles/bitwarden_rs/tasks/service.yml b/roles/bitwarden_rs/tasks/service.yml new file mode 100644 index 0000000..3426883 --- /dev/null +++ b/roles/bitwarden_rs/tasks/service.yml @@ -0,0 +1,6 @@ +--- + +- name: Start and enable the service + service: name=bitwarden_rs state=started enabled=True + register: bitwarden_started + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/user.yml b/roles/bitwarden_rs/tasks/user.yml new file mode 100644 index 0000000..f7cb253 --- /dev/null +++ b/roles/bitwarden_rs/tasks/user.yml @@ -0,0 +1,5 @@ +--- + +- name: Create bitwarden_rs user + user: name={{ bitwarden_user }} home={{ bitwarden_root_dir }} system=True + tags: bitwarden diff --git a/roles/bitwarden_rs/tasks/write_version.yml b/roles/bitwarden_rs/tasks/write_version.yml new file mode 100644 index 0000000..5b47c7e --- /dev/null +++ b/roles/bitwarden_rs/tasks/write_version.yml @@ -0,0 +1,10 @@ +--- + +- name: Write versions + copy: content={{ item.version }} dest={{ bitwarden_root_dir }}/meta/{{ item.file }} + loop: + - version: "{{ bitwarden_version }}" + file: ansible_version + - version: "{{ bitwarden_web_version }}" + file: ansible_web_version + tags: bitwarden diff --git a/roles/bitwarden_rs/templates/bitwarden_rs.conf.j2 b/roles/bitwarden_rs/templates/bitwarden_rs.conf.j2 new file mode 100644 index 0000000..698df67 --- /dev/null +++ b/roles/bitwarden_rs/templates/bitwarden_rs.conf.j2 @@ -0,0 +1,25 @@ +IP_HEADER=X-Forwarded-For +SIGNUPS_VERIFY=true +SIGNUPS_ALLOWED={{ bitwarden_registration | ternary('true','false') }} +{% if bitwarden_domains_whitelist | length > 0 %} +SIGNUPS_DOMAINS_WHITELIST={{ bitwarden_domains_whitelist | join(',') }} +{% endif %} +ADMIN_TOKEN={{ bitwarden_admin_token }} +DISABLE_ADMIN_TOKEN={{ bitwarden_disable_admin_token | ternary('true','false') }} +DOMAIN={{ bitwarden_public_url }} +ROCKET_ENV=prod +ROCKET_ADDRESS=0.0.0.0 +ROCKET_PORT={{ bitwarden_http_port }} +WEBSOCKET_ENABLED=true +WEBSOCKET_PORT={{ bitwarden_ws_port }} +SMTP_HOST=localhost +SMTP_PORT=25 +SMTP_SSL=false +SMTP_FROM=bitwarden-rs-noreply@{{ ansible_domain }} +{% if bitwarden_db_engine == 'mysql' %} +DATABASE_URL=mysql://{{ bitwarden_db_user }}:{{ bitwarden_db_pass | urlencode | regex_replace('/','%2F') }}@{{ bitwarden_db_server }}:{{ bitwarden_db_port }}/{{ bitwarden_db_name }} +ENABLE_DB_WAL=false +{% else %} +DATABASE_URL=data/db.sqlite3 +{% endif %} +# vim: syntax=ini diff --git a/roles/bitwarden_rs/templates/bitwarden_rs.service.j2 b/roles/bitwarden_rs/templates/bitwarden_rs.service.j2 new file mode 100644 index 0000000..0393c67 --- /dev/null +++ b/roles/bitwarden_rs/templates/bitwarden_rs.service.j2 @@ -0,0 +1,27 @@ +[Unit] +Description=Bitwarden Server (Rust Edition) +Documentation=https://github.com/dani-garcia/bitwarden_rs +After=network.target +{% if bitwarden_db_engine == 'mysql' and (bitwarden_db_server == 'localhost' or bitwarden_db_server == '127.0.0.1') %} +After=mariadb.service +Requires=mariadb.service +{% endif %} + +[Service] +User={{ bitwarden_user }} +Group={{ bitwarden_user }} +EnvironmentFile={{ bitwarden_root_dir }}/etc/bitwarden_rs.conf +ExecStart={{ bitwarden_root_dir }}/bitwarden_rs +PrivateTmp=true +PrivateDevices=true +ProtectHome=true +ProtectSystem=full +WorkingDirectory={{ bitwarden_root_dir }} +ReadWriteDirectories={{ bitwarden_root_dir }}/data +ReadOnlyDirectories={{ bitwarden_root_dir }}/etc {{ bitwarden_root_dir }}/web-vault +Restart=on-failure +StartLimitInterval=0 +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/roles/bitwarden_rs/templates/nginx.conf.j2 b/roles/bitwarden_rs/templates/nginx.conf.j2 new file mode 100644 index 0000000..81c4d34 --- /dev/null +++ b/roles/bitwarden_rs/templates/nginx.conf.j2 @@ -0,0 +1,71 @@ +server { + listen 443 ssl http2; + server_name {{ bitwarden_public_url | urlsplit('hostname') }}; + + include /etc/nginx/ansible_conf.d/acme.inc; + +{% if bitwarden_cert_path is defined and bitwarden_key_path is defined %} + ssl_certificate {{ bitwarden_cert_path }}; + ssl_certificate_key {{ bitwarden_key_path }}; +{% elif bitwarden_letsencrypt_cert is defined and bitwarden_letsencrypt_cert == True %} + ssl_certificate /var/lib/dehydrated/certificates/certs/{{ bitwarden_public_url | urlsplit('hostname') }}/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certificates/certs/{{ bitwarden_public_url | urlsplit('hostname') }}/privkey.pem; +{% elif bitwarden_letsencrypt_cert is string %} + ssl_certificate /var/lib/dehydrated/certificates/certs/{{ bitwarden_letsencrypt_cert }}/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certificates/certs/{{ bitwarden_letsencrypt_cert }}/privkey.pem; +{% endif %} + + server_name {{ bitwarden_public_url | urlsplit('hostname') }}; + + root {{ bitwarden_root_dir }}/web-vault; + + client_max_body_size 512M; + + if ($request_method !~ ^(GET|POST|HEAD|PUT|DELETE)$ ) { + return 405; + } + + location /notifications/hub { + proxy_pass http://localhost:{{ bitwarden_ws_port }}; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + location /notifications/hub/negotiate { + proxy_pass http://localhost:{{ bitwarden_http_port }}; + } + + location @proxy { + proxy_pass http://localhost:{{ bitwarden_http_port }}; + } + + location / { + try_files $uri $uri/index.html @proxy; + } + + add_header X-Frame-Options "DENY"; + add_header X-Content-Type-Options "nosniff"; + add_header X-XSS-Protection "1; mode=block"; + add_header Strict-Transport-Security "$hsts_header"; + + # 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"; + + # Set the timeout to read responses from the backend + proxy_read_timeout 60s; + + # Enable Keep Alive to the backend + proxy_socket_keepalive on; + + # Disable buffering large files + proxy_max_temp_file_size 5m; + + allow 127.0.0.1; +{% for ip in bitwarden_web_src_ip %} + allow {{ ip }}; +{% endfor %} + deny all; +} diff --git a/roles/bitwarden_rs/templates/post-backup.sh.j2 b/roles/bitwarden_rs/templates/post-backup.sh.j2 new file mode 100644 index 0000000..3166c61 --- /dev/null +++ b/roles/bitwarden_rs/templates/post-backup.sh.j2 @@ -0,0 +1,4 @@ +#!/bin/bash -e + +rm -f {{ bitwarden_root_dir }}/backup/* +umount /home/lbkp/bitwarden diff --git a/roles/bitwarden_rs/templates/pre-backup.sh.j2 b/roles/bitwarden_rs/templates/pre-backup.sh.j2 new file mode 100644 index 0000000..db61879 --- /dev/null +++ b/roles/bitwarden_rs/templates/pre-backup.sh.j2 @@ -0,0 +1,17 @@ +#!/bin/bash -e + +mkdir -p /home/lbkp/bitwarden_rs/ +cp {{ bitwarden_root_dir }}/data/rsa* {{ bitwarden_root_dir }}/backup/ +{% if bitwarden_db_engine == 'mysql' %} +/usr/bin/mysqldump \ +{% if bitwarden_db_server != 'localhost' and bitwarden_db_server != '127.0.0.1' %} + --user='{{ bitwarden_db_user }}' \ + --password='{{ bitwarden_db_pass }}' \ + --host='{{ bitwarden_db_server }}' \ +{% endif %} + --quick --single-transaction \ + --add-drop-table {{ bitwarden_db_name }} | zstd -T0 -c > {{ bitwarden_root_dir }}/backup/{{ bitwarden_db_name }}.sql.zstd +{% else %} +sqlite3 {{ bitwarden_root_dir }}/data/db.sqlite3 ".backup '{{ bitwarden_root_dir }}/backup/db.sqlite3'" +{% endif %} +mount -o bind,ro {{ bitwarden_root_dir }}/backup/ /home/lbkp/bitwarden_rs/ diff --git a/roles/bluemind/defaults/main.yml b/roles/bluemind/defaults/main.yml new file mode 100644 index 0000000..c109fd4 --- /dev/null +++ b/roles/bluemind/defaults/main.yml @@ -0,0 +1,117 @@ +--- + +bm_http_ports: + - 80 + - 443 +bm_http_src_ip: + - 0.0.0.0/0 + +bm_imap_ports: + - 143 + - 993 +bm_imap_src_ip: + - 0.0.0.0/0 + +bm_pop_ports: + - 110 + - 995 +bm_pop_src_ip: + - 0.0.0.0/0 + +bm_smtp_ports: + - 25 + - 465 + - 587 +bm_smtp_src_ip: + - 0.0.0.0/0 + +bm_milter_ports: + - 2500 +bm_milter_src:ip: [] + +bm_int_ports: + - 24 + - 144 + - 1110 + - 1143 + - 2000 + - 2400 + - 2500 + - 4444 + - 5280 + - 5290 + - 5432 + - '5701:5715' + - 8021 + - 8022 + - 8079 + - 8080 + - 8082 + - 8084 + - 8087 + - 9083 + - 9086 + - 9090 + - 9099 + - 9200 + - 9300 +bm_int_src_ip: [] + +# bm_letsencrypt_cert: bluemind.domain.tld + +bm_mem_alloc_base: + bm-core: + heap: 512 + direct: 512 + spare: 20 + bm-node: + heap: 128 + direct: 128 + spare: 0 + bm-eas: + heap: 256 + direct: 128 + spare: 2 + bm-mapi: + heap: 512 + direct: 256 + spare: 10 + bm-ips: + heap: 64 + direct: 64 + spare: 0 + bm-hps: + heap: 128 + direct: 128 + spare: 0 + bm-lmtpd: + heap: 128 + direct: 128 + spare: 0 + bm-locator: + heap: 64 + direct: 64 + spare: 0 + bm-milter: + heap: 64 + direct: 64 + spare: 0 + bm-tika: + heap: 128 + direct: 128 + spare: 0 + bm-xmpp: + heap: 32 + direct: 32 + spare: 0 + bm-ysnp: + heap: 64 + direct: 64 + spare: 0 + bm-elasticsearch: + heap: 512 + direct: 512 + spare: 20 +bm_mem_alloc: {} +bm_mem_alloc_rules: "{{ bm_mem_alloc_base | combine(bm_mem_alloc, recursive=True) }}" + diff --git a/roles/bluemind/handlers/main.yml b/roles/bluemind/handlers/main.yml new file mode 100644 index 0000000..9d32b33 --- /dev/null +++ b/roles/bluemind/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart bluemind + command: bmctl restart diff --git a/roles/bluemind/tasks/main.yml b/roles/bluemind/tasks/main.yml new file mode 100644 index 0000000..cfb390f --- /dev/null +++ b/roles/bluemind/tasks/main.yml @@ -0,0 +1,118 @@ +--- + +- name: Install tools + yum: + name: + - socat + tags: bm + +- name: Create dehydrated hook dir + file: path=/etc/dehydrated/hooks_deploy_cert.d state=directory + tags: bm + +- name: Deploy dehydrated hook + template: src=dehydrated_deploy_hook.j2 dest=/etc/dehydrated/hooks_deploy_cert.d/bluemind mode=755 + tags: bm + +- name: Create local conf directory + file: path=/etc/bm/local state=directory + tags: bm + +- name: Configure proxy + lineinfile: + regex: '^PROXY_OPTS=.*' + line: "PROXY_OPTS=\"{{ (system_proxy is defined and system_proxy != '') | ternary('-Dhttps.proxyHost=' ~ system_proxy | urlsplit('hostname') ~ ' -Dhttps.proxyPort=' ~ system_proxy | urlsplit('port') ~ ' -Dhttp.proxyHost=' ~ system_proxy | urlsplit('hostname') ~ ' -Dhttp.proxyPort=' ~ system_proxy | urlsplit('port'),'') }}\"" + path: /etc/bm/local/{{ item }}.ini + create: True + loop: + - bm-core + - bm-webserver + notify: restart bluemind + tags: bm + +- name: Configure JVM options + lineinfile: + regex: '^JVM_OPTS=.*' + line: "JVM_OPTS=\"${PROXY_OPTS}\"" + path: /etc/bm/local/{{ item }}.ini + insertafter: '^PROXY_OPTS=.*' + loop: + - bm-core + - bm-webserver + notify: restart bluemind + tags: bm + +- name: Configure memory allocation rules + template: src=rules.json.j2 dest=/etc/bm/local/rules.json + notify: restart bluemind + tags: bm + +- set_fact: + bm_restart_services: "[ 'bm-elasticsearch', 'bm-mapi' ]" + tags: bm + +- name: Create systemd unit snippet dirs + file: path=/etc/systemd/system/{{ item }}.service.d state=directory + loop: "{{ bm_restart_services }}" + tags: bm + +- name: Configure systemd to restart services on failure + copy: + content: | + [Service] + TimeoutSec=60 + StartLimitInterval=0 + RestartSec=1 + Restart=on-failure + dest: /etc/systemd/system/{{ item }}.service.d/restart.conf + loop: "{{ bm_restart_services }}" + register: bm_units + notify: restart bluemind + tags: bm + +- name: Reload systemd + systemd: daemon_reload=True + when: bm_units.results | selectattr('changed','equalto',True) | list | length > 0 + tags: bm + +- name: Handle firewall ports + iptables_raw: + name: "{{ item.name }}" + state: "{{ (item.src | length > 0) | ternary('present','absent') }}" + rules: "{% if 'tcp' in item.proto | default(['tcp']) or item.proto | default('tcp') == 'tcp' %}-A INPUT -m state --state NEW -p tcp -m multiport --dports {{ item.ports | join(',') }} -s {{ item.src | join(',') }} -j ACCEPT\n{% endif %} + {% if 'udp' in item.proto | default(['tcp']) or item.proto | default('tcp') == 'udp' %}-A INPUT -m state --state NEW -p udp -m multiport --dports {{ item.ports | join(',') }} -s {{ item.src | join(',') }} -j ACCEPT{% endif %}" + when: iptables_manage | default(True) + with_items: + - ports: "{{ bm_http_ports }}" + name: bm_http_ports + src: "{{ bm_http_src_ip }}" + - ports: "{{ bm_imap_ports }}" + name: bm_imap_ports + src: "{{ bm_imap_src_ip }}" + - ports: "{{ bm_pop_ports }}" + name: bm_pop_ports + src: "{{ bm_pop_src_ip }}" + - ports: "{{ bm_smtp_ports }}" + name: bm_smtp_ports + src: "{{ bm_smtp_src_ip }}" + - ports: "{{ bm_milter_ports }}" + name: bm_milter_ports + src: "{{ bm_milter_src_ip }}" + - ports: "{{ bm_int_ports }}" + name: bm_int_ports + src: "{{ bm_int_src_ip }}" + tags: bm,firewall + +- name: Create pre/post backup hook dir + file: path=/etc/backup/{{ item }}.d state=directory mode=750 + loop: + - pre + - post + tags: bm + +- name: Deploy pre and post backup script + template: src={{ item }}-backup.j2 dest=/etc/backup/{{ item }}.d/bluemind mode=755 + loop: + - pre + - post + tags: bm diff --git a/roles/bluemind/templates/bm-core.log.xml.j2 b/roles/bluemind/templates/bm-core.log.xml.j2 new file mode 100644 index 0000000..36715c9 --- /dev/null +++ b/roles/bluemind/templates/bm-core.log.xml.j2 @@ -0,0 +1,53 @@ + + + + localhost + 10514 + DAEMON + bm-core - [%thread] %c{1} %p - %m\n + + + + + + + + localhost + 10514 + DAEMON + bm-xmpp - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-mailindex - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-slowrestcall - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-js - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-eas.log.xml.j2 b/roles/bluemind/templates/bm-eas.log.xml.j2 new file mode 100644 index 0000000..99255e7 --- /dev/null +++ b/roles/bluemind/templates/bm-eas.log.xml.j2 @@ -0,0 +1,59 @@ + + + + localhost + 10514 + DAEMON + bm-eas - [%thread] %c{1} %p - %m\n + + + + localhost + 10514 + DAEMON + bm-eas-requests - [%thread] %c{1} %p - %m\n + + + + + + user + anonymous + + + + /var/log/bm-eas/user-eas-${user}.log + + 10 + /var/log/bm-eas/user-eas-${user}.log.%i.gz + + + 5000KB + + + %d [%thread] %c{1} %p - %m\n + + + + + + + 500 + 0 + + + + + + + + + + + + + + + diff --git a/roles/bluemind/templates/bm-hps.log.xml.j2 b/roles/bluemind/templates/bm-hps.log.xml.j2 new file mode 100644 index 0000000..730fb96 --- /dev/null +++ b/roles/bluemind/templates/bm-hps.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-hps - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-ips.log.xml.j2 b/roles/bluemind/templates/bm-ips.log.xml.j2 new file mode 100644 index 0000000..e56a109 --- /dev/null +++ b/roles/bluemind/templates/bm-ips.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-ips - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-lmtp.log.xml.j2 b/roles/bluemind/templates/bm-lmtp.log.xml.j2 new file mode 100644 index 0000000..55a5f12 --- /dev/null +++ b/roles/bluemind/templates/bm-lmtp.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-lmtp - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-locator.log.xml.j2 b/roles/bluemind/templates/bm-locator.log.xml.j2 new file mode 100644 index 0000000..7b60df6 --- /dev/null +++ b/roles/bluemind/templates/bm-locator.log.xml.j2 @@ -0,0 +1,13 @@ + + + + localhost + 10514 + DAEMON + bm-locator - [%thread] %c{1} %p - %m\n + + + + + + diff --git a/roles/bluemind/templates/bm-milter.log.xml.j2 b/roles/bluemind/templates/bm-milter.log.xml.j2 new file mode 100644 index 0000000..7796acd --- /dev/null +++ b/roles/bluemind/templates/bm-milter.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-milter - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-node.log.xml.j2 b/roles/bluemind/templates/bm-node.log.xml.j2 new file mode 100644 index 0000000..f592221 --- /dev/null +++ b/roles/bluemind/templates/bm-node.log.xml.j2 @@ -0,0 +1,13 @@ + + + + localhost + 10514 + DAEMON + bm-node - [%thread] %c{1} %p - %m\n + + + + + + diff --git a/roles/bluemind/templates/bm-syslog.service.j2 b/roles/bluemind/templates/bm-syslog.service.j2 new file mode 100644 index 0000000..cdebfa9 --- /dev/null +++ b/roles/bluemind/templates/bm-syslog.service.j2 @@ -0,0 +1,19 @@ +[Unit] +Description=Bluemind syslog daemon +After=syslog.target + +[Service] +Type=simple +ExecStart=/bin/socat -t0 -T0 -u -s udp4-recv:10514 stdout +User=bm-syslog +Group=bm-syslog +Restart=always +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +ProtectHome=yes +NoNewPrivileges=yes + +[Install] +WantedBy=multi-user.target + diff --git a/roles/bluemind/templates/bm-tika.log.xml.j2 b/roles/bluemind/templates/bm-tika.log.xml.j2 new file mode 100644 index 0000000..2d15cf6 --- /dev/null +++ b/roles/bluemind/templates/bm-tika.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-locator - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-webserver.log.xml.j2 b/roles/bluemind/templates/bm-webserver.log.xml.j2 new file mode 100644 index 0000000..1ed4e21 --- /dev/null +++ b/roles/bluemind/templates/bm-webserver.log.xml.j2 @@ -0,0 +1,43 @@ + + + + localhost + 10514 + DAEMON + bm-webserver - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-dav - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-setup - [%thread] %c{1} %p - %m\n + + + + + + + localhost + 10514 + DAEMON + bm-js-errors - [%thread] %c{1} %p - %m\n + + + + + + diff --git a/roles/bluemind/templates/bm-xmpp.log.xml.j2 b/roles/bluemind/templates/bm-xmpp.log.xml.j2 new file mode 100644 index 0000000..507acb3 --- /dev/null +++ b/roles/bluemind/templates/bm-xmpp.log.xml.j2 @@ -0,0 +1,12 @@ + + + + localhost + 10514 + DAEMON + bm-xmpp - [%thread] %c{1} %p - %m\n + + + + + diff --git a/roles/bluemind/templates/bm-ysnp.log.xml.j2 b/roles/bluemind/templates/bm-ysnp.log.xml.j2 new file mode 100644 index 0000000..2af5e95 --- /dev/null +++ b/roles/bluemind/templates/bm-ysnp.log.xml.j2 @@ -0,0 +1,14 @@ + + + + localhost + 10514 + DAEMON + bm-ysnp - [%thread] %c{1} %p - %m\n + + + + + + + diff --git a/roles/bluemind/templates/dehydrated_deploy_hook.j2 b/roles/bluemind/templates/dehydrated_deploy_hook.j2 new file mode 100644 index 0000000..1d9fcd1 --- /dev/null +++ b/roles/bluemind/templates/dehydrated_deploy_hook.j2 @@ -0,0 +1,12 @@ +#!/bin/bash -e + +{% if bm_letsencrypt_cert is defined %} +if [ $1 == "{{ bm_letsencrypt_cert }}" ]; then + cat /var/lib/dehydrated/certificates/certs/{{ bm_letsencrypt_cert }}/privkey.pem > /etc/ssl/certs/bm_cert.pem + cat /var/lib/dehydrated/certificates/certs/{{ bm_letsencrypt_cert }}/fullchain.pem >> /etc/ssl/certs/bm_cert.pem + chown root:root /etc/ssl/certs/bm_cert.pem + chmod 644 /etc/ssl/certs/bm_cert.pem + /bin/systemctl reload postfix + /bin/systemctl reload bm-nginx +fi +{% endif %} diff --git a/roles/bluemind/templates/post-backup.j2 b/roles/bluemind/templates/post-backup.j2 new file mode 100644 index 0000000..68e3ba7 --- /dev/null +++ b/roles/bluemind/templates/post-backup.j2 @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +rm -rf /home/lbkp/bm/* diff --git a/roles/bluemind/templates/pre-backup.j2 b/roles/bluemind/templates/pre-backup.j2 new file mode 100644 index 0000000..78007f9 --- /dev/null +++ b/roles/bluemind/templates/pre-backup.j2 @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +DEST=/home/lbkp/bm/pgsql +mkdir -p $DEST +chown postgres:postgres $DEST +chmod 700 $DEST + +for DB in $(su - postgres -c "/bin/psql -d postgres -qtc 'SELECT datname from pg_database' | grep -vP '^\s+?template[01]$'") +do + su - postgres -c "/bin/pg_dump -Fp -Cc $DB" | /bin/nice -n 10 lz4 -c > $DEST/$DB.sql.lz4 +done +su - postgres -c "/bin/pg_dumpall --globals-only" | /bin/nice -n 10 lz4 -c > $DEST/pg_globals.sql.lz4 +su - postgres -c "/bin/pg_dumpall --schema-only" | /bin/nice -n 10 lz4 -c > $DEST/pg_schema.sql.lz4 + +cp -a /etc/bm/local /home/lbkp/bm/conf diff --git a/roles/bluemind/templates/rules.json.j2 b/roles/bluemind/templates/rules.json.j2 new file mode 100644 index 0000000..d4b7279 --- /dev/null +++ b/roles/bluemind/templates/rules.json.j2 @@ -0,0 +1,11 @@ +[ +{% for product in bm_mem_alloc_rules.keys() | list %} + { + "product":"{{ product }}", + "defaultHeap":"{{ bm_mem_alloc_rules[product].heap }}", + "defaultDirect":"{{ bm_mem_alloc_rules[product].direct }}", + "sparePercent":{{ bm_mem_alloc_rules[product].spare }} + }{% if not loop.last %},{% endif %} + +{% endfor %} +] diff --git a/roles/bounca/defaults/main.yml b/roles/bounca/defaults/main.yml new file mode 100644 index 0000000..1013b79 --- /dev/null +++ b/roles/bounca/defaults/main.yml @@ -0,0 +1,19 @@ +--- + +bounca_version: 0.1.1 +#bounca_version: master +#bounca_git_url: https://github.com/repleo/bounca.git +bounca_archive_url: https://github.com/repleo/bounca/archive/v{{ bounca_version }}.tar.gz +bounca_root_dir: /opt/bounca +bounca_port: 8084 +bounca_src_ip: [] +bounca_user: bounca +bounca_db_server: "{{ pg_server | default('localhost') }}" +bounca_db_name: bounca +bounca_db_user: bounca +# Will be generated if not defined +# bounca_db_pass: +# bounca_secret_key: + +bounca_admin_mail: "{{ system_admin_email }}" +bounca_from_mail: bounca@{{ ansible_domain }} diff --git a/roles/bounca/handlers/main.yml b/roles/bounca/handlers/main.yml new file mode 100644 index 0000000..dd16589 --- /dev/null +++ b/roles/bounca/handlers/main.yml @@ -0,0 +1,5 @@ +--- + +- include: ../common/handlers/main.yml +- name: restart bounca + service: name=bounca state=restarted diff --git a/roles/bounca/meta/main.yml b/roles/bounca/meta/main.yml new file mode 100644 index 0000000..cd21505 --- /dev/null +++ b/roles/bounca/meta/main.yml @@ -0,0 +1,2 @@ +--- + diff --git a/roles/bounca/tasks/main.yml b/roles/bounca/tasks/main.yml new file mode 100644 index 0000000..571352f --- /dev/null +++ b/roles/bounca/tasks/main.yml @@ -0,0 +1,323 @@ +--- + +- name: Set default install mode to none + set_fact: bounca_install_mode="none" + tags: bounca + +- name: Check if bounca is installed + stat: path={{ bounca_root_dir }}/meta/ansible_version + register: bounca_version_file + tags: bounca + +- name: Check installed version + command: cat {{ bounca_root_dir }}/meta/ansible_version + register: bounca_current_version + changed_when: False + when: bounca_version_file.stat.exists + tags: bounca + +- name: Set install mode to install + set_fact: bounca_install_mode='install' + when: not bounca_version_file.stat.exists + tags: bounca + +- name: Set install mode to upgrade + set_fact: bounca_install_mode='upgrade' + when: + - bounca_version_file.stat.exists + - bounca_current_version is defined + - bounca_current_version.stdout != bounca_version + # - bounca_manage_upgrade + tags: bounca + +- name: Install dependencies + yum: + name: + - python34-virtualenv + - python34-pip + - uwsgi-plugin-python3 + - uwsgi-logger-systemd + - python-psycopg2 + - openssl-devel + - postgresql-devel + - postgresql + - gcc + - git + tags: bounca + +- name: Create user account for bounca + user: + name: bounca + system: True + shell: /sbin/nologin + home: "{{ bounca_root_dir }}" + tags: bounca + +- name: Create directories + file: path={{ item.dir }} state=directory owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + with_items: + - dir: "{{ bounca_root_dir }}/tmp" + - dir: "{{ bounca_root_dir }}/app" + - dir: "{{ bounca_root_dir }}/data" + mode: 700 + group: "{{ bounca_user }}" + owner: "{{ bounca_user }}" + - dir: "{{ bounca_root_dir }}/meta" + mode: 700 + - dir: "{{ bounca_root_dir }}/archives" + mode: 700 + - dir: /etc/bounca + mode: 750 + group: "{{ bounca_user }}" + tags: bounca + +- name: Create archive dir + file: path={{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }} state=directory mode=700 + when: bounca_install_mode == "upgrade" + tags: bounca + +- name: Archive current BounCA install + synchronize: + src: "{{ bounca_root_dir }}/app" + dest: "{{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }}/app" + recursive: True + delegate_to: "{{ inventory_hostname }}" + when: bounca_install_mode == "upgrade" + tags: bounca + +- name: Dump database + postgresql_db: + name: "{{ bounca_db_name }}" + state: dump + login_host: "{{ bounca_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + target: "{{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }}/{{ bounca_db_name }}.sql.gz" + when: bounca_install_mode == "upgrade" + tags: bounca + +- name: Compress previous version + command: tar cJf {{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }}.txz ./ + environment: + XZ_OPT: -T0 + args: + chdir: "{{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }}" + when: bounca_install_mode == 'upgrade' + tags: bounca + +- name: Remove the archive directory + file: path={{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }} state=absent + when: bounca_install_mode == 'upgrade' + tags: bounca + +- name: Download BounCA + get_url: + url: "{{ bounca_archive_url }}" + dest: "{{ bounca_root_dir }}/tmp" + when: bounca_install_mode != 'none' + tags: bounca + +- name: Extract BounCA + unarchive: + src: "{{ bounca_root_dir }}/tmp/bounca-{{ bounca_version }}.tar.gz" + dest: "{{ bounca_root_dir }}/tmp" + remote_src: yes + when: bounca_install_mode != "none" + tags: bounca + +- name: Move BounCA to it's directory + synchronize: + src: "{{ bounca_root_dir }}/tmp/bounca-{{ bounca_version }}/" + dest: "{{ bounca_root_dir }}/app/" + recursive: True + delete: True + when: bounca_install_mode != "none" + delegate_to: "{{ inventory_hostname }}" + tags: bounca + + #- name: Clone GIT repo + # git: + # repo: "{{ bounca_git_url }}" + # dest: "{{ bounca_root_dir }}/app" + # version: "{{ bounca_version }}" + # force: True + # register: bounca_git + # tags: bounca + # + #- name: Get new git commit + # command: git rev-parse HEAD + # args: + # chdir: "{{ bounca_root_dir }}/app" + # register: bounca_git_commit + # changed_when: False + # tags: bounca + # + #- name: Set install mode to upgrade + # set_fact: bounca_install_mode='upgrade' + # when: + # - bounca_install_mode == 'none' + # - bounca_git_commit.stdout != bounca_current_version.stdout + # tags: bounca + +- name: Create archive dir + file: path={{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }} state=directory mode=700 + when: bounca_install_mode == "upgrade" + tags: bounca + +- name: Dump database + postgresql_db: + name: "{{ bounca_db_name }}" + state: dump + login_host: "{{ bounca_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + target: "{{ bounca_root_dir }}/archives/{{ bounca_current_version.stdout }}/{{ bounca_db_name }}.sql.gz" + when: bounca_install_mode == "upgrade" + tags: bounca + +- name: Create the virtualenv + pip: + state: latest + virtualenv: "{{ bounca_root_dir }}" + virtualenv_command: /usr/bin/virtualenv-3 + requirements: "{{ bounca_root_dir }}/app/requirements.txt" + tags: bounca + +- name: Link pki to the data dir + file: src={{ bounca_root_dir }}/data dest={{ bounca_root_dir }}/app/pki state=link + tags: bounca + +- name: Handle bounca ports + iptables_raw: + name: bounca_ports + state: "{{ (bounca_src_ip | length > 0) | ternary('present','absent') }}" + rules: "-A INPUT -m state --state NEW -p tcp -m multiport --dports {{ bounca_port }} -s {{ bounca_src_ip | join(',') }} -j ACCEPT" + tags: [firewall,bounca] + + #- name: Install additional python module + # pip: + # state: latest + # virtualenv: "{{ bounca_root_dir }}" + # name: "{{ item }}" + # with_items: + # - django-lemonldap + # tags: bounca + +- name: Generate a random pass for the database + shell: openssl rand -base64 45 > {{ bounca_root_dir }}/meta/ansible_dbpass + args: + creates: "{{ bounca_root_dir }}/meta/ansible_dbpass" + when: bounca_db_pass is not defined + tags: bounca + +- name: Read database password + command: cat {{ bounca_root_dir }}/meta/ansible_dbpass + register: bounca_rand_pass + when: bounca_db_pass is not defined + changed_when: False + tags: bounca + +- name: Set database pass + set_fact: bounca_db_pass={{ bounca_rand_pass.stdout }} + when: bounca_db_pass is not defined + tags: bounca + +- name: Generate a random secret + shell: openssl rand -base64 45 > {{ bounca_root_dir }}/meta/ansible_secret + args: + creates: "{{ bounca_root_dir }}/meta/ansible_secret" + when: bounca_secret_key is not defined + tags: bounca + +- name: Read secret_key + command: cat {{ bounca_root_dir }}/meta/ansible_secret + register: bounca_rand_secret + when: bounca_secret_key is not defined + changed_when: False + tags: bounca + +- name: Set secret_key + set_fact: bounca_secret_key={{ bounca_rand_secret.stdout }} + when: bounca_secret_key is not defined + tags: bounca + +- name: Create the PostgreSQL role + postgresql_user: + db: postgres + name: "{{ bounca_db_user }}" + password: "{{ bounca_db_pass }}" + login_host: "{{ bounca_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + tags: bounca + +- name: Create the PostgreSQL database + postgresql_db: + name: "{{ bounca_db_name }}" + encoding: UTF-8 + lc_collate: C + lc_ctype: C + template: template0 + owner: "{{ bounca_db_user }}" + login_host: "{{ bounca_db_server }}" + login_user: sqladmin + login_password: "{{ pg_admin_pass }}" + tags: bounca + +- name: Deploy configuration + template: src={{ item.src }} dest={{ item.dest }} owner={{ item.owner | default(omit) }} group={{ item.group | default(omit) }} mode={{ item.mode | default(omit) }} + with_items: + - src: main.ini.j2 + dest: /etc/bounca/main.ini + group: bounca + mode: 640 + - src: uwsgi.ini.j2 + dest: /etc/bounca/uwsgi.ini + group: bounca + mode: 640 + notify: restart bounca + tags: bounca + + #- name: Add a tmpfiles.d snippet + # copy: content="d /run/bounca 750 bounca apache" dest=/etc/tmpfiles.d/bounca.conf + # register: bounca_tmpfiles + # tags: bounca + # + #- name: Create tmpdir + # command: systemd-tmpfiles --create + # when: bounca_tmpfiles.changed + # tags: bounca + +- name: Deploy BounCA unit + template: src=bounca.service.j2 dest=/etc/systemd/system/bounca.service + register: bounca_unit + tags: bounca + +- name: Reload systemd + command: systemctl daemon-reload + when: bounca_unit.changed + tags: bounca + +- name: Stop BounCA daemon for DB upgrade + service: name=bounca state=stopped + when: bounca_install_mode == 'upgrade' + tags: bounca + +- name: Migrate BounCA DB + django_manage: command="migrate --noinput" app_path={{ bounca_root_dir }}/app virtualenv={{ bounca_root_dir }} + when: bounca_install_mode != 'none' + tags: bounca + +- name: Collect static assets + django_manage: command="collectstatic --noinput" app_path={{ bounca_root_dir }}/app virtualenv={{ bounca_root_dir }} + when: bounca_install_mode != 'none' + tags: bounca + +- name: Start and enable the daemon + service: name=bounca state=started enabled=True + tags: bounca + +- name: Write installed version + # copy: content={{ bounca_git_commit.stdout}} dest={{ bounca_root_dir }}/meta/ansible_version + copy: content={{ bounca_version }} dest={{ bounca_root_dir }}/meta/ansible_version + tags: bounca diff --git a/roles/bounca/templates/bounca.service.j2 b/roles/bounca/templates/bounca.service.j2 new file mode 100644 index 0000000..b768ccd --- /dev/null +++ b/roles/bounca/templates/bounca.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=BounCA PKI Daemon +After=syslog.target + +[Service] +Environment=PYTHONPATH=/usr/bin/python34 +ExecStart=/usr/sbin/uwsgi --ini /etc/bounca/uwsgi.ini +ExecReload=/bin/kill -HUP $MAINPID +User={{ bounca_user }} +Group={{ bounca_user }} +KillSignal=SIGINT +Restart=always +Type=notify +NotifyAccess=all + +[Install] +WantedBy=multi-user.target diff --git a/roles/bounca/templates/main.ini.j2 b/roles/bounca/templates/main.ini.j2 new file mode 100644 index 0000000..3a53537 --- /dev/null +++ b/roles/bounca/templates/main.ini.j2 @@ -0,0 +1,14 @@ +[database] +DATABASE_USER: {{ bounca_db_user }} +DATABASE_PASSWORD: {{ bounca_db_pass }} +DATABASE_HOST: {{ bounca_db_server }} +DATABASE_NAME: {{ bounca_db_name }} + +[secrets] +SECRET_KEY: {{ bounca_secret_key }} + +[email] +EMAIL_HOST: localhost +ADMIN_MAIL: {{ bounca_admin_mail }} +FROM_MAIL: {{ bounca_from_mail }} + diff --git a/roles/bounca/templates/uwsgi.ini.j2 b/roles/bounca/templates/uwsgi.ini.j2 new file mode 100644 index 0000000..edbc8fc --- /dev/null +++ b/roles/bounca/templates/uwsgi.ini.j2 @@ -0,0 +1,17 @@ +[uwsgi] +plugin = python3 +thread = 4 +master = 1 +processes = 30 +vacuum = true +http11-socket = 0.0.0.0:{{ bounca_port }} +chdir = {{ bounca_root_dir }}/app +home = {{ bounca_root_dir }} +module = bounca.wsgi +check-static = {{ bounca_root_dir }}/app/media +static-skip-ext = .php +static-skip-ext = .cgi +static-skip-ext = .py +offload-threads = 4 +cache2 = name=bounca,items=200 +static-cache-paths = 300 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..91eecb6 --- /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 /var/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..4ea7791 --- /dev/null +++ b/roles/clamav/templates/freshclam.conf.j2 @@ -0,0 +1,13 @@ +DatabaseDirectory /var/lib/clamav +LogVerbose yes +LogSyslog yes +PidFile /var/run/freshclam.pid +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..04a9bcd --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,120 @@ +--- + +# 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 + - zstd + - vim + - bash-completion + - rsync + - lsof + - net-tools + - sysstat + - pciutils + - strace + - wget + - man-db + - unzip + - openssl + - pv + - less + - nano + - tree + - mc + +# List specific for EL based +system_utils_el: + - openssh-clients + - nc + - xz + - lz4 + - yum-utils + - fuse-sshfs + - policycoreutils-python + +# List specific for Debian based +system_utils_deb: + - openssh-client + - netcat + - xz-utils + - liblz4-tool + - sshfs + +# 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: daily + +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) }}" + +... 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..9524715 --- /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' + - role: sudo + - role: ssh + - role: patrix + when: + - patrix_enabled | default(True) + - patrix_server is defined + - patrix_user is defined + - patrix_pass is defined + - role: postfix + when: system_postfix | default(True) 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..a135544 --- /dev/null +++ b/roles/common/tasks/mail.yml @@ -0,0 +1,11 @@ +--- + +- name: Configure root email forward + lineinfile: + dest: /etc/aliases + regexp: "^root:.*" + line: "root: {{ system_admin_email }}" + notify: newaliases + when: system_admin_email is defined + +... diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..494b0f4 --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,18 @@ +--- + +- 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..b5d9266 --- /dev/null +++ b/roles/common/tasks/system.yml @@ -0,0 +1,139 @@ +--- + +- 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: 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' + +- 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 $(( 1$(/bin/date +\%N) \% 300 )); /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 systemd-journal-upload + yum: name=systemd-journal-gateway state=absent + when: ansible_os_family == 'RedHat' + +- name: Remove systemd-journal-upload + apt: name=systemd-journal-remote state=absent + when: ansible_os_family == 'Debian' + +- name: Remove Journal upload state directory + file: path=/var/lib/systemd/journal-upload state=absent + +- name: Remove journal-upload configuration + file: path={{ item }} state=absent + loop: + - /etc/systemd/journal-upload.conf + - /etc/systemd/system/systemd-journal-upload.service + +- 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 + +... 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..268ae60 --- /dev/null +++ b/roles/common/tasks/utils.yml @@ -0,0 +1,32 @@ +--- + +- name: Install common utilities + yum: + name: "{{ system_utils }} + {{ system_utils_el }}" + when: ansible_os_family == 'RedHat' + +- name: Install common utilities + apt: + name: "{{ system_utils }} + {{ system_utils_deb }}" + update_cache: True + when: ansible_os_family == 'Debian' + +- name: Install extra softwares + yum: + name: "{{ system_extra_pkgs }}" + when: ansible_os_family == 'RedHat' + +- name: Install extra softwares + apt: + name: "{{ system_extra_pkgs }}" + when: ansible_os_family == 'Debian' + + # 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..ede3b11 --- /dev/null +++ b/roles/common/templates/bash_aliases.sh.j2 @@ -0,0 +1,10 @@ +#!/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 %} 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/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/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..c971e7c --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,18 @@ +--- + +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..694db0d --- /dev/null +++ b/roles/docker/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: repo_docker diff --git a/roles/docker/tasks/conf.yml b/roles/docker/tasks/conf.yml new file mode 100644 index 0000000..80c9e50 --- /dev/null +++ b/roles/docker/tasks/conf.yml @@ -0,0 +1,60 @@ +--- + +- 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 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..1cbc07d --- /dev/null +++ b/roles/docker/tasks/install_RedHat.yml @@ -0,0 +1,12 @@ +--- + +- name: Install packages + yum: + name: + - docker-ce + - docker-ce-cli + - docker-compose + - device-mapper-persistent-data + - lvm2 + state: present + 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_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..45984cc --- /dev/null +++ b/roles/docker_volume_local_persist/tasks/main.yml @@ -0,0 +1,25 @@ +--- + +- name: Download binary + get_url: + url: "{{ docker_local_persist_url }}" + dest: /usr/local/bin/docker-volume-local-persist + mode: 755 + checksum: sha1:{{ docker_local_persist_sha1 }} + 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/dokuwiki/defaults/main.yml b/roles/dokuwiki/defaults/main.yml new file mode 100644 index 0000000..62d6ccf --- /dev/null +++ b/roles/dokuwiki/defaults/main.yml @@ -0,0 +1,203 @@ +--- + +# A unique ID for this instance. You can deploy several dokuwiki instances on the same machine +dokuwiki_id: 1 + +# Version to deploy +dokuwiki_version: 2018-04-22b +# The sha1 checksum of the archive +dokuwiki_archive_sha1: da296531ee64d9bf8b3a5f0170e902ccc8c9f2a6 + +# 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: 72 + +# 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 + - indexmenu + - 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 }}" +dokuwiki_ldap_starttls: True +dokuwiki_ldap_user_base: "{{ ldap_user_base + ',' + ldap_base }}" +dokuwiki_ldap_group_base: "{{ ldap_group_base + ',' + ldap_base }}" +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 }}" +dokuwiki_ad_starttls: True +dokuwiki_ad_user_base: "{{ ad_ldap_user_search_base | default('DC=' + ad_realm | default(samba_realm) | regex_replace('\\.',',DC=')) }}" +# dokuwiki_ad_bind_dn: +# 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..fa22068 --- /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..79537e5 --- /dev/null +++ b/roles/dokuwiki/tasks/main.yml @@ -0,0 +1,344 @@ +--- + +- name: Set default install mode to none + set_fact: dokuwiki_install_mode="none" + +- name: Install dependencies + yum: + name: + - acl + +- name: Create PHP user acount + user: + name: "{{ dokuwiki_php_user }}" + comment: "PHP FPM for dokuwiki {{ dokuwiki_id }}" + system: yes + shell: /sbin/nologin + +- name: Check if dokuwiki is already installed + stat: path={{ dokuwiki_root_dir }}/meta/ansible_version + register: dokuwiki_version_file + changed_when: False + +- 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 + +- name: Set installation process to install + set_fact: dokuwiki_install_mode='install' + when: not dokuwiki_version_file.stat.exists + +- name: Set installation process to upgrade + set_fact: dokuwiki_install_mode='upgrade' + when: + - dokuwiki_version_file.stat.exists + - dokuwiki_current_version.stdout != dokuwiki_version + - dokuwiki_manage_upgrade + +- name: Create archive dir + file: path={{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }} state=directory mode=700 + when: dokuwiki_install_mode == 'upgrade' + +- 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' + +- 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 + +- name: Download Dokuwiki + get_url: + url: "{{ dokuwiki_archive_url }}" + dest: "{{ dokuwiki_root_dir }}/tmp/" + checksum: "sha1:{{ dokuwiki_archive_sha1 }}" + when: dokuwiki_install_mode != 'none' + +- 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' + +- 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' + +- 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' + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- name: Write dokuwiki version + copy: content={{ dokuwiki_version }} dest={{ dokuwiki_root_dir }}/meta/ansible_version + when: dokuwiki_install_mode != 'none' + +- 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' + +- name: Remove archive directory + file: path={{ dokuwiki_root_dir }}/archives/{{ dokuwiki_current_version.stdout }} state=absent + when: dokuwiki_install_mode == 'upgrade' + +- 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 + +- name: Install authhttpldap plugin + copy: src=authhttpldap dest={{ dokuwiki_root_dir }}/web/lib/plugins + +- 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 }}" + +- 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 }}" + +- 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 }}" + +- 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 + +- 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 + +- 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 }}" + +- 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 }}" + +- 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 }}" + +- 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 + +- name: Remove temp files + file: path={{ dokuwiki_root_dir }}/tmp/{{ item }} state=absent + with_items: + - dokuwiki-{{ dokuwiki_version }} + - dokuwiki-{{ dokuwiki_version }}.tgz + +- 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 }}" + +- 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 }}" + +- name: Deploy permission script + template: src=perms.sh.j2 dest={{ dokuwiki_root_dir }}/perms.sh mode=755 + +- name: Deploy httpd configuration + template: src=httpd.conf.j2 dest=/etc/httpd/ansible_conf.d/10-dokuwiki_{{ dokuwiki_id }}.conf + notify: reload httpd + +- name: Deploy php configuration + template: src=php.conf.j2 dest={{ httpd_php_versions[dokuwiki_php_version].conf_path }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf + notify: restart php-fpm + +- name: Remove PHP config from other versions + file: path={{ httpd_php_versions[item].conf_path }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf state=absent + with_items: "{{ httpd_php_versions.keys() | list | difference([ dokuwiki_php_version ]) }}" + notify: restart php-fpm + +- name: Remove PHP config (using a custom pool) + file: path={{ httpd_php_versions[item].conf_path }}/php-fpm.d/dokuwiki_{{ dokuwiki_id }}.conf state=absent + with_items: "{{ httpd_php_versions.keys() | list }}" + when: dokuwiki_php_fpm_pool is defined + notify: restart php-fpm + +- 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 + +- name: Check if local.php exists + stat: path={{ dokuwiki_root_dir }}/web/conf/local.php + register: dokuwiki_local_php + +- name: Set default values + template: src=local.php.j2 dest={{ dokuwiki_root_dir }}/web/conf/local.php + when: not dokuwiki_local_php.stat.exists + +- name: Deploy htaccess + template: src=htaccess.j2 dest={{ dokuwiki_root_dir }}/web/.htaccess + +- name: Set correct SElinux context + sefcontext: + target: "{{ dokuwiki_root_dir }}(/.*)?" + setype: httpd_sys_content_t + state: present + when: ansible_selinux.status == 'enabled' + +- name: Set optimal permissions + command: "{{ dokuwiki_root_dir }}/perms.sh" + changed_when: False + +- 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..57d736c --- /dev/null +++ b/roles/dokuwiki/templates/local.protected.php.j2 @@ -0,0 +1,35 @@ + '/CN=(.+?),/i'); +{% endif %} +{% if dokuwiki_ldap_bind_dn is defined and dokuwiki_ldap_bind_pass is defined %} +$conf['plugin']['{{ dokuwiki_auth }}']['binddn'] = '{{ dokuwiki_ldap_bind_dn }}'; +$conf['plugin']['{{ dokuwiki_auth }}']['bindpw'] = '{{ dokuwiki_ldap_bind_pass }}'; +{% endif %} +{% elif dokuwiki_auth == 'authad' %} +$conf['plugin']['authad']['base_dn'] = '{{ dokuwiki_ad_user_base }}'; +$conf['plugin']['authad']['domain_controllers'] = '{{ dokuwiki_ad_dc | join(', ') }}'; +$conf['plugin']['authad']['admin_username'] = '{{ dokuwiki_ad_bind_dn }}'; +$conf['plugin']['authad']['admin_password'] = '{{ dokuwiki_ad_bind_pass }}'; +$conf['plugin']['authad']['sso'] = 1; +$conf['plugin']['authad']['use_tls'] = {{ dokuwiki_ad_starttls | ternary('1','0') }}; +$conf['plugin']['authad']['recursive_groups'] = 1; +$conf['plugin']['authad']['expirywarn'] = 15; +{% endif %} + +?> 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 @@ + {{ dolibarr_root_dir }}/db_dumps/{{ dolibarr_db_name }}.sql.lz4 diff --git a/roles/dolibarr/templates/httpd.conf.j2 b/roles/dolibarr/templates/httpd.conf.j2 new file mode 100644 index 0000000..7f3f13f --- /dev/null +++ b/roles/dolibarr/templates/httpd.conf.j2 @@ -0,0 +1,19 @@ +{% if dolibarr_alias is defined %} +Alias /{{ dolibarr_alias }} {{ dolibarr_root_dir }}/web/htdocs +{% else %} +# No alias defined, create a vhost to access it +{% endif %} + +RewriteEngine On + + 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..617823f --- /dev/null +++ b/roles/dolibarr/templates/logrotate.conf.j2 @@ -0,0 +1,11 @@ +{{ dolibarr_root_dir }}/data/dolibarr.log { + weekly + rotate 52 + compress + compressoptions -T0 + compresscmd /usr/bin/xz + compressext .xz + uncompresscmd /usr/bin/unxz + 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/rm_dump.j2 b/roles/dolibarr/templates/rm_dump.j2 new file mode 100644 index 0000000..baa9778 --- /dev/null +++ b/roles/dolibarr/templates/rm_dump.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ dolibarr_root_dir }}/db_dumps/* diff --git a/roles/elasticsearch/defaults/main.yml b/roles/elasticsearch/defaults/main.yml new file mode 100644 index 0000000..95a2546 --- /dev/null +++ b/roles/elasticsearch/defaults/main.yml @@ -0,0 +1,8 @@ +--- + +es_cluster_name: elasticsearch +es_node_name: "{{ inventory_hostname }}" +es_port: 9200 +es_src_ip: [] +es_data_dir: /opt/elasticsearch/data +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..c0f2548 --- /dev/null +++ b/roles/elasticsearch/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: repo_elasticsearch diff --git a/roles/elasticsearch/tasks/main.yml b/roles/elasticsearch/tasks/main.yml new file mode 100644 index 0000000..f846b45 --- /dev/null +++ b/roles/elasticsearch/tasks/main.yml @@ -0,0 +1,101 @@ +--- + +- name: Install needed packages + yum: + name: + - elasticsearch-oss + - java-1.8.0-openjdk-headless + tags: es + +- 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 + +- 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: 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" + when: iptables_manage | default(True) + loop: + - port: "{{ es_port }}" + name: es_port + src_ip: "{{ es_src_ip }}" + tags: firewall,es + +- name: Create pre/post backup dir + file: path=/etc/backup/{{ item }}.d state=directory + loop: + - pre + - post + 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 + +- name: Start and enable the service + service: name=elasticsearch state=started enabled=True + tags: es + +- name: Create backup dir + file: path={{ es_backup_dir }} state=directory owner=elasticsearch group=elasticsearch mode=700 + tags: es + +- 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/templates/elasticsearch.yml.j2 b/roles/elasticsearch/templates/elasticsearch.yml.j2 new file mode 100644 index 0000000..ceec23f --- /dev/null +++ b/roles/elasticsearch/templates/elasticsearch.yml.j2 @@ -0,0 +1,8 @@ +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 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..7332fc8 --- /dev/null +++ b/roles/elasticsearch/templates/post-backup.j2 @@ -0,0 +1,8 @@ +#!/bin/bash -e + +{% if es_backup_dir != '' and es_backup_dir != '/' %} +rm -rf {{ es_backup_dir }}/* +{% else %} +# Can't delete elasticsearch dumps, set es_backup_dir to a non empty path +{% endif %} +umount /home/lbkp/es diff --git a/roles/elasticsearch/templates/pre-backup.j2 b/roles/elasticsearch/templates/pre-backup.j2 new file mode 100644 index 0000000..b60cc17 --- /dev/null +++ b/roles/elasticsearch/templates/pre-backup.j2 @@ -0,0 +1,5 @@ +#!/bin/bash -e + +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..7efffb7 --- /dev/null +++ b/roles/etherpad/defaults/main.yml @@ -0,0 +1,32 @@ +--- + +etherpad_id: 1 +etherpad_root_dir: /opt/etherpad_{{ etherpad_id }} +etherpad_user: etherpad_{{ etherpad_id }} +etherpad_version: 1.8.0 +etherpad_archive_url: https://github.com/ether/etherpad-lite/archive/{{ etherpad_version }}.tar.gz +etherpad_archive_sha1: 4b7e49749adf63dc395ea8ed8e06f5e56c8fd2c2 +etherpad_port: 9003 +etherpad_src_ip: [] + +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 +# etherpad_db_pass: s3cr3t. + +etherpad_title: Etherpad +etherpad_theme: colibris +# A random one will be created if not defined +#etherpad_admin_pass: p@ssw0rd +#etherpad_api_key: 123456 + +etherpad_plugins_base: + - adminpads + - delete_after_delay + - delete_empty_pads + - small_list + - markdown +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..a317f2c --- /dev/null +++ b/roles/etherpad/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: repo_nodejs 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..db8c1ca --- /dev/null +++ b/roles/etherpad/tasks/archive_pre.yml @@ -0,0 +1,9 @@ +--- + +- import_tasks: ../includes/webapps_archive.yml + vars: + - root_dir: "{{ etherpad_root_dir }}" + - version: "{{ current_version }}" + - db_name: "{{ etherpad_db_name }}" + tags: etherpad + diff --git a/roles/etherpad/tasks/cleanup.yml b/roles/etherpad/tasks/cleanup.yml new file mode 100644 index 0000000..a4da541 --- /dev/null +++ b/roles/etherpad/tasks/cleanup.yml @@ -0,0 +1,8 @@ +--- + +- name: Remove temp files + file: path={{ etherpad_root_dir }}/tmp/{{ item }} state=absent + loop: + - etherpad-lite-{{ etherpad_version }} + - etherpad-lite-{{ etherpad_version }}.tar.gz + tags: etherpad diff --git a/roles/etherpad/tasks/conf.yml b/roles/etherpad/tasks/conf.yml new file mode 100644 index 0000000..eff9331 --- /dev/null +++ b/roles/etherpad/tasks/conf.yml @@ -0,0 +1,15 @@ +--- + +- name: Configure random keys + copy: content={{ item.value }} dest={{ etherpad_root_dir }}/web/{{ item.file }} + 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 }}/web/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..be9f4ce --- /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: db_dumps + mode: 700 + - dir: archives + mode: 700 + - dir: web + 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..327015e --- /dev/null +++ b/roles/etherpad/tasks/facts.yml @@ -0,0 +1,44 @@ +--- + +- import_tasks: ../includes/webapps_set_install_mode.yml + vars: + root_dir: "{{ etherpad_root_dir }}" + version: "{{ etherpad_version }}" + tags: etherpad +- set_fact: etherpad_install_mode={{ install_mode }} + tags: etherpad + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_dbpass" + when: etherpad_db_pass is not defined + tags: etherpad +- set_fact: etherpad_db_pass={{ rand_pass }} + when: etherpad_db_pass is not defined + tags: etherpad + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_session_key" + tags: etherpad +- set_fact: etherpad_session_key={{ rand_pass }} + tags: etherpad + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_api_key" + when: etherpad_api_key is not defined + tags: etherpad +- set_fact: etherpad_api_key={{ rand_pass }} + when: etherpad_api_key is not defined + tags: etherpad + +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{etherpad_root_dir }}/meta/ansible_admin_pass" + when: etherpad_admin_pass is not defined + tags: etherpad +- set_fact: etherpad_admin_pass={{ rand_pass }} + when: etherpad_admin_pass is not defined + tags: etherpad + diff --git a/roles/etherpad/tasks/install.yml b/roles/etherpad/tasks/install.yml new file mode 100644 index 0000000..abd6797 --- /dev/null +++ b/roles/etherpad/tasks/install.yml @@ -0,0 +1,81 @@ +--- + +- name: Install dependencies + yum: + name: + - nodejs + notify: restart etherpad + tags: etherpad + +- name: Download etherpad + get_url: + url: "{{ etherpad_archive_url }}" + dest: "{{ etherpad_root_dir }}/tmp" + checksum: "sha1:{{ etherpad_archive_sha1 }}" + when: etherpad_install_mode != 'none' + tags: etherpad + +- name: Extract etherpad + unarchive: + src: "{{ etherpad_root_dir }}/tmp/etherpad-lite-{{ etherpad_version }}.tar.gz" + dest: "{{ etherpad_root_dir }}/tmp/" + remote_src: True + when: etherpad_install_mode != 'none' + tags: etherpad + +- name: Move etherpad to its correct dir + synchronize: + src: "{{ etherpad_root_dir }}/tmp/etherpad-lite-{{ etherpad_version }}/" + dest: "{{ etherpad_root_dir }}/web/" + recursive: True + delete: True + delegate_to: "{{ inventory_hostname }}" + become_user: "{{ etherpad_user }}" + when: etherpad_install_mode != 'none' + tags: etherpad + +- name: Ensure node_modules dir exists + file: path={{ etherpad_root_dir }}/web/node_modules state=directory + tags: etherpad + +- name: Link etherpad sources in node_modules + file: src={{ etherpad_root_dir }}/web/src dest={{ etherpad_root_dir }}/web/node_modules/ep_etherpad-lite state=link + tags: etherpad + +- name: Install node modules + npm: path={{ etherpad_root_dir }}/web/node_modules/ep_etherpad-lite production=True state={{ (etherpad_install_mode == 'none') | ternary('present','latest') }} + become_user: "{{ etherpad_user }}" + tags: etherpad + +- name: Install plugins + npm: name=ep_{{ item }} path={{ etherpad_root_dir }}/web/node_modules/ep_etherpad-lite production=True 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..ae2735d --- /dev/null +++ b/roles/etherpad/tasks/main.yml @@ -0,0 +1,17 @@ +--- + +- include: facts.yml +- include: user.yml +- include: directories.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..7f89a0a --- /dev/null +++ b/roles/etherpad/templates/etherpad.service.j2 @@ -0,0 +1,25 @@ +[Unit] +Description=Etherpad ({{ etherpad_id }} Instance) +After=syslog.target network.target + +[Service] +Type=simple +User={{ etherpad_user }} +Group={{ etherpad_user }} +WorkingDirectory={{ etherpad_root_dir }}/web +ExecStartPre=/bin/rm -f {{ etherpad_root_dir }}/web/var/minified* +ExecStart=/usr/bin/node ./node_modules/ep_etherpad-lite/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..c8032bf --- /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 }}/web +find {{ etherpad_root_dir }}/web -type f -exec chmod 644 "{}" \; +find {{ etherpad_root_dir }}/web -type d -exec chmod 755 "{}" \; +chmod 640 {{ etherpad_root_dir }}/web/{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..a71aef4 --- /dev/null +++ b/roles/etherpad/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/sh + +rm -f {{ etherpad_root_dir }}/db_dump/* diff --git a/roles/etherpad/templates/pre_backup.sh.j2 b/roles/etherpad/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..dd73632 --- /dev/null +++ b/roles/etherpad/templates/pre_backup.sh.j2 @@ -0,0 +1,7 @@ +#!/bin/sh + +/usr/bin/mysqldump --user={{ etherpad_db_user }} \ + --password='{{ etherpad_db_pass }}' \ + --host={{ etherpad_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ etherpad_db_name }} | lz4 -c > {{ etherpad_root_dir }}/db_dumps/{{ etherpad_db_name }}.sql.lz4 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..bbd445b --- /dev/null +++ b/roles/filebeat/defaults/main.yml @@ -0,0 +1,11 @@ +--- +filebeat_output_type: logstash +filebeat_output_hosts: + - localhost: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..86813d5 --- /dev/null +++ b/roles/filebeat/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: restart filebeat + service: name=filebeat state=restarted + +- name: restart journalbeat + service: + name: journalbeat + state: "{{ (ansible_service_mgr == 'systemd') | ternary('restarted','stopped') }}" 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/install_filebeat_Debian.yml b/roles/filebeat/tasks/install_filebeat_Debian.yml new file mode 100644 index 0000000..27ce7df --- /dev/null +++ b/roles/filebeat/tasks/install_filebeat_Debian.yml @@ -0,0 +1,6 @@ +--- +- name: Install filebeat + apt: + name: + - filebeat + tags: logs diff --git a/roles/filebeat/tasks/install_filebeat_RedHat.yml b/roles/filebeat/tasks/install_filebeat_RedHat.yml new file mode 100644 index 0000000..ded2e78 --- /dev/null +++ b/roles/filebeat/tasks/install_filebeat_RedHat.yml @@ -0,0 +1,6 @@ +--- +- name: install filebeat + yum: + name: + - filebeat + tags: logs diff --git a/roles/filebeat/tasks/install_journalbeat_Debian.yml b/roles/filebeat/tasks/install_journalbeat_Debian.yml new file mode 100644 index 0000000..bf72b67 --- /dev/null +++ b/roles/filebeat/tasks/install_journalbeat_Debian.yml @@ -0,0 +1,8 @@ +--- +- name: Install journalbeat + apt: + name: + - journalbeat + environment: + https_proxy: "{{ system_proxy }}" + tags: logs diff --git a/roles/filebeat/tasks/install_journalbeat_RedHat.yml b/roles/filebeat/tasks/install_journalbeat_RedHat.yml new file mode 100644 index 0000000..f84afb3 --- /dev/null +++ b/roles/filebeat/tasks/install_journalbeat_RedHat.yml @@ -0,0 +1,6 @@ +--- +- name: Install journalbeat + yum: + name: + - journalbeat + tags: logs diff --git a/roles/filebeat/tasks/main.yml b/roles/filebeat/tasks/main.yml new file mode 100644 index 0000000..7c558b4 --- /dev/null +++ b/roles/filebeat/tasks/main.yml @@ -0,0 +1,64 @@ +--- + +- include_tasks: install_filebeat_{{ ansible_os_family }}.yml +- include_tasks: install_journalbeat_{{ ansible_os_family }}.yml + 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: Start and enable filebeat + service: name=filebeat state=started enabled=True + tags: logs + +- name: Handle journalbeat service + service: + name: journalbeat + state: "{{ (ansible_service_mgr == 'systemd') | ternary('started','stopped') }}" + enabled: "{{ (ansible_service_mgr == 'systemd') | 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..a9dccd5 --- /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.10' + +# 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: cb0dc2a2d173ac61de7aa19deb181392d3a5cbc0. + +# The user account under which PHP is executed +framadate_php_user: php-framadate_{{ framadate_id }} + +# The version of PHP to use +framadate_php_version: 73 + +# 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..f55a36f --- /dev/null +++ b/roles/framadate/meta/main.yml @@ -0,0 +1,3 @@ +--- +allow_duplicates: true +... diff --git a/roles/framadate/tasks/main.yml b/roles/framadate/tasks/main.yml new file mode 100644 index 0000000..bfb6b27 --- /dev/null +++ b/roles/framadate/tasks/main.yml @@ -0,0 +1,258 @@ +--- + +- name: Set install mode + set_fact: framadate_install_mode='none' + tags: framadate + +- name: Install needed tools + yum: + name: + - unzip + - MySQL-python + - acl + - composer + - 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 + environment: + php: /bin/php{{ framadate_php_version == '54' | ternary('',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={{ httpd_php_versions[framadate_php_version].conf_path }}/php-fpm.d/framadate_{{ framadate_id }}.conf + notify: restart php-fpm + tags: framadate + +- name: Remove PHP configuration from other versions + file: path={{ httpd_php_versions[item].conf_path }}/php-fpm.d/framadate_{{ framadate_id }}.conf state=absent + with_items: "{{ httpd_php_versions.keys() | list | difference([ framadate_php_version ]) }}" + notify: restart php-fpm + tags: framadate + +- name: Remove PHP configuration (using a custom pool) + file: path={{ httpd_php_versions[framadate_php_version].conf_path }}/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..4eeb3d2 --- /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 -x -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..f190b24 --- /dev/null +++ b/roles/freepbx/defaults/main.yml @@ -0,0 +1,52 @@ +--- + +fpbx_version: 15.0 +fpbx_archive_sha1: f9c076d5afbe787cb2a7068f02c96b4bc413f61e +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/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: 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..586b8ed --- /dev/null +++ b/roles/freepbx/templates/logrotate.conf.j2 @@ -0,0 +1,26 @@ +/var/log/asterisk/messages +/var/log/asterisk/event_log +/var/log/asterisk/queue_log +/var/log/asterisk/full +/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..fa29b22 --- /dev/null +++ b/roles/freepbx/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ fpbx_root_dir }}/db_dump/* diff --git a/roles/freepbx/templates/pre_backup.sh.j2 b/roles/freepbx/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..1a15cb0 --- /dev/null +++ b/roles/freepbx/templates/pre_backup.sh.j2 @@ -0,0 +1,12 @@ +#!/bin/bash -e + +/usr/bin/mysqldump --user={{ fpbx_db_user }} \ + --password='{{ fpbx_db_pass }}' \ + --host={{ fpbx_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ fpbx_db_name }} | lz4 -c > {{ fpbx_root_dir }}/db_dumps/{{ fpbx_db_name }}.sql.lz4 +/usr/bin/mysqldump --user={{ fpbx_db_user }} \ + --password='{{ fpbx_db_pass }}' \ + --host={{ fpbx_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ fpbx_cdr_db_name }} | lz4 -c > {{ fpbx_root_dir }}/db_dumps/{{ fpbx_db_name }}.sql.lz4 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/funkwhale/defaults/main.yml b/roles/funkwhale/defaults/main.yml new file mode 100644 index 0000000..cbd5c2c --- /dev/null +++ b/roles/funkwhale/defaults/main.yml @@ -0,0 +1,54 @@ +--- + +funkwhale_version: 0.20.1 +funkwhale_id: 1 +#funkwhale_archive_url: https://dev.funkwhale.audio/funkwhale/funkwhale/-/archive/{{ funkwhale_version }}/funkwhale-{{ funkwhale_version }}.tar.gz +funkwhale_base_url: https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/{{ funkwhale_version }}/download +funkwhale_archive_sha1: + api: 0fc2f0152c18bff3d33410d299b2c67f8a4fe101 + front: dc9094508048176ed9f7fe819d6d5e92ed415511 +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 +funkwale_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..5149246 --- /dev/null +++ b/roles/funkwhale/meta/main.yml @@ -0,0 +1,10 @@ +--- +allow_duplicates: true +dependencies: + - role: repo_scl # for python36 + - role: repo_nux_dextop # for ffmpeg + - 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/main.yml b/roles/funkwhale/tasks/main.yml new file mode 100644 index 0000000..7739163 --- /dev/null +++ b/roles/funkwhale/tasks/main.yml @@ -0,0 +1,317 @@ +--- + +- name: Install packages + yum: + name: + - gcc + - git + - postgresql11 + - postgresql-devel + - openldap-devel + - cyrus-sasl-devel + - libjpeg-turbo-devel + - python-psycopg2 + - python-setuptools + - rh-python36-python-virtualenv + - rh-python36-python-pip + - ffmpeg + - mod_xsendfile + 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 +- set_fact: funkwhale_install_mode={{ (install_mode == 'upgrade' and not funkwhale_manage_upgrade) | ternary('none',install_mode) }} + tags: funkwhale +- set_fact: funkwhale_current_version={{ current_version | default('') }} + tags: funkwhale + +- 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 + +- 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: venv + - dir: db_dumps + mode: 700 + tags: funkwhale + +- name: Create archive dir + file: path={{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }} state=directory + when: funkwhale_install_mode == 'upgrade' + 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 + delegate_to: "{{ inventory_hostname }}" + when: funkwhale_install_mode == 'upgrade' + tags: funkwhale + +- name: Archive a database dump + command: > + /usr/pgsql-11/bin/pg_dump + --clean + --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 }}" + when: funkwhale_install_mode == 'upgrade' + 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 + + # Create a random pass for the DB if needed +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ funkwhale_root_dir }}/meta/ansible_dbpass" + when: funkwhale_db_pass is not defined + tags: funkwhale +- set_fact: funkwhale_db_pass={{ rand_pass }} + when: funkwhale_db_pass is not defined + 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: Create the virtualenv + pip: + name: + - wheel + - pip + - virtualenv + - service_identity + state: latest + virtualenv: "{{ funkwhale_root_dir }}/venv" + virtualenv_command: /opt/rh/rh-python36/root/usr/bin/virtualenv + virtualenv_python: /opt/rh/rh-python36/root/usr/bin/python + 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" + state: latest + virtualenv: "{{ funkwhale_root_dir }}/venv" + virtualenv_command: /opt/rh/rh-python36/root/usr/bin/virtualenv + virtualenv_python: /opt/rh/rh-python36/root/usr/bin/python + when: funkwhale_install_mode != 'none' + notify: restart funkwhale + tags: funkwhale + + # Create a random django secret key +- import_tasks: ../includes/get_rand_pass.yml + vars: + - pass_file: "{{ funkwhale_root_dir }}/meta/ansible_django_key" + when: funkwhale_secret_key is not defined + tags: funkwhale +- set_fact: funkwhale_secret_key={{ rand_pass }} + when: funkwhale_secret_key is not defined + 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: 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 + +- name: Migrate database + django_manage: + command: migrate + app_path: "{{ funkwhale_root_dir }}/api" + virtualenv: "{{ funkwhale_root_dir }}/venv" + 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 + +- name: Start and enable funkwhale services + systemd: name=funkwhale_{{ funkwhale_id }}-{{ item }} state=started enabled=True + loop: + - server.service + - update-media.timer + tags: funkwhale + +- name: Write version + copy: content={{ funkwhale_version }} dest={{ funkwhale_root_dir }}/meta/ansible_version + when: funkwhale_install_mode != "none" + tags: funkwhale + +- name: Compress previous version + command: tar cf {{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}.txz ./ + environment: + XZ_OPT: -T0 + args: + chdir: "{{ funkwhale_root_dir }}/archives/{{ funkwhale_current_version }}" + warn: False + when: funkwhale_install_mode == 'upgrade' + tags: funkwhale + +- 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 }} + tags: funkwhale diff --git a/roles/funkwhale/templates/env.j2 b/roles/funkwhale/templates/env.j2 new file mode 100644 index 0000000..fd42a4c --- /dev/null +++ b/roles/funkwhale/templates/env.j2 @@ -0,0 +1,32 @@ +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 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..972ea32 --- /dev/null +++ b/roles/funkwhale/templates/funkwhale-update-media.service.j2 @@ -0,0 +1,25 @@ +[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 %} \ +{% for format in [ 'mp3', 'MP3', 'ogg', 'OGG', 'flac', 'FLAC' ] %} + "{{ lib.path | regex_replace('/$', '') }}/**/*.{{ format }}" \ +{% endfor %} + --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..bcd7e6d --- /dev/null +++ b/roles/funkwhale/templates/httpd.conf.j2 @@ -0,0 +1,79 @@ + + 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 + + + LoadModule xsendfile_module modules/mod_xsendfile.so + + 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..a603170 --- /dev/null +++ b/roles/funkwhale/templates/post-backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ funkwhale_root_dir }}/db_dumps/{{ funkwhale_db_name }}.sql.lz4 diff --git a/roles/funkwhale/templates/pre-backup.sh.j2 b/roles/funkwhale/templates/pre-backup.sh.j2 new file mode 100644 index 0000000..478893c --- /dev/null +++ b/roles/funkwhale/templates/pre-backup.sh.j2 @@ -0,0 +1,8 @@ +#!/bin/bash -e + +PGPASSWORD='{{ funkwhale_db_pass }}' /usr/pgsql-11/bin/pg_dump \ + --clean \ + --username={{ funkwhale_db_user }} \ + --host={{ funkwhale_db_server }} \ + {{ funkwhale_db_name }} | \ + lz4 -c > {{ funkwhale_root_dir }}/db_dumps/{{ funkwhale_db_name }}.sql.lz4 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..0925400 --- /dev/null +++ b/roles/fusioninventory_agent/tasks/install_Debian.yml @@ -0,0 +1,28 @@ +--- + +- 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('') }}" diff --git a/roles/fusioninventory_agent/tasks/install_RedHat.yml b/roles/fusioninventory_agent/tasks/install_RedHat.yml new file mode 100644 index 0000000..0425c54 --- /dev/null +++ b/roles/fusioninventory_agent/tasks/install_RedHat.yml @@ -0,0 +1,5 @@ +--- + +- name: Install FusionInventory Agent + yum: name=fusioninventory-agent + diff --git a/roles/fusioninventory_agent/tasks/main.yml b/roles/fusioninventory_agent/tasks/main.yml new file mode 100644 index 0000000..e839dbc --- /dev/null +++ b/roles/fusioninventory_agent/tasks/main.yml @@ -0,0 +1,20 @@ +--- + +- include_tasks: 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 + +- name: Check if the first inventory has been done + stat: path=/var/lib/fusioninventory-agent/FusionInventory-Agent.dump + register: first_inventory + +- name: First Fusion Inventory report + command: /usr/bin/fusioninventory-agent + when: not first_inventory.stat.exists + +- name: Start FusionInventory Agent + service: name=fusioninventory-agent state=started enabled=yes + +... 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/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/archive_pre.yml b/roles/gitea/archive_pre.yml new file mode 100644 index 0000000..d56edf5 --- /dev/null +++ b/roles/gitea/archive_pre.yml @@ -0,0 +1,21 @@ +--- +- 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 + tags: gitea + diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml new file mode 100644 index 0000000..c724580 --- /dev/null +++ b/roles/gitea/defaults/main.yml @@ -0,0 +1,40 @@ +--- + +# Version to install +gitea_version: 1.11.4 +# 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: 4408c781069c36cbb1b5923ae924e67ceee661ba9c9bd6c73cd7408c9cd62af6 +# 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: + - 0.0.0.0/0 +# 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..cbce481 --- /dev/null +++ b/roles/gitea/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: repo_scl diff --git a/roles/gitea/tasks/admin_user.yml b/roles/gitea/tasks/admin_user.yml new file mode 100644 index 0000000..a2914df --- /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 create-user --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/conf.yml b/roles/gitea/tasks/conf.yml new file mode 100644 index 0000000..0c14afb --- /dev/null +++ b/roles/gitea/tasks/conf.yml @@ -0,0 +1,16 @@ +--- + +- name: Set sclo-git212 as default git command + template: src=git.sh.j2 dest=/etc/profile.d/git.sh mode=755 + 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..ab75b7b --- /dev/null +++ b/roles/gitea/tasks/directories.yml @@ -0,0 +1,25 @@ +--- +- 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: data + - dir: data/repositories + - dir: custom + - dir: public + - dir: etc + - dir: tmp + - dir: bin + - dir: meta + owner: root + group: root + mode: 700 + - dir: db_dumps + 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..fec02d5 --- /dev/null +++ b/roles/gitea/tasks/facts.yml @@ -0,0 +1,46 @@ +--- +- 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 + +- 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 + tags: gitea + +- name: Read random tokens + command: cat {{ gitea_root_dir }}/meta/ansible_{{ item }} + with_items: + - INTERNAL_TOKEN + - LFS_JWT_SECRET + - SECRET_KEY + changed_when: False + register: gitea_tokens + tags: gitea diff --git a/roles/gitea/tasks/install.yml b/roles/gitea/tasks/install.yml new file mode 100644 index 0000000..59457ea --- /dev/null +++ b/roles/gitea/tasks/install.yml @@ -0,0 +1,64 @@ +--- +- name: Install packages + yum: + name: + - sclo-git212-git + - git-lfs + 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..2f8b4c6 --- /dev/null +++ b/roles/gitea/tasks/main.yml @@ -0,0 +1,16 @@ +--- + +- include: facts.yml +- include: user.yml +- include: directories.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 + 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..b2a3ae2 --- /dev/null +++ b/roles/gitea/templates/app.ini.j2 @@ -0,0 +1,94 @@ +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 %} + +[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 + +[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 + +[other] +SHOW_FOOTER_VERSION = false 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..190d84c --- /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=/bin/scl enable sclo-git212 -- {{ 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..08742c9 --- /dev/null +++ b/roles/gitea/templates/post_backup.sh.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ gitea_root_dir }}/db_dumps/* diff --git a/roles/gitea/templates/pre_backup.sh.j2 b/roles/gitea/templates/pre_backup.sh.j2 new file mode 100644 index 0000000..f7b93f3 --- /dev/null +++ b/roles/gitea/templates/pre_backup.sh.j2 @@ -0,0 +1,8 @@ +#!/bin/bash -e + +/usr/bin/mysqldump --user='{{ gitea_db_user }}' \ + --password='{{ gitea_db_pass }}' \ + --host={{ gitea_db_server }} \ + --quick --single-transaction \ + --add-drop-table {{ gitea_db_name }} | \ + lz4 -c > {{ gitea_root_dir }}/db_dumps/{{ gitea_db_name }}.sql.lz4 diff --git a/roles/glpi/defaults/main.yml b/roles/glpi/defaults/main.yml new file mode 100644 index 0000000..324d7f9 --- /dev/null +++ b/roles/glpi/defaults/main.yml @@ -0,0 +1,92 @@ +--- + +glpi_id: 1 +glpi_manage_upgrade: True +glpi_version: 9.4.5 +glpi_zip_url: https://github.com/glpi-project/glpi/releases/download/{{ glpi_version }}/glpi-{{ glpi_version }}.tgz +glpi_zip_sha1: 081098cc8d46f0e148dfe5e16859bb8b3ae1b227 +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: 73 +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.4+2.3' + sha1: f4d278aadf7fe363b8f1d74f5791950bd72ff34d + url: https://github.com/fusioninventory/fusioninventory-for-glpi/releases/download/glpi9.4%2B2.3/fusioninventory-9.4+2.3.tar.bz2 + reports: + version: 1.13.1 + sha1: 6a099b155ce1e9a55b425ddadde23166fe296e81 + url: https://forge.glpi-project.org/attachments/download/2291/glpi-plugin-reports-1.13.1.tar.gz + pdf: + version: 1.6.0 + sha1: cbfff59561e72492d02e36d2d84ec53e9ac0c923 + url: https://forge.glpi-project.org/attachments/download/2293/glpi-pdf-1.6.0.tar.gz + behaviors: + version: 2.2.1 + sha1: 26d75f2c7b036505b58c22eeac0c57765cbfe759 + url: https://forge.glpi-project.org/attachments/download/2287/glpi-behaviors-2.2.1.tar.gz + #manufacturersimports: + # version: 2.1.2 + # sha1: b97e92cde83070c37090eb64b6611ffc7f4039af + # url: https://github.com/InfotelGLPI/manufacturersimports/releases/download/2.1.2/glpi-manufacturersimports-2.1.2.tar.gz + appliances: + version: 2.5.0 + sha1: 7f42a9e64479c6e891e11daebeb41cbb49a1b39b + url: https://forge.glpi-project.org/attachments/download/2285/glpi-appliances-2.5.0.tar.gz + domains: + version: 2.1.0 + sha1: 65fdc064f5f6f9343a247cca23cfbe112fc63743 + url: https://github.com/InfotelGLPI/domains/releases/download/2.1.0/glpi-domains-2.1.0.tar.gz + formcreator: + version: 2.9.0 + sha1: 930aaec97c83492fcd6d826777ad62539544de1a + url: https://github.com/pluginsGLPI/formcreator/releases/download/v2.9.0/glpi-formcreator-2.9.0.tar.bz2 + tag: + version: 2.5.0 + sha1: de505ceb0c163ef988567c7253d8dbec2f7a3d9d + url: https://github.com/pluginsGLPI/tag/releases/download/2.5.0/glpi-tag-2.5.0.tar.bz2 + mreporting: + version: 1.6.1 + sha1: c814dd88662ca1504a788ac8f8313a596b2c2d16 + url: https://github.com/pluginsGLPI/mreporting/releases/download/1.6.1/glpi-mreporting-1.6.1.tar.bz2 + fields: + version: 1.10.2 + sha1: 1f263aae0f31d015ac6e5a172716f58d3febb0e8 + url: https://github.com/pluginsGLPI/fields/releases/download/1.10.2/glpi-fields-1.10.2.tar.bz2 + webapplications: + version: 2.6.0 + sha1: 389bd280f8aae17330092caa902a2fb7b752868b + url: https://github.com/InfotelGLPI/webapplications/releases/download/2.6.0/glpi-webapplications-2.6.0.tar.gz + genericobject: + version: 2.7.0 + sha1: 28280dd98d6dc575323f21e9bc4bd1d8a66f474e + url: https://github.com/pluginsGLPI/genericobject/releases/download/2.7.0/glpi-genericobject-2.7.0.tar.bz2 + mantis: + version: 4.3.1 + sha1: f54f90611b5d844bd1a90907d895eed222c96922 + url: https://github.com/pluginsGLPI/mantis/releases/download/4.3.1/glpi-mantis-4.3.1.tar.bz2 + archimap: + version: 2.1.3 + sha1: e747eb02dcb7b3723af453bb54f516cb2578f0c4 + url: https://github.com/ericferon/glpi-archimap/releases/download/v2.1.3/archimap-v2.1.3.tar.gz + dashboard: + version: 0.9.8 + sha1: ec0c5ee59d1f05f87f4d950a8760fbf35cd70eb2 + url: https://forge.glpi-project.org/attachments/download/2294/GLPI-dashboard_plugin-0.9.8.zip + +glpi_plugins_to_install: [] + +... 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..c07f269 --- /dev/null +++ b/roles/glpi/meta/main.yml @@ -0,0 +1,5 @@ +--- +allow_duplicates: true +dependencies: + - role: mkdir +... 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..f3f82ff --- /dev/null +++ b/roles/glpi/tasks/cleanup.yml @@ -0,0 +1,18 @@ +--- + +- 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={{ glpi_root_dir }}/tmp/{{ item }} state=absent + with_items: + - glpi + - glpi-{{ glpi_version }}.tgz + - glpi.sql + tags: glpi + diff --git a/roles/glpi/tasks/conf.yml b/roles/glpi/tasks/conf.yml new file mode 100644 index 0000000..660c76a --- /dev/null +++ b/roles/glpi/tasks/conf.yml @@ -0,0 +1,30 @@ +--- + +- 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: Upgrade database + command: "/bin/php{{ (glpi_php_version == '54') | ternary('',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 diff --git a/roles/glpi/tasks/directories.yml b/roles/glpi/tasks/directories.yml new file mode 100644 index 0000000..79bb494 --- /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 }}/db_dumps" + - "{{ 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..7c4333a --- /dev/null +++ b/roles/glpi/tasks/install.yml @@ -0,0 +1,132 @@ +--- + +- name: Install needed tools + yum: + name: + - unzip + - MySQL-python + - tar + - bzip2 + - acl + - mariadb + - php-pear-CAS + 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/' + 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 + +- 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 + +- name: Create a safer MySQL schema file + shell: grep -v 'DROP TABLE' {{ glpi_root_dir }}/web/install/mysql/glpi-empty.sql > {{ glpi_root_dir }}/tmp/glpi.sql + when: glpi_install_mode == 'install' + tags: glpi + +- name: Inject MySQL schema + mysql_db: + name: "{{ glpi_mysql_db }}" + state: import + target: "{{ glpi_root_dir }}/tmp/glpi.sql" + login_host: "{{ glpi_mysql_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + when: glpi_install_mode == 'install' + 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.script }}.j2 dest=/etc/backup/{{ item.type }}.d/glpi_{{ glpi_id }}_{{ item.script }} mode=750 + with_items: + - script: dump_db + type: pre + - script: rm_dump + type: post + 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/dump_db.j2 b/roles/glpi/templates/dump_db.j2 new file mode 100644 index 0000000..a343597 --- /dev/null +++ b/roles/glpi/templates/dump_db.j2 @@ -0,0 +1,7 @@ +#!/bin/bash -e + +/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 }} | lz4 -c > {{ glpi_root_dir }}/db_dumps/{{ glpi_mysql_db }}.sql.lz4 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/perms.sh.j2 b/roles/glpi/templates/perms.sh.j2 new file mode 100644 index 0000000..bb16d2a --- /dev/null +++ b/roles/glpi/templates/perms.sh.j2 @@ -0,0 +1,17 @@ +#!/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,db_dumps} +chmod 700 {{ glpi_root_dir }}/{meta,db_dumps} +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 +chmod 770 {{ glpi_root_dir }}/web/config +chmod 660 {{ glpi_root_dir }}/web/config/* 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/rm_dump.j2 b/roles/glpi/templates/rm_dump.j2 new file mode 100644 index 0000000..d732aa7 --- /dev/null +++ b/roles/glpi/templates/rm_dump.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -f {{ glpi_root_dir }}/db_dumps/* 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..ed845e4 --- /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..e739bc4 --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,165 @@ +--- +- name: Install grafana + yum: name=grafana state=present + register: grafana_install + +- name: Create unit snippet dir + file: path=/etc/systemd/system/grafana-server.service.d state=directory + +- 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 + +- name: Reload systemd + systemd: daemon_reload=True + when: grafana_unit.changed + +- name: Install MySQL support + yum: name=MySQL-python state=present + when: grafana_db_type == 'mysql' + +- name: Install PostgreSQL support + yum: name=python-psycopg2 state=present + when: grafana_db_type == 'postgres' + +- 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) + +- name: Generate a random pass for database + shell: openssl rand -base64 45 > /etc/grafana/ansible_db_pass + args: + creates: /etc/grafana/ansible_db_pass + when: + - grafana_db_type == 'mysql' or grafana_db_type == 'postgres' + - grafana_db_pass is not defined + +- name: Restrict permission on db pass file + file: path=/etc/grafana/ansible_db_pass mode=600 + when: + - grafana_db_type == 'mysql' or grafana_db_type == 'postgres' + - grafana_db_pass is not defined + +- name: Read db password + command: cat /etc/grafana/ansible_db_pass + register: grafana_rand_db_pass + when: + - grafana_db_type == 'mysql' or grafana_db_type == 'postgres' + - grafana_db_pass is not defined + +- name: Set db pass + set_fact: grafana_db_pass={{ grafana_rand_db_pass.stdout }} + when: + - grafana_db_type == 'mysql' or grafana_db_type == 'postgres' + - grafana_db_pass is not defined + +- name: Create MySQL database + mysql_db: + name: "{{ grafana_db_name }}" + state: present + login_host: "{{ grafana_db_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + when: grafana_db_type == 'mysql' + +- name: Create MySQL User + mysql_user: + name: "{{ grafana_db_user | default('grafana') }}" + password: "{{ grafana_db_pass }}" + priv: "{{ grafana_db_name | default('grafana') }}.*:ALL" + host: "{{ (grafana_db_server == 'localhost') | ternary('localhost', item) }}" + login_host: "{{ grafana_db_server }}" + login_user: sqladmin + login_password: "{{ mysql_admin_pass }}" + state: present + when: grafana_db_type == 'mysql' + with_items: "{{ ansible_all_ipv4_addresses }}" + +- 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 }}" + when: grafana_db_type == 'postgres' + +- 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 }}" + when: grafana_db_type == 'postgres' + +- name: Generate a secret key + shell: ?@[\]^_`|~' | head -c 50 > /etc/grafana/ansible_secret_key + args: + creates: /etc/grafana/ansible_secret_key + +- name: Restrict permission on the secret key file + file: path=/etc/grafana/ansible_secret_key mode=600 + +- name: Read the secret key + command: cat /etc/grafana/ansible_secret_key + register: grafana_secret_key + changed_when: False + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- name: Start and enable the service + service: name=grafana-server state=started enabled=yes + +- 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 diff --git a/roles/grafana/templates/grafana.ini.j2 b/roles/grafana/templates/grafana.ini.j2 new file mode 100644 index 0000000..e92e989 --- /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.stdout }} + +[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..efcfa35 --- /dev/null +++ b/roles/graylog/defaults/main.yml @@ -0,0 +1,63 @@ +--- + +graylog_version: 3.2.4 +graylog_archive_url: https://downloads.graylog.org/releases/graylog/graylog-{{ graylog_version }}.tgz +graylog_archive_sha1: 898e1c2665b94adff1b51baa690e48022b8abc5e +graylog_root_dir: /opt/graylog +graylog_manage_upgrade: True + +graylog_is_master: True + +# Additional libs to download +graylog_libs: + log4j-systemd-journal-appender: + version: 2.4.0 + sha1: a23b5c723712bfcf41cc3d962ea383c14b1a4532 + url: https://repo1.maven.org/maven2/de/bwaldvogel/log4j-systemd-journal-appender/2.4.0/log4j-systemd-journal-appender-2.4.0.jar + +graylog_plugins: + auth-sso: + version: 3.2.1 + sha1: e0e96424b763c635a1d5456a534704711fb2bbe2 + url: https://github.com/Graylog2/graylog-plugin-auth-sso/releases/download/3.2.1/graylog-plugin-auth-sso-3.2.1.jar + dnsresolver: + version: 1.2.0 + sha1: b470bd4b39a22574527e01a943a601c10cc2520b + url: https://github.com/graylog-labs/graylog-plugin-dnsresolver/releases/download/1.2.0/graylog-plugin-dnsresolver-1.2.0.jar + +# Plugins bundled, which should not be removed +graylog_plugins_core: + - aws + - collector + - threatintel +graylog_plugins_to_install: + - auth-sso + +# 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_mongodb_uri: + - mongodb://localhost/graylog + +# 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 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..3d532b1 --- /dev/null +++ b/roles/graylog/meta/main.yml @@ -0,0 +1,5 @@ +--- + +dependencies: + - 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..5133f15 --- /dev/null +++ b/roles/graylog/tasks/archive_pre.yml @@ -0,0 +1,18 @@ +--- + +- 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 + command: mongodump --quiet --out {{ graylog_root_dir }}/archives/{{ graylog_current_version }}/mongo --uri {{ graylog_mongodb_uri[0] }} + 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..66cbae6 --- /dev/null +++ b/roles/graylog/tasks/conf.yml @@ -0,0 +1,47 @@ +--- + +- 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 + +- 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: 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/directories.yml b/roles/graylog/tasks/directories.yml new file mode 100644 index 0000000..4c5a37b --- /dev/null +++ b/roles/graylog/tasks/directories.yml @@ -0,0 +1,33 @@ +--- + +- 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 + tags: graylog diff --git a/roles/graylog/tasks/facts.yml b/roles/graylog/tasks/facts.yml new file mode 100644 index 0000000..184166a --- /dev/null +++ b/roles/graylog/tasks/facts.yml @@ -0,0 +1,13 @@ +--- + +# 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 diff --git a/roles/graylog/tasks/install.yml b/roles/graylog/tasks/install.yml new file mode 100644 index 0000000..328bfee --- /dev/null +++ b/roles/graylog/tasks/install.yml @@ -0,0 +1,104 @@ +--- + +- 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' + tags: graylog + +- name: Install external libs + get_url: + url: "{{ graylog_libs[item].url }}" + dest: /opt/graylog/app/lib + checksum: sha1:{{ graylog_libs[item].sha1 }} + loop: "{{ graylog_libs.keys() | list }}" + notify: restart graylog-server + tags: graylog + +- name: Find existing libs + shell: find {{ graylog_root_dir }}/app/lib -type f -name \*.jar + register: graylog_libs_installed + changed_when: False + 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 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..1d80c5b --- /dev/null +++ b/roles/graylog/tasks/main.yml @@ -0,0 +1,15 @@ +--- + +- 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' 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/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..9ab0f5c --- /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 \ + -Xms1g -Xmx1g -XX:NewRatio=1 -server -XX:+ResizeTLAB \ + -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \ + -XX:+CMSClassUnloadingEnabled -XX:-OmitStackTraceInFastThrow \ + -cp {{ graylog_root_dir }}/app/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={{ graylog_root_dir }}/app/lib/sigar \ + org.graylog2.bootstrap.Main 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..120ba8b --- /dev/null +++ b/roles/graylog/templates/log4j2.xml.j2 @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roles/graylog/templates/post-backup.j2 b/roles/graylog/templates/post-backup.j2 new file mode 100644 index 0000000..23f4460 --- /dev/null +++ b/roles/graylog/templates/post-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +rm -rf {{ graylog_root_dir }}/dumps/{mongo,es}/* diff --git a/roles/graylog/templates/pre-backup.j2 b/roles/graylog/templates/pre-backup.j2 new file mode 100644 index 0000000..033bde3 --- /dev/null +++ b/roles/graylog/templates/pre-backup.j2 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +mongodump --quiet --out {{ graylog_root_dir }}/dumps/mongo --uri {{ graylog_mongodb_uri[0] }} diff --git a/roles/graylog/templates/server.conf.j2 b/roles/graylog/templates/server.conf.j2 new file mode 100644 index 0000000..2c648d3 --- /dev/null +++ b/roles/graylog/templates/server.conf.j2 @@ -0,0 +1,45 @@ +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') }} +mongodb_uri = {{ graylog_mongodb_uri | join(',') }} +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 = /usr/share/graylog-server/bin +data_dir = {{ graylog_root_dir }}/data +plugin_dir = /usr/share/graylog-server/plugin +message_journal_dir = {{ graylog_root_dir }}/data/journal + +allow_leading_wildcard_searches = true + +{% if 'dnsresolver' in graylog_plugins_to_install %} +dns_resolver_enabled = true +{% endif %} diff --git a/roles/httpd_common/defaults/main.yml b/roles/httpd_common/defaults/main.yml new file mode 100644 index 0000000..e8d5fec --- /dev/null +++ b/roles/httpd_common/defaults/main.yml @@ -0,0 +1,66 @@ +--- +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 + - 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..27bcb40 --- /dev/null +++ b/roles/httpd_common/tasks/main.yml @@ -0,0 +1,155 @@ +--- + +- name: Install packages + yum: + name: + - httpd + - mod_fcgid + - policycoreutils-python + - python-passlib + tags: [package,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: 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 + 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/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_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