Pull to refresh

Comments 36

Sparrowdo позволяет конфигурировать виртуальные машины

В статье не увидел ни слова, ни примера про виртуальные машины....

Окей . Фокус был на синтаксисе сценариев , видимо строило упомянуть, что созданный сценарий запускается по ssh на виртуальной машине или физическом хосте:

sparrowdo —host bla-bla-bla

Или на группе хостов

sparrowdo —host hosts.raku

Подробнее тут - https://github.com/melezhik/sparrowdo?tab=readme-ov-file#usage

Не однозначно. Из того что понравилось - плагины. Остальное сильно дольше по ощущением писать чем в yml, например inventory.

Спасибо за коммент. А чем дольше ? И там и там древовидная структура . В первом случае - yaml ( который потом все равно питоном ), во втором случае сразу в нативном виде Rakulang array of maps …

Просто из-за дополнительного сахара. Использовать двоеточие и ентер сильно проще на мой взгляд. Возможно дело привычки.

Окей . Это просто один из вариантов. Raku lang позволяет более «классический» синтаксис :

port => 80
debug => True
home => “/var/apps/“

YAML я, конечно, не люблю, но он уже привычен. А вот Raku вижу первый раз и учить пусть простой, но новый синтаксис - сомнительно. Кроме того, весь многословный boilerplate для Ansible просто замечательно подсказывает AI-автокомплит. У Ansible огромная пользовательская база и любой вопрос легко гуглится. У Ansible наработана куча всяких нюансов и настроек, которые могут пригодиться в разных случаях. Ну и готовых коллекций под разный софт, железки и т.п.

Ну а по содержанию статьи - не помешали бы более сложные примеры, с циклами, условиями, переменными (особенно переопределением их на разных уровнях, что в Ansible немного мозговыносяще, но очень гибко устроено). То, что сейчас, IMHO, каких-то особых преимуществ не демонстрирует - всё то же самое не особо длиннее в привычном Ansible напишу.

Тут все просто Rakulang - это язык общего назначения . Где циклы, условия, и прочее - нативно поддерживаются . В ансибле из-за того что это не язык общего назначения а yaml - сделать что-то даже чуть сложнее базовой логики - начинается боль и сложный в поддержании код .

Ну в Ansible что-то сложнее базовой логики считается антипаттерном ("bashsible") :) Но условия в зависимости от собранных фактов, результата выполнения предыдущего таска и пр., конечно, всё равно приходится делать. Писать это на ЯП общего назначения - ну ОК, только мы так от декларативности откатываемся обратно к bash-скриптам... в общем, не очень понятно пока, надо подумать.

Чтобы перейти с Ansible - нужен какой-то вот прямо яркий пример, когда код становится лаконичнее и гибче в разы. Иначе - описанные выше плюсы более зрелого инструмента с огромной экосистемой решают.

Да. Давайте подумаю над более сложными примерами. Может потом обзор еще один сделаю. Есть варианты может у вас ?

Есть варианты может у вас ?

Могу попробовать вытянуть из своих плейбуков какой-нибудь любопытный, но при этом не особо огромный и законченный кусок. Но время надо, может быть позже вернусь с примером.

У вас есть две проблемы:

  1. Существующую кодовая база

  2. Смотреть пункт 1

Если серьезно, то не понятно какую задачу это решает, кроме того, что теперь будет псевдополный тьюринг язык… так же есть hcl и packer

Я так понимаю, это смесь Ansible и Chef? Выглядит интересно, но только язык слишком уж экзотический.

Блин

Опять эти недоямл и т.д, однодневки.

Сделайте уже простыми скриптами.

directory-delete - что обкурился человек, придумавший это? Rmdir и rm отменили что ли? Или они чем хуже?

  • Рассуждение об идемпотентности с прикладыванием вызовов bash и curl через несколько абзацев... Может быть я чего-то не понимаю?

  • Простой dsl, лаконичность и т.д. Вот этот "directory-create" выглядит лаконично пока вы создаете одну директорию, да так что вам каким-то чудом подходят её параметны (owner/mode) поумолчанию. А попробуйсте сделать например вот так:

    - name: Prepare dirs
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: "{{ item.state | d('directory') }}"
        owner: "{{ item.owner | d('root') }}"
        group: "{{ item.group | (d(item.owner | d('root')) }}"
        mode: "{{ item.mode | d('0700') }}"
      when: item.path is regex('^/opt/.*')
      with_items: "{{ dir_config }}"
      # Конечно же эта структура должна быть скрыта от юзера в role/vars
      # Тут она для примера.
      vars:
        dir_config:
          - path: /opt/tmp
            state: absent
          - path: /opt/data
            owner: dataowner
            mode: '0770'
          - path: /opt/admin
          - path: /tmp/skipped
          ...
          и т.д. повторить 10 раз
    

    Ansible тоже имеет inline format. Но возможность его использования в средне-сложных случаях делает, ВНЕЗАПНО, код вообще нечитаемым, так что на практике им никто не пользуется. Плюс (скорее минус) ко всему оно плохо ревьювится и больше генерирует конфликты в VCS. Будем записывать это в достоинства ансибла?

  • Где наша любимая функциональщина: dict2items|items2dict|selectattr|map|map('map')|product|zip|... все вот эти штуки с помощью которых я в одну строчку (важно, чтобы засунуть её в vars и больше никогда не видеть) могу перекидать какую-то большую структуру конфигурации в то что мне нужно в конкретном месте (так чтобы не хардкодить связаные куски одной и той же конфигурации в разных местах)? Писать это на баше с помощью jq и/или yq? :)

  • В чем инвентори более гибкий? Более гибкое и удобное наследование переменных? Или чего по вашему не хватало в ansible в этом ключе?

Ну поехали

config.raku

#!raku
%(
  dir-config => [
    %(
      :path</tmp/foo/bar>,
      :delete,
    ),
    %(
      :path</opt/foo/bar>,
    ),
    %(
      :path</tmp/foo/data>,
      :owner<ubuntu>,
      :mode<0770>,
    ),
    %(
      :path</opt/foo/data>,
      :owner<alex>,
      :mode<0770>,
    ),
  ],
)

sparrowfile

#!raku

for config()<dir-config><>.grep({$_<path> ~~ /^ "/tmp/foo" .* $/}) -> $i {
  $i<delete> ?? directory-delete $i<path> !! directory-create $i<path>, %(
    :mode($i<mode> || "0700"),
    :owner($i<owner> || "root"),
    :group($i<group> || "root"),
  );
}

вывод

ubuntu@sparky02b:~/projects/ansible-dirs$ sparrowdo --localhost  --color
18:47:52 :: [repository] - index updated from http://sparrowhub.io/repo/api/v1/index
[task run: task.bash - delete directory /tmp/foo/bar]
[task stdout]
18:47:55 :: directory-delete-ok
[task check]
stdout match <directory-delete-ok> True
[task run: task.bash - create directory /tmp/foo/data]
[task stdout]
18:47:58 :: directory path: /tmp/foo/data
18:47:58 :: directory owner: <ubuntu>
18:47:58 :: directory group: <root>
18:47:58 :: directory access rights: drwxrwx---
[task check]
stdout match <owner: <ubuntu>> True
stdout match <group: <root>> True

Картинку цветного вывода можно увидеть тут https://pasteboard.co/tY2oJMK1RADL.jpg

Давайте продолжим сорвенования и усложним чуть чуть - я хочу пропускать элементы из массива dir-config если соответсвующие им директории существуют на Raku это делается добавление еще одного условия в grep:

