Search code examples
ansiblejinja2

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?


Solution

  • 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.