Ansible Cisco configuration compliance check for invalid users - ansible

I am attempting to validate a Cisco configuration with Ansible. I want to be able to tell whether any users have been configured other than the valid ones.
Valid users:
username admin,
username readonly
Invalid users:
username secretbackdoor
I have tried to create a list of users, then flag any which are not valid. The code i have so far is as follows:
---
- hosts: cisco
gather_facts: no
tasks:
- name: show run
ios_command:
commands:
- show run
register: cisco_show_run
- name: list_cisco_usernames
set_fact: cisco_usernames="{{ cisco_show_run.stdout[0] | regex_findall('username (\S+)', multiline=True) }}"
- name: print usernames
debug:
msg: {{ item }}
with_items: "{{ cisco_usernames }}"
This will print out the three users. Not sure where to go next.

"Set Theory Filters" might be next option. For example
- hosts: localhost
vars:
valid_users: [admin, readonly]
invalid_users: [secretbackdoor]
cisco_usernames: [admin, readonly, secretbackdoor]
tasks:
- name: Display users not in valid_users
debug:
msg: Not among valid users {{ not_valid }}
when: not_valid|length > 0
vars:
not_valid: "{{ cisco_usernames|difference(valid_users) }}"
- name: Display users in invalid_users
debug:
msg: Among invalid users {{ not_valid }}
when: not_valid|length > 0
vars:
not_valid: "{{ cisco_usernames|intersect(invalid_users) }}"
gives (abridged)
ok: [localhost] =>
msg: Not among valid users ['secretbackdoor']
ok: [localhost] =>
msg: Among invalid users ['secretbackdoor']

Thanks for this. Your solution is working fine. I put in the first option, as I do not always know what the 'incorrect' users are.

Related

How to iterate over a list in a condition

I have two hosts: One in production, another one in test.
test and prod are defined in a fact file available on target hosts (nodes).
[node]
type= prod
or
[node]
type= test
I have the following variables defined:
users:
- username: A
password: password_A
update_password: always
home: /home/A
state: present
nodes: ['test', 'prod']
- username: B
password: passwd_B
update_password: always
home: /home/B
state: present
nodes: ['test']
My A user shall be installed on production and test hosts.
B user only on test host.
Hereafter a role that works fine if i use a single value for nodes definition.
- name: create users
ansible.builtin.user:
name: "{{ item.username }}"
password: "{{ item.password }}"
uid: "{{ item.uid }}"
home: "{{ item.home }}"
create_home: yes
group : "{{ item.group }}"
shell: /bin/bash
state: present
expires: -1
with_items:
- "{{ users }}"
when: item.nodes == ansible_local['myfact']['node"']['type']
I don't know how to loop on each value of the item.nodes list and compare them with the local fact value. item.nodes[0], item.nodes[1], ... I might have other type of host, not only prod and test.
I tried subelements without success.
You don't need to iterate anything in your condition, you can assert that an element is in a list with the in test.
So your condition needs to be
when: ansible_local.myfact.node.type in item.nodes
Q: "A user shall be installed on production and test hosts.
B user only on test host."
A: Condition is not needed. Use filter selectattr and test if a list contains a value. For example, given the inventory for testing
shell> cat hosts
prod type=prod
test type=test
The task
shell> cat pb.yml
- hosts: all
tasks:
- debug:
msg: "Create user {{ item.username }}"
loop: "{{ users|selectattr('nodes', 'contains', type) }}"
loop_control:
label: "{{ item.username }}"
iterates selected users only
TASK [debug] ******************************************************
ok: [prod] => (item=A) =>
msg: Create user A
ok: [test] => (item=A) =>
msg: Create user A
ok: [test] => (item=B) =>
msg: Create user B

How do I access values of Ansible Facts when using a loop?

This works when I don't use a loop:
---
- hosts: localhost
tasks:
- name: Get a cert from gRPC port
get_certificate:
host: "labrouter.abc.com"
port: 50051
delegate_to: localhost
run_once: true
ignore_errors: yes
register: cert
- name: Get Cert Data
debug:
msg: "Not_After: {{ cert.not_after }}"
Output:
TASK [Get Cert Data] ****************************************
ok: [localhost] => {
"msg": "Not After: 20221210143235Z"
}
However, when I try to loop through my hosts like this:
---
- hosts: localhost
tasks:
- name: Get a cert from gRPC port
get_certificate:
host: "{{ item }}"
port: 50051
delegate_to: localhost
run_once: true
ignore_errors: yes
register: cert
loop: "{{ groups['lab'] }}"
- name: Get Cert Data
debug:
msg: "Not After: {{ cert.not_after }}"
I get this error:
The error was: 'dict object' has no attribute 'not_after'
My inventory file contains a single group called "lab" with one device "labrouter.abc.com" associated to it.
I want to be able to add more devices to the "lab" group and run this playbook so I can get the "not_after" value for each device. I can't figure out how to access the "not_after" value for each item in the loop.
Quoting the documentation:
When you use register with a loop, the data structure placed in the variable will contain a results attribute that is a list of all responses from the module. This differs from the data structure returned when using register without a loop
The easiest way to fix your actual problem is to loop over the results:
- name: Get Cert Data
debug:
msg: "Not After: {{ item.not_after }}"
loop: "{{ cert.results }}"
Although this is a direct answer to your question, if the final goal is to take actions on each target in the lab group, see #Jack's answer and drop your loop.
Stop looping, and have Ansible do its implicit looping over the hosts:
---
- hosts: lab
tasks:
- name: Get a cert from gRPC port
get_certificate:
host: "{{ ansible_host }}"
port: 50051
delegate_to: localhost
ignore_errors: yes
register: cert
- name: Get Cert Data
debug:
msg: "Not After: {{ cert.not_after }}"
(As a general "best practices" thing, if you are looping over hosts, there's probably a better way to do it by setting the hosts value for the play to those hosts, and letting Ansible do its implicit looping.)

Making prompted vars usable across the playbook

I have one playbook with multiple tasks that have to run on different hosts.
In the beginning of the play I want to prompt the operator for their credentials which are the same for every host in the play. I want to have those credentials "stored" somewhere so they can be used across the tasks to log in on the provided host(s).
Playbook looks as followed,
---
- name: Ask for credentials
vars_prompt:
- name: username
prompt: "Username?"
- name: password
prompt: "Password?"
tasks:
- set_fact:
username: "{{username}}"
- set_fact:
password: "{{password}}"
- hosts: Host1
vars:
ansible_user: "{{ username }}"
ansible_password: "{{ password }}"
tasks:
- name: Do stuff
- hosts: Host2
vars:
ansible_user: "{{username}}"
ansible_password: "{{password}}"
tasks:
- name: Do stuff
...
From the moment the play hits the first task it will fail with the flowing error,
msg: 'The field ''remote_user'' has an invalid value, which includes an undefined variable. The error was: ''username'' is undefined'
Anyone that has experience in making prompted vars usable across the whole play and all tasks?
Q: "Make prompted vars usable across the whole play and all tasks>"
A: Run the first play with the group of all hosts that should be connected later. Run once the set_fact task. This will create the variables username and password for all hosts in the group.
For example if the group test_jails comprises hosts test_01, test_02, test_03 the play
- hosts: test_jails
vars_prompt:
- name: "username"
prompt: "Username?"
- name: "password"
prompt: "Password?"
tasks:
- set_fact:
username: "{{ username }}"
password: "{{ password }}"
run_once: true
- hosts: test_01
vars:
ansible_user: "{{ username }}"
ansible_password: "{{ password }}"
tasks:
- debug:
msg: "{{ ansible_user }} {{ ansible_password }}"
gives
ok: [test_01] => {
"msg": "admin 1234"
}

How to change userid and get the home directory for that user?

I want to be able to change the userid within a playbook and then get the home directory on a host for that userid. How would I do this?
I know I can change the userid with 'become:' and 'become_user:', but that works at the task level. I can get the home directory with the '{{ lookup('env', 'HOME) }}' variable, but that seems to work at the playbook level. This was my latest attempt:
---
- hosts: all
vars:
user_id: "{{ userid }}"
tasks:
- set_fact:
home_dir: "{{ lookup('env', 'HOME') }}"
become: yes
become_user: "{{ user_id }}"
- debug:
msg: "Variable values: user_id: {{ user_id }} home_dir: {{ home_dir }}"
My command looked like this:
$ ansible-playbook myplaybook.yaml --limit MYHOSTS --extra-vars "userid=myid"
This was the response:
ok: [host-01] => {
"msg": "Variable values: user_id: myid home_dir: /root"
}
ok: [host-02] => {
"msg": "Variable values: user_id: myid home_dir: /root"
}
ok: [host-03] => {
"msg": "Variable values: user_id: myid home_dir: /root"
}
Is there a way I can change the userid to "myid" and get the playbook to know that the home directory is "/home/myid"?
Thanks!
When you use become and become_user, you're actually telling Ansible to change the current user, from what it initially logged-in with.
As per Ansible Docs:
These Directives (become, become_user, etc.) can be set from play to task level, but are overridden by connection variables as they can be host specific.
You must also note that by using the above syntax, you are actually activating a privilege escalation method, to become a non-privileged user. This is rather risky with quite a few limitation. Please read here: Becoming an Unprivileged User.
I hope the following play, will give you clearer picture:
---
- hosts: all
gather_facts: no
remote_user: "{{ userid }}"
tasks:
- name: first_task
register: first_task
shell: "whoami"
- name: become-root
register: become-root
shell: "whoami"
become: yes
become_user: root
- name: final_task
register: final_task
shell: "whoami"
You should run the above, by executing this command:
# ansible-playbook playbook.yaml -i /path/to/inventory/hosts -e "userid=myid"
It will return:
first_task --> myid
become-root --> root
final_task --> myid
Note: You can also override remote_user, separately for each task.
But if you still require to do it your own way, you can try this one:
---
- hosts: all
vars:
user_id: "{{ userid }}"
tasks:
- set_fact:
home_dir: "{{ lookup('env', 'HOME') }}"
become: true
- debug:
msg: "Variable values: user_id: {{ userid }} home_dir: {{ home_dir }}"
when: userid is defined
Run with this command:
# ansible-playbook myplaybook.yaml -i path/to/inventory/hosts --limit MYHOSTS --become-user=myid --extra-vars "userid=myid"
And finally, if none of these did what you were after, try getting the home directory, by using the command module and then retrieve its output by first registering it, and then using {{ registered_name.stdout_lines }}:
- name: user_home
register: user_home
action: command eval echo "~{{ userid }}"
- debug:
msg: "Home path is {{ user_home.stdout_lines }}
Using this last workaround, you don't even need to change user as it prints out the home directory for user userid.
# ansible-playbook myplaybook.yaml -i path/to/inventory/hosts --limit MYHOSTS -e "userid=myid"
Hope it helped; but this very question was asked before. You might be able to find more details here: How to switch a user per task or set of tasks?.

iteration using with_items and register

Looking for help with a problem I've been struggling with for a few hours. I want to iterate over a list, run a command, register the output for each command and then iterate with debug over each unique registers {{ someregister }}.stdout
For example, the following code will spit out "msg": "1" and "msg": "2"
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: result
with_items: "{{ numbers }}"
- debug: msg={{ item.stdout }}
with_items: "{{ result.results }}"
If however, I try and capture the output of a command in a register variable that is named using with_list, I am having trouble accessing the list or the elements within it. For example, altering the code slightly to:
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: "{{ item.name }}"
with_items: "{{ numbers }}"
- debug: var={{ item.name.stdout }}
with_items: "{{ numbers }}"
Gives me:
TASK [debug]
> ******************************************************************* fatal: [localhost]: FAILED! => {"failed": true, "msg": "'unicode
> object' has no attribute 'stdout'"}
Is it not possible to dynamically name the register the output of a command which can then be called later on in the play? I would like each iteration of the command and its subsequent register name to be accessed uniquely, e.g, given the last example I would expect there to be variables registered called "first" and "second" but there aren't.
Taking away the with_items from the debug stanza, and just explicitly defining the var or message using first.stdout returns "undefined".
Ansible version is 2.0.2.0 on Centos 7_2.
Thanks in advance.
OK so I found a post on stackoverflow that helped me better understand what is going on here and how to access the elements in result.results.
The resultant code I ended up with was:
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: echo_out
with_items: "{{ numbers }}"
- debug: msg="item.item={{item.item.name}}, item.stdout={{item.stdout}}"
with_items: "{{ echo_out.results }}"
Which gave me the desired result:
"msg": "item.item=first, item.stdout=1"
"msg": "item.item=second, item.stdout=2"
I am not sure if I understand the question correctly, but maybe this can help:
- debug: msg="{{ item.stdout }}"
with_items: echo_out.results
Please note that Ansible will print each item and the msg both - so you need to look carefully for a line that looks like "msg": "2".

Resources