This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Ansible: Repository Hierarchy

Tip

This document describes details about Ansible being used to manage installations of Tocco. For documentation about managing servers via Ansible, have a look at the docs/ directory in the Ansible repository.

Tip

Setup instructions can be found in Setup Ansible.

Repository

The Ansible configuration is stored in a Git repository in the /tocco directory. The root directory, /, is used for server management.

Overview of the repository structure:

tocco
    │
    ├── config.yml                                # Definition of existing installations
    │                                             # and parameterization.
    │
    ├── filter_plugins                            # Filter plugins for use with Jinja2.
    │   ├── crypto.py                             #
    │   ├── format.py                             # For instance, in {{ "a_string"|to_camelcase }},
    │   └── parse.py                              # `to_camelcase` is the filter
    │
    ├── global.yml                                # Global variables
    │
    ├── inventory.py                              # Script used to parse config.yml/global.yml and
    │                                             # convert it to a proper Ansible Inventory
    │
    ├── library                                   # Custom Ansible module
    │   ├── backoffice_installation.py            #
    │   ├── cloudscale_s3.py                      # Example showing module defined in teamcity_parameters
    │   ├── teamcity_parameters.py                # being used:
    │   ├── teamcity_project.py                   #
    │   └── vshn_openshift.py                     #     - name: TeamCity - set parameter
    │                                             #       teamcity_parameters:
    │                                             #         user: ansible
    │                                             #         password: '{{ secret }}'
    │                                             #         id: ProjectId
    │                                             #         params:
    │                                             #           branch: master
    │
    ├── playbook.yml                              # Starting point defining which roles
    │                                             # to execute for which installation.
    │
    ├── roles
    │   └── tocco
    │       ├── files                             # Files used in tasks
    │       │   └── history_db.sql                #
    │       │
    │       ├── tasks                             # Instructions for how to setup and configure
    │       │   ├── database.yml                  # installations.
    │       │   ├── mail_domains.yml              #
    │       │   ├── main.yml                      # `main.yml` the starting point and everything
    │       │   ├── route.yml                     # else is included from there as needed.
    │       │   └── teamcity.yml                  #
    │       │
    │       └── templates                         # Templates using the Jinja2 templating
    │           ├── deploymentconfig_nice.yml     # language. This templates are used
    │           └── rolebinding_ansible_edit.yml  # within tasks.
    │
    ├── secrets2.yml                              # Ansible Vault containing passwords
    │                                             # and other secrets in encrypted form.
    │
    └── test_plugins                              # Custom test for use in Jinja2
        └── basics.py                             #
                                                  # For instance, in {{ 5 is even }},
                                                  # `even` is the test.

Configuration (config.yml/global.yml)

Structure

global.yml:

# Global variables

db_server: db1.tocco.cust.vshn.net
s3_endpoint: https://objects.cloudscale.ch

config.yml:

abc:                                        # Customer "abc"

  s3_bucket: nice-abc                       # Customer variables for "abc"
  mail_relay: mxout1.tocco.ch               #

  installations:
    abc:                                    # Installation "abc"

      db_name: nice_abc                     # Installation variables for "abc"
      solr_core: nice-abc

    abctest:                                # Installation "abctest"

      db_name: nice_test                    # Installation variables for "abctest"
      solr_core: nice-test                  #

Variable Precedence

Variables from highest to lowest priority. Higher priority precedes lower priority:

  • Installation variables

  • Customer variables

  • Global variables

Example:

global.yml:

db_server: db1.tocco.ch

config.yml:

abc:
  db_server: db2.tocco.ch
  abc:                          # <= db_server is "db3.tocco.ch"
    db_server: db3.tocco.ch
  abctest:                      # <= db_server is "db2.tocco.ch"
xyz:
  xyz:                          # <= db_server is "db1.tocco.ch"
  xyztest:                      # <= db_server is "db4.tocco.ch"
    db_server: db4.tocco.ch

Merge Variables

By default, variables are replaced rather than merged:

Example, merging dictionaries

global.yml:

env:
  nice2.request.limit: '1000'

config.yml:

abc:
  env:
    nice2.history.enabled: 'true'
  abc:
    env:
      nice2.pool_name: 'test'
  abctest:

In the above example, the result will be:

Installation

Resulting Value

abc

env:
  nice2.pool_name: 'test'

abctest

env:
  nice2.history.enabled: 'true'

