I am building a play that requires the existence of a set of variables and also requires the variables to not equal bar. Below is an example of my definition of extra variables:
ansible.extra_vars = {
A: "foo",
B: "bar",
C: "foo",
...
}
When my play runs, I see the following when I print a debug message for each item:
(item=A) => {
"item": "A",
"var": {
"A": "foo"
}
}
When I try the below evaluation, I would expect a failure on B, however all tests pass:
- fail: msg="bar is not a valid variable value for this play"
with_items: required_vars
when: var.{{ item }} is not defined or (var.{{ item }} is defined and var.{{ item }} == "bar")
Does anyone have any suggestions on what I need to do to evaluate the value and cause a failure when bar is encountered?
extra_vars seems to be a dict, so you should use with_dict instead of with_items.
I'm not sure how exactly you did define extra_vars. When I define it in the playbook like below I get different output from debug. When I define it like that (ansible.extra_vars) in a group_vars file I do not get any data in the playbook at all.
Also, when I use my extra_vars dict together with with_items I get the following error:
fatal: [localhost] => with_items expects a list or a set
So there seems to be something strange about your extra_vars.
Here's my working example:
---
- hosts:
- localhost
gather_facts: no
vars:
extra_vars: {
A: "foo",
B: "bar",
C: "foo"
}
tasks:
- debug: var=extra_vars
- debug: msg="{{ item.key }}"
with_dict: extra_vars
when: item.value is not defined or (item.value is defined and item.value == "bar")
...
Output:
PLAY [localhost] **************************************************************
TASK: [debug var=extra_vars] **************************************************
ok: [localhost] => {
"var": {
"extra_vars": {
"A": "foo",
"B": "bar",
"C": "foo"
}
}
}
TASK: [debug msg="{{ item.key }}"] ********************************************
skipping: [localhost] => (item={'key': 'A', 'value': 'foo'})
skipping: [localhost] => (item={'key': 'C', 'value': 'foo'})
ok: [localhost] => (item={'key': 'B', 'value': 'bar'}) => {
"item": {
"key": "B",
"value": "bar"
},
"msg": "B"
}
PLAY RECAP ********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
Related
The idea is to add an item into the array if the item does not exist. The select attribute function returns an empty list even though there are items. Therefore the length of an empty is always 0, which in turn will create a new list every time in my case. So the union function in add a new comment section is returning a list with the only new comment erasing all the old ones.
I understand that the issue is with t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length == 0 conditional statement but I am not sure what I am doing wrong. I have tried many variations of this command. Few of them are commented. Any advice/suggestion is appreciated :)
main.yml:
- name: Get comment array from facts
set_fact:
common_motd_qsc_comments_array: "{{ ansible_local['snps']['motd']['comment_array'] }}"
register: t_array_exists
when:
- ansible_local['snps']['motd'] is defined
- ansible_local['snps']['motd']['comment_array'] is defined
- debug:
var: t_array_exists
- debug:
var: t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list
# var: ansible_facts|selectattr("common_motd_qsc_comments_array", "defined")|list
# var: t_array_exists|selectattr("common_motd_qsc_comments_array", "defined")|list
- name: Create an empty array if there is no array
set_fact:
common_motd_qsc_comments_array: []
when:
- t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length == 0
- debug:
var: t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length
- name: Deleting a comment if it exists
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | difference([t_new_entry]) }}"
loop: "{{ common_motd_qsc_delete_comment }}"
when: t_new_entry in common_motd_qsc_comments_array
vars:
t_new_entry: "{{ item | trim }}"
- name: Add a new comment if it doesn't exist
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_new_entry]) }}"
loop: "{{ common_motd_qsc_add_comment }}"
when: t_new_entry not in common_motd_qsc_comments_array
vars:
t_new_entry: "{{ item | trim }}"
- name: Saving comments to snps.fact file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'motd' # header
option: 'comment_array' # key
value: "{{ common_motd_qsc_comments_array }}" # value
Ansible output:
TASK [common/motd_scratch/v1 : Get comment array from facts] ***************************************************************************************
ok: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"t_array_exists": {
"ansible_facts": {
"common_motd_qsc_comments_array": [
"new comment 1"
]
},
"changed": false,
"failed": false
}
}
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"ansible_facts|selectattr(\"common_motd_qsc_comments_array\", \"defined\")|list": []
}
TASK [common/motd_scratch/v1 : Create an empty array if there is no array] *************************************************************************
ok: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"t_array_exists[\"ansible_facts\"]|selectattr(\"common_motd_qsc_comments_array\", \"defined\")|list|length": "0"
}
TASK [common/motd_scratch/v1 : Add a new comment if it doesn't exist] ******************************************************************************
ok: [ansible-poc-cos6] => (item=new comment 22)
TASK [common/motd_scratch/v1 : Saving comments to snps.fact file] **********************************************************************************
changed: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"common_motd_qsc_add_comment": [
"new comment 22"
]
}
snps.fact output:
Before:
[motd]
comment_array = ['new comment 1']
After passing new_comment as "new comment 2":
[motd]
comment_array = ['new comment 2']
Expected:
[motd]
comment_array = ['new comment 1', 'new comment 2']
You can do all of the above in one single easy step. Here is an MCVE playbook to illustrate.
Prior to running the playbook, I added the following file on my machine:
/etc/ansible/facts.d/snps.fact
{
"motd_example1": [
"One message",
"A message",
"Message to delete"
],
"motd_example2": [
"Some messages",
"Mandatory message",
"Other message"
]
}
The playbook
---
- name: Add/Remove list elements demo
hosts: localhost
gather_facts: true
vars:
# The (list of) custom message(s) I want to add if not present
my_custom_mandatory_messages:
- Mandatory message
# The (list of) custom message(s) I want to remove if present
my_custom_messages_to_delete:
- Message to delete
# The list of custom vars I'm going to loop over for demo
# In your case, you can simply use your single motd var
# directly in the below task without looping. I added this
# for convenience as the expression is exactly the same
# in all cases. Only the input data changes
my_motd_example_vars:
- motd_example1
- motd_example2
- motd_example3 # This one does not even exist in ansible_local.snps
tasks:
- name: Show result using local facts for each demo test var
debug:
msg: "{{ ansible_local.snps[item] | default([])
| union(my_custom_mandatory_messages)
| difference(my_custom_messages_to_delete) }}"
loop: "{{ my_motd_example_vars }}"
- name: Proove it works with a totaly undefined var
debug:
msg: "{{ totally.undefined.var | default([])
| union(my_custom_mandatory_messages)
| difference(my_custom_messages_to_delete) }}"
The result
TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show result using local facts for each demo test var] *********************************************************************************************************************************************************************************
ok: [localhost] => (item=motd_example1) => {
"msg": [
"One message",
"A message",
"Mandatory message"
]
}
ok: [localhost] => (item=motd_example2) => {
"msg": [
"Some messages",
"Mandatory message",
"Other message"
]
}
ok: [localhost] => (item=motd_example3) => {
"msg": [
"Mandatory message"
]
}
TASK [Simple proof that it works with a totaly undefined var] *******************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"Mandatory message"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Not sure I understood exactly what the problem is, but if you are trying to merge two dictionaries (with one overwriting the other if the key already exists), then you might want to take a look at:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_advanced_syntax.html#yaml-anchors-and-aliases-sharing-variable-values
The very first example shows exactly that, with just YAML. If that doesn't do the trick, then you might want to try the combine filter:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#combining-hashes-dictionaries
HTH
Alex
I was able to fix it with the help of my colleague. I just had to check if comment_array is not defined instead of trying to find its length. A simple alternate idea which works :)
- name: Create an empty array if there is no array
set_fact:
common_motd_qsc_comments_array: []
when:
- ansible_local['snps']['motd']['comment_array'] is not defined
Thank you all for your help :)
I want to run a task in ansible something similar to the following.
#Task in Playbook
- name : Include tasks
block:
- name: call example.yml
include_tasks: "example.yml"
vars:
my_var: item
with_items:
- [1, 2]
# example.yml
- name: Debug.
debug:
msg:
- "my_var: {{ my_var }}"
with_inventory_hostnames:
- 'all'
I expect the output to be printing the my_var as values 1 in the first iteration and 2 in second iteration of the loop in the playbook. But instead, it is printing the hostnames
# Output
TASK [proxysql : Debug.] ************************************************************************************************
[WARNING]: The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to something else to avoid variable collisions and unexpected behavior.
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.134.34.34"
]
}
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.123.23.23"
]
}
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.112.12.12"
]
}
TASK [proxysql : Debug.] ************************************************************************************************
[WARNING]: The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to something else to avoid variable collisions and unexpected behavior.
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.134.34.34"
]
}
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.123.23.23"
]
}
ok: [10.1xx.xx.xx] => (item=None) => {
"msg": [
"my_var: 10.112.12.12"
]
}
Thanks in advance
There are two problems:
In the playbook, tasks are included in a loop that has the loop variable name item and the included task also has a loop and
the default variable name is again item. This is why the warning
messages and to solve that use loop_control.
my_var: item assignment needs to be in format my_var: "{{ item }}" for correct assignment.
After both the corrections, playbook would look like this.
- name : Include tasks
block:
- name: call example.yml
include_tasks: "example.yml"
vars:
my_var: "{{ outer_item }}"
with_items:
- [1, 2]
loop_control:
loop_var: outer_item
I have a variable in my playbook that's derived from a list. In some instances this variable contains a "-" to separate two values. For example,
Numbers:
- 2211
- 2211-2212
When this is the case I would like to replace the "-" with a "_" based on a conditional: If the number is 4 characters long, do this. Else, replace the "-" with a " _ " and do that.
I've already tried to fiddle around with jinja2 ans regex in my playbooks but so far no luck. Here's what I tried,
number: {% if length(item) == 4 %} {{ item | regex_replace("^(.*)$", "Number_\1") | string }} {% else %} {{ item | regex_replace("^(.*)$", "Number_\1") |replace("-", "_") | string }}
The result that I would like to have,
Number is four characters long:
number: Number_2211
Number is more then 4 characters long:
number: Number_2211_2212
Some of the Error messages I have received are,
ERROR! Syntax Error while loading YAML.
did not find expected key
ERROR! Syntax Error while loading YAML.
found character that cannot start any token
Is there a way to achieve this within the Ansible playbook?
Thanks in advance!
It's not really clear how you're trying to use this data. Ansible isn't great at modifying complex data structures in place, but it has lots of way of transforming data when you access it. So for example, this playbook:
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- "2211"
- "2211-2212"
tasks:
- debug:
msg: "number: {{ item.replace('-', '_') }}"
loop: "{{ numbers }}"
Will output:
TASK [debug] **********************************************************************************
ok: [localhost] => (item=2211) => {
"msg": "number: 2211"
}
ok: [localhost] => (item=2211-2212) => {
"msg": "number: 2211_2212"
}
If you really need to make the transformation conditional on the length (and it's not clear that you do), you could do something like:
- debug:
msg: "{{ item.replace('-', '_') if item|length > 4 else item }}"
loop: "{{ numbers }}"
Update
I see you've selected the other answer. The solution presented here seems substantially simpler (there is no "incomprehensible sequence of filters, regex expressions, and equality checks"), and produces almost identical output:
TASK [debug] **********************************************************************************
ok: [localhost] => (item=445533) => {
"msg": "445533"
}
ok: [localhost] => (item=112234-538) => {
"msg": "112234_538"
}
ok: [localhost] => (item=11) => {
"msg": "11"
}
ok: [localhost] => (item=1111) => {
"msg": "1111"
}
ok: [localhost] => (item=1111-1111) => {
"msg": "1111_1111"
}
ok: [localhost] => (item=11-11) => {
"msg": "11_11"
}
It's not clear, given 11-11, whether you expect 11_11 or 11-11 as output. If you expect the former, this answer is more correct.
You can use an incomprehensible sequence of filters, regex expressions, and equality checks to do this.
#!/usr/bin/env ansible-playbook
- name: Lets munge some data
hosts: localhost
gather_facts: false
become: false
vars:
array:
- 445533
- 112234-538
- 11
- 1111
- 1111-1111
- 11-11
tasks:
- name: Replace hypens when starting with 4 numbers
debug:
msg: "{{ ((item | string)[0:4] | regex_search('[0-9]{4}') | string != 'None')
| ternary((item | regex_replace('-', '_')), item) }}"
loop: "{{ array }}"
PLAY [Lets munge some data] *****************************************************************************************************************************************************************************************************
TASK [Replace hypens when starting with 4 numbers] ******************************************************************************************************************************************************************************
ok: [localhost] => (item=445533) => {
"msg": "445533"
}
ok: [localhost] => (item=112234-538) => {
"msg": "112234_538"
}
ok: [localhost] => (item=11) => {
"msg": "11"
}
ok: [localhost] => (item=1111) => {
"msg": "1111"
}
ok: [localhost] => (item=1111-1111) => {
"msg": "1111_1111"
}
ok: [localhost] => (item=11-11) => {
"msg": "11-11"
}
PLAY RECAP **********************************************************************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
I have an input as such
ok: [localhost] => {
"static_plugin_versions": [
{
"name": "ace-editor",
"version": "1.1"
},
{
"name": "analysis-core",
"version": "1.95"
},
{
"name": "ant",
"version": "1.9"
},
{
"name": "antisamy-markup-formatter",
"version": "1.5"
},
{
"name": "apache-httpcomponents-client-4-api",
"version": "4.5.5-3.0"
}
]
}
My aim to to print out the version by specifying a particular name. In this case specifically looking for the version of analysis-core
What I have tried are the following
- debug:
var: static_plugin_versions['analysis-core']['version']
- debug:
var: static_plugin_versions['analysis-core'].version
- debug:
var: static_plugin_versions[analysis-core.version]
The only thing that works is
- debug:
var: static_plugin_versions[1].version
But this is not feasible since if more entries get added to the dictionary, it will break.
Any indication what Im doing wrong here. Am looking for a way that does not rely on looping.
EDIT
Just tried this
- set_fact:
analysis_core_version: "{{ item.version }}"
when: "'analysis-core' in item.name"
with_items: "{{ static_plugin_versions }}"
- debug:
var: analysis-core-version
But I get:
TASK [copy : set_fact] *******************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [localhost] => (item={u'version': u'1.1', u'name': u'ace-editor'})
ok: [localhost] => (item={u'version': u'1.95', u'name': u'analysis-core'})
skipping: [localhost] => (item={u'version': u'1.9', u'name': u'ant'})
skipping: [localhost] => (item={u'version': u'1.5', u'name': u'antisamy-markup-formatter'})
skipping: [localhost] => (item={u'version': u'4.5.5-3.0', u'name': u'apache-httpcomponents-client-4-api'})
TASK [copy : debug] **********************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"analysis-core-version": "VARIABLE IS NOT DEFINED!"
}
The easiest way to do this is with the selectattr filter, which lets you apply a filter to a list of objects. For example, if I have this playbook:
---
- hosts: localhost
gather_facts: false
vars:
"static_plugin_versions": [
{
"name": "ace-editor",
"version": "1.1"
},
{
"name": "analysis-core",
"version": "1.95"
},
{
"name": "ant",
"version": "1.9"
},
{
"name": "antisamy-markup-formatter",
"version": "1.5"
},
{
"name": "apache-httpcomponents-client-4-api",
"version": "4.5.5-3.0"
}
]
tasks:
- debug:
msg: "version of {{ item }} is {{ (static_plugin_versions|selectattr('name', 'eq', item)|first).version }}"
loop:
- ace-editor
The output will be:
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => (item=ace-editor) => {
"msg": "version of ace-editor is 1.1"
}
Or, using your example:
- set_fact:
analysis_core_version: "{{ (static_plugin_versions|selectattr('name', 'eq', 'analysis-core')|first).version }}"
- debug:
var: analysis-core-version
Which produces:
ok: [localhost] => {
"analysis_core_version": "1.95"
}
If necessary, the json_query filter allows for substantially more complex queries.
Solution 1 (not optimized)
As suggested by Illias comment, you can use a loop to go other each element in the list, match its name value and print its version when condition is met.
- name: print version of analysis-core
debug:
msg: "{{ item.version }}"
when: item.name == 'analysis-core'
loop: "{{ static_plugin_versions }}"
Meanwhile this will go other each element and skip task whenever there is no match. If you have hundreds of plugins, this will soon become unreadable in your ansible execution log.
Solution 2 (my preferred)
Query your data stucture to get exactly what you need. Your friend here is the json_query filter (and you should read jmespath doc if you want to go further). For your particular example
- name: print version of analysis-core
debug:
msg: >-
{{ (static_plugin_versions | json_query("[?name == 'analysis-core'].version")).0 }}
Notes:
I inferred that your plugin names where unique in your list. Therefore querying for a particular plugin name returns a single element and I can print the first one in list as the expected result.
The order of single and double quotes matters in the above example (see jmespath specification: you'll get an empty string result if you switch them around.
I used a yaml folded block in my example so I didn't have to escape the double quotes (because I'm a very lazy B#sT#rD :D), but you can use a normal string and escape double quotes if you wish.
Explanation
Assuming I have a dictionary mydict set to { "key1": "value1" }:
The result of dictsort filter (mydict|dictsort) in Ansible seems to be a list containing another list:
[
[
"key1",
"value1"
]
]
However, when accessing the first element of this list directly in Jinja2 template (mydict|dictsort)[0], it renders to a strangely looking:
(u'key1', u'value1')
Then, if I set a fact with the value of (mydict|dictsort), it behaves like a regular list - accessing the first element with [0] results in:
[
"key1",
"value1"
]
Accessing its [0] element returns key1.
But if I set a fact with the value of (mydict|dictsort)[0], it behaves like a string - accessing [0] element returns the first character, i.e. (.
On the other hand, if I access subelements directly, for example (mydict|dictsort)[0][0], it behaves like a list, i.e. returns key1.
Questions
What is (u'key1', u'value1')? What kind of object does dictsort produce?
How to access the dictsort results in a consistent, reliable way?
Full playbook:
---
- hosts: localhost
gather_facts: no
connection: local
vars:
mydict:
key1: value1
tasks:
- name: show dict
debug:
msg: "{{ mydict }}"
- name: show mydict|dictsort
debug:
msg: "{{ mydict|dictsort }}"
- set_fact:
mydict_dictsorted: "{{ mydict|dictsort }}"
- name: show (mydict|dictsort)[0]
debug:
msg: "{{ (mydict|dictsort)[0] }}"
- name: show mydict_dictsorted[0]
debug:
msg: "{{ mydict_dictsorted[0] }}"
- name: show (mydict|dictsort|list)[0]
debug:
msg: "{{ (mydict|dictsort|list)[0] }}"
- name: show (mydict_dictsorted|list)[0]
debug:
msg: "{{ (mydict_dictsorted|list)[0] }}"
- set_fact:
mydict_dictsorted_element: "{{ (mydict|dictsort)[0] }}"
- name: mydict_dictsorted_element
debug:
msg: "{{ mydict_dictsorted_element }}"
- name: mydict_dictsorted_element[0]
debug:
msg: "{{ mydict_dictsorted_element[0] }}"
- name: (mydict|dictsort)[0][0]
debug:
msg: "{{ (mydict|dictsort)[0][0] }}"
Full transcript:
PLAY [localhost] ********************************************************************************************
TASK [show dict] ********************************************************************************************
ok: [localhost] => {
"msg": {
"key1": "value1"
}
}
TASK [show mydict|dictsort] *********************************************************************************
ok: [localhost] => {
"msg": [
[
"key1",
"value1"
]
]
}
TASK [set_fact] *********************************************************************************************
ok: [localhost]
TASK [show (mydict|dictsort)[0]] ****************************************************************************
ok: [localhost] => {
"msg": "(u'key1', u'value1')"
}
TASK [show mydict_dictsorted[0]] ****************************************************************************
ok: [localhost] => {
"msg": [
"key1",
"value1"
]
}
TASK [show (mydict|dictsort|list)[0]] ***********************************************************************
ok: [localhost] => {
"msg": "(u'key1', u'value1')"
}
TASK [show (mydict_dictsorted|list)[0]] *********************************************************************
ok: [localhost] => {
"msg": [
"key1",
"value1"
]
}
TASK [set_fact] *********************************************************************************************
ok: [localhost]
TASK [mydict_dictsorted_element] ****************************************************************************
ok: [localhost] => {
"msg": "(u'key1', u'value1')"
}
TASK [mydict_dictsorted_element[0]] *************************************************************************
ok: [localhost] => {
"msg": "("
}
TASK [(mydict|dictsort)[0][0]] ******************************************************************************
ok: [localhost] => {
"msg": "key1"
I checked the values with copy/content and they are the same as debug's (except indentation), so posting debug results for clarity.
dictsort produces a list of tuples. It uses dict.items() under the hood.
So when you access it as (mydict|dictsort)[0], you access Python's tuple.
Whereas if you access it after it is templated, you get generic list, because JSON doesn't make difference between tuples and lists, it has only lists.
Update: how to test – insert print into _dump_results here, like this:
print("Unaltered: {}".format(abridged_result))
return json.dumps(abridged_result, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
And see this as the output:
TASK [show mydict|dictsort] ***************************
Unaltered: {'msg': [(u'key1', u'value1')]}
ok: [localhost] => {
"msg": [
[
"key1",
"value1"
]
]
}
Update2: why list of tuples becomes list of lists, but single tuple becomes string repr?
This is because of the fact that Jinja2 expression inside {{...}} can produce only string as its output, and there's some Ansible template magic done to try to type-cast it back to some complex type. But this magic only works with strings that looks like dicts or lists and not tuples. So if you have dict with tuples inside or list of tuples, you'll get it evaluated, but if you have a single tuple, it will remain a string. Here's demo of this:
- name: results in a string
debug:
msg: "{{ test_str }}"
vars:
test_str: "(u'a', u'b')"
- name: results in a list of tuples/lists
debug:
msg: "{{ test_str }}"
vars:
test_str: "[(u'a', u'b')]"
Output:
TASK [results in a string] ******************************************
ok: [localhost] => {
"msg": "(u'a', u'b')"
}
TASK [results in a list of tuples/lists] ****************************
ok: [localhost] => {
"msg": [
[
"a",
"b"
]
]
}