21 июн. 2017 г.

Lookaround assertions in Python

Приходится по рабочим задачкам ковыряться в регулярках на питоне. И очень плохо в голову складывалась такая удобная штука как Lookahead\Lookbehind assertions. Ясности сильно добавила вот эта статья, где с примерами и пояснениями разложена как сама концепция, так и особенности практического применения.

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-запросы.
        
:

27 апр. 2017 г.

dnsmasq on macos

Потихоньку превращаю сайтик в link-блог, ну да нестрашно.
Потребовалось на скорую руку поднять локальный dnsmasq на макбуке. Если с конфигом все ясно: brew подсказывает, где его взять и куда положить, а что писать внутри, мы и сами с усами, то как запускать и автостартовать демона, сходу неясно. Вот тут поясняют, спасибо.
Краткая выжимка  про запуск:
  • положить скрипт инициализации в ожидаемое ОС место для автозагрузки:
sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons

  • запустить вот прямо сейчас:

sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

25 апр. 2017 г.

Bash. Подводные камни

Прилетела в одном из чатиков ранее не замеченная статья про bash. Очень рекомендуется к прочтению.

13 апр. 2017 г.

Ansible -vvv reveals sensitive data

В процессе дебага плейбуков заметил очень неприятную особенность: при увеличении уровня verbose в лог (на экран и в файл) попадают те данные в открытом виде, что скрыты в vault-файле. В том случае, когда это критично (компрометация в логе закрытых ключей, паролей, и так далее), следует добавлять к заданиям параметр no_log. При его включении весь вывод команды заменяется на подавляется вне зависимости от количества -v.
- name: create user accounts
  user:
    state: present
    name: "{{ item.username }}"
    shell: "{{ item.shell | default(default_shell) }}"
    groups: "{{ item.groups | default(omit) }}"
    comment: "{{ item.name }} ({{ item.comment | default(omit) }})"
    password: "{{ item.password | default(dis_default_pass) }}"
    append: yes
    update_password: on_create
  with_items: "{{regular_users}} + {{vip_users}}"
  no_log: yes
Выглядит это так:
TASK [user-mgmt : create user accounts] ****************************************
ok: [01.example.com] => (item=(censored due to no_log)) => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result"} 
По этому поводу есть тикет в github, в котором также рекомендуется использовать no_log.  

10 апр. 2017 г.

ansible-playbook: exclude host

Если нужно исключить из списка хостов только один-два-несколько позиций, а на остальных плейбук проиграть, то делай так:
ansible-playbook --limit 'all:!bad_host' playbook.yml
Взято отсюда

6 апр. 2017 г.

yum + linux kernel

В CentOS (и, полагаю, во всех его "родственниках") довольно удобно удалять старые ядра:
package-cleanup --oldkernels --count=2
В результате останется текущее ядро и предыдущее.
Если команда не найдена, нужно доустановить пакет yum-utils. 

28 февр. 2017 г.

ansible: запрос значения переменных

В некоторых случаях требуется ручной ввод переменных. Это можно сделать с помощью параметра vars_prompt.
- name: get host info
  hosts: localhost
  vars_prompt:
    - name: remote_host
      prompt: "Where to go? Enter DNS/IP"
      private: no
      default: localhost
    - name: remote_user
      prompt: "Enter remote username"
      private: no
      default: root
    - name: remote_user_pass
      prompt: "Enter password"
      private: yes 
Описание довольно очевидно, из интересностей: private: yes скрывает вводимые символы в консоли (аналогично поведению при вводе пароля при логине); значение default подставляется в случае, если ввод был пустым (также оно отображается в скобках при запросе значения). 
В качестве примера использования можно предложить такое задание:
tasks:
    - name: create dynamic inventory
      add_host:
        name: "{{ remote_host }}"
        groups: temp_hosts
        ansible_user: "{{ remote_user }}"
        ansible_become: yes
        ansible_ssh_pass: "{{ remote_user_pass }}"
        ansible_sudo_pass: "{{ remote_user_pass }}"

21 февр. 2017 г.

ansible: list of facts to file

В последнее время активно работаю с системой управления конфигурациями Ansible, поэтому есть что записать. 
Одна из последних задач: получить список хостов, удовлетворяющий заданному условию. В моем случае нужно было отобрать серверы, у которых в перечне используемых файловых систем есть устаревшая ext2. Информация о смонтированных файловых системах есть в автоматически собираемых фактах (gather_facts: yes в ansible.cfg или описании плейбука), поэтому каких-то дополнительных действий на хосте собирать не нужно.
Чуть усложнило ситуацию то, что элемент ansible_mounts представляет собой не словарь, как большинство элементов, возвращаемых модулем setup, а массив. Таким образом, для получения, к примеру, точки монтирования, требуется использовать не ansible_mounts.fstype, а ansible_mounts[0].fstype, причем количество элементов этого массива будет уникальным для каждого хоста. 
Решением такой задачи будет плейбук следующего вида:
- name: Check hosts for ext2fs available
  hosts: all
  gather_facts: yes
  tasks:
    - name: create list of files with ext2fs partitions
      shell: "echo {{ansible_fqdn|quote}},{{item.mount|quote}},{{item.fstype|quote}}, >> ./ext2fs_list_`date +%s`.csv"
      args:
        chdir: /tmp/
      delegate_to: localhost
      with_items: "{{ ansible_mounts }}"
      when: item.fstype == "ext2"
Если исключить атрибут when: , в файл будут записаны все найденные точки монтирования.

17 февр. 2017 г.

usermod -p

Для тестовых целей может потребоваться образ linux-а с предустановленным паролем root. При создании такого образа (через Docker, Vagrant, установочные скрипты) можно воспользоваться командой 
# usemod -p encrypted_password root
Чтобы получить шифрованное значение пароля
# openssl passwd my_pass

26 янв. 2017 г.

Создание (регистрация) службы Windows

Если нужно сделать из исполняемого файла службу Windows, которая будет доступна в оснастке services.msc и вести себя как другие службы, следует обратиться к странице документации тут.