best way to modify json in ansible - json

I have a variable (via set_fact) containing a json string:
{
"PolicyVersion": {
"CreateDate": "2017-08-07T02:48:05Z",
"Document": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": [
"arn:aws:iam::123456789123:role/Root_Update_svcacct",
"arn:aws:iam::123456789123:role/Root_Delete_svcacct",
"arn:aws:iam::123456789123:role/Root_Create_svcacct",
"arn:aws:iam::123456789123:role/Root_Full_svcacct",
"arn:aws:iam::987654321987:role/Member1_Create_svcacct",
"arn:aws:iam::987654321987:role/Member1_Update_svcacct",
"arn:aws:iam::987654321987:role/Member1_Delete_svcacct",
"arn:aws:iam::987654321987:role/Member1_Full_svcacct"
]
}
],
"Version": "2012-10-17"
},
"IsDefaultVersion": true,
"VersionId": "v2"
}
}
What is the best way to insert more elements in the "Resource" array?
"arn:aws:iam::001122334455:role/Member1_Create_svcacct",
"arn:aws:iam::001122334455:role/Member1_Update_svcacct",
"arn:aws:iam::001122334455:role/Member1_Delete_svcacct",
"arn:aws:iam::001122334455:role/Member1_Full_svcacct"
I am exploring dumping the variable to a file and inserting the block I want with external shell tools, which does not seem to be elegant.

I don't know about the best way, but one option is to write a simple library module to handle the mechanics of the update for you. You could use the jsonpointer module as a way of locating the data you wish to modify, and then return the modified object to ansible. A starting point might look like:
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
import json
try:
import jsonpointer
except ImportError:
jsonpointer = None
def main():
module = AnsibleModule(
argument_spec=dict(
data=dict(required=True, type='dict'),
pointer=dict(required=True),
action=dict(required=True,
choices=['append', 'extend', 'update']),
update=dict(type='dict'),
extend=dict(type='list'),
append=dict(),
),
supports_check_mode=True,
)
if jsonpointer is None:
module.fail_json(msg='jsonpointer module is not available')
action = module.params['action']
data = module.params['data']
pointer = module.params['pointer']
if isinstance(data, str):
data = json.loads(str)
try:
res = jsonpointer.resolve_pointer(data, pointer)
except jsonpointer.JsonPointerException as err:
module.fail_json(msg=str(err))
if action == 'append':
res.append(module.params['append'])
if action == 'extend':
res.extend(module.params['extend'])
elif action == 'update':
res.update(module.params['update'])
module.exit_json(changed=True,
result=data)
if __name__ == '__main__':
main()
If you drop this into, e.g., library/json_modify.py, you can use it in a playbook like this:
- hosts: localhost
gather_facts: false
vars:
myvar: {
"PolicyVersion": {
"CreateDate": "2017-08-07T02:48:05Z",
"Document": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": [
"arn:aws:iam::123456789123:role/Root_Update_svcacct",
"arn:aws:iam::123456789123:role/Root_Delete_svcacct",
"arn:aws:iam::123456789123:role/Root_Create_svcacct",
"arn:aws:iam::123456789123:role/Root_Full_svcacct",
"arn:aws:iam::987654321987:role/Member1_Create_svcacct",
"arn:aws:iam::987654321987:role/Member1_Update_svcacct",
"arn:aws:iam::987654321987:role/Member1_Delete_svcacct",
"arn:aws:iam::987654321987:role/Member1_Full_svcacct"
]
}
],
"Version": "2012-10-17"
},
"IsDefaultVersion": true,
"VersionId": "v2"
}
}
tasks:
- json_modify:
data: "{{ myvar }}"
pointer: "/PolicyVersion/Document/Statement/0/Resource"
action: extend
extend:
- "arn:aws:iam::001122334455:role/Member1_Create_svcacct"
- "arn:aws:iam::001122334455:role/Member1_Update_svcacct"
- "arn:aws:iam::001122334455:role/Member1_Delete_svcacct"
- "arn:aws:iam::001122334455:role/Member1_Full_svcacct"
register: result
- debug:
var: result.result
The result of running this playbook and the proposed module is:
TASK [debug] *******************************************************************
ok: [localhost] => {
"result.result": {
"PolicyVersion": {
"CreateDate": "2017-08-07T02:48:05Z",
"Document": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": [
"arn:aws:iam::123456789123:role/Root_Update_svcacct",
"arn:aws:iam::123456789123:role/Root_Delete_svcacct",
"arn:aws:iam::123456789123:role/Root_Create_svcacct",
"arn:aws:iam::123456789123:role/Root_Full_svcacct",
"arn:aws:iam::987654321987:role/Member1_Create_svcacct",
"arn:aws:iam::987654321987:role/Member1_Update_svcacct",
"arn:aws:iam::987654321987:role/Member1_Delete_svcacct",
"arn:aws:iam::987654321987:role/Member1_Full_svcacct",
"arn:aws:iam::001122334455:role/Member1_Create_svcacct",
"arn:aws:iam::001122334455:role/Member1_Update_svcacct",
"arn:aws:iam::001122334455:role/Member1_Delete_svcacct",
"arn:aws:iam::001122334455:role/Member1_Full_svcacct"
]
}
],
"Version": "2012-10-17"
},
"IsDefaultVersion": true,
"VersionId": "v2"
}
}
}

