Если много работаешь с хостами через Ansible, быстро надоедает печатать их имена целиком или открывать инвентарь, чтобы скопировать имя. Мне очень хотелось получать список доступных хостов через нажатие Tab.
🖐️Эй!
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.
Также в блоге теперь доступно соавторство 🐧🐧🐧.
Ниже инструкция как настроить автоподстановку хостов из Ansible inventory с помощью клавиши Tab при использовании команд ssh, scp, rsync, ansible и ansible-playbook.
Вводные данные
Условно есть ~/Ansible/inventory.yml:
all:
children:
test:
children:
test_hosts_1:
hosts:
test-1.r4ven.me:
ansible_host: 192.168.122.140
test-2.r4ven.me:
ansible_host: 192.168.122.141
ansible_port: 22
test-3.r4ven.me:
ansible_host: 192.168.122.142
ansible_port: 22
test_hosts_2:
hosts:
test-4.r4ven.me:
ansible_host: 192.168.122.143
test-5.r4ven.me:
ansible_host: 192.168.122.144
ansible_port: 22
test-6.r4ven.me:
ansible_host: 192.168.122.145
ansible_port: 22И ~/.ssh/config
Host orangepi
HostName 10.10.10.12
User orange
Port 22
IdentityFile ~/.ssh/id_ed25519
Compression yes
Host router
HostName 10.10.10.13
User root
Port 22
IdentityFile ~/.ssh/id_ed25519
Compression yes
Host proxmox
HostName 10.10.10.15
User root
Port 22
IdentityFile ~/.ssh/id_ed25519Gif-демо
Посмотрите короткое gif-демо, чтобы наглядно понять о чем речь:

