Creating a fact array

I had another topic about using a fact in a loop, but since this question is slightly different, I figured I’d start a new topic.

Background:
I’m using the IPA API calls using the uri module to get a list of users in our IPA server. Once I get the uses via user_find, I set a fact of just the uid’s. This works well. Then I am using that fact to use the user_show API via uri. This also works, but since we have over 1600 users, it takes a bit of time. So I’m looping through the uid list to process the first 20.

Issue:

I am registering the user_show data in a variable called user_show. I’m trying to set a fact that has the uid, password expiration, and the age calculation. The problem I have is that the fact has the information from the last user processed, and is not an array of data as I expect. Here’s how I’m setting the facts:

  • name: Run user_show from IDM API using previously stored session cookie
    uri:
    url: “https://{{idmfqdn}}/ipa/session/json”
    method: POST
    headers:
    Cookie: “{{ login.set_cookie }}”
    Referer: “https://{{idmfqdn}}/ipa”
    Content-Type: “application/json”
    Accept: “application/json”
    body_format: json
    body: “{"method": "user_show","params": [[ "{{ uid[item|int] }}"],{"all": true,"version": "{{ api_vers }}"}]}”
    register: user_show
    with_sequence: start=0 end=19

  • name: Set user_show fact
    set_fact:
    users:

  • “{{ (user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’) }}”

  • “{{ (ansible_date_time.epoch|int - ((user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’))|int) / (606024) }}”

  • “{{ user_show.results[item|int].json.result.result.uid[0] }}”

  • name: Print users fact <------------------- This shows the last user and not the previous 19
    debug:
    msg: “{{ users }}”

So how can I accomplish this?

Thanks,
Harry

Hi there,
You can use the debug module directly to display the required output . This can be done using with_together in a loop as shown below

debug:
msg: “{{ item.0 }}{{ item.1 }}{{ item.2 }}”
with_together:

  • “{{ (user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’) }}”
  • “{{ (ansible_date_time.epoch|int - ((user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’))|int) / (606024) }}”
  • “{{ user_show.results[item|int].json.result.result.uid[0] }}”

If you need to store in a fact you can use the set_fact instead of debug module as seen below

set_fact:
users: “{{ item.0 }}{{ item.1 }}{{ item.2 }}”

with_together:

  • “{{ (user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’) }}”
  • “{{ (ansible_date_time.epoch|int - ((user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’))|int) / (606024) }}”
  • “{{ user_show.results[item|int].json.result.result.uid[0] }}”

register: res

set_fact:
userlist: “{{ item }}”
loop: “{{ res.results |map(attribute=‘ansible_facts.users’) |list }}”

Your desired content should now be stored in the userlist variable

Wole

The problem with implementing it that way is that the users’s fact is referencing "user_show.results[item|int], but I can’t add another loop variable. I’m using with_sequence earlier in the playbook for testing to limit the amount of users queried, and that number is needed for the user_show.results array.

Harry

The problem with implementing it that way is that the users’s fact is referencing "user_show.results[item|int], but I can’t add another loop variable. I’m using with_sequence earlier in the playbook for testing to limit the amount of users queried, and that number is needed for the user_show.results array.

You can use loop_control to use names other than item for your loop variables. https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#defining-inner-and-outer-variable-names-with-loop-var