Actually Ansible is natively able to read JSON files.
see this question:
add-a-new-key-value-to-a-json-file-using-ansible

This is a little bit old. I know but I felt it could be useful to many people so I'll post here my solution as "the best way to modify a json" that I could personally find.
First of all the JSON. In my use-case I had a bunch of aws snapshots that were encrypted with the wrong kms key and I had to recreate the AMI with the correct key.
So I had to :
get data of the old snapshots ( like size device_name etc.)
create the new snaps with the different key
re-register a new ami with the correct block_device_mapping
Here's the code
- name: get ami
amazon.aws.ec2_ami_info:
image_ids: ami-<id>
region: "{{ region }}"
register: ami
- name: save snapshot ids and device_name and volume_size
set_fact:
snapshot_ids: "{{ ami | json_query('images[].block_device_mappings[].ebs.snapshot_id') }}"
device_name: "{{ ami | json_query('images[].block_device_mappings[].device_name') }}"
volume_size: "{{ ami | json_query('images[].block_device_mappings[].ebs.volume_size') }}"
basically each of the above is a list of each of the 3 things (device_name, snap_id, volume_size) that I need (but it could be extended)
then:
- name: get kms arn
aws_kms_info:
filters:
alias: "{{ kms_keys.alias }}"
region: "{{ region }}"
register: aws_kms_facts_out
- debug:
var: aws_kms_facts_out
- set_fact:
kms_arn: "{{ aws_kms_facts_out['keys'][0].key_arn }}"
- name: copy snap with new encryption key
community.aws.ec2_snapshot_copy:
source_region: "{{ region }}"
region: "{{ region }}"
source_snapshot_id: "{{ item }}"
encrypted: yes
kms_key_id: "{{ kms_arn }}"
loop: "{{ snapshot_ids }}"
register: new_snapshots
and then here's the catch
- set_fact:
new_snapshot_ids: "{{ new_snapshots| json_query('snapshots[].snapshot_id') }}"
- name: creating the block_device_mappings structure (still a list of objects)
ansible.builtin.debug:
msg: '{
"device_name": "{{ item.2 }}",
"delete_on_termination": "true",
"snapshot_id": "{{ item.0 }}",
"volume_size": {{ item.1 }},
"volume_type": "gp2"
}'
loop: "{{ new_snapshot_ids|zip_longest(volume_size,device_name)|list }}"
register: block_device_mappings
- set_fact:
bdm: "{{ block_device_mappings | json_query('results[].msg') }}"
finally
- name: create new ami from newly created snapshots
amazon.aws.ec2_ami:
region: "{{ region }}"
name: "{{ instance.name }}-{{ ansible_date_time.date }}"
state: present
architecture: x86_64
virtualization_type: hvm
root_device_name: /dev/sda1
device_mapping: "{{ bdm }}"
This is how you can do it without requiring any additional trick.
Of course this is declined to my particular use case but you can adapt it to any circumstance, that do not require a complete disassemble and reassemble of the Json itself

Related

Pass JSON from output for another query Ansible

Please help me figure out what I'm wrong with. I'm getting a JSON from Ansible and filtering it, after which I want to save the output and reuse it. But, unfortunately, I get an error that this attribute does not exist. Where did I go wrong?
playbook code:
var:
query_general: "body.results[].{display_name: display_name, subnets: subnets[]}"
- name: parsing query
set_fact:
myvar: "{{ results | json_query(query_general) }}"
register: output
- name: qwe
set_fact:
scndjson: "{{ output.myvar[].display_name }}"
- name: print
debug:
msg: "{{ scndjson }}"
I tried the json_query second case as well, but that didn't work either.
in register:output i have:
[
{
"display_name": "1test",
"subnets": [
{
"gateway_address": "0.0.0.0/25",
"network": "0.0.0.0/25"
}
]
},
{
"display_name": "test",
"subnets": [
{
"gateway_address": "0.0.0.1/25",
"network": "0.0.0.1/25"
}
]
}
]
error:
The task includes an option with an undefined variable.
it can be: output, display_name, etc
UPD:
I corrected the yaml, there are no errors, but the data is not displayed.
tasks:
- name:
nsxt_rest:
hostname: anyhost
username: anyuser
password: anypass
validate_certs: false
method: get
path: /policy/api/v1/infra/segments
register: nsx_results
- debug:
var: nsx_query_general
vars:
nsx_query_general: "{{ nsx_results | json_query('body.results[].{display_name: display_name, subnets: subnets[]}') }}"
register: output
- debug:
var: secondjson
vars:
secondjson: "{{ output|json_query('[].display_name') }}"
Output from nsx_query_general:
{
"nsx_query_general": [
{
"display_name": "test",
"subnets": [
{
"gateway_address": "0.0.0.0/25",
"network": "0.0.0.0/25"
}
]
},
{
"display_name": "1test",
"subnets": [
{
"gateway_address": "0.0.0.1/25",
"network": "0.0.0.1/25"
}
]
}]}
Output from secondjson:
{
"secondjson": "",
"_ansible_verbose_always": true,
"_ansible_no_log": false,
"changed": false
}
Given the registered variable output
output:
- display_name: 1test
subnets:
- gateway_address: 0.0.0.0/25
network: 0.0.0.0/25
- display_name: test
subnets:
- gateway_address: 0.0.0.1/25
network: 0.0.0.1/25
Either use json_query
scndjson: "{{ output|json_query('[].display_name') }}"
, or map attribute
scndjson: "{{ output|map(attribute='display_name')|list }}"
Both declarations create the list
scndjson: [1test, test]
Example of a complete playbook
- hosts: localhost
vars:
output:
- display_name: 1test
subnets:
- gateway_address: 0.0.0.0/25
network: 0.0.0.0/25
- display_name: test
subnets:
- gateway_address: 0.0.0.1/25
network: 0.0.0.1/25
tasks:
- debug:
var: scndjson
vars:
scndjson: "{{ output|json_query('[].display_name') }}"
- debug:
var: scndjson
vars:
scndjson: "{{ output|map(attribute='display_name')|list }}"

ansible obtain values from changing key

I would like to get value from json, but one of the key can be differet.
here is example json
{
"json": {
"id": "9758b1e5-442e-4545-9364-45f28477edfb",
"results": [{
"code": 200,
"host": "localhost",
"message": "no change",
"runTime": 1233,
"tenant": "http-validate-2.usa-dc.com"
}],
"traces": {
"http-validate-2.usa-dc.comCurrent": {
"/Common/10.10.100.10": {
"command": "ltm node"
},
"http-validate-2.usa-dc.comDiff": [{
"command": "ltm virtual",
"kind": "D",
"lhs": {
"default": "yes"
},
"lhsCommand": "ltm virtual",
"path": [
"/http-validate-2.usa-dc.com/app/vs_http-validate-2.usa-dc.com_80",
"properties",
"persist",
"/Common/cookie"
],
"rhsCommand": "ltm virtual",
"tags": [
"tmsh"
]
}]
}
}
}
}
my ansible playbook
tasks:
- name : deploy json file AS3 to F5
debug:
msg: "{{ lookup('file', 'parse.json') }}"
register: atc_AS3_status
- name: debug
debug:
msg: "{{ atc_AS3_status.msg.json['traces']['.*Diff']}}"
I would like to reach key "path" but the key above "http-validate-2.usa-dc.comDiff" can be different like "http-validate-3.can-dc.comDiff" but always finish with Diff
Use json_query, e.g.
- debug:
msg: "{{ json.traces|json_query('*.*[][].path') }}"
should give the list of the paths (there might be more of them)
msg:
- - /http-validate-2.usa-dc.com/app/vs_http-validate-2.usa-dc.com_80
- properties
- persist
- /Common/cookie
Q: "I need path only in the key which ends Diff."
A: JMESPath is not able to search key wildcards, AFAIK. Instead, use select and create the list of the nested keys that match the regex, e.g.
- debug:
msg: "{{ json.traces|json_query('*.keys(#)')|flatten|
select('match', '^.*Diff$')|list }}"
gives
msg:
- http-validate-2.usa-dc.comDiff
Then iterate this list, select path and concatenate the list paths, e.g.
- set_fact:
paths: "{{ paths|d([]) + json.traces|json_query(query) }}"
loop: "{{ json.traces|json_query('*.keys(#)')|flatten|
select('match', '^.*Diff$')|list }}"
vars:
query: '*."{{ item }}"[].path'
gives the list of paths for the keys that match the regex
paths:
- - /http-validate-2.usa-dc.com/app/vs_http-validate-2.usa-dc.com_80
- properties
- persist
- /Common/cookie

Ansible - issue with loop and json_query's

