Ansible format list variable - ansible

I'm working on setting up some automation for an F5 BigIP load balancer. When creating the virtual server, I have a variable containing the various profiles I want to include and also a few that should always be included.
I want to use this variable:
domains:
- foo.example.com
- bar.example.com
- baz.example.com
In the following module under profiles. Note, I can't do a loop because that would replace the value each time. I want all list items from domains to be expanded in the single execution of this task. I've tried using a Jinja for loop but it just hangs when I try to execute.
- name: Configure virtual server
f5networks.f5_modules.bigip_virtual_server:
state: present
partition: Common
name: insite-ssl
destination: 10.10.10.10
port: 443
pool: example-pool
snat: Automap
description: Testing VIP
profiles: |
{% for domain in domains %}
- name: {{ domain }}
context: client-side
{% endfor %}
- http-SSL-XForwarded
- name: example.com_wildcard
context: client-side
provider: "{{ f5_conn }}"
tags:
- vip
What is the right way to solve this?

By using this construct, you are actually creating a string, not a list.
This can be tested doing:
- set_fact:
what_am_I: |
{% for domain in domains %}
- name: {{ domain }}
context: client-side
{% endfor %}
- http-SSL-XForwarded
- name: example.com_wildcard
context: client-side
- debug:
var: what_am_I is string
Which gives:
TASK [set_fact] ******************************************************************
ok: [localhost]
TASK [debug] *********************************************************************
ok: [localhost] =>
what_am_I is string: true
You could use json_query in order to create a list of dictionaries out of your domains list, something like:
- name: Configure virtual server
f5networks.f5_modules.bigip_virtual_server:
state: present
partition: Common
name: insite-ssl
destination: 10.10.10.10
port: 443
pool: example-pool
snat: Automap
description: Testing VIP
profiles: |
{{ domains | json_query("[].{name: #, context: 'client-side'}") + extra_domains }}
provider: "{{ f5_conn }}"
tags:
- vip
vars:
extra_domains:
- http-SSL-XForwarded
- name: example.com_wildcard
context: client-side

Related

generate a list of hashes from a list for every entry of a hash in Ansible

