---

- name: Debug tasks
  block:
    - ansible.builtin.debug:
        msg: I always display!
    - ansible.builtin.debug:
        msg: I only display with 'ansible-playbook -vvv' or with more 'v's
        verbosity: 3
    - debugger: on_failed
      ansible.builtin.fail:
      msg: Manual, enforced failure
      # print all variables at this point => p task_vars
      # continue => c
      # abort and quit => q

- name: Flush handlers
  ansible.builtin.meta: flush_handlers

- name: Retry tasks
  ansible.builtin.command: /usr/bin/false
  retries: 3
  delay: 3
  register: command_result
  until: command_result is not failed

- name: Run tasks locally
  delegate_to: 127.0.0.1  # 'localhost' works too
  ansible.builtin.command: hostname

- name: Assertions
  vars:
    installation_method: package
    url: https://www.google.com/
  ansible.builtin.assert:
    that:
      - installation_method in ['container', 'package']
      - "'https://www.google.com/' is ansible.builtin.url"
      - "'domain.example.com' is community.general.fqdn_valid(min_labels=2)"
      - url is regex('\w\.com/')
    fail_msg: What to say if any of the above conditions fail
    success_msg: What to say if all of the above conditions succeed

- name: Test types
  ansible.builtin.assert:
    that:
      # strings are classified as 'string', 'iterable' and 'sequence', but not 'mapping'
      - "'aa' is string"
      - "'aa' is iterable"
      - "'aa' is sequence"
      # numbers are classified as 'numbers', with 'integer' and 'float' being subclasses
      - 42 is number and 5 is integer
      - 21.34 is number and 12.1 is float
      # lists are classified as 'iterable' and 'sequence', but not as 'string' nor 'mapping'
      - "['list'] is iterable"
      - "['list'] is sequence"
      # dictionaries are classified as 'iterable', 'sequence' and 'mapping', but not as 'string'
      - "{'a': 'dict'} is iterable"
      - "{'a': 'dict'} is sequence"
      - "{'a': 'dict'} is mapping"
      # native booleans
      - true is boolean
      - True is boolean
      - false is boolean
      - False is boolean

- name: Type conversion
  ansible.builtin.assert:
    that:
      - "'string' | int is integer"
      - "'string' | float is float"
      - 12 | float is float
      - 21.02 | int is integer
      - 43 | string is string
      - 74.93 | string is string
      - 4 | bool is boolean

- name: Elvis operator
  # (condition) | bool | ternary(value_for_true_condition, value_for_false_condition, optional_value_for_null_condition)
  ansible.builtin.set_fact:
    acme_directory: >-
      {{
        this_is_a_test_run
        | default(true)
        | bool
        | ternary(
          'https://acme-staging-v02.api.letsencrypt.org/directory',
          'https://acme-v02.api.letsencrypt.org/directory'
        )
      }}

- name: Create directories recursively
  ansible.builtin.file:
    path: /tmp/path/to/final/dir
    state: directory

- name: Define files content in tasks
  ansible.builtin.copy:
    dest: "{{ ansible_user_dir }}/.tmux.conf"
    mode: u=rw,go=r
    content: |
      …

- name: Show input data type
  ansible.builtin.set_fact:
    should_be_string: "{{ 'this' | type_debug }}"

- name: Pretty print information
  ansible.builtin.debug:
    msg: >-
      {{
        dict([
          [ 'install_method', install_method ],
          [ 'install_method in supported_install_methods', install_method in supported_install_methods ],
        ])
      }}

- name: Use filters
  tags: filter
  ansible.builtin.set_fact:
    path_list_of_all_txt_files_in_dir: "{{ lookup('ansible.builtin.fileglob', '/my/path/*.txt') }}"

- name: Import tasks
  block:
    - name: By using absolute paths and special variables (preferred)
      ansible.builtin.import_tasks:
        file: "{{ role_path }}/tasks/install/{{ install_method }}.yml"
    - name: By using paths relative to the including file
      ansible.builtin.import_tasks:
        file: pre-flight.yml

