Trying to include a list of tasks from an playbook in Ansible - ansible

My folder structure:
First I'll give you this so you can see how this is laid out and reference it when reading below:
/environments
/development
hosts // Inventory file
/group_vars
proxies.yml
/custom_tasks
firewall_rules.yml // File I'm trying to bring in
playbook.yml // Root playbook, just brings in the plays
rev-proxy.yml // Reverse-proxy playbook, included by playbook.yml
playbook.yml:
---
- include: webserver.yml
- include: rev-proxy.yml
proxies.yml just contains firewall_custom_include_file: custom_tasks/firewall_rules.yml
firewall_rules.yml:
tasks:
- name: "Allowing traffic from webservers on 80"
ufw: src=10.10.10.3, port=80, direction=in, rule=allow
- name: "Allowing traffic all on 443"
ufw: port=443, rule=allow
and finally rev-proxy.yml play:
---
- hosts: proxies
become: yes
roles:
- { role: firewall }
- { role: geerlingguy.nginx }
pre_tasks:
# jessie-backports for nginx-extras 1.10
- name: "Adding jessie-backports repo"
copy: content="deb http://ftp.debian.org/debian jessie-backports main" dest="/etc/apt/sources.list.d/jessie-backports.list"
- name: Updating apt-cache.
apt: update_cache="yes"
- name: "Installing htop"
apt:
name: htop
state: present
- name: "Coopying SSL certificates"
copy: src=/vagrant/ansible/files/ssl/ dest=/etc/ssl/certs force=no
tasks:
- name: "Including custom firewall rules."
include: "{{ inventory_dir }}/{{ firewall_custom_include_file }}.yml"
when: firewall_custom_include_file is defined
vars_files:
- ./vars/nginx/common.yml
- ./vars/nginx/proxy.yml
What I'm trying to do:
Using Ansible 2.2.1.0
I'm trying to include a list of tasks that will be run if a variable firewall_custom_include_file is set. The list is included relative to the inventory directory by doing "{{ inventory_dir }}/{{ firewall_custom_include_file }}.yml" - in this case that works out to /vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml
Essentially the idea here is that I need to have different firewall rules be executed based on what environment I'm in, and what hosts are being provisioned.
To give a simple example: I might want to whitelist a database server IP on the production webserver, but not on the reverse proxy, and also not on my development box.
The problem:
Whenever I include firewall_rules.yml like above, it tells me:
TASK [Including custom firewall rules.] ****************************************
fatal: [proxy-1]: FAILED! => {"failed": true, "reason": "included task files must contain a list of tasks"}
I'm not sure what it's expecting, I tried taking out the tasks: at the beginning of the file, making it:
- name: "Allowing traffic from webservers on 80"
ufw: src=10.10.10.3, port=80, direction=in, rule=allow
- name: "Allowing traffic all on 443"
ufw: port=443, rule=allow
But then it gives me the error:
root#ansible-control:/vagrant/ansible# ansible-playbook -i environments/development playbook.yml
ERROR! Attempted to execute "/vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml" as inventory script: problem running /vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml --list ([Errno 8] Exec format error)
Attempted to read "/vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml" as YAML: 'AnsibleSequence' object has no attribute 'keys'
Attempted to read "/vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml" as ini file: /vagrant/ansible/environments/development/custom_tasks/firewall_rules.yml:2: Expected key=value host variable assignment, got: name:
At this point I'm not really sure what it's looking for in the included file, and I can't seem to really find clear documentation on this, or other people having this issue.

Try to execute with -i environments/development/hosts instead of directory.
But I bet that storing tasks file inside inventory is far from best practices.
You may want to define list of custom rules as inventory variable, e.g.:
custom_rules:
- src: 10.10.10.3
port: 80
direction: in
rule: allow
- port: 443
rule: allow
And instead of include task, make something like this:
- ufw:
port: "{{ item.port | default(omit) }}"
rule: "{{ item.rule | default(omit) }}"
direction: "{{ item.direction | default(omit) }}"
src: "{{ item.src | default(omit) }}"
with_items: "{{ custom_rules }}"

Related

Ansible: how to achieve idempotence with tasks that append files on host (w/o reverting to initial state)

