Ansible: передаем json в теле запроса, используя модуль uri
Предыстория вопроса
Задачка, которая заставила задаться этим вопросом выглядела совсем не сложной: есть ansible playbook-и, которые заказывают виртуальные машины в облаке, накатывают на них необходимое окружение,
после чего довольные разработчики и тестировщики идут на получившиеся стенды и ломают их заниматься делом. Но чтобы они наверняка знали куда идти и какое окружение есть на каком стенде
(а могут быть разные наборы) так повелось, что создавалась страничка в confluence (wiki-подобная штука от atalssian). Создавалась она
вручную и, естественно, делать это никто особо не хотел и частенько забывал об этом.
Решение напрашивалось давно: идем в гугл, находим документацию по REST API у Confluence, лепим на коленке POST запрос к https://confluencehost/rest/api/content:
{
"type":"page",
"title":"Стенд крутейший: описание",
"ancestors": [
{
"id": "123456789"
}
],
"space":{
"key":"SPACE_NAME"
},
"body":{
"storage":
{
"value":"<p>Тут крутое описание всего что надо</p>",
"representation":"storage"
}
}
}
Отправляем запрос через любой REST-клиент (я использую DHC для Chrome), видим статус код 200, радуемся. Идем в confluence - видим, страничка создалась!
Осталась самая малость - запилить все это в ansible, подставляя переменные в правильный Jinja2 - шаблон.
Сделаем такой HTML-шаблон будущей странички:
<h2>Назначение стенда</h2>
<p>{{work_description}} </p>
<p>
<strong>Ответственный за стенд</strong>: {{responsible_for_stand}}</p>
<h2>Описание серверов</h2>
...
и еще много букв
Подготовим структуру в ansible:
- inventory
-- ...
- roles
-- confluence
--- create_stand_description_page
---- tasks
----- main.yml
---- templates
----- stand_description.j2
-- ...
- playbook.yml
Добавим вызов нашей новой роли в playbook.yml:
---
...
- name: Create confluence page with stand description
hosts: localhost
gather_facts: no
roles:
- confluence/create_stand_description_page
...
И пойдем довольные читать документацию по модулю uri в ansible, который умеет делать HTTP-запросы.
Видим там такой пример:
- name: Create a JIRA issue
uri:
url: https://your.jira.example.com/rest/api/2/issue/
method: POST
user: your_username
password: your_pass
body: "{{ lookup('file','issue.json') }}"
force_basic_auth: yes
status_code: 201
body_format: json
Кажется, то что надо!
Но стоп, у нас HTML-страничка, которую надо передать в JSON-файле. Для этого сначала ее бы надо привести в правильный вид, иначе JSON будет невалидным. Для этого напрашивается использовать какой-нибудь jinja-фильтр, немного гуглим и находим его: он называется неожиданно to_json. Получается, что фильтр надо применить к страничке, в которую на тот момент уже будут подставлены необходимые переменные (т.к. они тоже могут содержать спец. символы).
Немного подумав, добавим в templates роли еще один шаблон stand_request.j2:
{
"type":"page",
"title":"{{page_name}}",
"ancestors": [
{
"id": "{{page_id}}"
}
],
"space":{
"key":"{{space_name}}"
},
"body":{
"storage":
{
"value":{{rendered_template|to_json}},
"representation":"storage"
}
}
}
Собираем из всего этого конструкцию:
---
- set_fact:
rendered_template: "{{ lookup('template', '../templates/stand_description.j2') }}"
- name: "Edit template"
template:
src: "../templates/stand_request.j2"
dest: "{{playbook_dir}}/request.json"
mode: 0777
- name: Create confluence page with stand description
uri:
url: https://confluencehost/rest/api/content
method: POST
user: "{{user}}"
password: "{{password}}"
body: "{{ lookup('file','{{playbook_dir}}/request.json') }}"
force_basic_auth: yes
status_code: 200
body_format: json
Ну, можно выдохнуть, теперь все будет хорошо...
При выполнении получаем в файле request.json вполне себе валидный json:
{
"type":"page",
"title":"TEST",
"ancestors":[
{
"id":"123456"
}
],
"space":{
"key":"KEY"
},
"body":{
"storage":{
"value":"<h2>\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0441\u0442\u0435\u043D\u0434\u0430<\/h2>",
"representation":"storage"
}
}
}
Плейбук выполнился без ошибок и пора бы уже отчитаться о проделанной работе. Идем в confluence и неожиданно для себя видим:
Копируем запрос из сформированного файла request.json и выполняем точно такой же запрос, но через DHC и не перестаем удивляться, увидев, что все работает как положено:
Часы, потраченные на попытки найти решение в гуглах, стековерфлоу и так далее успеха не принесли, но зато удалось узнать для себя кое-что новое.
Ansible: cпособы передать JSON в теле запроса
Способов таких, как оказалось, есть несколько.
1. Можно описать YAML - конструкцию и в конце указать ansible-у, что ее надо обрабатывать как json.
- uri:
url: some_url_here
method: POST
body:
level1:
level2string: level2value
level1_other: 1
level1_bool: false
level1_array:
- array_item1
- array_item2
status_code: 200
body_format: json
На выходе конструкция даст JSON вида:
{
"level1": {
"level2": "level2value"
},
"level1_number": 1,
"level1_bool": false,
"level1_array": [
"array_item1",
"array_item2"
]
}
Для отладки удобно использовать онлайн-конвертер YAML в JSON, например, тут.
Нужно понимать, что указав body_format: json, ansible сам добавит в заголовок запроса "Content-Type: application/json", если он не нужен - его надо переопределить, указав дополнительно HEADER_Content-Type: some_other_type.
2. Можно определить переменную и предоставить jinja возможность сделать из нее json.
vars:
json_var:
key: value
...
body: ' {{json_var|to_json}}'
3. С использованием lookup-плагина, как мы и пытались сделать:
body: "{{ lookup('file','{{playbook_dir}}/request.json') }}"
4. Быстрый путь - использовать YAML folded style:
body: >
{"items":{"key":"value"}}
5. Дешево и сердито - заэкранировать все что можно
body: "{\"items\":{\"key\":\"value\"}}
Возвращаясь к истории
А закончилось все в итоге хорошо - использовав первый вариант представления JSON из списка выше.
Playbook получился вот такого вида:
---
- set_fact:
rendered_template: "{{ lookup('template', '../templates/stand_description.j2') }}"
- name: Create confluence page with stand description
uri:
url: https://confluence.billing.ru/rest/api/content
method: POST
user: "{{user}}"
password: "{{password}}"
body:
type: page
title: "{{stand_name}}"
ancestors:
- id: "{{parent_page_id}}"
space:
key: "{{space_key}}"
body:
storage:
value: "{{rendered_template}}"
representation: "storage"
force_basic_auth: yes
status_code: 200
body_format: json
При таком варианте проблем с кодировками больше не возникало и странички успешно создавались, освобождая меня от лишней работы :)