- name: Conditionally include tasks
  block:
    - name: by leveraging the 'with_fileglob' loop filter (preferred)
      ansible.builtin.include_tasks:
        file: "{{ item }}"
      with_fileglob: "{{ install_method }}.yml"
    - name: by checking the files' existence
      vars:
        filename: "{{ install_method }}.yml"
      when: lookup('ansible.builtin.fileglob', filename) != []
      ansible.builtin.import_tasks:
        file: "{{ filename }}"

- name: Generate passwords
  block:
    - name: Randomly
      ansible.builtin.debug:
        msg: "{{ lookup('ansible.builtin.password', '/dev/null') }}"
    - name: Specifying requirements
      ansible.builtin.debug:
        msg: "{{ lookup('ansible.builtin.password', '/dev/null length=32 chars=ascii_letters,digits,punctuation') }}"
    - name: Random but idempotent, so it will not change at every execution
      ansible.builtin.debug:
        msg: "{{ lookup('ansible.builtin.password', '/dev/null', seed=inventory_hostname) }}"

- name: Add repositories
  block:
    - name: DNF/YUM
      ansible.builtin.yum_repository:
        name: epel
        description: EPEL YUM repo
        baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/

- name: Install packages
  block:
    - name: Generic module
      ansible.builtin.package:
        name:
          - tmux
          - screen
    - name: Via PIP
      ansible.builtin.pip:
        name:
          - bottle
          - django>1.11.0,<1.12.0
          - svn+http://myrepo/svn/MyApp#egg=MyApp
          - git+http://myrepo/app/MyApp
          - file:///path/to/MyApp.tar.gz

- name: Run containers
  block:
    - name: Directly
      community.docker.docker_container:
        name: gitlab
        image: gitlab/gitlab-ce:16.11.2-ce.0
        hostname: gitlab.lan
        published_ports:
          - "8022:22"
          - "8080:80"
          - "8443:443"
        env:
          GITLAB_OMNIBUS_CONFIG: >-
            external_url 'http://gitlab.lan';
        shm_size: 256m
        volumes:
          - ./config:/etc/gitlab:Z
          - ./logs:/var/log/gitlab:Z
          - ./data:/var/opt/gitlab:Z
        auto_remove: true
    - name: With Compose
      community.docker.docker_compose_v2:
        project_src: /home/user/flask

- name: Manipulate strings
  ansible.builtin.set_fact:
    string_with_first_letter_to_uppercase: "{{ 'all_lowercase' | capitalize }}"
    string_with_something_replaced: "{{ 'dots.to.dashes' | replace('.','-') }}"
    split_string: "{{ 'testMe@example.com' | split('@') | first }}"
    string_with_pattern_replaced: >-
      {{ '*.domain.com...' | regex_replace('*' | regex_escape, 'star') | regex_replace('\.+$', '') }}

- name: Manipulate lists
  block:
    - name: Add elements to lists
      vars:
        programming_languages:
          - C
          - Python
      ansible.builtin.set_fact:
        programming_languages: "{{ programming_languages + ['Ruby'] }}"
    - name: Remove elements from lists
      vars:
        dbs_list: ['primary', 'sales']
      ansible.builtin.set_fact:
        list_without_items: "{{ dbs_list | difference(['template0','template1','postgres','rdsadmin']) }}"
    - name: Get a random element
      ansible.builtin.set_fact:
        random_item: "{{ ['a','b','c'] | random }}"
    - name: Sort dict elements in list by attribute
      tags: order_by
      vars:
        snapshots:
          - name: sales
            create_time: '2024-06-25T00:52:55.127000+00:00'
          - name: test
            create_time: '2024-05-17T01:53:12.103220+00:00'
      ansible.builtin.set_fact:
        snapshot_latest: "{{ snapshots | sort(attribute='create_time') | last }}"
    - name: Give back the first not null value (coalesce-like)
      vars:
        list_with_null_values:
          - null
          - null
          - something
          - something else
      ansible.builtin.set_fact:
        first_non_null_value: "{{ list_with_null_values | select | first }}"
    - name: Get values for a specific attribute in a list of dictionaries
      ansible.builtin.set_fact:
        vpc_security_group_ids: >-
          {{ instance_information.vpc_security_groups | map(attribute='vpc_security_group_id') }}
        volume_ids: "{{ instances_information.instances[0].block_device_mappings | map(attribute='ebs.volume_id') }}"
    - name: Return only elements with specific attributes matching a filter
      ansible.builtin.set_fact:
        available_rds_snapshots: "{{ snapshots_list | selectattr('status', 'equalto', 'available') }}"
        mounts_with_path: "{{ ansible_facts.mounts | selectattr('mount', 'in', path) }}"
    - name: Return all elements *but* the ones with specific attributes matching a filter
      ansible.builtin.set_fact:
        available_rds_snapshots: "{{ snapshots_list | rejectattr('status', 'equalto', 'creating') }}"
        mounts_without_path: "{{ ansible_facts.mounts | rejectattr('mount', 'in', path) }}"
    - name: Remove lines about RDS protected users and permissions from a dump file
      # remove empty lines
      # remove comments
      # remove creation of the master user
      # remove anything involving 'rdsadmin'
      # remove changes to protected RDS users
      # remove protected 'superuser' and 'replication' assignments
      vars:
        # **Hack notice**: Ansible has issues with splitting on new lines if this template is quoted differently
        permissions_dump_content_as_lines: "{{ dump_file.content | ansible.builtin.b64decode | split('\n') }}"
        master_username: postgresql
      ansible.builtin.set_fact:
        permissions_commands: >-
          {{
            permissions_dump_content_as_lines
            | reject('match', '^$')
            | reject('match', '^--')
            | reject('match', '^CREATE ROLE ' + master_username)
            | reject('match', '.*rdsadmin.*')
            | reject('match', '^(CREATE|ALTER) ROLE rds_')
            | map('regex_replace', '(NO)(SUPERUSER|REPLICATION)\s?', '')
          }}

- name: Manipulate dictionaries
  vars:
    organization:
      address: 123 common lane
      id: 123abc
  block:
    - name: Add keys to dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | combine({ 'name': 'ExampleOrg' }) }}"
    - name: Sort keys in dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | dictsort }}"
    - name: Pretty print dictionaries
      ansible.builtin.set_fact:
        organization: "{{ organization | to_nice_json }}"
    - name: Merge dictionaries
      vars:
        dict_1:
          a: 43
          b: some string
        dict_2:
          y: true
          z:
            - 4
            - test
      ansible.builtin.set_fact:
        merged_dict: "{{ dict1 | ansible.builtin.combine(dict_2, {'z':'new_value','w':[44]}) }}"
        recursively_merged_dict: >-
          {{ {'rest':'test'} | ansible.builtin.combine({'z':'new_value','w':[44]}, dict_1, dict_2, recursive=true) }}
    - name: Register the list of extensions per DB
      vars:
        db_extensions: {}
      ansible.builtin.set_fact:
        db_extensions: >-
          {{
            db_extensions
            | combine({
                item.item: item.query_result | map(attribute='extname')
            })
          }}
      with_items: "{{ db_extensions_query.results }}"
    - name: Register the list of extensions per DB as 'db:extensions[]' pairs
      vars:
        db_extensions:
          sales:
            - pgaudit
            - plpgsql
          countries:
            - pgcrypto
            - postgis
            - pg_stat_statements
      ansible.builtin.set_fact:
        db_extension_pairs:
          # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's reason
          >-
            {%- set ns = namespace(output = []) -%}
            {%- for db in db_extensions.keys() -%}
              {%- for extension in db_extensions[db] -%}
                {{- ns.output.append({'db':db, 'extension': extension}) -}}
              {%- endfor -%}
            {%- endfor -%}
            {{- ns.output -}}
    - name: Get the device name and last snapshot id for all block devices in an EC2 instance
      # Useful to create AMIs from instance snapshots
      tags:
        - aws
        - ec2
        - snapshot
        - ami
      ansible.builtin.set_fact:
        last_snap_for_device:
          # Refer https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for the namespace object's reason
          >-
            {%- set ns = namespace(devices_list = []) -%}
            {%- for result in current_instance_snapshots.results -%}
            {%-   for device in current_instance_information.instances[0].block_device_mappings
                  | selectattr('ebs.volume_id', 'equalto', result.volume_id) -%}
            {{-
                    ns.devices_list.append({
                      'device_name': device.device_name,
                      'snapshot_id': result.snapshots | sort(attribute='start_time') | last | json_query('snapshot_id'),
                    })
            -}}
            {%-   endfor -%}
            {%- endfor -%}
            {{  ns.devices_list }}