I am having a hard time getting to know how to create Ansible roles that are following the best practices according to documentation. The following use-case which I am looking at is e.g. enabling Filebeat on host. Filebeat can be configured by placing a module definition in /etc/filebeat/modules.d folder.
It works fine when I am adding modules. Idempotence is working, everytime, on each run of the role (playbook), a given set of modules is enabled.
But what I should do when I decide that a given module is not longer needed? I remove it from role, rerun a playbook, so that all other modules are enabled. But: the previous run enabled a module that I am not installing directly with role after changes. So my server state is still altered in a way that is different than the role is imposing itself.
My question is: should I take care of removing modules before I apply them so I always start from, let's say, fresh state?
E.g.:
- name: Remove modules
file:
dest: "/etc/filebeat/modules.d/{{ item }}"
state: absent
loop:
- "module1.yml"
- "module2.yml"
- "module3.yml" # It was being installed in previous role, but not now
- name: Enable modules via 'modules.d' directory
template:
src: "modules.d/{{ item }}"
dest: "/etc/filebeat/modules.d/{{ item }}"
mode: '0644'
loop:
- "module1.yml"
- "module2.yml"
So I remove module3.yml, because I remember that I've installed it before, and install module1.yml and module2.yml.
Instead of just installing what I need, no matter what has been installed before:
- name: Enable modules via 'modules.d' directory
template:
src: "modules.d/{{ item }}"
dest: "/etc/filebeat/modules.d/{{ item }}"
mode: '0644'
loop:
- "module1.yml"
- "module2.yml"
Leaving me with module1.yml and module2.yml (desired) and, unfortunately: module3.yml (from previous role).
How to manage that to avoid such situations? And avoid treating server as one big stateful machine that even if I run a role, the output is different than desired, because something has been done before that I cannot see in current Ansible role code.
Do you code revert playbooks in your Ansible workflow to revert to initial state when needed?
I am curious. Thanks in advance for your reply.
In a nutshell:
- name: Configure filebeat modules
hosts: all
vars:
fb_modules_d:
- file: module1.yml
state: present
- file: module2.yml
state: present
- file: module3.yml
state: absent
tasks:
- name: Make sure all needed module files are present
template:
src: "modules.d/{{ item.file }}"
dest: "/etc/filebeat/modules.d/{{ item.file }}"
mode: '0644'
loop: "{{ fb_modules_d | selectattr('state', '==', 'present') }}"
notifiy: restart_filebeat
- name: Make sure all disabled modules are removed
file:
dest: "/etc/filebeat/modules.d/{{ item.file }}"
state: "{{ item.state }}"
loop: loop: "{{ fb_modules_d | selectattr('state', '==', 'absent') }}"
notify: restart_filebeat
handlers:
- name: Restart filebeat service
listen: restart_filebeat
systemd:
name: filebeat
state: restarted
Note: I declared the variable inside the playbook for the example but that one one should most probably go inside your inventory (group or host level), and certainly not in a role (except in defaults for documentation)
A simpler and more robust solution is to remove all files not listed. This would be especially useful when you don't know what is the current state of the directory. For example, given the directory (relative to PWD for testing)
shell> tree etc/filebeat/modules.d
etc/filebeat/modules.d
└── module99.yml
The playbook below
shell> cat playbook.yml
- name: Configure filebeat modules
hosts: localhost
vars:
fb_modules_path: etc/filebeat/modules.d
fb_modules_d:
- module1.yml
- module2.yml
- module3.yml
tasks:
- name: Create modules
template:
src: "modules.d/{{ item }}"
dest: "{{ fb_modules_path }}/{{ item }}"
mode: '0644'
loop: "{{ fb_modules_d }}"
notify: restart_filebeat
- name: Remove modules
file:
dest: "{{ fb_modules_path }}/{{ item }}"
state: absent
loop: "{{ query('fileglob', fb_modules_path ~ '/*')|
map('basename')|
difference(fb_modules_d) }}"
notify: restart_filebeat
handlers:
- name: Restart filebeat service
listen: restart_filebeat
# systemd:
# name: filebeat
# state: restarted
debug:
msg: Restart filebeat
creates the files from the list and removes others
TASK [Create modules] ***************************************************
changed: [localhost] => (item=module1.yml)
changed: [localhost] => (item=module2.yml)
changed: [localhost] => (item=module3.yml)
TASK [Remove modules] ***************************************************
changed: [localhost] => (item=module99.yml)
RUNNING HANDLER [Restart filebeat service] ******************************
ok: [localhost] =>
msg: Restart filebeat
shell> tree etc/filebeat/modules.d
etc/filebeat/modules.d
├── module1.yml
├── module2.yml
└── module3.yml
The playbook is idempotent.

Switching user for delegation to host outside of inventory with Ansible/awx

