15 мая 2017 г.

ansible host_vars

Оказался в моем inventory старый-старый хост, при работе с которым даже ping падал.

˜# ansible -m ping old.example.com -e ansible_user=ansible
 [WARNING]: Module invocation had junk after the JSON data: usage: sudo -e [-S] [-p prompt] [-u username|#uid] file ...
old.example.com | FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "Shared connection to 192.168.0.1 closed.\r\n",
    "module_stdout": "sudo: illegal option `-n'\r\nusage: sudo -h | -K | -k | -L | -l | -V | -v\r\nusage: sudo [-bEHPS] [-p prompt] [-u username|#uid] [VAR=value]\r\n            {-i | -s | }\r\nusage: sudo -e [-S] [-p prompt] [-u username|#uid] file ...\r\n",
    "msg": "MODULE FAILURE",
    "rc": 1
Дело оказалось в том, что со второй версии изменился набор опций для вызова sudo: добавился параметр -n, которого "старые" sudo могут не знать. Так как подобных хостов у меня пара штук, менять ради них настройки по умолчанию неинтересно. Вместо этого воспользуемся функционалом Host Variables. Идея простая, нам нужно чтобы переменная окружения ansible_sudo_flags не содержала в себе -n для хоста old.example.com. Решение:
# cat /data/ansible/inventory/host_vars/old.example.com.yml
---
## fix ansible regression for old distros (no -n option with sudo)
##
ansible_sudo_flags: "-H"
Почитать о том, какая переменная и откуда важнее при выполнении заданий, можно тут.

3 мая 2017 г.

ansible + cmdbuild

Как известно, любые задачи вокруг ansible начинаются с перечня ресурсов, управление которыми требуется. Основным способом инвентаризации является обычный текстовый файл (по умолчанию /hosts) c перечнем имен или адресов серверов. Многие знают, что при указании в качестве пути директории ansible будет использовать все находящиеся внутри оной файлы как источники данных о хостах.
Однако при количестве хостов более пары десятков файлы становится трудно поддерживать в актуальном состоянии, и тогда на помощь приходят различные системы инвентаризации и управления активами. В случае с виртуальными средами такой системой в первом приближении является гипервизор (или вышестоящее ПО, типа  vsphere, proxmox, etc). При наличии подобной базы данных единственно правильным и удобным решением является или автоматизация процесса синхронизации локальных inventory-файлов ansible с данными этой базы, или прямое использование данных базы во время выполнения плейбука - dynamic inventory. Для многих случаев существуют готовые скрипты, реализующие функционал динамического построения списка хостов. Также есть страничка, где описываются основные моменты, необходимые для написания собственного решения.
Но вот с программированием (да, даже на питоне :\) у меня никак, поэтому пришлось обходиться средствами самого ansible, а именно: для взаимодействия с CMDBuild пришлось написать плейбук, который актуализирует локальный inventory-файл.
Далее я хочу сохранить для истории все тонкости и интересные с точки зрения изучения продукта моменты, возникшие в процессе разработки этого решения.
Итак, в качестве средства инвентаризации и хранения данных об информационных ресурсах используется CMDBuild. Для автоматизированного общения с базой данных доступен REST API, познакомиться с которым можно вот тут
Так как неавторизованным пользователям доступ не предоставляется (401 при любом запросе), первая задача: пройти аутентификацию. Замечание: как выяснилось в процессе отладки, сервисные учетные записи (в отличие от учетных записей пользователей) не могут быть использованы в веб-интерфейсе, только через API.
Здесь и далее для работы с http-запросами используется ansible-модуль uri. Как обычно, документация к модулю достаточно полна и снабжена примерами, чтобы разобраться в его использовании.
    - name: Get ID and open session with CMDBuild API
      uri:
        url: "{{cmdb_url}}/sessions"
        validate_certs: no
        method: POST
        body: "{\"username\" : \"{{cmdb_user}}\" , \"password\" : \"{{cmdb_pass}}\"}"
        body_format: json
        status_code: 200
        headers:
          Content-Type: application/json
      register: auth_result
Стоит указать, что значение {{cmdb_url}} стоит подсмотреть в документации, в моем случае он выглядит примерно так: "https://cmdb.example.com/cmdbuild/services/rest/v2" , а учетные данные ({{cmdb_user}} и {{cmdb_pass}}) не следует держать в открытом виде. В случае ansible 2.3+ наиболее удобным способом будет использование in-place vault:
    cmdb_user: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          666239373962396262623031663239613432663366383435393634653436366133326339373735373063306464626631613765326331343364356337313432330a386164303261623339326435613862323433386361343262383432326439653235333665313666623431343532616437376163666561613737343535336533610a6234343263376465626264333630306639626137616165373066643034336262
Для более ранних версий - отдельный vault-файл, который следует подгрузить через include_vars. Подробнее о хранилище ansible-vault и методах работы с ним тут
Итак, задание выше отправляет POST по адресу url c учетными данными в json-формате, считается выполненным при получении http-кода 200 и записывает результат выполнения (тоже json) в переменную {{auth_result}}.
Результатом этого запроса будет открытая на сервере сессия с идентификатором {{auth_result.json.data._id}}, который далее необходимо вкладывать в заголовок каждого запроса. 
Далее, имея доступ к дереву ресурсов (в рамках прав учетной записи), можно запросить требуемые нам данные. В CMDBuild есть свои встроенные фильтры, которые можно передавать через http в виде параметра ?filter=, а также свой sql-подобный язык запросов CQL, передающийся через параметр ?cql=. Мои потребности полностью покрывались фильтрами, поэтому cql-запросы не рассматривались.
Однако с фильтрами тоже все непросто. В текущей версии документации эта тема практически не затронута. Где-то на форумах нашлось описание конструкции фильтра:
'{"filter": {"attribute": "Description","operator":"contain","value":["SomeValue"],"parameterType":"fixed" }}'
Там же поступило предложение настраивать фильтр в веб-интерфейсе и подглядывать за передаваемыми http-параметрами в окне консоли браузера. Иных способов пока, к сожалению, нет. В общем, неким магическим для меня образом было получено несколько фильтров, с каждым из которых был создан запрос. Вот пример одного из них:
    ## filter_prod: Description
    ##  Status: Production,[03]
    filter_prod: "?filter=%7B%22attribute%22%3A%7B%22simple%22%3A%7B%22attribute%22%3A%22Status%22%2C%22operator%22%3A%22equal%22%2C%22value%22%3A%5B03%5D%2C%22parameterType%22%3A%22fixed%22%7D%7D%7D"
     - name: Query hosts with "Production" status (filter_prod)
      uri:
        url: "{{cmdb_url}}/classes/Server/cards{{filter_prod}}"
        validate_certs: no
        method: GET
        status_code: 200
        headers:
          CMDBuild-Authorization: "{{auth_result.json.data._id}}"
          Content-Type: application/json
        when: auth_result.json.data._id is defined
      register: prod_result
      no_log: yes
Большая часть из описания задания совпадает с вышесказанным. Из особенностей: добавлен дополнительный параметр headers, где передается токен авторизации - CMDBuild-Authorization (название которого тоже, как и в случае с фильтрами, не освещено в документации). Результат выполнения GET запроса - JSON с описанием карточек всех ресурсов класса "Server", подходящих под фильтр {{filter_prod}}. Так как вывод идет в stdout, количество выпавшей в консоль информации может удивить, поэтому добавлен параметр  no_log: yes, запрещающий любой вывод результатов этого задания вне зависимости от уровня журналирования.
Также добавлено условие выполнения задания: when: auth_result.json.data._id is defined. В случае неуспешной аутентификации плейбук не будет сыпать ошибками, а просто завершится без выполнения запросов.
После выполнения всех запросов хорошим тоном будет закрыть сессию:
    - name: Close session with CMDBuild API
      uri:
        url: "{{cmdb_url}}/sessions/{{auth_result.json.data._id}}"
        validate_certs: no
        method: DELETE
        status_code: 204
        headers:
          CMDBuild-Authorization: "{{auth_result.json.data._id}}"
Отмечу лишь, что успешный код выполнения этого запроса: 204.
Для удобства дальнейшего использования полученные списки хостов следует прогруппировать, для этого запишем названия необходимых групп в inventory-файл:
    - name: Create ansible groups
      lineinfile:
        state: present
        dest: "{{cmdb_inventory}}"
        regexp: '^{{ item|replace("[", "\[")|replace("]", "\]") }}'
        line:  '{{ item }}'
        insertbefore: BOF
        create: yes
      with_items:
        - '[cmdb-prod]'
Из особенностей здесь описание регулярки: помимо обычного указания элемента здесь последовательно используется два фильтра replace, подставляющих символ экранирования для квадратных скобок. Без этого изменения мы получаем не выражение для поиска, а диапазон символов, и поиск не срабатывает. За подсказку по проблеме спасибо автору этого поста.
Наконец, осталось лишь записать выгруженные хосты в файл.
    - name: Write '[cmdb-prod]' hosts to inventory file
      lineinfile:
        state: present
        dest: "{{cmdb_inventory}}"
        line: "{{item}}"
        insertafter: '^\[cmdb-prod\]'
      with_items: "{{ prod_result.json.data | map(attribute='HostName') | list | sort(reverse = True) }}"
Стоит пояснить происходящее внутри параметра with_items. Если содержимое переменной в Ansible - JSON, то к ее элементам можно получить доступ через var_name.json. В выводе CMDBuild присуствуют массивы meta и data, содержимое последнего нас и интересует. Далее используется фильтр map, который показывает только содержимое атрибута HostName внутри  prod_result.json.data. Однако вывод остается в формате JSON, поэтому используется фильтр list, делающий из него plaintext. Сортировка по убыванию используется потому, что каждый {{item}} вставляется после [cmdb-prod], и таким образом мы получаем в итоге сортированный в алфавитном порядке список.

В процессе разработки плейбука запросы приходилось делать множество раз, поэтому очень полезным оказался проект resty. Это обертка над curl, которая встраивается в текущий шелл и позволяет более просто делать различные http-запросы.
        
: