Changeset - 67dd87d59abb
[Not reviewed]
0 6 0
Branko Majic (branko) - 9 years ago 2016-11-28 22:39:12
branko@majic.rs
MAR-83: Added support to wsgi_website role for specifying headers that should be passed on to Gunicorn by Nginx. Updated hello.wsgi app to demonstrate the feature.
6 files changed with 24 insertions and 3 deletions:
0 comments (0 inline, 0 general)
docs/rolereference.rst
Show inline comments
 
.. _rolereference:
 

	
 
Role Reference
 
==============
 

	
 

	
 
Common parameters
 
-----------------
 

	
 
A number of common parameters are used by all of the roles during
 
deployment. This section lists such parameters.
 

	
 
**enable_backup** (boolean, optional, ``False``)
 
  If set to ``True``, and the role supports backups, server will be configured
 
  for back-up of role's data. See role description for more details on what is
 
  backed-up and if the option is available. Just keep in mind that if you enable
 
  this globally, all the roles will be running backup-specific tasks. If the
 
  option has been enabled, the ``backup_client`` role will be included
 
  automatically (see the role reference for details on parameters that need to
 
  be provided in the case).
 

	
 
**tls_private_key_dir** (string, optional if paths to private keys for all roles are explicitly specified)
 
  Path to directory on Ansible host that contains the private keys used by
 
  services deployed by various roles. When TLS keys are not explicitly defined
 
  in a role, this is the directory where the TLS key will be looked-up during
 
  Ansible run. Expected filename pattern is ``FQDN_SERVICE.key`` (for example,
 
  ``mail.example.com_smtp.key`` or ``xmpp.example.com_xmpp.key``).
 

	
 
**tls_certificate_dir** (string, optional if paths to certificate files for all roles are explicitly specified)
 
  Path to directory on Ansible host that contains the X.509 certificate files
 
  used by services deployed by various roles. When X.509 certificate is not
 
  explicitly defined in a role, this is the directory where the X.509
 
  certificate will be looked-up during Ansible run. Expected filename pattern is
 
  ``FQDN_SERVICE.pem`` (for example, ``mail.example.com_smtp.pem`` or
 
  ``xmpp.example.com_xmpp.pem``).
 

	
 

	
 
Preseed
 
-------
 

	
 
The ``preseed`` role can be used for generating simple preseed files for Debian
 
Wheezy installations.
 

	
 
The generated preseed files allow simplified installation, with a single root
 
partition. There is a number of parameters that allow for customising the
 
content of preseed files.
 

	
 
It is possible to specify parameter values that should be used for all servers,
 
as well for individual servers. It is also possible to combine this approach,
 
defining global parameters that get overridden per server.
 

	
 
The role will by default process all hosts from the inventory, generating one
 
preseed file per server.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**ansible_key** (string, optional, ``{{ lookup('file', '~/.ssh/id_rsa.pub') }}``)
 
  SSH public key that should be deployed to authorized_keys truststore for
 
  operating system user ``root``. This is necessary for the bootstrap process
 
  to work since Debian Jessie does not allow password-based logins for root.
 

	
 
**preseed_country** (string, optional, ``SE``)
 
  Country.
 

	
 
**preseed_directory** (string, optional, ``../preseed_files/``)
 
  Destination directory where the preseed files should be stored.
 

	
 
  .. warning::
 
     Do not name this directory ``preseed`` if it lies on a path where Ansible
 
     would normally look-up the roles (it will conflict with the role name).
 

	
 
**preseed_dns** (string, mandatory if **preseed_network_auto** is ``no``)
 
  Comma-separated list of DNS servers.
 

	
 
**preseed_domain** (string, mandatory if **preseed_network_auto** is ``no``)
 
  Server domain.
 

	
 
**preseed_gateway** (string, mandatory if **preseed_network_auto** is ``no``)
 
  Default gateway for the server.
 

	
 
**preseed_hostname** (string, mandatory if **preseed_network_auto** is ``no``)
 
  Server hostname.
 

	
 
**preseed_ip** (string, mandatory if **preseed_network_auto** is ``no``)
 
  IP address for the server network interface.
 

	
 
**preseed_keymap** (string, optional, ``us``)
 
  Keymap.
 

	
 
**preseed_language** (string, optional, ``en``)
 
  Language.
 

	
 
**preseed_locale** (string, optional, ``en_US.UTF-8``)
 
  Locale.
 

	
 
**preseed_mirror_directory** (string, optional, ``/debian``)
 
  Directory under which the Debian apt repositories can be found on the
 
  specified mirror.
 

	
 
**preseed_mirror_hostname** (string, optional, ``ftp.se.debian.org``)
 
  Resolvable hostname of FQDN where the Debian apt repositories can be
 
  found. Only HTTP mirrors are supported.
 

	
 
**preseed_mirror_proxy** (string, optional, ``None``)
 
  An HTTP proxy that should be used for accessing the Debian apt
 
  repositories.
 

	
 
**preseed_netmask** (string, mandatory if **preseed_network_auto** is ``no``)
 
  Netmask for the server network interface.
 

	
 
**preseed_network_auto** (boolean, optional, ``yes``)
 
  Specifies whether the network configuration should be automatic (using DHCP)
 
  or manual. If manual configuration is selected a number of additional options
 
  needs to be specified: ``preseed_hostname``, ``preseed_domain``,
 
  ``preseed_ip``, ``preseed_netmask``, ``preseed_gateway``,
 
  ``preseed_dns``. For some of these values you may want to use per-server
 
  overrides - see parameter ``preseed_server_overrides``.
 

	
 
**preseed_network_interface** (string, optional, ``eth0``)
 
  Name of network interface (for example ``eth0``, ``eth1`` etc) that should be
 
  configured.
 

	
 
**preseed_root_password** (string, optional, ``root``)
 
  Initial password that should be set for the server during the installation.
 

	
 
**preseed_server_overrides** (string, optional, ``{}``)
 
  A dictionary consisting out of one or more entries where individual values for
 
  preseed files can be overridden per-server. Each entry's key should be the
 
  name of the server, as specified in the inventory. Each value should also be a
 
  dictionary, where valid keys are: ``country``, ``dns``, ``domain``,
 
  ``gateway``, ``hostname``, ``ip``, ``keymap``, ``language``, ``locale``,
 
  ``mirror_directory``, ``mirror_hostname``, ``mirror_proxy``, ``netmask``,
 
  ``network_auto``, ``network_interface``, ``root_password``,
 
  ``timezone``. These have the same meaning as their ``preseed_`` counterparts.
 

	
 
**preseed_timezone** (string, optional, ``Europe/Stockholm``)
 
  Timezone that should be used when calculating server time. It is assumed that
 
  the local hardware clock is set to UTC.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for a preseed file that sets some global
 
defaults to be used for all servers, and then overrides it for one server:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  ansible_key: {{ lookup('file', '~/.ssh/id_rsa.pub') }}
 
  preseed_country: UK
 
  preseed_directory: /var/www/preseed
 
  preseed_keymap: UK
 
  preseed_language: en
 
  preseed_locale: en_UK.UTF-8
 
  preseed_mirror_directory: /debian
 
  preseed_mirror_hostname: ftp.uk.debian.org
 
  preseed_mirror_proxy: ""
 
  preseed_network_auto: yes
 
  preseed_network_interface: eth0
 
  preseed_root_password: secret
 
  preseed_timezone: Europe/London
 
  preseed_server_overrides:
 
    ldap.example.com:
 
      network_auto: no
 
      hostname: ldap
 
      domain: example.com
 
      ip: 192.168.1.20
 
      netmask: 255.255.255.0
 
      gateway: 192.168.1.1
 
      dns: 192.168.1.1,192.168.1.2
 
      timezone: Europe/Stockholm
 

	
 

	
 
Bootstrap
 
---------
 

	
 
The ``bootstrap`` role can be used for bootstraping a new server with
 
Ansible. In order to apply this role to a server, all that is necessary is root
 
access to the server (either via SSH or locally).
 

	
 
The role implements the following:
 

	
 
* Installs sudo package.
 
* Creates operating system user and group for Ansible (``ansible``).
 
* Sets-up an authorized_key for operating system user ``ansible`` (for remote
 
  SSH access).
 
* Configures sudo to allow operating system user ``ansible`` to run sudo
 
  commands without password authentication.
 
* Removes the Ansible user's key from the list of authorized keys for user root
 
  at the end of bootstrap process. This key was necessary only for the bootstrap
 
  process.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**ansible_key** (string, optional, ``{{ lookup('file', '~/.ssh/id_rsa.pub') }}``)
 
  SSH public key that should be deployed to authorized_keys truststore for
 
  operating system user ``ansible``.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Since the role is meant to be used just after the server has been installed, and
 
using the ``root`` account, it is probably going to be invoked from a separate
 
playbook.
 

	
 
For example, a playbook (``bootstrap.yml``) could look something similar to:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  - hosts: "{{ server }}"
 
    remote_user: root
 
    roles:
 
      - bootstrap
 
    vars:
 
      ansible_key: "{{ lookup('file', 'authorized_keys/ansible.pub') }}"
 

	
 
With such a playbook in place, it would be invoked with:
 

	
 
  ansible-playbook --ask-pass -e server=test1.example.com bootstrap.yml
 

	
 

	
 
Common
 
------
 

	
 
The ``common`` role can be used for applying a common configuration and
 
hardening across all servers, no matter what services they provide.
 

	
 
The role implements the following:
 

	
 
* Configures apt to use caching proxy (if any was specified).
 
* Sets-up umask for all logins to ``0027``.
 
* Installs sudo.
 
* Sets-up uniform bash prompt for all accounts (optionally coloured and with
 
  identifier). This is useful for distinguishing machines and/or environments.
 
* Sets-up ability to have user-specific ``/etc/profile.d/`` entries via
 
  ``$HOME/.profile.d/``.
 
* Installs additional base packages, as configured.
 
* Disables ``electric-indent-mode`` in Emacs globally if either the ``emacs24``
 
  or ``emacs24-nox`` are installed through the role.
 
* Creates additional operating system groups, as configured.
 
* Creates additional operating system users, as configured.
 
* Hardens the SSH server by disabling remote ``root`` logins and password-based
 
  authentication.
 
* Allows traversing of directory ``/etc/ssl/private/`` to everyone. This lets
 
  you put TLS private keys in central location where any operating system user
 
  can reach them provided they have appropriate read/write rights on the file
 
  itself, and provided they know the exact path of the file.
 
* Deploys CA certificate files, normally used for truststore purposes, to
 
  ``/usr/local/share/ca-certificates/``.
 
* Installs ``ferm`` (for iptables management), configuring a basic firewall
 
  which allows ICMP echo requests (PING), incoming connection on TCP port 22
 
  (SSH), and also introduces rate-limitting for incoming ICMP echo request
 
  pacakges and (new) TCP connections. The rate-limitting is based on the source
 
  IP address, using the ``iptables hashlimit`` module.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/var/log**
 
  Log files from the system.
 

	
 
**/home**
 
  Home directory for regular users.
 

	
 
**/root**
 
  Root user's home directory.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**apt_proxy** (string, optional, ``None``)
 
  URI of a caching proxy that should be used when retrieving the packages via
 
  apt.
 

	
 
**os_users** (list, optional, ``[]``)
 
  A list of operating system users that should be set-up on a server. Each item
 
  is a dictionary with the following options describing the user parameters:
 

	
 
  **name** (string, mandatory)
 
    Name of the operating system user that should be created. User's default
 
    group will have the same name as the user.
 

	
 
  **uid** (number, optional, ``whatever OS picks``)
 
    UID for the operating system user. User's default group will have a GID
 
    identical to the user's UID if specified. Otherwise user's default group
 
    will have OS-determined GID.
 

	
 
  **additional_groups** (list, optional, ``[]``)
 
    Comma-separated list of additional groups that a user should belong to.
 

	
 
  **authorized_keys** (list, optional, ``[]``)
 
    List of SSH public keys that should be deployed to user's authorized_keys
 
    truststore.
 

	
 
  **password** (string, optional, ``!`` - no password)
 
    Encrypted password that should be set for the user.
 

	
 
**os_groups** (list, optional, ``[]``)
 
  A list of operating system groups that should be set-up on a server. Each item
 
  is a dictionary with the following options describing the group parameters:
 

	
 
  **name** (string, mandatory)
 
    Name of the operating system group that should be created.
 

	
 
  **gid** (number, optional, ``whatever OS picks``)
 
    GID for the operating system group.
 

	
 
**common_packages** (list, optional, ``[]``)
 
  List of additional operating system packages that should be installed on the
 
  server. Each element of the list should be a simple string denoting the name
 
  of the package.
 

	
 
**ca_certificates** (list, optional, ``{}``)
 
  Dictionary containing the CA certificates to deploy. Keys are base filenames
 
  (**without extension**) to be used when placing a certificate file in
 
  directory ``/usr/local/share/ca-certificates/``, while values are
 
  corresponding content to be placed in the file.
 

	
 
**incoming_connection_limit** (string, optional, ``3/second``)
 
  Rate at which the incoming ICMP echo-request packages and new TCP connections
 
  will be accepted at. The value should be specified in the same format as value
 
  for the ``iptables hashlimit`` option ``--hashlimit-upto``.
 

	
 
**incoming_connection_limit_burst** (string, optional, ``9``)
 
  Initial burst of packages that should be accepted when the client with
 
  distinct source IP address connects to the server for the first time (usually
 
  higher than ``incoming_connection_limit``), even if it would go above the
 
  specified connection limit.
 

	
 
**prompt_colour** (string, optional, ``none``)
 
  Colour for showing the Bash prompt. Supported values are:
 

	
 
  ``black``, ``red``, ``green``, ``brown``, ``blue``, ``purple``, ``cyan``,
 
  ``light_gray``, ``dark_gray``, ``light_red``, ``light_green``, ``yellow``,
 
  ``light_blue``, ``light_purple``, ``light_cyan``, ``white``, ``none``.
 

	
 
  You should probably *not* use the ``black`` colour. Setting affects Bash
 
  shells *only*. Setting the value to ``none`` uses default terminal colour.
 

	
 
**prompt_id** (string, optional, ``NONE``)
 
  Optional identifier appended to regular Bash prompt, useful for visually
 
  identifying distinct environments. For example, if set to ``test``, resulting
 
  prompt will be similar to ``admin@web[test]:~$``. Setting affects Bash shells
 
  *only*.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up some common users, groups, and
 
packages on all servers:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  os_users:
 
    - name: admin
 
      uid: 1000
 
      additional_groups:
 
        - sudo
 
      authorized_keys:
 
        - "{{ lookup('file', '/home/admin/.ssh/id_rsa.pub') }}"
 
      password: '$6$AaJRWtqyX5pk$IP8DUjgY0y2zqMom9BAc.O9qHoQWLFCmEsPRCika6l/Xh87cp2SnlMywH0.r4uEcbHnoicQG46V9VrJ8fxp2d.'
 
    - name: john
 
      uid: 1001
 
      password: '$6$AaJRWtqyX5pk$IP8DUjgY0y2zqMom9BAc.O9qHoQWLFCmEsPRCika6l/Xh87cp2SnlMywH0.r4uEcbHnoicQG46V9VrJ8fxp2d.'
 

	
 
  os_groups:
 
    - name: localusers
 
      gid: 2500
 

	
 
  common_packages:
 
    - emacs23-nox
 
    - screen
 
    - debconf-utils
 

	
 
  ca_certificates:
 
    "truststore": "{{ lookup('file', '../certs/truststore.pem') }}"
 

	
 
  incoming_connection_limit: 2/second
 

	
 
  incoming_connection_limit_burst: 6
 

	
 
  prompt_colour: light_green
 

	
 
  prompt_id: PROD
 

	
 
.. _ldap_client:
 

	
 
LDAP Client
 
-----------
 

	
 
The ``ldap_client`` role can be used for setting-up an OpenLDAP client on
 
destination machine.
 

	
 
The role implements the following:
 

	
 
* Installs OpenLDAP client tools.
 
* Sets-up global configuration file for OpenLDAP clients at /etc/ldap/ldap.conf.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**ldap_client_config** (list, optional, ``[]``)
 
  A list of configuration options that should be put into the LDAP configuration
 
  file. Each item is a dictionary with the following options defining the
 
  configuration parameter:
 

	
 
  **comment** (string, mandatory)
 
    Comment that will be shown in the file just above the configuration option.
 

	
 
  **option** (string, mandatory)
 
    Name of configuration option.
 

	
 
  **value** (string, mandatory)
 
    Value for configuration option.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting some common LDAP client options:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  ldap_client_config:
 
    - comment: Set the base DN
 
      option: BASE
 
      value: dc=example,dc=com
 
    - comment: Set the default URI
 
      option: URI
 
      value: ldap://ldap.example.com/
 
    - comment: Set the truststore for TLS/SSL
 
      option: TLS_CACERT
 
      value: /etc/ssl/certs/example_ca.pem
 
    - commment: Force basic server certificate verification
 
      option: TLS_REQCERT
 
      value: demand
 
    - comment: Disable CRL checks for server certificate
 
      option: TLS_CRLCHECK
 
      value: none
 

	
 

	
 
LDAP Server
 
-----------
 

	
 
The ``ldap_server`` role can be used for setting-up an OpenLDAP server on
 
destination machine.
 

	
 
The role implements the following:
 

	
 
* Deploys LDAP TLS private key and certificate.
 
* Hardens TLS configuration by allowing only TLSv1.2 and PFS ciphers.
 
* Installs OpenLDAP server (package ``slapd``).
 
* Configures OpenLDAP server (base DN - domain, organisation, TLS, SSF, log levels).
 
* Sets-up separate log file for OpenLDAP server at ``/var/log/slapd.log`` (with
 
  log rotation included).
 
* Enables the ``misc`` LDAP schema (from ``/etc/ldap/schema/misc.ldif``). This
 
  is necessary for the mail server role.
 
