ansiblenatural-sortversion-sort

How to version sort by an attribute of list members


I am trying to get a list sorted properly by the "name" field in the below code sample:

---
  - hosts: localhost
    vars:
      hosts:
        - name: host2
          uptime: 1d
        - name: host10
          uptime: 45d
        - name: host1
          uptime: 3m

    tasks:
    - name: version sort host list
      debug:
        #var: hosts | community.general.version_sort
        #var: hosts | dictsort(false, 'value')
        var: hosts | sort(attribute='name')

As you can see, it does not sort the hostnames properly (host2 should come before host10). I looked up the version_sort filter, but it does not support sorting by attribute. I understand that I wouldn't have been in this situation if the hostnames were properly padded. But it's what it is. I searched and did not see this asked. Any other ideas?

TASK [version sort host list] *************************************
ok: [localhost] => {
    "hosts | sort(attribute='name')": [
        {
            "name": "host1",
            "uptime": "3m"
        },
        {
            "name": "host10",  <-------
            "uptime": "45d"
        },
        {
            "name": "host2",
            "uptime": "1d"
        }
    ]
}

Summary:

Thanks to @Vladimir Botka for all the options! I consolidated choice #3 and came up with the playbook below. Note, I've updated the list of dicts to make it slightly more complex with the fqdn. But the solution works:

- hosts: localhost

  vars:
    hosts:
      - {name: host2.example.com, uptime: 1d}
      - {name: host10.example.com, uptime: 45d}
      - {name: host1.example.com, uptime: 3m}
      - {name: host3.example.com, uptime: 3m}
      - {name: host15.example.com, uptime: 45d}
      - {name: host20.example.com, uptime: 45d}
  tasks:
#    - debug:
#        msg: 
#        - "index: {{ hosts | map(attribute='name') | community.general.version_sort }}"
#        - "host_indexed: {{ dict(hosts|json_query('[].[name,@]')) }}"
#        - "solution: {{ (hosts | map(attribute='name') | community.general.version_sort) | map('extract', dict(hosts|json_query('[].[name,@]'))) }}"
    - debug: 
        var: (hosts | map(attribute='name') | community.general.version_sort) | map('extract', dict(hosts|json_query('[].[name,@]'))) 

Here is the result:

PLAY [localhost] *****************************************************************************************************************************************
    
TASK [Gathering Facts] ***********************************************************************************************************************************
ok: [localhost]
    
TASK [debug] *********************************************************************************************************************************************
[WARNING]: Collection community.general does not support Ansible version 2.14.17
ok: [localhost] => {
    "(hosts | map(attribute='name') | community.general.version_sort) | map('extract', dict(hosts|json_query('[].[name,@]')))": [
        {
            "name": "host1.example.com",
            "uptime": "3m"
        },
        {
            "name": "host2.example.com",
            "uptime": "1d"
        },
        {
            "name": "host3.example.com",
            "uptime": "3m"
        },
        {
            "name": "host10.example.com",
            "uptime": "45d"
        },
        {
            "name": "host15.example.com",
            "uptime": "45d"
        },
        {
            "name": "host20.example.com",
            "uptime": "45d"
        }
    ]
}
    
