Why Ansible didn't see attribute in variable? - linux

I have Ansible role "db" with simple task:
- name: Check repos
apt_repository: repo="{{ item.repo }}" state={{ item.state }}
with_items:
- "{{ apt_repos }}"
In /defaults/mail.yml:
apt_repos:
# Percona
- { state: present, repo: 'deb http://repo.percona.com/apt wheezy main', keyserver: 'keyserver.ubuntu.com', key: '1C4CBDCDCD2EFD2A', needkey: True }
- { state: present, repo: 'deb-src http://repo.percona.com/apt wheezy main', needkey: False }
When i try to run this ansible-playbook:
---
- hosts: test
roles:
- db
i see error:
fatal: [10.10.10.10] => One or more undefined variables: 'unicode object' has no attribute 'repo'
FATAL: all hosts have already failed -- aborting
But i have another role with same task and variable and it work perfectly. What's wrong?

You want to be doing this:
with_items: apt_repos
apt_repos is a list. By referencing it as - "{{ apt_repos }}" the extra - is turning it into a list of lists. You also don't need the quotes or braces in this case - those are pretty much just redundant in this type of situation.

Related

How can I pass a varialbe set by the set_fact module to the Jinja2 template?

I have a role to setup NATS cluster,, I've used host_vars to define which node is the master node like below:
is_master: true
Then in the setup-nats.yml task, I used the following to extract the master node's IP address based on the host_var I've set and then used it as a variable for the Jinja2 template, however, the variable doesn't get passed down to the template and I get the `variable 'master_ip' is undefined.
- name: Set master IP
set_fact:
set_master_ip: "{{ ansible_facts['default_ipv4']['address'] }}"
cacheable: yes
when: is_master
- name: debug
debug:
msg: "{{ set_master_ip }}"
run_once: true
- name: generate nats-server.conf for the slave nodes
template:
src: nats-server-slave.conf.j2
dest: /etc/nats-server.conf
owner: nats
group: nats
mode: 0644
when:
- is_master == false
vars:
master_ip: "{{ set_master_ip }}"
notify: nats-server
The variable is used like below in the Jinja2 template:
routes = [
nats-route://ruser:{{ nats_server_password }}#{{ master_ip }}:6222
]
}
Questions:
Is this approach according to the best practices?
What is the correct way of doing the above so the variable is passed down to the template?
Test Output:
I'm using Molecule to test my Ansible and even though in the debug task the IP address is visible, it doesn't get passed down to the template:
TASK [nats : Set master IP] ****************************************************
ok: [target1]
skipping: [target2]
skipping: [target3]
TASK [nats : debug] ************************************************************
ok: [target1] =>
msg: 10.0.2.15
TASK [nats : generate nats-server.conf for the slave nodes] ********************
skipping: [target1]
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target2]: FAILED! => changed=false
msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target3]: FAILED! => changed=false
msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'
Any help is appreciated, thanks in advance.
UPDATE: I suspect the issue has something to do with the variable scope being in the host context but cannot find a way to fix it ( I might be wrong though).
Far from being best practice IMO but answering your direct question. Your problem is not passing the variable to your template but the fact it is not assigned to all hosts in your play loop (and hence is undefined on any non master node). The following (untested) addresses that issue keeping the same task structure.
- name: Set master IP for all nodes
ansible.builtin.set_fact:
master_ip: "{{ hostvars | dict2items | map(attribute='value'
| selectattr('is_master', 'defined') | selectattr('is_master')
| map(attribute='ansible_facts.default_ipv4.address') | first }}"
cacheable: yes
run_once: true
- name: Show calculated master IP (making sure it is assigned everywhere)
ansible.builtin.debug:
msg: "{{ master_ip }}"
- name: generate nats-server.conf for the slave nodes
ansible.builtin.template:
src: nats-server-slave.conf.j2
dest: /etc/nats-server.conf
owner: nats
group: nats
mode: 0644
when: not is_master | bool
notify: nats-server
Ideas for enhancement (non exhaustive):
Select your master based on a group membership in the inventory rather than on a host attribute. This makes gathering the ip easier (e.g. master_ip: "{{ hostvars[groups.master | first].ansible_facts.default_ipv4.address }}"
Set the ip as a play var or directly inside the inventory for the node group rather than in a set_fact task.

Testing a task result which uses a loop?

So the first task is always skipped because RedHat 8 is detected which should trigger the fail module to run but this gets skipped also. Is works fine without the loop though.
- name: installing packages
hosts: ansible2
ignore_errors: true
vars_files: varsfile
tasks:
- name: install software based on OS distro and version
yum:
name: "{{ item.name }}"
state: latest
loop: "{{ packages }}"
when: >
( ansible_distribution == "RedHat" and ansible_distribution_major_version is version('12', '>=') )
or
( ansible_distribution == "CentOS" and ansible_distribution_major_version | int >= 8 )
register: result
- fail:
msg: error {{ ansible_hostname }} does not meet minimum requirements
when: result is skipped
Regarding
So the first task is always skipped because RedHat 8 is detected which should trigger the fail module to run but this gets skipped also.
with a small test setup and a debug task for the variable name to debug
---
- hosts: test.example.com
become: false
gather_facts: true
tasks:
- name: Install software based on OS distro and version
debug:
msg: "{{ ansible_distribution }} {{ ansible_distribution_major_version }}"
register: result
when: ansible_distribution == "RedHat" and ansible_distribution_major_version is version('12', '>=')
with_items: ['pkg1', 'pkg2']
- name: Show result
debug:
var: result
- fail:
msg: "Error: {{ ansible_hostname }} does not meet minimum requirements"
when: result.results[0].skipped
resulting into an output of
TASK [Gathering Facts] ********************************
ok: [test.example.com]
TASK [Show result] ************************************
ok: [test.example.com] =>
result:
changed: false
msg: All items completed
results:
- ansible_loop_var: item
changed: false
item: pkg1
skip_reason: Conditional result was False
skipped: true
...
TASK [fail] *******************************************
fatal: [test.example.com]: FAILED! => changed=false
msg: 'Error: test does not meet minimum requirements'
you can see that the loop will create a list.
- name: Show result
debug:
var: result.results | type_debug
TASK [Show result] ****************
ok: [test.example.com] =>
result.results | type_debug: list
Therefore you need to set the Conditional to when: result.results[0].skipped.
Regarding
Is works fine without the loop though.
it is recommended to simplify your use case with the following approach
- name: Install software based on OS distro and version
yum:
name: "{{ packages }}"
state: latest
according the yum module Notes
When used with a loop: each package will be processed individually, it is much more efficient to pass the list directly to the name option.
and as it is faster and consumes less resources. Furthermore the result set and conditionals are less complex.

ansible deprecation warning with custom objects in apt

I am getting a deprecated warning with Ansible
[DEPRECATION WARNING]: Invoking "apt" only once while using a loop via squash_actions is deprecated. Instead of using a loop to supply multiple items and specifying name: "{{ item.name | default(item) }}", please use name: '{{ apt_dependencies }}' and remove the loop. This feature will be removed in version 2.11. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
- name: 'Install system dependencies'
apt:
name: "{{ item.name | default(item) }}"
state: "{{ item.state | default('present') }}"
with_items: "{{ apt_dependencies }}"
This allows me to do
apt_dependencies:
- name: curl
state: absent
- name: ntp
state: present
- docker
It suggests replacing name with "{{ apt_dependencies }}" but that wont work right with the custom name/state
I am doing this so I can install dependencies as well as remove any I don't want on the server
Any ideas how to change this to work without having a warning, which I get I can turn off but I'd rather fix it before its removed
It suggests replacing name with "{{ apt_dependencies }}" but that wont work right with the custom name/state
It will if you batch them into two steps, adds and removals:
- set_fact:
remove_apt_packages: >-
{{ apt_dependencies
| selectattr("state", "==", "absent")
| map(attribute="name")
| list }}
# using "!= absent" allows for the case where the list item
# doesn't say "present" such as your "docker" example
add_apt_packages: >-
{{ apt_dependencies
| selectattr("state", "!=", "absent")
| map(attribute="name")
| list }}
- name: 'Remove system dependencies'
apt:
name: "{{ remove_apt_packages }}"
state: absent
- name: 'Install system dependencies'
apt:
name: "{{ add_apt_packages }}"
state: present

Ansible - Access output of a shell command with with_items

I wrote a python script which gets executed via my ansible playbook and returns the following output via stdout:
- { username: ansible, admin: yes}
- { username: test, admin: no }
The output then should get saved in the variable "users" and with the "with_items" (or the newer "loop" conditional) I want to iterate through the variable in order to assign the right permissions for each user separately:
- name: execute python script
command: "python3 /tmp/parse_string.py --user_permissions={{ user_permissions }}"
register: output
- name: register
set_fact:
users: "{{ output.stdout }}"
- name: output
debug: msg="{{ users }}"
- name: Add user to group -admin
user:
name={{ item.username }}
groups=admin
append=yes
state=present
when: "item.admin == yes"
with_items: '{{users}}
However when launching the playbook it says that the variable "users" has no attribute "username".
TASK [create_users : output] ***************************************************
ok: [ansible] => {
"msg": "- { username: ansible, admin: yes }\n- { username: test, admin: no }\n- { username: test2, admin: no }"
}
TASK [create_users : Add user to group -admin ***************
fatal: [ansible]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'username'\n\nThe error appears to be in '***': line 29, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: \n ^ here\n"}
Can anyone help me with this case?
BR
You are setting your users var to a string. It happens that this string is a yaml representation of a datastructure but ansible has no clue about that at this point.
To achieve your requirement, you need to parse that string as yaml and register the result. Luckily, there is a from_yaml filter for that purpose
You just have to modify your set_fact task as the following and everything should work as expected:
- name: register
set_fact:
users: "{{ output.stdout | from_yaml }}"

how to create Ansible playbook to obtain OS versions of the remote hosts?

I'm new to ansible. I have a requirement that requires me to pull OS version for of more than 450 linux severs hosted in AWS. AWS does not provide this feature - it rather suggests us to get it from puppet or chef.
I created few simple playbooks which does not run
---
- hosts: testmachine
user: ec2-user
sudo: yes
tasks:
- name: Update all packages to latest
yum: name=* state=latest
task:
- name: obtain OS version
shell: Redhat-release
playbook should output a text file with hostname and OS version. Any insight on this will be highly appreciated.
Use one of the following Jinja2 expressions:
{{ hostvars[inventory_hostname].ansible_distribution }}
{{ hostvars[inventory_hostname].ansible_distribution_major_version }}
{{ hostvars[inventory_hostname].ansible_distribution_version }}
where:
hostvars and ansible_... are built-in and automatically collected by Ansible
ansible_distribution is the host being processed by Ansible
For example, assuming you are running the Ansible role test_role against the host host.example.com running a CentOS 7 distribution:
---
- debug:
msg: "{{ hostvars[inventory_hostname].ansible_distribution }}"
- debug:
msg: "{{ hostvars[inventory_hostname].ansible_distribution_major_version }}"
- debug:
msg: "{{ hostvars[inventory_hostname].ansible_distribution_version }}"
will give you:
TASK [test_role : debug] *******************************************************
ok: [host.example.com] => {
"msg": "CentOS"
}
TASK [test_role : debug] *******************************************************
ok: [host.example.com] => {
"msg": "7"
}
TASK [test_role : debug] *******************************************************
ok: [host.example.com] => {
"msg": "7.5.1804"
}
In a structured way:
- hosts: all
become: no
vars:
output_file: os.csv
tasks:
- block:
# For permisison setup.
- name: get current user
command: whoami
register: whoami
run_once: yes
- name: clean file
copy:
dest: "{{ output_file }}"
content: 'hostname,distribution,version,release'
owner: "{{ whoami.stdout }}"
run_once: yes
- name: fill os information
lineinfile:
path: "{{ output_file }}"
line: "{{ ansible_hostname }},\
{{ ansible_distribution }},\
{{ ansible_distribution_version }},\
{{ ansible_distribution_release }}"
# Tries to prevent concurrent writes.
throttle: 1
delegate_to: localhost
Creates a comma separated file named os.csv in execution folder. You can use any variables you want editing line:.
Ansible already provides a lot of information about the remote host in the "hostvars" variable that is automatically available.
To see information of your host named "my_remote_box_name", e.g. do
- debug: var=hostvars['my_remote_box_name']
Some OS information is in
hostvars['my_remote_box_name']['ansible_lsb']
Which, for one of my ubuntu hosts would be along:
{
"hostvars['my_remote_box_name']['ansible_lsb']": {
"codename": "xenial",
"description": "Ubuntu 16.04.1 LTS",
"id": "Ubuntu",
"major_release": "16",
"release": "16.04"
}
You can just use those variables in your playbooks and templates, using the "{{ variable_name }}" notation.
- debug: msg="My release is {{ansible_lsb.release}}"
output:
"msg": "My release is 16.04"
For a couple of windows instances:
- debug:
msg:
- "ansible_distribution {{ hostvars[inventory_hostname].ansible_distribution }}"
- "major version {{ hostvars[inventory_hostname].ansible_distribution_major_version }}"
- "version {{ hostvars[inventory_hostname].ansible_distribution_version }}"
gives:
ok: [server1] => {
"msg": [
"ansible_distribution Microsoft Windows Server 2008 R2 Standard ",
"major version 6",
"version 6.1.7601.65536"
]
}
ok: [server2] => {
"msg": [
"ansible_distribution Microsoft Windows Server 2016 Standard",
"major version 10",
"version 10.0.14393.0"
]
}
"AWS does not provide this feature " - you can check file /etc/os-release to get details of aws instance.
For example
[ec2-user#ip-xx-xx-xx ~]$ cat /etc/os-release
NAME="Amazon Linux AMI"
VERSION="2016.03"
ID="amzn"
ID_LIKE="rhel fedora"
VERSION_ID="2016.03"
PRETTY_NAME="Amazon Linux AMI 2016.03"
ANSI_COLOR="0;33"
CPE_NAME="cpe:/o:amazon:linux:2016.03:ga"
HOME_URL="http://aws.amazon.com/amazon-linux-ami/"
- name: obtain OS version
shell: Redhat-release
register: result
- name: print OS version
debug: var=result.stdout

Resources