Merge branch 'master' into framadate
						commit
						a4e09e92f9
					
				
							
								
								
									
										25
									
								
								ansible.cfg
								
								
								
								
							
							
						
						
									
										25
									
								
								ansible.cfg
								
								
								
								
							| 
						 | 
				
			
			@ -2,6 +2,10 @@
 | 
			
		|||
 | 
			
		||||
[defaults]
 | 
			
		||||
 | 
			
		||||
# Explicitely redefined some defaults to make play execution work
 | 
			
		||||
roles_path = ./roles
 | 
			
		||||
vars_plugins = ./vars_plugins
 | 
			
		||||
 | 
			
		||||
# Do not create .retry files
 | 
			
		||||
retry_files_enabled = False
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +25,12 @@ forks = 15
 | 
			
		|||
# Some SSH connection will take time
 | 
			
		||||
timeout = 60
 | 
			
		||||
 | 
			
		||||
# Enable fact_caching
 | 
			
		||||
gathering = smart
 | 
			
		||||
fact_caching = jsonfile
 | 
			
		||||
fact_caching_connection = ~/.cache/ansible/json/
 | 
			
		||||
fact_caching_timeout = 86400
 | 
			
		||||
 | 
			
		||||
[privilege_escalation]
 | 
			
		||||
 | 
			
		||||
# Use sudo to get priviledge access
 | 
			
		||||
| 
						 | 
				
			
			@ -45,3 +55,18 @@ api_hostname = intranet.crans.org
 | 
			
		|||
 | 
			
		||||
# Whether or not using vault_cranspasswords
 | 
			
		||||
use_cpasswords = True
 | 
			
		||||
 | 
			
		||||
# Specify cache plugin for re2o API. By default, cache nothing
 | 
			
		||||
cache = jsonfile
 | 
			
		||||
 | 
			
		||||
# Only used for memcached plugin
 | 
			
		||||
# List of connection information for the memcached DBs
 | 
			
		||||
# Default is ['127.0.0.1:11211']
 | 
			
		||||
# memcached_connection = ['127.0.0.1:11211']
 | 
			
		||||
 | 
			
		||||
# Time in second before the cache expired. 0 means never expire cache.
 | 
			
		||||
# Default is 24 hours.
 | 
			
		||||
timeout = 86400
 | 
			
		||||
 | 
			
		||||
# Default is 12 hours.
 | 
			
		||||
timeout_token = 43200
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										50
									
								
								base.yml
								
								
								
								
							
							
						
						
									
										50
									
								
								base.yml
								
								
								
								
							| 
						 | 
				
			
			@ -35,9 +35,6 @@
 | 
			
		|||
    # Scripts will tell users to go there to manage their account
 | 
			
		||||
    intranet_url: 'https://intranet.crans.org/'
 | 
			
		||||
 | 
			
		||||
    # Backup password
 | 
			
		||||
    backuppc_rsyncd_passwd: "{{ vault_backuppc_rsyncd_passwd }}"
 | 
			
		||||
 | 
			
		||||
    # Will be in /usr/scripts/
 | 
			
		||||
    crans_scripts_git: "http://gitlab.adm.crans.org/nounous/scripts.git"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -51,46 +48,13 @@
 | 
			
		|||
    - ldap-client
 | 
			
		||||
    - openssh
 | 
			
		||||
    - sudo
 | 
			
		||||
    - rsync-client
 | 
			
		||||
    - ntp-client
 | 
			
		||||
    - crans-scripts
 | 
			
		||||
 | 
			
		||||
# Deploy NFS only on campus
 | 
			
		||||
- hosts: crans_server
 | 
			
		||||
  roles:
 | 
			
		||||
    - nfs-common
 | 
			
		||||
 | 
			
		||||
# Deploy LDAP replica
 | 
			
		||||
- hosts: odlyd.adm.crans.org,soyouz.adm.crans.org,fy.adm.crans.org,thot.adm.crans.org
 | 
			
		||||
  roles: []  # TODO
 | 
			
		||||
 | 
			
		||||
# Playbook to deploy autofs NFS
 | 
			
		||||
- hosts: crans_server,!odlyd.adm.crans.org,!zamok.adm.crans.org,!omnomnom.adm.crans.org,!owl.adm.crans.org,!owncloud-srv.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - nfs-autofs
 | 
			
		||||
 | 
			
		||||
# Deploy home permanent
 | 
			
		||||
- hosts: zamok.adm.crans.org,omnomnom.adm.crans.org,owl.adm.crans.org,owncloud-srv.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - home-permanent
 | 
			
		||||
 | 
			
		||||
# Redirect local mail to mailserver
 | 
			
		||||
- hosts: crans_server,test_vm,!redisdead.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    mail_root: root@crans.org
 | 
			
		||||
    mail_snmp_server: smtp.adm.crans.org
 | 
			
		||||
    mail_defaulthost: crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - nullmailer
 | 
			
		||||
 | 
			
		||||
# Send logs to thot
 | 
			
		||||
- hosts: server,!thot.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    rsyslog:
 | 
			
		||||
      server: thot.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - rsyslog-client
 | 
			
		||||
 | 
			
		||||
- hosts: otis.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - ansible
 | 
			
		||||
| 
						 | 
				
			
			@ -99,3 +63,17 @@
 | 
			
		|||
- hosts: zamok.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - zamok-tools
 | 
			
		||||
 | 
			
		||||
- import_playbook: plays/mail.yml
 | 
			
		||||
- import_playbook: plays/nfs.yml
 | 
			
		||||
- import_playbook: plays/logs.yml
 | 
			
		||||
- import_playbook: plays/backup.yml
 | 
			
		||||
- import_playbook: plays/network-interfaces.yml
 | 
			
		||||
- import_playbook: plays/monitoring.yml
 | 
			
		||||
 | 
			
		||||
# Services that only apply to a subset of server
 | 
			
		||||
- import_playbook: plays/tv.yml
 | 
			
		||||
- import_playbook: plays/mailman.yml
 | 
			
		||||
- import_playbook: plays/dhcp.yml
 | 
			
		||||
- import_playbook: plays/dns.yml
 | 
			
		||||
- import_playbook: plays/wireguard.yml
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@
 | 
			
		|||
      apt:
 | 
			
		||||
        state: absent
 | 
			
		||||
        name:
 | 
			
		||||
          - at
 | 
			
		||||
          - arpwatch  # old sniffing
 | 
			
		||||
          - collectd
 | 
			
		||||
          - collectd-utils  # old monitoring
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,7 @@
 | 
			
		|||
          - monitoring-plugins-standard
 | 
			
		||||
          - monitoring-plugins-basic
 | 
			
		||||
          - monitoring-plugins-common
 | 
			
		||||
          - monit
 | 
			
		||||
          - libmonitoring-plugin-perl
 | 
			
		||||
          - snmp
 | 
			
		||||
          - nagios-plugins-contrib
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +66,9 @@
 | 
			
		|||
        path: "{{ item }}"
 | 
			
		||||
        state: absent
 | 
			
		||||
      loop:
 | 
			
		||||
        - /etc/bcfg2.conf
 | 
			
		||||
        - /etc/bcfg2.conf.ucf-dist
 | 
			
		||||
        - /etc/crans
 | 
			
		||||
        - /etc/cron.d/munin-crans
 | 
			
		||||
        - /etc/cron.d/munin-node
 | 
			
		||||
        - /etc/cron.d/munin-node.dpkg-dist
 | 
			
		||||
| 
						 | 
				
			
			@ -76,15 +81,31 @@
 | 
			
		|||
        - /etc/cron.d/autobcfg2
 | 
			
		||||
        - /etc/cron.d/bcfg2-run
 | 
			
		||||
        - /etc/cron.d/pull-repos-scripts
 | 
			
		||||
        - /etc/default/bcfg2
 | 
			
		||||
        - /etc/default/bcfg2.ucf-dist
 | 
			
		||||
        - /etc/munin
 | 
			
		||||
        - /etc/icinga2
 | 
			
		||||
        - /etc/init.d/bcfg2
 | 
			
		||||
        - /etc/nagios
 | 
			
		||||
        - /etc/nagios-plugins
 | 
			
		||||
        - /etc/nut
 | 
			
		||||
        - /etc/nginx/sites-enabled/status
 | 
			
		||||
        - /etc/nginx/sites-available/status
 | 
			
		||||
        - /etc/pnp4nagios
 | 
			
		||||
        - /var/local/aptdater
 | 
			
		||||
        - /etc/apt-dater-host.conf
 | 
			
		||||
        - /etc/sudoers.d/apt-dater-host
 | 
			
		||||
 | 
			
		||||
        - /etc/apt/apt.conf.d/70debconf
 | 
			
		||||
        - /etc/apt/apt.conf.d/01aptitude
 | 
			
		||||
        - /etc/cron.weekly/git_dirty_repo
 | 
			
		||||
        - /etc/cron.daily/git_dirty_repo
 | 
			
		||||
        - /etc/cron.hourly/bcfg2
 | 
			
		||||
        - /etc/cron.d/letsencrypt_check_cert
 | 
			
		||||
        - /etc/nss-ldapd.conf
 | 
			
		||||
        - /etc/cron.daily/bcfg2
 | 
			
		||||
        - /etc/monit
 | 
			
		||||
        - /etc/ldap/ldap.conf
 | 
			
		||||
        - /etc/letsencrypt/conf.d/localhost.ini
 | 
			
		||||
#    - name: Upgrade
 | 
			
		||||
#      apt:
 | 
			
		||||
#        upgrade: dist
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
# Custom header
 | 
			
		||||
dirty: "{{lookup('pipe', 'git diff --quiet || echo dirty')}}"
 | 
			
		||||
ansible_header: |
 | 
			
		||||
    +++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
| 
						 | 
				
			
			@ -11,3 +12,6 @@ ansible_header: |
 | 
			
		|||
       {% endif %}
 | 
			
		||||
 | 
			
		||||
    +++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
# Crans subnets
 | 
			
		||||
adm_subnet: 10.231.136.0/24
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
---
 | 
			
		||||
postfix:
 | 
			
		||||
  primary: false
 | 
			
		||||
  secondary: true
 | 
			
		||||
  public: true
 | 
			
		||||
  dkim: true
 | 
			
		||||
  mailman: false
 | 
			
		||||
  titanic: true
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
---
 | 
			
		||||
postfix:
 | 
			
		||||
  primary: true
 | 
			
		||||
  secondary: false
 | 
			
		||||
  public: true
 | 
			
		||||
  dkim: true
 | 
			
		||||
  mailman: true
 | 
			
		||||
  titanic: false
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
---
 | 
			
		||||
postfix:
 | 
			
		||||
  primary: false
 | 
			
		||||
  secondary: true
 | 
			
		||||
  public: true
 | 
			
		||||
  dkim: true
 | 
			
		||||
  mailman: false
 | 
			
		||||
  titanic: false
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
---
 | 
			
		||||
postfix:
 | 
			
		||||
  primary: false
 | 
			
		||||
  secondary: true
 | 
			
		||||
  public: true
 | 
			
		||||
  dkim: true
 | 
			
		||||
  mailman: false
 | 
			
		||||
  titanic: true
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 10 KiB  | 
| 
						 | 
				
			
			@ -7,6 +7,8 @@ For a detailed example look at https://github.com/ansible/ansible/blob/3dbf89e8a
 | 
			
		|||
The API Client has been adapted from https://gitlab.federez.net/re2o/re2oapi
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from ansible.plugins.loader import cache_loader
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import datetime
 | 
			
		||||
import requests
 | 
			
		||||
| 
						 | 
				
			
			@ -28,38 +30,67 @@ from ansible.config.manager import ConfigManager
 | 
			
		|||
# Ansible Logger to stdout
 | 
			
		||||
display = Display()
 | 
			
		||||
 | 
			
		||||
# Number of seconds before expiration where renewing the token is done
 | 
			
		||||
TIME_FOR_RENEW = 120
 | 
			
		||||
# Default name of the file to store tokens. Path $HOME/{DEFAUlt_TOKEN_FILENAME}
 | 
			
		||||
DEFAULT_TOKEN_FILENAME = '.re2o.token'
 | 
			
		||||
 | 
			
		||||
# If no plugin is used, then use this as token timeout.
 | 
			
		||||
# Overriden by key timeout_token from ansible configuration.
 | 
			
		||||
TIME_FOR_RENEW = 43200 # 12 jours
 | 
			
		||||
 | 
			
		||||
class Client:
 | 
			
		||||
    """
 | 
			
		||||
    Class based client to contact re2o API.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, hostname, username, password, use_tls=True):
 | 
			
		||||
    def __init__(self, hostname, username, password,
 | 
			
		||||
                 use_tls=True, cachetoken=None):
 | 
			
		||||
        """
 | 
			
		||||
        :arg hostname: The hostname of the Re2o instance to use.
 | 
			
		||||
        :arg username: The username to use.
 | 
			
		||||
        :arg password: The password to use.
 | 
			
		||||
        :arg use_tls: A boolean to specify whether the client should use a
 | 
			
		||||
                      a TLS connection. Default is True. Please, keep it.
 | 
			
		||||
        :arg cachetoken: The cache to use to manage authentication token.
 | 
			
		||||
                         If it is None, then store the token in a file.
 | 
			
		||||
        """
 | 
			
		||||
        self.use_tls = use_tls
 | 
			
		||||
        self.hostname = hostname
 | 
			
		||||
        self._username = username
 | 
			
		||||
        self._password = password
 | 
			
		||||
 | 
			
		||||
        self.token_file = Path.home() / DEFAULT_TOKEN_FILENAME
 | 
			
		||||
        self._cachetoken = cachetoken
 | 
			
		||||
        self.token_file = None
 | 
			
		||||
        if self._cachetoken is None:
 | 
			
		||||
            self.token_file = Path.home() / DEFAULT_TOKEN_FILENAME
 | 
			
		||||
            display.vvv("Setting token file to {}".format(self.token_file))
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                display.vvv("Using {} as cache plugin"
 | 
			
		||||
                            .format(self._cachetoken.plugin_name))
 | 
			
		||||
            except AttributeError:
 | 
			
		||||
                # Happens when plugin_name is not implemented...
 | 
			
		||||
                # For example with memcached
 | 
			
		||||
                display.vvv("Using cache plugin specified in configuration.")
 | 
			
		||||
 | 
			
		||||
        display.v("Connecting to {hostname} as user {user}".format(
 | 
			
		||||
            hostname=to_native(self.hostname), user=to_native(self._username)))
 | 
			
		||||
        try:
 | 
			
		||||
            self.token = self._get_token_from_file()
 | 
			
		||||
        except AnsibleFileNotFound:
 | 
			
		||||
            display.vv("Force renew the token")
 | 
			
		||||
            self._force_renew_token()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def token(self):
 | 
			
		||||
        if self._cachetoken:
 | 
			
		||||
            display.vvv("Trying to get token from cache.")
 | 
			
		||||
            if self._cachetoken.contains("auth_token"):
 | 
			
		||||
                display.vvv("Found token in cache.")
 | 
			
		||||
                return self._cachetoken.get("auth_token")
 | 
			
		||||
            else:
 | 
			
		||||
                display.vvv("Token not found. Forcing renew.")
 | 
			
		||||
                return self._force_renew_token()
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                token = self._get_token_from_file()
 | 
			
		||||
                if token['expiration'] < datetime.datetime.now() + \
 | 
			
		||||
                   datetime.timedelta(seconds=TIME_FOR_RENEW):
 | 
			
		||||
                    return self._force_renew_token()
 | 
			
		||||
            except AnsibleError:
 | 
			
		||||
                return self._force_renew_token()
 | 
			
		||||
 | 
			
		||||
    def _get_token_from_file(self):
 | 
			
		||||
        display.vv("Trying to fetch token from {}".format(self.token_file))
 | 
			
		||||
| 
						 | 
				
			
			@ -93,13 +124,18 @@ class Client:
 | 
			
		|||
                                              )
 | 
			
		||||
                                     )
 | 
			
		||||
        else:
 | 
			
		||||
            display.vv("""Token successfully retreived from
 | 
			
		||||
            file {token}""".format(token=self.token_file))
 | 
			
		||||
            display.vv("Token successfully retreived from "
 | 
			
		||||
                       "file {token}".format(token=self.token_file))
 | 
			
		||||
            return ret
 | 
			
		||||
 | 
			
		||||
    def _force_renew_token(self):
 | 
			
		||||
        self.token = self._get_token_from_server()
 | 
			
		||||
        self._save_token_to_file()
 | 
			
		||||
        token = self._get_token_from_server()
 | 
			
		||||
        if self._cachetoken:
 | 
			
		||||
            display.vvv("Storing authentication token in cache")
 | 
			
		||||
            self._cachetoken.set("auth_token", token.get('token'))
 | 
			
		||||
        else:
 | 
			
		||||
            self._save_token_to_file(token)
 | 
			
		||||
        return token.get('token')
 | 
			
		||||
 | 
			
		||||
    def _get_token_from_server(self):
 | 
			
		||||
        display.vv("Requesting a new token for {user}@{host}".format(
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +175,7 @@ class Client:
 | 
			
		|||
    def _parse_date(self, date, date_format="%Y-%m-%dT%H:%M:%S"):
 | 
			
		||||
        return datetime.datetime.strptime(date.split('.')[0], date_format)
 | 
			
		||||
 | 
			
		||||
    def _save_token_to_file(self):
 | 
			
		||||
    def _save_token_to_file(self, token):
 | 
			
		||||
        display.vv("Saving token to file {}".format(self.token_file))
 | 
			
		||||
        try:
 | 
			
		||||
            # Read previous data to avoid erasures
 | 
			
		||||
| 
						 | 
				
			
			@ -153,8 +189,8 @@ class Client:
 | 
			
		|||
        if self.hostname not in data.keys():
 | 
			
		||||
            data[self.hostname] = {}
 | 
			
		||||
        data[self.hostname][self._username] = {
 | 
			
		||||
            'token': self.token['token'],
 | 
			
		||||
            'expiration': self.token['expiration'].isoformat(),
 | 
			
		||||
            'token': token['token'],
 | 
			
		||||
            'expiration': token['expiration'].isoformat(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
| 
						 | 
				
			
			@ -169,22 +205,6 @@ class Client:
 | 
			
		|||
            display.vv("Token successfully written to file {}"
 | 
			
		||||
                       .format(self.token_file))
 | 
			
		||||
 | 
			
		||||
    def get_token(self):
 | 
			
		||||
        """
 | 
			
		||||
        Retrieves the token to use for the current connection.
 | 
			
		||||
        Automatically renewed if needed.
 | 
			
		||||
        """
 | 
			
		||||
        if self.need_renew_token:
 | 
			
		||||
            self._force_renew_token()
 | 
			
		||||
 | 
			
		||||
        return self.token['token']
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def need_renew_token(self):
 | 
			
		||||
        return self.token['expiration'] < \
 | 
			
		||||
            datetime.datetime.now() + \
 | 
			
		||||
            datetime.timedelta(seconds=TIME_FOR_RENEW)
 | 
			
		||||
 | 
			
		||||
    def _request(self, method, url, headers={}, params={}, *args, **kwargs):
 | 
			
		||||
        display.vv("Building the {method} request to {url}.".format(
 | 
			
		||||
            method=method.upper(),
 | 
			
		||||
| 
						 | 
				
			
			@ -192,9 +212,9 @@ class Client:
 | 
			
		|||
        ))
 | 
			
		||||
 | 
			
		||||
        # Force the 'Authorization' field with the right token.
 | 
			
		||||
        display.vvv("Forcing authentication token.")
 | 
			
		||||
        display.vvv("Forcing authentication token in headers.")
 | 
			
		||||
        headers.update({
 | 
			
		||||
            'Authorization': 'Token {}'.format(self.get_token())
 | 
			
		||||
            'Authorization': 'Token {}'.format(self.token)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # Use a json format unless the user already specified something
 | 
			
		||||
| 
						 | 
				
			
			@ -213,10 +233,10 @@ class Client:
 | 
			
		|||
            # Force re-login to the server (case of a wrong token but valid
 | 
			
		||||
            # credentials) and then retry the request without catching errors.
 | 
			
		||||
            display.vv("Token refused. Trying to refresh the token.")
 | 
			
		||||
            self._force_renew_token()
 | 
			
		||||
            token = self._force_renew_token()
 | 
			
		||||
 | 
			
		||||
            headers.update({
 | 
			
		||||
                'Authorization': 'Token {}'.format(self.get_token())
 | 
			
		||||
                'Authorization': 'Token {}'.format(token)
 | 
			
		||||
            })
 | 
			
		||||
            display.vv("Re-performing the request {method} {url}".format(
 | 
			
		||||
                method=method.upper(),
 | 
			
		||||
| 
						 | 
				
			
			@ -320,6 +340,18 @@ class LookupModule(LookupBase):
 | 
			
		|||
                              Queries the re2o API and returns the list of
 | 
			
		||||
                              all machines whose role_type is role_name.
 | 
			
		||||
 | 
			
		||||
       - cidrs, a list of subnet_names: Will get back the list of all cidrs
 | 
			
		||||
                                        corresponding to this particular
 | 
			
		||||
                                        subnet.
 | 
			
		||||
 | 
			
		||||
       - prefixv6, a list of subnet_names: Will get back the list of all ipv6
 | 
			
		||||
                                           prefixes corresponding to this
 | 
			
		||||
                                           particular subnet.
 | 
			
		||||
 | 
			
		||||
       - A simple endpoint: Will make a raw query to the API using this
 | 
			
		||||
                            this endpoint.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    If a term is not in the previous list, make a raw query to the API
 | 
			
		||||
    with endpoint term.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -338,8 +370,135 @@ class LookupModule(LookupBase):
 | 
			
		|||
        dnszones: "{{ lookup('re2oapi', 'dnszones') }}"
 | 
			
		||||
      tasks:
 | 
			
		||||
        - debug: var=dnszones
 | 
			
		||||
 | 
			
		||||
    The following play will use the debug module to output
 | 
			
		||||
    all the ipv6 corresponding to adherents and adm networks
 | 
			
		||||
 | 
			
		||||
    - hosts: sputnik.adm.crans.org
 | 
			
		||||
      vars:
 | 
			
		||||
        prefixv6: "{{ lookup('re2oapi', 'previxv6', 'adherents', 'adm') }}"
 | 
			
		||||
      tasks:
 | 
			
		||||
        - debug:
 | 
			
		||||
            msg: "{{ prefixv6 | ipwrap }}"
 | 
			
		||||
 | 
			
		||||
    The following will get the ip addresses of all servers with role
 | 
			
		||||
    dns-authorithary-master on vlan 2.
 | 
			
		||||
 | 
			
		||||
    - hosts: sputnik.adm.crans.org
 | 
			
		||||
      vars:
 | 
			
		||||
        bind:
 | 
			
		||||
          masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
 | 
			
		||||
      tasks:
 | 
			
		||||
        - name: Display ipv6
 | 
			
		||||
          debug:
 | 
			
		||||
            ipv6: "{{ masters | json_query('servers[].interface[?vlan_id==`2`].ipv6[][].ipv6') }}"
 | 
			
		||||
 | 
			
		||||
        - name: Display ipv4
 | 
			
		||||
          debug:
 | 
			
		||||
            ipv4: "{{ masters | json_query('servers[].interface[?vlan_id==`2`].ipv4[]') }}"
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def _readconfig(self, section="re2o", key=None, default=None,
 | 
			
		||||
                    boolean=False, integer=False):
 | 
			
		||||
        config = self._config
 | 
			
		||||
        if not config:
 | 
			
		||||
            return default
 | 
			
		||||
        else:
 | 
			
		||||
            if config.has_option(section, key):
 | 
			
		||||
                display.vvv("Found key {} in configuration file".format(key))
 | 
			
		||||
                if boolean:
 | 
			
		||||
                    return config.getboolean(section, key)
 | 
			
		||||
                elif integer:
 | 
			
		||||
                    return config.getint(section, key)
 | 
			
		||||
                else:
 | 
			
		||||
                    return config.get(section, key)
 | 
			
		||||
            else:
 | 
			
		||||
                return default
 | 
			
		||||
 | 
			
		||||
    def _manage_cachedir(self, cachedir=None, plugin=None):
 | 
			
		||||
        try:
 | 
			
		||||
            self._uri = cachedir / plugin
 | 
			
		||||
        except Exception:
 | 
			
		||||
            raise AnsibleError("Undefined specification for cache plugin")
 | 
			
		||||
 | 
			
		||||
        display.vvv("Cache directory is {}".format(self._uri))
 | 
			
		||||
        if not self._uri.exists():
 | 
			
		||||
            # Creates Ansible cache directory with right permissions
 | 
			
		||||
            # if it doesn't exist yet.
 | 
			
		||||
            display.vvv("Cache directory doesn't exist. Creating it.")
 | 
			
		||||
            try:
 | 
			
		||||
                self._uri.mkdir(mode=0o700, parents=True)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                raise AnsibleError("""Unable to create {dir}.
 | 
			
		||||
                Original error was : {err}""".format(dir=self._uri,
 | 
			
		||||
                                                     err=to_native(e)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        config_manager = ConfigManager()
 | 
			
		||||
        config_file = config_manager.data.get_setting(name="CONFIG_FILE").value
 | 
			
		||||
        self._config = ConfigParser()
 | 
			
		||||
        self._config.read(config_file)
 | 
			
		||||
 | 
			
		||||
        display.vvv("Using {} as configuration file.".format(config_file))
 | 
			
		||||
 | 
			
		||||
        self._api_hostname = None
 | 
			
		||||
        self._api_username = None
 | 
			
		||||
        self._api_password = None
 | 
			
		||||
        self._use_cpasswords = None
 | 
			
		||||
        self._cache_plugin = None
 | 
			
		||||
        self._cache = None
 | 
			
		||||
        self._timeout = 86400  # 1 day
 | 
			
		||||
        self._cachetoken = None
 | 
			
		||||
        self._timeouttoken = TIME_FOR_RENEW  # 12 hours
 | 
			
		||||
 | 
			
		||||
        if self._config.has_section("re2o"):
 | 
			
		||||
            display.vvv("Found section re2o in configuration file")
 | 
			
		||||
 | 
			
		||||
            self._api_hostname = self._readconfig(key="api_hostname")
 | 
			
		||||
            self._use_cpasswords = self._readconfig(key="use_cpasswords",
 | 
			
		||||
                                                    boolean=True)
 | 
			
		||||
            self._cache_plugin = self._readconfig(key="cache")
 | 
			
		||||
            self._timeout = self._readconfig(key="timeout", integer=True,
 | 
			
		||||
                                             default=86400)
 | 
			
		||||
            self._timeouttoken = self._readconfig(key="timeout_token",
 | 
			
		||||
                                                  integer=True,
 | 
			
		||||
                                                  default=TIME_FOR_RENEW)
 | 
			
		||||
 | 
			
		||||
        if self._cache_plugin is not None:
 | 
			
		||||
            display.vvv("Using {} as cache plugin".format(self._cache_plugin))
 | 
			
		||||
            cachedir = Path.home() / ".cache/ansible/re2oapi"
 | 
			
		||||
 | 
			
		||||
            if self._cache_plugin == 'jsonfile':
 | 
			
		||||
                self._manage_cachedir(cachedir=cachedir, plugin='json')
 | 
			
		||||
            elif self._cache_plugin == 'yaml':
 | 
			
		||||
                self._manage_cachedir(cachedir=cachedir, plugin='yaml')
 | 
			
		||||
            elif self._cache_plugin == 'pickle':
 | 
			
		||||
                self._manage_cachedir(cachedir=cachedir, plugin='pickle')
 | 
			
		||||
            elif self._cache_plugin == 'memcached':
 | 
			
		||||
                # requires packages python3-memcache and memcached
 | 
			
		||||
                display.vvvv("Please make sure you have installed packages"
 | 
			
		||||
                             "python3-memcache and memcached"
 | 
			
		||||
                             )
 | 
			
		||||
                self._uri = self._readconfig(key='memcached_connection',
 | 
			
		||||
                                                  default=['127.0.0.1:11211'],
 | 
			
		||||
                                                  )
 | 
			
		||||
            else:
 | 
			
		||||
                raise AnsibleError("Cache plugin {} not supported"
 | 
			
		||||
                                   .format(self._cache_plugin))
 | 
			
		||||
 | 
			
		||||
            self._cache = cache_loader.get(self._cache_plugin,
 | 
			
		||||
                                           _uri=self._uri,
 | 
			
		||||
                                           _timeout=self._timeout,
 | 
			
		||||
                                           )
 | 
			
		||||
            self._cachetoken = cache_loader.get(self._cache_plugin,
 | 
			
		||||
                                                _uri=self._uri,
 | 
			
		||||
                                                _timeout=self._timeouttoken,
 | 
			
		||||
                                                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def run(self, terms, variables=None, api_hostname=None, api_username=None,
 | 
			
		||||
            api_password=None, use_tls=True):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -354,33 +513,20 @@ class LookupModule(LookupBase):
 | 
			
		|||
           :returns: A list of results to the specific queries.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        config_manager = ConfigManager()
 | 
			
		||||
        config_file = config_manager.data.get_setting(name="CONFIG_FILE").value
 | 
			
		||||
        config = ConfigParser()
 | 
			
		||||
        config.read(config_file)
 | 
			
		||||
        # Use the hostname specified by the user if it exists.
 | 
			
		||||
        if api_hostname is not None:
 | 
			
		||||
            display.vvv("Overriding api_hostname with {}".format(api_hostname))
 | 
			
		||||
        else:
 | 
			
		||||
            api_hostname = self._api_hostname
 | 
			
		||||
 | 
			
		||||
        use_cpasswords = False
 | 
			
		||||
 | 
			
		||||
        if config.has_section("re2o"):
 | 
			
		||||
            display.vvv("Found section re2o in configuration file")
 | 
			
		||||
            if config.has_option("re2o", "api_hostname"):
 | 
			
		||||
                display.vvv("Found option api_hostname in config file")
 | 
			
		||||
                api_hostname = config.get("re2o", "api_hostname")
 | 
			
		||||
                display.vvv("Override api_hostname with {} from configuration"
 | 
			
		||||
                            .format(api_hostname))
 | 
			
		||||
            if config.has_option("re2o", "use_cpasswords"):
 | 
			
		||||
                display.vvv("Found option use_cpasswords in config file")
 | 
			
		||||
                use_cpasswords = config.getboolean("re2o", "use_cpasswords")
 | 
			
		||||
                display.vvv("Override api_hostname with {} from configuration"
 | 
			
		||||
                            .format(use_cpasswords))
 | 
			
		||||
 | 
			
		||||
        if api_hostname is None:
 | 
			
		||||
        if self._api_hostname is None:
 | 
			
		||||
            raise AnsibleError(to_native(
 | 
			
		||||
                'You must specify a hostname to contact re2oAPI'
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
        if api_username is None and api_password is None and use_cpasswords:
 | 
			
		||||
            display.vvv("Use cpasswords vault to get API credentials.")
 | 
			
		||||
        if (api_username is None and api_password is None
 | 
			
		||||
                and self._use_cpasswords):
 | 
			
		||||
            display.vvv("Using cpasswords vault to get API credentials.")
 | 
			
		||||
            api_username = variables.get('vault_re2o_service_user')
 | 
			
		||||
            api_password = variables.get('vault_re2o_service_password')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -394,12 +540,12 @@ class LookupModule(LookupBase):
 | 
			
		|||
                'You must specify a valid password to connect to re2oAPI'
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
        api_client = Client(api_hostname, api_username,
 | 
			
		||||
                            api_password, use_tls=True)
 | 
			
		||||
        api_client = Client(api_hostname, api_username, api_password,
 | 
			
		||||
                            use_tls=True, cachetoken=self._cachetoken)
 | 
			
		||||
 | 
			
		||||
        res = []
 | 
			
		||||
        dterms = collections.deque(terms)
 | 
			
		||||
        machines_roles = None  # TODO : Cache this.
 | 
			
		||||
 | 
			
		||||
        display.vvv("Lookup terms are {}".format(terms))
 | 
			
		||||
        while dterms:
 | 
			
		||||
            term = dterms.popleft()
 | 
			
		||||
| 
						 | 
				
			
			@ -411,14 +557,31 @@ class LookupModule(LookupBase):
 | 
			
		|||
            elif term == 'get_role':
 | 
			
		||||
                try:
 | 
			
		||||
                    role_name = dterms.popleft()
 | 
			
		||||
                    roles, machines_roles = self._get_role(api_client,
 | 
			
		||||
                                                           role_name,
 | 
			
		||||
                                                           machines_roles,
 | 
			
		||||
                                                           )
 | 
			
		||||
                    roles = self._get_role(api_client, role_name)
 | 
			
		||||
                    res.append(roles)
 | 
			
		||||
                except IndexError:
 | 
			
		||||
                    display.v("Error in re2oapi : No role_name provided")
 | 
			
		||||
                    raise AnsibleError("role_name not found in arguments.")
 | 
			
		||||
            elif term == 'prefixv6':
 | 
			
		||||
                prefixes = []
 | 
			
		||||
                while dterms:
 | 
			
		||||
                    subnet_name = dterms.popleft()
 | 
			
		||||
                    prefixes.append([self._get_prefix(api_client, subnet_name)])
 | 
			
		||||
                if prefixes:
 | 
			
		||||
                    res.extend(prefixes)
 | 
			
		||||
                else:
 | 
			
		||||
                    display.v("Error in re2oapi : No subnet_name provided")
 | 
			
		||||
                    raise AnsibleError("subnet_name not found in arguments.")
 | 
			
		||||
            elif term == 'cidrs':
 | 
			
		||||
                cidrs = []
 | 
			
		||||
                while dterms:
 | 
			
		||||
                    subnet_name = dterms.popleft()
 | 
			
		||||
                    cidrs.append([self._get_cidrs(api_client, subnet_name)])
 | 
			
		||||
                if cidrs:
 | 
			
		||||
                    res.extend(cidrs)
 | 
			
		||||
                else:
 | 
			
		||||
                    display.v("Error in re2oapi : No subnet_name provided")
 | 
			
		||||
                    raise AnsibleError("subnet_name not found in arguments.")
 | 
			
		||||
            else:
 | 
			
		||||
                try:
 | 
			
		||||
                    res.append(self._rawquery(api_client, term))
 | 
			
		||||
| 
						 | 
				
			
			@ -429,59 +592,185 @@ class LookupModule(LookupBase):
 | 
			
		|||
                                       .format(to_native(e)))
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    def _get_cache(self, key):
 | 
			
		||||
        if self._cache:
 | 
			
		||||
            return self._cache.get(key)
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def _set_cache(self, key, value):
 | 
			
		||||
        if self._cache:
 | 
			
		||||
            return self._cache.set(key, value)
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def _is_cached(self, key):
 | 
			
		||||
        if self._cache:
 | 
			
		||||
            return self._cache.contains(key)
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def _getzones(self, api_client):
 | 
			
		||||
        display.v("Getting dns zone names")
 | 
			
		||||
        zones = api_client.list('dns/zones')
 | 
			
		||||
        zones_name = [zone["name"][1:] for zone in zones]
 | 
			
		||||
        zones, zones_name = None, None
 | 
			
		||||
 | 
			
		||||
        if self._is_cached('dnszones'):
 | 
			
		||||
            zones_name = self._get_cache('dnszones')
 | 
			
		||||
 | 
			
		||||
        if zones_name is not None:
 | 
			
		||||
            display.vvv("Found dnszones in cache.")
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            if self._is_cached('dns_zones'):
 | 
			
		||||
                zones = self._get_cache('dns_zones')
 | 
			
		||||
            if zones is not None:
 | 
			
		||||
                display.vvv("Found dns/zones in cache.")
 | 
			
		||||
            else:
 | 
			
		||||
                display.vvv("Contacting the API, endpoint dns/zones...")
 | 
			
		||||
                zones = api_client.list('dns/zones')
 | 
			
		||||
                display.vvv("...Done")
 | 
			
		||||
            zones_name = [zone["name"][1:] for zone in zones]
 | 
			
		||||
            display.vvv("Storing dnszones in cache.")
 | 
			
		||||
            self._set_cache('dnszones', zones_name)
 | 
			
		||||
        display.vvv('\n')
 | 
			
		||||
        return zones_name
 | 
			
		||||
 | 
			
		||||
    def _getreverse(self, api_client):
 | 
			
		||||
        display.v("Getting dns reverse zones")
 | 
			
		||||
        display.vvv("Contacting the API, endpoint dns/reverse-zones...")
 | 
			
		||||
        zones = api_client.list('dns/reverse-zones')
 | 
			
		||||
        display.vvv("...Done")
 | 
			
		||||
        res = []
 | 
			
		||||
        for zone in zones:
 | 
			
		||||
            if zone['ptr_records']:
 | 
			
		||||
                display.vvv('Found PTR records')
 | 
			
		||||
                subnets = []
 | 
			
		||||
                for net in zone['cidrs']:
 | 
			
		||||
                    net = netaddr.IPNetwork(net)
 | 
			
		||||
                    if net.prefixlen > 24:
 | 
			
		||||
                        subnets.extend(net.subnet(32))
 | 
			
		||||
                    elif net.prefixlen > 16:
 | 
			
		||||
                        subnets.extend(net.subnet(24))
 | 
			
		||||
                    elif net.prefixlen > 8:
 | 
			
		||||
                        subnets.extend(net.subnet(16))
 | 
			
		||||
                    else:
 | 
			
		||||
                        subnets.extend(net.subnet(8))
 | 
			
		||||
                for subnet in subnets:
 | 
			
		||||
                    _address = netaddr.IPAddress(subnet.first)
 | 
			
		||||
                    rev_dns_a = _address.reverse_dns.split('.')[:-1]
 | 
			
		||||
                    if subnet.prefixlen == 8:
 | 
			
		||||
                        zone_name = '.'.join(rev_dns_a[3:])
 | 
			
		||||
                    elif subnet.prefixlen == 16:
 | 
			
		||||
                        zone_name = '.'.join(rev_dns_a[2:])
 | 
			
		||||
                    elif subnet.prefixlen == 24:
 | 
			
		||||
                        zone_name = '.'.join(rev_dns_a[1:])
 | 
			
		||||
                    res.append(zone_name)
 | 
			
		||||
                    display.vvv("Found reverse zone {}".format(zone_name))
 | 
			
		||||
 | 
			
		||||
        zones, res = None, None
 | 
			
		||||
 | 
			
		||||
        if self._is_cached('dnsreverse'):
 | 
			
		||||
            res = self._get_cache('dnsreverse')
 | 
			
		||||
 | 
			
		||||
        if res is not None:
 | 
			
		||||
            display.vvv("Found dnsreverse in cache.")
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            if self._is_cached('dns_reverse-zones'):
 | 
			
		||||
                zones = self._get_cache('dns_reverse-zones')
 | 
			
		||||
 | 
			
		||||
            if zones is not None:
 | 
			
		||||
                display.vvv("Found dns/reverse-zones in cache.")
 | 
			
		||||
            else:
 | 
			
		||||
                display.vvv("Contacting the API, endpoint dns/reverse-zones..")
 | 
			
		||||
                zones = api_client.list('dns/reverse-zones')
 | 
			
		||||
                display.vvv("...Done")
 | 
			
		||||
 | 
			
		||||
            display.vvv("Trying to format dns reverse in a nice way.")
 | 
			
		||||
            res = []
 | 
			
		||||
            for zone in zones:
 | 
			
		||||
                if zone['ptr_records']:
 | 
			
		||||
                    display.vvv('Found PTR records')
 | 
			
		||||
                    subnets = []
 | 
			
		||||
                    for net in zone['cidrs']:
 | 
			
		||||
                        net = netaddr.IPNetwork(net)
 | 
			
		||||
                        if net.prefixlen > 24:
 | 
			
		||||
                            subnets.extend(net.subnet(32))
 | 
			
		||||
                        elif net.prefixlen > 16:
 | 
			
		||||
                            subnets.extend(net.subnet(24))
 | 
			
		||||
                        elif net.prefixlen > 8:
 | 
			
		||||
                            subnets.extend(net.subnet(16))
 | 
			
		||||
                        else:
 | 
			
		||||
                            subnets.extend(net.subnet(8))
 | 
			
		||||
 | 
			
		||||
                    for subnet in subnets:
 | 
			
		||||
                        _address = netaddr.IPAddress(subnet.first)
 | 
			
		||||
                        rev_dns_a = _address.reverse_dns.split('.')[:-1]
 | 
			
		||||
                        if subnet.prefixlen == 8:
 | 
			
		||||
                            zone_name = '.'.join(rev_dns_a[3:])
 | 
			
		||||
                        elif subnet.prefixlen == 16:
 | 
			
		||||
                            zone_name = '.'.join(rev_dns_a[2:])
 | 
			
		||||
                        elif subnet.prefixlen == 24:
 | 
			
		||||
                            zone_name = '.'.join(rev_dns_a[1:])
 | 
			
		||||
                        res.append(zone_name)
 | 
			
		||||
                        display.vvv("Found reverse zone {}".format(zone_name))
 | 
			
		||||
 | 
			
		||||
                if zone['ptr_v6_records']:
 | 
			
		||||
                    display.vvv("Found PTR v6 record")
 | 
			
		||||
                    net = netaddr.IPNetwork(zone['prefix_v6']+'/'+str(zone['prefix_v6_length']))
 | 
			
		||||
                    net_class = max(((net.prefixlen -1) // 4) +1, 1)
 | 
			
		||||
                    net = netaddr.IPNetwork(zone['prefix_v6']
 | 
			
		||||
                                            + '/'
 | 
			
		||||
                                            + str(zone['prefix_v6_length']))
 | 
			
		||||
                    net_class = max(((net.prefixlen - 1) // 4) + 1, 1)
 | 
			
		||||
                    zone6_name = ".".join(
 | 
			
		||||
                        netaddr.IPAddress(net.first).reverse_dns.split('.')[32 - net_class:])[:-1]
 | 
			
		||||
                        netaddr.IPAddress(net.first)
 | 
			
		||||
                        .reverse_dns.split('.')[32 - net_class:])[:-1]
 | 
			
		||||
                    res.append(zone6_name)
 | 
			
		||||
                    display.vvv("Found reverse zone {}".format(zone6_name))
 | 
			
		||||
        return list(set(res))
 | 
			
		||||
 | 
			
		||||
            display.vvv("Storing dns reverse zones in cache.")
 | 
			
		||||
            self._set_cache('dnsreverse', list(set(res)))
 | 
			
		||||
 | 
			
		||||
        display.vvv('\n')
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    def _rawquery(self, api_client, endpoint):
 | 
			
		||||
        display.v("Make a raw query to endpoint {}".format(endpoint))
 | 
			
		||||
        return api_client.list(endpoint)
 | 
			
		||||
        res = None
 | 
			
		||||
        if self._is_cached(endpoint.replace('/', '_')):
 | 
			
		||||
            res = self._get_cache(endpoint.replace('/', '_'))
 | 
			
		||||
        if res is not None:
 | 
			
		||||
            display.vvv("Found {} in cache.".format(endpoint))
 | 
			
		||||
        else:
 | 
			
		||||
            display.v("Making a raw query to {host}/api/{endpoint}"
 | 
			
		||||
                      .format(host=self._api_hostname, endpoint=endpoint))
 | 
			
		||||
            res = api_client.list(endpoint)
 | 
			
		||||
            display.vvv("Storing result in cache.")
 | 
			
		||||
            self._set_cache(endpoint.replace('/', '_'), res)
 | 
			
		||||
 | 
			
		||||
    def _get_role(self, api_client, role_name, machines_roles):
 | 
			
		||||
        if machines_roles is None:
 | 
			
		||||
            machines_roles = api_client.list("machines/role")
 | 
			
		||||
        return list(filter(lambda machine: machine["role_type"] == role_name,
 | 
			
		||||
                           machines_roles)), machines_roles
 | 
			
		||||
        display.vvv('\n')
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    def _get_role(self, api_client, role_name):
 | 
			
		||||
        res, machines_roles = None, None
 | 
			
		||||
 | 
			
		||||
        if self._is_cached(role_name):
 | 
			
		||||
            res = self._get_cache(role_name)
 | 
			
		||||
 | 
			
		||||
        if res is not None:
 | 
			
		||||
            display.vvv("Found {} in cache.".format(role_name))
 | 
			
		||||
        else:
 | 
			
		||||
            if self._is_cached("machines_role"):
 | 
			
		||||
                machines_roles = self._get_cache("machines_role")
 | 
			
		||||
 | 
			
		||||
            if machines_roles is not None:
 | 
			
		||||
                display.vvv("Found machines/roles in cache.")
 | 
			
		||||
            else:
 | 
			
		||||
                machines_roles = api_client.list("machines/role")
 | 
			
		||||
                display.vvv("Storing machines/role in cache.")
 | 
			
		||||
                self._set_cache("machines_role", machines_roles)
 | 
			
		||||
 | 
			
		||||
            res = list(filter(lambda m: m["role_type"] == role_name,
 | 
			
		||||
                              machines_roles))
 | 
			
		||||
            display.vvv("Storing {} in cache.".format(role_name))
 | 
			
		||||
            self._set_cache(role_name, res)
 | 
			
		||||
 | 
			
		||||
        display.vvv('\n')
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    def _get_prefix(self, api_client, subnet_name):
 | 
			
		||||
        prefixv6 = None
 | 
			
		||||
        if self._is_cached(subnet_name + '_v6'):
 | 
			
		||||
            display.vvv("Found subnet {} in cache.".format(subnet_name))
 | 
			
		||||
            prefixv6 = self._get_cache(subnet_name + '_v6')
 | 
			
		||||
        else:
 | 
			
		||||
            Mtypes = self._rawquery(api_client, 'machines/iptype')
 | 
			
		||||
            iptype = list(filter(lambda x: x['type'] == subnet_name, Mtypes))
 | 
			
		||||
            prefixv6 = iptype[0]['prefix_v6'] + '/64'
 | 
			
		||||
            display.vvv("Storing subnet {} in cache".format(subnet_name))
 | 
			
		||||
            self._set_cache(subnet_name + '_v6', prefixv6)
 | 
			
		||||
        return prefixv6
 | 
			
		||||
 | 
			
		||||
    def _get_cidrs(self, api_client, subnet_name):
 | 
			
		||||
        cidrs = None
 | 
			
		||||
        if self._is_cached(subnet_name):
 | 
			
		||||
            display.vvv("Found subnet {} in cache.".format(subnet_name))
 | 
			
		||||
            cidrs = self._get_cache(subnet_name)
 | 
			
		||||
        else:
 | 
			
		||||
            Mtypes = self._rawquery(api_client, 'machines/iptype')
 | 
			
		||||
            iptype = list(filter(lambda x: x['type'] == subnet_name, Mtypes))[0]
 | 
			
		||||
            ips = iptype['domaine_ip_start']
 | 
			
		||||
            ipe = iptype['domaine_ip_stop']
 | 
			
		||||
            cidrs = list(map(lambda a: str(a), netaddr.iprange_to_cidrs(ips, ipe)))
 | 
			
		||||
            display.vvv("Storing subnet {} in cache".format(subnet_name))
 | 
			
		||||
            self._set_cache(subnet_name, cidrs)
 | 
			
		||||
        return cidrs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,70 +0,0 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy Prometheus
 | 
			
		||||
- hosts: fyre.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    # Prometheus targets.json
 | 
			
		||||
    prometheus_targets:
 | 
			
		||||
      - targets: "{{ groups['server'] | list | sort }}"
 | 
			
		||||
    prometheus_ups_snmp_targets:
 | 
			
		||||
      - targets: [pulsar.adm.crans.org]
 | 
			
		||||
    prometheus_unifi_snmp_targets:
 | 
			
		||||
      - targets: "{{ groups['crans_unifi'] | list | sort }}"
 | 
			
		||||
    prometheus_blackbox_targets:
 | 
			
		||||
      - targets:
 | 
			
		||||
          - https://crans.org
 | 
			
		||||
          - https://www.crans.org
 | 
			
		||||
          - https://grafana.crans.org
 | 
			
		||||
          - https://wiki.crans.org
 | 
			
		||||
          - https://pad.crans.org
 | 
			
		||||
    prometheus_apache_targets:
 | 
			
		||||
      - targets: [zamok.adm.crans.org]
 | 
			
		||||
    snmp_unifi_password: "{{ vault_snmp_unifi_password }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - prometheus
 | 
			
		||||
    - prometheus-alertmanager
 | 
			
		||||
    - prometheus-snmp-exporter
 | 
			
		||||
    - prometheus-blackbox-exporter
 | 
			
		||||
 | 
			
		||||
# Monitor all hosts
 | 
			
		||||
- hosts: server,test_vm
 | 
			
		||||
  roles:
 | 
			
		||||
    - prometheus-node-exporter
 | 
			
		||||
 | 
			
		||||
# Export apache metrics
 | 
			
		||||
- hosts: zamok.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - prometheus-apache-exporter
 | 
			
		||||
 | 
			
		||||
# Configure HP RAID monitoring
 | 
			
		||||
# You can list SCSI drives with `lsscsi -g`
 | 
			
		||||
- hosts: fyre.adm.crans.org,gateau.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - smartd-hp-smartarray
 | 
			
		||||
 | 
			
		||||
# Deploy grafana
 | 
			
		||||
- hosts: fyre.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    grafana_root_url: https://grafana.crans.org
 | 
			
		||||
    ldap_base: 'dc=crans,dc=org'
 | 
			
		||||
    ldap_master_ipv4: '10.231.136.19'
 | 
			
		||||
    ldap_user_tree: "cn=Utilisateurs,{{ ldap_base }}"
 | 
			
		||||
    ldap_grafana_bind_dn: "cn=grafana,ou=service-users,{{ ldap_base }}"
 | 
			
		||||
    ldap_grafana_passwd: "{{ vault_ldap_grafana_passwd }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - grafana
 | 
			
		||||
 | 
			
		||||
# Deploy NinjaBot
 | 
			
		||||
- hosts: fyre.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - ninjabot
 | 
			
		||||
 | 
			
		||||
# Monitor mailq with a special text exporter
 | 
			
		||||
- hosts: redisdead.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - prometheus-node-exporter-postfix
 | 
			
		||||
 | 
			
		||||
# Monitor logs with mtail
 | 
			
		||||
- hosts: thot.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - mtail
 | 
			
		||||
							
								
								
									
										76
									
								
								network.yml
								
								
								
								
							
							
						
						
									
										76
									
								
								network.yml
								
								
								
								
							| 
						 | 
				
			
			@ -1,54 +1,5 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy tunnel
 | 
			
		||||
- hosts: sputnik.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    debian_mirror: http://mirror.crans.org/debian
 | 
			
		||||
    wireguard:
 | 
			
		||||
      sputnik: true
 | 
			
		||||
      private_key: "{{ vault_wireguard_sputnik_private_key }}"
 | 
			
		||||
      peer_public_key: "{{ vault_wireguard_boeing_public_key }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - wireguard
 | 
			
		||||
 | 
			
		||||
- hosts: boeing.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    # Debian mirror on adm
 | 
			
		||||
    debian_mirror: http://mirror.adm.crans.org/debian
 | 
			
		||||
    wireguard:
 | 
			
		||||
      sputnik: false
 | 
			
		||||
      if: ens20
 | 
			
		||||
      private_key: "{{ vault_wireguard_boeing_private_key }}"
 | 
			
		||||
      peer_public_key: "{{ vault_wireguard_sputnik_public_key }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - wireguard
 | 
			
		||||
 | 
			
		||||
# Deploy DHCP server
 | 
			
		||||
- hosts: dhcp.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    dhcp:
 | 
			
		||||
      authoritative: true
 | 
			
		||||
  roles:
 | 
			
		||||
    - isc-dhcp-server
 | 
			
		||||
 | 
			
		||||
# Deploy recursive DNS cache server
 | 
			
		||||
- hosts: odlyd.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - bind-recursive
 | 
			
		||||
 | 
			
		||||
# Deploy authoritative DNS server
 | 
			
		||||
- hosts: silice.adm.crans.org,sputnik.adm.crans.org,boeing.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    certbot_dns_secret: "{{ vault_certbot_dns_secret }}"
 | 
			
		||||
    certbot_adm_dns_secret: "{{ vault_certbot_adm_dns_secret }}"
 | 
			
		||||
    bind:
 | 
			
		||||
      masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
 | 
			
		||||
      slaves: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-slave')[0] }}"
 | 
			
		||||
      zones: "{{ lookup('re2oapi', 'dnszones') }}"
 | 
			
		||||
      reverse: "{{ lookup('re2oapi', 'dnsreverse') }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - bind-authoritative
 | 
			
		||||
 | 
			
		||||
# Deploy reverse proxy
 | 
			
		||||
- hosts: bakdaur.adm.crans.org,frontdaur.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +26,7 @@
 | 
			
		|||
        - {from: lutim.crans.org, to: 10.231.136.69}
 | 
			
		||||
        - {from: zero.crans.org, to: 10.231.136.76}
 | 
			
		||||
        - {from: pad.crans.org, to: "10.231.136.76:9001"}
 | 
			
		||||
        - {from: ethercalc.crans.org, to: 10.231.136.203}
 | 
			
		||||
        - {from: ethercalc.crans.org, to: "10.231.136.203:8000"}
 | 
			
		||||
        - {from: mediadrop.crans.org, to: 10.231.136.106}
 | 
			
		||||
        - {from: videos.crans.org, to: 10.231.136.106}
 | 
			
		||||
        - {from: video.crans.org, to: 10.231.136.106}
 | 
			
		||||
| 
						 | 
				
			
			@ -190,28 +141,3 @@
 | 
			
		|||
      remote_as: 8218
 | 
			
		||||
  roles:
 | 
			
		||||
    - quagga-ipv6
 | 
			
		||||
 | 
			
		||||
# Deploy postfix on mail servers
 | 
			
		||||
- hosts: titanic.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    postfix:
 | 
			
		||||
      primary: false
 | 
			
		||||
      secondary: true
 | 
			
		||||
      public: true
 | 
			
		||||
      dkim: true
 | 
			
		||||
      mailman: false
 | 
			
		||||
      titanic: true
 | 
			
		||||
  roles:
 | 
			
		||||
    - postfix
 | 
			
		||||
 | 
			
		||||
- hosts: sputnik.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    postfix:
 | 
			
		||||
      primary: false
 | 
			
		||||
      secondary: true
 | 
			
		||||
      public: true
 | 
			
		||||
      dkim: true
 | 
			
		||||
      mailman: false
 | 
			
		||||
      titanic: false
 | 
			
		||||
  roles:
 | 
			
		||||
    - postfix
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# zephir backups virtual machines.
 | 
			
		||||
# omnomnom backups home dirs.
 | 
			
		||||
 | 
			
		||||
# Rsync client on all server to allow backup
 | 
			
		||||
- hosts: server
 | 
			
		||||
  vars:
 | 
			
		||||
    # Backup password
 | 
			
		||||
    backuppc_rsyncd_passwd: "{{ vault_backuppc_rsyncd_passwd }}"
 | 
			
		||||
  roles: ["rsync-client"]
 | 
			
		||||
 | 
			
		||||
# Backuppc backup software
 | 
			
		||||
- hosts: zephir.adm.crans.org,omnomnom.adm.crans.org
 | 
			
		||||
  roles: ["backuppc"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy DHCP server
 | 
			
		||||
- hosts: dhcp.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    dhcp:
 | 
			
		||||
      authoritative: true
 | 
			
		||||
  roles: ["isc-dhcp-server"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy recursive DNS cache server
 | 
			
		||||
- hosts: odlyd.adm.crans.org
 | 
			
		||||
  roles: ["bind-recursive"]
 | 
			
		||||
 | 
			
		||||
# Deploy authoritative DNS server
 | 
			
		||||
- hosts: silice.adm.crans.org,sputnik.adm.crans.org,boeing.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    certbot_dns_secret: "{{ vault_certbot_dns_secret }}"
 | 
			
		||||
    certbot_adm_dns_secret: "{{ vault_certbot_adm_dns_secret }}"
 | 
			
		||||
    bind:
 | 
			
		||||
      masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
 | 
			
		||||
      slaves: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-slave')[0] }}"
 | 
			
		||||
      zones: "{{ lookup('re2oapi', 'dnszones') }}"
 | 
			
		||||
      reverse: "{{ lookup('re2oapi', 'dnsreverse') }}"
 | 
			
		||||
  roles: ["bind-authoritative"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# thot is the log server.
 | 
			
		||||
# Servers need to send their logs to thot.
 | 
			
		||||
 | 
			
		||||
# Send logs to thot
 | 
			
		||||
- hosts: server,!thot.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    rsyslog:
 | 
			
		||||
      server: thot.adm.crans.org
 | 
			
		||||
  roles: ["rsyslog-client"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Redisdead is the main MX.
 | 
			
		||||
# Soyouz and titanic are the old backup MX.
 | 
			
		||||
# Boeing and sputnik are the new MX (still in installation ?).
 | 
			
		||||
# All other servers uses nullmailer to send local mail to Crans SMTP.
 | 
			
		||||
 | 
			
		||||
# Redirect local mail to mailserver
 | 
			
		||||
- hosts: crans_server,!redisdead.adm.crans.org,!soyouz.adm.crans.org,!titanic.adm.crans.org,!boeing.adm.crans.org,!sputnik.adm.crans.org,!zamok.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    mail_root: root@crans.org
 | 
			
		||||
    mail_snmp_server: smtp.adm.crans.org
 | 
			
		||||
    mail_defaulthost: crans.org
 | 
			
		||||
  roles: ["nullmailer"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy Mailman
 | 
			
		||||
- hosts: redisdead.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    mailman:
 | 
			
		||||
      site_list: "nounou"
 | 
			
		||||
      default_url: "https://lists.crans.org/"
 | 
			
		||||
      default_host: "lists.crans.org"
 | 
			
		||||
      default_language: "fr"
 | 
			
		||||
      auth_basic: |
 | 
			
		||||
        "On n'aime pas les spambots, donc on a mis un mot de passe. Le login est Stop et le mot de passe est Spam.";
 | 
			
		||||
    spamassassin: "SpamAssassin_crans"
 | 
			
		||||
    smtphost: "smtp.adm.crans.org"
 | 
			
		||||
    mynetworks: ['138.231.0.0/16', '185.230.76.0/22', '2a0c:700:0::/40']
 | 
			
		||||
    nginx:
 | 
			
		||||
      ssl:
 | 
			
		||||
        cert: /etc/letsencrypt/live/crans.org/fullchain.pem
 | 
			
		||||
        key: /etc/letsencrypt/live/crans.org/privkey.pem
 | 
			
		||||
        trusted_cert: /etc/letsencrypt/live/crans.org/chain.pem
 | 
			
		||||
  roles:
 | 
			
		||||
    - mailman
 | 
			
		||||
    - nginx-mailman
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy Prometheus and Grafana on monitoring server
 | 
			
		||||
- hosts: fyre.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    # Prometheus targets.json
 | 
			
		||||
    prometheus:
 | 
			
		||||
      node_targets: "{{ groups['server'] | list | sort }}"
 | 
			
		||||
      ups_snmp_targets:
 | 
			
		||||
        - pulsar.adm.crans.org  # 0B
 | 
			
		||||
        - quasar.adm.crans.org  # 4J
 | 
			
		||||
      unifi_snmp_targets: "{{ groups['crans_unifi'] | list | sort }}"
 | 
			
		||||
      blackbox_targets:
 | 
			
		||||
        - https://crans.org
 | 
			
		||||
        - https://www.crans.org
 | 
			
		||||
        - https://grafana.crans.org
 | 
			
		||||
        - https://wiki.crans.org
 | 
			
		||||
        - https://pad.crans.org
 | 
			
		||||
      apache_targets: [zamok.adm.crans.org]
 | 
			
		||||
 | 
			
		||||
    snmp_unifi_password: "{{ vault_snmp_unifi_password }}"
 | 
			
		||||
 | 
			
		||||
    grafana:
 | 
			
		||||
      root_url: https://grafana.crans.org
 | 
			
		||||
      ldap_bind_dn: "cn=grafana,ou=service-users,{{ ldap_base }}"
 | 
			
		||||
      ldap_passwd: "{{ vault_ldap_grafana_passwd }}"
 | 
			
		||||
 | 
			
		||||
    ldap_base: 'dc=crans,dc=org'
 | 
			
		||||
    ldap_master_ipv4: '10.231.136.19'
 | 
			
		||||
    ldap_user_tree: "cn=Utilisateurs,{{ ldap_base }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - prometheus
 | 
			
		||||
    - prometheus-alertmanager
 | 
			
		||||
    - prometheus-snmp-exporter
 | 
			
		||||
    - prometheus-blackbox-exporter
 | 
			
		||||
    - ninjabot
 | 
			
		||||
    - grafana
 | 
			
		||||
 | 
			
		||||
# Monitor all hosts
 | 
			
		||||
- hosts: server,test_vm
 | 
			
		||||
  vars:
 | 
			
		||||
    adm_ipv4: "{{ ansible_all_ipv4_addresses | ipaddr(adm_subnet) | first }}"
 | 
			
		||||
  roles: ["prometheus-node-exporter"]
 | 
			
		||||
 | 
			
		||||
# Export apache metrics
 | 
			
		||||
- hosts: zamok.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    adm_ipv4: "{{ ansible_all_ipv4_addresses | ipaddr(adm_subnet) | first }}"
 | 
			
		||||
  roles: ["prometheus-apache-exporter"]
 | 
			
		||||
 | 
			
		||||
# Configure HP RAID monitoring
 | 
			
		||||
# You can list SCSI drives with `lsscsi -g`
 | 
			
		||||
- hosts: fyre.adm.crans.org,gateau.adm.crans.org
 | 
			
		||||
  roles: ["smartd-hp-smartarray"]
 | 
			
		||||
 | 
			
		||||
# Monitor mailq with a special text exporter
 | 
			
		||||
- hosts: redisdead.adm.crans.org
 | 
			
		||||
  roles: ["prometheus-node-exporter-postfix"]
 | 
			
		||||
 | 
			
		||||
# Monitor logs with mtail
 | 
			
		||||
- hosts: thot.adm.crans.org
 | 
			
		||||
  roles: ["mtail"]
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
        - switch
 | 
			
		||||
        - fil
 | 
			
		||||
 | 
			
		||||
- hosts: boeing.adm.crans.org,cochon.adm.crans.org,tracker.adm.crans.org,voyager.adm.crans.org,lutim.adm.crans.org,gateau.adm.crans.org,owncloud-srv.adm.crans.org,charybde.adm.crans.org,cas-srv.adm.crans.org,fyre.adm.crans.org,silice.adm.crans.org,frontdaur.adm.crans.org,bakdaur.adm.crans.org
 | 
			
		||||
- hosts: boeing.adm.crans.org,cochon.adm.crans.org,tracker.adm.crans.org,voyager.adm.crans.org,lutim.adm.crans.org,gateau.adm.crans.org,owncloud-srv.adm.crans.org,charybde.adm.crans.org,cas-srv.adm.crans.org,fyre.adm.crans.org,silice.adm.crans.org,frontdaur.adm.crans.org,bakdaur.adm.crans.org,ethercalc-srv.adm.crans.org,alice.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    vlan:
 | 
			
		||||
      - name: srv
 | 
			
		||||
| 
						 | 
				
			
			@ -66,5 +66,4 @@
 | 
			
		|||
        dns: 185.230.78.152 185.230.78.4
 | 
			
		||||
        dns_search: crans.org
 | 
			
		||||
        ifnames: "{{ ifaces | json_query('results[?item==`adh`].stdout') }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - interfaces
 | 
			
		||||
  roles: ["interfaces"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Odlyd do not use NFS as it is the master backup.
 | 
			
		||||
# Servers outside of campus do not use NFS.
 | 
			
		||||
# zamok, omnomnom, owl and owncloud-srv uses permanently mounted home dirs.
 | 
			
		||||
# all other servers on campus uses autofs to dynamically mount home dirs.
 | 
			
		||||
 | 
			
		||||
# Deploy NFS only on campus
 | 
			
		||||
- hosts: crans_server
 | 
			
		||||
  roles: ["nfs-common"]
 | 
			
		||||
 | 
			
		||||
# Deploy autofs NFS
 | 
			
		||||
- hosts: crans_server,!odlyd.adm.crans.org,!zamok.adm.crans.org,!omnomnom.adm.crans.org,!owl.adm.crans.org,!owncloud-srv.adm.crans.org
 | 
			
		||||
  roles: ["nfs-autofs"]
 | 
			
		||||
 | 
			
		||||
# Deploy home permanent
 | 
			
		||||
- hosts: zamok.adm.crans.org,omnomnom.adm.crans.org,owl.adm.crans.org,owncloud-srv.adm.crans.org
 | 
			
		||||
  roles: ["home-permanent"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Cochon contains DVB cards
 | 
			
		||||
 | 
			
		||||
- hosts: cochon.adm.crans.org
 | 
			
		||||
  roles: ["mumudvb"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
# Deploy tunnel
 | 
			
		||||
- hosts: sputnik.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    debian_mirror: http://mirror.crans.org/debian
 | 
			
		||||
    wireguard:
 | 
			
		||||
      sputnik: true
 | 
			
		||||
      private_key: "{{ vault_wireguard_sputnik_private_key }}"
 | 
			
		||||
      peer_public_key: "{{ vault_wireguard_boeing_public_key }}"
 | 
			
		||||
  roles: ["wireguard"]
 | 
			
		||||
 | 
			
		||||
- hosts: boeing.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    # Debian mirror on adm
 | 
			
		||||
    debian_mirror: http://mirror.adm.crans.org/debian
 | 
			
		||||
    wireguard:
 | 
			
		||||
      sputnik: false
 | 
			
		||||
      if: ens20
 | 
			
		||||
      private_key: "{{ vault_wireguard_boeing_private_key }}"
 | 
			
		||||
      peer_public_key: "{{ vault_wireguard_sputnik_public_key }}"
 | 
			
		||||
  roles: ["wireguard"]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
# Postfix playbook
 | 
			
		||||
---
 | 
			
		||||
- hosts: sputnik.adm.crans.org, boeing.adm.crans.org, redisdead.adm.crans.org, titanic.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    certbot:
 | 
			
		||||
      dns_rfc2136_name: certbot_challenge.
 | 
			
		||||
      dns_rfc2136_secret: "{{ vault_certbot_dns_secret }}"
 | 
			
		||||
      mail: root@crans.org
 | 
			
		||||
      certname: crans.org
 | 
			
		||||
      domains: "*.crans.org"
 | 
			
		||||
    bind:
 | 
			
		||||
      masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
 | 
			
		||||
    opendkim:
 | 
			
		||||
        private_key: "{{ vault_opendkim_private_key }}"
 | 
			
		||||
    policyd:
 | 
			
		||||
      mail: root@crans.org
 | 
			
		||||
      exemptions: "{{ lookup('re2oapi', 'get_role', 'user-server')[0] }}"
 | 
			
		||||
      mynetworks:
 | 
			
		||||
        ipv4:
 | 
			
		||||
          "{{ lookup('re2oapi', 'cidrs', 'serveurs',
 | 
			
		||||
                                         'adherents',
 | 
			
		||||
                                         'wifi-new-pub',
 | 
			
		||||
                                         'fil-new-pub',
 | 
			
		||||
                                         'fil-pub',
 | 
			
		||||
                                         'wifi-new-serveurs',
 | 
			
		||||
                                         'wifi-new-adherents',
 | 
			
		||||
                                         'wifi-new-federez',
 | 
			
		||||
                                         'fil-new-serveurs',
 | 
			
		||||
                                         'fil-new-adherents')
 | 
			
		||||
                                         | flatten }}"
 | 
			
		||||
        ipv6:
 | 
			
		||||
          "{{ lookup('re2oapi', 'prefixv6', 'adherents',
 | 
			
		||||
                                            'fil-new-pub',
 | 
			
		||||
                                            'wifi-new-pub')
 | 
			
		||||
                                            | flatten }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - certbot
 | 
			
		||||
    - postfix
 | 
			
		||||
    - opendkim
 | 
			
		||||
    - policyd
 | 
			
		||||
 | 
			
		||||
- hosts: redisdead.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - sqlgrey
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
#!/usr/bin/env ansible-playbook
 | 
			
		||||
---
 | 
			
		||||
- hosts: eap.adm.crans.org, odlyd.adm.crans.org, radius.adm.crans.org
 | 
			
		||||
  vars:
 | 
			
		||||
    certbot:
 | 
			
		||||
      dns_rfc2136_name: certbot_challenge.
 | 
			
		||||
      dns_rfc2136_secret: "{{ vault_certbot_dns_secret }}"
 | 
			
		||||
      mail: root@crans.org
 | 
			
		||||
      certname: crans.org
 | 
			
		||||
      domains: "crans.org"
 | 
			
		||||
    bind:
 | 
			
		||||
      masters: "{{ lookup('re2oapi', 'get_role', 'dns-authoritary-master')[0] }}"
 | 
			
		||||
  roles:
 | 
			
		||||
    - certbot
 | 
			
		||||
    - freeradius
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{{ ansible_header | comment(decoration='# ') }}
 | 
			
		||||
 | 
			
		||||
# Pour appliquer cette conf et générer la conf de renewal :
 | 
			
		||||
# certbot --config wildcard.ini certonly
 | 
			
		||||
# To generate the certificate, please use the following command
 | 
			
		||||
# certbot --config /etc/letsencrypt/conf.d/{{ certbot.certname }}.ini certonly
 | 
			
		||||
 | 
			
		||||
# Use a 4096 bit RSA key instead of 2048
 | 
			
		||||
rsa-key-size = 4096
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,10 @@
 | 
			
		|||
    update_cache: true
 | 
			
		||||
    install_recommends: false
 | 
			
		||||
    name:
 | 
			
		||||
      - apt-file
 | 
			
		||||
      - sudo
 | 
			
		||||
      - molly-guard  # prevent reboot
 | 
			
		||||
      - debsums
 | 
			
		||||
      - ntp  # network time sync
 | 
			
		||||
      - apt  # better than apt-get
 | 
			
		||||
      - nano  # for vulcain
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Install Redis and NPM
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - redis-server
 | 
			
		||||
      - nodejs
 | 
			
		||||
      - npm
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Install EtherCalc
 | 
			
		||||
  npm:
 | 
			
		||||
    name: ethercalc
 | 
			
		||||
    global: true
 | 
			
		||||
    state: latest
 | 
			
		||||
  register: npm_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: npm_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Install EtherCalc systemd unit
 | 
			
		||||
  template:
 | 
			
		||||
    src: systemd/system/ethercalc.service.j2
 | 
			
		||||
    dest: /etc/systemd/system/ethercalc.service
 | 
			
		||||
 | 
			
		||||
- name: Activate EtherCalc service
 | 
			
		||||
  systemd:
 | 
			
		||||
    daemon_reload: true
 | 
			
		||||
    name: ethercalc
 | 
			
		||||
    enabled: true
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
  template:
 | 
			
		||||
    src: update-motd.d/05-service.j2
 | 
			
		||||
    dest: /etc/update-motd.d/05-ethercalc
 | 
			
		||||
    mode: 0755
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Ethercalc
 | 
			
		||||
Require=redis-server.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
Restart=on-failure
 | 
			
		||||
RestartSec=3
 | 
			
		||||
User=redis
 | 
			
		||||
Group=redis
 | 
			
		||||
PIDFile=/var/run/ethercalc.pid
 | 
			
		||||
ExecStart=/usr/bin/ethercalc --host 10.231.136.203 --port 8000
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/tail +14
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
[0m> [38;5;82mEtherCalc[0m a été déployé sur cette machine. Voir [38;5;6m/usr/lib/node_modules/ethercalc/[0m.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Symlink radius certificates
 | 
			
		||||
  file:
 | 
			
		||||
    src: /etc/letsencrypt/live/crans.org/{{ item }}
 | 
			
		||||
    dest: /etc/freeradius/3.0/certs/letsencrypt/{{ item }}
 | 
			
		||||
    state: link
 | 
			
		||||
    force: yes
 | 
			
		||||
  loop:
 | 
			
		||||
    - fullchain.pem
 | 
			
		||||
    - privkey.pem
 | 
			
		||||
 | 
			
		||||
- name: Set permissions on certificates
 | 
			
		||||
  file:
 | 
			
		||||
    path: /etc/letsencrypt/{{ item }}
 | 
			
		||||
    group: freerad
 | 
			
		||||
    mode: '0755'
 | 
			
		||||
    recurse: yes
 | 
			
		||||
  loop:
 | 
			
		||||
    - live
 | 
			
		||||
    - archive
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@
 | 
			
		|||
  loop:
 | 
			
		||||
    - section: server
 | 
			
		||||
      option: root_url
 | 
			
		||||
      value: "{{ grafana_root_url }}"
 | 
			
		||||
      value: "{{ grafana.root_url }}"
 | 
			
		||||
    - section: session  # This will break with HTTPS
 | 
			
		||||
      option: cookie_secure
 | 
			
		||||
      value: "true"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,10 +21,10 @@ ssl_skip_verify = false
 | 
			
		|||
# client_key = "/path/to/client.key"
 | 
			
		||||
 | 
			
		||||
# Search user bind dn
 | 
			
		||||
bind_dn = "{{ ldap_grafana_bind_dn }}"
 | 
			
		||||
bind_dn = "{{ grafana.ldap_bind_dn }}"
 | 
			
		||||
# Search user bind password
 | 
			
		||||
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 | 
			
		||||
bind_password = '{{ ldap_grafana_passwd }}'
 | 
			
		||||
bind_password = '{{ grafana.ldap_passwd }}'
 | 
			
		||||
 | 
			
		||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
 | 
			
		||||
search_filter = "(cn=%s)"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,7 @@
 | 
			
		|||
# Disable passwd and chsh
 | 
			
		||||
- name: Copy passwd and chsh scripts
 | 
			
		||||
  template:
 | 
			
		||||
    src: bin/passwd.j2
 | 
			
		||||
    src: "bin/{{ item }}.j2"
 | 
			
		||||
    dest: "/usr/local/bin/{{ item }}"
 | 
			
		||||
    mode: 0755
 | 
			
		||||
  loop:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
#!/bin/sh
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
echo "Pour changer votre shell,\nAllez sur l'intranet : {{intranet_url}}"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
#!/bin/sh
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
echo "Pour changer votre shell,\nAllez sur l'intranet : {{intranet_url}}"
 | 
			
		||||
echo "De toutes façons la vraie commande aurait pas marché, on installe pas nslcd-utils sur les serveurs normalement."
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Reload mailman
 | 
			
		||||
  systemd:
 | 
			
		||||
    name: mailman
 | 
			
		||||
    state: reloaded
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Install mailman and SpamAssassin
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - mailman
 | 
			
		||||
      - spamassassin
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Deploy mailman config
 | 
			
		||||
  template:
 | 
			
		||||
    src: "mailman/{{ item }}.j2"
 | 
			
		||||
    dest: "/etc/mailman/{{ item }}"
 | 
			
		||||
    mode: 0755
 | 
			
		||||
  loop:
 | 
			
		||||
    - mm_cfg.py
 | 
			
		||||
    - create.html
 | 
			
		||||
  notify: Reload mailman
 | 
			
		||||
 | 
			
		||||
# Fanciness
 | 
			
		||||
- name: Deploy crans logo
 | 
			
		||||
  copy:
 | 
			
		||||
    src: ../../../logos/crans.png
 | 
			
		||||
    dest: /usr/share/images/mailman/crans.png
 | 
			
		||||
 | 
			
		||||
- name: Deploy crans logo
 | 
			
		||||
  template:
 | 
			
		||||
    src: usr/lib/mailman/Mailman/htmlformat.py.j2
 | 
			
		||||
    dest: /usr/lib/mailman/Mailman/htmlformat.py
 | 
			
		||||
    mode: 0755
 | 
			
		||||
  notify: Reload mailman
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
  template:
 | 
			
		||||
    src: update-motd.d/05-mailman.j2
 | 
			
		||||
    dest: /etc/update-motd.d/05-mailman
 | 
			
		||||
    mode: 0755
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
{{ ansible_header | comment('xml') }}
 | 
			
		||||
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 | 
			
		||||
<title>Creation de mailing list</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
<h1>Creation de mailing list</h1>
 | 
			
		||||
Il faut s'adresser a nounou arobase crans point org.
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,226 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
# -*- python -*-
 | 
			
		||||
 | 
			
		||||
# Copyright (C) 1998,1999,2000 by the Free Software Foundation, Inc.
 | 
			
		||||
#
 | 
			
		||||
# This program 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 2
 | 
			
		||||
# of the License, or (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program 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 this program; if not, write to the Free Software
 | 
			
		||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 | 
			
		||||
# 02110-1301 USA
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""This is the module which takes your site-specific settings.
 | 
			
		||||
 | 
			
		||||
From a raw distribution it should be copied to mm_cfg.py.  If you
 | 
			
		||||
already have an mm_cfg.py, be careful to add in only the new settings
 | 
			
		||||
you want.  The complete set of distributed defaults, with annotation,
 | 
			
		||||
are in ./Defaults.  In mm_cfg, override only those you want to
 | 
			
		||||
change, after the
 | 
			
		||||
 | 
			
		||||
  from Defaults import *
 | 
			
		||||
 | 
			
		||||
line (see below).
 | 
			
		||||
 | 
			
		||||
Note that these are just default settings - many can be overridden via the
 | 
			
		||||
admin and user interfaces on a per-list or per-user basis.
 | 
			
		||||
 | 
			
		||||
Note also that some of the settings are resolved against the active list
 | 
			
		||||
setting by using the value as a format string against the
 | 
			
		||||
list-instance-object's dictionary - see the distributed value of
 | 
			
		||||
DEFAULT_MSG_FOOTER for an example."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#######################################################
 | 
			
		||||
#    Here's where we get the distributed defaults.    #
 | 
			
		||||
 | 
			
		||||
from Defaults import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#####
 | 
			
		||||
# General system-wide defaults
 | 
			
		||||
#####
 | 
			
		||||
 | 
			
		||||
# Should image logos be used?  Set this to 0 to disable image logos from "our
 | 
			
		||||
# sponsors" and just use textual links instead (this will also disable the
 | 
			
		||||
# shortcut "favicon").  Otherwise, this should contain the URL base path to
 | 
			
		||||
# the logo images (and must contain the trailing slash)..  If you want to
 | 
			
		||||
# disable Mailman's logo footer altogther, hack
 | 
			
		||||
# Mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links
 | 
			
		||||
# and image names.
 | 
			
		||||
IMAGE_LOGOS = '/images/mailman/'
 | 
			
		||||
 | 
			
		||||
#-------------------------------------------------------------
 | 
			
		||||
# The name of the list Mailman uses to send password reminders
 | 
			
		||||
# and similar. Don't change if you want mailman-owner to be
 | 
			
		||||
# a valid local part.
 | 
			
		||||
MAILMAN_SITE_LIST = '{{ mailman.site_list }}'
 | 
			
		||||
 | 
			
		||||
DEFAULT_URL= '{{ mailman.default_url }}'
 | 
			
		||||
DEFAULT_URL_PATTERN = 'https://%s/'
 | 
			
		||||
add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
 | 
			
		||||
 | 
			
		||||
#-------------------------------------------------------------
 | 
			
		||||
# Default domain for email addresses of newly created MLs
 | 
			
		||||
DEFAULT_EMAIL_HOST = '{{ mailman.default_host }}'
 | 
			
		||||
#-------------------------------------------------------------
 | 
			
		||||
# Default host for web interface of newly created MLs
 | 
			
		||||
DEFAULT_URL_HOST   = '{{ mailman.default_host }}'
 | 
			
		||||
#-------------------------------------------------------------
 | 
			
		||||
# Required when setting any of its arguments.
 | 
			
		||||
add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
 | 
			
		||||
 | 
			
		||||
#-------------------------------------------------------------
 | 
			
		||||
# Do we send monthly reminders?
 | 
			
		||||
DEFAULT_SEND_REMINDERS = No
 | 
			
		||||
 | 
			
		||||
# Normally when a site administrator authenticates to a web page with the site
 | 
			
		||||
# password, they get a cookie which authorizes them as the list admin.  It
 | 
			
		||||
# makes me nervous to hand out site auth cookies because if this cookie is
 | 
			
		||||
# cracked or intercepted, the intruder will have access to every list on the
 | 
			
		||||
# site.  OTOH, it's dang handy to not have to re-authenticate to every list on
 | 
			
		||||
# the site.  Set this value to Yes to allow site admin cookies.
 | 
			
		||||
ALLOW_SITE_ADMIN_COOKIES = Yes
 | 
			
		||||
 | 
			
		||||
#####
 | 
			
		||||
# Archive defaults
 | 
			
		||||
#####
 | 
			
		||||
 | 
			
		||||
PUBLIC_ARCHIVE_URL = '{{ mailman.default_url }}archives/%(listname)s'
 | 
			
		||||
 | 
			
		||||
# Are archives on or off by default?
 | 
			
		||||
DEFAULT_ARCHIVE = Off
 | 
			
		||||
 | 
			
		||||
# Are archives public or private by default?
 | 
			
		||||
# 0=public, 1=private
 | 
			
		||||
DEFAULT_ARCHIVE_PRIVATE = 1
 | 
			
		||||
 | 
			
		||||
# Pipermail assumes that messages bodies contain US-ASCII text.
 | 
			
		||||
# Change this option to define a different character set to be used as
 | 
			
		||||
# the default character set for the archive.  The term "character set"
 | 
			
		||||
# is used in MIME to refer to a method of converting a sequence of
 | 
			
		||||
# octets into a sequence of characters.  If you change the default
 | 
			
		||||
# charset, you might need to add it to VERBATIM_ENCODING below.
 | 
			
		||||
DEFAULT_CHARSET = 'utf-8'
 | 
			
		||||
 | 
			
		||||
# Most character set encodings require special HTML entity characters to be
 | 
			
		||||
# quoted, otherwise they won't look right in the Pipermail archives.  However
 | 
			
		||||
# some character sets must not quote these characters so that they can be
 | 
			
		||||
# rendered properly in the browsers.  The primary issue is multi-byte
 | 
			
		||||
# encodings where the octet 0x26 does not always represent the & character.
 | 
			
		||||
# This variable contains a list of such characters sets which are not
 | 
			
		||||
# HTML-quoted in the archives.
 | 
			
		||||
VERBATIM_ENCODING = ['utf-8']
 | 
			
		||||
 | 
			
		||||
#####
 | 
			
		||||
# General defaults
 | 
			
		||||
#####
 | 
			
		||||
 | 
			
		||||
# The default language for this server.  Whenever we can't figure out the list
 | 
			
		||||
# context or user context, we'll fall back to using this language.  See
 | 
			
		||||
# LC_DESCRIPTIONS below for legal values.
 | 
			
		||||
DEFAULT_SERVER_LANGUAGE = '{{ mailman.default_language }}'
 | 
			
		||||
 | 
			
		||||
# How many members to display at a time on the admin cgi to unsubscribe them
 | 
			
		||||
# or change their options?
 | 
			
		||||
DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 50
 | 
			
		||||
 | 
			
		||||
# set this variable to Yes to allow list owners to delete their own mailing
 | 
			
		||||
# lists.  You may not want to give them this power, in which case, setting
 | 
			
		||||
# this variable to No instead requires list removal to be done by the site
 | 
			
		||||
# administrator, via the command line script bin/rmlist.
 | 
			
		||||
#OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No
 | 
			
		||||
 | 
			
		||||
# Set this variable to Yes to allow list owners to set the "personalized"
 | 
			
		||||
# flags on their mailing lists.  Turning these on tells Mailman to send
 | 
			
		||||
# separate email messages to each user instead of batching them together for
 | 
			
		||||
# delivery to the MTA.  This gives each member a more personalized message,
 | 
			
		||||
# but can have a heavy impact on the performance of your system.
 | 
			
		||||
#OWNERS_CAN_ENABLE_PERSONALIZATION = No
 | 
			
		||||
 | 
			
		||||
#####
 | 
			
		||||
# List defaults.  NOTE: Changing these values does NOT change the
 | 
			
		||||
# configuration of an existing list.  It only defines the default for new
 | 
			
		||||
# lists you subsequently create.
 | 
			
		||||
#####
 | 
			
		||||
 | 
			
		||||
# Should a list, by default be advertised?  What is the default maximum number
 | 
			
		||||
# of explicit recipients allowed?  What is the default maximum message size
 | 
			
		||||
# allowed?
 | 
			
		||||
DEFAULT_LIST_ADVERTISED = Yes
 | 
			
		||||
 | 
			
		||||
# {header-name: regexp} spam filtering - we include some for example sake.
 | 
			
		||||
DEFAULT_BOUNCE_MATCHING_HEADERS = """
 | 
			
		||||
# Les lignes commencant par # sont des commentairtes.
 | 
			
		||||
#from: .*-owner@yahoogroups.com
 | 
			
		||||
#from: .*@uplinkpro.com
 | 
			
		||||
#from: .*@coolstats.comic.com
 | 
			
		||||
#from: .*@trafficmagnet.com
 | 
			
		||||
#from: .*@hotmail.com
 | 
			
		||||
#X-Reject: 450
 | 
			
		||||
#X-Reject: 554
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# Mailman can be configured to strip any existing Reply-To: header, or simply
 | 
			
		||||
# extend any existing Reply-To: with one based on the above setting.
 | 
			
		||||
DEFAULT_FIRST_STRIP_REPLY_TO = Yes
 | 
			
		||||
 | 
			
		||||
# SUBSCRIBE POLICY
 | 
			
		||||
# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) **
 | 
			
		||||
# 1 - confirmation required for subscribes
 | 
			
		||||
# 2 - admin approval required for subscribes
 | 
			
		||||
# 3 - both confirmation and admin approval required
 | 
			
		||||
#
 | 
			
		||||
# ** please do not choose option 0 if you are not allowing open
 | 
			
		||||
# subscribes (next variable)
 | 
			
		||||
DEFAULT_SUBSCRIBE_POLICY = 3
 | 
			
		||||
 | 
			
		||||
# Is the list owner notified of subscribes/unsubscribes?
 | 
			
		||||
DEFAULT_ADMIN_NOTIFY_MCHANGES = Yes
 | 
			
		||||
 | 
			
		||||
# Do we send monthly reminders?
 | 
			
		||||
DEFAULT_SEND_REMINDERS = No
 | 
			
		||||
 | 
			
		||||
# What should happen to non-member posts which do not match explicit
 | 
			
		||||
# non-member actions?
 | 
			
		||||
# 0 = Accept
 | 
			
		||||
# 1 = Hold
 | 
			
		||||
# 2 = Reject
 | 
			
		||||
# 3 = Discard
 | 
			
		||||
DEFAULT_GENERIC_NONMEMBER_ACTION = 1
 | 
			
		||||
 | 
			
		||||
# Use spamassassin automatically
 | 
			
		||||
GLOBAL_PIPELINE.insert(5, '{{ spamassassin }}')
 | 
			
		||||
# Discard messages with score higher than ...
 | 
			
		||||
SPAMASSASSIN_DISCARD_SCORE = 8
 | 
			
		||||
# Hold in moderation messages with score higher than ...
 | 
			
		||||
SPAMASSASSIN_HOLD_SCORE = 2.1
 | 
			
		||||
 | 
			
		||||
# Add SpamAssassin administration interface on gui
 | 
			
		||||
# To make it work, you need to edit Gui/__init__.py
 | 
			
		||||
# with
 | 
			
		||||
# from SpamAssassin import SpamAssassin
 | 
			
		||||
ADMIN_CATEGORIES.append("spamassassin")
 | 
			
		||||
 | 
			
		||||
# Add header to keep
 | 
			
		||||
PLAIN_DIGEST_KEEP_HEADERS.append('X-Spam-Score')
 | 
			
		||||
 | 
			
		||||
# configure MTA
 | 
			
		||||
MTA = 'Postfix'
 | 
			
		||||
SMTPHOST = '{{ smtphost }}'
 | 
			
		||||
SMTP_MAX_RCPTS = 50
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
POSTFIX_STYLE_VIRTUAL_DOMAINS = ["{{ mailman.default_host }}"]
 | 
			
		||||
 | 
			
		||||
# Note - if you're looking for something that is imported from mm_cfg, but you
 | 
			
		||||
# didn't find it above, it's probably in /usr/lib/mailman/Mailman/Defaults.py.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/tail +14
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
[0m> [38;5;82mMailman[0m a été déployé sur cette machine. Voir [38;5;6m/etc/mailman/[0m et [38;5;6m/var/lib/mailman/[0m.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,742 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
 | 
			
		||||
#
 | 
			
		||||
# This program 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 2
 | 
			
		||||
# of the License, or (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program 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 this program; if not, write to the Free Software
 | 
			
		||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 | 
			
		||||
# USA.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""Library for program-based construction of an HTML documents.
 | 
			
		||||
 | 
			
		||||
Encapsulate HTML formatting directives in classes that act as containers
 | 
			
		||||
for python and, recursively, for nested HTML formatting objects.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Eventually could abstract down to HtmlItem, which outputs an arbitrary html
 | 
			
		||||
# object given start / end tags, valid options, and a value.  Ug, objects
 | 
			
		||||
# shouldn't be adding their own newlines.  The next object should.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import types
 | 
			
		||||
 | 
			
		||||
from Mailman import mm_cfg
 | 
			
		||||
from Mailman import Utils
 | 
			
		||||
from Mailman.i18n import _, get_translation
 | 
			
		||||
 | 
			
		||||
from Mailman.CSRFcheck import csrf_token
 | 
			
		||||
 | 
			
		||||
SPACE = ' '
 | 
			
		||||
EMPTYSTRING = ''
 | 
			
		||||
NL = '\n'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Format an arbitrary object.
 | 
			
		||||
def HTMLFormatObject(item, indent):
 | 
			
		||||
    "Return a presentation of an object, invoking their Format method if any."
 | 
			
		||||
    if type(item) == type(''):
 | 
			
		||||
        return item
 | 
			
		||||
    elif not hasattr(item, "Format"):
 | 
			
		||||
        return `item`
 | 
			
		||||
    else:
 | 
			
		||||
        return item.Format(indent)
 | 
			
		||||
 | 
			
		||||
def CaseInsensitiveKeyedDict(d):
 | 
			
		||||
    result = {}
 | 
			
		||||
    for (k,v) in d.items():
 | 
			
		||||
        result[k.lower()] = v
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
# Given references to two dictionaries, copy the second dictionary into the
 | 
			
		||||
# first one.
 | 
			
		||||
def DictMerge(destination, fresh_dict):
 | 
			
		||||
    for (key, value) in fresh_dict.items():
 | 
			
		||||
        destination[key] = value
 | 
			
		||||
 | 
			
		||||
class Table:
 | 
			
		||||
    def __init__(self, **table_opts):
 | 
			
		||||
        self.cells = []
 | 
			
		||||
        self.cell_info = {}
 | 
			
		||||
        self.row_info = {}
 | 
			
		||||
        self.opts = table_opts
 | 
			
		||||
 | 
			
		||||
    def AddOptions(self, opts):
 | 
			
		||||
        DictMerge(self.opts, opts)
 | 
			
		||||
 | 
			
		||||
    # Sets all of the cells.  It writes over whatever cells you had there
 | 
			
		||||
    # previously.
 | 
			
		||||
 | 
			
		||||
    def SetAllCells(self, cells):
 | 
			
		||||
        self.cells = cells
 | 
			
		||||
 | 
			
		||||
    # Add a new blank row at the end
 | 
			
		||||
    def NewRow(self):
 | 
			
		||||
        self.cells.append([])
 | 
			
		||||
 | 
			
		||||
    # Add a new blank cell at the end
 | 
			
		||||
    def NewCell(self):
 | 
			
		||||
        self.cells[-1].append('')
 | 
			
		||||
 | 
			
		||||
    def AddRow(self, row):
 | 
			
		||||
        self.cells.append(row)
 | 
			
		||||
 | 
			
		||||
    def AddCell(self, cell):
 | 
			
		||||
        self.cells[-1].append(cell)
 | 
			
		||||
 | 
			
		||||
    def AddCellInfo(self, row, col, **kws):
 | 
			
		||||
        kws = CaseInsensitiveKeyedDict(kws)
 | 
			
		||||
        if not self.cell_info.has_key(row):
 | 
			
		||||
            self.cell_info[row] = { col : kws }
 | 
			
		||||
        elif self.cell_info[row].has_key(col):
 | 
			
		||||
            DictMerge(self.cell_info[row], kws)
 | 
			
		||||
        else:
 | 
			
		||||
            self.cell_info[row][col] = kws
 | 
			
		||||
 | 
			
		||||
    def AddRowInfo(self, row, **kws):
 | 
			
		||||
        kws = CaseInsensitiveKeyedDict(kws)
 | 
			
		||||
        if not self.row_info.has_key(row):
 | 
			
		||||
            self.row_info[row] = kws
 | 
			
		||||
        else:
 | 
			
		||||
            DictMerge(self.row_info[row], kws)
 | 
			
		||||
 | 
			
		||||
    # What's the index for the row we just put in?
 | 
			
		||||
    def GetCurrentRowIndex(self):
 | 
			
		||||
        return len(self.cells)-1
 | 
			
		||||
 | 
			
		||||
    # What's the index for the col we just put in?
 | 
			
		||||
    def GetCurrentCellIndex(self):
 | 
			
		||||
        return len(self.cells[-1])-1
 | 
			
		||||
 | 
			
		||||
    def ExtractCellInfo(self, info):
 | 
			
		||||
        valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan',
 | 
			
		||||
                      'bgcolor']
 | 
			
		||||
        output = ''
 | 
			
		||||
 | 
			
		||||
        for (key, val) in info.items():
 | 
			
		||||
            if not key in valid_mods:
 | 
			
		||||
                continue
 | 
			
		||||
            if key == 'nowrap':
 | 
			
		||||
                output = output + ' NOWRAP'
 | 
			
		||||
                continue
 | 
			
		||||
            else:
 | 
			
		||||
                output = output + ' %s="%s"' % (key.upper(), val)
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def ExtractRowInfo(self, info):
 | 
			
		||||
        valid_mods = ['align', 'valign', 'bgcolor']
 | 
			
		||||
        output = ''
 | 
			
		||||
 | 
			
		||||
        for (key, val) in info.items():
 | 
			
		||||
            if not key in valid_mods:
 | 
			
		||||
                continue
 | 
			
		||||
            output = output + ' %s="%s"' % (key.upper(), val)
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def ExtractTableInfo(self, info):
 | 
			
		||||
        valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding',
 | 
			
		||||
                      'bgcolor']
 | 
			
		||||
 | 
			
		||||
        output = ''
 | 
			
		||||
 | 
			
		||||
        for (key, val) in info.items():
 | 
			
		||||
            if not key in valid_mods:
 | 
			
		||||
                continue
 | 
			
		||||
            if key == 'border' and val == None:
 | 
			
		||||
                output = output + ' BORDER'
 | 
			
		||||
                continue
 | 
			
		||||
            else:
 | 
			
		||||
                output = output + ' %s="%s"' % (key.upper(), val)
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def FormatCell(self, row, col, indent):
 | 
			
		||||
        try:
 | 
			
		||||
            my_info = self.cell_info[row][col]
 | 
			
		||||
        except:
 | 
			
		||||
            my_info = None
 | 
			
		||||
 | 
			
		||||
        output = '\n' + ' '*indent + '<td'
 | 
			
		||||
        if my_info:
 | 
			
		||||
            output = output + self.ExtractCellInfo(my_info)
 | 
			
		||||
        item = self.cells[row][col]
 | 
			
		||||
        item_format = HTMLFormatObject(item, indent+4)
 | 
			
		||||
        output = '%s>%s</td>' % (output, item_format)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def FormatRow(self, row, indent):
 | 
			
		||||
        try:
 | 
			
		||||
            my_info = self.row_info[row]
 | 
			
		||||
        except:
 | 
			
		||||
            my_info = None
 | 
			
		||||
 | 
			
		||||
        output = '\n' + ' '*indent + '<tr'
 | 
			
		||||
        if my_info:
 | 
			
		||||
            output = output + self.ExtractRowInfo(my_info)
 | 
			
		||||
        output = output + '>'
 | 
			
		||||
 | 
			
		||||
        for i in range(len(self.cells[row])):
 | 
			
		||||
            output = output + self.FormatCell(row, i, indent + 2)
 | 
			
		||||
 | 
			
		||||
        output = output + '\n' + ' '*indent + '</tr>'
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        output = '\n' + ' '*indent + '<table'
 | 
			
		||||
        output = output + self.ExtractTableInfo(self.opts)
 | 
			
		||||
        output = output + '>'
 | 
			
		||||
 | 
			
		||||
        for i in range(len(self.cells)):
 | 
			
		||||
            output = output + self.FormatRow(i, indent + 2)
 | 
			
		||||
 | 
			
		||||
        output = output + '\n' + ' '*indent + '</table>\n'
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Link:
 | 
			
		||||
    def __init__(self, href, text, target=None):
 | 
			
		||||
        self.href = href
 | 
			
		||||
        self.text = text
 | 
			
		||||
        self.target = target
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        texpr = ""
 | 
			
		||||
        if self.target != None:
 | 
			
		||||
            texpr = ' target="%s"' % self.target
 | 
			
		||||
        return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent),
 | 
			
		||||
                                          texpr,
 | 
			
		||||
                                          HTMLFormatObject(self.text, indent))
 | 
			
		||||
 | 
			
		||||
class FontSize:
 | 
			
		||||
    """FontSize is being deprecated - use FontAttr(..., size="...") instead."""
 | 
			
		||||
    def __init__(self, size, *items):
 | 
			
		||||
        self.items = list(items)
 | 
			
		||||
        self.size = size
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        output = '<font size="%s">' % self.size
 | 
			
		||||
        for item in self.items:
 | 
			
		||||
            output = output + HTMLFormatObject(item, indent)
 | 
			
		||||
        output = output + '</font>'
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class FontAttr:
 | 
			
		||||
    """Present arbitrary font attributes."""
 | 
			
		||||
    def __init__(self, *items, **kw):
 | 
			
		||||
        self.items = list(items)
 | 
			
		||||
        self.attrs = kw
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        seq = []
 | 
			
		||||
        for k, v in self.attrs.items():
 | 
			
		||||
            seq.append('%s="%s"' % (k, v))
 | 
			
		||||
        output = '<font %s>' % SPACE.join(seq)
 | 
			
		||||
        for item in self.items:
 | 
			
		||||
            output = output + HTMLFormatObject(item, indent)
 | 
			
		||||
        output = output + '</font>'
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Container:
 | 
			
		||||
    def __init__(self, *items):
 | 
			
		||||
        if not items:
 | 
			
		||||
            self.items = []
 | 
			
		||||
        else:
 | 
			
		||||
            self.items = items
 | 
			
		||||
 | 
			
		||||
    def AddItem(self, obj):
 | 
			
		||||
        self.items.append(obj)
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        output = []
 | 
			
		||||
        for item in self.items:
 | 
			
		||||
            output.append(HTMLFormatObject(item, indent))
 | 
			
		||||
        return EMPTYSTRING.join(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Label(Container):
 | 
			
		||||
    align = 'right'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *items):
 | 
			
		||||
        Container.__init__(self, *items)
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        return ('<div align="%s">' % self.align) + \
 | 
			
		||||
               Container.Format(self, indent) + \
 | 
			
		||||
               '</div>'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# My own standard document template.  YMMV.
 | 
			
		||||
# something more abstract would be more work to use...
 | 
			
		||||
 | 
			
		||||
class Document(Container):
 | 
			
		||||
    title = None
 | 
			
		||||
    language = None
 | 
			
		||||
    bgcolor = mm_cfg.WEB_BG_COLOR
 | 
			
		||||
    suppress_head = 0
 | 
			
		||||
 | 
			
		||||
    def set_language(self, lang=None):
 | 
			
		||||
        self.language = lang
 | 
			
		||||
 | 
			
		||||
    def set_bgcolor(self, color):
 | 
			
		||||
        self.bgcolor = color
 | 
			
		||||
 | 
			
		||||
    def SetTitle(self, title):
 | 
			
		||||
        self.title = title
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0, **kws):
 | 
			
		||||
        charset = 'us-ascii'
 | 
			
		||||
        if self.language and Utils.IsLanguage(self.language):
 | 
			
		||||
            charset = Utils.GetCharSet(self.language)
 | 
			
		||||
        output = ['Content-Type: text/html; charset=%s' % charset]
 | 
			
		||||
        output.append('Cache-control: no-cache\n')
 | 
			
		||||
        if not self.suppress_head:
 | 
			
		||||
            kws.setdefault('bgcolor', self.bgcolor)
 | 
			
		||||
            tab = ' ' * indent
 | 
			
		||||
            output.extend([tab,
 | 
			
		||||
                           '<HTML>',
 | 
			
		||||
                           '<HEAD>'
 | 
			
		||||
                           ])
 | 
			
		||||
            if mm_cfg.IMAGE_LOGOS:
 | 
			
		||||
                output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' %
 | 
			
		||||
                              (mm_cfg.IMAGE_LOGOS + mm_cfg.SHORTCUT_ICON))
 | 
			
		||||
            # Hit all the bases
 | 
			
		||||
            output.append('<META http-equiv="Content-Type" '
 | 
			
		||||
                          'content="text/html; charset=%s">' % charset)
 | 
			
		||||
            if self.title:
 | 
			
		||||
                output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
 | 
			
		||||
            # Add CSS to visually hide some labeling text but allow screen
 | 
			
		||||
            # readers to read it.
 | 
			
		||||
            output.append("""\
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
    div.hidden
 | 
			
		||||
        {position:absolute;
 | 
			
		||||
        left:-10000px;
 | 
			
		||||
        top:auto;
 | 
			
		||||
        width:1px;
 | 
			
		||||
        height:1px;
 | 
			
		||||
        overflow:hidden;}
 | 
			
		||||
</style>
 | 
			
		||||
""")
 | 
			
		||||
            if mm_cfg.WEB_HEAD_ADD:
 | 
			
		||||
                output.append(mm_cfg.WEB_HEAD_ADD)
 | 
			
		||||
            output.append('%s</HEAD>' % tab)
 | 
			
		||||
            quals = []
 | 
			
		||||
            # Default link colors
 | 
			
		||||
            if mm_cfg.WEB_VLINK_COLOR:
 | 
			
		||||
                kws.setdefault('vlink', mm_cfg.WEB_VLINK_COLOR)
 | 
			
		||||
            if mm_cfg.WEB_ALINK_COLOR:
 | 
			
		||||
                kws.setdefault('alink', mm_cfg.WEB_ALINK_COLOR)
 | 
			
		||||
            if mm_cfg.WEB_LINK_COLOR:
 | 
			
		||||
                kws.setdefault('link', mm_cfg.WEB_LINK_COLOR)
 | 
			
		||||
            for k, v in kws.items():
 | 
			
		||||
                quals.append('%s="%s"' % (k, v))
 | 
			
		||||
            output.append('%s<BODY %s' % (tab, SPACE.join(quals)))
 | 
			
		||||
            # Language direction
 | 
			
		||||
            direction = Utils.GetDirection(self.language)
 | 
			
		||||
            output.append('dir="%s">' % direction)
 | 
			
		||||
        # Always do this...
 | 
			
		||||
        output.append(Container.Format(self, indent))
 | 
			
		||||
        if not self.suppress_head:
 | 
			
		||||
            output.append('%s</BODY>' % tab)
 | 
			
		||||
            output.append('%s</HTML>' % tab)
 | 
			
		||||
        return NL.join(output)
 | 
			
		||||
 | 
			
		||||
    def addError(self, errmsg, tag=None):
 | 
			
		||||
        if tag is None:
 | 
			
		||||
            tag = _('Error: ')
 | 
			
		||||
        self.AddItem(Header(3, Bold(FontAttr(
 | 
			
		||||
            _(tag), color=mm_cfg.WEB_ERROR_COLOR, size='+2')).Format() +
 | 
			
		||||
                            Italic(errmsg).Format()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HeadlessDocument(Document):
 | 
			
		||||
    """Document without head section, for templates that provide their own."""
 | 
			
		||||
    suppress_head = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StdContainer(Container):
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        # If I don't start a new I ignore indent
 | 
			
		||||
        output = '<%s>' % self.tag
 | 
			
		||||
        output = output + Container.Format(self, indent)
 | 
			
		||||
        output = '%s</%s>' % (output, self.tag)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QuotedContainer(Container):
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        # If I don't start a new I ignore indent
 | 
			
		||||
        output = '<%s>%s</%s>' % (
 | 
			
		||||
            self.tag,
 | 
			
		||||
            Utils.websafe(Container.Format(self, indent)),
 | 
			
		||||
            self.tag)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class Header(StdContainer):
 | 
			
		||||
    def __init__(self, num, *items):
 | 
			
		||||
        self.items = items
 | 
			
		||||
        self.tag = 'h%d' % num
 | 
			
		||||
 | 
			
		||||
class Address(StdContainer):
 | 
			
		||||
    tag = 'address'
 | 
			
		||||
 | 
			
		||||
class Underline(StdContainer):
 | 
			
		||||
    tag = 'u'
 | 
			
		||||
 | 
			
		||||
class Bold(StdContainer):
 | 
			
		||||
    tag = 'strong'
 | 
			
		||||
 | 
			
		||||
class Italic(StdContainer):
 | 
			
		||||
    tag = 'em'
 | 
			
		||||
 | 
			
		||||
class Preformatted(QuotedContainer):
 | 
			
		||||
    tag = 'pre'
 | 
			
		||||
 | 
			
		||||
class Subscript(StdContainer):
 | 
			
		||||
    tag = 'sub'
 | 
			
		||||
 | 
			
		||||
class Superscript(StdContainer):
 | 
			
		||||
    tag = 'sup'
 | 
			
		||||
 | 
			
		||||
class Strikeout(StdContainer):
 | 
			
		||||
    tag = 'strike'
 | 
			
		||||
 | 
			
		||||
class Center(StdContainer):
 | 
			
		||||
    tag = 'center'
 | 
			
		||||
 | 
			
		||||
class Form(Container):
 | 
			
		||||
    def __init__(self, action='', method='POST', encoding=None,
 | 
			
		||||
                       mlist=None, contexts=None, user=None, *items):
 | 
			
		||||
        apply(Container.__init__, (self,) +  items)
 | 
			
		||||
        self.action = action
 | 
			
		||||
        self.method = method
 | 
			
		||||
        self.encoding = encoding
 | 
			
		||||
        self.mlist = mlist
 | 
			
		||||
        self.contexts = contexts
 | 
			
		||||
        self.user = user
 | 
			
		||||
 | 
			
		||||
    def set_action(self, action):
 | 
			
		||||
        self.action = action
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        spaces = ' ' * indent
 | 
			
		||||
        encoding = ''
 | 
			
		||||
        if self.encoding:
 | 
			
		||||
            encoding = 'enctype="%s"' % self.encoding
 | 
			
		||||
        output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
 | 
			
		||||
            spaces, self.action, self.method, encoding)
 | 
			
		||||
        if self.mlist:
 | 
			
		||||
            output = output + \
 | 
			
		||||
                '<input type="hidden" name="csrf_token" value="%s">\n' \
 | 
			
		||||
                % csrf_token(self.mlist, self.contexts, self.user)
 | 
			
		||||
        output = output + Container.Format(self, indent+2)
 | 
			
		||||
        output = '%s\n%s</FORM>\n' % (output, spaces)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InputObj:
 | 
			
		||||
    def __init__(self, name, ty, value, checked, **kws):
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.type = ty
 | 
			
		||||
        self.value = value
 | 
			
		||||
        self.checked = checked
 | 
			
		||||
        self.kws = kws
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        charset = get_translation().charset() or 'us-ascii'
 | 
			
		||||
        output = ['<INPUT name="%s" type="%s" value="%s"' %
 | 
			
		||||
                  (self.name, self.type, self.value)]
 | 
			
		||||
        for item in self.kws.items():
 | 
			
		||||
            output.append('%s="%s"' % item)
 | 
			
		||||
        if self.checked:
 | 
			
		||||
            output.append('CHECKED')
 | 
			
		||||
        output.append('>')
 | 
			
		||||
        ret = SPACE.join(output)
 | 
			
		||||
        if self.type == 'TEXT' and isinstance(ret, unicode):
 | 
			
		||||
            ret = ret.encode(charset, 'xmlcharrefreplace')
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SubmitButton(InputObj):
 | 
			
		||||
    def __init__(self, name, button_text):
 | 
			
		||||
        InputObj.__init__(self, name, "SUBMIT", button_text, checked=0)
 | 
			
		||||
 | 
			
		||||
class PasswordBox(InputObj):
 | 
			
		||||
    def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
 | 
			
		||||
        InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size)
 | 
			
		||||
 | 
			
		||||
class TextBox(InputObj):
 | 
			
		||||
    def __init__(self, name, value='', size=mm_cfg.TEXTFIELDWIDTH):
 | 
			
		||||
        if isinstance(value, str):
 | 
			
		||||
            safevalue = Utils.websafe(value)
 | 
			
		||||
        else:
 | 
			
		||||
            safevalue = value
 | 
			
		||||
        InputObj.__init__(self, name, "TEXT", safevalue, checked=0, size=size)
 | 
			
		||||
 | 
			
		||||
class Hidden(InputObj):
 | 
			
		||||
    def __init__(self, name, value=''):
 | 
			
		||||
        InputObj.__init__(self, name, 'HIDDEN', value, checked=0)
 | 
			
		||||
 | 
			
		||||
class TextArea:
 | 
			
		||||
    def __init__(self, name, text='', rows=None, cols=None, wrap='soft',
 | 
			
		||||
                 readonly=0):
 | 
			
		||||
        if isinstance(text, str):
 | 
			
		||||
            # Double escape HTML entities in non-readonly areas.
 | 
			
		||||
            doubleescape = not readonly
 | 
			
		||||
            safetext = Utils.websafe(text, doubleescape)
 | 
			
		||||
        else:
 | 
			
		||||
            safetext = text
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.text = safetext
 | 
			
		||||
        self.rows = rows
 | 
			
		||||
        self.cols = cols
 | 
			
		||||
        self.wrap = wrap
 | 
			
		||||
        self.readonly = readonly
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        charset = get_translation().charset() or 'us-ascii'
 | 
			
		||||
        output = '<TEXTAREA NAME=%s' % self.name
 | 
			
		||||
        if self.rows:
 | 
			
		||||
            output += ' ROWS=%s' % self.rows
 | 
			
		||||
        if self.cols:
 | 
			
		||||
            output += ' COLS=%s' % self.cols
 | 
			
		||||
        if self.wrap:
 | 
			
		||||
            output += ' WRAP=%s' % self.wrap
 | 
			
		||||
        if self.readonly:
 | 
			
		||||
            output += ' READONLY'
 | 
			
		||||
        output += '>%s</TEXTAREA>' % self.text
 | 
			
		||||
        if isinstance(output, unicode):
 | 
			
		||||
            output = output.encode(charset, 'xmlcharrefreplace')
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class FileUpload(InputObj):
 | 
			
		||||
    def __init__(self, name, rows=None, cols=None, **kws):
 | 
			
		||||
        apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws)
 | 
			
		||||
 | 
			
		||||
class RadioButton(InputObj):
 | 
			
		||||
    def __init__(self, name, value, checked=0, **kws):
 | 
			
		||||
        apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws)
 | 
			
		||||
 | 
			
		||||
class CheckBox(InputObj):
 | 
			
		||||
    def __init__(self, name, value, checked=0, **kws):
 | 
			
		||||
        apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws)
 | 
			
		||||
 | 
			
		||||
class VerticalSpacer:
 | 
			
		||||
    def __init__(self, size=10):
 | 
			
		||||
        self.size = size
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        output = '<spacer type="vertical" height="%d">' % self.size
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class WidgetArray:
 | 
			
		||||
    Widget = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, button_names, checked, horizontal, values):
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.button_names = button_names
 | 
			
		||||
        self.checked = checked
 | 
			
		||||
        self.horizontal = horizontal
 | 
			
		||||
        self.values = values
 | 
			
		||||
        assert len(values) == len(button_names)
 | 
			
		||||
        # Don't assert `checked' because for RadioButtons it is a scalar while
 | 
			
		||||
        # for CheckedBoxes it is a vector.  Subclasses will assert length.
 | 
			
		||||
 | 
			
		||||
    def ischecked(self, i):
 | 
			
		||||
        raise NotImplemented
 | 
			
		||||
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        t = Table(cellspacing=5)
 | 
			
		||||
        items = []
 | 
			
		||||
        for i, name, value in zip(range(len(self.button_names)),
 | 
			
		||||
                                  self.button_names,
 | 
			
		||||
                                  self.values):
 | 
			
		||||
            ischecked = (self.ischecked(i))
 | 
			
		||||
            item = ('<label>' +
 | 
			
		||||
                    self.Widget(self.name, value, ischecked).Format() +
 | 
			
		||||
                    name + '</label>')
 | 
			
		||||
            items.append(item)
 | 
			
		||||
            if not self.horizontal:
 | 
			
		||||
                t.AddRow(items)
 | 
			
		||||
                items = []
 | 
			
		||||
        if self.horizontal:
 | 
			
		||||
            t.AddRow(items)
 | 
			
		||||
        return t.Format(indent)
 | 
			
		||||
 | 
			
		||||
class RadioButtonArray(WidgetArray):
 | 
			
		||||
    Widget = RadioButton
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, button_names, checked=None, horizontal=1,
 | 
			
		||||
                 values=None):
 | 
			
		||||
        if values is None:
 | 
			
		||||
            values = range(len(button_names))
 | 
			
		||||
        # BAW: assert checked is a scalar...
 | 
			
		||||
        WidgetArray.__init__(self, name, button_names, checked, horizontal,
 | 
			
		||||
                             values)
 | 
			
		||||
 | 
			
		||||
    def ischecked(self, i):
 | 
			
		||||
        return self.checked == i
 | 
			
		||||
 | 
			
		||||
class CheckBoxArray(WidgetArray):
 | 
			
		||||
    Widget = CheckBox
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, button_names, checked=None, horizontal=0,
 | 
			
		||||
                 values=None):
 | 
			
		||||
        if checked is None:
 | 
			
		||||
            checked = [0] * len(button_names)
 | 
			
		||||
        else:
 | 
			
		||||
            assert len(checked) == len(button_names)
 | 
			
		||||
        if values is None:
 | 
			
		||||
            values = range(len(button_names))
 | 
			
		||||
        WidgetArray.__init__(self, name, button_names, checked, horizontal,
 | 
			
		||||
                             values)
 | 
			
		||||
 | 
			
		||||
    def ischecked(self, i):
 | 
			
		||||
        return self.checked[i]
 | 
			
		||||
 | 
			
		||||
class UnorderedList(Container):
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        spaces = ' ' * indent
 | 
			
		||||
        output = '\n%s<ul>\n' % spaces
 | 
			
		||||
        for item in self.items:
 | 
			
		||||
            output = output + '%s<li>%s\n' % \
 | 
			
		||||
                     (spaces, HTMLFormatObject(item, indent + 2))
 | 
			
		||||
        output = output + '%s</ul>\n' % spaces
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class OrderedList(Container):
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        spaces = ' ' * indent
 | 
			
		||||
        output = '\n%s<ol>\n' % spaces
 | 
			
		||||
        for item in self.items:
 | 
			
		||||
            output = output + '%s<li>%s\n' % \
 | 
			
		||||
                     (spaces, HTMLFormatObject(item, indent + 2))
 | 
			
		||||
        output = output + '%s</ol>\n' % spaces
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
class DefinitionList(Container):
 | 
			
		||||
    def Format(self, indent=0):
 | 
			
		||||
        spaces = ' ' * indent
 | 
			
		||||
        output = '\n%s<dl>\n' % spaces
 | 
			
		||||
        for dt, dd in self.items:
 | 
			
		||||
            output = output + '%s<dt>%s\n<dd>%s\n' % \
 | 
			
		||||
                     (spaces, HTMLFormatObject(dt, indent+2),
 | 
			
		||||
                      HTMLFormatObject(dd, indent+2))
 | 
			
		||||
        output = output + '%s</dl>\n' % spaces
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Logo constants
 | 
			
		||||
#
 | 
			
		||||
# These are the URLs which the image logos link to.  The Mailman home page now
 | 
			
		||||
# points at the gnu.org site instead of the www.list.org mirror.
 | 
			
		||||
#
 | 
			
		||||
from mm_cfg import MAILMAN_URL
 | 
			
		||||
PYTHON_URL  = 'http://www.python.org/'
 | 
			
		||||
GNU_URL     = 'http://www.gnu.org/'
 | 
			
		||||
CRANS_URL   = 'http://www.crans.org/'
 | 
			
		||||
 | 
			
		||||
# The names of the image logo files.  These are concatentated onto
 | 
			
		||||
# mm_cfg.IMAGE_LOGOS (not urljoined).
 | 
			
		||||
DELIVERED_BY = 'mailman.jpg'
 | 
			
		||||
PYTHON_POWERED = 'PythonPowered.png'
 | 
			
		||||
GNU_HEAD = 'gnu-head-tiny.jpg'
 | 
			
		||||
CRANS_LOGO = 'crans.png'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def MailmanLogo():
 | 
			
		||||
    t = Table(border=0, width='100%')
 | 
			
		||||
 | 
			
		||||
    version = mm_cfg.VERSION
 | 
			
		||||
    mmlink = _("Delivered by Mailman")
 | 
			
		||||
    pylink = _("Python Powered")
 | 
			
		||||
    gnulink = _("GNU's Not Unix")
 | 
			
		||||
    cranslink = _("CRANS")
 | 
			
		||||
    if mm_cfg.SITE_LINK:
 | 
			
		||||
        sitelink = mm_cfg.SITE_TEXT
 | 
			
		||||
 | 
			
		||||
    if mm_cfg.IMAGE_LOGOS:
 | 
			
		||||
        def logo(file, alt, base=mm_cfg.IMAGE_LOGOS):
 | 
			
		||||
            return '<img src="%s" alt="%s" border="0" />' % \
 | 
			
		||||
              (base + file, alt)
 | 
			
		||||
        mmlink = logo(DELIVERED_BY, mmlink)
 | 
			
		||||
        pylink = logo(PYTHON_POWERED, pylink)
 | 
			
		||||
        gnulink = logo(GNU_HEAD, gnulink)
 | 
			
		||||
        cranslink = logo(CRANS_LOGO, cranslink)
 | 
			
		||||
        if mm_cfg.SITE_LINK:
 | 
			
		||||
            sitelink = logo(mm_cfg.SITE_LOGO, sitelink, "")
 | 
			
		||||
 | 
			
		||||
    mmlink = Link(MAILMAN_URL, mmlink + _('<br>version %(version)s'))
 | 
			
		||||
    pylink = Link(PYTHON_URL, pylink)
 | 
			
		||||
    gnulink = Link(GNU_URL, gnulink)
 | 
			
		||||
    cranslink = Link(CRANS_URL, cranslink)
 | 
			
		||||
    links = [mmlink, pylink, gnulink, cranslink]
 | 
			
		||||
    if mm_cfg.SITE_LINK:
 | 
			
		||||
        if mm_cfg.SITE_URL:
 | 
			
		||||
            sitelink = Link(mm_cfg.SITE_URL, sitelink)
 | 
			
		||||
        links.append(sitelink)
 | 
			
		||||
    t.AddRow(links)
 | 
			
		||||
    return t
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SelectOptions:
 | 
			
		||||
   def __init__(self, varname, values, legend,
 | 
			
		||||
                selected=0, size=1, multiple=None):
 | 
			
		||||
      self.varname  = varname
 | 
			
		||||
      self.values   = values
 | 
			
		||||
      self.legend   = legend
 | 
			
		||||
      self.size     = size
 | 
			
		||||
      self.multiple = multiple
 | 
			
		||||
      # we convert any type to tuple, commas are needed
 | 
			
		||||
      if not multiple:
 | 
			
		||||
         if type(selected) == types.IntType:
 | 
			
		||||
             self.selected = (selected,)
 | 
			
		||||
         elif type(selected) == types.TupleType:
 | 
			
		||||
             self.selected = (selected[0],)
 | 
			
		||||
         elif type(selected) == types.ListType:
 | 
			
		||||
             self.selected = (selected[0],)
 | 
			
		||||
         else:
 | 
			
		||||
             self.selected = (0,)
 | 
			
		||||
 | 
			
		||||
   def Format(self, indent=0):
 | 
			
		||||
      spaces = " " * indent
 | 
			
		||||
      items  = min( len(self.values), len(self.legend) )
 | 
			
		||||
 | 
			
		||||
      # jcrey: If there is no argument, we return nothing to avoid errors
 | 
			
		||||
      if items == 0:
 | 
			
		||||
          return ""
 | 
			
		||||
 | 
			
		||||
      text = "\n" + spaces + "<Select name=\"%s\"" % self.varname
 | 
			
		||||
      if self.size > 1:
 | 
			
		||||
          text = text + " size=%d" % self.size
 | 
			
		||||
      if self.multiple:
 | 
			
		||||
          text = text + " multiple"
 | 
			
		||||
      text = text + ">\n"
 | 
			
		||||
 | 
			
		||||
      for i in range(items):
 | 
			
		||||
          if i in self.selected:
 | 
			
		||||
              checked = " Selected"
 | 
			
		||||
          else:
 | 
			
		||||
              checked = ""
 | 
			
		||||
 | 
			
		||||
          opt = " <option value=\"%s\"%s> %s </option>" % (
 | 
			
		||||
              self.values[i], checked, self.legend[i])
 | 
			
		||||
          text = text + spaces + opt + "\n"
 | 
			
		||||
 | 
			
		||||
      return text + spaces + '</Select>'
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +19,7 @@
 | 
			
		|||
    dest: "/etc/mtail/{{ item }}"
 | 
			
		||||
  loop:
 | 
			
		||||
    - dhcpd.mtail
 | 
			
		||||
    - radiusd.mtail
 | 
			
		||||
  notify: Restart mtail
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
# radiusd template by erdnaxe@crans.org
 | 
			
		||||
 | 
			
		||||
# Define the exported metric names.  The `by' keyword indicates the metric has
 | 
			
		||||
# dimensions.  For example, `request_total' counts the frequency of each
 | 
			
		||||
# request's "command".  The name `command' will be exported as the label name
 | 
			
		||||
# for the metric.  The command provided in the code below will be exported as
 | 
			
		||||
# the label value.
 | 
			
		||||
counter radiusd_access_ok
 | 
			
		||||
counter radiusd_access_refused by reason
 | 
			
		||||
 | 
			
		||||
# The `syslog' decorator defines a procedure.  When a block of mtail code is
 | 
			
		||||
# "decorated", it is called before entering the block.  The block is entered
 | 
			
		||||
# when the keyword `next' is reached.
 | 
			
		||||