This behavior can be changed using the !merge type:

global.yml:

env:
  nice2.request.limit: '1000'

config.yml:

abc:
  env: !merge
    nice2.history.enabled: 'true'
  abc:
    env: !merge
      nice2.pool_name: 'test'
      nice2.request.limit: null
  abctest:
    env: !merge
      nice2_request.limit: '2000'

In the above example, the result will be:

Installation

Resulting Value

abc

env:
  nice2.history.enabled: 'true'
  nice2.pool_name: 'test'

  # setting the value to null removes the item
  # nice2.request.limit: null

abctest

env:
  nice2.history.enabled: 'true'
  nice2_request.limit: '2000'

Tip

In addition to null the string "__null__" can be used to remove a value. Using this string may be required in Jinja2 expressions as there null can’t be used:

env:
  nice2.history.enabled: "{{ '__null__' if condition else false }}"

Example, merging lists

global.yml:

mail_allowed_recipients:
- tocco.ch
- frank@example.com

config.yml:

abc:
  mail_allowed_recipients: !merge
  - fritz@example.com
  - frank@example.com
  abc:
    mail_allowed_recipients: !merge
    - example.net
  abctest:

In the above example, the result will be:

Installation

Resulting Value

abc

mail_allowed_recipients:
- tocco.ch
- frank@example.com
- fritz@example.com
# - frank@example.com  # duplicates are ignored
- example.net

abctest

mail_allowed_recipients:
- tocco.ch
- frank@example.com
- fritz@example.com

Limitations:

This is only implemented for dictionaries and lists defined directly on the customer or installation.

Implementation:

The !merge type is implemented within the inventory script (tocco/inventory.py). It handles merging the dictionaries and lists, and hands the variables over to Ansible afterwards.

Templating with Jinja2

The templating language Jinja2 can be used in variables as well as on templates and in tasks.

Documentation:

Example:

global.yml:

db_name: nice_{{ installation_name }}
history_db_name: '{{ db_name }}_history'
db_server: |-
  {% if location == 'blue' -%}
  db1.blue.tocco.ch
  {%- else -}
  db1.red.tocco.ch
  {%- endif %}

config.yml:

abc:                                            # <= db_name is "nice_abc"
  location: red                                 #    db_server is "db1.red.tocco.ch"
                                                #    history_db_name is "nice_abc_history"

abctest:                                        # <= db_name is "NICE2_ABCTEST"
  db_name: NICE2_{{ installation_name|upper }}  #    db_server is "db1.blue.tocco.ch"
  location: blue                                #    history_db_name is "NICE2_ABCTEST_history"

Evaluation:

Junja2 templates are evaluated for every installation independently. Thus, {{ installation_name }} always correspond to the name of the installation being processed.

Also, all expressions and statements are only evaluated when used. Thus, when setting these variables …:

is_production: "{{ not is_test }}"
is_test: "{{ installation_name.endswith('test') }}"
db_user: "{% if location == 'nine' %}{{ installation_name }}_user{% else %}{{ installation_name }}{% endif %}"

… they are not evaluated until used. Here for instance by passing them to the debug module:

- name: print debug info
  debug:
    msg: '{{ db_user }}'
  when: is_production

{{ not is_test }}, {{ installation_name.endswith('test') }} and {% if location == 'nine' %}…{% endif %}, defined in the variables above, are only evaluated now, and will be evaluated again when used again. Consequently, the variables installation_name, location and is_test used in the expressions/statements can be referenced before they exist. This delayed evaluation is used extensively throughout the Ansible playbooks. It allows the use of global, customer, installation and run time variables without having to worry whether they have been set at that point.

Special variables:

A bunch of special variables are set transparently based on the definitions in config.yml and can be used anywhere in a playbook. These variables are set by the inventory script (inventory.py).

customer_name

The customer to which the installation belongs.

installation_name

Name of the installation.

sibling_installations

Names of all other installations belonging to the same customer.

Ansible itself has built-in special variables that can be used too.

Ansible does not understand the concept of customers or installations. For Ansible to be able make sense of it, installations are translated to hosts and customers to groups. This means, for instance, hostvars, contains the variables belonging to all installations and groups contains the names of all customers.

Hint

In Yaml, quotes have to be used for any value starting with {{:

db_server:  {{ var }}       # Invalid, the first { is consider a start of
                            # dictionary by Yaml.

db_server: '{{ var }}'      # ok