- name: Use the users' home directory for something
  block:
    - name: Executing commands from specified users
      block:
        - name: Get users' homedir back
          become: true
          become_user: "{{ item }}"
          become_flags: -iH
          check_mode: false
          ansible.builtin.command: >-
            echo "{{ item }}: $HOME"
          changed_when: false
          with_items:
            - root
            - ec2-user
          register: users_homedir_retrieve
        - name: Compute and register the results
          tags: AnsibleUnsafeText_to_Dict
          ansible.builtin.set_fact:
            users_homedir: >-
              {{
                users_homedir_retrieve
                | community.general.json_query('results[].stdout')
                | map('from_yaml')
                | combine
              }}
        - name: Do your thing!
          become: true
          become_user: "{{ item.key }}"
          ansible.builtin.file:
            path: "{{ item.value }}/placeholder"
            state: touch
          with_dict: "{{ users_homedir }}"
    - name: From the system's entries
      block:
        - name: Get raw information from the system's entries
          ansible.builtin.getent:
            database: passwd
            key: "{{ item }}"
            split: ':'
          with_items:
            - root
            - ec2-user
          register: users_entries
        - name: Compute and register the results
          ansible.builtin.set_fact:
            users_info: >-
              {{
                users_entries
                | community.general.json_query('results[].ansible_facts.getent_passwd[]')
                | combine
              }}
        - name: Do your thing!
          ansible.builtin.file:
            path: "{{ item.value[4] }}/placeholder"
            owner: "{{ item.key }}"
            state: touch
          with_dict: "{{ users_info }}"

- name: Cronjobs
  block:
    - name: At specific times
      become: true
      ansible.builtin.cron:
        name: Prometheus manual data backup
        cron_file: prometheus-manual-data-backup

        # Mind this is based on the hosts' time.
        hour: 4
        minute: 0

        user: root
        job:
          # - Keep '%' characters escaped or they'll be treated as newlines.
          # - Archive creation returns 1 if it detects changes to read files.
          #   Using ';' instead of '&&' to ignore.
          >
            FILENAME="/tmp/prometheus-data-$(date +'\%s-\%F-\%H-\%m-\%S').tar.gz"
            && tar -czf "$FILENAME" '/var/lib/prometheus/data'
            ; tar -tf "$FILENAME" > '/dev/null'
            && aws s3 cp "$FILENAME" 's3://backups/prometheus/'
            && rm "$FILENAME"

- name: Error handling in blocks
  block:
    - name: This executes normally
      ansible.builtin.debug:
        msg: I execute normally
    - name: This errors out
      ansible.builtin.command: /bin/false
    - name: This never executes
      ansible.builtin.debug:
        msg: I never execute due to the above task failing
  rescue:
    - name: This executes if any errors arose in the block
      ansible.builtin.debug:
        msg: I caught an error and can do stuff here to fix it
  always:
    - name: This always executes
      ansible.builtin.debug:
        msg: I always execute