I am trying to do the following using Ansible 2.8.4 and awx:
Read some facts from Cisco IOS devices (works)
Put results into a local file using a template (works)
Copy/Move the resulting file to a different server
Since I have to use a different user to access IOS devices and servers, and the servers in question aren't part of the inventory used for the playbook, I am trying to achieve this using become_user and delegate_to.
The initial user (defined in the awx template) is allowed to connect to the IOS devices, while different_user can connect to servers using a ssh private key.
The playbook:
---
- name: Read Switch Infos
hosts: all
gather_facts: no
tasks:
- name: Gather IOS Facts
ios_facts:
- debug: var=ansible_net_version
- name: Set Facts IOS
set_fact:
ios_version: "{{ ansible_net_version }}"
- name: Create Output file
file: path=/tmp/test state=directory mode=0755
delegate_to: 127.0.0.1
run_once: true
- name: Run Template
template:
src: ios_firmware_check.j2
dest: /tmp/test/output.txt
delegate_to: 127.0.0.1
run_once: true
- name: Set up keys
become: yes
become_method: su
become_user: different_user
authorized_key:
user: different_user
state: present
key: "{{ lookup('file', '/home/different_user/.ssh/key_file') }}"
delegate_to: 127.0.0.1
run_once: true
- name: Copy to remote server
remote_user: different_user
copy:
src: /tmp/test/output.txt
dest: /tmp/test/output.txt
delegate_to: remote.server.fqdn
run_once: true
When run, the playbook fails in the Set up keys task trying to access the home directory with the ssh key:
TASK [Set up keys] *************************************************************
task path: /tmp/awx_2206_mz90qvh9/project/IOS/ios_version.yml:23
[WARNING]: Unable to find '/home/different_user/.ssh/key_file' in expected paths
(use -vvvvv to see paths)
File lookup using None as file
fatal: [host]: FAILED! => {
"msg": "An unhandled exception occurred while running the lookup plugin 'file'. Error was a <class 'ansible.errors.AnsibleError'>, original message: could not locate file in lookup: /home/different_user/.ssh/key_file"
}
I'm assuming my mistake is somehow related to which user is trying to access the /home/ directory on which device.
Is there a better/more elegant/working way of connecting to a different server using an ssh key to move around files?
I know one possibility would be to just scp using the shell module, but that always feels a bit hacky.
(sort of) solved using encrypted variables in hostvars with Ansible vault.
How to get there:
Encrypting the passwords:
This needs to be done from any commandline with Ansible installed, for some reason this can't be done in tower/awx
ansible-vault encrypt_string "password"
You'll be prompted for a password to encrypt/decrypt.
If you're doing this for Cisco devices, you'll want to encrypt both the ssh and the enable password using this method.
Add encrypted passwords to inventory
For testing, I put it in hostvars for a single switch, should be fine to put it into groupvars and use it on multiple switches as well.
ansible_ssh_pass should be the password to access the switch, ansible_become_pass is the enable password.
---
all:
children:
Cisco:
children:
switches:
switches:
hosts:
HOSTNAME:
ansible_host: ip-address
ansible_user: username
ansible_ssh_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
[encrypted string]
ansible_connection: network_cli
ansible_network_os: ios
ansible_become: yes
ansible_become_method: enable
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
[encrypted string]
Adding the vault password to tower/awx
Add a new credential with credential type "Vault" and the password you used earlier to encrypt the strings.
Now, all you need to do is add the credential to your job template (the template can have one "normal" credential (machine, network, etc.) and multiple vaults).
The playbook then automagically accesses the vault credential to decrypt the strings in the inventory.
Playbook to get Switch Infos and drop template file on a server
The playbook now looks something like below, and does the following:
Gather Facts on all Switches in Inventory
Write all facts into a .csv using a template, save the file on the ansible host
Copy said file to a different server using a different user
The template is configured with the user able to access the server, the user used to access switches with a password is stored in the inventory as seen above.
---
- name: Read Switch Infos
hosts: all
gather_facts: no
tasks:
- name: Create Output file
file: path=/output/directory state=directory mode=0755
delegate_to: 127.0.0.1
run_once: true
- debug:
var: network
- name: Gather IOS Facts
remote_user: username
ios_facts:
- debug: var=ansible_net_version
- name: Set Facts IOS
set_fact:
ios_version: "{{ ansible_net_version }}"
- name: Run Template
template:
src: ios_firmware_check.csv.j2
dest: /output/directory/filename.csv
delegate_to: 127.0.0.1
run_once: true
- name: Create Destination folder on remote server outside inventory
remote_user: different_username
file: path=/destination/directory mode=0755
delegate_to: remote.server.fqdn
run_once: true
- name: Copy to remote server outside inventory
remote_user: different_username
copy:
src: /output/directory/filename.csv
dest: /destination/directory/filename.csv
delegate_to: remote.server.fqdn
run_once: true