So if I have this:

  • name: Set user_show fact
    set_fact:
    users: “{{ item.0 }}{{ item.1 }}{{ item.2 }}”
    with_together:
  • “{{ user_show.results[item|int].json.result.result.uid[0] }}”
  • “{{ (user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’) }}”
  • “{{ (ansible_date_time.epoch|int - ((user_show.results[item|int].json.result.result.krbpasswordexpiration[0][‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’))|int) / (606024) }}”
    register: res

How do I add/use a loop_control variable to that? Also, with_together seems to have been replaced by loop and the zip filter.

Thanks,
Harry

Hi,
I wanted to know why you are using item to retrieve results - “{{ user_show.results[item|int].json.result.result.uid[0] }}” . since item is useful when you have a loop running. Is it not possible to retrieve the result using json_query or the map filter ?If that can be done , then we won’t need to bring in the loop_control variable.From my experience, its better to get the basic stuff (with_X)to work first before using the latest stuff- loop/zip (filter).

For my testing, I’m calling the user_show IPA API via uri for the first 5 items. The registered variable called “user_show” has the json value of “user_show.results.json.result.result.Y” (where X is 0 for the first user, 1 for the second, etc; and Y is the user property (uid, firstname, etc.). I don’t know how to get/retrieve just the uid property of those 5 users, or just the krbpasswordexpiration of those 5 users. I don’t have to do a loop, but nothing I try seems to let me get past the fact that the results json is an array, and I need to reference the particular items of that array.

Thanks,
Harry

Can you show a sample of user_show.results

I’m including the playbook below, followed by the relevant output. I have redacted the real data values with dummy values, but it should give you an idea of what I’m trying and what I’m getting.

Thanks,
Harry

Playbook:

Hi there,
I created this sample playbook and based on the data you sent.
I used the mail field because that was not embedded in another key/value pair like krbpasswordexpiration but the principle is basically the same.
The difference with your playbook is that I used a variable to register the set_fact . Without that, it would just show the value of the last item in the loop which will overwrite the previous values. By registering the output , I can extract out all the values in the last task.
There are probably other methods but this one works.

tasks:

  • name: lookup
    set_fact:
    fil: “{{ lookup(‘file’, ‘json.json’) }}”
  • name: set
    set_fact:
    users: “{{ fil |json_query(‘[].json.result.result.uid’)|flatten }}"
    pwdexp: "{{ fil |json_query('[
    ].json.result.result.mail’)|flatten }}”
  • name: sf1
    set_fact:
    factone: “{{ item.0 }} {{ item.1 }}”
    with_together:
  • “{{ users }}”
  • “{{ pwdexp }}”
    register: facttwo
  • name: sf2
    set_fact:
    factthree: “{{ facttwo.results | map(attribute=‘ansible_facts.factone’)|list }}”
  • name: debug
    debug:
    msg: “{{ factthree }}”

The Output is displayed below

ASK [sf1] *********************************************************************************************************************************
ok: [localhost] => (item=[‘test.user1’, ‘test.user1@example.com’])
ok: [localhost] => (item=[‘test.user2’, ‘test.user2@example.com’])

TASK [sf2] *********************************************************************************************************************************
ok: [localhost]

TASK [debug] *******************************************************************************************************************************
ok: [localhost] => {
“msg”: [
“test.user1 test.user1@example.com”,
“test.user2 test.user2@example.com
]
}

I finally installed FreeIPA in a VM on my laptop so I could see what you’re seeing. Except for having a self-signed certificate and way less than 1500 users, I think I’ve got something I can test on. A few things to note, though they may not be all that you’re looking for:

  • Exclude your ‘admin’ user. It shouldn’t be an issue, but, just in case. :slight_smile:

  • I used loop: “{{ uid | json_query(‘[:20]’) }}” to limit the ‘user_show’ calls rather than with_sequence:. Not better or worse, just showing another way to do it. Since we’re both using json_query later, it has to be available anyway.

  • Once you’ve got your show_user output, you can go straight to a list of users to delete - with their pw expiration dates - in one json_query. There’s no need to create so many intermediate set_fact lists.
    I didn’t notice before, but you aren’t doing nested loops after all, so all that stuff I mentioned about loop_control is irrelevant. Sorry about the rabbit hole.

Here’s my current playbook to produce a list like

  • uid: user1
    pwdepx: 20201210012032Z

  • uid: user2
    pwdepx: 20181080021425Z

    of just the users who are eligible for deletion. Hope this helps.

  • name: Set fact for users
    set_fact:
    uid: “{{ user_find.json.result.result|map(attribute=‘uid’)|flatten|difference([‘admin’]) }}”

  • name: Run user_show from IDM API using previously stored session cookie
    uri:
    url: “https://{{idmfqdn}}/ipa/session/json”
    validate_certs: “{{ validate_certs }}”
    method: POST
    headers:
    Cookie: “{{ login.set_cookie }}”
    Referer: “https://{{idmfqdn}}/ipa”
    Content-Type: “application/json”
    Accept: “application/json”
    body_format: json
    body:
    method: user_show
    params:

    • “{{ item }}”
  • all: true
    version: “{{ api_vers }}”
    register: user_show
    loop: “{{ uid | json_query(‘[:20]’) }}”

  • name: Print user_show variable
    debug:
    msg: “{{ user_show }}”

  • name: Save “{{ cutoff_days }} days ago” in krbpasswordexpiration.datetime format
    set_fact:
    cutoff_date: ‘{{ lookup(’‘pipe’‘,
    ‘‘date -u --date=“{{ cutoff_days }} days ago” +%Y%m%d000000Z’’) }}’

  • name: Set deletable_users fact
    set_fact:
    deletable_users: “{{ user_show.results |
    json_query(‘[*].json.result.result.{uid: uid[0], pwdexp: krbpasswordexpiration[0].datetime}’) |
    selectattr(‘pwdexp’,‘<’,cutoff_date) | list }}”

  • name: Print deletable_users
    debug:
    msg: “{{ deletable_users }}”

OK, I got that to work following your example. Thanks for that. So the next step I’m trying to get to is to determine if the user’s password has been expired for more than 180 days. A negative value means its not expired, and a positive value means that it is expired. I know you used email, but I’m using password expiration (krbpasswordexpiration), and I am actually calculating the password age in the fact at the same time. So now the playbook looks like this:

  • name: Grab UID/Password Expiration/Disabled Status from user_show
    set_fact:
    users: “{{ user_show.results|json_query(‘[].json.result.result.uid[0]‘)|flatten }}"
    pwdexp: "{{ user_show.results|json_query(’[
    ].json.result.result.krbpasswordexpiration[0]’)|flatten }}”

  • name: Combine data into one fact
    set_fact:
    tmpuserlist1: “{{ item.0 }} {{ (ansible_date_time.epoch|int - ((item.1[‘datetime’] | to_datetime(‘%Y%m%d%H%M%SZ’)).strftime(‘%s’))|int) / (606024) }}”
    with_together:

  • “{{ users }}”

  • “{{ pwdexp }}”
    register: tmpuserlist2

  • name: Set Temporary User List fact
    set_fact:
    userlist: “{{ tmpuserlist2.results|map(attribute=‘ansible_facts.tmpuserlist1’)|list }}”

  • name: Print Final User List
    debug:
    msg: “{{ userlist }}”

Which gives the following:

TASK [Grab UID/Password Expiration/Disabled Status from user_show] ****************************************************************
ok: [ipaserver.example.com]

TASK [Combine data into one fact] *************************************************************************************************
ok: [ipaserver.example.com] => (item=[u’test.user1’, {u’datetime’: u’20220103195934Z’}])
ok: [ipaserver.example.com] => (item=[u’test.user2’, {u’datetime’: u’20200218151047Z’}])

TASK [Set Temporary User List fact] ***********************************************************************************************
ok: [ipaserver.example.com]

TASK [Print Final User List] ******************************************************************************************************
ok: [ipaserver.example.com] => {
“msg”: [
“test.user1 -24.2161574074”,
“test.user2 660.984386574”
]
}

I think that the next step should be to create a list of users who’s age is greater than or equal to 180, then go loop through that list and disable them. But I can’t seem to figure out how to grab the age field now that it’s in the new “userlist” fact. If I try to show each item individually via a loop like this:

  • name: Print Final User List
    debug:
    msg: “{{ item }}”
    loop: “{{ userlist }}”

I get:

TASK [Print Final User List] ******************************************************************************************************
ok: [ipaserver.example.com] => (item=test.user1 -24.2126273148) => {
“msg”: “test.user1 -24.2126273148”
}
ok: [ipaserver.example.com] => (item=test.user2 660.987916667) => {
“msg”: “test.user2 660.987916667”
}

Should I be using the tmpuserlist2 fact that was generated using “with_together” instead for this loop/process?

Thanks,
Harry

So a few things that I tried based on Uto’s suggestion: I changed the cutoff days to 30 for testing, and I only look over the first 10 users. I had to change the < check to >, but when the disabled_users fact is printed, It shows the same user all 10 times, almost as if it added the user 10 times. Any ideas?

Harry

  • name: Run user_show from IDM API using previously stored session cookie
    uri:
    url: “https://{{idmfqdn}}/ipa/session/json”
    method: POST
    headers:
    Cookie: “{{ login.set_cookie }}”
    Referer: “https://{{idmfqdn}}/ipa”
    Content-Type: “application/json”
    Accept: “application/json”
    body_format: json
    body: “{"method": "user_show","params": [[ "{{ uid[item|int] }}"],{"all": true,"version": "{{ api_vers }}"}]}”
    register: user_show
    loop: “{{ uid|json_query(‘[:10]’) }}”

  • name: Save “{{ cutoff_days }}” in Password Expiration format
    set_fact:
    cutoff_date: ‘{{ lookup(’‘pipe’‘, ‘‘date -u --date=“{{ cutoff_days }} days ago” +%Y%m%d000000Z’’) }}’

  • name: Print cutoff_date
    debug:
    msg: “{{ cutoff_date }}”

  • name: Set Disabled Users fact
    set_fact:
    disabled_users: “{{ user_show.results | json_query(‘[*].json.result.result.{uid: uid[0], pwdexp: krbpasswordexpiration[0].datetime}’) | selectattr(‘pwdexp’,‘>’,cutoff_date) | list }}”

  • name: Print disabled users
    debug:
    msg: “{{ disabled_users }}”

Output:

TASK [Print disabled users] *******************************************************************************************************
ok: [ipaserver.example.com] => {
“msg”: [
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
},
{
“pwdexp”: “20220103195934Z”,
“uid”: “test.user1”
}
]
}

So a few things that I tried based on Uto's suggestion: I changed the cutoff days to 30 for testing, and I only look over the first 10 users. I had to change the < check to >, but when the disabled_users fact is printed, It shows the same user all 10 times, almost as if it added the user 10 times. Any ideas?

That is strange. I'll go create some more test users and back-date their pw expiration dates. Hmm.

When you change from with_sequence to loop: “{{ uid|json_query(‘[:10]’) }}”, item is no longer an index into the uid list. It’s actual uids. So your body becomes
body:
method: user_show
params:

    • “{{ item }}”
  • all: true
    version: “{{ api_vers }}”
    register: user_show
    loop: “{{ uid | json_query(‘[:20]’) }}”

(And you don’t have to generate the json string yourself. The module will do that for you. Much easier to read/maintain than
body: “{"method": "user_show","params": [[ "{{ item }}"],{"all": true,"version": "{{ api_vers }}"}]}”
but I fixed that line just in case you like that better.)

Also, change the ‘>’ back to ‘<’. Earlier date strings are ‘<’ later date strings.

Todd,

Thank you a MILLION! Got it to work, and its much simpler too. I can’t thank you enough for going above and beyond the call in helping me out. I’ll keep working on it next week, but for now its doing exactly what I need it to do at this point. And I picked up a few tricks along the way, like the json_query(‘[:20]’) and how to use the difference filter to ignore user names I don’t need to check. Great stuff here!

Thanks!
Harry

I have another quick question: I got this to work and it currently shows ALL disabled accounts. I can also grab the nsaccountlock value, which will show True if the account is already disabled and False if it is not. What’d I’d like to do is filter the results to users who’s password has been expired 180 days or longer (selectattr(‘pwdexp’, “<”,cutoff_date) AND nsaccountlock is false. How do I specify that in the example you gave using selectattr? My “disabled users” fact is currently:

  • name: Set Disabled Users fact
    set_fact:
    disabled_users: “{{ user_show.results | json_query(‘[*].json.result.result.{uid: uid[0], mail: mail[0], disabled: nsaccountlock, pwdexp: krbpasswordexpiration[0].datetime}’) | selectattr(‘pwdexp’,‘<’,cutoff_date) | list }}”

  • name: Print disabled users
    debug:
    msg: “{{ item.uid }} / {{ item.mail }} / {{ item.disabled |ternary(‘Disabled’, ‘Not Disabled’) }} / {{ item.pwdexp }}”
    loop: “{{ disabled_users }}”

Thanks,
Harry

Hi there,
Have you tried using selectattr(‘nsaccountlock’, ‘true’) if the value is logical (True / false) as an extra filter in the set_fact task?
Also a long route could be to compare deletable_users with disabled_users and use the intersection filter
{{ deletable_users | intersect(disabled_users) }}

  • name: Set Disabled Users fact
    set_fact:

disabled_users: “{{ user_show.results | json_query(‘[*].json.result.result.{uid: uid[0], mail: mail[0], disabled: nsaccountlock, pwdexp: krbpasswordexpiration[0].datetime}’) |
selectattr(‘pwdexp’, ‘<’, cutoff_date) |
selectattr(‘disabled’, ‘==’, false) | list }}”

I used “selectattr(‘disabled’,‘equalto’,False)”, but it gave me the desired result. Thanks, I appreciate it!

Harry