def syslog {
 | 
			
		||||
  /^(?P<date>(?P<legacy_date>\w+\s+\d+\s+\d+:\d+:\d+)|(?P<rfc3339_date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+[+-]\d{2}:\d{2}))/ +
 | 
			
		||||
  /\s+(?:\w+@)?(?P<hostname>[\w\.-]+)\s+(?P<application>[\w\.-]+)(?:\[(?P<pid>\d+)\])?:\s+(?P<message>.*)/ {
 | 
			
		||||
    # If the legacy_date regexp matched, try this format.
 | 
			
		||||
    len($legacy_date) > 0 {
 | 
			
		||||
      strptime($2, "Jan _2 15:04:05")
 | 
			
		||||
    }
 | 
			
		||||
    # If the RFC3339 style matched, parse it this way.
 | 
			
		||||
    len($rfc3339_date) > 0 {
 | 
			
		||||
      strptime($rfc3339_date, "2006-01-02T15:04:05.999999999Z07:00")
 | 
			
		||||
    }
 | 
			
		||||
    # Call into the decorated block
 | 
			
		||||
    next
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Define some pattern constants for reuse in the patterns below.
 | 
			
		||||
const IP /\d+(\.\d+){3}/
 | 
			
		||||
const MATCH_IP /(?P<ip>/ + IP + /)/
 | 
			
		||||
const MATCH_NETWORK /(?P<network>\d+(\.\d+){1,3}\/\d+)/
 | 
			
		||||
const MATCH_MAC /(?P<mac>([\da-f]{2}:){5}[\da-f]{2})/
 | 
			
		||||
 | 
			
		||||
@syslog {
 | 
			
		||||
    # Access ok!
 | 
			
		||||
    /Access ok/ {
 | 
			
		||||
        radiusd_access_ok++
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Bouh!
 | 
			
		||||
    /Adherent non cotisant/ {
 | 
			
		||||
        radiusd_access_refused["Did not pay"]++
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Reload nginx
 | 
			
		||||
  systemd:
 | 
			
		||||
    name: nginx
 | 
			
		||||
    state: reloaded
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Install NGINX
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - nginx
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Copy configuration files
 | 
			
		||||
  template:
 | 
			
		||||
    src: "{{ item.src }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
  loop:
 | 
			
		||||
    - src: nginx/sites-available/mailman.j2
 | 
			
		||||
      dest: /etc/nginx/sites-available/mailman
 | 
			
		||||
    - src: nginx/mailman_passwd.j2
 | 
			
		||||
      dest: /etc/nginx/mailman_passwd
 | 
			
		||||
    - src: nginx/snippets/fastcgi-mailman.conf.j2
 | 
			
		||||
      dest: /etc/nginx/snippets/fastcgi-mailman.conf
 | 
			
		||||
    - src: nginx/snippets/options-ssl.conf.j2
 | 
			
		||||
      dest: /etc/nginx/snippets/options-ssl.conf
 | 
			
		||||
    - src: var/www/robots.txt.j2
 | 
			
		||||
      dest: /var/www/robots.txt
 | 
			
		||||
    - src: var/www/custom_401.html.j2
 | 
			
		||||
      dest: /var/www/custom_401.html
 | 
			
		||||
  notify: Reload nginx
 | 
			
		||||
 | 
			
		||||
- name: Enable mailman
 | 
			
		||||
  file:
 | 
			
		||||
    src: /etc/nginx/sites-available/mailman
 | 
			
		||||
    dest: /etc/nginx/sites-enabled/mailman
 | 
			
		||||
    state: link
 | 
			
		||||
    force: true
 | 
			
		||||
  when: not ansible_check_mode
 | 
			
		||||
  notify: Reload nginx
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
  template:
 | 
			
		||||
    src: update-motd.d/05-service.j2
 | 
			
		||||
    dest: /etc/update-motd.d/05-nginx-mailman
 | 
			
		||||
    mode: 0755
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
Stop:$apr1$NXaV5H7Q$J3ora3Jo5h775Y1nm93PN1
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
server {
 | 
			
		||||
	listen 80 default;
 | 
			
		||||
	listen [::]:80 default;
 | 
			
		||||
 | 
			
		||||
	server_name _;
 | 
			
		||||
 | 
			
		||||
	location / {
 | 
			
		||||
	    return 302 https://{{ mailman.default_host }}$request_uri;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Redirect everybody to mailing lists
 | 
			
		||||
server {
 | 
			
		||||
	listen 443 default_server ssl;
 | 
			
		||||
	listen [::]:443 default_server ssl;
 | 
			
		||||
	server_name _;
 | 
			
		||||
 | 
			
		||||
	include "/etc/nginx/snippets/options-ssl.conf";
 | 
			
		||||
 | 
			
		||||
	location / {
 | 
			
		||||
		 return 302 https://{{ mailman.default_host }}$request_uri;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
server {
 | 
			
		||||
	listen 443 ssl http2;
 | 
			
		||||
	listen [::]:443 ssl http2;
 | 
			
		||||
	server_name {{ mailman.default_host }};
 | 
			
		||||
 | 
			
		||||
	include "/etc/nginx/snippets/options-ssl.conf";
 | 
			
		||||
 | 
			
		||||
	root /usr/lib/cgi-bin/mailman/;
 | 
			
		||||
	index index.htm index.html;
 | 
			
		||||
 | 
			
		||||
        location /error/ {
 | 
			
		||||
		internal;
 | 
			
		||||
		alias /var/www/;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
	location /create {
 | 
			
		||||
		default_type text/html;
 | 
			
		||||
		alias /etc/mailman/create.html;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	location ~ ^/$ {
 | 
			
		||||
		return 302 https://{{ mailman.default_host }}/listinfo;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	location / {
 | 
			
		||||
		include "/etc/nginx/snippets/fastcgi-mailman.conf";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
        location ~ ^/listinfo {
 | 
			
		||||
                satisfy any;
 | 
			
		||||
		include "/etc/nginx/snippets/fastcgi-mailman.conf";
 | 
			
		||||
 | 
			
		||||
		{% for net in mynetworks -%}
 | 
			
		||||
                allow {{ net }};
 | 
			
		||||
		{% endfor -%}
 | 
			
		||||
                deny all;
 | 
			
		||||
 | 
			
		||||
	        auth_basic {{ mailman.auth_basic }}
 | 
			
		||||
		auth_basic_user_file /etc/nginx/mailman_passwd;
 | 
			
		||||
 | 
			
		||||
		error_page 401 /error/custom_401.html;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        location ~ ^/admin {
 | 
			
		||||
                satisfy any;
 | 
			
		||||
 | 
			
		||||
		include "/etc/nginx/snippets/fastcgi-mailman.conf";
 | 
			
		||||
 | 
			
		||||
		{% for net in mynetworks -%}
 | 
			
		||||
                allow {{ net }};
 | 
			
		||||
		{% endfor -%}
 | 
			
		||||
                deny all;
 | 
			
		||||
 | 
			
		||||
	        auth_basic {{ mailman.auth_basic }}
 | 
			
		||||
		auth_basic_user_file /etc/nginx/mailman_passwd;
 | 
			
		||||
		error_page 401 /error/custom_401.html;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	location /images/mailman { alias /usr/share/images/mailman;}
 | 
			
		||||
 | 
			
		||||
	location /robots.txt { alias /var/www/robots.txt;}
 | 
			
		||||
 | 
			
		||||
	location /archives {
 | 
			
		||||
		alias /var/lib/mailman/archives/public;
 | 
			
		||||
		autoindex on;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
 | 
			
		||||
fastcgi_split_path_info (^/[^/]*)(.*)$;
 | 
			
		||||
 | 
			
		||||
# check that the PHP script exists before passing it
 | 
			
		||||
try_files $fastcgi_script_name =404;
 | 
			
		||||
 | 
			
		||||
# Bypass the fact that try_files resets $fastcgi_path_info
 | 
			
		||||
# see: http://trac.nginx.org/nginx/ticket/321
 | 
			
		||||
set $path_info $fastcgi_path_info;
 | 
			
		||||
fastcgi_param PATH_INFO $path_info;
 | 
			
		||||
 | 
			
		||||
# Let NGINX handle errors
 | 
			
		||||
fastcgi_intercept_errors on;
 | 
			
		||||
 | 
			
		||||
include /etc/nginx/fastcgi.conf;
 | 
			
		||||
fastcgi_pass unix:/var/run/fcgiwrap.socket;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
 | 
			
		||||
fastcgi_split_path_info (^/[^/]*)(.*)$;
 | 
			
		||||
 | 
			
		||||
# check that the PHP script exists before passing it
 | 
			
		||||
try_files $fastcgi_script_name =404;
 | 
			
		||||
 | 
			
		||||
# Bypass the fact that try_files resets $fastcgi_path_info
 | 
			
		||||
# see: http://trac.nginx.org/nginx/ticket/321
 | 
			
		||||
set $path_info $fastcgi_path_info;
 | 
			
		||||
fastcgi_param PATH_INFO $path_info;
 | 
			
		||||
 | 
			
		||||
# Let NGINX handle errors
 | 
			
		||||
fastcgi_intercept_errors on;
 | 
			
		||||
 | 
			
		||||
include /etc/nginx/fastcgi.conf;
 | 
			
		||||
fastcgi_pass unix:/var/run/fcgiwrap.socket;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
ssl_certificate {{ nginx.ssl.cert }};
 | 
			
		||||
ssl_certificate_key {{ nginx.ssl.key }};
 | 
			
		||||
ssl_session_timeout 1d;
 | 
			
		||||
ssl_session_cache shared:MozSSL:10m;
 | 
			
		||||
ssl_session_tickets off;
 | 
			
		||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
 | 
			
		||||
ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
 | 
			
		||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
 | 
			
		||||
ssl_prefer_server_ciphers off;
 | 
			
		||||
 | 
			
		||||
# Enable OCSP Stapling, point to certificate chain
 | 
			
		||||
ssl_stapling on;
 | 
			
		||||
ssl_stapling_verify on;
 | 
			
		||||
ssl_trusted_certificate {{ nginx.ssl.trusted_cert }};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/tail +14
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
[0m> [38;5;82mNGINX[0m a été déployé sur cette machine. Voir [38;5;6m/etc/nginx/[0m.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
{{ ansible_header | comment('xml') }}
 | 
			
		||||
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <title>Accès refusé</title>
 | 
			
		||||
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <h1>Accès refusé</h1>
 | 
			
		||||
  <p>
 | 
			
		||||
    Pour éviter le scan des adresses de diffusions par un robot, cette page demande un identifiant et mot de passe.
 | 
			
		||||
  </p>
 | 
			
		||||
  <ul>
 | 
			
		||||
    <li>Identifiant : <em>Stop</em></li>
 | 
			
		||||
    <li>Mot de passe : <em>Spam</em></li>
 | 
			
		||||
  </ul>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
User-agent: *
 | 
			
		||||
Disallow: /
 | 
			
		||||
| 
						 | 
				
			
			@ -2,9 +2,7 @@
 | 
			
		|||
- name: Install NGINX
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - nginx
 | 
			
		||||
      - python3-certbot-nginx  # for options-ssl-nginx.conf
 | 
			
		||||
    name: nginx
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +15,16 @@
 | 
			
		|||
    - options-ssl.conf
 | 
			
		||||
    - options-proxypass.conf
 | 
			
		||||
 | 
			
		||||
- name: Has dhparam been copied?
 | 
			
		||||
  stat:
 | 
			
		||||
    path: /etc/letsencrypt/dhparam
 | 
			
		||||
  register: stat_result
 | 
			
		||||
 | 
			
		||||
- name: Copy dhparam
 | 
			
		||||
  template:
 | 
			
		||||
    src: letsencrypt/dhparam.j2
 | 
			
		||||
    dest: /etc/letsencrypt/dhparam
 | 
			
		||||
  when: not stat_result.stat.exists
 | 
			
		||||
 | 
			
		||||
- name: Copy reverse proxy sites
 | 
			
		||||
  template:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ server {
 | 
			
		|||
 | 
			
		||||
    # Keep the TCP connection open a bit for faster browsing
 | 
			
		||||
    keepalive_timeout 70;
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
    # Custom error page
 | 
			
		||||
    error_page  500 502 503 504  /50x.html;
 | 
			
		||||
    location = /50x.html {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,3 +15,5 @@ proxy_http_version 1.1;
 | 
			
		|||
proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
proxy_set_header Connection $connection_upgrade;
 | 
			
		||||
 | 
			
		||||
# For Owncloud WebDav
 | 
			
		||||
client_max_body_size 10G;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
ssl_certificate {{ nginx.ssl.cert }};
 | 
			
		||||
ssl_certificate_key {{ nginx.ssl.cert_key }};
 | 
			
		||||
ssl_session_timeout 1d;
 | 
			
		||||
ssl_session_cache shared:MozSSL:10m;  
 | 
			
		||||
ssl_session_cache shared:MozSSL:10m;
 | 
			
		||||
ssl_session_tickets off;
 | 
			
		||||
ssl_dhparam /etc/letsencrypt/dhparam;
 | 
			
		||||
ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Install opendkim
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - opendkim
 | 
			
		||||
      - opendkim-tools
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Ensure opendkim directories are here
 | 
			
		||||
  file:
 | 
			
		||||
    path: /etc/opendkim/keys/crans.org
 | 
			
		||||
    state: directory
 | 
			
		||||
    mode: 0750
 | 
			
		||||
    owner: opendkim
 | 
			
		||||
    group: opendkim
 | 
			
		||||
  when: not ansible_check_mode
 | 
			
		||||
 | 
			
		||||
- name: Deploy opendkim configuration
 | 
			
		||||
  template:
 | 
			
		||||
    src: opendkim.conf.j2
 | 
			
		||||
    dest: /etc/opendkim.conf
 | 
			
		||||
    mode: 644
 | 
			
		||||
    owner: opendkim
 | 
			
		||||
    group: opendkim
 | 
			
		||||
 | 
			
		||||
- name: Deploy opendkim configuration
 | 
			
		||||
  template:
 | 
			
		||||
    src: opendkim/{{ item }}.j2
 | 
			
		||||
    dest: /etc/opendkim/{{ item }}
 | 
			
		||||
    mode: 0644
 | 
			
		||||
    owner: opendkim
 | 
			
		||||
    group: opendkim
 | 
			
		||||
  loop:
 | 
			
		||||
    - KeyTable
 | 
			
		||||
    - SigningTable
 | 
			
		||||
    - TrustedHosts
 | 
			
		||||
 | 
			
		||||
- name: Deploy opendkim key
 | 
			
		||||
  template:
 | 
			
		||||
    src: opendkim/keys/crans.org/{{ item }}.j2
 | 
			
		||||
    dest: /etc/opendkim/keys/crans.org/{{ item }}
 | 
			
		||||
    mode: 0600
 | 
			
		||||
    owner: opendkim
 | 
			
		||||
    group: opendkim
 | 
			
		||||
  loop:
 | 
			
		||||
    - mail.private
 | 
			
		||||
    - mail.txt
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,110 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# This is a basic configuration that can easily be adapted to suit a standard
 | 
			
		||||
# installation. For more advanced options, see opendkim.conf(5) and/or
 | 
			
		||||
# /usr/share/doc/opendkim/examples/opendkim.conf.sample.
 | 
			
		||||
 | 
			
		||||
AutoRestart             Yes
 | 
			
		||||
AutoRestartRate         10/1h
 | 
			
		||||
 | 
			
		||||
# Log to syslog
 | 
			
		||||
Syslog          yes
 | 
			
		||||
SyslogSuccess           Yes
 | 
			
		||||
LogWhy                  Yes
 | 
			
		||||
# Required to use local socket with MTAs that access the socket as a non-
 | 
			
		||||
# privileged user (e.g. Postfix)
 | 
			
		||||
UMask           002
 | 
			
		||||
 | 
			
		||||
# Sign for example.com with key in /etc/mail/dkim.key using
 | 
			
		||||
# selector '2007' (e.g. 2007._domainkey.example.com)
 | 
			
		||||
#Domain         example.com
 | 
			
		||||
#KeyFile        /etc/mail/dkim.key
 | 
			
		||||
#Selector       2007
 | 
			
		||||
 | 
			
		||||
# Commonly-used options; the commented-out versions show the defaults.
 | 
			
		||||
Canonicalization    relaxed/simple
 | 
			
		||||
 | 
			
		||||
#mode			sv
 | 
			
		||||
#subdomains		no
 | 
			
		||||
 | 
			
		||||
# socket smtp://localhost
 | 
			
		||||
#
 | 
			
		||||
# ##  socket socketspec
 | 
			
		||||
# ##
 | 
			
		||||
# ##  names the socket where this filter should listen for milter connections
 | 
			
		||||
# ##  from the mta.  required.  should be in one of these forms:
 | 
			
		||||
# ##
 | 
			
		||||
# ##  inet:port@address           to listen on a specific interface
 | 
			
		||||
# ##  inet:port                   to listen on all interfaces
 | 
			
		||||
# ##  local:/path/to/socket       to listen on a unix domain socket
 | 
			
		||||
#
 | 
			
		||||
#socket                  inet:8892@localhost
 | 
			
		||||
socket                  inet:12301@localhost
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##  pidfile filename
 | 
			
		||||
###      default (none)
 | 
			
		||||
###
 | 
			
		||||
###  name of the file where the filter should write its pid before beginning
 | 
			
		||||
###  normal operations.
 | 
			
		||||
#
 | 
			
		||||
pidfile                 /var/run/opendkim/opendkim.pid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# list domains to use for rfc 6541 dkim authorized third-party signatures
 | 
			
		||||
# (atps) (experimental)
 | 
			
		||||
 | 
			
		||||
#atpsdomains        example.com
 | 
			
		||||
 | 
			
		||||
signaturealgorithm      rsa-sha256
 | 
			
		||||
 | 
			
		||||
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
 | 
			
		||||
InternalHosts           refile:/etc/opendkim/TrustedHosts
 | 
			
		||||
KeyTable                refile:/etc/opendkim/KeyTable
 | 
			
		||||
SigningTable            refile:/etc/opendkim/SigningTable
 | 
			
		||||
 | 
			
		||||
Mode            sv
 | 
			
		||||
#SubDomains     no
 | 
			
		||||
#ADSPDiscard        no
 | 
			
		||||
 | 
			
		||||
# Always oversign From (sign using actual From and a null From to prevent
 | 
			
		||||
# malicious signatures header fields (From and/or others) between the signer
 | 
			
		||||
# and the verifier.  From is oversigned by default in the Debian pacakge
 | 
			
		||||
# because it is often the identity key used by reputation systems and thus
 | 
			
		||||
# somewhat security sensitive.
 | 
			
		||||
OversignHeaders     From
 | 
			
		||||
 | 
			
		||||
##  resolverconfiguration filename
 | 
			
		||||
##      default (none)
 | 
			
		||||
##
 | 
			
		||||
##  specifies a configuration file to be passed to the unbound library that
 | 
			
		||||
##  performs dns queries applying the dnssec protocol.  see the unbound
 | 
			
		||||
##  documentation at http://unbound.net for the expected content of this file.
 | 
			
		||||
##  the results of using this and the trustanchorfile setting at the same
 | 
			
		||||
##  time are undefined.
 | 
			
		||||
##  in debian, /etc/unbound/unbound.conf is shipped as part of the suggested
 | 
			
		||||
##  unbound package
 | 
			
		||||
 | 
			
		||||
# resolverconfiguration     /etc/unbound/unbound.conf
 | 
			
		||||
 | 
			
		||||
##  trustanchorfile filename
 | 
			
		||||
##      default (none)
 | 
			
		||||
##
 | 
			
		||||
## specifies a file from which trust anchor data should be read when doing
 | 
			
		||||
## dns queries and applying the dnssec protocol.  see the unbound documentation
 | 
			
		||||
## at http://unbound.net for the expected format of this file.
 | 
			
		||||
 | 
			
		||||
trustanchorfile       /usr/share/dns/root.key
 | 
			
		||||
 | 
			
		||||
##  userid userid
 | 
			
		||||
###      default (none)
 | 
			
		||||
###
 | 
			
		||||
###  change to user "userid" before starting normal operation?  may include
 | 
			
		||||
###  a group id as well, separated from the userid by a colon.
 | 
			
		||||
#
 | 
			
		||||
userid                  opendkim:opendkim
 | 
			
		||||
 | 
			
		||||
#   Whether to decode non- UTF-8 and non-ASCII textual parts and recode
 | 
			
		||||
#   them to UTF-8 before the text is given over to rules processing.
 | 
			
		||||
#
 | 
			
		||||
# normalize_charset 1
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
mail._domainkey.crans.org crans.org:mail:/etc/opendkim/keys/crans.org/mail.private
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
*@crans.org mail._domainkey.crans.org
 | 
			
		||||
*@crans.eu  mail._domainkey.crans.org
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
127.0.0.1
 | 
			
		||||
localhost
 | 
			
		||||
::1
 | 
			
		||||
 | 
			
		||||
138.231.136.0/21
 | 
			
		||||
138.231.144.0/21
 | 
			
		||||
 | 
			
		||||
10.231.136.0/24
 | 
			
		||||
10.2.9.0/24
 | 
			
		||||
 | 
			
		||||
2a0c:700:0:1::/64
 | 
			
		||||
2a0c:700:0:2::/64
 | 
			
		||||
2a0c:700:0:21::/64
 | 
			
		||||
2a0c:700:0:22::/64
 | 
			
		||||
2a0c:700:0:23::/64
 | 
			
		||||
 | 
			
		||||
*.crans.org
 | 
			
		||||
*.crans.fr
 | 
			
		||||
*.crans.eu
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
{{ opendkim.private_key }}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
mail._domainkey IN TXT "v=DKIM1; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtwkNVd9Mmz8S4WcfuPk0X2drG39gS8+uxAv8igRILgzWeN8j2hjeZesl8pm/1UTVU87bYcdfUgXiGfQy9nR5p/Vmt2kS7sXk9nsJ/VYENgb3IJQ6paWupSTFMyeKycJ4ZHCEZB/bVvifoG6vLKqW5jpsfCiOcfdcgXATn0UPuVx9t93yRrhoEMntMv9TSodjqd3FKCtJUoh5cNQHo0T6dWKtxoIgNi/mvZ92D/IACwu/XOU+Rq9fnoEI8GukBQUR5AkP0B/JrvwWXWX/3EjY8X37ljEX0XUdq/ShzTl5iK+CM83stgkFUQh/rpww5mnxYEW3X4uirJ7VJHmY4KPoIU+2DPjLQj9Hz63CMWY3Ks2pXWzxD3V+GI1aJTMFOv2LeHnI3ScqFaKj9FR4ZKMb0OW2BEFBIY3J3aeo/paRwdbVCMM7twDtZY9uInR/NhVa1v9hlOxwp4/2pGSKQYoN2CkAZ1Alzwf8M3EONLKeiC43JLYwKH1uBB1oikSVhMnLjG0219XvfG/tphyoOqJR/bCc2rdv5pLwKUl4wVuygfpvOw12bcvnTfYuk/BXzVHg9t4H8k/DJR6GAoeNAapXIS8AfAScF8QdKfplhKLJyQGJ6lQ75YD9IwRAN0oV+8NTjl46lI/C+b7mpfXCew+p6YPwfNvV2shiR0Ez8ZGUQIcCAwEAAQ==" ; ----- DKIM key mail for crans.org
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
- name: Install policyd-rate-limit
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - policyd-rate-limit
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
  when: postfix.primary
 | 
			
		||||
 | 
			
		||||
- name: Deploy policyd-rate-limit
 | 
			
		||||
  vars:
 | 
			
		||||
    exempt_v4: "{{ policyd.exemptions | json_query('servers[].interface[?vlan_id==`2`].ipv4[]') }}"
 | 
			
		||||
    exempt_v6: "{{ policyd.exemptions | json_query('servers[].interface[?vlan_id==`2`].ipv6[][].ipv6') }}"
 | 
			
		||||
  template:
 | 
			
		||||
    src: "{{ item.src }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
    chmod: 0640
 | 
			
		||||
  loop:
 | 
			
		||||
    - { src: policyd/policyd-rate-limit.yaml.j2, dest: /etc/policyd-rate-limit.yaml }
 | 
			
		||||
    - { src: policyd/policyd.py.j2, dest: /usr/lib/python3/dist-packages/policyd_rate_limit }
 | 
			
		||||
  when: postfix.primary
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
  template:
 | 
			
		||||
    src: update-motd.d/05-policyd.j2
 | 
			
		||||
    dest: /etc/update-motd.d/05-policyd
 | 
			
		||||
    mode: 0755
 | 
			
		||||
  when: postfix.primary
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,107 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# Make policyd-rate-limit output logs to stderr
 | 
			
		||||
debug: False
 | 
			
		||||
 | 
			
		||||
# The user policyd-rate-limit will use to drop privileges.
 | 
			
		||||
user: "policyd-rate-limit"
 | 
			
		||||
# The group policyd-rate-limit will use to drop privileges.
 | 
			
		||||
group: "policyd-rate-limit"
 | 
			
		||||
 | 
			
		||||
# path where the program will try to write its pid to.
 | 
			
		||||
pidfile: "/var/run/policyd-rate-limit/policyd-rate-limit.pid"
 | 
			
		||||
 | 
			
		||||
# The config to connect to a mysql server.
 | 
			
		||||
mysql_config:
 | 
			
		||||
    user: "username"
 | 
			
		||||
    passwd: "*****"
 | 
			
		||||
    db: "database"
 | 
			
		||||
    host: "localhost"
 | 
			
		||||
    charset: 'utf8'
 | 
			
		||||
 | 
			
		||||
# The config to connect to a sqlite3 database.
 | 
			
		||||
sqlite_config:
 | 
			
		||||
    database: "/var/lib/policyd-rate-limit/db.sqlite3"
 | 
			
		||||
 | 
			
		||||
# The config to connect to a postgresql server.
 | 
			
		||||
pgsql_config:
 | 
			
		||||
    database: "database"
 | 
			
		||||
    user: "username"
 | 
			
		||||
    password: "*****"
 | 
			
		||||
    host: "localhost"
 | 
			
		||||
 | 
			
		||||
# Which data backend to use. Possible values are 0 for sqlite3, 1 for mysql and 2 for postgresql.
 | 
			
		||||
backend: 0
 | 
			
		||||
 | 
			
		||||
# The socket to bind to. Can be a path to an unix socket or a couple [ip, port].
 | 
			
		||||
# SOCKET: ["127.0.0.1", 8552]
 | 
			
		||||
SOCKET: "/var/spool/postfix/ratelimit/policy"
 | 
			
		||||
# Permissions on the unix socket (if unix socket used).
 | 
			
		||||
socket_permission: 0666
 | 
			
		||||
 | 
			
		||||
# A list of couple [number of emails, number of seconds]. If one of the element of the list is
 | 
			
		||||
# exeeded (more than 'number of emails' on 'number of seconds' for an ip address or an sasl
 | 
			
		||||
# username), postfix will return a temporary failure.
 | 
			
		||||
limits:
 | 
			
		||||
    - [75, 60] # limit to 75 mails by minutes
 | 
			
		||||
    - [200, 86400] # limits to 200 mails by days
 | 
			
		||||
 | 
			
		||||
# dict of id -> limit list. Used to override limits and use custom limits for
 | 
			
		||||
# a particular id. Use an empty list for no limits for a particular id.
 | 
			
		||||
# ids are sasl usernames or ip addresses
 | 
			
		||||
# limits_by_id:
 | 
			
		||||
#     foo: []
 | 
			
		||||
#     192.168.0.254:
 | 
			
		||||
#         - [1000, 86400] # limits to 1000 mails by days
 | 
			
		||||
#     2a06:e042:100:4:219:bbff:fe3c:4f76: []
 | 
			
		||||
limits_by_id:
 | 
			
		||||
{% for server in exempt_v4 %}
 | 
			
		||||
    {{ server }} : []
 | 
			
		||||
{% endfor %}
 | 
			
		||||
{% for server in exempt_v6 %}
 | 
			
		||||
    {{ server }} : [] 
 | 
			
		||||
{% endfor %}
 | 
			
		||||
 | 
			
		||||
# Apply limits by sasl usernames.
 | 
			
		||||
limit_by_sasl: True
 | 
			
		||||
# If no sasl username is found, apply limits by ip addresses.
 | 
			
		||||
limit_by_ip: True
 | 
			
		||||
 | 
			
		||||
# A list of ip networks in cidr notation on which limits are applied. An empty list is equal
 | 
			
		||||
# to limit_by_ip: False, put "0.0.0.0/0" and "::/0" for every ip addresses.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
limited_networks: {{ policyd.mynetworks.ipv4 | union(policyd.mynetworks.ipv6) }}
 | 
			
		||||
 | 
			
		||||
# If not limits are reach, which action postfix should do.
 | 
			
		||||
# see http://www.postfix.org/access.5.html for a list of actions.
 | 
			
		||||
success_action: "dunno"
 | 
			
		||||
# If a limit is reach, which action postfix should do.
 | 
			
		||||
# see http://www.postfix.org/access.5.html for a list of actions.
 | 
			
		||||
fail_action: "defer_if_permit Rate limit reach, retry later"
 | 
			
		||||
# If we are unable to to contect the database backend, which action postfix should do.
 | 
			
		||||
# see http://www.postfix.org/access.5.html for a list of actions.
 | 
			
		||||
db_error_action: "dunno"
 | 
			
		||||
 | 
			
		||||
# If True, send a report to report_to about users reaching limits each time --clean is called
 | 
			
		||||
report: True
 | 
			
		||||
# from who to send emails reports. Must be defined if report: True
 | 
			
		||||
report_from: "{{ policyd.mail }}"
 | 
			
		||||
# Address to send emails reports to. Must be defined if report: True
 | 
			
		||||
report_to: "{{ policyd.mail }}"
 | 
			
		||||
# Subject of the report email
 | 
			
		||||
report_subject: "policyd-rate-limit report"
 | 
			
		||||
# List of number of seconds from the limits list for which you want to be reported.
 | 
			
		||||
report_limits: [86400]
 | 
			
		||||
# Only send a report if some users have reach a reported limit.
 | 
			
		||||
# Otherwise, empty reports may be sent.
 | 
			
		||||
report_only_if_needed: True
 | 
			
		||||
 | 
			
		||||
# The smtp server to use to send emails [host, port]
 | 
			
		||||
smtp_server: ["localhost", 25]
 | 
			
		||||
# Should we use starttls (you should set this to True if you use smtp_credentials)
 | 
			
		||||
smtp_starttls: False
 | 
			
		||||
# Should we use credentials to connect to smtp_server ? if yes set ["user", "password"], else null
 | 
			
		||||
smtp_credentials: null
 | 
			
		||||
 | 
			
		||||
delay_to_close: 300
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,285 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
# This program 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 version 3 for
 | 
			
		||||
# more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License version 3
 | 
			
		||||
# along with this program; if not, write to the Free Software Foundation, Inc., 51
 | 
			
		||||
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | 
			
		||||
#
 | 
			
		||||
# (c) 2015-2016 Valentin Samir
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import socket
 | 
			
		||||
import time
 | 
			
		||||
import select
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from policyd_rate_limit import utils
 | 
			
		||||
from policyd_rate_limit.utils import config
 | 
			
		||||
 | 
			
		||||
class PolicydError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
class PolicydConnectionClosed(PolicydError):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
class Pass(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Policyd(object):
 | 
			
		||||
    """The policy server class"""
 | 
			
		||||
    socket_data_read = {}
 | 
			
		||||
    socket_data_write = {}
 | 
			
		||||
    last_used = {}
 | 
			
		||||
 | 
			
		||||
    def socket(self):
 | 
			
		||||
        """initialize the socket from the config parameters"""
 | 
			
		||||
        # if socket is a string assume it is the path to an unix socket
 | 
			
		||||
        if isinstance(config.SOCKET, str):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(config.SOCKET)
 | 
			
		||||
            except OSError:
 | 
			
		||||
                if os.path.exists(config.SOCKET):  # pragma: no cover (should not happen)
 | 
			
		||||
                    raise
 | 
			
		||||
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 | 
			
		||||
        # else asume its a tuple (bind_ip, bind_port)
 | 
			
		||||
        elif ':' in config.SOCKET[0]:  # assume ipv6 bind addresse
 | 
			
		||||
            sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
 | 
			
		||||
        elif '.' in config.SOCKET[0]:  # assume ipv4 bind addresse
 | 
			
		||||
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError("bad socket %s" % (config.SOCKET,))
 | 
			
		||||
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | 
			
		||||
        self.sock = sock
 | 
			
		||||
 | 
			
		||||
    def close_socket(self):
 | 
			
		||||
        """close the socket depending of the config parameters"""
 | 
			
		||||
        self.sock.close()
 | 
			
		||||
        # if socket was an unix socket, delete it after closing.
 | 
			
		||||
        if isinstance(config.SOCKET, str):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(config.SOCKET)
 | 
			
		||||
            except OSError as error:  # pragma: no cover (should not happen)
 | 
			
		||||
                sys.stderr.write("%s\n" % error)
 | 
			
		||||
                sys.stderr.flush()
 | 
			
		||||
 | 
			
		||||
    def close_connection(self, connection):
 | 
			
		||||
        """close a connection and clean read/write dict"""
 | 
			
		||||
        # Clean up the connection
 | 
			
		||||
        try:
 | 
			
		||||
            del self.socket_data_read[connection]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass
 | 
			
		||||
        try:
 | 
			
		||||
            del self.socket_data_write[connection]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass
 | 
			
		||||
        connection.close()
 | 
			
		||||
 | 
			
		||||
    def close_write_conn(self, connection):
 | 
			
		||||
        """Removes a socket from the write dict"""
 | 
			
		||||
        try:
 | 
			
		||||
            del self.socket_data_write[connection]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            if config.debug:
 | 
			
		||||
                sys.stderr.write(
 | 
			
		||||
                    (
 | 
			
		||||
                        "Hmmm, a socket actually used to write a little "
 | 
			
		||||
                        "time ago wasn\'t in socket_data_write. Weird.\n"
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        """The main server loop"""
 | 
			
		||||
        try:
 | 
			
		||||
            sock = self.sock
 | 
			
		||||
            sock.bind(config.SOCKET)
 | 
			
		||||
            if isinstance(config.SOCKET, str):
 | 
			
		||||
                os.chmod(config.SOCKET, config.socket_permission)
 | 
			
		||||
            sock.listen(5)
 | 
			
		||||
            self.socket_data_read[sock] = []
 | 
			
		||||
            if config.debug:
 | 
			
		||||
                sys.stderr.write('waiting for connections\n')
 | 
			
		||||
                sys.stderr.flush()
 | 
			
		||||
            while True:
 | 
			
		||||
                # wait for a socket to read to or to write to
 | 
			
		||||
                (rlist, wlist, _) = select.select(
 | 
			
		||||
                    self.socket_data_read.keys(), self.socket_data_write.keys(), []
 | 
			
		||||
                )
 | 
			
		||||
                for socket in rlist:
 | 
			
		||||
                    # if the socket is the main socket, there is a new connection to accept
 | 
			
		||||
                    if socket == sock:
 | 
			
		||||
                        connection, client_address = sock.accept()
 | 
			
		||||
                        if config.debug:
 | 
			
		||||
                            sys.stderr.write('connection from %s\n' % (client_address,))
 | 
			
		||||
                            sys.stderr.flush()
 | 
			
		||||
                        self.socket_data_read[connection] = []
 | 
			
		||||
 | 
			
		||||
                        # Updates the last_sed time for the socket.
 | 
			
		||||
                        self.last_used[connection] = time.time()
 | 
			
		||||
                    # else there is data to read on a client socket
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.read(socket)
 | 
			
		||||
                for socket in wlist:
 | 
			
		||||
                    try:
 | 
			
		||||
                        data = self.socket_data_write[socket]
 | 
			
		||||
                        sent = socket.send(data)
 | 
			
		||||
                        data_not_sent = data[sent:]
 | 
			
		||||
                        if data_not_sent:
 | 
			
		||||
                            self.socket_data_write[socket] = data_not_sent
 | 
			
		||||
                        else:
 | 
			
		||||
                            self.close_write_conn(socket)
 | 
			
		||||
 | 
			
		||||
                        # Socket has been used, let's update its last_used time.
 | 
			
		||||
                        self.last_used[socket] = time.time()
 | 
			
		||||
                    # the socket has been closed during read
 | 
			
		||||
                    except KeyError:
 | 
			
		||||
                        pass
 | 
			
		||||
                # Closes unused socket for a long time.
 | 
			
		||||
                __to_rm = []
 | 
			
		||||
                for (socket, last_used) in self.last_used.items():
 | 
			
		||||
                    if socket == sock:
 | 
			
		||||
                        continue
 | 
			
		||||
                    if time.time() - last_used > config.delay_to_close:
 | 
			
		||||
                        self.close_connection(socket)
 | 
			
		||||
                        __to_rm.append(socket)
 | 
			
		||||
                for socket in __to_rm:
 | 
			
		||||
                    self.last_used.pop(socket)
 | 
			
		||||
 | 
			
		||||
        except (KeyboardInterrupt, utils.Exit):
 | 
			
		||||
            for socket in list(self.socket_data_read.keys()):
 | 
			
		||||
                if socket != self.sock:
 | 
			
		||||
                    self.close_connection(sock)
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
    def read(self, connection):
 | 
			
		||||
        """Called then a connection is ready for reads"""
 | 
			
		||||
        try:
 | 
			
		||||
            # get the current buffer of the connection
 | 
			
		||||
            buffer = self.socket_data_read[connection]
 | 
			
		||||
            # read data
 | 
			
		||||
            data = connection.recv(1024).decode('UTF-8')
 | 
			
		||||
            if not data:
 | 
			
		||||
                #raise ValueError("connection closed")
 | 
			
		||||
                raise PolicydConnectionClosed()
 | 
			
		||||
            if config.debug:
 | 
			
		||||
                sys.stderr.write(data)
 | 
			
		||||
                sys.stderr.flush()
 | 
			
		||||
            # accumulate it in buffer
 | 
			
		||||
            buffer.append(data)
 | 
			
		||||
            # if data len too short to determine if we are on an empty line, we
 | 
			
		||||
            # concatene datas in buffer
 | 
			
		||||
            if len(data) < 2:
 | 
			
		||||
                data = u"".join(buffer)
 | 
			
		||||
                buffer = [data]
 | 
			
		||||
            # We reach on empty line so the client has finish to send and wait for a response
 | 
			
		||||
            if data[-2:] == "\n\n":
 | 
			
		||||
                data = u"".join(buffer)
 | 
			
		||||
                request = {}
 | 
			
		||||
                # read data are like one key=value per line
 | 
			
		||||
                for line in data.split("\n"):
 | 
			
		||||
                    line = line.strip()
 | 
			
		||||
                    try:
 | 
			
		||||
                        key, value = line.split(u"=", 1)
 | 
			
		||||
                        if value:
 | 
			
		||||
                            request[key] = value
 | 
			
		||||
                    # if value is empty, ignore it
 | 
			
		||||
                    except ValueError:
 | 
			
		||||
                        pass
 | 
			
		||||
                # process the collected data in the action method
 | 
			
		||||
                self.action(connection, request)
 | 
			
		||||
            else:
 | 
			
		||||
                self.socket_data_read[connection] = buffer
 | 
			
		||||
            # Socket has been used, let's update its last_used time.
 | 
			
		||||
            self.last_used[connection] = time.time()
 | 
			
		||||
        except (KeyboardInterrupt, utils.Exit):
 | 
			
		||||
            self.close_connection(connection)
 | 
			
		||||
            raise
 | 
			
		||||
        except PolicydConnectionClosed:
 | 
			
		||||
            if config.debug:
 | 
			
		||||
                sys.stderr.write("Connection closed\n")
 | 
			
		||||
                sys.stderr.flush()
 | 
			
		||||
            self.close_connection(connection)
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            traceback.print_exc()
 | 
			
		||||
            sys.stderr.flush()
 | 
			
		||||
            self.close_connection(connection)
 | 
			
		||||
 | 
			
		||||
    def action(self, connection, request):
 | 
			
		||||
        """Called then the client has sent an empty line"""
 | 
			
		||||
        id = None
 | 
			
		||||
        # By default, we do not block emails
 | 
			
		||||
        action = config.success_action
 | 
			
		||||
        try:
 | 
			
		||||
            if not config.database_is_initialized:
 | 
			
		||||
                utils.database_init()
 | 
			
		||||
            with utils.cursor() as cur:
 | 
			
		||||
                try:
 | 
			
		||||
                    # only care if the protocol states is RCTP. If the policy delegation in postfix
 | 
			
		||||
                    # configuration is in smtpd_recipient_restrictions as said in the doc,
 | 
			
		||||
                    # possible states are RCPT and VRFY.
 | 
			
		||||
                    if 'protocol_state' in request and request['protocol_state'].upper() != "RCPT":
 | 
			
		||||
                        raise Pass()
 | 
			
		||||
                    # if user is authenticated, we filter by sasl username
 | 
			
		||||
                    if config.limit_by_sasl and u'sasl_username' in request:
 | 
			
		||||
                        id = request[u'sasl_username']
 | 
			
		||||
                    # else, if activated, we filter by sender
 | 
			
		||||
                    elif config.limit_by_sender and u'sender' in request:
 | 
			
		||||
                        id = request[u'sender']
 | 
			
		||||
                    # else, if activated, we filter by ip source addresse
 | 
			
		||||
                    elif (
 | 
			
		||||
                        config.limit_by_ip and
 | 
			
		||||
                        u'client_address' in request and
 | 
			
		||||
                        utils.is_ip_limited(request[u'client_address'])
 | 
			
		||||
                    ):
 | 
			
		||||
                        id = request[u'client_address']
 | 
			
		||||
                    # if the client neither send us client ip adresse nor sasl username, jump
 | 
			
		||||
                    # to the next section
 | 
			
		||||
                    else:
 | 
			
		||||
                        raise Pass()
 | 
			
		||||
                    # Here we are limiting against sasl username, sender or source ip addresses.
 | 
			
		||||
                    # for each limit periods, we count the number of mails already send.
 | 
			
		||||
                    # if the a limit is reach, we change action to fail (deny the mail).
 | 
			
		||||
                    for mail_nb, delta in config.limits_by_id.get(id, config.limits):
 | 
			
		||||
                        cur.execute(
 | 
			
		||||
                            (
 | 
			
		||||
                                "SELECT COUNT(*) FROM mail_count "
 | 
			
		||||
                                "WHERE id = %s AND date >= %s"
 | 
			
		||||
                            ) % ((config.format_str,)*2),
 | 
			
		||||
                            (id, int(time.time() - delta))
 | 
			
		||||
                        )
 | 
			
		||||
                        nb = cur.fetchone()[0]
 | 
			
		||||
                        if config.debug:
 | 
			
		||||
                            sys.stderr.write("%03d/%03d hit since %ss\n" % (nb, mail_nb, delta))
 | 
			
		||||
                            sys.stderr.flush()
 | 
			
		||||
                        if nb >= mail_nb:
 | 
			
		||||
                            action = config.fail_action
 | 
			
		||||
                            if config.report and delta in config.report_limits:
 | 
			
		||||
                                utils.hit(cur, delta, id)
 | 
			
		||||
                            raise Pass()
 | 
			
		||||
                except Pass:
 | 
			
		||||
                    pass
 | 
			
		||||
                # If action is a success, record in the database that a new mail has just been sent
 | 
			
		||||
                if action == config.success_action and id is not None:
 | 
			
		||||
                    if config.debug:
 | 
			
		||||
                        sys.stderr.write(u"insert id %s\n" % id)
 | 
			
		||||
                        sys.stderr.flush()
 | 
			
		||||
                    cur.execute(
 | 
			
		||||
                        "INSERT INTO mail_count VALUES (%s, %s)" % ((config.format_str,)*2),
 | 
			
		||||
                        (id, int(time.time()))
 | 
			
		||||
                    )
 | 
			
		||||
        except utils.cursor.backend_module.Error as error:
 | 
			
		||||
            utils.cursor.del_db()
 | 
			
		||||
            action = config.db_error_action
 | 
			
		||||
            sys.stderr.write("Database error: %r\n" % error)
 | 
			
		||||
        data = u"action=%s\n\n" % action
 | 
			
		||||
        if config.debug:
 | 
			
		||||
            sys.stderr.write(data)
 | 
			
		||||
            sys.stderr.flush()
 | 
			
		||||
        # return the result to the client
 | 
			
		||||
        self.socket_data_write[connection] = data.encode('UTF-8')
 | 
			
		||||
        # Socket has been used, let's update its last_used time.
 | 
			
		||||
        self.last_used[connection] = time.time()
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/tail +14
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
[0m> [38;5;82mpolicyd-rate-limit[0m a été déployé sur cette machine.
 | 
			
		||||
| 
						 | 
				
			
			@ -2,8 +2,8 @@
 | 
			
		|||
- name: generate postmaps
 | 
			
		||||
  command: /usr/sbin/postmap {{ item }}
 | 
			
		||||
  loop:
 | 
			
		||||
    - /etc/postfix/canonical
 | 
			
		||||
    - /etc/postfix/mime_header_checks
 | 
			
		||||
    - /etc/postfix/recipient_access
 | 
			
		||||
    - /etc/postfix/sender_login_maps
 | 
			
		||||
    - /etc/postfix/transport
 | 
			
		||||
    - /etc/postfix/client_checks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,24 @@
 | 
			
		|||
    - sender_login_maps
 | 
			
		||||
    - postscreen_access.cidr
 | 
			
		||||
    - sasl/smtpd.conf
 | 
			
		||||
    - canonical
 | 
			
		||||
    - client_checks
 | 
			
		||||
  notify:
 | 
			
		||||
    - generate postmaps
 | 
			
		||||
 | 
			
		||||
- name: Make sure let's encrypt renewal-hooks exists
 | 
			
		||||
  file:
 | 
			
		||||
    path: /etc/letsencrypt/renewal-hooks/deploy
 | 
			
		||||
    state: directory
 | 
			
		||||
  when: not ansible_check_mode
 | 
			
		||||
 | 
			
		||||
- name: Reload postfix after certificate renewal
 | 
			
		||||
  template:
 | 
			
		||||
    src: letsencrypt/renewal-hooks/deploy/reload-postfix.sh.j2
 | 
			
		||||
    dest: /etc/letsencrypt/renewal-hooks/deploy/reload-postfix.sh
 | 
			
		||||
    mode: 0755
 | 
			
		||||
 | 
			
		||||
- name: Indicate role in motd
 | 
			
		||||
  template:
 | 
			
		||||
    src: update-motd.d/05-postfix.j2
 | 
			
		||||
    dest: /etc/update-motd.d/05-postfix
 | 
			
		||||
    mode: 0755
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/bin/sh
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
systemctl reload postfix
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
# Fichier fournissant des méthodes pour traduire certaines adresses
 | 
			
		||||
 | 
			
		||||
/^(.*)@localhost(\.crans\.org)?$/   ${1}@crans.org
 | 
			
		||||
/^(.*)@{{ ansible_hostname }}.adm.crans.org$/   ${1}@crans.org
 | 
			
		||||
/^(.*)@{{ ansible_hostname }}.crans.org$/       ${1}@crans.org
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
185.50.149.0/24 REJECT Spammers are not welcome here!
 | 
			
		||||
| 
						 | 
				
			
			@ -24,15 +24,12 @@ mydestination = {{ ansible_hostname }}, $myhostname, localhost, localhost.$mydom
 | 
			
		|||
{% endif %}
 | 
			
		||||
# Domaine relaye par ce MX
 | 
			
		||||
relay_domains = $mydestination
 | 
			
		||||
{% if postfix.mailman %}
 | 
			
		||||
{% if postfix.mailman or postfix.public %}
 | 
			
		||||
                lists.$mydomain
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% if postfix.secondary %}
 | 
			
		||||
                $mydomain, crans.ens-cachan.fr, clubs.ens-cachan.fr, install-party.ens-cachan.fr, crans.fr, crans.eu
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% if postfix.public %}
 | 
			
		||||
                lists.$mydomain
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% if postfix.mailman %}
 | 
			
		||||
relay_recipient_maps =
 | 
			
		||||
    hash:/var/local/re2o-services/mail-server/generated/virtual
 | 
			
		||||
| 
						 | 
				
			
			@ -86,8 +83,8 @@ virtual_alias_maps = hash:/var/local/re2o-services/mail-server/generated/virtual
 | 
			
		|||
# TLS pour la reception
 | 
			
		||||
smtpd_use_tls=yes
 | 
			
		||||
smtpd_tls_security_level=may
 | 
			
		||||
smtpd_tls_cert_file=/etc/ssl/certs/smtp.pem
 | 
			
		||||
smtpd_tls_key_file=/etc/ssl/private/smtp.pem
 | 
			
		||||
smtpd_tls_cert_file=/etc/letsencrypt/live/crans.org/fullchain.pem
 | 
			
		||||
smtpd_tls_key_file=/etc/letsencrypt/live/crans.org/privkey.pem
 | 
			
		||||
smtpd_tls_loglevel=0
 | 
			
		||||
smtpd_tls_received_header=yes
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +117,16 @@ smtpd_helo_required = yes
 | 
			
		|||
smtpd_helo_restrictions = permit_mynetworks
 | 
			
		||||
                          reject_invalid_helo_hostname
 | 
			
		||||
                          reject_non_fqdn_helo_hostname
 | 
			
		||||
# Vérifie que le client n'est pas dans un / d'ips blacklistées
 | 
			
		||||
                          check_client_access cidr:/etc/postfix/client_checks
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% if postfix.primary %}
 | 
			
		||||
submission_client_restrictions =
 | 
			
		||||
			  check_client_access cidr:/etc/postfix/client_checks
 | 
			
		||||
submission_relay_restrictions =
 | 
			
		||||
			  permit_sasl_authenticated
 | 
			
		||||
			  reject
 | 
			
		||||
 | 
			
		||||
{% endif %}
 | 
			
		||||
## Limitation des messages envoyés par minute
 | 
			
		||||
# On n'ignore que les messages venant d'adresses "protégées"
 | 
			
		||||
| 
						 | 
				
			
			@ -154,7 +161,7 @@ smtpd_policy_service_request_limit = 1
 | 
			
		|||
smtpd_recipient_restrictions =
 | 
			
		||||
{% if postfix.primary %}
 | 
			
		||||
# Test avec policyd-rate-limit pour limiter le nombre de mails par utilisateur SASL
 | 
			
		||||
                               check_policy_service unix:ratelimit/policy
 | 
			
		||||
                               check_policy_service { unix:ratelimit/policy, default_action=DUNNO }
 | 
			
		||||
{% endif %}
 | 
			
		||||
# permet si le client est dans le reseau local
 | 
			
		||||
                               permit_mynetworks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,8 +71,8 @@
 | 
			
		|||
# DO NOT SHARE THE POSTFIX QUEUE BETWEEN MULTIPLE POSTFIX INSTANCES.
 | 
			
		||||
#
 | 
			
		||||
# ==========================================================================
 | 
			
		||||
# service type  private unpriv  chroot  wakeup  maxproc command + args
 | 
			
		||||
#               (yes)   (yes)   (yes)   (never) (50)
 | 
			
		||||
# service type private unpriv  chroot  wakeup  maxproc command + args
 | 
			
		||||
#              (yes)   (yes)   (yes)   (never) (50)
 | 
			
		||||
# ==========================================================================
 | 
			
		||||
{% if postfix.primary or postfix.secondary %}
 | 
			
		||||
smtp      inet  n       -       -       -       1       postscreen
 | 
			
		||||
| 
						 | 
				
			
			@ -87,14 +87,17 @@ dnsblog   unix  -       -       -       -       0       dnsblog
 | 
			
		|||
submission inet n       -       -       -       -       smtpd
 | 
			
		||||
  -o smtpd_tls_security_level=encrypt
 | 
			
		||||
  -o smtpd_sasl_auth_enable=yes
 | 
			
		||||
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
 | 
			
		||||
  -o smtpd_delay_reject=no
 | 
			
		||||
  -o smtpd_client_restrictions=$submission_client_restrictions
 | 
			
		||||
  -o smtpd_relay_restrictions=$submission_relay_restrictions
 | 
			
		||||
  -o milter_macro_daemon_name=ORIGINATING
 | 
			
		||||
smtps     inet  n       -       -       -       -       smtpd
 | 
			
		||||
  -o smtpd_tls_wrappermode=yes
 | 
			
		||||
  -o smtpd_sasl_auth_enable=yes
 | 
			
		||||
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
 | 
			
		||||
  -o smtpd_delay_reject=no
 | 
			
		||||
  -o smtpd_client_restrictions=$submission_client_restrictions
 | 
			
		||||
  -o smtpd_relay_restrictions=$submission_relay_restrictions
 | 
			
		||||
{% endif %}
 | 
			
		||||
#628      inet  n       -       -       -       -       qmqpd
 | 
			
		||||
pickup    fifo  n       -       -       60      1       pickup
 | 
			
		||||
cleanup   unix  n       -       -       -       0       cleanup
 | 
			
		||||
qmgr      fifo  n       -       -       300     1       qmgr
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,3 +59,6 @@
 | 
			
		|||
# Non, nous ne voulons pas traiter l'alcoolisme à l'insu du patient.
 | 
			
		||||
94.242.206.15                             reject
 | 
			
		||||
91.188.222.33                             reject
 | 
			
		||||
 | 
			
		||||
# Et les russes ils dégagent aussi
 | 
			
		||||
185.50.149.0/24                           reject
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
@crans.org root
 | 
			
		||||
@crans.fr root
 | 
			
		||||
@crans.eu root
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/tail +14
 | 
			
		||||
{{ ansible_header | comment }}
 | 
			
		||||
[0m> [38;5;82mPostfix[0m a été déployé sur cette machine. Voir [38;5;6m/etc/postfix/[0m.
 | 
			
		||||
| 
						 | 
				
			
			@ -12,5 +12,5 @@
 | 
			
		|||
    path: /etc/default/prometheus-apache-exporter
 | 
			
		||||
    regexp: '^ARGS='
 | 
			
		||||
    line: |
 | 
			
		||||
      ARGS="-telemetry.address={{ ansible_hostname }}.adm.crans.org:9117"
 | 
			
		||||
      ARGS="-telemetry.address={{ adm_ipv4 }}:9117"
 | 
			
		||||
  notify: Restart prometheus-apache-exporter
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,11 +31,9 @@
 | 
			
		|||
 | 
			
		||||
# Doesn't work on Debian Stretch with the old prometheus package
 | 
			
		||||
- name: Make Prometheus node-exporter listen on adm only
 | 
			
		||||
  lineinfile:
 | 
			
		||||
    path: /etc/default/prometheus-node-exporter
 | 
			
		||||
    regexp: '^ARGS='
 | 
			
		||||
    line: |
 | 
			
		||||
      ARGS="--web.listen-address={{ ansible_hostname }}.adm.crans.org:9100"
 | 
			
		||||
  template:
 | 
			
		||||
    src: default/prometheus-node-exporter.j2
 | 
			
		||||
    dest: /etc/default/prometheus-node-exporter
 | 
			
		||||
  notify: Restart prometheus-node-exporter
 | 
			
		||||
  tags: restart-node-exporter
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,130 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# Set the command-line arguments to pass to the server.
 | 
			
		||||
# Due to shell scaping, to pass backslashes for regexes, you need to double
 | 
			
		||||
# them (\\d for \d). If running under systemd, you need to double them again
 | 
			
		||||
# (\\\\d to mean \d), and escape newlines too.
 | 
			
		||||
ARGS="--web.listen-address={{ adm_ipv4 }}:9100"
 | 
			
		||||
 | 
			
		||||
# Prometheus-node-exporter supports the following options:
 | 
			
		||||
#
 | 
			
		||||
#  --collector.diskstats.ignored-devices="^(ram|loop|fd|(h|s|v|xv)d[a-z]|nvme\\d+n\\d+p)\\d+$"
 | 
			
		||||
#                            Regexp of devices to ignore for diskstats.
 | 
			
		||||
#  --collector.filesystem.ignored-mount-points="^/(dev|proc|run|sys|mnt|media|var/lib/docker)($|/)"
 | 
			
		||||
#                            Regexp of mount points to ignore for filesystem
 | 
			
		||||
#                            collector.
 | 
			
		||||
#  --collector.filesystem.ignored-fs-types="^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$"
 | 
			
		||||
#                            Regexp of filesystem types to ignore for
 | 
			
		||||
#                            filesystem collector.
 | 
			
		||||
#  --collector.netdev.ignored-devices="^lo$"
 | 
			
		||||
#                            Regexp of net devices to ignore for netdev
 | 
			
		||||
#                            collector.
 | 
			
		||||
#  --collector.netstat.fields="^(.*_(InErrors|InErrs)|Ip_Forwarding|Ip(6|Ext)_(InOctets|OutOctets)|Icmp6?_(InMsgs|OutMsgs)|TcpExt_(Listen.*|Syncookies.*)|Tcp_(ActiveOpens|PassiveOpens|RetransSegs|CurrEstab)|Udp6?_(InDatagrams|OutDatagrams|NoPorts))$"
 | 
			
		||||
#                            Regexp of fields to return for netstat
 | 
			
		||||
#                            collector.
 | 
			
		||||
#  --collector.ntp.server="127.0.0.1"
 | 
			
		||||
#                            NTP server to use for ntp collector
 | 
			
		||||
#  --collector.ntp.protocol-version=4
 | 
			
		||||
#                            NTP protocol version
 | 
			
		||||
#  --collector.ntp.server-is-local
 | 
			
		||||
#                            Certify that collector.ntp.server address is the
 | 
			
		||||
#                            same local host as this collector.
 | 
			
		||||
#  --collector.ntp.ip-ttl=1  IP TTL to use while sending NTP query
 | 
			
		||||
#  --collector.ntp.max-distance=3.46608s
 | 
			
		||||
#                            Max accumulated distance to the root
 | 
			
		||||
#  --collector.ntp.local-offset-tolerance=1ms
 | 
			
		||||
#                            Offset between local clock and local ntpd time
 | 
			
		||||
#                            to tolerate
 | 
			
		||||
#  --path.procfs="/proc"     procfs mountpoint.
 | 
			
		||||
#  --path.sysfs="/sys"       sysfs mountpoint.
 | 
			
		||||
#  --collector.qdisc.fixtures=""
 | 
			
		||||
#                            test fixtures to use for qdisc collector
 | 
			
		||||
#                            end-to-end testing
 | 
			
		||||
#  --collector.runit.servicedir="/etc/service"
 | 
			
		||||
#                            Path to runit service directory.
 | 
			
		||||
#  --collector.supervisord.url="http://localhost:9001/RPC2"
 | 
			
		||||
#                            XML RPC endpoint.
 | 
			
		||||
#  --collector.systemd.unit-whitelist=".+"
 | 
			
		||||
#                            Regexp of systemd units to whitelist. Units must
 | 
			
		||||
#                            both match whitelist and not match blacklist to
 | 
			
		||||
#                            be included.
 | 
			
		||||
#  --collector.systemd.unit-blacklist=".+(\\.device|\\.scope|\\.slice|\\.target)"
 | 
			
		||||
#                            Regexp of systemd units to blacklist. Units must
 | 
			
		||||
#                            both match whitelist and not match blacklist to
 | 
			
		||||
#                            be included.
 | 
			
		||||
#  --collector.systemd.private
 | 
			
		||||
#                            Establish a private, direct connection to
 | 
			
		||||
#                            systemd without dbus.
 | 
			
		||||
#  --collector.textfile.directory="/var/lib/prometheus/node-exporter"
 | 
			
		||||
#                            Directory to read text files with metrics from.
 | 
			
		||||
#  --collector.vmstat.fields="^(oom_kill|pgpg|pswp|pg.*fault).*"
 | 
			
		||||
#                            Regexp of fields to return for vmstat collector.
 | 
			
		||||
#  --collector.wifi.fixtures=""
 | 
			
		||||
#                            test fixtures to use for wifi collector metrics
 | 
			
		||||
#  --collector.arp           Enable the arp collector (default: enabled).
 | 
			
		||||
#  --collector.bcache        Enable the bcache collector (default: enabled).
 | 
			
		||||
#  --collector.bonding       Enable the bonding collector (default: enabled).
 | 
			
		||||
#  --collector.buddyinfo     Enable the buddyinfo collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.conntrack     Enable the conntrack collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.cpu           Enable the cpu collector (default: enabled).
 | 
			
		||||
#  --collector.diskstats     Enable the diskstats collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.drbd          Enable the drbd collector (default: disabled).
 | 
			
		||||
#  --collector.edac          Enable the edac collector (default: enabled).
 | 
			
		||||
#  --collector.entropy       Enable the entropy collector (default: enabled).
 | 
			
		||||
#  --collector.filefd        Enable the filefd collector (default: enabled).
 | 
			
		||||
#  --collector.filesystem    Enable the filesystem collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.hwmon         Enable the hwmon collector (default: enabled).
 | 
			
		||||
#  --collector.infiniband    Enable the infiniband collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.interrupts    Enable the interrupts collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.ipvs          Enable the ipvs collector (default: enabled).
 | 
			
		||||
#  --collector.ksmd          Enable the ksmd collector (default: disabled).
 | 
			
		||||
#  --collector.loadavg       Enable the loadavg collector (default: enabled).
 | 
			
		||||
#  --collector.logind        Enable the logind collector (default: disabled).
 | 
			
		||||
#  --collector.mdadm         Enable the mdadm collector (default: enabled).
 | 
			
		||||
#  --collector.meminfo       Enable the meminfo collector (default: enabled).
 | 
			
		||||
#  --collector.meminfo_numa  Enable the meminfo_numa collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.mountstats    Enable the mountstats collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.netdev        Enable the netdev collector (default: enabled).
 | 
			
		||||
#  --collector.netstat       Enable the netstat collector (default: enabled).
 | 
			
		||||
#  --collector.nfs           Enable the nfs collector (default: enabled).
 | 
			
		||||
#  --collector.nfsd          Enable the nfsd collector (default: enabled).
 | 
			
		||||
#  --collector.ntp           Enable the ntp collector (default: disabled).
 | 
			
		||||
#  --collector.qdisc         Enable the qdisc collector (default: disabled).
 | 
			
		||||
#  --collector.runit         Enable the runit collector (default: disabled).
 | 
			
		||||
#  --collector.sockstat      Enable the sockstat collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.stat          Enable the stat collector (default: enabled).
 | 
			
		||||
#  --collector.supervisord   Enable the supervisord collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.systemd       Enable the systemd collector (default: enabled).
 | 
			
		||||
#  --collector.tcpstat       Enable the tcpstat collector (default:
 | 
			
		||||
#                            disabled).
 | 
			
		||||
#  --collector.textfile      Enable the textfile collector (default:
 | 
			
		||||
#                            enabled).
 | 
			
		||||
#  --collector.time          Enable the time collector (default: enabled).
 | 
			
		||||
#  --collector.uname         Enable the uname collector (default: enabled).
 | 
			
		||||
#  --collector.vmstat        Enable the vmstat collector (default: enabled).
 | 
			
		||||
#  --collector.wifi          Enable the wifi collector (default: enabled).
 | 
			
		||||
#  --collector.xfs           Enable the xfs collector (default: enabled).
 | 
			
		||||
#  --collector.zfs           Enable the zfs collector (default: enabled).
 | 
			
		||||
#  --collector.timex         Enable the timex collector (default: enabled).
 | 
			
		||||
#  --web.listen-address=":9100"
 | 
			
		||||
#                            Address on which to expose metrics and web
 | 
			
		||||
#                            interface.
 | 
			
		||||
#  --web.telemetry-path="/metrics"
 | 
			
		||||
#                            Path under which to expose metrics.
 | 
			
		||||
#  --log.level="info"        Only log messages with the given severity or
 | 
			
		||||
#                            above. Valid levels: [debug, info, warn, error,
 | 
			
		||||
#                            fatal]
 | 
			
		||||
#  --log.format="logger:stderr"
 | 
			
		||||
#                            Set the log target and format. Example:
 | 
			
		||||
#                            "logger:syslog?appname=bob&local=7" or
 | 
			
		||||
#                            "logger:stdout?json=true"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,31 +25,31 @@
 | 
			
		|||
# We don't need to restart Prometheus when updating nodes
 | 
			
		||||
- name: Configure Prometheus nodes
 | 
			
		||||
  copy:
 | 
			
		||||
    content: "{{ prometheus_targets | to_nice_json }}"
 | 
			
		||||
    content: "{{ [{'targets': prometheus.node_targets}] | to_nice_json }}"
 | 
			
		||||
    dest: /etc/prometheus/targets.json
 | 
			
		||||
 | 
			
		||||
# We don't need to restart Prometheus when updating nodes
 | 
			
		||||
- name: Configure Prometheus UPS SNMP devices
 | 
			
		||||
  copy:
 | 
			
		||||
    content: "{{ prometheus_ups_snmp_targets | to_nice_json }}"
 | 
			
		||||
    content: "{{ [{'targets': prometheus.ups_snmp_targets}] | to_nice_json }}"
 | 
			
		||||
    dest: /etc/prometheus/targets_ups_snmp.json
 | 
			
		||||
 | 
			
		||||
# We don't need to restart Prometheus when updating nodes
 | 
			
		||||
- name: Configure Prometheus Ubiquity Unifi SNMP devices
 | 
			
		||||
  copy:
 | 
			
		||||
    content: "{{ prometheus_unifi_snmp_targets | to_nice_json }}"
 | 
			
		||||
    content: "{{ [{'targets': prometheus.unifi_snmp_targets}] | to_nice_json }}"
 | 
			
		||||
    dest: /etc/prometheus/targets_unifi_snmp.json
 | 
			
		||||
 | 
			
		||||
# We don't need to restart Prometheus when updating nodes
 | 
			
		||||
- name: Configure Prometheus Apache targets
 | 
			
		||||
  copy:
 | 
			
		||||
    content: "{{ prometheus_apache_targets | to_nice_json }}"
 | 
			
		||||
    content: "{{ [{'targets': prometheus.apache_targets}] | to_nice_json }}"
 | 
			
		||||
    dest: /etc/prometheus/targets_apache.json
 | 
			
		||||
 | 
			
		||||
# We don't need to restart Prometheus when updating nodes
 | 
			
		||||
- name: Configure Prometheus Blackbox targets
 | 
			
		||||
  copy:
 | 
			
		||||
    content: "{{ prometheus_blackbox_targets | to_nice_json }}"
 | 
			
		||||
    content: "{{ [{'targets': prometheus.blackbox_targets}] | to_nice_json }}"
 | 
			
		||||
    dest: /etc/prometheus/targets_blackbox.json
 | 
			
		||||
 | 
			
		||||
- name: Activate prometheus service
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ groups:
 | 
			
		|||
 | 
			
		||||
  # Alert for high CPU usage
 | 
			
		||||
  - alert: CpuBusy
 | 
			
		||||
    expr: node_load5 > 5
 | 
			
		||||
    expr: node_load5{instance="zbee.adm.crans.org"} > 7 or node_load5{instance!="zbee.adm.crans.org"} > 5
 | 
			
		||||
    for: 10m
 | 
			
		||||
    labels:
 | 
			
		||||
      severity: warning
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ groups:
 | 
			
		|||
      description: "https://grafana.crans.org/d/qtbg59mZz/alimentation"
 | 
			
		||||
 | 
			
		||||
  - alert: UpsTemperatureWarning
 | 
			
		||||
    expr: (xupsEnvRemoteTemp < 10) or (xupsEnvRemoteTemp > 24)
 | 
			
		||||
    expr: (xupsEnvRemoteTemp < 10) or (xupsEnvRemoteTemp > 26)
 | 
			
		||||
    for: 5m
 | 
			
		||||
    labels:
 | 
			
		||||
      severity: warning
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
Subproject commit 3fa31a218d75835aa196ebd174906f3656ef22bd
 | 
			
		||||
Subproject commit 1869e9e08e926da376c2f7a6db69a6a5dc126b86
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +62,16 @@ hosts allow = *
 | 
			
		|||
read only = yes
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{# on veut backuper /var/lib/mailman sur redisdead #}
 | 
			
		||||
{% if ansible_hostname == "redisdead" %}
 | 
			
		||||
[mailman]
 | 
			
		||||
path = /var/lib/mailman
 | 
			
		||||
auth users = backupcrans
 | 
			
		||||
secrets file = /etc/rsyncd.secrets
 | 
			
		||||
hosts allow = zephir.adm.crans.org 10.231.136.6
 | 
			
		||||
{% endif %} 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{# TODO: implémenter le vrai système comme dans BCFG2 #}
 | 
			
		||||
{# TODO: implémenter le cas particulier cpasswords-main et wiki #}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
---
 | 
			
		||||
- name: Install sqlgrey
 | 
			
		||||
  apt:
 | 
			
		||||
    update_cache: true
 | 
			
		||||
    name:
 | 
			
		||||
      - sqlgrey
 | 
			
		||||
  register: apt_result
 | 
			
		||||
  retries: 3
 | 
			
		||||
  until: apt_result is succeeded
 | 
			
		||||
 | 
			
		||||
- name: Deploy sqlgrey configuration
 | 
			
		||||
  template:
 | 
			
		||||
    src: sqlgrey/{{ item }}.j2
 | 
			
		||||
    dest: /etc/sqlgrey/{{ item }}
 | 
			
		||||
    mode: 0644
 | 
			
		||||
  loop:
 | 
			
		||||
    - sqlgrey.conf
 | 
			
		||||
    - clients_fqdn_whitelist.local
 | 
			
		||||
    - clients_ip_whitelist.local
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# Gandi
 | 
			
		||||
*.mail.gandi.net
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
# Bouygues Télécom... les MX ne rententent pas la délivrance des mails.
 | 
			
		||||
62.201.140
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,189 @@
 | 
			
		|||
{{ ansible_header | comment }}
 | 
			
		||||
 | 
			
		||||
#########################
 | 
			
		||||
## SQLgrey config file ##
 | 
			
		||||
#########################
 | 
			
		||||
 | 
			
		||||
# Notes:
 | 
			
		||||
# - Unless specified otherwise commented settings are SQLgrey's defaults
 | 
			
		||||
# - SQLgrey uses a specific config file when called with -f <conf_file>
 | 
			
		||||
 | 
			
		||||
## Configuration files
 | 
			
		||||
# conf_dir = /etc/sqlgrey
 | 
			
		||||
 | 
			
		||||
## Log level
 | 
			
		||||
# Uncomment to change the log level (default is normal: 2)
 | 
			
		||||
# nothing: O, errors only: 0, warnings: 1, normal: 2, verbose: 3, debug: 4
 | 
			
		||||
loglevel = 2
 | 
			
		||||
 | 
			
		||||
## log categories can be fine-tuned,
 | 
			
		||||
# here are the log messages sorted by types and levels,
 | 
			
		||||
# (anything over the loglevel is discarded):
 | 
			
		||||
#
 | 
			
		||||
# grey     : (0) internal errors,
 | 
			
		||||
#	     (2) initial connections, early reconnections,
 | 
			
		||||
#	         awl matches, successful reconnections, AWL additions,
 | 
			
		||||
#	     (3) smart decision process debug,
 | 
			
		||||
# whitelist: (2) whitelisted connections,
 | 
			
		||||
#	     (3) actual whitelist hit,
 | 
			
		||||
#	     (4) whitelists reloads,
 | 
			
		||||
# optin:     (3) optin/optout global result
 | 
			
		||||
#	     (4) optin/optout SQL query results
 | 
			
		||||
# spam     : (2) attempts never retried,
 | 
			
		||||
# mail     : (1) error sending mails,
 | 
			
		||||
#	     (4) rate-limiter debug,
 | 
			
		||||
# dbaccess : (0) DB errors,
 | 
			
		||||
#            (1) DB upgrade,
 | 
			
		||||
#	     (2) DB upgrade details,
 | 
			
		||||
# martians : (2) invalid e-mail addresses,
 | 
			
		||||
# perf     : (2) cleanup time,
 | 
			
		||||
# system   : (0) error forking,
 | 
			
		||||
#	     (3) forked children PIDs, children exits,
 | 
			
		||||
# conf     : (0) errors in config files, missing required file,
 | 
			
		||||
# 	     (1) warnings in config files,
 | 
			
		||||
#	         missing optional configuration files,
 | 
			
		||||
#	     (2) reloading configuration files,
 | 
			
		||||
# other    : (4) Startup cleanup
 | 
			
		||||
# you can set a level to O (capital o) to disable logs completely,
 | 
			
		||||
# but be aware that then SQLgrey can come back to haunt you...
 | 
			
		||||
 | 
			
		||||
# Provide a coma-separated "logtype:loglevel" string
 | 
			
		||||
# For example if you set the loglevel to 3 (verbose) but want SQLgrey to be:
 | 
			
		||||
# . quiet for whitelists
 | 
			
		||||
# . normal for greylisting
 | 
			
		||||
# uncomment the following line.
 | 
			
		||||
# log_override = whitelist:1,grey:2
 | 
			
		||||
# By default, log_override is empty
 | 
			
		||||
 | 
			
		||||
## Log identification
 | 
			
		||||
# by default this is the process name. If you define the following variable
 | 
			
		||||
# SQLgrey will use whatever you set it to
 | 
			
		||||
# log_ident =
 | 
			
		||||
 | 
			
		||||
## username and groupname the daemon runs as
 | 
			
		||||
user = sqlgrey
 | 
			
		||||
group = nogroup
 | 
			
		||||
 | 
			
		||||
## Socket
 | 
			
		||||
# On which socket do SQLgrey wait for queries
 | 
			
		||||
# use the following if you need to bind on a public IP address
 | 
			
		||||
# inet = <public ip>:port
 | 
			
		||||
# default :
 | 
			
		||||
# inet = 2501    # bind to localhost:2501
 | 
			
		||||
 | 
			
		||||
## PID
 | 
			
		||||
# where to store the process PID
 | 
			
		||||
# pidfile = /var/run/sqlgrey.pid
 | 
			
		||||
 | 
			
		||||
## Config directory
 | 
			
		||||
# where to look for other configuration files (whitelists)
 | 
			
		||||
# confdir = /etc/sqlgrey
 | 
			
		||||
 | 
			
		||||
## Greylisting delays
 | 
			
		||||
# If you want to be really strict (RFC-wise) use these
 | 
			
		||||
# This is *not* recommended, you'll have false positives
 | 
			
		||||
# reconnect_delay = 15    # don't allow a reconnection before 15 minutes
 | 
			
		||||
# max_connect_age = 2     # don't allow a reconnection after 2 hours
 | 
			
		||||
 | 
			
		||||
# default: (based on real-life experience)
 | 
			
		||||
reconnect_delay = 6
 | 
			
		||||
max_connect_age = 24
 | 
			
		||||
 | 
			
		||||
## Throttling too many new entries from new host
 | 
			
		||||
# Setting this optional parameter will refuse an excessive number of
 | 
			
		||||
# new entries in the connect table from the same host, in the following
 | 
			
		||||
# manner:
 | 
			
		||||
# - If there are already "connect_src_throttle" entries in the connect
 | 
			
		||||
#   table from the same host (e-mails which have not been retried yet)
 | 
			
		||||
# - And there is NO entry for this host in domain_awl
 | 
			
		||||
# - And there are LESS than "connect_src_throttle" entries in the
 | 
			
		||||
#   from_awl table for this host
 | 
			
		||||
# THEN further incoming connections from this host will be (temporarily)
 | 
			
		||||
# refused without new entries being created in the connect table (until
 | 
			
		||||
# some already waiting entries have been successfully retried).
 | 
			
		||||
# This feature may prevent the connect table from growing too big and
 | 
			
		||||
# being polluted by spambots, viruses, zombie machines and the like.
 | 
			
		||||
# If set to "0" (default), this feature won't be used.
 | 
			
		||||
connect_src_throttle = 5
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Auto whitelists settings
 | 
			
		||||
# default is tailored for small sites
 | 
			
		||||
# awl_age = 60
 | 
			
		||||
# group_domain_level = 2
 | 
			
		||||
 | 
			
		||||
# For bigger sites you may want
 | 
			
		||||
# a smaller awl_age and a bigger group_domain_level
 | 
			
		||||
# AWL must be renewed at least once a month
 | 
			
		||||
# 32 > 31 (max delay between monthly newsletters)
 | 
			
		||||
awl_age = 33
 | 
			
		||||
# wait for 10 validated adresses to add a whole
 | 
			
		||||
# domain in AWL
 | 
			
		||||
group_domain_level = 10
 | 
			
		||||
 | 
			
		||||
## Database settings
 | 
			
		||||
# instead of Pg below use "mysql" for MySQL, "SQLite" for SQLite
 | 
			
		||||
# any DBD driver is allowed, but only the previous 3 have been tested
 | 
			
		||||
db_type = Pg
 | 
			
		||||
db_name = sqlgrey
 | 
			
		||||
# Note: the following are not used with SQLite
 | 
			
		||||
# On laisse pgsql meme pour ovh, sqlgrey sait detecter s'il perd le
 | 
			
		||||
# lien avec la base.
 | 
			
		||||
db_host = pgsql.adm.crans.org
 | 
			
		||||
 | 
			
		||||
db_user = sqlgrey
 | 
			
		||||
# db_pass = spaces_are_not_supported
 | 
			
		||||
# db_cleandelay = 1800 # in seconds, how much time between database cleanups
 | 
			
		||||
# clean_method = sync # sync : cleanup is done in the main process,
 | 
			
		||||
		      #        delaying other operations
 | 
			
		||||
                      # async: cleanup is done in a forked process,
 | 
			
		||||
		      #        it won't delay mail processing
 | 
			
		||||
                      #        BEWARE: lockups have been reported
 | 
			
		||||
		      #        and are still investigated
 | 
			
		||||
 | 
			
		||||
## X-Greylist header added?
 | 
			
		||||
# This adds delay, whitelist and autowhitelist information in the headers
 | 
			
		||||
prepend = 1
 | 
			
		||||
 | 
			
		||||
## Greylisting method:
 | 
			
		||||
# - full   : greylist by IP address
 | 
			
		||||
# - classc : greylist by class C network. eg:
 | 
			
		||||
#            2.3.4.6 connection accepted if 2.3.4.145 did connect earlier
 | 
			
		||||
# - smart  : greylist by class C network unless there is no reverse lookup
 | 
			
		||||
#            or it looks like a home-user address
 | 
			
		||||
# Default is smart
 | 
			
		||||
greymethod = smart
 | 
			
		||||
 | 
			
		||||
## Optin/Optout (see README.OPTINOUT for details)
 | 
			
		||||
# - none   : everyone is greylisted (default)
 | 
			
		||||
# - optin  : one must optin to have its (incoming) messages being greylisted
 | 
			
		||||
# - optout : one must optout to not have its messages being greylisted
 | 
			
		||||
optmethod = optout
 | 
			
		||||
 | 
			
		||||
## SQLgrey return value.
 | 
			
		||||
# SQLgrey can tell Postfix to:
 | 
			
		||||
# - immediately reject a message with a temporary reject code
 | 
			
		||||
# - only do so if following rules would allow the message to pass
 | 
			
		||||
# The first choice will prevent Postfix from spending time evaluating
 | 
			
		||||
# potentially expensive rules.
 | 
			
		||||
# In some cases you may want following rules to be aware of the connection
 | 
			
		||||
# this.
 | 
			
		||||
#
 | 
			
		||||
# We can specify a different rejection strategy for the first connection
 | 
			
		||||
# attempt, and for early reconnections. 'immed' chooses immediate rejection
 | 
			
		||||
# 'delay' choose delayed rejection
 | 
			
		||||
#
 | 
			
		||||
# By default we use delay on first attempt
 | 
			
		||||
# reject_first_attempt = delay
 | 
			
		||||
# Default for early reconnection is the value affected to reject_first_attempt
 | 
			
		||||
# reject_early_reconnect = delay
 | 
			
		||||
 | 
			
		||||
## Update server
 | 
			
		||||
# where to get updates for whitelists
 | 
			
		||||
# whitelists_host = sqlgrey.bouton.name
 | 
			
		||||
 | 
			
		||||
## Postmaster address
 | 
			
		||||
# who gets urgent notifications (DB is down for example)
 | 
			
		||||
# default or empty: don't send mail notifications
 | 
			
		||||
admin_mail = roots@crans.org
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,20 +17,15 @@
 | 
			
		|||
  roles:
 | 
			
		||||
    - framadate
 | 
			
		||||
 | 
			
		||||
# Deploy CAS
 | 
			
		||||
- hosts: cas-srv.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - django-cas
 | 
			
		||||
  roles: ["django-cas"]
 | 
			
		||||
 | 
			
		||||
# Deploy Gitlab CI
 | 
			
		||||
- hosts: gateau.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - docker
 | 
			
		||||
  roles: ["docker"]
 | 
			
		||||
 | 
			
		||||
# Deploy TV
 | 
			
		||||
- hosts: cochon.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - mumudvb
 | 
			
		||||
- hosts: ethercalc-srv.adm.crans.org
 | 
			
		||||
  roles: ["ethercalc"]
 | 
			
		||||
 | 
			
		||||
# Deploy OwnCloud
 | 
			
		||||
- hosts: owncloud-srv.adm.crans.org
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +108,3 @@
 | 
			
		|||
    - ftpsync
 | 
			
		||||
    - rsync-mirror
 | 
			
		||||
    - nginx-pubftp
 | 
			
		||||
 | 
			
		||||
- hosts: zephir.adm.crans.org,omnomnom.adm.crans.org
 | 
			
		||||
  roles:
 | 
			
		||||
    - backuppc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue