I have made an ansible script to collect IP in the nginx log and then the IP that has been collected will be analyzed, if a malicious IP is detected it will be blocked.
The idea is that in the ansible script each IP analyzed is not created individually but using a loop function
But I'm having a hard time making a looping script in ansible, can anyone help?
Here is the script that i have created
- name: Execute a command using the shell module
shell: awk '{print $1}' /var/log/nginx/*.log | sort | uniq -c | sort -nr| head -5
register: results
- debug: var=results.stdout_lines
- name: Write Output
local_action: shell echo "{{ results.stdout_lines }}" > /tmp/output
#Collect IP
- name: Check 1st IP
local_action: shell cat /tmp/output | awk '{ print $3 }'| sed 's/.$//'|sed 's/.$//'
register: ip1
- debug: var=ip1.stdout
- name: Check 2nd IP
local_action: shell cat /tmp/output | awk '{ print $6 }'| sed 's/.$//'|sed 's/.$//'
register: ip2
- debug: var=ip2.stdout
- name: Check 3rd IP
local_action: shell cat /tmp/output | awk '{ print $9 }'| sed 's/.$//'|sed 's/.$//'
register: ip3
- debug: var=ip3.stdout
- name: Check 4th IP
local_action: shell cat /tmp/output | awk '{ print $12 }'| sed 's/.$//'|sed 's/.$//'
register: ip4
- debug: var=ip4.stdout
- name: Check 5th IP
local_action: shell cat /tmp/output | awk '{ print $15 }'| sed 's/.$//'|sed 's/.$//'
register: ip5
- debug: var=ip5.stdout
#Anlyze IP
- name: Analyze 1st IP
local_action: shell /usr/bin/abuseipdb -C {{ ip1.stdout }} -o plaintext
register: vb1
- debug: var=vb1.stdout
- name: Analyze 2nd IP
local_action: shell /usr/bin/abuseipdb -C {{ ip2.stdout }} -o plaintext
register: vb2
- debug: var=vb2.stdout
- name: Analyze 3rd IP
local_action: shell /usr/bin/abuseipdb -C {{ ip3.stdout }} -o plaintext
register: vb3
- debug: var=vb3.stdout
- name: Analyze 4th IP
local_action: shell /usr/bin/abuseipdb -C {{ ip4.stdout }} -o plaintext
register: vb4
- debug: var=vb4.stdout
- name: Analyze 5th IP
local_action: shell /usr/bin/abuseipdb -C {{ ip5.stdout }} -o plaintext
register: vb5
- debug: var=vb5.stdout
##Block IP if score more than 25
- name: Check 1rst IP and Block IP if score more than 25
shell: /sbin/route add "{{ ip1.stdout }}" gw 127.0.0.1 lo
when: "{{ vb1.stdout }} >= 25"
- name: Check 2nd IP and Block IP if score more than 25
shell: /sbin/route add "{{ ip2.stdout }}" gw 127.0.0.1 lo
when: "{{ vb2.stdout }} >= 25"
- name: Check 3rd IP and Block IP if score more than 25
shell: /sbin/route add "{{ ip3.stdout }}" gw 127.0.0.1 lo
when: "{{ vb3.stdout }} >= 25"
- name: Check 4th IP and Block IP if score more than 25
shell: /sbin/route add "{{ ip4.stdout }}" gw 127.0.0.1 lo
when: "{{ vb4.stdout }} >= 25"
- name: Check 5th IP and Block IP if score more than 25
shell: /sbin/route add "{{ ip5.stdout }}" gw 127.0.0.1 lo
when: "{{ vb5.stdout }} >= 25"
#Check Route
- name: Make Sure the IP has been blocked
shell: /sbin/route -n
register: route
- debug: var=route.stdout_lines
Here is the /tmp/output
└─$ cat /tmp/output
[' 40545 87.250.224.147', ' 20873 87.250.224.126', ' 16665 213.180.203.67', ' 15420 87.250.224.142', ' 14503 13.81.52.25']
Here is the output when running
└─$ cat /tmp/output | awk '{ print $3 }'| sed 's/.$//'|sed 's/.$//'
87.250.224.147
└─$ cat /tmp/output | awk '{ print $6 }'| sed 's/.$//'|sed 's/.$//'
87.250.224.126
└─$ cat /tmp/output | awk '{ print $9 }'| sed 's/.$//'|sed 's/.$//'
213.180.203.67
└─$ cat /tmp/output | awk '{ print $12 }'| sed 's/.$//'|sed 's/.$//'
87.250.224.142
└─$ cat /tmp/output | awk '{ print $15 }'| sed 's/.$//'|sed 's/.$//'
13.81.52.25
It is probably better to generate a JAML file from your Nginx log in order to use include_vars after that.
Btw: The idea of Ansible is to be platform independent. Because of that, it is better to have most of the platform specific stuff in an external script and not in the playbook. When you change the platform, you just have to migrate the external script, instead of rewriting the playbook.
Another alternative is to use scripted inventories. You can create a dynamic inventory by reading the IP addresses to block from your Nginx log. If you put the hosts to block into a group, you can define a single task for the group and delegate it to the firewall, where you block the hosts. You do not need to care about iterations. It is the job of Ansible to iterate.
This is much easier to maintain, than the playbook in your question. And it is extremely inefficient to put every line of a shell script into one local action. Don't do it.
PS: Yes you can program in a playbook, somehow. But try to avoid it. A playbook is not a useful programming language. You can not abstract with procedures and it has almost no scope. When you work with Ansible you have to change the way you think. Do not think about procedures. Instead thing about data. Take your data and arrange the data in a way, that is suitable for Ansible. If the data does not fit, use Jinja filters to rearrange the data until it fits. But avoid "programming" in a playbook by all means. And if you need to program, better do it in Jinja statements or Python.
so you could use range to loop over your command shell, i show you an example: when you loop with register, it records all output in list.
- name: "tips2"
hosts: localhost
tasks:
- name: Check 1st IP
local_action: shell echo {{ item }}
register: result
loop: "{{ range(0, 4 + 1, 2)|list }}" #loop from 0 to 4 with a step 2
- debug: var=result
#then you build your final list
- set_fact:
ips: "{{ ips | d([]) + item.stdout_lines }}"
loop: "{{ result.results }}"
- debug: var=ips
display variable result:
ok: [localhost] => {
"result": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
:
:
}
},
"item": 0,
"rc": 0,
"start": "2022-01-21 09:38:12.092426",
"stderr": "",
"stderr_lines": [],
"stdout": "0",
"stdout_lines": [
"0"
]
},
{
"ansible_loop_var": "item",
:
:
}
},
"item": 2,
"rc": 0,
"start": "2022-01-21 09:38:12.348007",
"stderr": "",
"stderr_lines": [],
"stdout": "2",
"stdout_lines": [
"2"
]
},
{
"ansible_loop_var": "item",
:
:
}
},
"item": 4,
"rc": 0,
"start": "2022-01-21 09:38:12.607555",
"stderr": "",
"stderr_lines": [],
"stdout": "4",
"stdout_lines": [
"4"
]
}
]
}
}
and result final ips after set_fact:
ok: [localhost] => {
"ips": [
"0",
"2",
"4"
]
}
just adapt the solution to your case: as i dont know your output, check the value of register...
- name: Check All IPs
local_action: shell cat /tmp/output | awk '{ print ${{item}} }'| sed 's/.$//'|sed 's/.$//'
register: result
loop: "{{ range(3, 15 + 1, 3)|list }}" #loop from 3 to 15 with a step 3
- set_fact:
ips: "{{ ips | d([]) + item.stdout_lines }}"
loop: "{{ result.results }}"
- name: Analyze All IPs
local_action: shell /usr/bin/abuseipdb -C {{ item }} -o plaintext
register: vbresult
loop: "{{ ips }}"
- debug: var=vbresult
another solution will be to trap alls your ips from your file in one task..
but you have to know your output, but the solution i show is easily adaptable to your case quickly
EDITED:
following your output, you could work directly from your result.stdout_lines:
- name: simulate your output result.stdout_lines
set_fact:
values:
- ' 40545 87.250.224.147'
- ' 20873 87.250.224.126'
- ' 16665 213.180.203.67'
- ' 15420 87.250.224.142'
- ' 14503 13.81.52.25'
- name: trap ips values
set_fact:
ips: "{{ ips | d([]) + [_ip] }}"
loop: "{{ values }}"
vars:
_ip: "{{ (item | trim).split(' ') | last }}"
- debug:
var: ips
result:
ok: [localhost] => {
"ips": [
"87.250.224.147",
"87.250.224.126",
"213.180.203.67",
"87.250.224.142",
"13.81.52.25"
]
}
Related
Hello Developer Community!
I have been working on developing some Ansible playbooks to manage Citrix NetScaler configuration and would like to get some help about the following. I have the following data structure defined in a variable named nsapp_lb_server:
nsapp_lb_server:
- name: "SRV-1"
ipaddress: "10.102.102.1"
comment: "Chewbacca"
- name: "SRV-2"
ipaddress: "10.102.102.2"
comment: "C-3PO"
- name: "SRV-3"
ipaddress: "10.102.102.3"
comment: "Obi-Wan Kenobi"
...
[+ another 1200 item...]
and I have the follow task:
- name: "Check variables (loop)"
ansible.builtin.assert:
that:
- ( (item.name is defined) and (item.name | length > 0) )
- ( (item.ipaddress is defined) and (item.ipaddress | ipaddr() == item.ipaddress) )
- ( (item.comment | length > 0) if (item.comment is defined) else omit )
loop: "{{ nsapp_lb_server }}"
My problem is that, when I have thousands of records in the nsapp_lb_server variable, the loop is incredibly slow. The task finishes in 30 minutes, which is a very long time... :-(
After some digging on the Internet, it seems, the issue is caused by Ansible "loop" function, so I would like to check if there are any other methods what I can use instead of loop.
Are there any alternatives of Ansible "loop" which can provide the same result (looping over the entries of the variable)? I was thinking about using json_query, but still do not know how to implement it in this specific case.
My environment:
$ ansible --version
ansible [core 2.12.6]
config file = /home/ansible/.ansible.cfg
configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /home/ansible/ansible/lib/python3.9/site-packages/ansible
ansible collection location = /home/ansible/.ansible/collections:/usr/share/ansible/collections
executable location = /home/ansible/ansible/bin/ansible
python version = 3.9.7 (default, Sep 21 2021, 00:13:39) [GCC 8.5.0 20210514 (Red Hat 8.5.0-3)]
jinja version = 3.0.3
libyaml = True
Could anyone please point me to the right direction? I have been working on my code set for a very long time and after testing the code with large data, the code seems to be useless, because of the running time. I have also checked the hardware resource allocated to the VM where Ansible controller is running on, nothing problematic.
Many thanks in advance!
Running this validation as thousands of individual tasks is very slow because it adds a lot of execution and callback overhead. You can instead do it in a single task, with the caveat that it will be harder to track down the invalid list item(s):
- hosts: localhost
gather_facts: false
vars:
nsapp_lb_server: "{{ nsapp_lb_samples * 10000 }}"
nsapp_lb_samples:
- name: "SRV-1"
ipaddress: "10.102.102.1"
comment: "Chewbacca"
- name: "SRV-2"
ipaddress: "10.102.102.2"
comment: "C-3PO"
- name: "SRV-3"
ipaddress: "10.102.102.3"
comment: "Obi-Wan Kenobi"
tasks:
- assert:
that:
- nsapp_lb_server | rejectattr('name') | length == 0
- (nsapp_lb_server | map(attribute='ipaddress') | map('ipaddr')) == (nsapp_lb_server | map(attribute='ipaddress'))
- nsapp_lb_server | selectattr('comment', 'defined') | rejectattr('comment') | length == 0
This runs in ~5 seconds for the 30,000 test entries I fed it.
To make it easier to find the bad values without making the task extremely ugly, you can split it up into a series of tasks:
- hosts: localhost
gather_facts: false
vars:
nsapp_lb_server: "{{ nsapp_lb_samples * 10000 }}"
nsapp_lb_samples:
- name: "SRV-1"
ipaddress: "10.102.102.1"
comment: "Chewbacca"
- name: "SRV-2"
ipaddress: "10.102.102.2"
comment: "C-3PO"
- name: "SRV-3"
ipaddress: "10.102.102.3"
comment: "Obi-Wan Kenobi"
tasks:
- name: Check for missing names
assert:
that: nsapp_lb_server | rejectattr('name', 'defined') | length == 0
fail_msg: "Bad entries: {{ nsapp_lb_server | rejectattr('name', 'defined') }}"
- name: Check for bad names
assert:
that: nsapp_lb_server | rejectattr('name') | length == 0
fail_msg: "Bad entries: {{ nsapp_lb_server | rejectattr('name') }}"
- name: Check for missing IP addresses
assert:
that: nsapp_lb_server | rejectattr('ipaddress', 'defined') | length == 0
fail_msg: "Bad entries: {{ nsapp_lb_server | rejectattr('ipaddress', 'defined') }}"
- name: Check for bad IP addresses
assert:
that: (nsapp_lb_server | map(attribute='ipaddress') | map('ipaddr')) == (nsapp_lb_server | map(attribute='ipaddress'))
fail_msg: "Suspicious values: {{ nsapp_lb_server | map(attribute='ipaddress') | map('ipaddr') | symmetric_difference(nsapp_lb_server | map(attribute='ipaddress')) }}"
- name: Check for bad comments
assert:
that: nsapp_lb_server | selectattr('comment', 'defined') | rejectattr('comment') | length == 0
fail_msg: "Bad entries: {{ nsapp_lb_server | selectattr('comment', 'defined') | rejectattr('comment') }}"
This runs in ~12 seconds for the same list of 30,000 test entries.
I am trying to set an Ansible variable based on the count returned by the grep command.
The code is as follows.
tasks:
- name: Check for 12.1 databases
shell: "grep -c /u1/app/oracle/product/12.1.0/dbhome_1 /etc/oratab"
register: grep_output
- name: Print
debug:
msg="grep_output.stdout"
- name: Set the variable
set_fact:
oracle_version_12_1_0: "true"
when: "grep_output.stdout > 0"
- name: Print variable
command: echo "{{ oracle_version_12_1_0 }}"
The code errors out as follows.
PLAY [oradbpoc1] ***********************************************************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [oradbpoc1]
TASK [Check for 12.1 databases] *********************************************************************************************************
fatal: [oradbpoc1]: FAILED! => {"changed": true, "cmd": "grep -c /u1/app/oracle/product/12.1.0/dbhome_1 /etc/oratab", "delta": "0:00:00.003425", "end": "2020-08-06 18:33:04.476299", "msg": "non-zero return code", "rc": 1, "start": "2020-08-06 18:33:04.472874", "stderr": "", "stderr_lines": [], "stdout": "0", "stdout_lines": ["0"]}
PLAY RECAP *********************************************************************************************************
oradbpoc1 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
PS: Also, is there a way to assign a default value ("false") to the oracle_version_12_1_0 variable?
Thanks
FR
With regards to the Fail condition, it is because a grep -c PATTERN FILE will return a non-zero exit code if the PATTERN is not found in a file.
Quick example:
# grep -c root /etc/passwd; echo $?
2
0
# grep -c rootNO /etc/passwd; echo $?
0
1
So, I believe there are two options:
Just base upon the exit code
Set to ignore the error, and deal with the value
Since the count should be sufficient, Option 2 is shown here:
- hosts: localhost
tasks:
- name: get value
shell:
cmd: grep -c root /etc/passwd
register: grep_output
ignore_errors: yes
changed_when: false
- name: Print
debug:
msg: "{{ inventory_hostname }}: '{{ grep_output.stdout }}'"
- name : Set the variable
set_fact:
oracle_version_12_1_0: "{% if grep_output.stdout == '0' %}false{% else %}true{% endif %}"
- name: Print
debug:
msg: "val: {{ oracle_version_12_1_0 }}
Adjust root to rootNo (for example) to see the difference in true/false.
Also, depending upon exactly what you are wishing to do, it is also possible to register the variable as indicated in the get value, and then use it in a when check (i.e., avoid setting the variable):
- name : task name
...
when: grep_output.stdout != '0'
Whether this latter approach is helpful or not depends on the overall use case.
Variable file
I have a variable file
fruits:
- apple
- banana
- orange
- strawberry
- grapes
- papaya
users:
- user_name: 'john'
user_age: 45
- user_name: 'yash'
user_age: 95
- user_name: 'srk'
user_age: 52
- user_name: 'alia'
user_age: 26
Playbook Tasks
and my ansible tasks, just trying to make a text file and adding variables in file in vertical order.
- hosts: localhost
gather_facts: true
vars_files:
- variables.var # this is my variable file in the same dir that playbook have.
tasks:
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: /home/ansible/ansible-demo2/fruits.txt
loop:
- "{{ fruits|flatten }}"
- name: add uses to the list
lineinfile:
create: yes
line: "{{ item.user_name }} ==> {{ item.user_age }}"
path: /home/ansible/ansible-demo2/users.txt
loop:
- "{{ users|flatten(levels=1) }}"
Errors
But I am getting weird behavior. Below is the output of fruits task and error of the users task.
TASK [add fruits to the list] ***************************************************************************************************************************
ok: [localhost] => (item=[u'apple', u'banana', u'orange', u'strawberry', u'grapes', u'papaya'])
[WARNING]: The value ['apple', 'banana', 'orange', 'strawberry', 'grapes', 'papaya'] (type list) in a string field was converted to u"['apple',
'banana', 'orange', 'strawberry', 'grapes', 'papaya']" (type string). If this does not look like what you expect, quote the entire value to ensure it
does not change.
TASK [add uses to the list] *****************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'user_name'
\n\nThe error appears to be in '/home/ansible/ansible-demo2/myansible.yml': line 18, column 7, but may\nbe elsewhere in the file depending on the exact s
yntax problem.\n\nThe offending line appears to be:\n\n\n - name: add uses to the list\n ^ here\n"}
Expected Output
List in vertical order in text file.
Comment on question:
I am amazed every thing works fine with with_items and not with loop, even user_name variable is defined but still it is saying undefined. Now I unable to find out what's wrong going on.
Reference:
Here is the doc that I am referencing : Migrating from with_X to loop
Edit: Variables debug output
I debug the variables. output is below
TASK [debug] ********************************************************************************************************************************************
ok: [localhost] => {
"fruits": [
"apple",
"banana",
"orange",
"strawberry", "grapes", "papaya"
]
}
TASK [debug] ********************************************************************************************************************************************
ok: [localhost] => {
"users": [
{
"user_age": 45,
"user_name": "john"
},
{
"user_age": 95,
"user_name": "yash"
},
{
"user_age": 52,
"user_name": "srk"
},
{
"user_age": 26,
"user_name": "alia"
}
]
}
Q: *"[WARNING]: The value ['apple', ... ] (type list) in a string field was converted to u"['apple', ... ]" (type string).
A: From the code, it's not clear what is the reason for this conversion. The data and the playbook below work as expected.
shell> cat data.yml
fruits:
- apple
- banana
- orange
- strawberry
- grapes
- papaya
users:
- user_name: 'john'
user_age: 45
- user_name: 'yash'
user_age: 95
- user_name: 'srk'
user_age: 52
- user_name: 'alia'
user_age: 26
shell> cat playbook.yml
- hosts: localhost
vars_files:
- data.yml
tasks:
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: fruits.txt
loop: "{{ fruits }}"
- name: add uses to the list
lineinfile:
create: yes
line: "{{ item.user_name }} ==> {{ item.user_age }}"
path: users.txt
loop: "{{ users }}"
Give
shell> cat fruits.txt
apple
banana
orange
strawberry
grapes
papaya
shell> cat users.txt
john ==> 45
yash ==> 95
srk ==> 52
alia ==> 26
Q: "2 variables fruits1 and fruits2 ... append their data into single file ... in single task with 2 vars"
A: With the modified data
shell> cat data.yml
fruits1:
- apple
- banana
- orange
fruits2:
- strawberry
- grapes
- papaya
this task gives the same result
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: fruits.txt
loop: "{{ fruits1 + fruits2 }}"
See Append list variable to another list in Ansible.
Comment on the question: "I am amazed everything works fine with with_items and not with loop"
A: See Comparing loop and with_*.
I am newbie to ansible and trying to write my first playbook.
- name: create volume
volume:
state: present
username: "{{ username }}"
password: "{{ password }}"
hostname: "{{ inventory_hostname }}"
vserver: "{{item[0]}}"
name: "{{item[1]}}"
aggregate_name: "{{output}}"
with_nested:
- [ 'vs10' , 'vs11' ]
- [ 'vol1' , 'vol2', 'vol3' , 'vol4' ,'vol5', ''vol6']
connection: local
Actual output:
vs10-vol1 vol2 vol3 vol4
vs11- vol1 vol2 vol3 vol4
Expected output:
vs10-vol1, vol3 vol5
vs11-vol2, vol4 vol6
This would probably work. I'm basically looping the task against volumes and calculating the vserver in the task.
- name: create volume
volume:
state: present
username: "{{ username }}"
password: "{{ password }}"
hostname: "{{ inventory_hostname }}"
# Calculate which vserver to use based on 'current volume index in the volumes list' and 'length of vservers list'.
# The logic uses modulus to try and distribute volumes across given vcenters
vserver: "{{vservers[(current_index % (vservers|length))]}}"
# Name is item itself because you are looping volumes
name: "{{item}}"
aggregate_name: "{{output}}"
# Loop the volumes
loop: [ 'vol1' , 'vol2', 'vol3' , 'vol4' ,'vol5', 'vol6']
# This is make a loop_control variable available. This will give us 'current_index'
loop_control:
index_var: current_index
# Vservers are defined here
vars:
vservers: [ 'vs10' , 'vs11' ]
I want to loop over the first task output in second task. In the first task getting hostname and IP in the array. In the second task loop over the array print the each item in the array in separate line.
here the code I have so for.
- name: Store known hosts of 'all' the hosts in the inventory file
hosts: localhost
connection: local
vars:
ssh_known_hosts_command: "ssh-keyscan -T 10"
ssh_known_hosts_file: "{{ lookup('env','HOME') + '/.ssh/known_hosts' }}"
ssh_known_hosts: "{{ groups['all'] }}"
tasks:
- name: For each host, find the ip
shell: 'echo -e "{{ item }}\n`dig +short {{ item }}`"'
with_items: "{{ ssh_known_hosts }}"
register: ssh_known_host_results
ignore_errors: yes
- name: print message
debug:
msg: "{{ item.stdout_lines[0] + ' test' }}"
with_items: "{{ ssh_known_host_results.results }}"
in the second task how can I loop over ssh_known_host_results.results array?
thanks
SR
I am looking for something like this:
- name: print message
debug:
msg: "{{ item+ ' test' }}"
with_items: "{{outer_item.stdout_lines "
with_items: "{{ ssh_known_host_results.results }}"
loop_control:
loop_var: outer_item
when I add to ignore localhost its not giving array item. how can it make return hostname and ip as two array elements?
- name: For each host, find the ip
shell: 'echo -e "{{ item }}\n`dig +short {{ item }}`"'
with_items: "{{ ssh_known_hosts }}"
when: not item == 'localhost'
register: ssh_known_host_results
ignore_errors: yes
- name: print message
debug:
msg: "{{ item + ' test' }}"
with_items: "{{ ssh_known_host_results.results | map(attribute='stdout_lines') | list }}"
Support for dynamic nested loops is not implemented in Ansible.
To iterate over each line, you can flatten the result:
- name: print message
debug:
msg: "{{ item + ' test' }}"
with_items: "{{ ssh_known_host_results.results | map(attribute='stdout_lines') | list }}"