I have the following two variables:
target_groups:
- name: http
port: 80
- name: https
port: 443
targets:
- 1.2.3.4
- 2.3.4.5
For an elb_target_group task I need a list of hashes as targets parameter. So what I'd like to have is a structure like the following:
target_groups:
- name: http
port: 80
targets:
- Id: 1.2.3.4
Port: 80
- Id: 2.3.4.5
Port: 80
- name: https
port: 443
targets:
- Id: 1.2.3.4
Port: 443
- Id: 2.3.4.5
Port: 443
So the targets entry of each target_groups element must be composed of the port of the element and all IPs of the targets list.
I have twisted my head around all map, combine... whatever filter I could find but couldn't come up with a solution.
Actually I don't even need that targets element in the list, as long as I can generate a suitable list of hashes on the fly, I'd be happy to do that. My task would look like that:
- name: update target groups
elb_target_group:
name: "{{ item.name }}"
protocol: tcp
port: "{{ item.port }}"
state: present
vpc_id: "{{ vpc_id }}"
targets: <<NEEDHELPHERE>>
with_items:
- { name: http, port: 80 }
- { name: https, port: 443 }
Is this even possible? Thanks in advance.
There are more options.
Iterate the list and combine the dictionaries. For example,
- set_fact:
tg2: "{{ tg2|d([]) + [item|combine({'targets':_targets})] }}"
loop: "{{ target_groups }}"
vars:
_targets: "{{ dict(targets|product([item.port]))|
dict2items(key_name='Id', value_name='Port') }}"
gives the updated list of dictionaries
tg2:
- name: http
port: 80
targets:
- Id: 1.2.3.4
Port: 80
- Id: 2.3.4.5
Port: 80
- name: https
port: 443
targets:
- Id: 1.2.3.4
Port: 443
- Id: 2.3.4.5
Port: 443
The next option is putting the code into the vars. For example, the expression below gives the same result
tg2: "{{ target_groups|
json_query('[].[port]')|
map('product', targets)|
map('map', 'zip', ['Port', 'Id'])|
map('map', 'map', 'reverse')|
map('map', 'community.general.dict')|
map('community.general.dict_kv', 'targets')|
zip(target_groups)|
map('combine')|
list }}"
Example of a complete playbook
- hosts: localhost
vars:
target_groups:
- name: http
port: 80
- name: https
port: 443
targets:
- 1.2.3.4
- 2.3.4.5
tg2: "{{ target_groups|
json_query('[].[port]')|
map('product', targets)|
map('map', 'zip', ['Port', 'Id'])|
map('map', 'map', 'reverse')|
map('map', 'community.general.dict')|
map('community.general.dict_kv', 'targets')|
zip(target_groups)|
map('combine')|
list }}"
tasks:
- debug:
var: tg2
Create the structure in Jinja if you want to. For example, the expressions below give the same result too
_tg2: |-
{% for i in target_groups %}
-
{% for k, v in i.items() %}
{{ k }}: {{ v }}
{% endfor %}
targets:
{% for ip in targets %}
- Id: {{ ip }}
Port: {{ i.port }}
{% endfor %}
{% endfor %}
tg2: "{{ _tg2|from_yaml }}"
Still rather ugly, but a slightly more readable solution is to build YAML and use the to_yaml filter. I've not found a way to avoid doing this in two steps so far, but this is an example of what I mean:
--- # test.yml
- name: test
hosts: localhost
# user: root
vars:
bar:
- apple
- banana
- carrot
foo: |-
{% for x in bar %}
- greet: "hello {{ x }}"
farewell: "hello {{ x }}"
{% endfor %}
tasks:
- name: test
debug:
msg: "{{ item }}"
loop: "{{ foo | from_yaml }}"
Running:
ansible-playbook test.yml
Gives:
PLAY [test] ******************************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [localhost]
TASK [test] ******************************************************************************************
ok: [localhost] => (item={'greet': 'hello apple', 'farewell': 'hello apple'}) => {
"msg": {
"farewell": "hello apple",
"greet": "hello apple"
}
}
ok: [localhost] => (item={'greet': 'hello banana', 'farewell': 'hello banana'}) => {
"msg": {
"farewell": "hello banana",
"greet": "hello banana"
}
}
ok: [localhost] => (item={'greet': 'hello carrot', 'farewell': 'hello carrot'}) => {
"msg": {
"farewell": "hello carrot",
"greet": "hello carrot"
}
}

Ansible search sublists for value

A webhook triggers an AWX job and I want to run the deployment on a certain host depending on the service, since they run on different servers. I need to know which server uses that service to set is as a var so it can be used as a host in the following play.
My variable inside vars.yaml looks like this:
staging_hosts:
server1: ['service1', 'service2', 'service3']
server2: ['service4', 'service5', 'service6']
server3: ['service7', 'service8', 'service9']
Playbook:
- name: write deployment hosts
hosts: localhost
vars:
deployment_hosts: absent
vars_files:
- ./group_vars/vars.yaml
tasks:
- set_fact:
modified_repos: (small regex filter to find modified repository)
- set_fact:
deployment_hosts: "{{ item }}"
when: '{{ modified_repos }} in {{ item }}'
with_list:
- "{{ staging_hosts }}"
- name: connect to Cluster
hosts: "{{ hostvars['localhost']['deployment_hosts'] }}"
What can I do against this warning and error?
[WARNING]: conditional statements should not include jinja2 templating
delimiters such as {{ }} or {% %}. Found: {{ modified_repos }} in {{ item }}
fatal: [localhost]: FAILED! => {"msg": "The conditional check '{{ modified_repos }} in {{ item }}' failed. True {% else %} False {% endif %}): unhashable type: 'list'
Oh I forgot to mention. It is important, that deployment_hosts could also contain two hosts if modified repos include for example service1 and service4.
Q: "deployment_hosts could also contain two hosts if modified repos include for example service1 and service4."
A: Use intersect filter. For example, the playbook
- hosts: localhost
vars:
staging_hosts:
server1: ['service1', 'service2', 'service3']
server2: ['service4', 'service5', 'service6']
server3: ['service7', 'service8', 'service9']
modified_repos: ['service1', 'service4']
tasks:
- set_fact:
deployment_hosts: "{{ deployment_hosts|default([]) + [item.key] }}"
loop: "{{ staging_hosts|dict2items }}"
when: modified_repos|intersect(item.value)|length > 0
- debug:
var: deployment_hosts
gives
deployment_hosts:
- server1
- server2

Missing variable in jinja2, no output with Ansible

I have an Ansible playbook that gathers facts from Cisco switches.
---
- hosts: switches
gather_facts: False
connection: network_cli
vars:
backup_root: ./configs
cli:
host: "{{ inventory_hostname }}"
tasks:
- name: ensure device folder is created
file:
path: "{{ backup_root }}/{{ inventory_hostname }}"
state: directory
- name: Gather all facts
cisco.ios.ios_facts:
gather_subset: all
- name: Serial Number
debug: var=ansible_net_serialnum
- name: Model
debug: var=ansible_net_model
- name: Hostname
debug: var=ansible_net_hostname
- name: Version
debug: var=ansible_net_version
- name: CDP
debug: var=ansible_net_neighbors
- name: Config file
debug: var=ansible_net_config
- name: Stack SW Model Numbs
debug: var=ansible_net_stacked_models
- name: Stack SW Model Numbs
debug: var=ansible_net_stacked_serialnums
- name: Get VLAN Info
cisco.ios.ios_command:
commands: show vlan brief
register: show_vlan
- name: get timestamp
command: date +%Y%m%d
register: timestamp
- name: Generate configuration files
template:
src=roles/discovery/templates/ios_switches.j2
dest="{{ backup_root }}/{{ inventory_hostname }}/{{ inventory_hostname }}.txt" `
Here is the jinja file.
Hostname: {{ ansible_net_hostname }}
Model: {{ansible_net_model}}
Serial Number: {{ansible_net_serialnum}}
IOS Version: {{ansible_net_version}}
IOS Image: {{ansible_net_image}}
Switch Stack Models:
{{ansible_net_stacked_models | to_nice_yaml(indent=2)}}
Switch Stack Serials:
{{ansible_net_stacked_serialnums | to_nice_yaml(indent=2)}}
CDP Neighbors:
{{ansible_net_neighbors | to_nice_yaml(indent=2)}}
Configuration:
{{ansible_net_config}}
VLAN:
{{show_vlan.stdout[0] | to_nice_yaml(indent=2)}}
This all works fine until it hits a switch that cannot stack (e.g. chassis or VSS). When I run the playbook, I get the following-
msg: 'AnsibleUndefinedVariable: ''ansible_net_stacked_models'' is undefined
I've tried using if in Jinja2 like the following
...
Switch Stack Models:
{% if ansible_net_stacked_models is not defined %}
NOT A STACKABLE SWITCH
{% else %}
{{ansible_net_stacked_models | to_nice_yaml(indent=2)}}
{% endif %}
however it fails in the Jinja rendering and does not produce any output.
Is there a way to ignore missing variables in jinja or is there a better way to do this?
you can set a default value to your variable if is not defined
{{ ansible_net_stacked_models|default("NOT A STACKABLE SWITCH", true) | to_nice_yaml(indent=2) }}
Come to find out the error was stopping Ansible from even calling the jinja file. I dug around some more and found that I could set the variable to a default value in the inventory.
[switch:vars]
ansible_net_stacked_models='NOT A STACKABLE SWITCH'
When running the playbook if the device has valid info it will overwrite the default variable we defined in inventory. If the device doesn't have valid info, Ansible will simply pass the default variable we set in inventory, over to jinja2.

Ansible loop list in dictionary and retain the key

I'm trying to create a series of firewalld rules using a variable imported from a yaml file. The yaml file creates a dictionary of service names and the associated ports are a list within each item. A segment of the yaml looks like this:
---
myservice:
description: My service desc
ports:
- 1234/tcp
- 1235/tcp
another:
description: Another service
ports:
- 2222/tcp
- 3333/tcp
The Ansible role I have so far is:
- name: Read services from file
include_vars:
file: "services.yml"
name: services
- name: Create firewalld services
command: >
firewall-cmd --permanent
--new-service={{ item.key }}
--set-description="{{ item.value.description }}"
register: addserv
failed_when: addserv.rc != 26 and addserv.rc != 0
changed_when: not "NAME_CONFLICT" in addserv.stderr
with_dict: "{{ services }}"
- name: Add ports to firewalld service
command: >
firewall-cmd --permanent
--service={{ item.key }} --add-port={{ item.value.ports }}
register: addport
changed_when: not "ALREADY_ENABLED" in addport.stderr
The first segment to create the firewalld service works fine but I'm stumped on the second part with how to loop over the list of ports while retaining the dictionary key. I've tried using subelements to extract the ports list and that works but I can't figure out how to retain the service name from the key.
Use subelements. For example
- debug:
msg: "{{ item.0.key }} - {{ item.0.value.description }} - {{ item.1 }}"
with_subelements:
- "{{ services|dict2items }}"
- value.ports
gives
"msg": "myservice - My service desc - 1234/tcp"
"msg": "myservice - My service desc - 1235/tcp"
"msg": "another - Another service - 2222/tcp"
"msg": "another - Another service - 3333/tcp"

How to increment a value on hosts (Ansible)

I have endless kafka servers to configure, their ids must be different from each other. Configuration must be done through jinja templating.
Their broker.id field should start from 0 to infinity if there are infinite number of servers.
# The id of the broker. This must be set to a unique integer for each broker.
broker.id={{ broker_id }}
Expected on the conf files:
server1
broker.id=0
server2
broker.id=1
serverN
broker.id=N-1
EDIT
main.yml
---
- include: install.yml
tags:
- kafka
- install
- include: config.yml
tags:
- kafka
- config
config.yml
---
- name: server properties
template:
src: server.properties
dest: /opt/kafka/config/server.properties
- name: zookeeper properties
template:
src: zookeeper.properties
dest: /opt/kafka/config/zookeeper.properties
defaults/main.yml
---
#server.properties
broker_id: 0
templates/server.properties
.
.
.
############################# Server Basics #############################
# The id of the broker. This must be set to a unique integer for each broker.
broker.id={{ broker_id }}
############################# Socket Server Settings #############################
.
.
.
Ansible is applying same configuration to multiple servers, as normal behavior. But while applying same configuration broker.id must be unique.
{{ 99999999 | random | to_uuid }}
This is working, still i'm curious if it's possible to assign 0 to broker.id and increment +1 on each server?
Add below
- name: Set Broker ID
set_fact:
broker_id: {{ groups['all'].index(inventory_hostname) }}
followed by other tasks.
May be this helps
- name: Set Broker ID
set_fact:
broker_id: {{ ansible_play_hosts.index(inventory_hostname) }}
I've just added
broker.id={{ groups['kafka'].index(inventory_hostname) | int + 1 }}
to my server.properties.j2 template and it works.
It's possible to create the inventory dynamically in the first play and use it in the second play. For example the playbook below
- hosts: localhost
vars:
no_of_servers: 3
tasks:
- add_host:
name: "srv-{{ item }}"
groups: kafka
id: "{{ my_idx }}"
loop: "{{ range(0, no_of_servers)|list }}"
loop_control:
index_var: my_idx
- hosts: kafka
tasks:
- debug:
msg: "{{ inventory_hostname }} id: {{ id }}"
gives
ok: [srv-0] => {
"msg": "srv-0 id: 0"
}
ok: [srv-1] => {
"msg": "srv-1 id: 1"
}
ok: [srv-2] => {
"msg": "srv-2 id: 2"
}
Try this, I think it will solve
broker.id={{ (inventory_hostname.split('0')[-1] | int) }}

Resources