Ansible get specific value from output - ansible

I have been pulling my hair out over something I thought would have been easy to do.
I have playbook:
- hosts: all
tasks:
- name: Get F5 connections
bigip_command:
commands: show ltm node /mypartition/myserver.domain.com
provider:
server: "F5server.domain.com"
user: ""
password: ""
validate_certs: "no"
server_port: 443
register: connections
delegate_to: localhost
- name: output
debug:
msg: "{{ connections }}"
When I run that playbook, it outputs this:
ok: [myserver.domain.com] => {
"msg": {
"changed": false,
"executed_commands": [
"tmsh -c \\\"show ltm node /mypartition/myserver.domain.com\\\""
],
"failed": false,
"stdout": [
"-------------------------------------------------------------\nLtm::Node: /mypartition/myserver.domain.com (123.45.67.89)\n-------------------------------------------------------------\nStatus \n Availability : available\n State : enabled\n Reason : Node address is available\n Monitor : /Common/gateway_icmp (default node monitor)\n Monitor Status : up\n Session Status : enabled\n \nTraffic ServerSide General\n Bits In 635.9M -\n Bits Out 2.8G -\n Packets In 102.8K -\n Packets Out 266.6K -\n Current Connections 7 -\n Maximum Connections 11 -\n Total Connections 6.5K -\n Total Requests - 13.0K\n Current Sessions - 0"
],
"stdout_lines": [
[
"-------------------------------------------------------------",
"Ltm::Node: /mypartition/myserver.domain.com (123.45.67.89)",
"-------------------------------------------------------------",
"Status ",
" Availability : available",
" State : enabled",
" Reason : Node address is available",
" Monitor : /Common/gateway_icmp (default node monitor)",
" Monitor Status : up",
" Session Status : enabled",
" ",
"Traffic ServerSide General",
" Bits In 635.9M -",
" Bits Out 2.8G -",
" Packets In 102.8K -",
" Packets Out 266.6K -",
" Current Connections 7 -",
" Maximum Connections 11 -",
" Total Connections 6.5K -",
" Total Requests - 13.0K",
" Current Sessions - 0"
]
]
}
}
My question is, how do I simply get the "Current Connections" value i.e. in this example it is 7 and store it in a variable.
I have tried various different solutions but nothing seems to work.
My Ansible version is 2.9
Can someone please help?

The task
- debug:
msg: "Current Connections: {{ item.split().2 }}"
loop: "{{ connections.stdout_lines.0 }}"
when: item is match('^ Current Connections(.*)$')
gives
"msg": "Current Connections: 7"

Use a regular expression to extract the value and set the variable "current_connections"
- name: Current Connections
vars:
pattern: '(?<=Current\sConnections)\s*\d+'
set_fact:
current_connections: "{{ connections.stdout | regex_search(pattern) }}"
- debug:
var: hostvars[inventory_hostname]['current_connections']

Related

Checking my vmnic on ESXi server using Ansible

I want to Checking my vmnic on ESXi server.
Let me show my Ansible-playbook yaml code, it's very simple.
---
- hosts: host
vars:
vcenter_server: "192.168.35.219"
vcenter_user: "root"
vcenter_pass: "P#ssw0rd"
esxi_hostname: "esxihost1"
gather_facts: true
- name: Gather info about vmnics of an ESXi Host
community.vmware.vmware_host_vmnic_info:
hostname: '{{ vcenter_server }}'
username: '{{ vcenter_user }}'
password: '{{ vcenter_pass }}'
esxi_hostname: '{{ esxi_hostname }}'
validate_certs: no
delegate_to: localhost
register: host_vmnics
- name: print esxi info
ansible.builtin.debug:
var: host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details
I want display vmnic status using Ansible playbook.
I created a playbook as below and ran it.
However, it contains a lot of unnecessary information.
TASK [Gather info about vmnics of an ESXi Host] ***********************************************************************************************************
ok: [192.168.35.219 -> localhost]
TASK [print esxi info] ************************************************************************************************************************************
ok: [192.168.35.219] => {
"host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details": [
{
"actual_duplex": "Full Duplex",
"actual_speed": 10000,
"adapter": "VMware Inc. vmxnet3 Virtual Ethernet Controller",
"configured_duplex": "Full Duplex",
"configured_speed": 10000,
"device": "vmnic0",
"driver": "nvmxnet3",
"lldp_info": "N/A",
"location": "0000:0b:00.0",
"mac": "00:0c:29:bc:67:65",
"status": "Connected"
}
]
}
I only want to see actual_duplex,actual_speed,device,status
like below
"host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details": [
{
"actual_duplex": "Full Duplex",
"actual_speed": 10000,
"device": "vmnic0",
"status": "Connected"
}
]
}
And, actual duplex: Full Duplex actual speed: 10000, device: vmnic0, status: Connected If each value is correct, "OK"
If not, I want to display it as "NOTOK".
Is it possible?
Regarding
it contains a lot of unnecessary information
you may have a look into the general documentation about Ansible Return Values
Ansible modules normally return a data structure that can be registered into a variable, or seen directly when output by the ansible program.
and because of
Each module can optionally document its own unique return values
into the specific documentation about vmware_host_vmnic_info module - Return Values.
dict with hostname as key and dict with vmnics information as value
In other words, the data structure hosts_vmnics_info returned just contain all this vmnic_details.
Regarding
I only want to see actual_duplex, actual_speed, device, status
you can access the key values by using in example something like
- name: Show
debug:
msg: "Device {{ host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details[0].device }} is in status {{ host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details[0].status }} at an actual speed of {{ host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details[0].actual_speed }} in mode {{
host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details[0].actual_duplex }}"
Further Documenation
Using Variables
Referencing list variables
Referencing key:value dictionary variables
Regarding
If the vmnic is not status: Connected, "NOT OK" is output. I want to do something similar a shell script if [[ "foo" == "$var" ]]; Then OK else Not OK fi
you could use an approach with the assert module like
- name: Check connection status
ansible.builtin.assert:
that:
- "host_vmnics.hosts_vmnics_info.esxihost1.vmnic_details[0].status == 'Connected'"
fail_msg: "NOT OK"
success_msg: "OK"

How do I do the equivalent of combining a with_subelements loop and a with_nested loop in Ansible?

I'm writing a playbook to get a list of services deployed to a Mirantis' UCP/MKE cluster and check on all the Docker workers in that cluster, that each external swarm service port is open.
The playbook makes an API call from localhost to get an extensive JSON object of services deployed to the cluster, which is simplified using jmespath to just name, ID, ports.
As another play, my playbook runs a shell command on each worker in the cluster to obtain a list of open ports.
I'd like to loop through each port for each service and confirm if the port is open on each and every worker node.
My services/ports data object can look like this:
[
{
"ID": "aefgergergergergerg",
"Name": "application1_service",
"Ports": [
[
30950,
"tcp"
],
[
30951,
"tcp"
]
]
},
{
"ID": "sdfthtrhrthrthrthrthtrh",
"Name": "application2_service",
"Ports": [
[
31190,
"tcp"
]
]
},
...
]
(obtained via an API call and can be simplified with a jmespath query:
'[?Endpoint.Ports].{ ID: ID, Name: Spec.Name, Ports: Endpoint.Ports[?contains(#.PublishMode,`ingress`)].[PublishedPort, PublishMode, Protocol] }'
And my worker's open ports objects look like this:
ok: [worker1] => {
"msg": [
"tcp:31557",
"tcp:31501",
"tcp:31556",
"tcp:31500",
"tcp:30231",
"tcp:30230",
"tcp:30651",
"tcp:30650"
]
}
ok: [worker2] => {
"msg": [
"tcp:31557",
"tcp:31501",
"tcp:31556",
"tcp:31500",
"tcp:30231",
"tcp:30230",
"tcp:30651",
"tcp:30650"
]
}
ok: [worker3] => {
"msg": [
"tcp:31557",
"tcp:31501",
"tcp:31556",
"tcp:31500",
"tcp:30231",
"tcp:30230",
"tcp:30651",
"tcp:30650"
]
}
obtained with
iptables -L DOCKER-INGRESS | awk -F ' {2,}' '($1 == "ACCEPT") && ($6 ~ /dpt/) {print $6}' | sed 's/ dpt//g')
In my head, I want to combine a with_subelements loop (ports for each given service) with a with_nested loop (my subelements as the first list, and my open ports as the nested list), but I am sure this isn't quite possible.
This is the relevant part of my playbook (I've cut out the auth logic as it's not relevant)
- name: Ensure secrets are in the required collections
hosts: localhost
gather_facts: false
vars_files: vars.yaml
[SNIP]
- name: "Get a list of services from https://{{ endpoint }}/services"
ansible.builtin.uri:
url: "https://{{ endpoint }}/services"
body_format: json
headers:
Authorization: "Bearer {{ auth.json.auth_token }}"
validate_certs: "{{ validate_ssl_certs | default('yes') }}"
register: services
- name: "Create a simplified JSON object of services and ports"
ansible.builtin.set_fact:
services_ports: "{{ services.json | json_query(jmesquery) }}"
vars:
jmesquery: "{{ jmesquery_services }}"
- name: See what ports are open on which workers
hosts: workers
gather_facts: false
become: true
vars_files: vars.yaml
tasks:
- name: Get the list of open ports
shell: iptables -L DOCKER-INGRESS | awk -F ' {2,}' '($1 == "ACCEPT") && ($6 ~ /dpt/) {print $6}' | sed 's/ dpt//g'
register: iptables_rules
- name: debug
debug:
msg: "{{ iptables_rules.stdout_lines }}"
And relevant bit of vars.yaml:
---
jmesquery_services: '[?Endpoint.Ports].{ ID: ID, Name: Spec.Name, Ports: Endpoint.Ports[?contains(#.PublishMode,`ingress`)].[PublishedPort, PublishMode, Protocol] }'
How best to check each of these ports for a service against each open port on each worker?
You are on a right track with subelements lookup plugin, you should simply loop over the service/port pairs and check if such port exists in iptables_rules.stdout_lines.
Here is an example playbook with dummy data on how to so:
- hosts: localhost
gather_facts: false
become: false
tasks:
- name: check if all service ports are open
# Loop over service/port pairs
loop: "{{ lookup('subelements', services_ports, 'Ports') }}"
# Set variables for clarity
vars:
service_name: "{{ item[0]['Name'] }}"
iptables_port: "{{ item[1][1] ~ ':' ~ item[1][0] }}"
iptables_port_exists: "{{ iptables_port in iptables_rules.stdout_lines }}"
# Fail the module if port is not found
failed_when: "not iptables_port_exists"
# For demo, print out the service status
debug:
msg: "Service {{ service_name }} is {{ 'up' if iptables_port_exists else 'down' }} on port {{ iptables_port }}"
# Example data
vars:
services_ports:
- ID: aefgergergergergerg
Name: application1_service
Ports:
- ["30950", "tcp"]
- ["30951", "tcp"]
- ID: sdfthtrhrthrthrthrthtrh
Name: application2_service
Ports:
- ["31190", "tcp"]
iptables_rules:
stdout_lines: [
"tcp:30950",
"tcp:31190",
]
This produces output like so:
TASK [check if all service ports are open] *************************************
ok: [localhost] => (item=[{'ID': 'aefgergergergergerg', 'Name': 'application1_service'}, ['30950', 'tcp']]) => {
"msg": "Service application1_service is up on port tcp:30950"
}
failed: [localhost] (item=[{'ID': 'aefgergergergergerg', 'Name': 'application1_service'}, ['30951', 'tcp']]) => {
"msg": "Service application1_service is down on port tcp:30951"
}
ok: [localhost] => (item=[{'ID': 'sdfthtrhrthrthrthrthtrh', 'Name': 'application2_service'}, ['31190', 'tcp']]) => {
"msg": "Service application2_service is up on port tcp:31190"
}
fatal: [localhost]: FAILED! => {"msg": "One or more items failed"}
PS! I'm not familiar with jmespath, but you might need to use hostvars['localhost']['services_ports'] on the workers in order to access variables created on localhost

Ansible filter to find all lists with specified name

I have a task like
- name: Verify all app states
shell: get_cluster_states {{item.name}}
register: service_states
until: <all services are in 'up' state, this is what I need>
retries: 20
delay: 3
with_items: "{{ apps }}"
The returned "service_states" has contents in format as below:
"service_states": {
"changed": true,
"msg": "All items completed",
"results": [
{
......
"stdout": "Service Status: up\nService Status: up\nService Status: up",
"stdout_lines": [
"Service Status: up",
"Service Status: down",
"Service Status: up"
]
},
{
......
"stdout": "Service Status: up\nService Status: up\nService Status: up",
"stdout_lines": [
"Service Status: up",
"Service Status: up",
"Service Status: down"
]
}
]
}
I need filters to apply to the service_states data, so I can get a combined simple list from all "stdout_lines". In this case, totally 6 values from the 2 lists - number of list elements are not fixed:
expected: [
"Service Status: up",
"Service Status: down",
"Service Status: up",
"Service Status: up",
"Service Status: up",
"Service Status: down"
]
If I could achieve this, I'd be able to use unique/count/value to determine all services are up or not. I tried using {{ service_states.results | subelements('stdout_lines') }} to get the 6 entries, but don't know how to further filter out to get the final list as above.
Use either json_query, e.g.
- set_fact:
expected: "{{ service_states.results|
json_query('[].stdout_lines')|flatten }}"
, or map attribute, e.g.
- set_fact:
expected: "{{ service_states.results|
map(attribute='stdout_lines')|flatten }}"
Q: "Determine all services are up or not."
A: For example
- debug:
msg: All services are up.
when: _all == _up
vars:
_list: "{{ service_states.results|
map(attribute='stdout_lines')|flatten }}"
_all: "{{ _list|length }}"
_up: "{{ _list|select('match', 'Service Status: up')|list|length }}"
- debug:
msg: "{{ _all|int - _up|int }} services are down."
vars:
_list: "{{ service_states.results|
map(attribute='stdout_lines')|flatten }}"
_all: "{{ _list|length }}"
_up: "{{ _list|select('match', 'Service Status: up')|list|length }}"
gives
TASK [debug] *************************************************************
skipping: [localhost]
TASK [debug] *************************************************************
ok: [localhost] =>
msg: 2 services are down.
Q: "Is there a way to print out what the service_states look like at the until location?"
A: No. There is no method to print the intermediate results. The task is running at the remote host. After the task completes the registered variable, service_states in our case, will be sent back to the controller. Instead, you can debug the module to see in detail what's going on. See Debugging modules.
Q: "Maybe inside the loop there was no results layer"
A: Yes. The results inside a loop are different. When the loop completes the registered variable must comprise the list of all results. See Looping a task until it succeeds
Q: "Need the until"
A: Without the loop until works as described in the documentation, e.g. create the script below to generate random data
shell> cat random.sh
#!/bin/bash
printf "Service Status: "
[ "$((1 + $RANDOM % 10))" -lt "5" ] && echo up || echo down
printf "Service Status: "
[ "$((1 + $RANDOM % 10))" -lt "5" ] && echo up || echo down
printf "Service Status: "
[ "$((1 + $RANDOM % 10))" -lt "5" ] && echo up || echo down
shell> ./random.sh
Service Status: up
Service Status: up
Service Status: down
Then, the playbook
shell> cat playbook.yml
- hosts: localhost
vars:
_pattern: "Service Status: up"
tasks:
- command: "{{ playbook_dir }}/random.sh"
register: service_states
until: service_states.stdout_lines|
select('match', _pattern)|list|length == 3
retries: 10
delay: 1
- debug:
msg: "{{ service_states.stdout_lines }}"
gives
shell> ansible-playbook playbook.yml
TASK [command] **********************************************************
FAILED - RETRYING: command (10 retries left).
FAILED - RETRYING: command (9 retries left).
FAILED - RETRYING: command (8 retries left).
changed: [localhost]
TASK [debug] ************************************************************
ok: [localhost] =>
msg:
- 'Service Status: up'
- 'Service Status: up'
- 'Service Status: up'

Ansible: Find (first) free IP-address and set it

I have a couple of dozen Fedora-clients with DHCP-IP-addresses and I need to set them to static IPs. Usually there is (in this case) only one client per VLAN, but there can be more. The idea is to check if a IP-address (in the same range) is pingable and if not I set the clients IP to this address. This would be sufficient for my case but I'm happy to hear better suggestions.
Setting a given IP-address works fine, but how can I find the first "free" address and use it later in the playbook? So i guess: How can I register the first "failed" IP-address?
My ping-test-command
- name: "Test if the ip address is already online."
command: "ping -q -c 1 -W 1 192.168.178.{{ item }}"
register: res
loop: "{{ range(1,5) | list }}"
#failed_when: res.rc == 0
Output:
[user#host ansible]$ ansible-playbook change_network.yml
PLAY [all] **************************************************************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************************************************************
ok: [192.168.178.40]
TASK [change_network : Test if the ip address is already online.] *******************************************************************************************************************************************
changed: [192.168.178.40] => (item=1)
changed: [192.168.178.40] => (item=2)
failed: [192.168.178.40] (item=3) => {"ansible_loop_var": "item", "changed": true, "cmd": ["ping", "-q", "-c", "1", "-W", "1", "192.168.178.3"], "delta": "0:00:01.003836", "end": "2020-12-30 12:04:18.700349", "item": 3, "msg": "non-zero return code", "rc": 1, "start": "2020-12-30 12:04:17.696513", "stderr": "", "stderr_lines": [], "stdout": "PING 192.168.178.3 (192.168.178.3) 56(84) bytes of data.\n\n--- 192.168.178.3 ping statistics ---\n1 packets transmitted, 0 received, 100% packet loss, time 0ms", "stdout_lines": ["PING 192.168.178.3 (192.168.178.3) 56(84) bytes of data.", "", "--- 192.168.178.3 ping statistics ---", "1 packets transmitted, 0 received, 100% packet loss, time 0ms"]}
failed: [192.168.178.40] (item=4) => {"ansible_loop_var": "item", "changed": true, "cmd": ["ping", "-q", "-c", "1", "-W", "1", "192.168.178.4"], "delta": "0:00:01.004021", "end": "2020-12-30 12:04:20.741041", "item": 4, "msg": "non-zero return code", "rc": 1, "start": "2020-12-30 12:04:19.737020", "stderr": "", "stderr_lines": [], "stdout": "PING 192.168.178.4 (192.168.178.4) 56(84) bytes of data.\n\n--- 192.168.178.4 ping statistics ---\n1 packets transmitted, 0 received, 100% packet loss, time 0ms", "stdout_lines": ["PING 192.168.178.4 (192.168.178.4) 56(84) bytes of data.", "", "--- 192.168.178.4 ping statistics ---", "1 packets transmitted, 0 received, 100% packet loss, time 0ms"]}
So 192.168.178.3 is the first free address and could be used in this example.
The second problem is, that I need to search in the correct range, i.e. replace the hardcoded "192.168.178" with an ansible variable (the first three octets of the DHCP-address stay the same). I tried something like from this question: Ansible increment IP address
- name: Append the last octet to the first 3 octets
set_fact: new_ip_address="{{ ansible_default_ipv4.address | regex_replace('(^.*\.).*$', '\\1') }}{{ item }}"
loop: "{{ range(1,5) | list }}"
- debug: var=new_ip_address
It works in this form, but I couldn't get this to work in my ping-command. Also maybe there is a prettier solution with a jinja-filter instead of the regex?
as serverfault question you can use nmap to issue a subnet scan.
Pay attentions: unless you know the networking infrastructure, there is still the possibility that picked IP address is used, firewall can block scanning.
To increase the probability that the found IP is really free, you can also use a DNS reverse query, to check if that IP has a PTR record:
- name: Scanning for free ip address
command:>
nmap -v -sn -n "{{ subnet }}" -oG - \
| awk '/Status: Down/{print $2}' \
| awk 'NR>2 {print last} {last=$0}'
register: ipscan
- name: Get fist free ip address
set_fact:
ipv4_address: "{{ ipscan.stdout_lines | first }}"
We need the second awk to remove the network address and broadcast address (first and last) from the output.
Checking also for reverse DNS on picked ip address:
- name: Retrieving host DNS Data.
set_fact:
vm_ipv4_dns_names: "{{ lookup('dig', ipv4_address ~ '/PTR').split(',') | map('regex_replace', '\\.?$', '' ) }}"
- name: Check reverse DNS returns data
fail:
msg: Picked ip address is on DNS
when: ('NXDOMAIN' not in vm_ipv4_dns_names)
I hope you find this useful.

failed_when do not work with telnet output

I will try to set a telnet task to failed if "instance" is in the output message, but it didn't work.
here is the log:
TASK [ISAM Log to log1 and log2] **************************************************************************************************************************************
task path: /Users/sascha.mueller/Downloads/isam_log.yml:9
changed: [1.2.3.4] => {
"changed": true,
"failed_when_result": false,
"output": [
"configure system syslog no route dslamlog msg-type all\r\n\r\nError : instance does not exist\r\n\r\nISAM-ISAM_7363_Labor_Zw>#",
"configure system syslog no destination dslamlog\r\n\r\nError : instance does not exist\r\n\r\nISAM-ISAM_7363_Labor_Zw>#"
]
}
TASK [fail the play if the previous command did not succeed] **********************************************************************************************************
task path: /Users/sascha.mueller/Downloads/isam_log.yml:27
skipping: [1.2.3.4] => {
"changed": false,
"skip_reason": "Conditional result was False"
}
I also tried command_output.stderr e.g. but all these values doesn't exist
---
-
hosts: all
connection: local
become: no
tasks:
- name: ISAM Log to log1 and log2
ignore_unreachable: yes
telnet:
user: bla
password: blubb
login_prompt: "login: "
password_prompt: "password: "
timeout: 5
prompts:
- "[#|$]"
command:
- configure system syslog no route dslamlog msg-type all
- configure system syslog no destination dslamlog
register: command_output
failed_when: "'instance' in command_output.output"
- name: fail the play if the previous command did not succeed
fail:
msg: "the command failed"
when: "'instance' in command_output.output"
Is it possible to check this with an telnet command or to direct check it with variable.output or only with variable.stderr?
You can see in your example that output is a list:
"output": [
"configure system syslog no route dslamlog msg-type all\r\n\r\nError : instance does not exist\r\n\r\nISAM-ISAM_7363_Labor_Zw>#",
"configure system syslog no destination dslamlog\r\n\r\nError : instance does not exist\r\n\r\nISAM-ISAM_7363_Labor_Zw>#"
]
When you ask if "'instance' in command_output.output", you are asking if there is an item in outout that is the string instance. That is, your expression would evaluate to true if output looked like this:
"output": [
"this item does not match",
"instance"
]
What you really want to ask is "does any item in output contain the string instance?". The simplest way to do that is probably by concatenating all the lines into a single string:
"'instance' in ''.join(command_output.output)"
You should be able to use this in either you failed_when condition:
- name: ISAM Log to log1 and log2
ignore_unreachable: yes
telnet:
user: bla
password: blubb
login_prompt: "login: "
password_prompt: "password: "
timeout: 5
prompts:
- "[#|$]"
command:
- configure system syslog no route dslamlog msg-type all
- configure system syslog no destination dslamlog
register: command_output
failed_when: "'instance' in ''.join(command_output.output)"
Or in the when statement on your fail task:
- name: fail the play if the previous command did not succeed
fail:
msg: "the command failed"
when: "'instance' in ''.join(command_output.output)"

Resources