PLAY RECAP ***********************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Solution

  • There are more options:

    1. Create an index. The declaration below removes 'host' from the string. Fit it to your needs
      index: "{{ hosts | map(attribute='name')
                       | map('regex_replace', 'host', '')
                       | map('int') }}"
    

    gives

      index: [2, 10, 1]
    

    Note: For more sofisticated patterns use Python regular expressions. For example, the declarations below give the same result

      regex: '^\D*(\d+)$'
      replace: '\1'
      index: "{{ hosts | map(attribute='name')
                       | map('regex_replace', regex, replace)
                       | map('int') }}"
    

    Combine the index

      hosts_indexed: "{{ index | map('community.general.dict_kv', 'index')
                               | zip(hosts)
                               | map('flatten')
                               | map('combine') }}"
    

    gives

      hosts_indexed:
          - {index: 2, name: host2, uptime: 1d}
          - {index: 10, name: host10, uptime: 45d}
          - {index: 1, name: host1, uptime: 3m}
    

    Now, sort the list

      result: "{{ hosts_indexed | sort(attribute='index') }}"
    

    gives

      result:
          - {index: 1, name: host1, uptime: 3m}
          - {index: 2, name: host2, uptime: 1d}
          - {index: 10, name: host10, uptime: 45d}
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        hosts:
          - {name: host2, uptime: 1d}
          - {name: host10, uptime: 45d}
          - {name: host1, uptime: 3m}
    
        index: "{{ hosts | map(attribute='name')
                         | map('regex_replace', 'host', '')
                         | map('int') }}"
        hosts_indexed: "{{ index | map('community.general.dict_kv', 'index')
                                 | zip(hosts)
                                 | map('flatten')
                                 | map('combine') }}"
        result: "{{ hosts_indexed | sort(attribute='index') }}"
    
      tasks:
    
        - debug:
            var: index | to_yaml
    
        - debug:
            var: hosts_indexed | to_yaml
    
        - debug:
            var: result | to_yaml
    

    1. Create a dictionary from the index
      hosts_indexed: "{{ dict(index | zip(hosts)) }}"
    

    gives

      hosts_indexed:
        1: {name: host1, uptime: 3m}
        2: {name: host2, uptime: 1d}
        10: {name: host10, uptime: 45d}
    

    Now, sort the dictionary

      result: "{{ hosts_indexed | dict2items
                                | sort(attribute='key')
                                | map(attribute='value') }}"
    

    gives the same result, but without the attribute 'index'

      result:
        - {name: host1, uptime: 3m}
        - {name: host2, uptime: 1d}
        - {name: host10, uptime: 45d}
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        hosts:
          - {name: host2, uptime: 1d}
          - {name: host10, uptime: 45d}
          - {name: host1, uptime: 3m}
    
        index: "{{ hosts | map(attribute='name')
                         | map('regex_replace', 'host', '')
                         | map('int') }}"
        hosts_indexed: "{{ dict(index | zip(hosts)) }}"
        result: "{{ hosts_indexed | dict2items
                                  | sort(attribute='key')
                                  | map(attribute='value') }}"
    
      tasks:
    
        - debug:
            var: index | to_yaml
    
        - debug:
            var: hosts_indexed | to_yaml
    
        - debug:
            var: result | to_yaml
    

    1. Create a custom filter. For example,
    shell> cat filter_plugins/my_sort.py 
    from distutils.version import LooseVersion
    
    
    def my_sort(l):
        return sorted(l, key=LooseVersion)
    
    
    class FilterModule(object):
        def filters(self):
            return {
                'my_sort': my_sort,
                }
    

    Use it to sort the names

      index: "{{ hosts | map(attribute='name') | my_sort }}"
    

    gives

      index: [host1, host2, host10]
    

    Use the attribute 'name' as a key

      hosts_indexed: "{{ dict(hosts | json_query('[].[name, @]')) }}"
    

    gives

      hosts_indexed:
        host1: {name: host1, uptime: 3m}
        host10: {name: host10, uptime: 45d}
        host2: {name: host2, uptime: 1d}
    

    Now, use the list 'index' to extract the sorted list

      result: "{{ index | map('extract', hosts_indexed) }}"
    

    gives

      result:
        - {name: host1, uptime: 3m}
        - {name: host2, uptime: 1d}
        - {name: host10, uptime: 45d
    

    Example of a complete playbook for testing

    - hosts: localhost
    
      vars:
    
        hosts:
          - {name: host2, uptime: 1d}
          - {name: host10, uptime: 45d}
          - {name: host1, uptime: 3m}
    
        index: "{{ hosts | map(attribute='name') | my_sort }}"
        hosts_indexed: "{{ dict(hosts | json_query('[].[name, @]')) }}"
        result: "{{ index | map('extract', hosts_indexed) }}"
    
      tasks:
    
        - debug:
            var: index | to_yaml
    
        - debug:
            var: hosts_indexed | to_yaml
    
        - debug:
            var: result | to_yaml