------------------------------------------------------------------------------- Ansible Yaml Code First understand YAML.. https://antofthy.gitlab.io/data/yaml.txt Playbook structure project/main.yml group_vars/all.yml # \ group_vars/{group}.yml # > it is NOT "vars/" in project host_vars/{fqdn}.yml # | host_vars/{fqdn}/{name}.yml # / templates/{name}.j2 roles/{name}/tasks/main.yml # just the tasks defaults/main.yml # default variables for role vars/mail.yml # variables ofr playbook files/{filename} # files for uploading templates/{name}.j2 # templates to craet files handlers/main.yml In a playbook yaml file (in sequence)... hosts: the hosts to run against (host pattern internal to playbook) vars: variables set to use in other parts pre_tasks: tasks to run before roles roles: imported task roles tasks: define tasks to be performed handlers: called from tasks using "notify: ..." when something changed templates: files to install ".j2" for jinga insertions ------------------------------------------------------------------------------- Ansible Tasks... Yaml tasks return these common values... https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html NOTE: 'var:' in the main playbook overrides host_vars/*.yml Task additions - name: "name of task" ansible.task..... args to the task register: result # Save the results of task in this var check_mode: true # skip task by forcing check mode check_mode: false # task must run even in check mode diff: false # don't run in diff mode ignore_errors: true # Don't stop on error (fail/false) changed_when: false # this did not make a change (see shell) failed_when: 'result.rc == 2' # only fail if result return code is 2 no_log: true # do not log details (EG: passwords) when: # When can the task be applied (see below) run_once: true # Only run on first host loop: # loop once for each item on each host - item1 # var set to item=item1 environment: # Add some extra environment on remote http_proxy: http://proxy.example.com:8080 tags: # allow you to run JUST this task - this_task # eg: ANSIBLE_FLAGS='--tag this_task' ------------------------------------------------------------------------------- Yaml Lint Control # yamllint disable rule:comments-indentation # all these comments can be indented by any amount # yamllint disable rule:commas rule:braces - { port: 4005, label: 'pp-php-tst', mailbox: 'pre-prod-php' } - { port: 4006, label: 'pp-php', mailbox: 'pre-prod-php' } - { port: 4007, label: '', mailbox: '' } - { port: 4008, label: '', mailbox: '' } # yamllint enable rule:commas rule:braces ------------------------------------------------------------------------------- Control of the playbook You can limit hosts to development in hosts: line... - hosts: 'app_xyz:&env_devlopment' --- Set a 'skip variable' for manual 'pushing', but not for automated runs. vars: # Skip soe stuff on push, but still run if scheduled skip_soe: "{{ lookup('ansible.builtin.env', 'CI_PIPELINE_SOURCE') == 'push' }}" --- Set a starting task... tasks: - name: START ansible.builtin.meta: flush_handlers Then run... git push -o ci.variable='ANSIBLE_FLAGS=-v --start-at-task=START' NOTE: the task name for start-at-task can NOT contain spaces! Arrgghhh.... You can also use tags... --tag firewall to limit to specific tagged tasks. (But this is NOT a starting point) --- Using When... tasks: - name: STOP when: '"env_production" in group_names' ansible.builtin.meta: end_host - name: Fail when true when: condition to fail on ansible.builtin.fail: msg: ABORTING: Playbook can not continue on host - name: Assert this is true ansible.builtin.assert: that: not file_stat.stat.exists # file does not exist success_msg: 'OK - File has not been installed yet' fail_msg: | ERROR: File has already been installed - ABORTING ANSIBLE RUN # on non-test machines (no alternative) when: '"-tst-" not in inventory_hostname' # on production machines when: '"-prd-" in inventory_hostname' # OR alt method (both dev and tst) when: '"env_development" in group_names' # specific OS when: '"os_rhel_9" in group_names' # specific host when: 'inventory_hostname == "sign-on.example.com"' # list of hosts when: 'inventory_hostname in ["hosta", "hostb"]' when: 'inventory_hostname_short in ["shrek", "bigdata"]' # Specific Date (before) # when: {{ now() < "2023-03-28 00:00:00" | to_datetime }}' # NOTE: in a task with a "loop:" # You can use 'item' in the "when:" to determine if it should apply it when: 'item not in this_dictionary' NOTE: when: can be a list which is treated as a logical AND when: - this is true - and that is true ------------------------------------------------------------------------------- Output format. By default output from failures, and 'debug' or 'assert' will be in horible to read JSON. You can modify this in either your ".ansible.cfg" or set the environment ANSIBLE_STDOUT_CALLBACK=debug Unfortunatally you can NOT change the output style for just one particualr task such as a 'error termination' task to something more human readable. ;-( ------------------------------------------------------------------------------- Optional Settings for tasks... Using a 'block' of tasks. they wall all generate 'skipped' when not done. - name: Do some major task group when 'gu_variable | default(False)' block: - name task 1 ... - name task 2 ... - name task 3 ... Unfortunately you can NOT use a 'loop:' on a block. You can use var and loop each individual task in the block... block: - name: 'Config directory links' ansible.builtin.file: state: directory path: '/app/nagios_{{ item }}' mode: '0755' owner: '{{ nagios_config_user }}' group: 'nagios' loop: '{{ config_dirs }}' - name: 'Convenience links' ansible.builtin.file: state: 'link' path: '/home/nagios_cfg/{{ item }}' src: '/app/nagios_{{ item }}' owner: '{{ nagios_config_user }}' group: 'nagios' vars: config_dirs: ['configs', 'scripts'] ---- Included Task List -- an alternative to using 'block' That is the task list is only loaded and followed if 'when' is true. This is perhaps perferred, as nothing is listed if tasks are not needed. - name: Do some major task group when 'gu_variable | default(False)' ansible.builtin.include_tasks: task_group.yml However you can not add 'start' task in included tasks, as it will not be found by the '--start-at-task' option. (makes sense it is optional). Simularly errors will not be reported until run. You can use a loop, and '{{ group }}'' within the task file. If the task file is skipped (when), none of the tasks are listed in output. - name: Do some major task group when 'gu_variable | default(False)' ansible.builtin.include_tasks: task_group.yml loop: [ 'group1', 'group2' ] loop_control: { loop_var: group } # name of variable within included tasks ------------------------------------------------------------------------------- Apply tasks to specific nodes NOTE using a 'when' will generate 'skipped' results for the other nodes. Which is not very nice. This included a different 'task file' for different hosts, or does nothing (without error), if there is no task file for that host. Read tasks for a specific hosts (if present) - name: Include Host Tasks ansible.builtin.include_tasks: >- {{ lookup("first_found", inventory_hostname+'.yml', '/dev/null' ) }} --- One host only.... Directly test for a specific host when: 'inventory_hostname == "sign-on.example.com"' Select a specific host using a set variable (say from a 'host_vars' file) when: 'init_node | default(false)' The inventory list is ordered, so if the first one is the special one You can use this to only run on the first host in inventory run_once: true Run the task on a specific node This may be different to the current {{ inventory_hostname }} delegate_to: { host_name or 127.0.0.1 } If you add 'run_once' to the above it only runs ONE TIME for all the inventory hosts the playbook the task is being applied to. Is host development when: '"env_development" in group_names' --- Run task on a different host to the current inventory host. Example take all the inventory hosts out of a 'pool' by running a command on the localhost - name: Take out of load balancer pool delegate_to: 127.0.0.1 ansible.builtin.command: cmd: /usr/bin/take_out_of_pool {{ inventory_hostname }} - do what is needed ... - name: Add back to load balancer pool delegate_to: 127.0.0.1 ansible.builtin.command: cmd: /usr/bin/add_back_to_pool {{ inventory_hostname }} ------------------------------------------------------------------------------- Looping over multiple Items https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html tasks: - name: Create multiple files (empty if not present) ansible.builtin.copy: dest: "/some/path/{{ item }}" mode: 0755 content: '' force: False loop: - test01.txt - test02.txt - test03.txt - test04.txt You can use python arrays instead of a list... loop: [ 1, 2, 3, 4 ] or from a Jinga expression loop: '{{ [ 1, 2, 3, 4 ] }}' - name: Install application files ansible.builtin.copy: src: "{{ item.src }}" dest: '/app/{{ item.dest }}' owner: 'app' group: 'app' mode: '{{ item.mode }}' # see below for alternative loop: # yamllint disable rule:commas rule:braces - { src: 'application.pl', dest: 'application', mode: '0755' } - { src: 'application.rc', dest: '.applicationrc', mode: '0644' } # yamllint enable rule:commas rule:braces loop_control: # loop_var: 'file' # set different name for 'item' variable label: "{{ item.src }}" less compact alternative loop: - src: 'application.pl' dest: 'application' mode: '0755' - src: 'application.rc' dest: '.applicationrc' mode: '0644' Using a dictionary.... - name: Set Sysctls ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" loop: "{{ sysctl_values | dict2items }}" loop_control: {label: "{{ item.key }}"} vars: sysctl_values: 'kernel.msgmnb': 262144000 # msg_qbytes size 'kernel.msgmax': 262144000 # sys v message queue max 'kernel.msgmni': 32000 # sys v message queue max identifiers 'kernel.shmall': 32000 # sys v shared mem pages loop over two arrays (servers and packages) - name: Install packages on servers hosts: localhost gather_facts: no tasks: - name: Install packages ansible.builtin.debug: msg: "Installing {{ item.1 }} on {{ item.0 }}" loop: "{{ ['server1', 'server2'] | product(['nginx', 'mysql', 'php']) | list }}" Simplification, direct provision of arguments to module This is classed as unsafe, but if the source is controled should be fine. - name: Enforce file permissions ansible.builtin.file: "{{ item }}" loop: - path: '/boot/grub2/grub.cfg' owner: 'root' group: 'root' mode: '0600' - path: '/etc/cron.daily' owner: 'root' group: 'root' mode: '0700' state: directory Repeat until success... - name: Retry a task until a certain condition is met ansible.builtin.shell: /usr/bin/foo register: result until: result.stdout.find("all systems go") != -1 retries: 5 delay: 10 If the "with_items:" are extensive datastructures you can limit the ansible output using loop_control. - authorized_key: user: "{{ item.user }}" key: "{{ item.key }}" loop: "{{ ssh_keys }}" loop_control: { label: '{{ item.user }}' } This only outputs the "user" part of the "items" instead of the whole structure OR limit the 'item' contents directly - authorized_key: user: "{{ item }}" key: "{{ ssh_keys[item] }}" loop: "{{ ssh_keys.keys() }}" ------------------------------------------------------------------------------- Install file setting mode based on suffix #Set mode to be executable for scripts, and readable for text files - name: Install Files ansible.builtin.copy: src: "{{ item }}" dest: '/installation/directory/' owner: 'root' group: 'root' # set mode based on the files suffix! mode: "{{ (item | splitext | last) in [ '.sh', '.pl' ] and '0755' or '0644' }}" with_fileglob: - '*.pl' - '*.sh' - '*.txt' - 'junk' # Or if you need more than two modes! mode: "{{ { '.pl':'0755', '.sh':'0755', '.txt':'0644' }[ item | splitext | last ] | default('0600') }}" for templates... - name: Update existing NRDP scripts ansible.builtin.template: src: '{{ item }}' dest: "/opt/admin/nrdp/{{ item | basename | regex_replace('\\.j2$', '') }}" owner: 'root' group: 'root' mode: '0750' with_fileglob: '../templates/nrdp/*.j2' loop_control: {label: '{{ item | basename }}'} ------------------------------------------------------------------------------- Handling a recursive tree of templates... See https://docs.ansible.com/ansible/latest/collections/community/general/filetree_lookup.html item.src Full path to item on localhost item.path Relative path to item on localhost item.size Size of item item.state 'directory', 'file', or 'link' (not in gitlab) - name: Approve certs server directories ansible.builtin.file: state: directory dest: '~/{{ item.path | regex_replace('^\.\./templates\/', '') }}' with_community.general.filetree: '../templates' when: item.state == 'directory' - name: Approve certs server files ansible.builtin.template: src: '{{ item.src }}' dest: '~/{{ item.path | regex_replace('^\.\./templates\/', '') | regex_replace('\\.j2$', '') }}' with_community.general.filetree: '../templates' loop_control: { label: '{{ item | basename }}' } when: item.state == 'file' ------------------------------------------------------------------------------- Copying files between hosts Dump a specific host (located outside the inventory) - name: Save state from the source host delegate_to: '{{ source_host }}' run_once: true changed_when: false # success (true) is 'ok' not 'changed' ansible.builtin.shell: | fsfreeze -f /app # stop changes tar -czf /tmp/swarm.tar.gz -C /app/docker swarm fsfreeze -u /app # resume changes # fsfreeze prevents any changes being made to the file system # Processes attempting to do so 'freeze' in a uniteruptable wait # Alturnatives # * Stop the application making changes (not good). # * Make a snapshot of the filesystem (LVM, Btrfs, ZFS) - name: download state to the ansible runner delegate_to: '{{ clone_host }}' ansible.builtin.fetch: src: /tmp/swarm.tar.gz dest: /tmp/swarm.tar.gz flat: true # DO NOT MODIFY DESTINATION NAME - remove tar from source host delegate_to: '{{ source_host }}' run_once: true ansible.builtin.file: path: /tmp/swarm.tar.gz state: absent Upload to destination hosts - name: Remove each host from running status when: 'inventory_hostname != source_host' ... - name: Delete state from host (except on source host) when: 'inventory_hostname != source_host' ansible.builtin.file: path: /app/docker/swarm/ state: absent - name: Upload state from ansible runner when: 'inventory_hostname != source_host' ansible.builtin.unarchive: src: /tmp/swarm.tar.gz dest: /app/docker/ - name: fix state for this host when: 'inventory_hostname != source_host' ansible.builtin.unarchive: src: /tmp/swarm.tar.gz dest: /app/docker/ - nome: restore host to running status (if wanted) when: 'inventory_hostname != source_host' ... If you have SSH keys set up between two hosts you can use synchronize, which is a wrapper around rsync, using -a (archive) by default. This is NOT however good security. This pushes to or pulls from the current "inventory host", by running on the current 'delegated_to' host. - name: Copy the file from src_host to dest_host using Method Pull when: 'inventory_host == "src_host"' delegate_to: "dest_host" ansible.postix.synchronize: src: /some/directory/ dest: /some/directory/ mode: pull Without the delegation it runs on the local (control) host. Default is 'push' (to the inverntory host). It can copy directories recursivally, unlike 'fetch'. WARNING: It will not reuse the the SSH session ansible is using! ------------------------------------------------------------------------------- Create files WARNING: using "Touch always changes file", not idempotent file touch always 'changed' https://github.com/ansible/ansible/issues/30226 How to create an empty file with Ansible? https://stackoverflow.com/questions/28347717/ # WARNING this will always touch the file # causing it to always update the files timestamps. - name: create empty file is not exist ansible.builtin.file: dest: '/some/path/to/file' state: touch Alternatives # Use 'copy' to create file (force: no) - name: create empty file if it does not exist ansible.builtin.copy: dest: '/some/path/to/file' mode: '0755' content: '' force: false # tell touch to leave the date as-is (it still touches though) - name: create empty file if it does not exist ansible.builtin.file: dest: '/some/path/to/file' state: touch modification_time: preserve access_time: preserve ------------------------------------------------------------------------------- Copy files on the remote machine Make backup of an existing file on a remote server (if it does not exist) - name: 'backup original configs' ansible.builtin.copy: src: '{{ item }}' dest: '{{ item }}.orig' remote_src: true # source is on remote server force: false # only if original copy does not exist loop: - '/path/to/config_file_1' - '/path/to/config_file_2' - '/path/to/config_file_3' Warning: copy will nver delete files! --- Using ansible.posix.synchronize Whole directory copy (with delete) Note the '/' on end of 'src:) WARNING: synchronize always returns 'changed'. The fix is to stop it checking timestamps during recursion. recursive: true checksum: true archive: false #times: false # as an alternative to archive: false If you use "rsync_opts: ['-q']" to quiten the output, task will then always return 'ok', regardless of any changes trasnfered. - name: 'Install nagios configs into testing (with delete)' ansible.posix.synchronize: src: 'nagios_configs/' dest: '/app/nagios_testing' delete: true # delete files too recursive: true # include "hosts" and "services" sub-dirs checksum: true # don't use timestamps, use checksums #times: false # don't worry about resetting the file times archive: false # We don't want perms,owner,times preserved, # # the default file settings is fine. - name: 'Install validated configs (remote host only)' ansible.posix.synchronize: src: '/app/nagios_testing/' dest: '/app/nagios_configs' delete: true # delete files too recursive: true # include "hosts" and "services" sub-dirs checksum: true # don't use timestamps, use checksums times: false # don't worry about resetting the dir times delegate_to: '{{ inventory_hostname }}' # run on remote host only notify: 'reload nagios' ------------------------------------------------------------------------------- Finding Files (get file name or loop over files)... Is a file or directory present - name: 'Check if user playbook has installed configs' ansible.builtin.stat: path: '/app/nagios_configs/commands.cfg' register: nagios_configs_stat - name: 'Configure nagios master config' when: nagios_configs_stat.stat.exists block: ... Get ONE file ONLY (or fail)... Demonstrating referencing a item in a JSON structure... - name: extract CDN Client Cert Key Filename ansible.builtin.find: paths: '/etc/pki/entitlement' file_type: file use_regex: true patterns: '[0-9]*-key.pem' register: client_cert # find generates a list of dicts EG: # - { path: "/etc/pki/entitlement/9893e3-key", .... } - name: Check number of RHSM client certificates ansible.builtin.assert: that: client_cert.matched == 1 quiet: true success_msg: 'Found RHSM certificate file' fail_msg: | ERROR: Found {{ client_cert.matched }} RHSM cert files. The machine {{ inventory_hostname }} needs to be subscribed 1 time. Please subscribe or remove obsolete client certs from the directory /etc/pki/entitlement Just reference first file found - ansible.builtin.debug: msg: | Key file: '{{ client_cert.files.0.path }}' Cert file: '{{ client_cert.files.0.path | replace("-key","") }}' Save file as a fact (jinga2) - name: set Cert Key Filename Fact ansible.builtin.set_fact: key_path: '{{ client_cert.files|map(attribute="path")|list|first }}' - ansible.builtin.debug: msg: | Key file: '{{ key_path }}' Cert file: '{{ key_path | replace("-key","") }}' Extract filenames from a find (list of dicts) - name: find existing access files. ansible.builtin.find: paths: '/app/access' patterns: 'access.*.txt' register: 'find_access' # list of dicts EG: # - { path: "/app/access/access.ansible.txt", .... } # - { path: "/app/access/access.guapps.txt", .... } # - { path: "/app/access/access.root.txt", .... } - name: Remove files that were not managed # loop over the filenames that were found... loop: >- {{ find_access.files | map(attribute="path") | map("basename") | map("regex_replace", "^access\.(.*)\.txt", "\1") }} when: 'item not in managed_access_users' ansible.builtin.file: path: '/app/access/access.{{ item }}.txt' state: absent Just Delete all the files found... - name: delete directory found loop: '{{ find_access | map(attribute="path") | flatten }}' ansible.builtin.file: path: '{{ item }}' state: absent ------------------------------------------------------------------------------- Replace directory with symlink The ansible.builtin.file can NOT do this! Directory must be removed first, so you need to check if it is directory or link first! - name: check /var/spool/squid symlink ansible.builtin.stat: path: '/var/spool/squid' register: link_stat - name: remove /var/spool/squid directory when: link_stat.stat.isdir|default(false) ansible.builtin.file: state: absent path: '/var/spool/squid' - name: create /var/spool/squid symlink ansible.builtin.file: state: link src: '/app/squid/cache' dest: '/var/spool/squid' owner: 'squid' group: 'squid' follow: false # otherwise symlinks will be root! ------------------------------------------------------------------------------- Change existing file contents "lineinfile", "replace", "blockinfile" WARNING: "blockinfile" has START-END markers - name: update or add a single line in a file ansible.builtin.lineinfile: path: /etc/fstab regexp: "^/dev/sdb1 " line: '/dev/sdb1 /data ext4 defaults 1 2' #create: yes #state: present - name: Comment out between two lines ansible.builtin.replace: path: /etc/hosts after: '^$' before: '$$' regexp: '^(.+)$' replace: '# \1' - name: Comment out a set of settings (added later by a block) ansible.builtin.replace: path: '/etc/nagios/nagios.cfg' regexp: '^(cfg_dir|cfg_file|resource_file)=' replace: '#\1=' before: 'BEGIN BLOCK Object Config Files' - name: Insert configuration ansible.builtin.blockinfile: path: /some/config/file insertafter: "// This Marker" marker: "// {mark} ANSIBLE CONFIG BLOCK" block: | Lines of configuration code - name: 'Set NRDP token' ansible.builtin.lineinfile: path: /usr/local/nrdp/server/config.inc.php insertafter: '^\$cfg\["authorized_tokens"\] = array(' line: ' "{{ nrdp_server_token }}",' diff: false # output can get too verbose loop_control: "adding token" loop: - 'token_1' - 'token_2' - 'token_3' Indented Data # The number defined how much the content below 'block' is indented # relative to the 'b' in 'block' (what is left is the string indent! block: |2 With extra indent # Alturnative is to use jinga to indent the whole data vars: data: | data line 1 data line 2 tasks: - ansible.builtin.copy: dest: "/tmp/file" content: "{{ data | indent(width=6, indentfirst=True ) }}" Looped Updates of Values in a file (using dictionary) - name: 'Set Apache SSL Certificates' ansible.builtin.lineinfile: path: '/etc/httpd/conf.d/ssl.conf' regexp: '^(\s*)#?{{ item.key }}\s+' line: '\1{{ item.key }} {{ item.value }}' backrefs: true loop: '{{ ssl_values | dict2items }}' loop_control: {label: '{{ item.key }}'} diff: false # output can get too verbose vars: ssl_values: 'SSLCertificateFile': /etc/letsencrypt/live/primary/cert.pem 'SSLCertificateKeyFile': /etc/letsencrypt/live/primary/privkey.pem 'SSLCertificateChainFile': /etc/letsencrypt/live/primary/chain.pem '#SSLCACertificateFile': -- notify: 'Reload Httpd' Comment out a line (use backrefs) the '(?i)' makes it case insensitive - name: Comment out pipeline archive in fstab ansible.builtin.lineinfile: path: /etc/fstab regexp: '(?i)^(//archive/pipeline.*)' line: '# \1' backrefs: yes state: present Alternative using replace (just the matched string) module - name: Comment out pipeline archive in fstab ansible.builtin.replace: path: /etc/fstab regexp: '^(//archive/pipeline)' replace: '#\1' Add word to line, if word is NOT present # This works... BUT fails if 'word' contains a '-' or '.' - name: Add Group to AllowGroups ansible.builtin.lineinfile: dest: /etc/ssh/sshd_config regexp: '^(AllowGroups(?!.*\b{{ sftp_group_name }}\b).*)$' replace: '\1 {{ sftp_group_name }}' backrefs: true diff: false # output can get too version # This does it correctly, for white space separated words # checking each word for the word to add... # https://stackoverflow.com/questions/31432367 - name: 'add "content" as an alias for "localhost"' ansible.builtin.lineinfile: path: /etc/hosts # check that each white space separated word, is not 'content' regexp: '^127\.0\.0\.1((?:(?:\s+\S+(?!\S))(?> /etc/fstab when: shell_out.std_out != '' Run multiple commands without shell - name: Build and install policy ansible.builtin.command: '{{ item }}' args: chdir: '{{ selinux_workdir }}' loop: - 'make -f /usr/share/selinux/devel/Makefile {{ selinux_module }}.pp' - 'semodule -i {{ selinux_module }}.pp' Run in a full shell allowing pipelining (lint warning)... The set -x has it log the commands being run, but to see them properly you may need to change the output callback from the default 'json' formay to something else like 'yaml', 'minimal' or 'debug' or extract the output from the logs and convert it yourself. - name: 'Download Teampass {{ teampass.version }} from Github' # Also see "nagios_core" root playbook. # The installation for NRDP has follow a HTML 'include-fragment' # to actually locate the tar download! Arrgghh... ansible.builtin.shell: creates: '{{ guapps_home }}/teampass' executable: /bin/bash cmd: | exec 2>&1; set -eux api="https://api.github.com/repos/{{ teampass.repo }}" api="$api/releases/tags/{{ teampass.version }}" url="$( curl -skL "$api" | grep -oP '"tarball_url": "\K[^"]+' )" cd "/apps" curl -skL "$url" | tar zxf - mv nilsteampassnet-TeamPass-* teampass mkdir teampass/.git && true # stops image downloading latest version #ignore_errors: true # don't stop on error (see next) register: results Better shell output report.... NOTE: this must be run in debug mode to actually see output nicely! - name: 'run a testing shell script' ansible.builtin.shell: creates: '/tmp/xyzzy' # actually it doesn't :-) executable: /bin/bash cmd: | exec 2>&1; set -eux echo testing false register: results ignore_errors: true # don't stop on error (next task does this) - name: Check Previous command (assert) ansible.builtin.assert: # Reports results in green or red as appropriet - excelent that: results.rc == 0 success_msg: "Shell Output...\n{{ results.stdout }}" fail_msg: "ABORTING: Shell Failed...\n{{ results.stdout }}" #- name: 'script results (fail)' # # Quite good output (and right color), only on failure. # when: results.rc != 0 # ansible.builtin.fail: # msg: "ABORTING: Shell Failed...\n{{ results.stdout }}" #- name: 'script results (debug)' # # with the "failed_when" debug is true, task stops here # # BUT while output color is red, output format is not 'debug' # failed_when: results.rc != 0 # ansible.builtin.debug: # var: results.stdout # #msg: "Shell Output..\n{{ results.stdout }}" Run a script from a local file. - name: run script 'files/script.py' using python ansible.builtin.script: executable: python3 command: 'script.py {{ arguments }}' register: script_result # NOTE: script runs in a psuedo-tty, so stderr merges with stdout. NOTE: Tasks can have a status of: ok, fail, change. But generally shell command results are either: change (true) or fail (false)! Unless there is a 'creates' or 'removes' entry to make a 'ok'. - ansible.builtin.shell: removes: {file} # task ok (not run) if file is not present creates: {file} # task ok (not run) if file is present cmd: false register: results check_mode: false # run (collect info) even in check mode ignore_errors: true # Don't stop on error (fail/false) changed_when: false # success (true) is 'ok' not 'changed' failed_when: 'result.rc == 2' # only fail if return code is 2 register: result changed_when: 'result.rc == 0' # Success is Changed, otherwise it is OK failed_when: false # never a failure no_log: true # do not log on change failed_when # ALL items must be true to fail! - result.rc == 0 - '"No such" not in results.stdout' environment: # Add some extra environment on remote http_proxy: http://proxy.example.com:8080 ------------------------------------------------------------------------------- CLI-parse https://docs.ansible.com/ansible/latest/network/user_guide/cli_parsing.html Run a CLI command, or text string, and parse output to structured data 'fact'. Can parse many different styles of data output. ------------------------------------------------------------------------------- Linux Alternatives... Simple - name: set MTA alternatives to ExIM community.general.alternatives: name: mta path: /usr/sbin/sendmail.exim A Whole set of alternatives... vars: jdk_dir: /usr/java/jdk1.7.0_55 tasks: - name: Configure Java alternatives alternatives: name={{ item.name }} link={{ item.link }} path={{ item.path }} with_items: - { name: jar, link: /usr/bin/jar, path: "{{ jdk_dir }}/bin/jar" } - { name: jps, link: /usr/bin/jps, path: "{{ jdk_dir }}/bin/jps" } - { name: java, link: /usr/bin/java, path: "{{ jdk_dir }}/bin/java" } - { name: jmap, link: /usr/bin/jmap, path: "{{ jdk_dir }}/bin/jmap" } - { name: javac, link: /usr/bin/javac, path: "{{ jdk_dir }}/bin/javac" } - { name: javaws, link: /usr/bin/javaws, path: "{{ jdk_dir }}/bin/javaws" } - { name: jstack, link: /usr/bin/jstack, path: "{{ jdk_dir }}/bin/jstack" } - { name: jvisualvm, link: /usr/bin/jvisualvm, path: "{{ jdk_dir }}/bin/jvisualvm" } ------------------------------------------------------------------------------- Firewall variable rule merging... For in root apps playbooks var: # firewall app rules nft_input_group_rules: ... gu_app_iptables_rules: ... optional_nftable_rules: ... optional_iptable_rules: ... pre_tasks: - name: 'Optional Firewall Rules' when: '"-prd-" in inventory_hostname' block: - name: 'merge optional nftable rules' ansible.builtin.set_fact: nft_input_group_rules: '{{ nft_input_group_rules | combine(optional_nftable_rules) }}' - name: 'merge optional iptable rules' ansible.builtin.set_fact: gu_app_iptables_rules: '{{ gu_app_iptables_rules | combine(optional_iptable_rules) }}' roles: - gu.firewall.apply_firewall ------------------------------------------------------------------------------- Assert order of tasks... EG check firewall rules have not been applied. so you can merge more rules before it is applied In firewall role... - name: Set variable for firewall applied ansible.builtin.set_fact: firewall_applied: true check_mode: false This in your role (to put before firewall)... - name: Assert that firewall has not already been applied ansible.builtin.assert: that: 'not firewall_applied | defined(false)' success_msg: 'OK - firewall has not already been applied' fail_msg: 'ERROR: Firewall has already been applied. This role must be applied before 'gu.firewall.apply_firewall'' check_mode: false NOTE: Unless you use callback_debug the assert messages is buried in a mass of yaml on one line. :-( Better to have a debug task before it. See 'shell' below. ------------------------------------------------------------------------------- RPM and Version checks... vars: rubrik_pkg: rubrik_agent Using shell (to be avoided) tasks: # Is Package installed, and what version - name: Get Current Rubrik Version ansible.builtin.shell: cmd: 'rpm -q --qf "%{VERSION}\n" {{ rubrik_pkg }}' changed_when: false failed_when: false check_mode: false register: rubrik_installed # OPTIONAL... is it installed - name: Is Rubric Installed when: '"not installed" in rubrik_installed.stdout' debug: msg: "========> Rubrik is not installed" # OPTIONAL... is it the latest! - name: Compare version when: '"not installed" not in rubrik_installed.stdout' ansible.builtin.assert: that: rubrik_installed.stdout is version(rubrik_version, '>=') success_msg: '======> Rubrik Backup is Up-to-date' fail_msg: '=====> Rubrik Backup is Out-of-date' ignore_errors: true # don't stop on error, just report it # Install (or upgrade) the RPM - name: Install Rubrik RPM when: - rubrik_backup # host or app IS marked for rubrik installation - '"not installed" in rubrik_installed.stdout' # remove to upgrade ansible.builtin.yum: name: 'https://{{rubrik_site}}/{{rubrik_path}}/{{rubrik_pkg}}-{{rubrik_version}}-gc.x86_64.rpm' state: present Using package facts tasks: - name: Collect package info ansible.builtin.package_facts: no_log: true - name: Is rubric-agent installed when: '"rubrik-agent" in ansible_facts.packages' ansible.builtin.debug: msg: "rubrik-agent is installed!" - name: how many versions is installed when: '"rubrik-agent" in ansible_facts.packages' ansible.builtin.debug: msg: "{{ ansible_facts.packages['rubrik-agent'] | length }} versions" - name: Install if not present, or Upgrade old version when: >- 'rubrik-agent' not in ansible_facts.packages or ansible_facts.packages[ 'rubrik-agent'][0]['version' ].split('.')[0:3] | join('.') is version(rubrik_version, '<') ansible.builtin.debug: msg: Install/Upgrade Rubrik Direct version Check - name: Get NPM version ansible.builtin.command: cmd: npm -v register: npm_version - name: If NPM is still default v6 upgrade it when: npm_version.stdout is match('6\..*') ansible.builtin.command: command: 'npm -g install npm@latest' OR when: npm_version.stdout|string is version('8.0', '<') when: ansible_facts.distribution_major_version|string is version("8", ">=") when: jre_installed|string is version((autosys_jre_version|string), "<") ------------------------------------------------------------------------------- Comparing Lists/Dicts for changes The problem is locating differences to be fixed in existing services (ports). Finding the ports that need to be added or removed is straight forward. But secondary changes for ports that already exist is harder! EG: 'label' (or a different element) has changed, and needs to be fixed. vars: # List of services that need to be setup # A blank 'label' means that port is unused # # Stop yamllint from complaining about an 'easy to read' table of data # yamllint disable rule:commas rule:braces relay_services: - { port: 4000, label: 'EIS-Portal-preprod' } - { port: 4001, label: 'EIS-CS-preprod' } - { port: 4002, label: 'EIS-HR-preprod' } - { port: 4003, label: 'EIS-FS-preprod' } - { port: 4004, label: '' } - { port: 4005, label: 'pre-prod-php' } - { port: 4006, label: 'pre-prod-php' } - { port: 4007, label: '' } - { port: 4008, label: '' } - { port: 4009, label: 'test' } # List of the existing services recovered from the machine # It is in same order relay_existing: - { port: 4006, label: pre-prod-changed } - { port: 4008, label: nobody } - { port: 4009, label: test } # yamllint enable rule:commas rule:braces tasks: # extract the port list... - set_fact: relay_services_ports: >- {{ relay_services | selectattr('label') | map(attribute='port') | list }} relay_existing_ports: >- {{ relay_existing | map(attribute='port') | list }} # How do the port lists differ... - set_fact: relay_ports_to_add: >- {{ relay_services_ports | difference(relay_existing_ports) | list }} relay_ports_to_remove: >- {{ relay_existing_ports | difference(relay_services_ports) | list }} relay_ports_already_present: >- {{ relay_services_ports | intersect(relay_existing_ports) | list }} - debug: var: relay_ports_to_add - debug: var: relay_ports_to_remove - debug: var: relay_ports_already_present # Find changes more generaly... # # This compares everything in one list, to everything in second list # As such it is of order O^2, and generates a LOT of skips. # # As 'relay_services' is complete, this will find removals and changes # but as 'relay_existing' is not complete it does not find additions. # - debug: msg: "{{ item[0] }}" when: item[0].port == item[1].port and item[0].label != item[1].label with_nested: - "{{ relay_services }}" - "{{ relay_existing }}" -------------------------------------------------------------------------------