Read a file locally and use the vars remote in Ansible

I read a YAML file locally with the following playbook:
- name: Ensure the deploy_manifest var is defined and read deploy manifest
hosts: localhost
connection: local
gather_facts: False
tasks:
- assert:
that: deploy_manifest is defined
msg: |
Error: Must provide providers config path. Fix: Add '-e deploy_manifest=/path/to/manifest' to the ansible-playbook command
- name: Read deploy manifest
include_vars:
file: "{{ deploy_manifest }}"
name: manifest
register: manifest
- debug:
msg: "[{{ manifest.key }}]: {{ manifest.value }}"
with_dict: "{{ manifest.ansible_facts }}"
and then in the same playbook YAML file I run:
- name: Deploy Backend services
hosts: backend
remote_user: ubuntu
gather_facts: False
vars:
env: "{{ env }}"
services: "{{ manifest.ansible_facts }}"
tasks:
- include_role:
name: services_backend
when: backend | default(true) | bool
However it doesn't work because debug fails. It says that manifest is empty.
Which is the best way to read a YAML file or generally a configuration in a playbook and then have the variables passed in another playbook?
Your debug module doesn't say "that manifest is empty", it says the key manifest.key does not exist because it does not.
You registered a fact named manifest with:
register: manifest
You try to refer to a key of the above manifest named key and another key (!) named value:
msg: "[{{ manifest.key }}]: {{ manifest.value }}"
Please read Looping over Hashes chapter and acknowledge that (without using loop control) you refer to the iterated variable using item.
Please note that with name: manifest and register: manifest you read your vars file into manifest.ansible_facts.manifest.

Ansible-Playbook - Save output to a remote server

I'm new to all the Ansible stuff. So most of the time I'm in "Trial and Error"-Mode.
Now I'm facing a challenge with a playbook and I do not know to look further.
The main task of this playbook should be to get a "Show run" from a Cisco Device and save this in a text file on a backup server (which is a remote server).
The only task, which is not working, is the Backup Task.
Here is my playbook:
- hosts: IOSGATEWAY
gather_facts: no
connection: local
tasks:
- name: GET CREDENTIALS
include_vars: path/to/all/all.yml
- name: DEFINE CONNECTION TO GW
set_fact:
connection:
host: "{{ inventory_hostname }}"
username: "{{ creds['username'] }}"
password: "{{ creds['password'] }}"
- name: GET SHOW RUN
ios_command:
provider: "{{ connection }}"
commands:
- show run
register: show_run
- name: SAVE TO BACKUP SERVER
copy:
content: "{{ show_run.stdout[0] }}"
dest: "path/to/Directory/{{ inventory_hostname }}.txt"
delegate_to: BACKUPSERVER
Can someone hint me in the right direction?
You set connection: local for the playbook, so everything you do is executed locally (which is correct for ios_... modules, but not what you actually want for copy module).
I'd recommend to define ansible_connection variable in your inventory per group of hosts/devices, so Ansible will use local connection for your ios devices, and ssh for backup-server.

how to run a particular task on specific host in ansible

my inventory file's contents -
[webservers]
x.x.x.x ansible_ssh_user=ubuntu
[dbservers]
x.x.x.x ansible_ssh_user=ubuntu
in my tasks file which is in common role i.e. it will run on both hosts but I want to run a following task on host webservers not in dbservers which is defined in inventory file
- name: Install required packages
apt: name={{ item }} state=present
with_items:
- '{{ programs }}'
become: yes
tags: programs
is when module helpful or there is any other way? How could I do this ?
If you want to run your role on all hosts but only a single task limited to the webservers group, then - like you already suggested - when is your friend.
You could define a condition like:
when: inventory_hostname in groups['webservers']
Thank you, this helps me too.
hosts file:
[production]
host1.dns.name
[internal]
host2.dns.name
requirements.yml file:
- name: install the sphinx-search rpm from a remote repo on x86_64 - internal host
when: inventory_hostname in groups['internal']
yum:
name: http://sphinxsearch.com/files/sphinx-2.2.11-1.rhel7.x86_64.rpm
state: present
- name: install the sphinx-search rpm from a remote repo on i386 - Production
when: inventory_hostname in groups['production']
yum:
name: http://sphinxsearch.com/files/sphinx-2.2.11-2.rhel6.i386.rpm
state: present
An alternative to consider in some scenarios is -
delegate_to: hostname
There is also this example form the ansible docs, to loop over a group. https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html -
- hosts: app_servers
tasks:
- name: gather facts from db servers
setup:
delegate_to: "{{item}}"
delegate_facts: True
loop: "{{groups['dbservers']}}"

Resources