Question
I need to get the ID from the GET because it's needed in the URL in the PUT task to edit a specific "input" entry. I'm using the Ansible URI to talk to a REST API to manage this.
playbook
*host_vars/host.yml
*
---
inputs:
- title: "test_input_api"
type: "org.graylog2.inputs.syslog.udp.SyslogUDPInput"
global: false
configuration:
allow_override_date: false
bind_address: "0.0.0.0"
expand_structured_data: false
force_rdns: false
number_worker_threads: 8
override_source: null
port: 5999
recv_buffer_size: null
store_full_message: true
- title: "test_input_api_2"
type: "org.graylog2.inputs.syslog.udp.SyslogUDPInput"
global: false
configuration:
allow_override_date: false
bind_address: "0.0.0.0"
expand_structured_data: false
force_rdns: false
number_worker_threads: 8
override_source: null
port: 5998
recv_buffer_size: null
store_full_message: true
playbook.yml
---
- name: Configure system
hosts: graylog
connection: local
gather_facts: no
roles:
- graylog/inputs
roles/graylog/inputs/tasks/main.yml
---
- include_tasks: get_inputs.yml
- include_tasks: put_inputs.yml
roles/graylog/inputs/tasks/get_inputs.yml
---
- name: "API GET System Inputs"
uri:
url: http://{{ ansible_host }}:9000/api/system/inputs
url_username : "{{ system.users.triple_admin.api_token }}"
url_password: token
method: GET
return_content: yes
register: get_graylog_inputs
- name: Set Fact
set_fact:
get_input_id: "{{ get_graylog_inputs.content | from_json | json_query('inputs[?title == `{}`] | [0].id '.format(input.title)) }}"
loop: "{{ inputs }}"
loop_control:
loop_var: input
The registered var from the get show's the following
{
"json": {
"inputs": [
{
"attributes": {
"allow_override_date": "False",
"bind_address": "0.0.0.0",
"expand_structured_data": "False",
"force_rdns": "False",
"number_worker_threads": 8,
"override_source": "",
"port": 5999,
"recv_buffer_size": "",
"store_full_message": "True"
},
"content_pack": null,
"created_at": "2021-07-30T15:21:47.590Z",
"creator_user_id": "triple_admin",
"global": false,
"id": "6104170beca15547502665d6",
"name": "Syslog UDP",
"node": "ba52ad48-0b13-419d-b957-d47d8911b413",
"static_fields": {},
"title": "test_input_api",
"type": "org.graylog2.inputs.syslog.udp.SyslogUDPInput"
},
roles/graylog/inputs/tasks/put_inputs.yml
---
- name: "API PUT System Inputs"
uri:
url: http://{{ ansible_host }}:9000/api/system/inputs/{{ get_input_id }}
url_username : "{{ system.users.triple_admin.api_token }}"
url_password: token
headers:
X-Requested-By: X-Ansible
method: PUT
body_format: json
body: "{{ lookup('template', 'templates/post_template.j2') }}"
status_code: 201
return_content: yes
loop: "{{ inputs }}"
loop_control:
loop_var: input
"ansible_facts": {
"get_input_id": "61015085eca1554750236084",
"get_input_titles": "test_input_api"
},
"ansible_facts": {
"get_input_id": "610282d0eca155475024ac91",
"get_input_titles": "test_input_api_2"
Results of running the play
loop 1 - this needs to be matched to the title and therefor get id "61015085eca1554750236084"
"title": "test_input_api",
"url": "http://192.168.21.82:9000/api/system/inputs/610282d0eca155475024ac91",
loop 2
"title": "test_input_api_2",
"url": "http://192.168.21.82:9000/api/system/inputs/610282d0eca155475024ac91",
All help is welcome !
(not related) You don't need to json_decode the result get_graylog_inputs.content. If the server on the over side sends the correct Content-type: application/json header, you should have a get_graylog_inputs.json entry containing the already decoded json result.
You don't need to loop twice. Remove the set_fact loop (which is not correct anyway) in your first file and use the value from your register directly in the second loop.
You did not show any example of your input data so I have to guess a bit here from your jmespath expression... but you basically don't need json_query at all and can stick to generic core ansible filters.
Here is how I see the solution in the second file once you cleaned-up the first:
---
- name: "API PUT System Inputs"
vars:
get_input_id: "{{ get_graylog_inputs.json.inputs | selectattr('title', '==', input.title) | map(attribute='id') | first }}"
uri:
url: http://{{ ansible_host }}:9000/api/system/inputs/{{ get_input_id }}
url_username : "{{ system.users.triple_admin.api_token }}"
url_password: token
headers:
X-Requested-By: X-Ansible
method: PUT
body_format: json
body: "{{ lookup('template', 'templates/post_template.j2') }}"
status_code: 201
return_content: yes
loop: "{{ inputs }}"
loop_control:
loop_var: input
You will probably have to debug and tune the expression to get the input id as I could not do it myself against an example data structure.

Create list of key-values from JSON file in Ansible

My data is in json file as shown below:
vmdata.json
{
"VMDetails":
[
{
"name": "Owner1",
"vms": [ "vm10", "vm11", "vm12", "vm13" ]
},
{
"name": "Owner2",
"vms": [ "vm20", "vm21", "vm22", "vm23" ]
},
{
"name": "Owner3",
"vms": [ "vm30", "vm31", "vm32", "vm33" ]
}
]
}
I need this json data converted to list of key-value pairs and later used for tagging the VMs on vcenter.
Owner1: vm11
Owner1: vm12
Owner1: vm13
Owner1: vm14
Owner2: vm21
Owner2: vm22
Owner2: vm23
Owner2: vm24
Owner3: vm31
Owner3: vm32
Owner3: vm33
Owner3: vm34
I have assigned the content from the data file to variable using :
vms_tobe_tagged: "{{ lookup ('file', 'vmtags.json')| from_json}}"
I query and get the list of owners using this and it works well:
- set_fact:
Owner: "{{ vms_tobe_tagged| json_query('OwnerDetails[*].name') }}"
- name: Test loop
debug:
msg: "{{ Owner }}"
Is it possible to generate the data using this?
loop: "{{ ['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list }}"
Q: "I need this json data converted to a list of key-value pairs ..."
Owner1: vm11
Owner1: vm12
Owner1: vm13
Owner1: vm14
Owner2: vm21
...
Is it possible to generate the data using 'product' filter?
A: Yes. With the product filter it's possible to create a list that comprises all products. The list of the key-value pairs can be created in the next loop. For example the tasks below
- set_fact:
owner_list: "{{ owner_list|
default([]) +
[item.name]|product(item.vms)|list }}"
loop: "{{ vms_tobe_tagged.VMDetails }}"
- set_fact:
owner: "{{ owner|
default([]) +
[{item.0: item.1}] }}"
loop: "{{ owner_list }}"
- debug:
var: owner
give
"owner": [
{
"Owner1": "vm10"
},
{
"Owner1": "vm11"
},
{
"Owner1": "vm12"
},
{
"Owner1": "vm13"
},
{
"Owner2": "vm20"
...
This helped me. I used json_query, with_subelements to iterate.
- set_fact:
VMOwnerdetails: "{{ vms_tobe_tagged| json_query('VMDetails[*]') }}"
- name: Verbose updates
with_subelements:
- "{{VMOwnerdetails}}"
- vms
debug:
msg: "{{ item.0.name }} : {{ item.1 }}"

ansible parse json with several keys with the same name to one list variable

I have an issue to parse a json using ansible
I have a task that connected to rancher and get a json file
task:
- uri:
url: http://rancher.local:8080/v1/hosts
method: GET
user: ##################
password: ################
body_format: json
register: hosts_json
- name: test
set_fact:
rancher_env_hosts: "{{ item.hostname }}"
#when: item.hostname == "*-i-*"
with_items: "{{hosts_json.json.data}}"
- name: output
debug:
msg: "hosts: {{rancher_env_hosts}}"
and I get the following json (after edit it to be more readable):
{
"json": {
"data": [
{
"hostname": "rancher-i-host-02",
"id": "adsfsa"
},
{
"hostname": "rancher-i-host-01",
"id": "gfdgfdg"
},
{
"hostname": "rancher-q-host-01",
"id": "dfgdg"
},
{
"hostname": "rancher-q-host-02",
"id": "dfgdg"
}
]
}
}
When I start the playbook I get only the last host name in the variable and not all the list of hostname. can I register all the list to the same variable?
In addition, I also added a line with the a comment "#" in order to get only the host names that match the string "-i-" bit it's not working. any idea?
This is what filters (and this) for:
- set_fact:
hosts_all: "{{ hosts_json.json.data | map(attribute='hostname') | list }}"
hosts_i: "{{ hosts_json.json.data | map(attribute='hostname') | map('regex_search','.*-i-.*') | select('string') | list }}"
host_all will contain all hostnames, host_i will contain only .*-i-.* matching hostnames.
Try this
- uri:
url: http://rancher.local:8080/v1/hosts
method: GET
user: ##################
password: ################
body_format: json
register: hosts_json
- name: init fact
set_fact:
rancher_env_hosts: "[]"
- name: test
set_fact:
rancher_env_hosts: "{{rancher_env_hosts}} + [ {{item.hostname}} ]"
when: item.hostname | search(".*-i-.*")
with_items: "{{hosts_json.json.data}}"
- name: output
debug:
msg: "hosts: {{rancher_env_hosts}}"
About search you can read here http://docs.ansible.com/ansible/playbooks_tests.html
UPD:
About adding values to array here: Is it possible to set a fact of an array in Ansible?