- name: Integrate Ansible Vault
  tags: ansible_vault
  block:
    - name: Use encrypted values
      ansible.builtin.set_fact:
        var_from_encrypted_value:
          # password: '1q2w3e4r', plaintext value: 'very secret string'
          !vault |
            $ANSIBLE_VAULT;1.1;AES256
            34646464653830386631363430386432666530356364313532313336373665613038633464376335
            3539363530613130623638313063363165386230646566640a313438386133366137383939336637
            33333365393337326239336264623462373064383663363234353635316538356461353061646563
            3037306464363439340a663430313739393439363936613862316361353330363638323065383063
            39613935613035343637336537643266313737666635313730353034373736353736
    - name: Use encrypted files
      # The 'unvault' filter requires files to exist beforehand, but it is fine for them to be plaintext. \_(-_-)_/
      tags: tls_certificate
      ansible.builtin.copy:
        dest: /etc/haproxy/certificate.pem
        content: |
          {{ lookup('ansible.builtin.unvault', 'path/to/cert/key.pem') | string | trim }}
          {{ lookup('ansible.builtin.unvault', 'path/to/cert/full_chain.pem') | string | trim }}
    - name: Save data to encrypted files
      # Of fu*king course the 'vault' filter would use the 'filter_default' vault ID by default to encrypt content.
      # Set that parameter to '' to *not* specify a vault ID.
      vars:
        ansible_vault_password: >-
          {{ lookup('ansible.builtin.file', [playbook_dir, 'ansible_vault_password_file.txt'] | path_join) }}
      ansible.builtin.copy:
        dest: path/to/file
        decrypt: false  # necessary if the file does not exist beforehand
        content: "{{ 'some string' | ansible.builtin.vault(ansible_vault_password, vault_id='') }}"

- name: AWS
  tags: aws
  block:
    - name: Get current IP ranges
      # too many to be put into security group rules
      ansible.builtin.set_fact:
        ip_ranges: >-
          lookup('url', 'https://ip-ranges.amazonaws.com/ip-ranges.json', split_lines=False)
          | from_json
          | json_query('prefixes')
          | selectattr('region', 'equalto', 'eu-west-1')
          | selectattr('service', 'equalto', 'AMAZON')
          | map(attribute='ip_prefix')
    - name: Assume roles
      block:
        - name: Get session tokens
          amazon.aws.sts_assume_role:
            access_key: AKIA1EXAMPLE1EXAMPLE                      # optional if defined as environment variable
            secret_key: 123456789abcdefghijklmnopqrstuvwxyzABCDE  # optional if defined as environment variable
            profile: someProfile                                  # optional if defined as environment variable
            role_arn: "arn:aws:iam::123456789012:role/someRole"
            role_session_name: someRoleSession
          register: assumed_role
        - name: Use the assumed role to take action
          amazon.aws.ec2_tag:
            access_key: "{{ assumed_role.sts_creds.access_key }}"
            secret_key: "{{ assumed_role.sts_creds.secret_key }}"
            profile: null  # required to use the assumed role's token, if profile is specified via environment variable
            session_token: "{{ assumed_role.sts_creds.session_token }}"
            resource: i-xyzxyz01
            tags:
              MyNewTag: value
    - name: EC2
      block:
        - name: Get running instances with 'K8S' as the 'Application' tag
          amazon.aws.ec2_instance_info:
            filters:
              "tag:Application": K8S
              instance-state-name: ["running"]
        - name: Clone EC2 instances
          vars:
            source_instance_id: i-0123456789abcdef0
          block:
            - name: Get instance information from the original instance
              amazon.aws.ec2_instance_info:
                instance_ids:
                  - "{{ source_instance_id }}"
              register: source_instance_info
            - name: Create an AMI of the original instance
              amazon.aws.ec2_ami:
                instance_id: "{{ source_instance_id }}"
                no_reboot: true     # remove if the instance rebooting upon AMI creation is no biggie
                wait: true
                wait_timeout: 3600  # big volumes call for bit wait times (a 200GiB volume took )
                name: ami-source
              register: source_ami
            - name: Use the AMI to launch clones identical to the original
              when: source_ami.image_id is defined
              amazon.aws.ec2_instance:
                name: clone
                vpc_subnet_id: "{{ source_instance_info.instances[0].subnet_id }}"
                instance_type: "{{ source_instance_info.instances[0].instance_type }}"
                image:
                  id: "{{ source_ami.image_id }}"
    - name: RDS
      block:
        - name: Create an instance's snapshot
          block:
            - name: Create the snapshot
              amazon.aws.rds_instance_snapshot:
                db_instance_identifier: identifier-for-db-instance
                db_snapshot_identifier: identifier-for-db-snapshot
              register: snapshot_creation
            - name: Wait for the snapshot to be in the 'available' state
              when: snapshot_creation.snapshot_create_time is defined
              amazon.aws.rds_snapshot_info:
                db_snapshot_identifier: "{{ snapshot_creation.db_snapshot_identifier }}"
              register: snapshot_check
              retries: 3
              delay: 120
              until: snapshot_check.snapshots | selectattr('status', 'equalto', 'available') | length > 0
        - name: Dump roles' privileges
          block:
            - name: Dump to file
              environment:
                PGPASSWORD: someRandomString
              vars:
                out_file: /tmp/instance-id_roles.sql
              ansible.builtin.command: >-
                pg_dumpall
                --host 'instance-id.0123456789ab.eu-west-1.rds.amazonaws.com' --port '5432'
                --user 'postgres' --database 'postgres' --no-password
                --roles-only --no-role-passwords
                --file '{{ out_file }}'
              changed_when: false
            - name: Dump to variable for later use through 'dump_execution.stdout_lines'
              environment:
                PGPASSWORD: someRandomString
              ansible.builtin.command: >-
                pg_dumpall
                -h 'instance-id.0123456789ab.eu-west-1.rds.amazonaws.com' -p '5432'
                -U 'postgres' -l 'postgres' -w
                -r --no-role-passwords
              changed_when: false
              register: dump_execution
        - name: Wait for pending changes to be applied
          amazon.aws.rds_instance_info:
            db_instance_identifier: identifier-for-db-instance
          register: instance_check
          retries: 12
          delay: 15
          until: instance_check.instances[0].pending_modified_values.keys() | length == 0
    - name: Wait for AWS to realize some requests have been made
      ansible.builtin.pause:
        seconds: 60

