Set_fact dynamic variable name inside loop - ansible

I have a list of accounts, a dictionary mappedsecrets which has the accounts as keys, and a list of secrets as values and finally a dictionary secrets contains the secret-names and secret-values
#variables
accounts:
acc_one
acc_two
mappedsecrets:
acc_one:
- keyone
- keytwo
acc_two:
- keythree
- keyfour
secrets:
keyone: secret_1
keytwo: secret_2
keythree: secret_3
keyfour: secret_4
I have an include_task looping over accounts, with loop_var:account.
Inside the loop, I want to create a dict all key - secrets that the account has mapped:
so for example:
acc_one:
keyone: secret_1
keytwo: secret_2
I went the set_fact route, with the combine filter.
- name: "Create dict of account secrets"
set_fact:
account_secrets: "{{ account_secrets |default({}) | combine( {item: secrets[item]} ) }}"
with_items:
- "{{ mappedsecrets[account] }}"
The problem is:
each loop just keeps appending the account_secrets variable, which causes the last account have all the secrets from the previous iterations.
(correct me if i'm wrong) I've read that it's not possible to reset facts inside an ansible-play.
So I figure I can make dynamic names, based on the loop iteration: like account_secrets_{{ ansible_loop.index }}
But I'm stuck figuring out the correct syntax for the below:
set_fact:
account_secrets_{{ ansible_loop.index }}: "{{ account_secrets_{{ ansible_loop.index }} |default({}) | combine( {item: secrets[item]} ) }}"

I figured it out:
set_fact:
account_secrets_{{ ansible_loop.index }}: "{{ lookup('vars', 'account_secrets_' + ansible_loop.index|string) |default({}) | combine( {item: secrets[item]} ) }}"
I'm still curious tho, is there a more 'ansible' way to achieve this use-case?

Related

ansible Iterate over list in list

I'm trying to adopt the task in the last answer from this post Ansible - managing multiple SSH keys for multiple users & roles
My variable looks like:
provisioning_user:
- name: ansible
state: present
ssh_public_keyfiles:
- ansible.pub
- user.pub
- name: foo
state: present
ssh_public_keyfiles:
- bar.pub
- key.pub
and my code looks like
- name: lookup ssh pubkeys from keyfiles and create ssh_pubkeys_list
set_fact:
ssh_pubkeys_list: "{{ lookup('file', item.ssh_public_keyfiles) }}"
loop: "{{ provisioning_user }}"
register: ssh_pubkeys_results_list
I want to store keys under the files directory and assign them with the variable to different users so that when a key changes, i only have to change the file and run the playbook instead of changing it in any hostvars file where the old key is used. But I get the following error and dont know how to solve it. I want to do this for every user defined in provisioning_user
fatal: [cloud.xxx.xxx]: FAILED! =>
msg: 'An unhandled exception occurred while running the lookup plugin ''file''. Error was a <class ''AttributeError''>, original message: ''list'' object has no attribute ''startswith''. ''list'' object has no attribute ''startswith'''
Can anyone please help me?
The file lookup reads the content of a single file, but you're passing it a list. That's not going to work, and is the direct cause of the error message you've reported.
You're also using both set_fact and register in the same task, which doesn't make much sense: the whole point of a set_fact task is to create a new variable; you shouldn't need to register the result.
Your life is going to be complicated by the fact that each user can have multiple key files. We need to build a data structure that maps each user name to a list of keys; we can do that like this:
- name: lookup ssh pubkeys from keyfiles
set_fact:
pubkeys: >-
{{
pubkeys |
combine({
item.0.name: pubkeys.get(item.0.name, []) + [lookup('file', item.1)]})
}}
vars:
pubkeys: {}
loop: "{{ provisioning_user|subelements('ssh_public_keyfiles') }}"
This creates the variable pubkeys, which is a dictionary that maps usernames to keys. Assuming that our provisioning_user variable looks like this:
provisioning_user:
- name: ansible
state: present
ssh_public_keyfiles:
- ansible.pub
- user.pub
- name: foo
state: present
ssh_public_keyfiles:
- bar.pub
- key.pub
- name: bar
state: present
ssh_public_keyfiles: []
After running the above task, pubkeys looks like:
"pubkeys": {
"ansible": [
"ssh-rsa ...",
"ssh-rsa ..."
],
"foo": [
"ssh-rsa ...",
"ssh-rsa ..."
]
}
We can use pubkeys in our authorized_keys task like this:
- name: test key
authorized_key:
user: "{{ item.name }}"
key: "{{ '\n'.join(pubkeys[item.name]) }}"
comment: "{{ item.key_comment | default('managed by ansible') }}"
state: "{{ item.state | default('true') }}"
exclusive: "{{ item.key_exclusive | default('true') }}"
key_options: "{{ item.key_options | default(omit) }}"
manage_dir: "{{ item.manage_dir | default('true') }}"
loop: "{{ provisioning_user }}"
when: item.name in pubkeys
I think your life would be easier if you were to rethink how you're managing keys. Instead of allowing each user to have a list of multiple key files, have a single public key file for each user -- named after the username -- that may contain multiple public keys.
That reduces your provisioning_user data to:
provisioning_user:
- name: ansible
state: present
- name: foo
state: present
- name: bar
state: present
And in our files/ directory, we have:
files
├── ansible.keys
└── foo.keys
You no longer need the set_fact task at all, and the authorized_keys task looks like:
- name: test key
authorized_key:
user: "{{ item.name }}"
key: "{{ keys }}"
comment: "{{ item.key_comment | default('managed by ansible') }}"
state: "{{ item.state | default('true') }}"
exclusive: "{{ item.key_exclusive | default('true') }}"
key_options: "{{ item.key_options | default(omit) }}"
manage_dir: "{{ item.manage_dir | default('true') }}"
when: >-
('files/%s.keys' % item.name) is exists
vars:
keys: "{{ lookup('file', '%s.keys' % item.name) }}"
loop: "{{ provisioning_user }}"
Note that in the above the when condition requires an explicit path, while the file lookup will implicitly look in the files directory.
These changes dramatically simplify your playbook.

Ansible: tasks to append number to name tag

I'm trying to append a number to the end of the instance name tag, i have the following code but there's a problem with the second task which i cannot find, and i've not been able to find an example of anyone else trying to solve this problem.
I'm also relatively new to Ansible and cannot find the relevant documentation to do certain things like how to lookup a value in a list like how i'm doing with my until: which is probably invalid syntax
Ansible is 2.9 and runs on the instance itself
The Tasks I have are setup to do the following
Get a list of running EC2 instances that belong to the same launch template
Loop the same amount of times as the instance list until the desired name based on item index is not found in the name tags of the ec2 list
Set the updated tag name
Current Code:
---
- name: "{{ role_name }} | Name Instance: Gather facts about current LT instances"
ec2_instance_info:
tags:
"aws:autoscaling:groupName": "{{ tag_asg }}"
"aws:ec2launchtemplate:id": "{{ tag_lt }}"
Application: "{{ tag_application }}"
filters:
instance-state-name: [ "running" ]
no_log: false
failed_when: false
register: ec2_list
- name: "{{ role_name }} | Name Instance: Generate Name"
with_sequence: count="{{ ec2_list.instances|length }}"
until: not "{{ tag_default_name }} {{ '%02d' % (item + 1) }}" in ec2_list.instances[*].tags['Name']
when: tag_name == tag_default_name
no_log: false
failed_when: false
register: item
- name: "{{ role_name }} | Name Instance: Append Name Tag"
ec2_tag:
region: eu-west-1
resource: "{{ instance_id }}"
state: present
tags:
Name: "{{ tag_default_name }} {{ '%02d' % (item + 1) }}"
when: tag_name == tag_default_name
no_log: false
failed_when: false
As requested here's the error I am getting:
ERROR! no module/action detected in task.
The error appears to be in '/app/deploy/Ansible/roles/Boilerplate/tasks/name_instance.yml': line 14, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: "{{ role_name }} | Name Instance: Generate Name"
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"
The error is not with the name: as i constantly get that error when there's some other error within the task body
You don't appear to have a module listed in the second task. You might be able to use debug as the module, or use set_fact and skip the register line.
I think it might also be possible to combine the last two tasks with some more advanced looping, but I'd have to play around with it to give you a working example.
Thanks to bnabbs answer i realised the problem with that version was the lack of module, after fixing that i then finished creating and testing it and now have a fully working set of tasks which has resulted in the following code.
---
- name: "{{ role_name }} | Name Instance: Gather facts about current LT instances"
ec2_instance_info:
filters:
"tag:aws:autoscaling:groupName": "{{ tag_asg }}"
"tag:aws:ec2launchtemplate:id": "{{ tag_lt }}"
"tag:Application": "{{ tag_application }}"
instance-state-name: [ "pending", "running" ]
register: ec2_list
- name: "{{ role_name }} | Name Instance: Set Needed Naming Facts"
set_fact:
tag_name_seperator: " "
start_index: "{{ (ec2_list.instances | sort(attribute='instance_id') | sort(attribute='launch_time') | map(attribute='instance_id') | list).index(instance_id) }}"
name_tag_list: "{{ (ec2_list.instances | map(attribute='tags') | list) | map(attribute='Name') | list }}"
# Generate Name from starting index of array and mod to the length of the amount of instances to help prevent conflicts when multiple instances are launched at the same time
- name: "{{ role_name }} | Name Instance: Generate Name"
set_fact:
desired_tag_name: "{{ tag_default_name }}{{ tag_name_seperator }}{{ '%02d' % (((item|int) + (start_index|int) % (name_tag_list|length)) + 1) }}"
loop: "{{ name_tag_list }}"
until: not "{{ tag_default_name }}{{ tag_name_seperator }}{{ '%02d' % (((item|int) + (start_index|int) % (name_tag_list|length)) + 1) }}" in name_tag_list
- name: "{{ role_name }} | Name Instance: Append Name Tag"
ec2_tag:
region: eu-west-1
resource: "{{ instance_id }}"
state: present
tags:
Name: "{{ desired_tag_name }}"

Ansible Registers - Dynamic naming

I am trying to use a register in Ansible playbook to store my output. Below is the code which i am using.
I have tried below code
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: {{ item }}_service_status
with_items:
- XXX
- YYY
- ZZZ
I need different outputs to be stored in different register variables based on the items as mentioned in the code. It is failing and not able to proceed. Any help would be appreciated.
Updated answer
I think you need to put quotes around it:
register: "{{ item }}_service_status"
Or you can use set_fact (1, 2, 3, 4)
register all the output to a single static variable output and then use a loop to iteratively build a new variable service_status (a list) by looping over each item in the static variable output
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: output
with_items:
- XXX
- YYY
- ZZZ
- name: Setting fact using output of loop
set_fact:
service_status:
- rc: "{{ item.rc }}"
stdout: "{{ item.stdout }}"
id: "{{ item.id }}"
with_items:
- "{{ output }}"
- debug:
msg: "ID and stdout: {{ item.id }} - {{ item.stdout }}"
with_items:
- "{{ service_status }}"
Initial Answer
IIUC, this link from the Ansible docs shows how to use register inside a loop (see another example in this SO post).
A couple of points
it may be more convenient to assign the list (XXX, YYY, ZZZ) to a separate variable (eg. 1, 2)
I don't know if this is part of the problem, but with_items is no longer the recommended approach to loop over a variable: instead use loop - see here for an example
vars:
items:
- XXX
- YYY
- ZZZ
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: service_status
loop: "{{ items|flatten(levels=1) }}"
- name: Show the return code and stdout
debug:
msg: "Cmd {{ item.cmd }}, return code {{ item.rc }}, stdout {{ item.stdout }}"
when: item.rc != 0
with_items: "{{ service_status.results }}"

How to encapsulate ansible filters?

I have an ansible variable that contains a list of win_uri responses (created by loop).
I want to create a dictionary where each single response body (json) contains a value (title) that I want to use as a key and another one as a value (id).
Right now I am lost.
My current implementation ignores the json - which obviously does not work:
- name: populate folder dictionary
set_fact:
app_folders: "{{ app_folders | default({}) | combine({item.jsonContent.title : item.id}) }}"
with_items: "{{ response.results }}"
I know, that it is possible to read JSON into a variable with the from_json - but I do not know how to combine it with the above code.
If I got your question right, try:
- name: populate folder dictionary
set_fact:
app_folders: "{{ app_folders | default({}) | combine({(item.jsonContent|from_json).title : item.id}) }}"
with_items: "{{ response.results }}"

Ansible: Access facts set by set_fact

I need to be able to set variables using tasks in Ansible. I use set_fact for this, but cannot seem to access the fact I set with this. What is wrong with the code below:
- name: kludge1
set_fact: fake_y = "{{ [] }}"
- name: Loop
debug:
msg: "{{ item }}"
with_items: "{{ fake_y }}"
You have spaces before and after =...
- name: kludge1
set_fact: fake_y="{{ [] }}"
Avoid var= shortcut syntax. Use original YAML syntax instead, it gives less errors:
- name: kludge1
set_fact:
fake_y: "{{ [] }}"

Resources