вторник, 14 июля 2015 г.

Ansible - модули

В предыдущей статье удалось заставить ansible работать со стандартными модулями, в частности устанавливать пакеты через менеджер pacman. Однако в archlinux одним pacman сыт не будешь, большинство "вкусных" пакетов находится в пользовательском репозитории пакетов AUR. А разработчики дистрибутива заняли принципиальную позицию, что никогда не будет официального способа ставить неофициальные пакеты, мол если уж решились - то качайте и ставьте самостоятельно. Сообщество выпустило несколько альтернативных менеджеров для работы с AUR, одним из самых популярных считается yaourt. Я это всё к тому, что из ansible просто необходимо уметь вызывать yaourt, а такого модуля у них нет. В принципе это даже хорошо, поскольку даёт повод немного глубже окунуться в ansible и написать такой модуль самостоятельно.


Начинаем как ни странно с документации. Ещё не лишним будет иметь перед глазами то, как написан модуль pacman, исходники ведь открытые, нужно пользоваться. Приведу сразу результат того, что получилось (файл "~/.config/ansible/library/yaourt.py"):
#!/usr/bin/python2
# -*- coding: utf-8 -*-

DOCUMENTATION = '''
---
module: yaourt
short_description: Manage packages with I(yaourt)
description:
    - Manage packages with the I(yaourt) package manager, which is used by
      Arch Linux and its variants.
version_added: "1.0"
author:
    - "'ReanGD (@novovladimir)'"
notes: []
requirements: []
options:
    name:
        description:
            - Name of the package to install.
        required: false
        default: null

    state:
        description:
            - Desired state of the package.
        required: false
        default: "present"
        choices: ["present"]

    update_cache:
        description:
            - Whether or not to refresh the master package lists. This can be
              run as part of a package installation or as a separate step.
        required: false
        default: "no"
        choices: ["yes", "no"]
'''

EXAMPLES = '''
# Install package foo
- yaourt: name=foo state=present

# Run the equivalent of "yaourt -Sya" as a separate step
- yaourt: update_cache=yes
'''

import os.path
from ansible.module_utils.basic import *

YAOURT_PATH = "/usr/bin/yaourt"


def query_package(module, name):
    lcmd = "pacman -Qi %s" % (name)
    lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False)
    return lrc == 0


def update_package_db(module):
    cmd = "yaourt -Sya"
    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
    if rc == 0:
        return True
    else:
        module.fail_json(msg="could not update package db")


def install_packages(module, packages):
    install_c = 0

    for i, package in enumerate(packages):
        installed = query_package(module, package)
        if not installed:
            cmd = "yaourt -S %s --noconfirm" % (package)
            rc, stdout, stderr = module.run_command(cmd, check_rc=False)
            if rc != 0:
                module.fail_json(msg="failed to install %s" % (package))
            install_c += 1

    if install_c != 0:
        msg = "installed %s package(s)" % (install_c)
        module.exit_json(changed=True, msg=msg)

    module.exit_json(changed=False, msg="package(s) already installed")


def check_packages(module, packages):
    would_be_changed = []
    for package in packages:
        if not query_package(module, package):
            would_be_changed.append(package)
    if would_be_changed:
        msg = "%s package(s) would be installed" % (len(would_be_changed))
        module.exit_json(changed=True, msg=msg)
    else:
        module.exit_json(change=False, msg="package(s) already installed")


def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(aliases=['pkg']),
            state=dict(default='present', choices=['present']),
            update_cache=dict(default='no', aliases=['update-cache'],
                              choices=BOOLEANS, type='bool')),
        required_one_of=[['name', 'update_cache']],
        supports_check_mode=True)

    if not os.path.exists(YAOURT_PATH):
        msg = "cannot find yaourt, looking for %s" % (YAOURT_PATH)
        module.fail_json(msg=msg)

    p = module.params

    if p["update_cache"] and not module.check_mode:
        update_package_db(module)
        if not p['name']:
            module.exit_json(changed=True,
                             msg='updated the package master lists')

    if p['update_cache'] and module.check_mode and not p['name']:
        module.exit_json(changed=True,
                         msg='Would have updated the package cache')

    if p['name']:
        pkgs = p['name'].split(',')
        if module.check_mode:
            check_packages(module, pkgs)

        if p['state'] in ['present']:
            install_packages(module, pkgs)


if __name__ == '__main__':
    main()
Как видно, это полностью автономный python модуль, его можно запустить даже без ansible и он сделает свою работу. Управляющие команды в него передаются через параметры командной строки, а результат он в виде json выдаёт на stdout. Это замедляет работу, но такое решение объяснимо - разработчики хотели дать возможность писать расширения на любом языке, но не уверен, что много кто этим пользуется

Требования к такому модулю почти полностью отсутствуют. Регламентируется лишь, что входные будут передаваться в формате "key=value". А для выходные значение требуется выдавать в виде json с некоторыми обязательными полями, говорящих насколько успешно удалось справится с работой, я их описывать не буду, проще посмотреть в документации. Обращу внимание, что нельзя печатать что-то лишнее в stdout, поскольку результирующий json нужно будет отдавать туда же, ansible может не разобрать ответ. Для python они предлагают небольшую библиотеку, которая поможет разобрать входные параметры и сформировать ответ, но конечно же можно легко обойтись и без неё. Да и ещё убедительно просят писать документацию и примеры использования.
Описывать что твориться в модуле, я думаю смысла не имеет - там только запуск yaourt и разбор результатов. По интерфейсу получился аналог стандартного модуля pacman, правда умеет он лишь устанавливать пакеты и обновлять кеш, но для текущих нужд достаточно и этого. Заменим в playbook из прошлой статьи название модуля "pacman" на "yaourt":
- hosts: all

  tasks:
    - name: Refresh package list
      yaourt: update_cache=yes

    - name: Install python packages
      yaourt: name={{ item }} state=present
      with_items:
        - python2-idna
        - python2-notify
И запустим:
$ ansible-playbook ~/.config/ansible/test.yml -i ~/.config/ansible/hosts

PLAY [all] ******************************************************************** 

GATHERING FACTS *************************************************************** 
ok: [home]

TASK: [Refresh package list] ************************************************** 
changed: [home]

TASK: [Install python packages] *********************************************** 
changed: [home] => (item=python2-idna)
changed: [home] => (item=python2-notify)

PLAY RECAP ******************************************************************** 
home                       : ok=3    changed=2    unreachable=0    failed=0
Поведение и результаты точно такие же как и в оригинальном модуле, но теперь нам стали доступны любые пакеты из AUR.

Промежуточные итоги: разработать свой модуль - оказалось проще простого, документация, открытый код и простая архитектура сделали своё грязное дело. Хотя именно для данного примера bash оказался бы сильно проще - вообще ничего писать бы не пришлось, но в общем случае понятно, что в такой модуль можно засунуть куда больше, чем простой вызов одной команды bash.

Посмотрим, что будет дальше.

Комментариев нет:

Отправить комментарий