Сниппеты для Zsh и Bash
Если вы используете замечательный ZSH, то открываем .zshrc с помощью vim и добавляем в него следующий сниппет:
vim ~/.zshrc_ansible_inventory_hosts() {
local inv="${ANSIBLE_INVENTORY:-$HOME/Ansible/inventory.yml}"
[[ -r "$inv" ]] || return 0
grep -oP '^\s*\K[^:#\s]+(?=:\s*$)' "$inv" 2>/dev/null \
| grep -vxE 'all|hosts|children|vars|ansible_.*' | sort -u
}
if [[ -r "${ANSIBLE_INVENTORY:-$HOME/Ansible/inventory.yml}" ]]; then
zstyle -e ':completion:*:(ssh|scp|sftp|rsync):*' hosts 'reply=(
${(f)"$(_ansible_inventory_hosts)"}
${=${${${(M)${(f)"$(cat /dev/null ~/.ssh/config(N))"}:#(#i)Host *}#(#i)Host }:#*[*?]*}}
${(@)${=${${(M)${(f)"$(cat /dev/null {/etc/ssh/ssh_,$HOME/.ssh/}known_hosts(N))"}:#[^|#]*}%%[[:space:]]*}//,/ }}
)'
_ansible_hosts_completion() {
local -a hosts
if [[ $words[CURRENT-1] == (-l|--limit) ]] ||
[[ $service == ansible && $CURRENT -eq 2 && $words[CURRENT] != -* ]]; then
hosts=( ${(f)"$(_ansible_inventory_hosts)"} )
if (( ${#hosts} )); then
_describe 'ansible hosts' hosts
return
fi
fi
_default
}
(( $+functions[compdef] )) && compdef _ansible_hosts_completion ansible ansible-playbook
fiЕсли ваша оболочка не менее прекрасный Bash, то редактируем .bashrc:
vim ~/.bashrc_ansible_inventory_hosts() {
local inv="${ANSIBLE_INVENTORY:-$HOME/Ansible/inventory.yml}"
[[ -r "$inv" ]] || return 0
grep -oP '^\s*\K[^:#\s]+(?=:\s*$)' "$inv" 2> /dev/null \
| grep -vxE 'all|hosts|children|vars|ansible_.*' | sort -u
}
if [[ -r "${ANSIBLE_INVENTORY:-$HOME/Ansible/inventory.yml}" ]]; then
declare -A _ansible_orig_comp
_ansible_hosts_augmented() {
local fn=${_ansible_orig_comp[$1]}
if [[ -n $fn ]] && declare -F "$fn" &>/dev/null; then
"$fn" "$@"
fi
local cur=${COMP_WORDS[COMP_CWORD]}
if [[ $cur != -* ]]; then
local hosts
hosts=$(_ansible_inventory_hosts)
[[ -n $hosts ]] && COMPREPLY+=( $(compgen -W "$hosts" -- "$cur") )
fi
}
_ansible_augment_hosts_completion() {
local cmd spec fn
for cmd in "$@"; do
if ! complete -p "$cmd" &>/dev/null; then
if declare -F _comp_load &>/dev/null; then
_comp_load "$cmd" 2>/dev/null
elif declare -F _completion_loader &>/dev/null; then
_completion_loader "$cmd" 2>/dev/null
fi
fi
fn=""
spec=$(complete -p "$cmd" 2>/dev/null) || spec=""
if [[ $spec =~ \ -F\ ([^\ ]+) ]]; then
fn=${BASH_REMATCH[1]}
fi
_ansible_orig_comp[$cmd]=$fn
complete -o default -F _ansible_hosts_augmented "$cmd"
done
}
_ansible_augment_hosts_completion ssh scp sftp rsync
_ansible_cli_completion() {
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
COMPREPLY=()
if [[ $prev == -l || $prev == --limit ]] ||
[[ $1 == ansible && $COMP_CWORD -eq 1 && $cur != -* ]]; then
COMPREPLY=( $(compgen -W "$(_ansible_inventory_hosts)" -- "$cur") )
fi
}
complete -o default -F _ansible_cli_completion ansible ansible-playbook
fiОбратите внимание, что важно корректно указать путь до вашего файла инвентаря. По умолчанию сниппет ищет inventory по пути $HOME/Ansible/inventory.yml. Если путь другой, то можете переопределить его в самом сниппете, либо через переменную ANSIBLE_INVENTORY:
export ANSIBLE_INVENTORY=/path/to/your/inventory.ymlПрименение изменений в текущей сессии
В случае Zsh:
exec zshВ случае Bash:
exec bashCompletion загрузится автоматически при первом Tab.
Проверка работы
ssh <TAB><TAB>
scp <TAB><TAB>
rsync -e ssh ivan@<TAB><TAB>
ansible <TAB><TAB>
ansible-playbook -l <TAB><TAB>Должно вывести список всех хостов из inventory плюс те, что в ~/.ssh/config и known_hosts. Если ничего не появляется - проверьте, что файл inventory читаемый и что там действительно есть хосты в формате YAML.
На практике использование данного сниппета сэкономит кучу времени, особенно если хостов много и названия длинные 🤯.
Почему код для bash раза в три длиннее zsh?
Описание от нейронки
Разница в размерах фактически не про язык, а про архитектуру систем автодополнения в двух оболочках.
В zsh-compsys изначально заложена точка расширения. Штатные комплитеры _ssh/_scp/_rsync не содержат список хостов в виде констант - они спрашивают его у общей функции _hosts, которая в свою очередь читает конфигурацию через zstyle. Результат: чтобы влить свои хосты, нужен буквально один вызов zstyle -e, без загрузки чужих функций и без оборачивания.
В bash-completion всё устроено проще и скромнее. Система соглашений: одна функция на команду, регистрация через complete -F, и готово. Никаких слоёв конфигурации, никакого общего хранилища хостов. Поэтому чтобы дополнить, не перезаписав родную регистрацию, нужно:
- Сначала загрузить оригинальный комплитер. bash-completion ленивый - регистрирует комплитеры только при первом Tab. Придётся явно вызвать
_comp_load(новые версии) или_completion_loader(старые), потом распарсить выводcomplete -p, чтобы вытащить имя функции. - Сохранить оригинал. Если просто вызовешь
complete -F новая_функция ssh, затрёшь старую регистрацию. Нужен ассоциативный массив, чтобы помнить имя оригинальной функции и вызывать её сначала. - Склеить результаты. Пропустить через оригинальную функцию, потом дописать свои хосты в
COMPREPLYчерезcompgen.
Плюс есть второй фактор - плотность zsh как языка. Однострочные раскрытия параметров вроде ${(f)...} и ${(M)...:#...} заменяют целые циклы с grep и sed. В bash пришлось бы это всё расписать явно, но смысла нет - _ssh из bash-completion это уже умеет, зачем дублировать.
Честно говоря, zsh-код «маленький» ещё и потому, что сложность просто живёт в другом месте. Функции _hosts и весь compsys - это тысячи строк фреймворка, поддерживаемого апстримом. В bash аналогичной инфраструктуры нет, поэтому часть сантехники оказалась в конфиге. Типичный trade-off: богатый фреймворк даёт короткие расширения, тонкое соглашение требует DIY-обвязки у каждого.
👨💻Ну и…
Не забывайте про нашу телегу📱и чат 💬
Или может хотите стать соавтором? Тогда клик сюда🔗
Всех благ✌️
That should be it. If not, check the logs 🙂