* Enables the ``memberof`` overlay on top of default database. The overlay is
 
  configured to keep track of membership changes for object class
 
  ``groupOfUniqueNames`` via attribute ``uniqueMember``. Enforcement of
 
  referential integrity is turned on as well (modifications of ``memberof``
 
  attribute will update corresponding group as well.
 
* Creates a basic directory structure used by most of the other roles.
 
* Creates a basic directory structure used by the mail server role.
 
* Creates login entries for services that need to consume LDAP directory data in
 
  some way.
 
* Creates user-supplied groups in LDAP.
 
* Configures permissions.
 
* Creates LDAP entries.
 
* Configures firewall to allow incoming connections to the LDAP server (via both
 
  TCP 389 and 636).
 
* Sets the LDAP server administrator's password.
 

	
 

	
 
LDIF Templates
 
~~~~~~~~~~~~~~
 

	
 
For adding users, use::
 

	
 
  dn: uid=USERNAME,ou=people,BASE_DN
 
  objectClass: inetOrgPerson
 
  objectClass: simpleSecurityObject
 
  uid: USERNAME
 
  userPassword: PASSWORD_FROM_SLAPPASSWD
 
  cn: NAME SURNAME
 
  sn: SURNAME
 
  gn: NAME
 
  displayName: DISPLAYNAME
 
  initials: INITIALS
 
  mail: MAIL
 
  mobile: MOBILE
 

	
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **ldap_client**
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/srv/backup/slapd.bak**
 
  Dump of the LDAP database. LDAP database dump is created every day at 01:45 in
 
  the morning. This does *not* include the dump of the config database
 
  (``cn=config``).
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**ldap_admin_password** (string, mandatory)
 
  Password for the default administrator account of LDAP server (the
 
  ``cn=admin,DOMAIN`` entry/user).
 

	
 
**ldap_entries** (list, optional, ``[]``)
 
  List of entries that should be kept in the LDAP directory. Each item is a
 
  dictionary describing a single LDAP entry, with all of its attributes
 
  listed. The keys in this dictionary should be the attribute names. The values
 
  should be either strings, for setting a single attribute value, or a list of
 
  strings if it is necessary to set multiple values for the same attribute.
 

	
 
**ldap_permissions** (list, optional, ``see below``)
 
  List of LDAP access rules to apply to base DN served by the LDAP server. The
 
  listed access control rules will *replace* all existing rules, and will be
 
  added in the same order they are listed in. Each item is a string that
 
  constitutes a single access control rule. The format should be the same as
 
  described in `OpenLDAP Administrator's Guide
 
  <http://www.openldap.org/doc/admin24/access-control.html#Access%20Control%20via%20Dynamic%20Configuration>`.
 

	
 
  Default value is:
 

	
 
  .. code-block:: yaml
 

	
 
    - >
 
      to *
 
      by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
 
      by * break
 
    - >
 
      to attrs=userPassword,shadowLastChange
 
      by self write
 
      by anonymous auth
 
      by dn="cn=admin,BASEDN" write
 
      by * none
 
    - >
 
      to dn.base=""
 
      by * read
 
    - >
 
      to *
 
      by self write
 
      by dn="cn=admin,BASEDN" write
 
      by * none
 

	
 
**ldap_server_consumers** (list, optional, ``[]``)
 
  List of items describing additional login entries that should be created for
 
  services that want to be able to log-in into the LDAP server and consume the
 
  data present within. Each item should be a dictionary, with the following keys
 
  avaialable:
 

	
 
  - **name** (name of the service, mandatory, this will be used to construct the
 
    login entry DN in format of ``cn=NAME,ou=services,BASE_DN``)
 
  - **password** (password for the login entry, mandatory)
 
  - **state** (state of the service, optional, defaults to ``present``, this
 
    should be ``present`` or ``absent``, allowing for removal of old services)
 

	
 
**ldap_server_groups** (list, optional, ``[]``)
 
  List of groups that should be created in the LDAP directory. Each item should
 
  be a dictionary containing the following keys:
 

	
 
  - **name** (name of the group, mandatory, this will be used to construct the
 
    group DN in format of ``cn=NAME,ou=groups,BASE_DN``)
 
  - **state** (state of the group, optional, defaults to ``present``, this
 
    should be ``present`` or ``absent``, allowing for removal of old groups)
 

	
 
**ldap_server_domain** (string, optional, ``{{ ansible_domain }}``)
 
  Domain that should be used for constructing the base DN of default user LDAP
 
  database. This should be a sub-domain dedicated to organisation. The base DN
 
  will be constructed by putting all elements of the sub-domain as ``dc``
 
  entries (as per standard Debian convention). I.e. ``example.com`` would get
 
  transformed into ``dc=example,dc=com``.
 

	
 
**ldap_server_organization** (string, optional, ``Private``)
 
  Organization that should be specified in the base DN entry.
 

	
 
**ldap_server_log_level** (string, optional, ``256``)
 
  Log level to use for the server. This should be compatible with OpenLDAP
 
  configuration option ``olcLogLevel``. See `OpenLDAP Administrator's Guide
 
  <http://www.openldap.org/doc/admin24/slapdconf2.html#cn=config>` for value
 
  description and syntax.
 

	
 
**ldap_server_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' ansible_fqdn + '_ldap.pem') }}``)
 
  X.509 certificate used for TLS for LDAP service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_ldap.pem``.
 

	
 
**ldap_server_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' ansible_fqdn + '_ldap.key') }}``)
 
  Private key used for TLS for LDAP service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_ldap.key``.
 

	
 
**ldap_server_ssf** (number, optional, ``128``)
 
  Minimum *Security Strength Factor* to require from all incoming
 
  connections. This applies for both remote and local connections.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up LDAP server:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  ldap_server_domain: "example.com"
 
  ldap_server_organization: "Example Corporation"
 
  ldap_server_log_level: 256
 
  ldap_server_tls_certificate: "{{ lookup('file', '~/tls/ldap.example.com_ldap.pem') }}"
 
  ldap_server_tls_key: "{{ lookup('file', '~/tls/ldap.example.com_ldap.key') }}"
 
  ldap_server_ssf: 128
 

	
 
  ldap_permissions:
 
    - >
 
      to *
 
      by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
 
      by * break
 
    - >
 
      to attrs=userPassword,shadowLastChange
 
      by self write
 
      by anonymous auth
 
      by dn="cn=admin,dc=example,dc=com" write
 
      by * none
 
    - >
 
      to dn.base=""
 
      by * read
 
    - >
 
      to *
 
      by self write
 
      by dn="cn=admin,dc=example,dc=com" write
 
      by users read
 
      by * none
 

	
 
  ldap_entries:
 
    - dn: ou=people,dc=example,dc=com
 
      objectClass: organizationalUnit
 
      ou: people
 
    - dn: ou=groups,dc=example,dc=com
 
      objectClass: organizationalUnit
 
      ou: groups
 
    - dn: uid=john,dc=example,dc=com
 
      objectClass:
 
        - inetOrgPerson
 
        - simpleSecurityObject
 
      userPassword: somepassword
 
      uid: john
 
      cn: John Doe
 
      sn: Doe
 

	
 

	
 
XMPP Server
 
-----------
 

	
 
The ``xmpp_server`` role can be used for setting-up Prosody, an XMPP server, on
 
destination machine.
 

	
 
The role implements the following:
 

	
 
* Sets-up the Prosody apt repository.
 
* Deploys XMPP TLS private key and certificate.
 
* Installs Prosody.
 
* Configures Prosody.
 
* Configures firewall to allow incoming connections to the XMPP server.
 

	
 
Prosody is configured as follows:
 

	
 
* Modules enabled: roster, saslauth, tls, dialback, posix, private, vcard,
 
  version, uptime, time, ping, pep, register, admin_adhoc, announce, legacyauth.
 
* Self-registration is not allowed.
 
* TLS is configured. Legacy TLS is available on port 5223.
 
* Client-to-server communication requires encryption (TLS).
 
* Authentication is done via LDAP. For setting the LDAP TLS truststore, see
 
  :ref:`LDAP Client <ldap_client>`.
 
* Internal storage is used.
 
* For each domain specified, a dedicated conference/multi-user chat (MUC)
 
  service is set-up, with FQDN set to ``conference.DOMAIN``.
 
* For each domain specified, a dedicated file proxy service will be set-up, with
 
  FQDN set to ``proxy.DOMAIN``.
 

	
 
.. warning::
 
   Since it is not possible to set-up separate TLS configuration for *c2s* and
 
   *s2s* connections in Prosody 0.9.x, no hardening of TLS is performed in order
 
   to improve interoperability. This will be changed in Prosody 0.10.x, at which
 
   point hardening can be revisited.
 

	
 
Prosody expects a specific directory structure in LDAP when doing look-ups:
 

	
 
* Prosody will log-in to LDAP as user
 
  ``cn=prosody,ou=services,XMPP_LDAP_BASE_DN``.
 
* User entries are read from sub-tree (first-level only)
 
  ``ou=people,XMPP_LDAP_BASE_DN``. Query filter used for finding users is
 
  ``(&(mail=$user@$host)(memberOf=cn=xmpp,ou=groups,XMPP_LDAP_BASE_DN))``. This
 
  allows group-based granting of XMPP service to users.
 

	
 

	
 
LDIF Templates
 
~~~~~~~~~~~~~~
 

	
 
For adding user to a group, use::
 

	
 
  dn: cn=xmpp,ou=groups,BASE_DN
 
  changetype: modify
 
  add: uniqueMember
 
  uniqueMember: uid=USERNAME,ou=people,BASE_DN
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/var/lib/prosody/**
 
  Roster information, as well as undelivered (offline) messages for all XMPP
 
  users. Keep in mind that list of available users and their credentials are
 
  stored in the LDAP directory (which is backed-up via LDAP server role).
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**xmpp_administrators** (list, mandatory)
 
  List of Prosody users that should be granted administrator privileges over
 
  Prosody. Each item is a string with value equal to XMPP user ID
 
  (i.e. ``john.doe@example.com``).
 

	
 
**xmpp_domains** (list, optional, ``{{ ansible_domain }}``)
 
  List of domains that are served by this Prosody instance. Each item is a
 
  string specifying a domain.
 

	
 
**xmpp_ldap_base_dn** (string, mandatory)
 
  Base DN on the LDAP server. A specific directory structure is expected under
 
  this entry (as explained above) in order to locate the available domains,
 
  users, aliases etc.
 

	
 
**xmpp_ldap_password** (string, mandatory)
 
  Password used for authenticating to the LDAP server.
 

	
 
**xmpp_ldap_server** (string, mandatory)
 
  Fully qualified domain name, hostname, or IP address of the LDAP server used
 
  for user authentication and listing.
 

	
 
**xmpp_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_xmpp.pem') }}``)
 
  X.509 certificate used for TLS for XMPP service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_xmpp.pem``.
 

	
 
**xmpp_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' + fqdn + '_xmpp.key') }}``)
 
  Private key used for TLS for XMPP service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_xmpp.key``.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up XMPP server using Prosody:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  xmpp_administrators:
 
    - john.doe@example.com
 
  xmpp_domains:
 
    - example.com
 
  xmpp_ldap_base_dn: dc=example,dc=com
 
  xmpp_ldap_password: xmpp
 
  xmpp_ldap_server: ldap.example.com
 
  # These are default key and certificate that generated during Prosody
 
  # installation. Possibly you want to deploy your own.
 
  xmpp_tls_key: "{{ lookup('file', '/etc/prosody/certs/localhost.key') }}"
 
  xmpp_tls_certificate: "{{ lookup('file', '/etc/prosody/certs/localhost.crt') }}"
 

	
 

	
 
Mail Server
 
-----------
 

	
 
.. warning::
 
   It may happen that the ``clamav-freshclam`` service hasn't finished
 
   downloading the virus database before the ``clamav-daemon`` and
 
   ``clamav-milter`` services are enabled during the initial run. If mail server
 
   is not operational, you may need to wait for a little while for download to
 
   finish, and then restart the ``clamav-daemon`` and ``clamav-milter``
 
   services.
 

	
 
The ``mail_server`` role can be used for setting-up a complete mail server
 
solution, which includes both SMTP and IMAP service, on destination machine.
 

	
 
Postfix is used SMTP, while Dovecot is used for IMAP.
 

	
 
The role implements the following:
 

	
 
* Installs rsync.
 
* Deploys IMAP/SMTP TLS private keys and certificates.
 
* Installs and configures Dovecot, Postfix, ClamAV, and ClamAV Milter.
 
* Purges Exim4 configuration (just in case).
 
* Sets-up aliases for the local recipients.
 
* Installs SWAKS (utility for testing SMTP servers).
 
* Sets-up the necessary directories and files under Postfix chroot.
 
* Configures firewall to allow incoming connections to the mail server. This
 
  includes set-up of redirection from TCP port 26 to TCP port 587 (alternate
 
  SMTP submission port to work around common network blocks).
 

	
 
Deployed services are configured as follows:
 

	
 
* Both Postfix and Dovecot look-up available domains, users, and aliases in
 
  LDAP.
 
* Incoming and outgoing mail is scanned with ClamAV (via ClamAV
 
  Milter). Infected mails are rejected.
 
* Mail is stored in directory ``/var/MAIL_USER/DOMAIN/USER``, using ``Maildir``
 
  format.
 
* TLS is required for user log-ins for both SMTP and IMAP.
 
* For user submission (SMTP), users must connect and authenticate over TCP
 
  port 587.
 
* TLS configuration is hardened for Dovecot, allowing only TLSv1.2 and PFS
 
  ciphers.
 
* TLS configuration is hardened for Postfix on submission port 587, allowing
 
  only TLSv1.2 and PFS ciphers. No TLS hardening is performed on port 25 in
 
  order to maintain maximum interoperability.
 
* RBL's are used for combating spam (if any is specified in configuration, see
 
  below).
 
* Postfix is configured to deliver undeliverable bounces to postmaster. This
 
  helps with detecting misconfigured applications and servers.
 

	
 
Both Postfix and Dovecot expect a specific directory structure in LDAP when
 
doing look-ups:
 

	
 
* Postfix will log-in to LDAP as user
 
  ``cn=postfix,ou=services,MAIL_LDAP_BASE_DN``.
 
* Dovecot will log-in to LDAP as user
 
  ``cn=dovecot,ou=services,MAIL_LDAP_BASE_DN``.
 
* Domain entries need to be available as
 
  ``dc=DOMAIN,ou=domains,ou=mail,ou=services,MAIL_LDAP_BASE_DN``.
 
* Alias entries need to be available as
 
  ``cn=ALIAS,ou=aliases,ou=mail,ou=services,MAIL_LDAP_BASE_DN``.
 
* User entries are read from sub-tree (first-level only)
 
  ``ou=people,MAIL_LDAP_BASE_DN``. Query filter used for finding users is
 
  ``(&(mail=%s)(memberOf=cn=mail,ou=groups,MAIL_LDAP_BASE_DN))``. This allows
 
  group-based granting of mail services to users.
 

	
 

	
 
LDIF Templates
 
~~~~~~~~~~~~~~
 

	
 
For adding domains, use::
 

	
 
  dn: dc=DOMAIN,ou=domains,ou=mail,ou=services,BASE_DN
 
  objectClass: dNSDomain
 
  dc: DOMAIN
 

	
 
For adding aliases, use::
 

	
 
  dn: cn=ALIAS,ou=aliases,ou=mail,ou=services,BASE_DN
 
  objectClass: nisMailAlias
 
  cn: ALIAS
 
  rfc822MailMember: REALEMAIL
 

	
 
For adding user to a group, use::
 

	
 
  dn: cn=mail,ou=groups,BASE_DN
 
  changetype: modify
 
  add: uniqueMember
 
  uniqueMember: uid=USERNAME,ou=people,BASE_DN
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/var/{{ mail_user }}**
 
  All data stored by the mail server, including mails and Sieve scripts. Keep in
 
  mind that list of available users and their credentials are stored in the LDAP
 
  directory (which is backed-up via LDAP server role).
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**mail_ldap_base_dn** (string, mandatory)
 
  Base DN on the LDAP server. A specific directory structure is expected under
 
  this entry (as explained above) in order to locate the available domains,
 
  users, aliases etc.
 

	
 
**mail_ldap_url** (string, mandatory)
 
  LDAP URL that should be used for connecting to the LDAP server for doing
 
  domain/user look-ups.
 

	
 
**mail_ldap_tls_truststore** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/truststore.pem') }}``)
 
  X.509 certificate chain used for issuing certificate for the LDAP service. The
 
  file will be stored in locations ``/etc/ssl/certs/mail_ldap_tls_truststore.pem``
 
  and ``/var/spool/postfix/etc/ssl/certs/mail_ldap_tls_truststore.pem``.
 

	
 
**mail_ldap_postfix_password** (string, mandatory)
 
  Password for authenticating the Postfix LDAP user.
 

	
 
**mail_ldap_dovecot_password** (string, mandatory)
 
  Password for authenticating the Dovecot LDAP user.
 

	
 
**mail_user** (string, optional, ``vmail``)
 
  Name of the user that owns all the mail files.
 

	
 
**mail_user_uid** (integer, optional, ``whatever OS picks``)
 
  UID of the user that owns all the mail files.
 

	
 
**mail_user_gid** (integer, optional, ``whatever OS picks``)
 
  GID of the user that owns all the mail files.
 

	
 
**imap_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' ansible_fqdn + '_imap.pem') }}``)
 
  X.509 certificate used for TLS for IMAP service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_imap.pem``.
 

	
 
**imap_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' ansible_fqdn + '_imap.key') }}``)
 
  Private key used for TLS for IMAP service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_imap.key``.
 

	
 
**local_mail_aliases** (dictionary, optional, ``[]``)
 
  Dictionary defining the local aliases. Aliases defined this way will either be
 
  appended to default aliases on the server, or replace the existing entries (if
 
  the alias/recipient is already present). Keys in the dictionary are the local
 
  recipients/aliases, while the value provided should be a space-separated list
 
  of mail addresses (or local users) where the mails should be forwarded.
 

	
 
**smtp_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' ansible_fqdn + '_smtp.pem') }}``)
 
  X.509 certificate used for TLS for SMTP service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_smtp.pem``.
 

	
 
**smtp_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' ansible_fqdn + '_smtp.key') }}``)
 
  Private key used for TLS for SMTP service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_smtp.key``.
 

	
 
**imap_folder_separator** (string, optional, ``/``)
 
  Character used for separating the IMAP folders when clients are requesting
 
  listing from the server. Usually either slash(``/``) or dot(``.``).
 

	
 
**smtp_rbl** (list, optional, ``[]``)
 
  List of RBLs to use for detecting servers which send out spam. Each item is a
 
  string resembling the RBL domain.
 

	
 
**mail_postmaster** (string, optional, ``postmaster@{{ ansible_domain}}``)
 
  Mail address to use for the postmaster account in Dovecot.
 

	
 
**smtp_allow_relay_from** (list, optional, [])
 
  List of networks from which mail relaying is allowed even without
 
  authentication. Each item in the list is a string defining a network. The
 
  format must be compatible with Postfix ``mynetworks`` setting (for example:
 
  ``192.168.1.0/24``, ``myhost.example.com`` etc).
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up XMPP server using Prosody:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  mail_ldap_url: ldap://ldap.example.com/
 
  mail_ldap_tls_truststore: /etc/ssl/certs/truststore.pem
 
  mail_ldap_base_dn: dc=example,dc=com
 
  mail_ldap_postfix_password: postfix
 
  mail_ldap_dovecot_password: dovecot
 

	
 
  mail_user: vmail
 
  mail_user_uid: 5000
 
  mail_user_gid: 5000
 

	
 
  # All mails sent to local user root will be forwarded to external account as
 
  # well.
 
  local_mail_aliases:
 
    root: "root john.doe@example.com"
 

	
 
  imap_tls_certificate: "{{ lookup('file', '~/tls/mail.example.com_imap.pem') }}"
 
  imap_tls_key: "{{ lookup('file', '~/tls/mail.example.com_imap.key') }}"
 
  smtp_tls_certificate: "{{ lookup('file', '~/tls/mail.example.com_smtp.pem') }}"
 
  smtp_tls_key: "{{ lookup('file', '~/tls/mail.example.com_smtp.key') }}"
 
  imap_folder_separator: /
 
  smtp_rbl:
 
    - bl.spamcop.net
 
    - zen.spamhaus.org
 
  mail_postmaster: postmaster@example.com
 

	
 
  smtp_allow_relay_from:
 
    - ldap.example.com
 
    - xmpp.example.com
 

	
 

	
 
Mail Forwarder
 
--------------
 

	
 
The ``mail_forwarder`` role can be used for setting-up a local SMTP server for
 
sending out mails and receiving mails for local users. The SMTP server is
 
provided by Postfix.
 

	
 
SMTP service on server set-up this way is not meant to be exposed to the
 
Internet directly, and should receive delivery failures from the relay server
 
instead.
 

	
 
The role implements the following:
 

	
 
* Installs and configures Postfix.
 
* Purges Exim4 configuration (just in case).
 
* Sets-up aliases for the local recipients.
 
* Installs SWAKS (utility for testing SMTP servers).
 
* Configures firewall to accept SMTP connections from SMTP relay (if one has
 
  been configured). This allows for delivery of bounced e-mails.
 

	
 
Postfix is configured as follows:
 

	
 
* Local destinations are set-up.
 
* A relay host is set.
 
* TLS is enforced for relaying mails, with configurable truststore for server
 
  certificate verification if SMTP relay is used. If SMTP relay is not used
 
  (configured), no certificate verification is done.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**local_mail_aliases** (dictionary, optional, ``[]``)
 
  Dictionary defining the local aliases. Aliases defined this way will either be
 
  appended to default aliases on the server, or replace the existing entries (if
 
  the alias/recipient is already present). Keys in the dictionary are the local
 
  recipients/aliases, while the value provided should be a space-separated list
 
  of mail addresses (or local users) where the mails should be forwarded.
 

	
 
**smtp_relay_host** (string, optional, ``None``)
 
  SMTP server via which the mails are sent out for non-local recipients.
 

	
 
**smtp_relay_truststore** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/truststore.pem') }}``)
 
  X.509 certificate chain used for issuing certificate for the SMTP relay
 
  service. The file will be stored in location
 
  ``/etc/ssl/certs/smtp_relay_truststore.pem``
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up the mail forwarder:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  # All mails sent to local user root will be forwarded to external account as
 
  # well.
 
  local_mail_aliases:
 
    root: "root john.doe@example.com"
 

	
 
  smtp_relay_host: mail.example.com
 

	
 
  smtp_relay_truststore: /etc/ssl/certs/example_ca_chain.pem
 

	
 

	
 
Web Server
 
----------
 

	
 
The ``web_server`` role can be used for setting-up a web server on destination
 
machine.
 

	
 
The role is supposed to be very lightweight, providing a basis for deployment of
 
web applications.
 

	
 
The role implements the following:
 

	
 
* Installs and configures nginx with a single, default vhost with a small static
 
  index page.
 
* Deploys the HTTPS TLS private key and certificate (for default vhost).
 
* Hardens TLS configuration by allowing only TLSv1.2 and PFS ciphers.
 
* Configures firewall to allow incoming connections to the web server.
 
* Installs and configures virtualenv and virtualenvwrapper as a common base for
 
  Python apps.
 
* Installs and configures PHP FPM as a common base for PHP apps.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**default_enforce_https** (boolean, optional, ``True``)
 
  Specify if HTTPS should be enforced for the default virtual host or not. If
 
  enforced, clients connecting via plaintext will be redirected to HTTPS, and
 
  clients will be served with ``Strict-Transport-Security`` header with value of
 
  ``max-age=31536000; includeSubDomains``.
 

	
 
**default_https_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' + ansible_fqdn + '_https.pem') }}``)
 
  X.509 certificate used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_https.pem``.
 

	
 
**default_https_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' + ansible_fqdn + '_https.key') }}``)
 
  Private key used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_https.key``.
 

	
 
**web_default_title** (string, optional, ``Welcome``)
 
  Title for the default web page shown to users (if no other vhosts were matched).
 

	
 
**web_default_message** (string, optional, ``You are attempting to access the web server using a wrong name or an IP address. Please check your URL.``)
 
  Message for the default web page shown to users (if no other vhosts were
 
  matched).
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up web server:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  default_https_tls_key: "{{ lookup('file', inventory_dir + '/tls/web.example.com_https.key') }}"
 
  default_https_tls_certificate: "{{ lookup('file', inventory_dir + '/tls/web.example.com_https.pem') }}"
 

	
 
  web_default_title: "Welcome to Example Inc."
 
  web_default_message: "You are attempting to access the web server using a wrong name or an IP address. Please check your URL."
 

	
 

	
 
PHP Website
 
-----------
 

	
 
The ``php_website`` role can be used for setting-up a website powered by PHP on
 
destination machine.
 

	
 
This role is normally not supposed to be used directly, but should instead serve
 
as the basis for writing website-specific roles. Therefore the role is written
 
in quite generic way, allowing the integrator to write his/her own logic for
 
deploying the necessary PHP applications, while still reusing a common base and
 
reducing the workload.
 

	
 
The role implements the following:
 

	
 
* Creates a dedicated user/group for running the PHP scripts.
 
* Creates a dedicated administrator user for maintaining the website.
 
* Creates a base directory where the website-specific code and data should be
 
  stored at.
 
* Adds nginx to website's group, so nginx could read the necessary files.
 
* Adds website administrator to website's group, so administrator could manage
 
  the code and data.
 
* Installs additional packages required for running the role (as configured).
 
* Deploys the HTTPS TLS private key and certificate (for website vhost).
 
* Configures PHP FPM and nginx to serve the website.
 

	
 
The role is implemented with the following layout/logic in mind:
 

	
 
* Website users are named after the ``FQDN`` (fully qualified domain name) of
 
  website, in format of ``web-ESCAPEDFQDN``, where ``ESCAPEDFQDN`` is equal to
 
  ``FQDN`` where dots have been replaced by underscores (for example,
 
  ``web-cloud_example_com``).
 
* Website users are set-up via GECOS field to have their umask set to ``0007``
 
  (in combination with ``pam_umask``).
 
* Administrator users are named after the ``FQDN`` (fully qualified domain name)
 
  of website, in format of ``admin-ESCAPEDFQDN``, where ``ESCAPEDFQDN`` is equal
 
  to ``FQDN`` where dots have been replaced by underscores (for example,
 
  ``admin-cloud_example_com``).
 
* All websites reside within a dedicated sub-directory in ``/var/www``. The
 
  sub-directory name is equal to the ``FQDN`` used for accessing the
 
  website. Owner of the directory is set to be the application administrator,
 
  while group is set to be the website group. Additionally, ``SGID`` bit is set
 
  on the directory. This allows admin, with correct umask, to create necessary
 
  files and directories that should be readable (and eventually writeable) by
 
  the website user (running the PHP scripts) without having to become root.
 
* All files placed in the website directory should be either created there
 
  directly, or copied to the directory in order to make sure the ``SGID`` gets
 
  honored. **Do not move the files, the permissions will not be set correctly.**
 
* Within the website directory, nginx/php5-fpm will expect to find the relevant
 
  files within the htdocs sub-directory (this can be symlink too).
 
* nginx communicates with PHP FPM over a dedicated Unix socket for each website.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **web_server**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**additional_fpm_config** (dict, optional, ``{}``)
 
  Additional PHP FPM configuration options that should be included for PHP
 
  website's pool. Keys are parameter names, values are associated values. Don't
 
  forget to include quotes in the value itself if expected value type is string.
 

	
 
**additional_nginx_config** (list, optional, ``[]``)
 
  List providing additional Nginx configuration options to include. This can be
 
  useful for specifying things like error pages. Options are applied inside of a
 
  **server** context of Nginx configuration file.
 

	
 
  Each item is a dictionary with the following options describing the extra
 
  configuration option:
 

	
 
  **comment** (string, mandatory)
 
    Comment describing the configuration option.
 

	
 
  **value** (string, mandatory)
 
    Configuration option.
 

	
 
**admin_uid** (integer, optional, ``whatever OS picks``)
 
  UID of the dedicated website administrator user. The user will be member of
 
  website group.
 

	
 
**deny_files_regex** (list, optional, ``[]``)
 
  List of regular expressions for matching files/locations to which the web
 
  server should deny access. This is useful to block access to any sensitive
 
  files that should not be served directly by the web server. The format must be
 
  compatible with regular expressions used by ``nginx`` for ``location ~``
 
  syntax.
 

	
 
**enforce_https** (boolean, optional, ``True``)
 
  Specify if HTTPS should be enforced for the website or not. If enforced,
 
  clients connecting via plaintext will be redirected to HTTPS, and clients will
 
  be served with ``Strict-Transport-Security`` header with value of
 
  ``max-age=31536000; includeSubDomains``.
 

	
 
**environment_indicator** (dictionary, optional, ``null``)
 
  Specify configuration for including environment indicator on all HTML
 
  pages. Indicator is a simple strip at bottom of a page with custom background
 
  colour, text colour, and text.
 

	
 
  Specifying environment indicator is useful for avoiding mistakes when testing
 
  by having better visibility what environment you are in
 
  (production/staging/test).
 

	
 
  The following keys need to be specified:
 

	
 
  **background_colour** (string, mandatory)
 
    Background colour to use for the strip at bottom. This should be value
 
    compatible with CSS ``background-color`` attribute.
 

	
 
  **text_colour** (string, mandatory
 
    Text colour to use for the strip at bottom. This should be value compatible
 
    with CSS ``color`` attribute.
 

	
 
  **text** (string, mandatory)
 
    Text to show in show in the strip at bottom.
 

	
 
**fqdn** (string, mandatory)
 
  Fully-qualified domain name where the website is reachable. This value is used
 
  for calculating the user/group name for dedicated website user, as well as
 
  home directory of the website user (where data/code should be stored at).
 

	
 
**index** (string, optional, ``index.php``)
 
  Space-separated list of files which should be treated as index files by the
 
  web server. The web server will attempt opening these index files, in
 
  succession, until the first match, or until it runs out of matches, when a
 
  client requests an URI pointing to directory.
 

	
 
**https_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_https.pem') }}``)
 
  X.509 certificate used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ fqdn }}_https.pem``.
 

	
 
**https_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' + fqdn + '_https.key') }}``)
 
  Private key used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ fqdn }}_https.key``.
 

	
 
**php_file_regex** (string, optional, ``\.php$``)
 
  Regular expression used for determining which file should be interepted via
 
  PHP.
 

	
 
**php_rewrite_urls** (list, optional, ``[]``)
 
  A list of rewrite rules that are applied to incoming requests. These rewrite
 
  rules are specifically targetted at prettying-up the URLs used by the PHP
 
  scripts. Each element of the list should be a string value compatible with the
 
  format of ``nginx`` option ``rewrite``. The keyword ``rewrite`` itself should
 
  be omitted, as well as trailing semi-colon (``;``).
 

	
 
**rewrites** (list, optional, ``[]``)
 
  A list of rewrite rules that are applied to incoming requests. Each element of
 
  the list should be a string value compatible with the format of ``nginx``
 
  option ``rewrite``. The keyword ``rewrite`` itself should be omitted, as well
 
  as trailing semi-colon (``;``).
 

	
 
**packages** (list, optional, ``[]``)
 
  A list of additional packages to install for this particular PHP
 
  appliction. This is usually going to be different PHP extensions.
 

	
 
**uid** (integer, optional, ``whatever OS picks``)
 
  UID/GID (they are set-up to be the same) of the dedicated website
 
  user/group.
 

	
 
**website_mail_recipients** (string, optional, ``root``)
 
  Space-separated list of e-mails or local users to which the mails, sent to
 
  either the website admin or website user, should be forwarded to. Forwarding
 
  is configured via ``~/.forward`` configuration file.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up two (base) PHP websites (for
 
running *ownCloud* and *The Bug Genie* applications):
 

	
 
.. code-block:: yaml
 

	
 
    - role: php_website
 
      fqdn: cloud.example.com
 
      uid: 2001
 
      php_file_regex: \.php($|/)
 
      rewrites:
 
        - ^/\.well-known/host-meta /public.php?service=host-meta
 
        - ^/\.well-known/host-meta\.json /public.php?service=host-meta-json
 
        - ^/\.well-known/carddav /remote.php/carddav/ redirect
 
        - ^/\.well-known/caldav /remote.php/caldav/ redirect
 
        - ^/apps/calendar/caldav\.php /remote.php/caldav/
 
        - ^/apps/contacts/carddav\.php /remote.php/carddav/
 
        - ^/remote/(.*) /remote.php
 
      deny_files_regex:
 
        - ^(\.|autotest|occ|issue|indie|db_|console|build/|tests/|config/|lib/|3rdparty/|templates/).*
 
      packages:
 
        # For ownCloud
 
        - php5-gd
 
        - php5-json
 
        - php5-mysql
 
        - php5-curl
 
      https_tls_key: "{{ lookup('file', inventory_dir + '/tls/cloud.example.com_https.key') }}"
 
      https_tls_certificate: "{{ lookup('file', inventory_dir + '/tls/cloud.example.com_https.pem') }}"
 
      additional_nginx_config:
 
        - comment: Use custom page for forbidden files.
 
          value: error_page 403 /core/templates/403.php;
 
        - comment: Use custom page for non-existing locations/files.
 
          value: error_page 404 /core/templates/404.php;
 
      additional_fpm_config:
 
        "env[PATH]": "\"/usr/local/bin:/usr/bin:/bin\""
 
      website_mail_recipients: "root john.doe@example.com"
 
      environment_indicator:
 
        background_colour: "green"
 
        text_colour: "black"
 
        text: "TEST ENVIRONMENT"
 
    - role: php_website
 
      deny_files_regex:
 
        - ^\..*
 
      php_rewrite_urls:
 
        - ^(.*) /index.php?url=$1
 
      fqdn: tbg.example.com
 
      uid: 2007
 
      https_tls_key: "{{ lookup('file', inventory_dir + '/tls/tbg.example.com_https.key') }}"
 
      https_tls_certificate: "{{ lookup('file', inventory_dir + '/tls/tbg.example.com_https.pem') }}"
 

	
 

	
 
WSGI Website
 
------------
 

	
 
The ``wsgi_website`` role can be used for setting-up a website powered by Python
 
on destination machine. The website needs to use the WSGI specification for
 
making the Python web application(s) available.
 

	
 
This role is normally not supposed to be used directly, but should instead serve
 
as the basis for writing website-specific roles. Therefore the role is written
 
in quite generic way, allowing the integrator to write his/her own logic for
 
deploying the necessary Python applications/packages, while still reusing a
 
common base and reducing the workload.
 

	
 
The role implements the following:
 

	
 
* Creates a dedicated user/group for running the WSGI application.
 
* Creates a dedicated administrator user for maintaining the website.
 
* Creates a base directory where the website-specific code and data should be
 
  stored at.
 
* Adds nginx to website's group, so nginx could read the necessary files.
 
* Adds website administrator to website's group, so administrator could manage
 
  the code and data.
 
* Installs additional packages required for running the role (as configured).
 
* Sets-up a dedicated Python virtual environment for website.
 
* Install ``futures`` package in Python virtual environment (required for
 
  Gunicorn in combination withg Python 2.7).
 
* Install Gunicorn in Python virtual environment.
 
* Installs additional packages required for running the role in Python virtual
 
  environment (as configured).
 
* Configures systemd to run the website code (using Gunicorn)
 
* Deploys the HTTPS TLS private key and certificate (for website vhost).
 
* Configures nginx to serve the website (static files served directly, requests
 
  passed on to Gunicorn).
 

	
 
The role is implemented with the following layout/logic in mind:
 

	
 
* Website users are named after the ``FQDN`` (fully qualified domain name) of
 
  website, in format of ``web-ESCAPEDFQDN``, where ``ESCAPEDFQDN`` is equal to
 
  ``FQDN`` where dots have been replaced by underscores (for example,
 
  ``web-wiki_example_com``).
 
* Website users are set-up via GECOS field to have their umask set to ``0007``
 
  (in combination with ``pam_umask``).
 
* Administrator users are named after the ``FQDN`` (fully qualified domain name)
 
  of website, in format of ``admin-ESCAPEDFQDN``, where ``ESCAPEDFQDN`` is equal
 
  to ``FQDN`` where dots have been replaced by underscores (for example,
 
  ``admin-cloud_example_com``).
 
* All websites reside within a dedicated sub-directory in ``/var/www``. The
 
  sub-directory name is equal to the ``FQDN`` used for accessing the
 
  website. Owner of the directory is set to be the application administrator,
 
  while group is set to be the website group. Additionally, ``SGID`` bit is set
 
  on the directory. This allows admin, with correct umask, to create necessary
 
  files and directories that should be readable (and eventually writeable) by
 
  the website user (running the WSGI application) without having to become root.
 
* All files placed in the website directory should be either created there
 
  directly, or copied to the directory in order to make sure the ``SGID`` gets
 
  honored. **Do not move the files, the permissions will not be set correctly.**
 
* Within the website directory, Python virtual environment can be found within
 
  the ``virtualenv`` sub-directory. Switching to administrator user via login
 
  shell will automatically activate the virtual environment.
 
* Within the website directory, nginx will expect to find the static files
 
  within the ``htdocs`` sub-directory (this can be symlink too). Locations/aliases
 
  can be configured for static file serving.
 
* Within the website directory, systemd service will expect to find the website
 
  code within the ``code`` sub-directory (this can be symlink too).
 
* nginx communicates with WSGI server over a dedicated Unix socket for each
 
  website.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **web_server**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**additional_nginx_config** (list, optional, ``[]``)
 
  List providing additional Nginx configuration options to include. This can be
 
  useful for specifying things like error pages. Options are applied inside of a
 
  **server** context of Nginx configuration file.
 

	
 
  Each item is a dictionary with the following options describing the extra
 
  configuration option:
 

	
 
  **comment** (string, mandatory)
 
    Comment describing the configuration option.
 

	
 
  **value** (string, mandatory)
 
    Configuration option.
 

	
 
**admin_uid** (integer, optional, ``whatever OS picks``)
 
  UID of the dedicated website administrator user. The user will be member of
 
  website group.
 

	
 
**enforce_https** (boolean, optional, ``True``)
 
  Specify if HTTPS should be enforced for the website or not. If enforced,
 
  clients connecting via plaintext will be redirected to HTTPS, and clients will
 
  be served with ``Strict-Transport-Security`` header with value of
 
  ``max-age=31536000; includeSubDomains``.
 

	
 
**environment_indicator** (dictionary, optional, ``null``)
 
  Specify configuration for including environment indicator on all HTML
 
  pages. Indicator is a simple strip at bottom of a page with custom background
 
  colour, text colour, and text.
 

	
 
  Specifying environment indicator is useful for avoiding mistakes when testing
 
  by having better visibility what environment you are in
 
  (production/staging/test).
 

	
 
  The following keys need to be specified:
 

	
 
  **background_colour** (string, mandatory)
 
    Background colour to use for the strip at bottom. This should be value
 
    compatible with CSS ``background-color`` attribute.
 

	
 
  **text_colour** (string, mandatory
 
    Text colour to use for the strip at bottom. This should be value compatible
 
    with CSS ``color`` attribute.
 

	
 
  **text** (string, mandatory)
 
    Text to show in show in the strip at bottom.
 

	
 
**environment_variables** (dict, optional, ``{}``)
 
  Specify additional environment variables that should be set for running the
 
  service. Environment variables will be set in both the systemd service and for
 
  the application's administrator user (when logged in as one).
 

	
 
**fqdn** (string, mandatory)
 
  Fully-qualified domain name where the website is reachable. This value is used
 
  for calculating the user/group name for dedicated website user, as well as
 
  home directory of the website user (where data/code should be stored at).
 

	
 
**futures_version** (string, optional, ``3.0.5``)
 
  Version of ``futures`` package to deploy in virtual environment. Required by
 
  Gunicorn when using Python 2.7. Default version is tested with the test site.
 

	
 
**gunicorn_version** (string, optional, ``19.6.0``)
 
  Version of Gunicorn to deploy in virtual environment for running the WSGI
 
  application. Default version is tested with the test site.
 

	
 
**https_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_https.pem') }}``)
 
  X.509 certificate used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ fqdn }}_https.pem``.
 

	
 
**https_tls_key** (string, optional, ``{{ lookup('file', tls_private_key_dir + '/' + fqdn + '_https.key') }}``)
 
  Private key used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ fqdn }}_https.key``.
 

	
 
**packages** (list, optional, ``[]``)
 
  A list of additional packages to install for this particular WSGI
 
  website. This is usually going to be development libraries for building Python
 
  packages.
 

	
 
**proxy_headers** (dictionary, optional, ``{}``)
 
  Additional headers to set when proxying request to Gunicorn. Keys are header
 
  names, values are header values. Both should be compatible with Nginx
 
  ``proxy_set_header``. If you need to provide an empty value, use quotes (don't
 
  forget to surround them by another set of quotes for YAML syntax, for example
 
  ``"\"\""`` or ``'""'``).
 

	
 
**rewrites** (list, optional, ``[]``)
 
  A list of rewrite rules that are applied to incoming requests. Each element of
 
  the list should be a string value compatible with the format of ``nginx``
 
  option ``rewrite``. The keyword ``rewrite`` itself should be omitted, as well
 
  as trailing semi-colon (``;``).
 

	
 
**static_locations** (list, optional, ``[]``)
 
  List of locations that should be treated as static-only, and not processed by
 
  the WSGI application at all. This is normally used for designating serving of
 
  static/media files by Nginx (for example, in case of Django projects for
 
  ``/static/`` and ``/media/``).
 

	
 
**uid** (integer, optional, ``whatever OS picks``)
 
  UID/GID (they are set-up to be the same) of the dedicated website
 
  user/group.
 

	
 
**use_paste** (boolean, optional, ``False``)
 
  Tell Gunicorn to assume that the passed-in ``wsgi_application`` value is a
 
  filename of a Python Paste ``ini`` file instead of WSGI application.
 

	
 
**virtuaelnv_packages** (list, optional, ``[]``)
 
  A list of additional packages to install for this particular WSGI appliction
 
  in its virtual environment using ``pip``.
 

	
 
**website_mail_recipients** (string, optional, ``root``)
 
  Space-separated list of e-mails or local users to which the mails, sent to
 
  either the website admin or website user, should be forwarded to. Forwarding
 
  is configured via ``~/.forward`` configuration file.
 

	
 
**wsgi_application** (string, mandatory)
 
  WSGI application that should be started by Gunicorn. The format should be
 
  conformant to what the ``gunicorn`` command-line tool accepts. If the
 
  ``use_paste`` option is enabled, the value should be equal to filename of the
 
  Python Paste ini file, located in the ``code`` sub-directory. It should be
 
  noted that in either case the value should be specsified relative to the
 
  ``code`` sub-directory. I.e. don't use full paths.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up a (base) WSGI website (for
 
running a bare Django project):
 

	
 
.. code-block:: yaml
 

	
 
    - role: wsgi_website
 
      fqdn: django.example.com
 
      static_locations:
 
        - /static
 
        - /media
 
      uid: 2004
 
      virtualenv_packages:
 
        - django
 
      wsgi_application: django_example_com.wsgi:application
 
      environment_variables:
 
        DJANGO_SETTINGS_MODULE: "django_example_com.settings.production"
 
      https_tls_key: "{{ lookup('file', inventory_dir + '/tls/wsgi.example.com_https.key') }}"
 
      https_tls_certificate: "{{ lookup('file', inventory_dir + '/tls/wsgi.example.com_https.pem') }}"
 
      futures_version: 3.0.5
 
      gunicorn_version: 19.6.0
 
      additional_nginx_config:
 
        - comment: Use custom page for forbidden files.
 
          value: error_page 403 /static/403.html;
 
        - comment: Use custom page for non-existing locations/files.
 
          value: error_page 404 /static/404.html;
 
      website_mail_recipients: "root john.doe@example.com"
 
      environment_indicator:
 
        background_colour: "green"
 
        text_colour: "black"
 
        text: "TEST ENVIRONMENT"
 
      proxy_headers:
 
        Accept-Encoding: '""'
 

	
 

	
 
Database Server
 
---------------
 

	
 
The ``database_server`` role can be used for setting-up a MariaDB database
 
server on destination machine.
 

	
 
The role implements the following:
 

	
 
* Installs MariaDB server and client.
 
* Configures MariaDB server and client to use *UTF-8* encoding by default.
 
* Sets password for the database root user.
 
* Deploys MariaDB client configuration in location ``/root/.my.cnf`` that
 
  contains username and password for the root database user.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**db_root_password** (string, mandatory)
 
  Password for the *root* database user.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up the database server:
 

	
 
.. code-block:: yaml
 

	
 
   ---
 

	
 
   db_root_password: root
 

	
 

	
 
Database
 
--------
 

	
 
The ``database`` role can be used for creating a MariaDB database and
 
accompanying user on destination machine.
 

	
 
The role implements the following:
 

	
 
* Creates MariaDB database.
 
* Creates a dedicated user capable of performing any operation on the created
 
  database. Username is set to be same as the name of database.
 
* Sets-up pre-backup task that creates database dump in location
 
  ``/srv/backup/mariadb/{{ db_name }}.sql``.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **database_server**
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/srv/backup/maraidb/{{ db_name }}.sql**
 
  Dump of the database. Database dump is created every day at 01:45 in the
 
  morning.
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**db_name** (string, mandatory)
 
  Name of the database that should be created.
 

	
 
**db_password** (string, mandatory)
 
  Password for the database user.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for creating a single database (for some
 
website):
 

	
 
.. code-block:: yaml
 

	
 
  - role: database
 
    db_name: phpinfo_example_com
 
    db_password: phpinfo_example_com
 

	
 

	
 
Backup Server
 
-------------
 

	
 
The ``backup_server`` role can be used for setting-up a server to act as backup
 
storage for the backup clients. Storage is made available to the clients
 
exclusively via SFTP on a dedicated port and dedicated OpenSSH server
 
instance. This instance is specifically configured and tailored for this
 
purpose.
 

	
 
The role is primarily aimed for use with `Duplicity
 
<http://duplicity.nongnu.org/>`_, but should be also usable for generic SFTP
 
uploads.
 

	
 
The role implements the following:
 

	
 
* Installs backup software (Duplicity, Duply).
 
* Creates a dedicated directory structure for backups with the following structure:
 

	
 
  * ``/srv/backups/`` - main directory under which all the backups reside.
 
  * ``/srv/backups/SERVER_NAME/`` - home directory for the backup user, name
 
    after the server. Backup users are confined to their respective home
 
    directory via chroot. Backup users can't write to their own home directory,
 
    though.
 
  * ``/srv/backups/SERVER_NAME/duplicity/`` - directory where the Duplicity
 
    backups are stored at. This directory is writable by the respective backup
 
    user.
 
  * ``SERVER_NAME/.ssh/`` - directory where authorized keys are stored. Backup
 
    user is not allowed to make modifications to this directory and files
 
    contained within (i.e. backup users can't add more keys to the
 
    ``authorized_keys`` file).
 
* Creates dedicated operating system users for backup clients. These users will
 
  be made members of the ``backup`` group as well (as an additional group).
 
* Sets-up ``authorized_keys`` for the backup clients.
 
* Makes sure the backup users can't log-in via regular OpenSSH server instance.
 
* Sets-up dedicated OpenSSH server instances to be used exclusively by backup
 
  clients. The instance listens on TCP port ``2222``.
 
* Updates firewall to allow incoming TCP connections to port
 
  ``2222``. Connections are allowed only from the configured IP addresses
 
  associated with backup clients.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**backup_clients** (list, optional)
 
  List of backup clients that are connecting to the backup server. This is
 
  usually done on a per-server basis. Each item in the list is a dictionary
 
  describing the backup client. The following keys are available:
 

	
 
  **server** (string, mandatory)
 
    Name of the server that is backed up. It is highly recommended to use
 
    server's FQDN for this purpose. The dedicated operating system user created
 
    will have the name of format ``bak-ESCAPED_SERVER_NAME``, where
 
    ``ESCAPED_SERVER_NAME`` is calculated by taking the passed-in server name
 
    and replacing all dots (``.``) with undescores (``_``). For example,
 
    ``web.example.com`` will be turned into ``bak-web_example_com``.
 

	
 
  **uid** (integer, optional, ``whatever OS picks``)
 
    Uid for the operating system user. User's default group will have a GID
 
    identical to the user's UID if specified. Otherwise user's default group
 
    will have OS-determined GID.
 

	
 
  **ip** (IPv4 address, mandatory)
 
    IPv4 address from which the backup client server is connecting to the backup
 
    server. Used for introducing stricter firewall rules.
 

	
 
  **public_key** (string, mandatory)
 
    SSH public key used by backup client to connect to the backup server.
 

	
 
**backup_host_ssh_private_keys** (dictionary, mandatory)
 
  Defines host keys used for the dedicated OpenSSH server instance for
 
  backup. Key values that must be provided are: **dsa**, **rsa**, **ed25519**,
 
  and **ecdsa**, with values for each one of them corresponding to a private key
 
  generated using the appropriate algorithm. Keys for this purpose can be easily
 
  created via commands::
 

	
 
    ssh-keygen -f backup_server_dsa_key -N '' -t dsa
 
    ssh-keygen -f backup_server_rsa_key -N '' -t rsa
 
    ssh-keygen -f backup_server_ed25519_key -N '' -t ed25519
 
    ssh-keygen -f backup_server_ecdsa_key -N '' -t ecdsa
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up the backup server role:
 

	
 
.. code-block:: yaml
 

	
 
  - role: backup_server
 
    backup_clients:
 
      - server: web.example.com
 
        uid: 3000
 
        public_key: "{{ lookup('file', inventory_dir + '/ssh/web.example.com.pub') }}"
 
        ip: 10.32.64.18
 
      - server: mail.example.com
 
        public_key: "{{ lookup('file', inventory_dir + '/ssh/mail.example.com.pub') }}"
 
        ip: 10.32.64.15
 
    backup_host_ssh_private_keys:
 
      dsa: "{{ lookup('file', inventory_dir + '/ssh/backup_server_dsa_key') }}"
 
      rsa: "{{ lookup('file', inventory_dir + '/ssh/backup_server_rsa_key') }}"
 
      ed25519: "{{ lookup('file', inventory_dir + '/ssh/backup_server_ed25519_key') }}"
 
      ecdsa: "{{ lookup('file', inventory_dir + '/ssh/backup_server_ecdsa_key') }}"
 

	
 

	
 
Backup Client
 
-------------
 

	
 
The ``backup_client`` role can be used for setting-up the server as a backup
 
client so it can perform backups to the backup server.
 

	
 
Backup clients utilise duplicity (via the duply convenience wrapper) for
 
performing the backups to a backup server via *SFTP* protocol.
 

	
 
The role itself will take care of deploying the necessary software,
 
configuration files, and encryption/signing private key to the backup client in
 
order to be able to perform backup.
 

	
 
Files that should be backed-up are specified using the ``backup`` role.
 

	
 
The role implements the following:
 

	
 
* Installs backup software (Duplicity, Duply).
 
* Sets-up Duply configuration under directory ``/etc/duply/main/``.
 
* Deploys encryption/signing private key (usually host-specific), as well as
 
  additional encryption public keys to the server, and imports them into local
 
  GnuPG keyring used by backup software.
 
* Deploys private SSH key for logging-in into the backup server over SFTP.
 
* Deploys ``known_hosts`` file for SFTP fingerprint verification.
 
* Sets-up a handler that runs scripts/binaries before the actual backup
 
  run. This is helpful for producing database backups. Such scripts/binaries
 
  should be deployed to directory ``/etc/duply/main/pre.d/``, and marked
 
  executable by the root user.
 

	
 
Duply is configured as follows:
 

	
 
* GnuPG keyring is stored under ``/etc/duply/main/gnupg/``. The keyring should
 
  not be managed manually.
 
* SSH private key for logging-in into backup server is stored in location
 
  ``/etc/duply/main/ssh/identity``. Backup server SFTP fingerprint is stored in
 
  location ``/etc/duply/main/ssh/known_hosts``.
 
* Base directory for back-ups is root (``/``), but *all* files are excluded by
 
  default to prevent huge back-ups. Ansible roles that want to utilise the
 
  backup client role can specify which patterns should be included in the backup
 
  when including the ``backup`` role. Include pattern file is assembled and
 
  stored in location ``/etc/duply/main/include``.
 
* Backups are encrypted and signed with the specified encryption key.
 
* Maximum age for old backups is set to 6 months.
 
* Maximum age for full backups is set to 1 month.
 
* Volume size is set to 1GB.
 
* Pre-backup scripts are run via ``/etc/duply/main/pre`` handler that tries to
 
  execute scripts/binaries from directory ``/etc/duply/main/pre.d/``.
 

	
 
.. note::
 
   Since at time of this writing there are no lookup plugins for extracting key
 
   material/information from GnuPG keyring, you may want to resort to extraction
 
   of keys on the controller machine via lookups similar to::
 

	
 
     lookup('pipe', 'gpg2 --homedir /path/to/your/keyring --armor --export some_identifier')
 
     lookup('pipe', 'gpg2 --homedir /path/to/your/keyring --armor --export-secret-keys some_identifier')
 

	
 
   This may not be the most elegant solution, but for now it offers better
 
   flexibility (theoretically, you could store all those keys etc as plaintext
 
   files instead).
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**backup_additional_encryption_keys** (list, optional, ``[]``)
 
  List of additional public encryption keys used for backup operation. Each item
 
  in the list should be an ASCII armour-encoded public key exported from a GnuPG
 
  keyring. These additional public keys are useful in cases where the backups
 
  should be decryptable with some master key in addition to server-specific key.
 

	
 
**backup_client_username** (string, optional, ``bak-{{ ansible_fqdn | replace('.', '_') }}``)
 
  Username for connecting to the backup server via SFTP.
 

	
 
**backup_encryption_key** (string, mandatory)
 
  Private GnuPG key, encoded using ASCII armor, used for encryption and signing
 
  operations when running the backup on the client server. This *must* be a
 
  private key! This is normally host-specific encryption key that is distributed
 
  to destination server and that can be also used for the restore operations
 
  (for data decryption). The key must not be password-protected.
 

	
 
**backup_server** (string, mandatory)
 
  Backup server to connect to.
 

	
 
**backup_server_destination** (string, optional, ``//duplicity``)
 
  Target directory on the backup server where the backups are stored.
 

	
 
**backup_server_host_ssh_public_keys** (list, mandatory)
 
  SSH public keys presented by the server during client authentication. These
 
  public keys are used for populating the known hosts on the backup client side
 
  for host verification purposes.
 

	
 
**backup_server_port** (int, optional, ``2222``)
 
  Port on the backup server to connect to for accessing the SFTP service.
 

	
 
**backup_ssh_key** (string, mandatory)
 
  SSH private key for logging-in into the backup server.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up the role (take note that lookup
 
plugin is quite useful here for fetching key values from some local directory):
 

	
 
.. code-block:: yaml
 

	
 
  - role: backup_client
 
    backup_additional_encryption_keys: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----"
 
    backup_client_username: "user"
 
    backup_encryption_key: "-----BEGIN PGP PRIVATE KEY BLOCK-----\n...\n-----END PGP PRIVATE KEY BLOCK-----"
 
    backup_server: "backup.example.com"
 
    backup_server_destination: "//example/host"
 
    backup_server_host_ssh_public_keys:
 
      - "{{ lookup('file', inventory_dir + '/ssh/backup_server_dsa_key.pub') }}"
 
      - "{{ lookup('file', inventory_dir + '/ssh/backup_server_ecdsa_key.pub') }}"
 
      - "{{ lookup('file', inventory_dir + '/ssh/backup_server_ed25519_key.pub') }}"
 
      - "{{ lookup('file', inventory_dir + '/ssh/backup_server_rsa_key.pub') }}"
 
    backup_server_port: 22
 
    backup_ssh_key: "{{ lookup('file', inventory_dir + '/ssh/web.example.com') }}"
 

	
 

	
 
Backup
 
------
 

	
 
The ``backup`` role can be used to specify what files should be backed-up to the
 
backup server.
 

	
 
The role provides a convenient way to deploy a file containing file and
 
directory patterns describing the file/directory paths that should be included
 
in the back-up.
 

	
 
The role implements the following:
 

	
 
* Installs a file with provided patterns in directory
 
  ``/etc/duply/main/patterns/``.
 
* Assembles/refresshes the main include pattern file at
 
  ``/etc/duply/main/include``.
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **backup_client**
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**backup_patterns_filename** (string, mandatory)
 
  Name of the backup patterns file. The file is stored in directory
 
  ``/etc/duply/main/patterns/``. This should be a unique filename amongst all
 
  roles. If role can be included multiple times, make sure the filename is
 
  always unique when depending on the backup role.
 

	
 
**backup_patterns** (list, optional, ``[]``)
 
  List of globbing patterns defining which files or directories should be
 
  backed-up.
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up the role:
 

	
 
.. code-block:: yaml
 

	
 
  - role: backup
 
    backup_patterns_filename: myapp
 
    backup_patters:
 
      - /var/www/myapp.example.com
docs/usage.rst
Show inline comments
 
.. _usage:
 

	
 
Usage
 
=====
 

	
 
Majic Ansible Roles are targeted at sysadmins who wish to deploy services for
 
their own, small-scale use. This chapter gives a simple tutorial-like set of
 
instructions for using all of the roles available.
 

	
 
.. contents:: :local:
 

	
 

	
 
Overview
 
--------
 

	
 
There is a number of different roles that can prove useful for setting-up a
 
small infrastructure of your own.
 

	
 
Some roles are suited for one-off operations during installation, like the
 
``preseed`` and ``bootstrap``, while some are better suited for periodic runs
 
for maintaining the users and integrity of the system.
 

	
 
By the end of the instructions you will have the following:
 

	
 
* Ansible server, used for configuring the remaining servers.
 
* Communications server, providing the LDAP, mail, and XMPP services.
 
* Web server, providing the web services.
 
* Backup server, where the backups will be stored at.
 

	
 

	
 
Pre-requisites
 
--------------
 

	
 
For the set-up outlined in this usage guide you'll need the following:
 

	
 
* One server where Ansible will be installed at. Debian Jessie will be installed
 
  on top of this server. The server will be set-up manually (this is currently
 
  out of scope for the *Majic Ansible Roles* automated set-up).
 
* Three servers where the services will be set-up. All servers must be able to
 
  communicate over network with each-other, the Ansible servers, and with
 
  Internet. Debian Jessie will be installed on top of this server as part of the
 
  usage instructions.
 
* Debian Jessie network install CD.
 
* All servers should be on the same network.
 
* IP addresses for all servers should be known.
 
* Netmask for all servers should be known.
 
* Gateway for all servers should be known.
 

	
 
In case of the servers listed above, it might be safest to have them as VMs -
 
this is cheapest thing to do, and simplest (who wants to deal with pesky hardware anyway?).
 

	
 
Usage instructions assume the following:
 

	
 
* Domain used for all servers is ``example.com``. If you wish to use a different
 
  domain, adjust the instructions accordingly.
 
* Server hostnames are ``ansible``, ``comms``, ``www``, and ``bak`` (for Ansible
 
  server, communications server, web server, and backup server, respectively).
 

	
 

	
 
Installing the OS on Ansible server
 
-----------------------------------
 

	
 
Start-off by installing the operating system on the Ansible server:
 

	
 
1. Fire-up the ``ansible`` server, and boot from the network installation CD.
 

	
 
2. Select the **Install** option.
 

	
 
3. Pick **English** as language.
 

	
 
4. Pick the country you are living in (or whatever else you want).
 

	
 
5. Pick the **en_US.UTF-8** locale.
 

	
 
6. Pick the **American English** keymap.
 

	
 
7. Configure the network if necessary.
 

	
 
8. Set the hostname to ``ansible``.
 

	
 
9. Set the domain to ``example.com``.
 

	
 
10. Set the root password.
 

	
 
11. Create a new user. For simplicity, call the user **Ansible user**, with
 
    username **ansible**.
 

	
 
12. Set-up partitioning in any way you want. You can go for **Guided - use
 
    entire disk** if you want to keep it simple and are just testing things.
 

	
 
13. Wait until the base system has been installed.
 

	
 
14. Pick whatever Debian archive mirror is closest to you.
 

	
 
15. If you have an HTTP proxy, provide its URL.
 

	
 
16. Pick if you want to participate in package survey or not.
 

	
 
17. Make sure that at least the **standard system utilities** and **SSH server**
 
    options are selected on task selection screen.
 

	
 
18. Wait for packages to be installed.
 

	
 
19. Install the GRUB boot loader on MBR.
 

	
 
20. Finalise the server install, and remove the installation media from server.
 

	
 

	
 
Installing required packages
 
----------------------------
 

	
 
With the operating system installed, it is necessary to install a couple of
 
packages, and to prepare the environment a bit on the Ansible server:
 

	
 
1. Install the necessary system packages (using the ``root`` account)::
 

	
 
     apt-get install -y virtualenv virtualenvwrapper git python-pip python-dev libffi-dev libssl-dev
 

	
 

	
 
2. Set-up the virtual environment (using the ``ansible`` account):
 

	
 
   .. warning::
 
      If you are already logged-in as user ``ansible`` in the server, you will
 
      need to log-out and log-in again in order to be able to use
 
      ``virtualenvwrapper`` commands!
 

	
 
   ::
 

	
 
     mkdir ~/mysite/
 
     mkvirtualenv -a ~/mysite/ mysite
 
     pip install -U pip setuptools
 
     pip install 'ansible~=1.9.0'
 

	
 

	
 
Cloning the *Majic Ansible Roles*
 
---------------------------------
 

	
 
With most of the software pieces in place, the only missing thing is the Majic
 
Ansible Roles:
 

	
 
1. Clone the git repository::
 

	
 
     git clone http://code.majic.rs/majic-ansible-roles ~/majic-ansible-roles
 

	
 
2. Checkout the correct version of the roles::
 

	
 
     cd ~/majic-ansible-roles/
 
     git checkout -b 1.1.0 1.1.0
 

	
 

	
 
Preparing the basic site configuration
 
--------------------------------------
 

	
 
Phew... Now that was a bit tedious and boring... But at least you are now ready
 
to set-up your own site :)
 

	
 
First of all, let's set-up some basic directory structure and configuration:
 

	
 
1. Create Ansible configuration file.
 

	
 
   :file:`~/mysite/ansible.cfg`
 

	
 
   ::
 

	
 
     [defaults]
 

	
 
     roles_path=/home/ansible/majic-ansible-roles/roles:/home/ansible/mysite/roles
 
     force_handlers = True
 
     retry_files_save_path = /home/ansible/mysite/retry
 
     inventory = /home/ansible/mysite/hosts
 

	
 
2. Create directory where retry files will be stored at (so they woudln't
 
   pollute your home directory)::
 

	
 
     mkdir ~/mysite/retry
 

	
 
3. Create the hosts file.
 

	
 
   :file:`~/mysite/hosts`
 

	
 
   ::
 

	
 
     [preseed]
 
     localhost ansible_connection=local
 

	
 
     [communications]
 
     comms.example.com
 

	
 
     [web]
 
     www.example.com
 

	
 
     [backup]
 
     bak.example.com
 

	
 
4. Create a number of directories for storing playbooks, group variables, SSH
 
   keys, and GnuPG keyring (we'll get to this later)::
 

	
 
     mkdir ~/mysite/playbooks/
 
     mkdir ~/mysite/group_vars/
 
     mkdir ~/mysite/ssh/
 
     mkdir ~/mysite/gnupg/
 

	
 
5. Before moving ahead, we should also create SSH private/public key pair that
 
   will be used by Ansible for connecting to destination servers, as well as
 
   for some roles::
 

	
 
     ssh-keygen -f ~/.ssh/id_rsa -N ''
 

	
 

	
 
Preseed files
 
-------------
 

	
 
The ``preseed`` role is useful for generating Debian preseed files. Preseed
 
files can be used for automating the Debian installation process.
 

	
 
Preseed files are commonly created on the Ansible host, and then in some way
 
served to the servers using them during install.
 

	
 
So, let's set this up for start:
 

	
 
1. First of all, create the playbook for generating the preseed files locally.
 

	
 
   :file:`~/mysite/playbooks/preseed.yml`
 

	
 
   ::
 

	
 
      ---
 
      - hosts: preseed
 
        roles:
 
          - preseed
 

	
 
2. And that is about it to be able to actually use this particular role! So
 
   let's try running it::
 

	
 
     workon mysite && ansible-playbook playbooks/preseed.yml
 

	
 
3. If all went well, you should have the following files created:
 

	
 
   * :file:`~/mysite/preseed_files/comms.example.com.cfg`
 
   * :file:`~/mysite/preseed_files/www.example.com.cfg`
 
   * :file:`~/mysite/preseed_files/bak.example.com.cfg`
 

	
 
4. You can have a look at them, but you might notice the settings in the file
 
   might not be to your liking. In particular, it could be using wrong timezone,
 
   defaulting to DHCP for network configuration etc. Let's concentrate on making
 
   the network configuration changes - this is the main thing that will probably
 
   differ in your environment. Create a new configuration file:
 

	
 
   :file:`~/mysite/group_vars/preseed.yml`
 

	
 
   ::
 

	
 
      ---
 

	
 
      # Set your default (initial) root password.
 
      preseed_root_password: changeit
 
      # Use manual network configuration (no DHCP).
 
      preseed_network_auto: no
 
      # Set the gateway for all servers.
 
      preseed_gateway: 10.32.64.1
 
      # Set the netmask for all servers.
 
      preseed_netmask: 255.255.255.0
 
      # Set the DNS for all servers.
 
      preseed_dns: 10.32.64.1
 
      # Set the domain for all servers.
 
      preseed_domain: example.com
 
      # Set the server-specific options.
 
      preseed_server_overrides:
 
        comms.example.com:
 
          hostname: comms
 
          ip: 10.32.64.19
 
        www.example.com:
 
          hostname: www
 
          ip: 10.32.64.20
 
        bak.example.com:
 
          hostname: bak
 
          ip: 10.32.64.23
 

	
 
5. Now re-run the preseed playbook::
 

	
 
     workon mysite && ansible-playbook playbooks/preseed.yml
 

	
 
6. The preseed files should have been updated now, and you should have the new
 
   customised configuration files in the ``preseed_files`` directory. You can
 
   now use these to install the servers.
 

	
 

	
 
Installing the servers with preseed files
 
-----------------------------------------
 

	
 
You have your preseed files now, so you can go ahead and install the servers
 
``comms.example.com``, ``www.example.com``, and ``bak.example.com`` using
 
them with network install CD. Have a look at `Debian
 
<https://www.debian.org/releases/stable/amd64/apbs02.html.en>`_ instructions for
 
more details.
 

	
 
If you need to, you can easily serve the preseed files from the Ansible server
 
with Python's built-in HTTP server::
 

	
 
  cd ~/mysite/preseed_files/
 
  python -m SimpleHTTPServer 8000
 

	
 

	
 
Bootstrapping servers for Ansible set-up
 
----------------------------------------
 

	
 
In order to effectively use Ansible, a small initial bootstrap always has to be
 
done for managed servers. This mainly involves set-up of Ansible users on the
 
destination machine, and distributing the SSH public keys for authorisation.
 

	
 
When you use the preseed configuration files to deploy a server, you get the
 
benefit of having the authorized_keys set-up for the root operating system user,
 
making it easier to bootstrap the machines subsequently via Ansible.
 

	
 
Let's bootstrap our machines now:
 

	
 
1. For start, create a dedicated playbook for the bootstrap process.
 

	
 
   :file:`~/mysite/playbooks/bootstrap.yml`
 

	
 
   ::
 

	
 
      ---
 

	
 
      - hosts: [communications, web, backup]
 
        remote_user: root
 
        roles:
 
          - bootstrap
 

	
 
2. The ``bootstrap`` role has only one parameter - an SSH key which should be
 
   deployed for the Ansible user on managed server (in the ``authorized_keys``
 
   file). This defaults to content of local file ``~/.ssh/id_rsa.pub``, so no
 
   need to make any changes so far.
 

	
 
3. SSH into all machines at least once from the Ansible server in order to
 
   store the SSH fingerprints into known hosts file::
 

	
 
     ssh root@comms.example.com date
 
     ssh root@www.example.com date
 
     ssh root@bak.example.com date
 

	
 
4. Now, simply run the bootstrap role against the servers::
 

	
 
     workon mysite && ansible-playbook playbooks/bootstrap.yml
 

	
 
6. At this point you won't be able to ssh into the machines with root account
 
   anymore. You will be able to ssh into the machines using the private key of
 
   the ``ansible`` user (from the Ansible server). The ``ansible`` user will
 
   also be granted ability to run the ``sudo`` commands without providing
 
   password.
 

	
 
7. After this you can finally move on to configuring what you really want -
 
   common configuration and services for your site.
 

	
 

	
 
Common server configuration
 
---------------------------
 

	
 
Each server needs to share some common configuration in order to be functioning
 
properly. This includes set-up of some shared accounts, perhaps some hardening
 
etc.
 

	
 
Let's take care of this common configuration right away:
 

	
 
1. Create playbook for the communications server:
 

	
 
   :file:`~/mysite/playbooks/communications.yml`
 
   ::
 

	
 
      ---
 
      - hosts: communications
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 

	
 
2. Create playbook for the web server:
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 

	
 
3. Create playbook for the backup server:
 

	
 
   :file:`~/mysite/playbooks/backup.yml`
 
   ::
 

	
 
      ---
 
      - hosts: backup
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 

	
 
4. Create the global site playbook:
 

	
 
   :file:`~/mysite/playbooks/site.yml`
 
   ::
 

	
 
      ---
 
      - include: preseed.yml
 
      - include: communications.yml
 
      - include: web.yml
 
      - include: backup.yml
 

	
 
5. Time to create configuration for the role. Since this role is supposed to
 
   set-up a common base, we'll set-up the variables file that applies to all
 
   roles:
 

	
 
   :file:`~/mysite/group_vars/all.yml`
 
   ::
 

	
 
      ---
 

	
 
      os_users:
 
        - name: admin
 
          uid: 1000
 
          additional_groups:
 
            - sudo
 
          authorized_keys:
 
            - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
 
          password: "{{ 'admin' | password_hash('sha512') }}"
 

	
 
      common_packages:
 
        - emacs24-nox
 

	
 
6. That's all for configuration, time to apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
7. After this you should be able to *ssh* from Ansible server onto the managed
 
   servers as user ``admin`` using the *SSH* private key. The ``admin`` user's
 
   password has also been set to ``admin``, and the user will be member of
 
   ``sudo`` group.
 

	
 

	
 
Introducing LDAP
 
----------------
 

	
 
Since some of the services actually depend on LDAP, we'll go ahead and set that
 
one up first. This includes both the LDAP *server* and *client* configuration.
 

	
 
1. Update the playbook for communications server to include the LDAP client and
 
   server roles (``ldap_client`` and ``ldap_server``, respectively).
 

	
 
   :file:`~/mysite/playbooks/communications.yml`
 
   ::
 

	
 
      ---
 
      - hosts: communications
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - ldap_server
 

	
 
2. Update the playbook for web server to include the LDAP client role
 
   (``ldap_client``). You never know when it might come in handy :)
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 

	
 
3. Time to configure the roles. For start, let us configure the LDAP server
 
   role. Keep in mind that there is a lot of default variables set-up by the
 
   role itself, making our config rather short.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      ---
 

	
 
      ldap_admin_password: admin
 
      ldap_server_organization: "Example Inc."
 

	
 
4. Phew. That was... Well, actually, easy :) Technically, only the LDAP admin
 
   password *must* be set, but it's nice to have better organisation specified
 
   than the default one. Let's add the LDAP client configuration next. We will
 
   start off with global LDAP client configuration. In case of LDAP client,
 
   we've got to be a bit more explicit.
 

	
 
   :file:`~/mysite/group_vars/all.yml`
 
   ::
 

	
 
      # Observe how we set the base DN. By default the ldap_server role
 
      # (defined up there) will use server's domain to form the base for LDAP.
 
      ldap_client_config:
 
        - comment: Set the base DN
 
          option: BASE
 
          value: dc=example,dc=com
 
        - comment: Set the default URI
 
          option: URI
 
          value: ldap://comms.example.com/
 
        - comment: Set the LDAP TLS truststore
 
          option: TLS_CACERT
 
          value: /etc/ssl/certs/truststore.pem
 
        - comment: Enforce TLS
 
          option: TLS_REQCERT
 
          value: demand
 

	
 
5. Ok, so this looks nice and dandy... But, let's have a bit better
 
   configuration on the communications server itself. Namely, on that one we
 
   should be able to connect to socket with LDAP clients instead over TCP port.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      ldap_client_config:
 
        - comment: Set the base DN
 
          option: BASE
 
          value: dc=example,dc=com
 
        - comment: Set the default URI
 
          option: URI
 
          value: ldapi:///
 
        - comment: Set the default bind DN, useful for administration.
 
          option: BINDDN
 
          value: cn=admin,dc=example,dc=com
 
        - comment: Set the LDAP TLS truststore
 
          option: TLS_CACERT
 
          value: /etc/ssl/certs/truststore.pem
 
        - comment: Enforce TLS
 
          option: TLS_REQCERT
 
          value: demand
 

	
 
6. Ok, time to re-run the playbooks again... Wait a minute, something is missing
 
   here... Oh, right, forgot to mention one thing - Majic Ansible Roles use TLS
 
   throughout wherever possible. In other words, you *must* have TLS private
 
   keys and certificates issued by some CA for all servers in order to be able
 
   to use most of the roles (actually, you need them issued per *service*). This
 
   includes ``ldap_server`` too. So, let's make a slight detour to create a CA
 
   of our own, plus the necessary server certificate for the LDAP service...
 

	
 
   1. Let's first install a couple of more tools on the Ansible server, since we
 
      will be using ``certtool`` for our improvised CA needs (run this as
 
      ``root``)::
 

	
 
        apt-get install -y gnutls-bin
 

	
 
   2. Create directory where the private keys and certificates will be stored
 
      at (you can switch back to the ``ansible`` user now)::
 

	
 
        mkdir ~/mysite/tls/
 

	
 
   3. It is time to create a couple of templates for the ``certtool`` so it
 
      would know what extensions and content to have in the certificates:
 

	
 
      :file:`~/mysite/tls/ca.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = "SE"
 
         cn = "Example Inc. Test Site CA"
 
         expiration_days = 1825
 
         ca
 
         cert_signing_key
 
         crl_signing_key
 

	
 
      :file:`~/mysite/tls/comms.example.com_ldap.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. LDAP Server"
 
         expiration_days = 365
 
         dns_name = "comms.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   4. Almost there... Now let's generate the keys and certificates::
 

	
 
        certtool --sec-param high --generate-privkey --outfile ~/mysite/tls/ca.key
 
        certtool --template ~/mysite/tls/ca.cfg --generate-self-signed --load-privkey ~/mysite/tls/ca.key --outfile ~/mysite/tls/ca.pem
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/comms.example.com_ldap.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/comms.example.com_ldap.cfg --load-privkey ~/mysite/tls/comms.example.com_ldap.key --outfile ~/mysite/tls/comms.example.com_ldap.pem
 

	
 
   5. And just one more small tweak - we need to provide a truststore PEM file
 
      containing all CA certificates in the chain. In this particular case we
 
      have a super-simple hierarchy (root CA is also issuing the end entity
 
      certificates), so simply make a copy of the ``ca.pem``. The
 
      ``truststore.pem`` file is picked-up automatically by many other roles::
 

	
 
        cp ~/mysite/tls/ca.pem ~/mysite/tls/truststore.pem
 

	
 
7. With private keys and certificates in place, we are almost ready to re-run
 
   the playbooks! Now, just a *small* tweak to the general configuration to set
 
   where the TLS private keys and certificates can be found at, and all should
 
   be fine.
 

	
 
   :file:`~/mysite/group_vars/all.yml`
 
   ::
 

	
 
      tls_private_key_dir: "~/mysite/tls/"
 
      tls_certificate_dir: "~/mysite/tls/"
 
      ca_certificates:
 
         "truststore": "{{ lookup('file', '~/mysite/tls/truststore.pem') }}"
 

	
 
8. And now as finishing touch, simply run the playbooks again::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 

	
 
Adding mail server
 
------------------
 

	
 
The next thing in line is to implement the mail server capability. *Majic
 
Ansible Roles* come with two distinct mail server-related roles. One for
 
setting-up a mail server host (with authenticated IMAP, SMTP, mail storage etc),
 
and one for setting-up a local SMTP mail forwarder (for having the rest of your
 
servers relay their mails to the mail server host).
 

	
 
The mail server role looks-up available mail domains, users, and aliases in the
 
LDAP directory. This has already been set-up on the server
 
``comms.example.com``, but some changes will be required.
 

	
 
1. Update the playbook for communications server to include the mail server
 
role.
 

	
 

	
 
    :file:`~/mysite/playbooks/communications.yml`
 
    ::
 

	
 
      ---
 
      - hosts: communications
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - ldap_server
 
          - mail_server
 

	
 
2. Let's configure the role next.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # Set the LDAP URL to connect through. Keep in mind TLS is required.
 
      mail_ldap_url: ldap://comms.example.com/
 

	
 
      # Here we need to point to the base DN of LDAP server. A bunch of entries
 
      # will need to exist under it for service to function correctly, though.
 
      mail_ldap_base_dn: dc=example,dc=com
 

	
 
      # Separate LDAP entries are used for Postfix/Dovecot
 
      # authentication. Therefore we have two passwords here.
 
      mail_ldap_postfix_password: postfix
 
      mail_ldap_dovecot_password: dovecot
 

	
 
      # Setting uid/gid is optional, but you might have a policy on how to
 
      # assign UIDs and GIDs, so it is convenient to be able to change this.
 
      mail_user_uid: 5000
 
      mail_user_gid: 5000
 

	
 
3. There are two distinct mail services that need to access the LDAP directory -
 
   *Postfix* (serving as an SMTP server), and *Dovecot* (serving as an IMAP
 
   server). These two need their own dedicated LDAP entries on the LDAP server in
 
   order to log-in. Luckily, it is easy to create such entries through the options
 
   provided by the LDAP server role. In addition to this, the Postfix and Dovecot
 
   services will check if users are members of ``mail`` group in LDAP directory
 
   before accepting them as valid mail users. Once again, the LDAP server role
 
   comes with a simple option for creating groups.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # Don't forget, the passwords here must match with passwords specified
 
      # under options mail_ldap_postfix_password/mail_ldap_dovecot_password.
 
      ldap_server_consumers:
 
         - name: postfix
 
           password: postfix
 
         - name: dovecot
 
           password: dovecot
 

	
 
      ldap_server_groups:
 
         - name: mail
 

	
 
4. Ok, so now our SMTP and IMAP service can log-in into the LDAP server to
 
   look-up the mail server information. We have also defined the mail group for
 
   limitting which users get mail service. However, we don't have any
 
   user/domain information yet. So let's change that, using the ``ldap_entries``
 
   option from LDAP server role.
 

	
 
   .. warning::
 
      Long-term, you probably want to manage these entries manually or through
 
      other means than the ``ldap_entries`` option. The reason for this is
 
      because this type of data in LDAP directory can be considered more of an
 
      operational/application data than configuration data that frequently
 
      changes (especially the user passwords/info). Backups of LDAP directory on
 
      regular basis are important. We will get to that at a later point.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      ldap_entries:
 
          # Create first a couple of user entries. Don't forget to set the
 
          # "mail" attribute for them.
 
          - dn: uid=johndoe,ou=people,dc=example,dc=com
 
            attributes:
 
              objectClass:
 
                - inetOrgPerson
 
              uid: johndoe
 
              cn: John Doe
 
              sn: Doe
 
              userPassword: johndoe
 
              mail: john.doe@example.com
 
          - dn: uid=janedoe,ou=people,dc=example,dc=com
 
            attributes:
 
              objectClass:
 
                - inetOrgPerson
 
              uid: janedoe
 
              cn: Jane Doe
 
              sn: Doe
 
              userPassword: janedoe
 
              mail: jane.doe@example.com
 

	
 
          # Now, let's add the two users to the mail group. Observe that we use
 
          # the "state: append" option. This is a bit of a cheat since the
 
          # ldap_entries option passes the provided entries directly to the
 
          # ldap_entry module. "state: append" will make sure we don't overwrite
 
          # the group, and instead add the attributes to it (in this case we add
 
          # the two users from above).
 
          - dn: cn=mail,ou=groups,dc=example,dc=com
 
            state: append
 
            attributes:
 
              uniqueMember:
 
                - uid=johndoe,ou=people,dc=example,dc=com
 
                - uid=janedoe,ou=people,dc=example,dc=com
 

	
 
          # Let's register our domain in LDAP directory.
 
          - dn: dc=example.com,ou=domains,ou=mail,ou=services,dc=example,dc=com
 
            attributes:
 
              objectClass: dNSDomain
 
              dc: "example.com"
 

	
 
          # Finally, for the lolz, let's also add the standard postmaster alias
 
          # for our domain. This one will also receive any undeliverable bounced
 
          # mails.
 
          - dn: cn=postmaster@example.com,ou=aliases,ou=mail,ou=services,dc=example,dc=com
 
            attributes:
 
              objectClass: nisMailAlias
 
              cn: postmaster@example.com
 
              rfc822MailMember: john.doe@example.com
 

	
 
5. Once again, before we apply the configuration, we must make sure the
 
   necessary TLS private keys and certificates are available. In this particular
 
   case, we need to set-up separate key/certificate pair for both the SMTP and
 
   IMAP service:
 

	
 
   1. Create new templates for ``certtool``:
 

	
 
      :file:`~/mysite/tls/comms.example.com_smtp.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. SMTP Server"
 
         expiration_days = 365
 
         dns_name = "comms.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
      :file:`~/mysite/tls/comms.example.com_imap.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. IMAP Server"
 
         expiration_days = 365
 
         dns_name = "comms.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for SMTP/IMAP services based on the templates::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/comms.example.com_smtp.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/comms.example.com_smtp.cfg --load-privkey ~/mysite/tls/comms.example.com_smtp.key --outfile ~/mysite/tls/comms.example.com_smtp.pem
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/comms.example.com_imap.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/comms.example.com_imap.cfg --load-privkey ~/mysite/tls/comms.example.com_imap.key --outfile ~/mysite/tls/comms.example.com_imap.pem
 

	
 
6. Configuration and TLS keys have ben set-up, so it is time to apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
7. If no errors have been reported, at this point you should have two mail
 
   accounts - ``john.doe@example.com``, with password ``johndoe``, and
 
   ``jane.doe@example.com``, with password ``janedoe``. In this particular
 
   set-up, the mail addresses are used as usernames. If you want to test it out,
 
   simply install ``swaks`` on your Ansible machine, and run something along the
 
   lines of
 

	
 
   ::
 

	
 
     swaks --to john.doe@example.com --server comms.example.com
 
     swaks --to jane.doe@example.com --server comms.example.com
 

	
 
  Of course, free feel to also test out the mail server using any mail client of
 
  your choice. When doing so, use port 587 for SMTP. Port 25 is reserved for
 
  unauthenticated server-to-server mail deliveries. TLS has also been hardened
 
  on port 587 to allow only TLSv1.2 and PFS ciphers.
 

	
 

	
 
Setting-up mail relaying from web and backup servers
 
----------------------------------------------------
 

	
 
With the mail server set-up, the next thing to do would be to set-up the SMTP
 
server on web and backup servers to relay mails via the communications
 
server. This way we can make sure that mail that gets sent via local SMTP to
 
external addresses on those two servers goes through our anti-virus scanner.
 

	
 
1. Update the list of roles for web and backup server to include the mail
 
   forwarder role.
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - mail_forwarder
 

	
 
   :file:`~/mysite/playbooks/backup.yml`
 
   ::
 

	
 
      ---
 
      - hosts: backup
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - mail_forwarder
 

	
 
2. The next thing is to set-up the configuration for the new role. We can define
 
   this globally for all servers
 

	
 
   :file:`~/mysite/group_vars/all.yml`
 
   ::
 

	
 
      # First, let's make sure any mails directed to localhost root account get
 
      # forwarded to one of our mail users as well.
 
      local_mail_aliases:
 
         root: root john.doe@example.com
 

	
 
      # Now signal the local SMTP to relay any non-local mails via our
 
      # communications server. Don't forget to specify your own IP address (or
 
      # FQDN) here. Without this option, the SMTP would send out the mails
 
      # directly.
 
      smtp_relay_host: comms.example.com
 

	
 
3. Although we have told our web and backup servers to use the communications
 
   server as relay for non-local mail, the communications server is not aware of
 
   this. This would result in the communications server refusing all relay
 
   attempts (if not, it would be an open relay, which is bad).
 

	
 
   So, let's fix this a bit - we have a configuration option for the mail server
 
   for exactly this purpose.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # We want to allow relaying of mails from our web and backup servers
 
      # here.Beware the IP spoofing, though! Don't forget to change the bellow
 
      # IP for your server ;)
 
      smtp_allow_relay_from:
 
         - 10.32.64.20
 
         - 10.32.64.23
 

	
 
4. Let's apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
5. After this you may want to test out sending mail via web or backup server's
 
   local SMTP to the root user (to see if the aliasing works), and to some
 
   external mail address to check if forwarding works correctly too. Run some
 
   something similar to the following on your web server::
 

	
 
     swaks --to root@localhost --server localhost
 
     swaks --to YOUR_MAIL --server localhost
 

	
 
   If all went well, you should be able to see a new mail in John Doe's mailbox,
 
   as well as your own mailbox.
 

	
 

	
 
Adding XMPP server
 
------------------
 

	
 
Now that the users can communicate via mail server, we might as well add support
 
for some instant messaging. For this purpose, we will use the ``xmpp_server``
 
role.
 

	
 
1. Update the playbook for communications server to include the XMPP server
 
   role.
 

	
 
   :file:`~/mysite/playbooks/communications.yml`
 
   ::
 

	
 
      ---
 
      - hosts: communications
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - ldap_server
 
          - mail_server
 
          - xmpp_server
 

	
 
2. Configure the role.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # Set one of the users to also be an XMPP administrator.
 
      xmpp_administrators:
 
         - john.doe@example.com
 

	
 
      # Unfortunately, XMPP can't look-up domains via LDAP, so we need to be
 
      # explicit here.
 
      xmpp_domains:
 
         - example.com
 

	
 
      # Simply point the XMPP server to base DN of LDAP server, and let it use
 
      # specific directory structure it expects.
 
      xmpp_ldap_base_dn: dc=example,dc=com
 

	
 
      # Password for logging-in into the LDAP directory.
 
      xmpp_ldap_password: prosody
 

	
 
      # Where the LDAP server is located at. Full-blown LDAP URIs are _not_
 
      # supported!
 
      xmpp_ldap_server: comms.example.com
 

	
 
3. Now, like in case of the mail server role, we need to set-up authentication
 
   for the XMPP service. In this particular case a single consumer is present -
 
   Prosody itself. We should also create the group for granting the users right
 
   to use the service.
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # Just make sure the new entry is added for the prosody user - you can
 
      # leave the postfix/dovecot intact in your file if you use different
 
      # passwords. Keep in mind password for prosody user must match with
 
      # password specified under xmpp_ldap_password.
 
      ldap_server_consumers:
 
         - name: postfix
 
           password: postfix
 
         - name: dovecot
 
           password: dovecot
 
         - name: prosody
 
           password: prosody
 

	
 
      # And simply append a new group here...
 
      ldap_server_groups:
 
         - name: mail
 
         - name: xmpp
 

	
 
4. Ok, configuration of the role is almost complete. You may have noticed that
 
   we still haven't added any users to the new LDAP group called "xmpp". So let
 
   us correct this in similar way as we did for the mail server. Since we have
 
   the user entries already, no need to recreate them here. We will just update
 
   the group membership instead.
 

	
 
   .. warning::
 
      Same warning applies here as for mail server role for managing the
 
      user/group entries! Scroll up and re-read it if you missed it!
 

	
 
   :file:`~/mysite/group_vars/communications.yml`
 
   ::
 

	
 
      # Don't replace the entire ldap_entries, just append the new group
 
      # modification.
 
      ldap_entries:
 
          # Add the two users to the xmpp group. Observe that we use
 
          # the "state: append" option. This is a bit of a cheat since the
 
          # ldap_entries option passes the provided entries directly to the
 
          # ldap_entry module. "state: append" will make sure we don't overwrite
 
          # the group, and instead add the attributes to it (in this case we add
 
          # the two users).
 
          - dn: cn=xmpp,ou=groups,dc=example,dc=com
 
            state: append
 
            attributes:
 
              uniqueMember:
 
                - uid=johndoe,ou=people,dc=example,dc=com
 
                - uid=janedoe,ou=people,dc=example,dc=com
 

	
 
5. Do you know what comes next? Yes! Create some more TLS private keys
 
   and certificates, this time for our XMPP server ;)
 

	
 
   1. Create new template for ``certtool``:
 

	
 
      :file:`~/mysite/tls/comms.example.com_xmpp.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. XMPP Server"
 
         expiration_days = 365
 
         dns_name = "comms.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for XMPP service based on the template::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/comms.example.com_xmpp.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/comms.example.com_xmpp.cfg --load-privkey ~/mysite/tls/comms.example.com_xmpp.key --outfile ~/mysite/tls/comms.example.com_xmpp.pem
 

	
 
6. Apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
7. If no errors have been reported, at this point you should have two users
 
   capable of using the XMPP service - one with username
 
   ``john.doe@example.com`` and one with username ``jane.doe@example.com``. Same
 
   passwords are used as for when you were creating the two users for mail
 
   server. For testing you can turn to your favourite XMPP client (I don't know
 
   of any quick CLI-based tools to test the XMPP server functionality,
 
   unfortunately, but you could try using `mcabber <https://mcabber.com/>`_).
 

	
 

	
 
Taking a step back - preparing for web server
 
---------------------------------------------
 

	
 
Up until now the usage instructions have dealt almost exclusively with the
 
communications server. That is, we haven't done anything beyond the basic set-up
 
of the other servers.
 

	
 
Let us first define what we want to deploy on the web server. Here is the plan:
 

	
 
1. First off, we will set-up the web server. This will be necessary no matter
 
   what web application we decide to deploy later on.
 

	
 
2. Next, we will set-up a database server. Why? Well, most web applications
 
   need to use some sort of database to store all the data, so we might as well
 
   try to take that one out of the way.
 

	
 
3. With this basic deployment for a web server in place, we can move on to
 
   setting-up a couple of web applications. For the purpose of the usage
 
   instructions, we will deploy the following two:
 

	
 
   1. `The Bug Genie <http://thebuggenie.org/>`_ - an issue tracker. To keep
 
      things simple, we will not integrate it with our LDAP server (although
 
      this is supported and possible). Being written in PHP, this will
 
      demonstrate the role for PHP web application deployment.
 

	
 
   2. `Django Wiki <https://github.com/django-wiki/django-wiki>`_ - a wiki
 
      application written in Django. This will serve as a demo of how the WSGI
 
      role works.
 

	
 
It should be noted that the web application deployment roles are a bit more
 
complex - namely they are not meant to be used directly, but instead as a
 
dependency for a custom role. They do come with decent amount of batteries
 
included, and also play nice with the web server role.
 

	
 
As mentioned before, all roles will enforce TLS by default. The web server roles
 
will additionaly implement HSTS policy by sending connecting clients
 
``Strict-Transport-Security`` header with value set to ``max-age=31536000;
 
includeSubDomains`` (if you disable enforcement of TLS, the header will not be
 
sent).
 

	
 
With all the above noted, let us finally move on to the next step.
 

	
 

	
 
Setting-up the web server
 
-------------------------
 

	
 
Finally we are moving on to the web server deployment, and we shell start
 
with... Well, erm, web server deployment! To be more precise, we will set-up
 
Nginx.
 

	
 
1. Update the playbook for web server to include the web server role.
 

	
 

	
 
    :file:`~/mysite/playbooks/web.yml`
 
    ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - mail_forwarder
 
          - web_server
 

	
 
2. You know the drill, role configuration comes up next. Actually... The web
 
   server role parameters are all optional, and they default to some ok-ish
 
   values. But let us spicen up things a bit nevertheless. No configuration has
 
   been deployed before for the web server, so we will be creating a new file.
 

	
 
   :file:`~/mysite/group_vars/web.yml`
 
   ::
 

	
 
      ---
 

	
 
      web_default_title: "Welcome to default page!"
 
      web_default_message: "Nothing to see here, move along..."
 

	
 
3. The only thing left now is to create the TLS private key/certificate pair
 
   that should be used for default virtual host.
 

	
 
   1. Create new template for ``certtool``:
 

	
 
      :file:`~/mysite/tls/www.example.com_https.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. Web Server"
 
         expiration_days = 365
 
         dns_name = "www.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for default web server virtual host based
 
      on the template::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/www.example.com_https.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/www.example.com_https.cfg --load-privkey ~/mysite/tls/www.example.com_https.key --outfile ~/mysite/tls/www.example.com_https.pem
 

	
 
4. Apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
5. If no errors have been reported, at this point you should have a default web
 
   page available and visible at https://www.example.com/ . By default plaintext
 
   connections are disabled, and trying to visit http://www.example.com/ should
 
   simply redirect you to the HTTPS address. Feel free to try it out with some
 
   browser. Keep in mind you will get a warning about the untrusted certificate!
 

	
 

	
 
Adding the database server
 
--------------------------
 

	
 
Since both of the web applications we want to deploy need a database, we will
 
proceed to set-up the database server role on the web server itself. *Majic
 
Ansible Roles* in particular come with a role that will deploy MariaDB database
 
server.
 

	
 
1. Update the playbook for web server to include the database server role.
 

	
 

	
 
    :file:`~/mysite/playbooks/web.yml`
 
    ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - mail_forwarder
 
          - web_server
 
          - database_server
 

	
 
2. Now let's configure the role. This is rather simplistic, since we only need
 
   to set the database server root (admin) password.
 

	
 
   :file:`~/mysite/group_vars/web.yml`
 
   ::
 

	
 
      db_root_password: root
 

	
 
3. No TLS support has been implemented for this role (yet), so simply apply the
 
   changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
4. If no errors have been reported, you should have a database server up and
 
   running on the web server. You should be able to log-in using password
 
   ``root`` by running the following command on the web server itself::
 

	
 
     mysql -uroot -p
 

	
 
   Of course, no database has been created for either of the web applications,
 
   but we will get to that one later (there is a dedicated ``database`` role
 
   which can be combined with web app roles for this purpose).
 

	
 

	
 
Deploying a PHP web application (The Bug Genie)
 
-----------------------------------------------
 

	
 
We have some basic infrastructure up and running now on our web server, so we
 
shall move on to setting-up a PHP web application on it. As mentioned before, we
 
will take *The Bug Genie* as an example.
 

	
 
For this we will create a local role in our site to take care of it. This role
 
will in turn utilise two roles coming from *Majic Ansible Roles* that will make
 
our life (a little) easier.
 

	
 
To make the example a bit simpler, no parameters will be introducd for this role
 
(not even the password for database, we'll hard-code everything).
 

	
 
Before we start, here is a couple of useful pointers regarding the
 
``php_website`` role we'll be using for the PHP part:
 

	
 
* The role is designed to execute every application via dedicated user and
 
  group. The user/group name is automatically derived from the FQDN of website,
 
  for example ``web-tbg_example_com``.
 
* While running the application, application user's umask is set to ``0007``
 
  (letting the administrator user be able to manage any files created while the
 
  application is running).
 
* An administrative user is created as well, and this user should be used when
 
  running maintenance and installation commands. Similar to application user,
 
  the name is also derived from the FQDN of website, for example
 
  ``admin-tbg_example_com``. Administrative user does not have a dedicated
 
  group, and instead belongs to same group as the application user.
 
* PHP applications are executed via FastCGI, using the ``php5-fpm`` package.
 
* If you ever need to set some additional PHP FPM settings, this can easily be
 
  done via the ``additional_fpm_config`` role parameter. This particular example
 
  does not set any, though.
 
* Mails deliverd to local admin/application users are forwarded to ``root``
 
  account instead (this can be configured via ``website_mail_recipients`` role
 
  parameter.
 
* If you ever find yourself mixing-up test and production websites, have a look
 
  at ``environment_indicator`` role parameter. It lets you insert small strip at
 
  bottom of each HTML page automatically.
 
* Static content (non-PHP) is served directly by *Nginx*.
 
* Each web application gets distinct sub-directory under ``/var/www``, named
 
  after the FQDN. All sub-directories created under there are created with
 
  ``2750`` permissions, with ownership set to admin user, and group set to the
 
  application's group. In other words, all directories will have ``SGID`` bit
 
  set-up, allowing you to create files/directories that will have their group
 
  automatically set to the group of the parent directory.
 
* Files are served (both by *Nginx* and *php5-fpm*) from sub-directory called
 
  ``htdocs`` (located in website directory). For example
 
  ``/var/www/tbg.example.com/htdocs/``. Normally, this can be a symlink to some
 
  other sub-directory within the website directory (useful for having multiple
 
  versions for easier downgrades etc).
 
* Combination of admin user membership in application group, ``SGID``
 
  permission, and the way ownership of sub-directories is set-up usually means
 
  that the administrator will be capable of managing application files, and
 
  application can be granted write permissions to a *minimum* of necessary
 
  files.
 

	
 
  .. warning::
 
     Just keep in mind that some file-management commands, like ``mv``, do *not*
 
     respect the ``SGID`` bit. In fact, I would recommend using ``cp`` when you
 
     deploy new files to the directory instead (don't simply move them from your
 
     home).
 

	
 
1. Start-off with creating the necessary directories for the new role::
 

	
 
     mkdir -p ~/mysite/roles/tbg/{tasks,meta,files}/
 

	
 
2. Let's set-up role dependencies, reusing some common roles to make our life
 
   easier.
 

	
 
   :file:`~/mysite/roles/tbg/meta/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      dependencies:
 
         # Ok, so this role helps us set-up Nginx virtual host for serving our
 
         # app.
 
         - role: php_website
 
           # Our virtual host will for PHP website will respond to this name.
 
           fqdn: tbg.example.com
 
           # Some additional packages are required in order to deploy and use TBG.
 
           packages:
 
              - php5-gd
 
              - php5-curl
 
              - git
 
              - php5-mysql
 
              - expect
 
           # Set-up URL rewriting. This is based on public/.htaccess file from
 
           # TBG.
 
           php_rewrite_urls:
 
              - ^(.*)$ /index.php?url=$1
 
           # We don't necessarily need this, but in case you have a policy on
 
           # uid/gid usage, this is useful. Take note that below value is used
 
           # for both the dedicated uid and gid for application user.
 
           uid: 2000
 
           admin_uid: 3000
 
         # And this role sets up a new dedicated database for our web
 
         # application.
 
         - role: database
 
           # This is both the database name, _and_ name of the database user
 
           # that will be granted full privileges on the database.
 
           db_name: tbg
 
           # This will be the password of our user 'tbg' for accessing the
 
           # database. Take note the user can only login from localhost.
 
           db_password: tbg
 

	
 
3. Now for my favourite part again - creating private keys and certificates!
 
   Why?  Because the ``php_website`` role requires a private key/certificate
 
   pair to be deployed. So... Moving on:
 

	
 
   1. Create new template for ``certtool``:
 

	
 
      :file:`~/mysite/tls/tbg.example.com_https.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. Issue Tracker"
 
         expiration_days = 365
 
         dns_name = "tbg.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for the application::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/tbg.example.com_https.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/tbg.example.com_https.cfg --load-privkey ~/mysite/tls/tbg.example.com_https.key --outfile ~/mysite/tls/tbg.example.com_https.pem
 

	
 
4. Time to get our hands a bit more dirty... Up until now we didn't have to write
 
   custom tasks, but at this point we need to.
 

	
 
   :file:`~/mysite/roles/tbg/tasks/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      - name: Define TBG version
 
        set_fact: tbg_version=4.1.0
 

	
 
      - name: Download the TBG archive
 
        get_url: url=https://github.com/thebuggenie/thebuggenie/archive/v{{ tbg_version }}.tar.gz
 
                 dest="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}.tar.gz"
 
                 sha256sum=0fd0a680ba281adc97d5d2c720e63b995225c99716a36eca6a198b8a5ebf8057
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Download Composer
 
        get_url: url=https://getcomposer.org/download/1.0.0-alpha10/composer.phar
 
                 dest="/usr/local/bin/composer"
 
                 sha256sum=9f2c7d0364bc743bcde9cfe1fe84749e5ac38c46d47cf42966ce499135fd4628
 
                 owner=root group=root mode=755
 

	
 
      - name: Unpack TBG
 
        unarchive: src="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}.tar.gz"
 
                   dest="/var/www/tbg.example.com/" copy=no
 
                   creates="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}"
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Create TBG cache directory
 
        file: path="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/cache" state=directory mode=2770
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Set-up the necessary write permissions for TBG directories
 
        file: path="{{ item }}" mode=g+w
 
        with_items:
 
           - /var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/
 
           - /var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/public/
 
           - /var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/core/config/
 

	
 
      - name: Create symbolic link to TBG application
 
        file: src="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/public"
 
              path="/var/www/tbg.example.com/htdocs"
 
              state=link
 
              owner="admin-tbg_example_com" group="web-tbg_example_com" mode=2750
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Install TBG dependencies
 
        composer: command=install working_dir="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}"
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Deploy database configuration file for TBG
 
        copy: src="b2db.yml" dest="/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/core/config/b2db.yml"
 
              mode=640 owner=admin-tbg_example_com group=web-tbg_example_com
 

	
 
      - name: Deploy expect script for installing TBG
 
        copy: src="tbg_expect_install" dest="/var/www/tbg.example.com/tbg_expect_install" mode=750
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
      - name: Run TBG installer via expect script
 
        command: /var/www/tbg.example.com/tbg_expect_install
 
                 chdir=/var/www/tbg.example.com/thebuggenie-{{ tbg_version }} 
 
                 creates=/var/www/tbg.example.com/thebuggenie-{{ tbg_version }}/installed
 
        become: yes
 
        become_user: admin-tbg_example_com
 

	
 
5. Set-up the files that are deployed by our role.
 

	
 
   :file:`~/mysite/roles/tbg/files/b2db.yml`
 
   ::
 

	
 
      b2db:
 
          username: "tbg"
 
          password: "tbg"
 
          dsn: "mysql:host=localhost;dbname=tbg"
 
          tableprefix: ''
 
          cacheclass: '\thebuggenie\core\framework\Cache'
 

	
 
   :file:`~/mysite/roles/tbg/files/tbg_expect_install`
 
   ::
 

	
 
      #!/usr/bin/expect
 

	
 
      spawn ./tbg_cli install --accept_license=yes --url_subdir=/ --use_existing_db_info=yes --enable_all_modules=yes --setup_htaccess=yes
 

	
 
      expect "Press ENTER to continue with the installation: "
 
      send "\r"
 
      expect "Press ENTER to continue: "
 
      send "\r"
 
      interact
 

	
 
6. And... Let's add the new role to our web server.
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - mail_forwarder
 
          - web_server
 
          - database_server
 
          - tbg
 

	
 
7. Apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
8. At this point *The Bug Genie* has been installed, and you should be able to
 
   open the URL https://tbg.example.com/ (if you open http://tbg.example.com/ ,
 
   you will be redirected to the HTTPS URL) and log-in into *The Bug Genie*
 
   with username ``administrator`` and password ``admin``.
 

	
 

	
 
Deploying a WSGI application (Django Wiki)
 
------------------------------------------
 

	
 
Next thing up will be to deploy a WSGI Python application.
 

	
 
Similar to the PHP application deployment, we will use a couple of roles to make
 
it easier to deploy it in a standardised manner, and we will not have any kind
 
of parameters for configuring the role to keep things simple.
 

	
 
Most of the notes on how a ``php_website`` role is deployed also stand for the
 
``wsgi_website`` role, but we will reiterate and clarify them a bit just to be
 
on the safe side:
 

	
 
* The role is designed to execute every application via dedicated user and
 
  group. The user/group name is automatically derived from the FQDN of website,
 
  for example ``web-wiki_example_com``.
 
* While running the application, application user's umask is set to ``0007``
 
  (letting the administrator user be able to manage any files created while the
 
  application is running).
 
* An administrative user is created as well, and this user should be used when
 
  running maintenance and installation commands. Similar to application user,
 
  the name is also derived from the FQDN of website, for example
 
  ``admin-wiki_example_com``. Administrative user does not have a dedicated
 
  group, and instead belongs to same group as the application user. As
 
  convenience, whenever you switch to this user the Python virtual environment
 
  will be automatically activated for you.
 
* WSGI applications are executed via *Gunicorn*. The WSGI server listens on a
 
  Unix socket, making the socket accessible by *Nginx*.
 
* If you ever need to set some environment variables, this can easily be done
 
  via the ``environment_variables`` role parameter. This particular example does
 
  not set any, though.
 
* You can also specify headers to be passed on via Nginx ``proxy_set_header``
 
  directive to Gunicorn running the application.
 
* Mails deliverd to local admin/application users are forwarded to ``root``
 
  account instead (this can be configured via ``website_mail_recipients`` role
 
  parameter.
 
* If you ever find yourself mixing-up test and production websites, have a look
 
  at ``environment_indicator`` role parameter. It lets you insert small strip at
 
  bottom of each HTML page automatically.
 
* Static content is served directly by *Nginx*.
 
* Each web application gets distinct sub-directory under ``/var/www``, named
 
  after the FQDN. All sub-directories created under there are created with
 
  ``2750`` permissions, with ownership set to admin user, and group set to the
 
  application's group. In other words, all directories will have ``SGID`` bit
 
  set-up, allowing you to create files/directories that will have their group
 
  automatically set to the group of the parent directory.
 
* Each WSGI website gets a dedicated virtual environment, stored in the
 
  sub-directory ``virtualenv`` of the website directory, for example
 
  ``/var/www/wiki.example.com/virtualenv``.
 
* Static files are served from sub-directory ``htdocs`` in the website
 
  directory, for example ``/var/www/wiki.example.com/htdocs/``.
 
* The base directory where your website/application code should be at is
 
  expected to be in sub-directory ``code`` in the website directory, for example
 
  ``/var/www/wiki.example.com/code/``.
 
* Combination of admin user membership in application group, ``SGID``
 
  permission, and the way ownership of sub-directories is set-up usually means
 
  that the administrator will be capable of managing application files, and
 
  application can be granted write permissions to a *minimum* of necessary
 
  files.
 

	
 
  .. warning::
 
     Just keep in mind that some file-management commands, like ``mv``, do *not*
 
     respect the ``SGID`` bit. In fact, I would recommend using ``cp`` when you
 
     deploy new files to the directory instead (don't simply move them from your
 
     home).
 

	
 
1. Set-up the necessary directories first::
 

	
 
     mkdir -p ~/mysite/roles/wiki/{tasks,meta,files}/
 

	
 
2. Set-up some role dependencies, reusing the common role infrastructure.
 

	
 
   :file:`~/mysite/roles/wiki/meta/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      dependencies:
 
         - role: wsgi_website
 
           fqdn: wiki.example.com
 
           # In many cases you need to have some development packages available
 
           # in order to build Python packages installed via pip
 
           packages:
 
              - build-essential
 
              - python-dev
 
              - libjpeg-dev
 
              - libzip-dev
 
              - libtiff-dev
 
              - libfreetype6-dev
 
              - liblcms2-dev
 
              - libwebp-dev
 
              - libopenjpeg-dev
 
              - libmariadb-client-lgpl-dev
 
              - libmariadb-client-lgpl-dev-compat
 
           # Here we specify that anything accessing our website with "/static/"
 
           # URL should be treated as request to a static file, to be served
 
           # directly by Nginx instead of the WSGI server.
 
           static_locations:
 
              - /static/
 
           # Again, not mandatory, but it is good to have some sort of policy
 
           # for assigning UIDs.
 
           uid: 2001
 
           admin_uid: 3001
 
           # These are additional packages that should be installed in the
 
           # virtual environment.
 
           virtualenv_packages:
 
             - pillow
 
             - django==1.8.13
 
             - wiki
 
             - MySQL-python
 
           # This is the name of the WSGI application to
 
           # serve. wiki_example_com.wsgi will be the Python "module" that is
 
           # accesed, while application is the object instantiated within it (the
 
           # application itself). The module is referenced relative to the code
 
           # directory (in our case /var/www/wiki.example.com/code/).
 
           wsgi_application: wiki_example_com.wsgi:application
 
         - role: database
 
           db_name: wiki
 
           db_password: wiki
 

	
 
3. Let's create a dedicated private key/certificate pair for the wiki website:
 

	
 
   1. Create new template for ``certtool``:
 

	
 
      :file:`~/mysite/tls/wiki.example.com_https.cfg`
 
      ::
 

	
 
         organization = "Example Inc."
 
         country = SE
 
         cn = "Exampe Inc. Wiki"
 
         expiration_days = 365
 
         dns_name = "wiki.example.com"
 
         tls_www_server
 
         signing_key
 
         encryption_key
 

	
 
   2. Create the keys and certificates for the application::
 

	
 
        certtool --sec-param normal --generate-privkey --outfile ~/mysite/tls/wiki.example.com_https.key
 
        certtool --generate-certificate --load-ca-privkey ~/mysite/tls/ca.key --load-ca-certificate ~/mysite/tls/ca.pem --template ~/mysite/tls/wiki.example.com_https.cfg --load-privkey ~/mysite/tls/wiki.example.com_https.key --outfile ~/mysite/tls/wiki.example.com_https.pem
 

	
 
4. At this point we have exhausted what we can do with the built-in roles. Time
 
   to add some custom tasks.
 

	
 
   :file:`~/mysite/roles/wiki/tasks/main.yml`
 
   ::
 

	
 
      ---
 

	
 
      - name: Create Django project directory
 
        file: dest="/var/www/wiki.example.com/code" state=directory
 
              owner=admin-wiki_example_com group=web-wiki_example_com
 
              mode=2750
 

	
 
      - name: Start Django project for the Wiki website
 
        command: /var/www/wiki.example.com/virtualenv/bin/exec django-admin.py startproject wiki_example_com /var/www/wiki.example.com/code
 
                 chdir=/var/www/wiki.example.com
 
                 creates=/var/www/wiki.example.com/code/wiki_example_com
 
        become: yes
 
        become_user: admin-wiki_example_com
 

	
 
      - name: Deploy settings for wiki website
 
        copy: src="{{ item }}" dest="/var/www/wiki.example.com/code/wiki_example_com/{{ item }}"
 
              mode=640 owner=admin group=web-wiki_example_com
 
        with_items:
 
           - settings.py
 
           - urls.py
 
        notify:
 
           - Restart website wiki.example.com
 

	
 
      - name: Deploy project database and deploy static files
 
        django_manage: command="{{ item }}"
 
                       app_path="/var/www/wiki.example.com/code/"
 
                       virtualenv="/var/www/wiki.example.com/virtualenv/"
 
        become: yes
 
        become_user: admin-wiki_example_com
 
        with_items:
 
           - syncdb
 
           - migrate
 
           - collectstatic
 

	
 
      - name: Deploy the superadmin creation script
 
        copy: src="create_superadmin.py" dest="/var/www/wiki.example.com/code/create_superadmin.py"
 
              owner=admin-wiki_example_com group=web-wiki_example_com mode=750
 

	
 
      - name: Create initial superuser
 
        command: /var/www/wiki.example.com/virtualenv/bin/exec ./create_superadmin.py
 
                 chdir=/var/www/wiki.example.com/code/
 
        become: yes
 
        become_user: admin-wiki_example_com
 
        register: wiki_superuser
 
        changed_when: wiki_superuser.stdout == "Created superuser."
 

	
 
5. There is a couple of files that we are deploying through the above
 
   tasks. Let's create them as well.
 

	
 
   :file:`~/mysite/roles/wiki/files/settings.py`
 
   ::
 

	
 
      """
 
      Django settings for wiki_example_com project.
 

	
 
      For more information on this file, see
 
      https://docs.djangoproject.com/en/1.6/topics/settings/
 

	
 
      For the full list of settings and their values, see
 
      https://docs.djangoproject.com/en/1.6/ref/settings/
 
      """
 

	
 
      # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 
      import os
 
      BASE_DIR = os.path.dirname(os.path.dirname(__file__))
 

	
 

	
 
      # Quick-start development settings - unsuitable for production
 
      # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
 

	
 
      # SECURITY WARNING: keep the secret key used in production secret!
 
      SECRET_KEY = 'l^q+t$7h$ebls)v34+w9m9v4$n+^(9guxqntu&#cc4m&lfd-6_'
 

	
 
      # SECURITY WARNING: don't run with debug turned on in production!
 
      DEBUG = False
 

	
 
      TEMPLATE_DEBUG = False
 

	
 
      ALLOWED_HOSTS = ["wiki.example.com", "localhost"]
 

	
 

	
 
      # Application definition
 

	
 
      INSTALLED_APPS = (
 
          'django.contrib.admin',
 
          'django.contrib.auth',
 
          'django.contrib.contenttypes',
 
          'django.contrib.sessions',
 
          'django.contrib.messages',
 
          'django.contrib.staticfiles',
 
          'django.contrib.sites',
 
          'django.contrib.humanize',
 
          'django_nyt',
 
          'mptt',
 
          'sekizai',
 
          'sorl.thumbnail',
 
          'wiki',
 
          'wiki.plugins.attachments',
 
          'wiki.plugins.notifications',
 
          'wiki.plugins.images',
 
          'wiki.plugins.macros',
 
      )
 

	
 
      MIDDLEWARE_CLASSES = (
 
          'django.contrib.sessions.middleware.SessionMiddleware',
 
          'django.middleware.common.CommonMiddleware',
 
          'django.middleware.csrf.CsrfViewMiddleware',
 
          'django.contrib.auth.middleware.AuthenticationMiddleware',
 
          'django.contrib.messages.middleware.MessageMiddleware',
 
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
 
      )
 

	
 
      ROOT_URLCONF = 'wiki_example_com.urls'
 

	
 
      WSGI_APPLICATION = 'wiki_example_com.wsgi.application'
 

	
 

	
 
      # Database
 
      # https://docs.djangoproject.com/en/1.6/ref/settings/#databases
 

	
 
      DATABASES = {
 
          'default': {
 
              'ENGINE': 'django.db.backends.mysql',
 
              'NAME': 'wiki',
 
              'USER': 'wiki',
 
              'PASSWORD': 'wiki',
 
              'HOST': '127.0.0.1',
 
              'PORT': '3306',
 
          }
 
      }
 

	
 
      # Internationalization
 
      # https://docs.djangoproject.com/en/1.6/topics/i18n/
 

	
 
      LANGUAGE_CODE = 'en-us'
 

	
 
      TIME_ZONE = 'Europe/Stockholm'
 

	
 
      USE_I18N = True
 

	
 
      USE_L10N = True
 

	
 
      USE_TZ = True
 

	
 

	
 
      # Static files (CSS, JavaScript, Images)
 
      # https://docs.djangoproject.com/en/1.6/howto/static-files/
 

	
 
      STATIC_URL = '/static/'
 

	
 
      STATIC_ROOT = '/var/www/wiki.example.com/htdocs/static'
 

	
 
      from django.conf import settings
 

	
 
      TEMPLATE_CONTEXT_PROCESSORS = settings.TEMPLATE_CONTEXT_PROCESSORS + (
 
          "django.core.context_processors.debug",
 
          "django.core.context_processors.request",
 
          "sekizai.context_processors.sekizai",
 
      )
 

	
 
      SITE_ID=1
 

	
 
   :file:`~/mysite/roles/wiki/files/urls.py`
 
   ::
 

	
 
      from django.conf.urls import patterns, include, url
 
      from wiki.urls import get_pattern as get_wiki_pattern
 
      from django_nyt.urls import get_pattern as get_nyt_pattern
 

	
 
      from django.contrib import admin
 
      admin.autodiscover()
 

	
 
      urlpatterns = patterns('',
 
          # Examples:
 
          # url(r'^$', 'wiki_example_com.views.home', name='home'),
 
          # url(r'^blog/', include('blog.urls')),
 

	
 
          url(r'^admin/', include(admin.site.urls)),
 
      )
 

	
 
      urlpatterns += patterns('',
 
          (r'^notifications/', get_nyt_pattern()),
 
          (r'', get_wiki_pattern())
 
      )
 

	
 
   :file:`~/mysite/roles/wiki/files/create_superadmin.py`
 
   ::
 

	
 
      #!/usr/bin/env python
 

	
 
      import os
 
      from django import setup
 
      os.environ['DJANGO_SETTINGS_MODULE']='wiki_example_com.settings'
 
      setup()
 
      from django.conf import settings
 
      from django.contrib.auth.models import User
 

	
 
      User.objects.all()
 
      if len(User.objects.filter(username="admin")) == 0:
 
          User.objects.create_superuser('admin', 'john.doe@example.com', 'admin')
 
          print("Created superuser.")
 

	
 
6. Time to add the new role to our web server.
 

	
 
   :file:`~/mysite/playbooks/web.yml`
 
   ::
 

	
 
      ---
 
      - hosts: web
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - ldap_client
 
          - mail_forwarder
 
          - web_server
 
          - database_server
 
          - tbg
 
          - wiki
 

	
 
7. Apply the changes:
 

	
 
   .. warning::
 

	
 
      Due to `Debian Bug 766996
 
      <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=766996>`_ Majic Ansible
 
      Roles try to detect if you install ``libmariadb-client-lgpl-dev-compat``
 
      package, and create symbolic link from ``/usr/local/bin/mysql_config`` to
 
      ``/usr/bin/mariadb_config`` automatically. Otherwise the MySQL-python
 
      package will fail to build due to missing ``mysql_config`` binary.
 

	
 
   ::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
8. At this point Django Wiki has been installed, and you should be able to open
 
   the URL https://wiki.example.com/ (if you open http://wiki.example.com/ , you
 
   will be redirected to the HTTPS URL) and log-in into *Django Wiki* with
 
   username ``admin`` and password ``admin``.
 

	
 

	
 
Backups, backups, backups!
 
--------------------------
 

	
 
As it is well known, everyone has backups of their important data. Right?
 
Riiiiight?
 

	
 
There are three Ansible roles that implement backup functionality -
 
``backup_server``, ``backup_client``, and ``backup``. Backup is based around the
 
use of `Duplicity <http://duplicity.nongnu.org/>`_ and its convenience wrapper,
 
`Duply <http://duply.net>`_. Due to this selection, it should be noted that the
 
backup clients are the ones making connection to the backup server (not the
 
other way around).
 

	
 
Backups are encrypted and signed using GnuPG before being stored on the backup
 
server. Private key used for encryption and signing is therefore stored on the
 
client side. This key should not be encrypted in order to allow for unattended
 
backups.
 

	
 
Although not necessary, it is highly recommended to have separation between
 
different backup clients and the keys used for encryption and
 
signing. I.e. stick to a separate encryption/signing key for each backup
 
client. It should be noted that it is also possible to specify additional
 
*public* keys to encrypt with. This lets you have a backup decryptable with some
 
other, "master" key.
 

	
 
The backups are transferred to the backup server via SFTP - the
 
``backup_server`` role sets-up a dedicated OpenSSH server instance that limits
 
the connecting clients to a SFTP chroot.
 

	
 
All backups are stored within directory ``/srv/backups`` (on the backup
 
server). Within this directory, every client server has a dedicated
 
sub-directory, and within this sub-directory another sub-directory called
 
``duplicity``, where the actual *Duplicity* backups are stored. So, for example,
 
the directory where backups for ``www.example.com`` are stored at would be
 
``/srv/backups/www.example.com/duplicity``.
 

	
 
When backups are configured, they are set-up to be running every morning at
 
02:00. Before the backup run it is possible to run a preparation task, and a lot
 
of roles do this in order to create database dumps etc.
 

	
 

	
 
Setting-up the backup server
 
----------------------------
 

	
 
With the overview of backups out of the way, it is time to set-up the backup
 
server itself first. This is a farily simple task to perform, so let's get
 
straight to it:
 

	
 
1. Update the playbook for backup server to include the backup server role.
 

	
 
   :file:`~/mysite/playbooks/backup.yml`
 
   ::
 

	
 
      ---
 
      - hosts: backup
 
        remote_user: ansible
 
        become: yes
 
        roles:
 
          - common
 
          - mail_forwarder
 
          - backup_server
 

	
 
2. There is just one mandatory parameter for the role - OpenSSH server keys to
 
   be used for backup-dedicated instance:
 

	
 
   :file:`~/mysite/group_vars/backup.yml`
 
   ::
 

	
 
      ---
 

	
 
      backup_host_ssh_private_keys:
 
        dsa: "{{ lookup('file', inventory_dir + '/ssh/bak_dsa_key') }}"
 
        rsa: "{{ lookup('file', inventory_dir + '/ssh/bak_rsa_key') }}"
 
        ed25519: "{{ lookup('file', inventory_dir + '/ssh/bak_ed25519_key') }}"
 
        ecdsa: "{{ lookup('file', inventory_dir + '/ssh/bak_ecdsa_key') }}"
 

	
 
3. Since we have decided to specify the keys above through file lookup, the
 
   above-listed keys now need to be generated::
 

	
 
     ssh-keygen -f ~/mysite/ssh/bak_dsa_key -N '' -t dsa
 
     ssh-keygen -f ~/mysite/ssh/bak_rsa_key -N '' -t rsa
 
     ssh-keygen -f ~/mysite/ssh/bak_ed25519_key -N '' -t ed25519
 
     ssh-keygen -f ~/mysite/ssh/bak_ecdsa_key -N '' -t ecdsa
 

	
 

	
 
Adding backup clients
 
---------------------
 

	
 
Well, that was all nice and dandy, but it does look like something is
 
missing... Ah, we haven't really configured any backup clients, right?
 
Surprisingly enough, specifying backup clients is optional, but that won't get
 
you far.
 

	
 
Luckily for you, all relevant *Majic Ansible Roles* are *backup-aware*. In other
 
words, all the roles have been implemented with support for doing back-ups - it
 
is just that by default this functionality is disabled (since you might be
 
relying on some other schema to back things up - LVM snapshots or what-not).
 

	
 
All that is needed is to enable the backups for each role, and provide some
 
extra variables required by the ``backup_client`` role.
 

	
 
For this a set of GnuPG private keys are necessary. These need to be provided as
 
ASCII-armoured GnuPG-exported files. For simplicity sake, this example documents
 
use of GnuPG keyring in conjunction with Ansible's ``pipe`` lookup.
 

	
 
So, back to the business:
 

	
 
1. Update the backup server configuration - each client needs to be explicitly
 
   registered:
 

	
 
   :file:`~/mysite/group_vars/backup.yml`
 
   ::
 

	
 
      backup_clients:
 
        - server: comms.example.com
 
          public_key: "{{ lookup('file', inventory_dir + '/ssh/comms.example.com.pub') }}"
 
          ip: 10.32.64.19
 
        - server: www.example.com
 
          public_key: "{{ lookup('file', inventory_dir + '/ssh/www.example.com.pub') }}"
 
          ip: 10.32.64.20
 
        # Ah, this is a bit interesting - we can back-up the backup server
 
        # itself! Don't worry, though, this is mainly so the logs and home
 
        # directories are preserved, so it won't take too much space ;)
 
        - server: bak.example.com
 
          public_key: "{{ lookup('file', inventory_dir + '/ssh/bak.example.com.pub') }}"
 
          ip: 127.0.0.1
 

	
 
2. And now to configure backup clients for all servers:
 

	
 
   :file:`~/mysite/group_vars/all.yml`
 
   ::
 

	
 
      enable_backup: yes
 
      backup_encryption_key: "{{ lookup('pipe', 'gpg2 --homedir ~/mysite/gnupg/ --armour --export-secret-keys ' + ansible_fqdn ) }}"
 
      backup_server: bak.example.com
 
      backup_server_host_ssh_public_keys:
 
        - "{{ lookup('file', inventory_dir + '/ssh/bak_dsa_key.pub') }}"
 
        - "{{ lookup('file', inventory_dir + '/ssh/bak_rsa_key.pub') }}"
 
        - "{{ lookup('file', inventory_dir + '/ssh/bak_ed25519_key.pub') }}"
 
        - "{{ lookup('file', inventory_dir + '/ssh/bak_ecdsa_key.pub') }}"
 
      backup_ssh_key: "{{ lookup('file', inventory_dir + '/ssh/' + ansible_fqdn) }}"
 

	
 
3. So, looking at the configuration up there, there is a couple of file lookups
 
   for getting the variable values, as well as one pipe lookup for fetching the
 
   encryption keys. For start, let's create the SSH private keys used for client
 
   log-ins to backup server::
 

	
 
     ssh-keygen -f ~/mysite/ssh/comms.example.com -N ''
 
     ssh-keygen -f ~/mysite/ssh/www.example.com -N ''
 
     ssh-keygen -f ~/mysite/ssh/bak.example.com -N ''
 

	
 
4. Next off, a GnuPG keyring needs to be populated with private keys that will
 
   be used for backup encryption and signing. In total, we need three keys, one
 
   for each server. The keys should not be encrypted, and they should be named
 
   after the respective server's FQDN. For simplicity sake, here is a nice
 
   copy-pastable batch version for doing so:
 

	
 
   .. note:: Key generation requires a lot of entropy. If you are running this
 
             command on a VM, you may want to ``apt-get install haveged`` to
 
             speed this up. Please do read up on haveged if deploying to a real
 
             system, though! Don't trust it blindly!
 

	
 
   ::
 

	
 
     chmod 700 ~/mysite/gnupg
 
     cat << EOF | gpg2 --homedir ~/mysite/gnupg --batch --gen-key
 
     Key-Type:RSA
 
     Key-Length:1024
 
     Name-Real:comms.example.com
 
     Expire-Date:0
 
     %commit
 

	
 
     Key-Type:RSA
 
     Key-Length:1024
 
     Name-Real:www.example.com
 
     Expire-Date:0
 
     %commit
 

	
 
     Key-Type:RSA
 
     Key-Length:1024
 
     Name-Real:bak.example.com
 
     Expire-Date:0
 
     %commit
 
     EOF
 

	
 
5. And... Apply::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
6. At this point the backup roles have been set-up everywhere, and the backups
 
   will be running every day at 02:00 in the morning. Of course, you may want to
 
   test out a backup yourself immediatelly by running the following command on
 
   servers::
 

	
 
     duply main backup
 

	
 
   .. note:: For more information on available commands and how to work with
 
             backup tool, please have look at `Official Duply Pages
 
             <http://duply.net/>`_.
 

	
 

	
 
Adding backup support to custom roles
 
-------------------------------------
 

	
 
As mentioned before, all of the supplied roles coming with *Majic Ansible Roles*
 
include backup support. What gets backed-up depends on the role implementation
 
(see role reference for details). What about backup support for custom roles?
 
This is something that has to be done by hand. However, it is quite simple to do
 
so.
 

	
 
Backup integration will be demonstrated with the previously implemented
 
``tbg`` role.
 

	
 
*The Bug Genie* stores most of its data in database, but thanks to the
 
``database`` role its backup is already handled for us. As a side-note, just
 
before every backup run the database is dumped and stored in location
 
``/srv/backup/tbg.sql``. That file is subsequently backed-up via *Duply* run.
 

	
 
What is not backed-up for us, though, are the files uploaded to *The Bug
 
Genie*. So let's fix that one.
 

	
 
1. Add the ``backup`` role to list of dependencies. Take note that while the
 
   ``backup_client`` role deals with basic set-up of backup client and its
 
   configuration, the ``backup`` role is used to define what should be
 
   backed-up. It is important to define unique filename for the backup patterns
 
   file. Take into account that you can use pretty much any globbing pattern
 
   supported by Duplicity.
 

	
 
   .. warning::
 

	
 
      Make sure the addition is properly aligned in the yaml file to previous
 
      role dependency definitions.
 

	
 
   :file:`~/mysite/roles/tbg/meta/main.yml`
 
   ::
 

	
 
        - role: backup
 
          when: enable_backup
 
          backup_patterns_filename: "tbg"
 
          backup_patterns:
 
            - "/var/www/tbg.example.com/files"
 

	
 
2. Apply the changes::
 

	
 
     workon mysite && ansible-playbook playbooks/site.yml
 

	
 
3. Now rerun the backup on server ``www.example.com`` (as root). If you haven't
 
   uploaded any files, you may want to do so before testing to make sure
 
   something is backed-up.
 

	
 
   ::
 

	
 
     duply main backup
 

	
 
4. Verify that the files have been backed-up:
 

	
 
   ::
 

	
 
      duply main list
 

	
 
.. note:: If you wanted to run a script prior to backup run, you would simply
 
          deploy a shell script with desired content to
 
          ``/etc/duply/main/pre.d/``. Just make sure the permissions for it are
 
          ok (it has to be executable by the root user).
 

	
 

	
 
Dealing with failures
 
---------------------
 

	
 
While the roles have been designed to be fairly robust, it should be taken into
 
account that certain handlers are used to bring the system into consistent
 
state. These handlers are mostly the ones dealing with service restarts, but
 
there are also a couple of handlers that take care of transforming certain data
 
into the required formats, import of files etc.
 

	
 
This means that failure to successfully execute such handlers could result in
 
inconsistent state on the server. Think of service configuration files being
 
updated, yet the service itself is not restarted and therefore continues to run
 
with the old configuration.
 

	
 
Handler execution failure can depend on a couple of things, including the loss
 
of SSH connectivity to managed machine, or some kind of unusual time-out during
 
handler execution.
 

	
 
To help handle this situation, Majic Ansible Roles all come with a special way
 
to invoke the handlers explicitly. Each role will include handlers as tasks,
 
provided that a special variable (``handlers``) is passed in to playbook run. To
 
make the run shorter, the handlers in such a run are also tagged with
 
``handlers``. This doubling of environment variable + tagging stems from current
 
limitations of Ansible (it is not possible to specify that certain task should
 
be run only if a tag is specified, therefore an additional variable has to be
 
used).
 

	
 
Handlers alone can be invoked specifically with command similar to::
 

	
 
  ansible-playbook -t handlers -e handlers=true playbooks/site.yml
 

	
 
The ``handlers`` variable is treated as boolean, and by default it is not set.
 

	
 

	
 
Where to go next?
 
-----------------
 

	
 
Well, those were some rather lengthy usage instructions, but hopefuly they are
 
useful. Things you might want to check-out next:
 

	
 
* :ref:`rolereference`
 
* :ref:`testsite`
 
* Finally, if it tickles your interest, have a look at role implementations
 
  themselves.
roles/wsgi_website/defaults/main.yml
Show inline comments
 
---
 

	
 
additional_nginx_config: {}
 
enforce_https: True
 
packages: []
 
rewrites: []
 
static_locations: []
 
use_paste: False
 
virtualenv_packages: []
 
environment_variables: {}
 
admin: "web-{{ fqdn | replace('.', '_') }}"
 
https_tls_certificate: "{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_https.pem') }}"
 
https_tls_key: "{{ lookup('file', tls_private_key_dir + '/' + fqdn + '_https.key') }}"
 
gunicorn_version: "19.6.0"
 
futures_version: "3.0.5"
 
website_mail_recipients: "root"
 
environment_indicator: null
 
\ No newline at end of file
 
environment_indicator: null
 
proxy_headers: {}
roles/wsgi_website/templates/nginx_site.j2
Show inline comments
 
{% if enforce_https -%}
 
server {
 
    # HTTP (plaintext) configuration.
 
    listen 80;
 
    server_name {{ fqdn }};
 

	
 
    # Redirect plaintext connections to HTTPS
 
    return 301 https://$host$request_uri;
 
}
 

	
 
{% endif -%}
 
server {
 
    # Base settings.
 
    root {{ home }}/htdocs/;
 
    server_name {{ fqdn }};
 
{% if not enforce_https %}
 

	
 
    # HTTP (plaintext) configuration.
 
    listen 80;
 

	
 
{% endif %}
 
    # HTTPS (TLS) configuration.
 
    listen 443 ssl;
 
    listen [::]:443 ssl;
 
    ssl_certificate_key /etc/ssl/private/{{ fqdn }}_https.key;
 
    ssl_certificate /etc/ssl/certs/{{ fqdn }}_https.pem;
 

	
 
{% if default_enforce_https -%}
 
    # Set-up HSTS header for preventing downgrades for users that visited the
 
    # site via HTTPS at least once.
 
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
 
{% endif -%}
 

	
 
    {% for config in additional_nginx_config -%}
 
    # {{ config.comment }}
 
    {{ config.value }}
 
    {% endfor -%}
 

	
 
    {% if rewrites -%}
 
    # Site rewrites.
 
    {% for rewrite in rewrites -%}
 
    rewrite {{ rewrite }};
 
    {% endfor -%}
 
    {% endif %}
 

	
 
    {% if static_locations -%}
 
    # Static locations
 
    {% for location in static_locations -%}
 
    location {{ location }} {
 
        try_files $uri $uri/ =404;
 
    }
 
    {% endfor -%}
 
    {% endif %}
 

	
 
    # Pass remaining requests to the WSGI server.
 
    location / {
 
        try_files $uri @proxy_to_app;
 
    }
 

	
 
    location @proxy_to_app {
 
        proxy_set_header X-Forwarded-Proto $scheme;
 
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
        proxy_set_header Host $http_host;
 
        proxy_redirect off;
 

	
 
    {% for header, value in proxy_headers.iteritems() -%}
 
    proxy_set_header {{ header }} {{ value }};
 
    {% endfor -%}
 

	
 
        proxy_pass http://unix:/run/wsgi/{{ fqdn }}.sock;
 
    }
 

	
 
    {% if environment_indicator -%}
 
    # Show environment indicator on HTML pages.
 
    sub_filter_types text/html;
 
    sub_filter_once on;
 
    sub_filter "</body>" "<div id='website-environment' style='background-color: {{ environment_indicator.background_colour }}; width: 100%; text-align: center; position: fixed; bottom: 5px; color: {{ environment_indicator.text_colour }}; font-weight: bold;'>{{ environment_indicator.text }}</div></body>";
 
    {% endif -%}
 

	
 
    access_log /var/log/nginx/{{ fqdn }}-access.log;
 
    error_log /var/log/nginx/{{ fqdn }}-error.log;
 
}
testsite/group_vars/web.yml
Show inline comments
 
---
 

	
 
local_mail_aliases:
 
  root: "root john.doe@{{ testsite_domain }}"
 

	
 
smtp_relay_host: mail.{{ testsite_domain }}
 

	
 
smtp_relay_truststore: "{{ lookup('file', inventory_dir + '/tls/ca.pem') }}"
 

	
 
default_https_tls_key: "{{ lookup('file', inventory_dir + '/tls/web.' + testsite_domain + '_https.key') }}"
 
default_https_tls_certificate: "{{ lookup('file', inventory_dir + '/tls/web.' + testsite_domain + '_https.pem') }}"
 

	
 
web_default_title: "Welcome to Example Inc."
 
web_default_message: "You are attempting to access the web server using a wrong name or an IP address. Please check your URL."
 

	
 
db_root_password: "root"
 

	
 
website_mail_recipients: "john.doe@example.com"
 

	
 
environment_indicator:
 
  background_colour: "purple"
 
  text_colour: "white"
 
  text: "Majic Ansible Roles Test Site"
 
\ No newline at end of file
 
  text: "Majic Ansible Roles Test Site"
 

	
 
proxy_headers:
 
  Accept-Encoding: '"gzip"'
testsite/playbooks/roles/wsgihello/files/hello.wsgi
Show inline comments
 
#!/usr/bin/env python
 

	
 
import os
 

	
 
def application(environ, start_response):
 
    status = '200 OK'
 

	
 
    template = """<!DOCTYPE html>
 
<html lang="en">
 
  <head>
 
    <meta charset="utf-8">
 
    <title>{title}</title>
 
  </head>
 
  <body>
 
    <h1>Hello, world!</h1>
 
    <p>I am website {title}</p>
 
    <p>Accept-Encoding header was set to {acceptencoding}</p>
 
  </body>
 
</html>
 
"""
 
    output = template.format(title=os.environ.get("WEBSITE_NAME", "that nobody set a name for :("))
 
    output = template.format(title=os.environ.get("WEBSITE_NAME", "that nobody set a name for :("),
 
                             acceptencoding=environ.get("HTTP_ACCEPT_ENCODING"))
 

	
 
    response_headers = [('Content-type', 'text/html'),
 
                        ('Content-Length', str(len(output)))]
 
    start_response(status, response_headers)
 

	
 
    return [output]
0 comments (0 inline, 0 general)