I am trying to create a template of a markdown file using jinja and the values of the variables are stocked in .yml file ( kind of an inventory of hosts).
my problem is that I think that markdown table that I am trying to fill are not making it easy and since I have tried many alternatives using jinja2 tools and function and still no success I am adressing this issue to the community in hope of getting some insight or tips to get over the problem:
my markdown file contains table such as :
## Servers
### Cluster 1
| | IP | FQDN |
|-------|----|------|
| | | |
my value file .yml is as follows :
servers:
clusters:
- id: 1
test: X.X.X.X
nodes:
- X.X.X.X
- X.X.X.X
- X.X.X.X
in order to retrieve the right values to fill the table I wrote this :
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
| test | {{servers.clusters.id.test}} | |
{% for node in servers.clusters.id.nodes %}|node{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
but it doesn't seem to work and the error is not very explicit (to a jinja2 beginner of course) :
File "[PATH]/filename.md", line 34, in top-level template code
| test | {{server.clusters.id.test}} | |
File "/usr/lib/python3.8/site-packages/jinja2/environment.py", line 471, in getattr
return getattr(obj, attribute)
jinja2.exceptions.UndefinedError: 'list object' has no attribute 'id'
All suggestions are welcome.
You need to loop through the items contained in
clusters
or reference it by index as it is a list.The trick is to understand the structure of the data returned by the YAML parser and how to access that structure from within Jinja.
Jinja expressions are mostly just Python code with a few minor differences. For example, Jinja provides a shortcut which allows you to access dicts using dot syntax. Normally, in Python one would do
mydict['keyname']
to retrieve a value. However, Jinja also supports doingmydict.keyname
Under the hood it actually callsmydict.keyname
but when that fails, it triesmydict['keyname']
as a fallback. If both fail, it raises the first error, which is what you are seeing ('list object' has no attribute 'id'
).Note that the
clusters
item contains a list (as indicated by the-
), which is why the error refers to a 'list object'. You cannot access items of a list using eithermylist.keyname
ormylist['keyname']
. You either need to loop through the list or reference a specific item by its index by number (mylist[0]
for the first item). Unless you are certain that the list will never contain more than one item (in which case why is it a list?) then you will likely want to loop through the items.It appears that you are trying to only include the data for the single item with
id
is1
. If that will always be the first item in the list, you could do:servers.clusters.[0].test
. However, if you can't be sure of that, then you would need to loop through all items and wrap the actual statement within an if statement which checks that the id is equal to the previous set variableid
.Like this for the
IP
column of the first row:Note that
cluster.id
makes reference to the keyid
of an item inclusters
. It is not theid
set by{% set id = 1 %}
. However, it can be compared to it and if the two are equal (both contain the value1
in this case), then the expression is True and the expression contained within is executed. When the two are not equal, then it is ignored.You have the same issue for the second row. However,
nodes
also contains a list of strings. There are no attributes on any of those items so you would just rendernode
in the second loop (rather thannode.id
,node.ip
andnode.fqdn
). Therefore, I assume you want something like this:Of course, you could combine that all together and only do the loop once:
Naturally, if you wanted to combine all
clusters
in a single table, you would remove theif
check and have a single row for each cluster. But that would be a different table that the one you have asked for.If you wanted a separate table for each cluster, you have a few options. You could use the same approach and simply redefine the
id
variable. Like this:Note that the only difference between the two are the first two lines:
Everything else is the same. But if you are just repeating the same code, that is not very efficient. Any future changes would need to be made for each cluster. Instead, just wrap the whole thing up in a loop:
Notice that the loop wraps everything, including the header. The header then gets the
cluster.id
. And, as the body of the table will be repeated for each cluster, we do not need the if statement confining it to only one cluster.It is important to note that this approach will only work if the data for each and every cluster is in the same format/structure.