- name: Send messages to Slack channels
  vars:
    slack_notification_hook_url: https://hooks.slack.com/services/AB01CD23EF4/ABCD0123E/aBcDefGh0123456789iJKLmn
  block:
    - name: Send plain messages
      ansible.builtin.uri:
        url: "{{ slack_notification_hook_url }}"
        method: POST
        body_format: json
        body:
          text: (╥╯ᗝ╰╥) task XYZ failed
    - name: Send mrkdwn (Slack-specific markdown) text
      # FIXME: still to be tested
      ansible.builtin.uri:
        url: "{{ slack_notification_hook_url }}"
        method: POST
        body_format: json
        body:
          blocks:
            - type: section
              text:
                type: mrkdwn
                text: This is a *_fancy_* message

- name: AWX
  environment:
    CONTROLLER_HOST: https://awx.example.org/
    CONTROLLER_VERIFY_SSL: false
    CONTROLLER_USERNAME: admin
    CONTROLLER_PASSWORD: somethingSecret
  block:
    - name: Export all data from existing instances
      # At the time of writing: applications, credential_types, credentials, execution_environments, inventory,
      # inventory_sources, job_templates, notification_templates, organizations, projects, schedules, teams, and users.
      awx.awx.export:
        all: true
      register: awx_export_output

- name: Let's Encrypt
  # The 'acme_certificate' module takes in file paths for the certificate's files; those need either to *not* exist
  # beforehand, or their content to be in specific formats.
  block:
    - name: Revoke test certificates with account key
      community.crypto.acme_certificate_revoke:
        acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
        acme_version: 2
        account_key_src: path/to/acme_account.key.pem
        certificate: path/to/certificate.crt.pem
