Ansible blockinfile re-print - ansible

I am using Ansible's blockinfile feature to insert user's public keys into authorized_keys file. Keys are stored as variable in group_vars.
Inserting works fine, but is there a way to ask blockinfile to print blocks from the beginning every time? What I mean is, if I remove one key from variable, and run playbook it still exists in authorized file, because blockinfile is only printing once.
Can I make it print whole variable again everytime?
Playbook:
- name: Add root authorized keys
blockinfile:
state: present
path: /root/.ssh/authorized_keys
block: |
{{ item.key }} {{ item.label }}
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.label }}"
backup: yes
with_items: "{{ root_keys }}"
This is how variable looks like:
root_keys:
- { label: somelabel, key: somekey }
- { label: somelabel2, key: somekey2 }
So what I am trying to achieve is that when I remove somekey2 from root_keys variable, it will disappear from authorized_keys file.
Can this be done?

You should use a native Ansible authorized_key for these operations.
As for the question itself if you plan to reorganise root_keys in the following way:
root_keys:
- { label: somelabel, key: somekey }
- { label: somelabel2 }
You can redefine the task to:
- name: Add root authorized keys
blockinfile:
state: "{{ (item.key is defined ) | ternary('present', 'absent') }}"
path: /root/.ssh/authorized_keys
block: |
{{ item.key | default(omit) }} {{ item.label }}
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.label }}"
backup: yes
with_items: "{{ root_keys }}"
If you want to remove the whole element { label: somelabel2, key: somekey2 }, then you should store all possible label-values in a separate list and iterate over that list checking if an element is present in a union or difference of that all-attributes-list and root_keys|map(attribute='label') to determine if a value should be included or not;
A bad practice-variation would be to parse the file to create this list by parsing the destination file in search for ANSIBLE MANAGED BLOCK. But that, on the other hand would be the only reason you might want to use blockinfile instead of authorized_key module.

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.

Is there a loopable way to only execute a task when a file exists in Ansible?

I'm looking for an easy way to add public keys to the authorized_keys file, if the key for the user is present in a specific directory. Right now it throws an error if the public key doesn't exist.
I want to create users on systems and push their public keys. For that, I am using the authorized_key module:
- name: Add pubkeys
ansible.posix.authorized_key:
user: "{{ item.username }}"
state: present
key: "{{ lookup('file', '~/ap/ansible/sonderfiles/{{ item.username }}_pubkey.pub') }}"
loop: "{{ userlist }}"
I found threads mentioning the module stat but I cant figure out a way to iterate through a list of files and use the results in a when condition in the authorized_key module.
This can be achieve with a condition and an is file test.
This said, there is a little trick to it, like in maths, some operators are taking precedence on others, and in this case, the is operator of the test is taking precedent on the concatenation operator ~.
So, the trick is to put the concatenated path in parenthesis:
- name: Add pubkeys
ansible.posix.authorized_key:
user: "{{ item.username }}"
state: present
key: >-
{{ lookup(
'file',
'~/ap/ansible/sonderfiles/' ~ item.username ~ '_pubkey.pub'
) }}
loop: "{{ userlist }}"
when: "('~/ap/ansible/sonderfiles/' ~ item.username ~ '_pubkey.pub') is file"

Loop variable is not appending file

When i use blockinfile loop to append /etc/environment file it only adds last key and value of item from loop variable rather then adding all of it.
I am trying to modify files using Blockinfile module in roles main.yml:
- name: Add proxy to global /etc/environments
blockinfile:
path: "/etc/environment"
block: |
export {{item.key}}={{item.value}}
loop: "{{proxy_details}}"
my vars/main.yaml looks like this:
proxy_details:
- key: http_proxy
value: "http://"{{ProxyHost}}":"{{ProxyPort}}""
- key: https_proxy
value: "http://"{{ProxyHost}}":"{{ProxyPort}}""
my group_vars/all looks like this:
ProxyHost: test.com
ProxyPort: 9999
See the last example in the documentaion at https://docs.ansible.com/ansible/latest/modules/blockinfile_module.html. You need to use a custom marker for each item so Ansible knows where each one is in the file to replace it.
Per documentation note:
When using ‘with_*’ loops be aware that if you do not set a unique mark the block will be overwritten on each iteration.
The example is:
- name: Add mappings to /etc/hosts
blockinfile:
path: /etc/hosts
block: |
{{ item.ip }} {{ item.name }}
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.name }}"
loop:
- { name: host1, ip: 10.10.1.10 }
- { name: host2, ip: 10.10.1.11 }
- { name: host3, ip: 10.10.1.12 }
You could modify yours to be:
- name: Add proxy to global /etc/environments
blockinfile:
path: "/etc/environment"
marker: "# {mark} ANSIBLE MANAGED BLOCK FOR {{item.key}}"
block: |
export {{item.key}}={{item.value}}
loop: "{{proxy_details}}"

How to use ansible blockinfile on multiple files?

hi I would like to use the blockinfile function on multiple files going to change blocks of text in each of them.
unfortunately blockinfile does not support this function. can someone help me?
To use blockinfile on multiple files to change blocks of text ...
you might want to create templates and loop the blockinfile module.
- blockinfile:
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.template }}"
create: yes
path: "{{ item.file }}"
block: "{{ lookup('template', item.template) }}"
loop: "{{ files_templates }}"
You can do it like this:
- name: Add same block of text in multiple files/paths
blockinfile:
path: "{{ item.path }}"
marker: "###### {mark} Ansible Config #####"
insertafter: EOF
state: present
block: |
# Some random text comment
Some random command1
Some random command2
with_items:
- {path: '/your/path/one'}
- {path: '/your/path/two'}

loop control with digital_ocean ansible module

Thanks to other stackoverflow users, I have managed to pull some data out of a variable registered by the digital_ocean ansible module. I attempted to use loop_control to print only part of the huge variable that is registered. Here is an extract from the role:
- name: Add droplet
digital_ocean: >
{ some parameters }
with_dict: "{{ droplets_up }}"
register: my_droplet
- debug: msg="Droplet IP is {{ item.droplet.ip_address }}"
with_items: "{{ my_droplet.results }}"
loop_control:
label: "{{ item }}"
I'm obviously doing it wrong here, as it prints the whole variable as well as the debug message. I don't quite understand loop_control at this point, but does anyone know if it's possible to use it in this manner with this module?
debug action has result['_ansible_verbose_always'] = True, so it will always print full item, no matter what your label is (although label: "{{item}}" doesn't change anything, try label: "{{ item.droplet.ip_address }}").
If you just need to list all your IP addresses, use map filter and single debug statement:
- name: Print droplets IP
debug:
msg: "{{ my_droplet.results | map(attribute='droplet.ip_address') | list }}"

Resources