grep({$_<path> ~~ /^ "/tmp/foo" .* $/ && $_<path>.IO ~~ :d }

А вот в ансибле я прогнозирую танцы с бубнами виде register и прочих приколов YAML программирования - https://stackoverflow.com/questions/35654286/how-to-check-if-a-file-exists-in-ansible

Ну вот у вас получилось тоже самое с точки зрения лаконичности. Только больше разных спецсимволов и используется кастомный формат сериализации.

Давайте продолжим сорвенования и усложним чуть чуть
если соответсвующие им директории существуют

Кстати очень хороший пример. Прямо в точку.
В Ансибле усложнены возможности делать странные вещи. А вот эта идея, когда нужно получить стейт с хоста и потом применить к самому же хосту какой-то результат зависящий от его стейта - это именно такая странная потребность. Это не имеет отношения к декларативному подходу конфигурирования. Суть которого заявление: "хочу вот так". Но никак не: "посмотри как там сейчас и сделай так, если так или сяк." В идеально декларативном случае информация о состоянии должна двигаться из кода на контроллере к хосту, но никак ни в обе стороны.
Все эти stat, find и т.д. они имеются, но ими лучше не пользоваться. Для примера в Ансибле можно даже получить файл с хоста, отредактировать и положить назад. Но исходя и того что это инструмент для декларативного конфигурирования, это делается еще более некрасиво чем в вашем примере. На столько (slurp + base64 decode) что автоматически стараешься этого избегать. Помоему все разумно.

Это не имеет отношения к декларативному подходу конфигурирования. Суть которого заявление: "хочу вот так". Но никак не: "посмотри как там сейчас и сделай так, если так или сяк." В идеально декларативном случае информация о состоянии должна двигаться из кода на контроллере к хосту, но никак ни в обе стороны.

...а потом выясняется, что реализовать "хочу так" можно только узнав "как там сейчас". Вот самый ужасный кусок "bashsible", который у меня выполняет простую задачу - включить Ubuntu Pro, привязать к аккаунту, заданному в переменной для данного хоста (угу, экономлю и раскидываю по 5 машин, полагающихся бесплатно, на разные аккаунты :) и включить LivePatch:

Слабонервным не смотреть
- name: Enable Ubuntu Pro
  when: ansible_distribution == 'Ubuntu'
  become: true
  block:
    - name: Get current Pro status # noqa: command-instead-of-shell
      ansible.builtin.shell:
        cmd: "pro status --format json"
      register: pro_status
      changed_when: false
      failed_when: pro_status.rc != 0
    - name: Decipher Pro status
      ansible.builtin.set_fact:
        pro_attached: "{{ pro_status.stdout | from_json | community.general.json_query('attached') }}"
        pro_account: "{{ pro_status.stdout | from_json | community.general.json_query('account.name') }}"
        pro_livepatch_enabled: "{{ (pro_status.stdout | from_json | community.general.json_query('services[?name==`livepatch`].status') | first) | default('disabled') == 'enabled' }}"
      changed_when: false
    - name: Detach Ubuntu Pro if attached to another account
      when: pro_attached == "true" and pro_account != ubuntu_pro_account
      ansible.builtin.shell:
        cmd: "set -o pipefail && echo y | pro detach"
        register: detach_result
      changed_when: detach_result.rc == 0
      failed_when: detach_result !=0
    - name: Attach Ubuntu Pro
      when: pro_attached == "false" or pro_account != ubuntu_pro_account
      ansible.builtin.command:
        cmd: "pro attach {{ ubuntu_pro_token }}"
      register: common_pro_attached_success
      changed_when: common_pro_attached_success.rc != 2
      failed_when: common_pro_attached_success.rc != 0 and common_pro_attached_success.rc != 2
    - name: Enable Pro Livepatch
      when: not pro_livepatch_enabled
      ansible.builtin.command:
        cmd: "pro enable livepatch"
      register: pro_livepatch_enabled_success
      changed_when: pro_livepatch_enabled_success.rc != 2
      failed_when: false # Sometimes it returns 'already enabled' error though it's not true

Вот в таком случае у автора с его Raku явно получится изящнее, да. Другой вопрос, что такие случаи ну это никак не норма и на них ориентироваться не стóит.

Ok. Посмотрел . Чуть позже покажу как эту будет в Sparrowdo

Да не сто́ит :) Тут понятно, что и на баше будет короче и яснее, и на любом другом ЯП. Это просто вырожденный такой пример, когда записать конфиг этого самого Про невозможно (непонятно где он вообще и в каком формате) и приходится извращаться. Но это точно не тот пример, который сто́ит повторять.

вобщем вот как это будет на Sparrowdo (там в одном месте я нашел у вас некую логическую багу - смотрите комменты в коде, но остальное все соответствует оригинальной логике )

tasks/pro-status/task.bash

set -e
pro status --format json > $cache_root_dir/state.json

files/tasks/pro-detach/task.bash

set -e
set -o pipefail
echo y | pro detach

files/tasks/pro-attach/task.bash

pro attach $(config token)

rc=$?

# возможно следующая строчка не нужна
# так как нас интересует только статус равный нулю

echo "{ \"rc\":  $rc }" > $cache_root_dir/state.json 

files/tasks/enanle-live-patch/task.bash

pro enable livepatch

rc=$?

echo "{ \"rc\":  $rc }" > $cache_root_dir/state.json 

sparrowfile

#!raku

my %s = task-run "files/tasks/pro-status";

if %s<attached> && %s<account><name> ne config()<ubuntu_pro_account> {
   task-run "files/tasks/pro-detach";
}

if %s<attached> && %s<account><name> ne config()<ubuntu_pro_account> {
   my %s = task-run "files/tasks/pro-attach", %( 
     :token(config()<ubuntu_pro_token>)
   );
   die "failed to ubuntu_pro_token" unless %s<rc> == 0
}

if ! %s<services>.grep($_<name> eq "livepatch" && $_<status>) {
   my %s = task-run "files/tasks/enanle-live-patch";
   say "enanle-live-patch status: ", %s<rc>;
}

Вобщем все как видите в этом решении изящно бьется на функции, которые потом просто вызываются в основном сценарии - с логикой ветвления и прочим написанной на чистом Raku - все красиво и хорошо поддерживается в случае необходимости

Если есть необходимость все files/tasks/*/task.bash могут быть превращены в плагины и использоваться в других сценариях - в этом фишка Sparrowdo

Спасибо за затраченное время, но как я уже выше написал, этот пример не особо убеждает в преимуществах Sparrowdo. Ну да, то, что в принципе делать не надо, с Вашим инструментом делать проще :) Ну так на ансибле в таких вырожденных случаях можно написать скрипт хоть на баше, закинуть его на хост и выполнить там, если уж на то пошлó. Но в светлом будущем, когда Canonical формат конфига своего Pro стабилизирует хотя бы, я просто буду кидать готовый конфиг из шаблона и хэндлером перезапускать службу, как в остальных 99% подобных задач.

Действительно интересный пример Вы выше с @PetyaUmniy обсудили, стало сильно понятнее. Я думал для разбора сделать выжимку из своего плейбука, который на нескольких хостах VPN туннели настраивает в режиме mesh (все со всеми), но пожалуй не стóит, на том примере и так всё понятно.

понял, кстати для сценариев настройки на группе хостов, где одни хосты от других зависят как-то сложно или там нужно соблюсти порядок обновления нод или там retries делать - можете использовать Sparrowdo в связке со Sparky - там в общем случае такие задачи очень хорошо решаются - https://habr.com/ru/articles/886660/

Ps извините если резок, или за ошибки в тексте, просто выскажу свою глупую мысль, можете игнорировать ее.

Я не программист и не девопс, я админ. Yaml только познаю. И ансибл для меня перегружен сущностями, которые делают намного больше чем от них нужно. Вы вот расписали простыню целую, которую при обширных настройках фиг прочитаешь, уж не говоря, что нужно выискивать почему где-то что-то не так. В реальности на баше это две, ну ладно три строчки, а через ; вся одна!

Ваш инструмент для Аля Яндекса, а такие как я ожидают упрощения.

Учить новый синтаксис, в компании где 2-4 человека в отделе.....

У вас нет лаконичности, у вас все перегружено, не читаемо и фиг напишешь, это касается всего yaml-подобного.

И ансибл для меня перегружен сущностями

Это на первых порах кажется. Рекомендую оф. документацию вдумчиво прочитать, там очень много про concepts и best practices. Да даже и тут на Хабре по Ансиблу было несколько очень полезных статей.

Учить новый синтаксис, в компании где 2-4 человека в отделе.....

Да ладно. Я вообще владелец пары маленьких бизнесов, и всю серверную часть (а селф-хостить предпочитаю вообще все, включая почту, файловое облако и пр.), администрирую сам, поскольку у меня такое хобби (даже не профессия по образованию). И то Ansible мне конкретно облегчил жизнь. А если когда-нибудь найму человека с этим всем помогать - так и подавно, потому как плейбук - это лучшая документация на тему "как там всё устроено".

фиг напишешь, это касается всего yaml-подобного.

Рекомендую или подключить GitHub Copilot в VS Code, или поставить Cursor. Ансибловский YAML и правда многословен и руками всё писать утомительно, но вот AI-автокомплит целые типовые блоки подсказывает просто божественно, ускоряет работу в разы.

Вы наверное не yaml изучаете, а Ansible dsl. В yaml нет предмета для изучения. Выучили отступы, list и map = знаете yaml. Да и к тому же, если вам вдруг не нравится писать Ansible на yaml? - пишите на json, который будучи подмножеством yaml будет так же работать. :)
Когда есть опыт использования Ansible, то он читается и правится гораздо проще баша. И внезапно как раз из-за своей ограничености. В Ansible (в идеальном случае) каждая task - вещь в себе, которая выполняется не породив никакой контекст и не используя контекст предыдущих тасок. Если нужно что-то отредактировать - берешь и редактируешь в конкретном месте не вникая в происходящее до и после. Если какие-то таски все же зависят от других, то существует очень ограниченное число способов как это реализуется, через зарезервированные слова вроде: set_fact, register, notify (лучше не пользоваться никакими из них). Т.е. достаточно проверить их наличие и можно смело править в моменте. В bash же горы глобальных переменных (на мой взгдят только процентов 10 людей использую директиву local), постоянно выскакивают из ниоткуда функции, циклы и условия высокой вложености в пол экрана, source этой "так важной" библиотеки по раскрашиванию текста и не только. Для того чтобы в общем случае поправить баш скрипт - с ним нужно разобраться.

notify (лучше не пользоваться никакими из них)

А как Вы сервисам рестарт/релоад делаете после изменения конфига без notify и хэндлеров, можно поинтересоваться?..

Сразу на месте изменений по register.changed. Так имхо меньше шансов получить неожиданное поведение. Если это релоад, то нет никаких проблем делать его сколько угодно раз. Если это рестарт, то это в любом случае подразумевает вывод хоста в maintenance, т.е. тоже можно рестартить.

Логику понял, но , пожалуй, продолжу с хэндлерами, плейбуки и так зело многословны. Какой-никакой уровень DRY надо же оставить :)

У меня ещё и зависимости между ролями, так что некоторые общие хэндлеры типа перезагрузки правил nftables (там свои приколы с докером и иже с ним, которые в фаервол лезут беспардонно, так что одним таском не ограничишься) вынесены в отдельную роль, которая через meta/dependencies подключается везде, где это актуально.

Может, не самый bulletproof подход, но с моими масштабами пока получается не путаться :) Ну и мне проще, для моих микробизнесовых задач простой на пару часов в любое время суток, даже если какой-то косяк и допущу, совершенно приемлем.

В bash же горы глобальных переменных

В ансибле тоже все на глобальных переменных построено - это дефолтные/vars переменные роли если в каком-то месте переменная переопределяется, можно, того не ведая, поменять поведение роли, вызываемой в куче других мест в коде, вы конечно скажите что так делать не надо , и я соглашусь - но беда в том что ансибл что называется by design не предотвращает такие проблемы ( в отличие от обычных яп - где есть инкапсуляция , интерфейсы и scopes ) - и я часто видело подобное в больших проектах - где такое практиковалось и дебажить такие вещи была огромная боль

В Ansible (в идеальном случае) каждая task - вещь в себе,

Само по себе это не является какой-то ценностью, а просто следствием того что ансибл - просто DSL , а не полноценный язык разработки где функции могу возвращать параметры . И вы можете сказать что - это как раз дает очень простой в поддержке код - но:

  • это даёт очень странный и сложный в поддержке код когда вам все же порой приходится сталкиваться с ситуациями когда таски должны что-то ввернуть и вы должны это как от обработать и дальше в логике своего основного сценария, у тогда в ансибле у вас появляются весьма неуклюжие конструкции типа set facts , notifiers , registers, множественные when и прочее.

На практике в более-менее крупных ансибл проектах такие ситуации встречаются часто , даже если вы из-за всех сил стремитесь применять декларативный подход, этого не неизбежать

  • фреймворки построенные на обычных современных ЯП, как например sparrowdo, также позволяют и поощряют писать в стиле - списка задач никак не зависящих друг от друга ( как в ансибле ) -это еще раз не прерагатива ансибл и в этом случае Bash - неудачный пример для сравнения - потому что он тут плохо подходит, просто потому что его сложно использовать в декларативном стиле - а более современные языки можно

Примеры с bash ну прям по рукам бы дал. Как и писали выше какие задачи решает против ансибла? Ансибл не люблю :) но что делать.

Зачем изобретать велосипед? Зачем простой и понятный ямл заменять целым чертовым ЯП? Просто потому что модно? У них разные задачи.

В YAML есть сниппеты. В зависимости от диалекта, их синтаксис различается, но они есть. Их можно применять, причём в мутабельном стиле - можно импортировать сниппет на любом уровне структуры и переопределить часть его полей.

Тут такое есть? Если честно, поверхностный поиск не дал ответа...

В sparrowdo есть плагины - обычные библиотеки - которые вы используете в сценарии , плагин-функция с входными параметрами , есть дефолтные значения , которые можно переопределять . Вот репозиторий публичных sparrowdo плагинов - https://sparrowhub.io/

Вы можете создавать свои плагины и загружать их в свои репозитории

Плагины пишутся на одном из языков на выбор - Bash, Raku, Python, Go и так далее

На что только не идут люди лишь бы не разобраться с nix (

nix, nixpks, nixos, nixops4 - все обмазано flakes

Sign up to leave a comment.

Articles