I'm facing the following problem set in writing an Ansible playbook:
A list is given to me:
rooms:
- room_name: bedroom-1
chairs: 1
- room_name: bedroom-2
chairs: 0
- room_name: bathroom
chairs: 0
- room_name: kitchen
chairs: 4
And I need to transform that into a list of dictionaries with these two keys per item:
chair_name: <room_name>-chair-<chair_number>
room_name: <room_name>
With the example above that would give me:
chair_names:
- chair_name: bedroom-1-chair-1
room_name: bedroom-1
- chair_name: kitchen-chair-1
room_name: kitchen
- chair_name: kitchen-chair-2
room_name: kitchen
- chair_name: kitchen-chair-3
room_name: kitchen
- chair_name: kitchen-chair-4
room_name: kitchen
I've been struggling to accomplish this using Ansible. Is there a way to do it?
Use Jinja to create the structure
chair_names_str: |-
{% for room in rooms %}
{% for i in range(1, room.chairs + 1) %}
- chair_name: {{ room.room_name }}-chair-{{ i }}
room_name: {{ room.room_name }}
{% endfor %}
{% endfor %}
chair_names: "{{ chair_names_str|from_yaml }}"
gives
chair_names:
- {chair_name: bedroom-1-chair-1, room_name: bedroom-1}
- {chair_name: kitchen-chair-1, room_name: kitchen}
- {chair_name: kitchen-chair-2, room_name: kitchen}
- {chair_name: kitchen-chair-3, room_name: kitchen}
- {chair_name: kitchen-chair-4, room_name: kitchen}
Q: "Is it possible to achieve the same results using Ansible loops?"
A: Yes. It is. In the first loop create lists of the chairs
- set_fact:
chairs: "{{ chairs|d([]) + [{'chairs': range(1, item|int + 1)|list}] }}"
loop: "{{ rooms|map(attribute='chairs')|list }}"
gives
chairs:
- chairs: [1]
- chairs: []
- chairs: []
- chairs: [1, 2, 3, 4]
In the second loop create the list. The task below gives the same result
- set_fact:
chair_names: "{{ chair_names|d([]) + [_item] }}"
with_subelements:
- "{{ rooms|zip(chairs)|map('combine') }}"
- chairs
vars:
_item:
chair_name: "{{ item.0.room_name }}-chair-{{ item.1 }}"
room_name: "{{ item.0.room_name }}"
You can see that the first option is much simpler.
Related
Heaving some trouble getting something to work in the argo-helm helm chart.
I am trying to make use of the very conveinet extraObjects value in their argo-cd chart.
Setup
values.yaml
extraObjects:
- apiVersion: v1
kind: ConfigMap
metadata:
labels: "{{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | nindent 4 | trim }}"
name: some-config-map
data:
something: something
template/extra-manifests.yaml
{{ range .Values.extraObjects }}
---
{{ tpl (toYaml .) $ }}
{{ end }}
Result
sadly it will produce
# Source: argo-cd/templates/extra-manifests.yaml
apiVersion: v1
data:
something: something
kind: ConfigMap
metadata:
labels: '
helm.sh/chart: argo-cd-5.19.12
app.kubernetes.io/name: some-config-map
app.kubernetes.io/instance: argocd
app.kubernetes.io/component: configmap
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/part-of: argocd'
name: some-config-map
---
The problem
This is unsurprising really. We put the template into a string so it makes sense that we see a string with the templated content.
I want the template to "float" on the labels field. But I can't do that because {{ blablaba }} isnt a vaild yaml object.
(Or it is but {} is object notation)
Because they are using the toYaml function to turn a valid YAML object into a string for use in the tpl function, I can't represent the template any other way other than using the string.
What I've tried
Comment Trick?
labels: # {{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | nindent 4 | trim }}
Thought becuse of the nindent it could insert the labels under the comment but it must be striped by toYaml or helm when the values used
Yaml !!map
labels: !!map |
{{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | nindent 4 | trim }}
and
labels: !!map |
{{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | fromYaml }}
I don't think yaml has built in conversion but it was worth a shot.
Yaml !!yaml
Tried to play around with !!yaml but I don't understand what it's supposed to do.
Tried this:
labels:
!!yaml: '!' : '!map'
!!value = : {{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | nindent 4 | trim }}
and
labels:
!!yaml: '!' : '!map'
!!value = : {{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | fromYaml }}
Question
Is there any valid YAML notation or object I can use to pass the template through toYaml without using a string?
Is there anyway to represent {{ blabla }} in YAML without using a string?
The Correct Way
Obviously doing away with toYaml and using strings will result in the expected behavior
extraObjects:
- |
apiVersion: v1
kind: ConfigMap
metadata:
labels:
{{ include `argo-cd.labels` (dict `context` . `component` `configmap` `name` `some-config-map`) | nindent 4 | trim }}
name: some-config-map
data:
something: something
{{ range .Values.extraObjects }}
---
{{ tpl . $ }}
{{ end }}
Before I open an issue with the Argo-CD Helm team I just want to know if there is any workaround?
I am trying to following but I am getting this error "The conditional check 'item.vrf = 'default'' failed. The error was: template error while templating string: expected token 'end of statement block', got '='. String: {% if item.vrf = 'default' %} True {% else %} False {% endif %}" how to fix it?
proc_vrf: [{''proc'': ''T1'', ''vrf'': ''default''}, {''proc'': ''T2'', ''vrf'': ''vrf_T2''}, {''proc'': ''T3'', ''vrf'': ''default''}, {''proc'': ''T3'', ''vrf'': ''vrf_T3''}]
- name: Shut ospf for default vrf
cisco.nxos.nxos_config:
lines:
- shutdown
parents: router ospf {{ item.proc }}
save_when: modified
when: item.vrf|lower == 'default'
with_items: "{{ proc_vrf }}"
- name: Shut ospf for other vrf
cisco.nxos.nxos_config:
lines:
- shutdown
parents: router ospf {{ item.proc }}; vrf {{ proc_vrf }}
save_when: modified
when: item.vrf|lower != 'default'
with_items: "{{ proc_vrf }}"
There was an operator error. corrected comparison operator, ==
I am trying to associate users in a aws-auth config map using Helm. I'd like to loop through a nested map in our values file. My attempt to do this is as follows:
mapUsers: |
{{- range $username := .Values.users }}
- groups:
- system:masters
userarn: {{ $username.adminArn }}
username: {{ $username }}
{{- end }}
And the values file is as follows:
users:
username: user.name
userArn: user/Arn
adminArn: user/AdminArn
I'm not certain if this will solve my problem and would like some feedback.
After taking feedback from #Andrew, I was able to get this to work by first changing the structure of my values file to:
users:
testuser:
username: test.user
userArn: testuser/arn
adminArn: testuser/adminArn
I then was able to update my loop to:
{{- range $k := .Values.users }}
- groups:
- system:masters
userarn: {{ .adminArn }}
username: {{ .username }}
{{- end }}
I would like to iterate over a variable in ansible yaml and add key and value in jinja template
variable:
my:
variable:
- name: test
path: /etc/apt
cert: key.crt
my template
{% for key, value in item() %}
{{key}}: {{value}}
{% endfor %}
ansible yaml
- name: test
template:
force: yes
src: test.conf.j2
dest: /tmp/test.conf"
become: yes
with_items:
- "{{ my.variable }}"
How my yaml should look like:
path: /etc/apt
cert: key.crt
You actually have three issues in your task:
When using a loop, may it be loop or all the flavour of with_* you access the element currently looped in with the variable item, so not a function like you used in your task (item())
You are doing a superfluous list of list in
with_items:
- "{{ my.variable }}"
A first step would be to do with_items: "{{ my.variable }}".
An ever better step would be to use the loop replacement of the with_* syntax as suggested in the documentation
We added loop in Ansible 2.5. It is not yet a full replacement for with_<lookup>, but we recommend it for most use cases.
So you will end up with
loop: "{{ my.variable }}"
Then accessing properties of a dictionary in Jinja is done using the syntax
{% for key, value in dict.items() %}
Source: https://jinja.palletsprojects.com/en/2.11.x/templates/#for
So in your case:
{% for key, value in item.items() %}
All together, a working playbook demonstrating this would be:
- hosts: all
gather_facts: no
tasks:
- debug:
msg: |
{% for key, value in item.items() %}
{{key}}: {{value}}
{% endfor %}
loop: "{{ my.variable }}"
vars:
my:
variable:
- name: test
path: /etc/apt
cert: key.crt
That yields the result:
PLAY [all] *******************************************************************************************************
TASK [debug] *****************************************************************************************************
ok: [localhost] => (item={'name': 'test', 'path': '/etc/apt', 'cert': 'key.crt'}) => {
"msg": " name: test\n path: /etc/apt\n cert: key.crt\n"
}
PLAY RECAP *******************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now you just have to reuse this in your template and loop and you should have what you expect.
Objective
I am trying to append a string to a list belonging to a key which is a few layers down within a dictionary.
Explanation
I initialize an empty list buried within some pre-existing variable called info:
tasks:
- set_fact:
info: "{{ info | combine( { host: { repo: { folders: [] }}}, recursive=true ) }}"
In a later task, I wish to append a string to that empty list. This may happen over multiple tasks, so I don't want to replace the empty list, but add onto it as needed. I am currently trying this:
tasks:
- set_fact:
info: "{{ info | combine( { 'host': { 'repo': { 'folders': [] }}}, recursive=true ) }}"
- set_fact:
info: "{{ info.host.repo.folders + ['ERROR. folderX does not exist'] }}"
when: folderX does not exist
- set_fact:
info: "{{ info.host.repo.folders + ['ERROR. folderY does not exist'] }}"
when: folderY does not exist
However, I receive a template error:
FAILED! => {"msg": "template error while templating string: expected name or number. String: {{ info.host.repo.folder + ['ERROR. folderX does not exist'] }}"}
I know that you can simply add elements to a list when the destination is on topmost layer. For example:
- set_fact:
folders: []
- set_fact:
folders: "{{ folders + ['ERROR. folderX does not exist'] }}"
when: folderX does not exist
- set_fact:
folders: "{{ folders + ['ERROR. folderY does not exist'] }}"
when: folderY does not exist
- debug: var=folders
Which, as desired, gives:
TASK [debug] ***************************************************************************************************
"folders": [
"ERROR. folderX does not exist",
"ERROR. folderY does not exist"
]
So, how does the syntax change when I am trying to descend multiple layers and access a list that resides in a nested dictionary? Thank you!
dicts and lists in ansible are "live", so you can update them via set statements
- set_fact:
info: >-
{%- set _ = info.host.repo.update({"folders": []}) -%}
{{ info }}
- set_fact:
info: >-
{%- set _ = info.host.repo.folders.append("ERROR. folderY does not exist") -%}
{{ info }}
when: folderY does not exist
that set _ = business is because ansible's jinja does not support the do statement so one cannot have an assignment statement by itself