Так получилось что в рамках моей основной деятельности пришла пора сделать сервис для манипуляции с ресурсами СХД для виртуальных машин (ВМ). Они подаются в SAN в виде "LUN" ("Logical Unit Number"). Пока речь шла о десятках .. первых сотнях LUN, хватало моего старого решения (оно изначально про телефонию и блок-схемы, но на самом деле всё равно подо что делать очередной модуль). А потом он рос, рос, и…

В общем, взял я в руки python.

(Как удобный для прототипирования/"склейки" инструмент. И при этом в моём отделе примерно все могут, как минимум, прочитать написанное).
И наваял обёртку "lun" и пачку модулей в соседней папочке "lun.d". (Догадываюсь что на питоне принято раскладывать в другое место, но здесь — сервис местного значения.)

И вот теперь — главное. Эргономика рабочего места.

Структура вызова запроектирована в CLI таким образом (откидывая ненужные подробности):

# lun edit dc 60:0a:09:80:00:01:02:03:00:00:02:7d:5a:3a:e3:b9 [--extra-keys]
# lun attach dc machine-name --lun=lun_name

Ровно один "фактор", и он не должен выламывать пальцы инженеру ТП!
WWN ("60:0a:09:80:00:01:02:03:00:00:02:7d:5a:3a:e3:b9") копипастится с консольки СХД или из задачи. Остальное должно заезжать "магически", примерно всё "интересное".

К делу

Чтобы не плеваться от UI, нужно автодополнять:

  • Имя модуля (список модулей покажет ls в подкаталоге).

  • Аббревиатуру датацентра ("dc" в примере выше, спросить можно "lun get list-dc").

  • Третий позиционный аргумент (больше низзя!), или "--ключ".

  • Прочие "--ключи", по необходимости.

И в этом месте начался реальный трэш. По итогам которого я дал себе обещание написать "хавтушку" для других желающих "как лучше".

  • Хабро-перевод я просмотрел одним из первых. Но, "фактуры маловато".

  • Что можно найти в "man bash", читатель, я полагаю, в курсе. Я не настолько фанат bash (но в этот раз таки немного пришлось и там поползать).

  • Подход к подобному классу задач обычно состоит в том чтобы взять чей-нибудь код для затравки. И порыться в интернетах на предмет приличной статьи на тему. И тут — нате вам. Гугл принялся выдавать репосты одного и того же chatGPTбреда про автодополнение из bash_history. А подручные "дополнялки" (модули bash-completion от используемого софта) демонстрируют откровенно слабое владение мат.частью.

  • В какой-то момент повезло, наконец, наткнуться на devmanual.gentoo.org. Сразу дам ссылки: Ключевые понятия, Подробности про "compgen", Подробности про "complete". Все 3 статьи достаточно компактные, но позволяют начать разбираться в вопросе.

Дальше показываю "как", на своём примере.

"Рыба":

_lun() {
  local cur="$2"
  local prev="$3"
  local obj cmd base keys key val
  local LIST=""
  local WWID=""
  local LUN=""
  local cmd="${COMP_WORDS[1]}"
  local DC="${COMP_WORDS[2]}"
…
} && complete -F _lun lun
  • complete -F вызывает функцию _lun(), когда дополняет команду lun.

  • Для простых манипуляций достаточно трёх аргументов вызываемой функции (подробнее). Какую команду дополняем ($1), что дополняем ($2) и что было перед этим ($3).

  • Ключевое слово local призвано подстраховать переменные внутри функции от "протекания" наружу. Вообще, всё автодополнение в bash свалено в одну кучу, и безграмотными действиями легко сломать работу чужого кода.

  • Для более сложных манипуляций доступны массив COMP_WORDS[] и указатель на его последний элемент COMP_CWORD. Выше видно, как я достаю из него пару "позиционных" аргументов.

  • Для "высшего пилотажа" оставлен доступ к COMP_LINE и COMP_POINT (вся дополняемая строка и текущее положение курсора).

Первый аргумент:

  if [ "${COMP_CWORD}" = "1" ]
  then
    # first level -> base objects
    base="/usr/local/sbin/lun.d"
    obj=$(cd ${base} && ls -1 *.py | cut -f 1 -d "." | sort -u)
    COMPREPLY=( $(compgen -W "help ${obj}" -- "${cur}") )
  • Башисты в этом месте должны стукнуть мне по рукам за cd. Потому что есть пара pushd/popd.

  • Берём список всех модулей *.py, добавляем ещё одно ключевое слово help (вывести хинт к обёртке lun) и формируем из этого набор "слов" для compgen -W. В COMPREPLY возвращается bash-массив (то что внутри круглых скобок "( … )").

  • В конце вызова compgen нужно обязательно ставить дополняемое (${cur}). "--"подсказывают GNU'тым утилитам, что дальше -ключей не будет.

Второй аргумент:

  elif [ "${COMP_CWORD}" = "2" ]
  then
    # second level -> commands
    DC=$(sudo lun get list-dc)
    if [ "${cmd}" = "get" ]; then
      COMPREPLY=( $(compgen -W "help ${DC} list-dc" -- "${cur}") )
    else
      COMPREPLY=( $(compgen -W "help ${DC}" -- "${cur}") )
    fi
  • Применение sudo здесь реально важно. Пользователь имеет доступ к неким привилегированным командам, но не sudo -i для всех подряд же!

  • Вызов sudo lun поломает работу автодополнения. Потому что дополнение будет для sudo, а мнение писавших sudo не всегда совпадает с моим. Чтобы у пользователя всё работало, нужен альяс (где-нибудь в ~/.bashrc): alias lun="sudo lun"

Третий аргумент:

  elif [ "${COMP_CWORD}" = "3" ]
  then
    # third level -> keys
    keys=`sudo lun ${cmd} args`
    if [[ "${cmd}" =~ get|add|edit ]]; then
      if [ "${DC}" = "list-dc" ]; then
        return
      fi
      WWID=$(sudo lun get ${DC} --fields=wwid --compact | cut -d ']' -f 1 | cut -d ' ' -f 2 | tail -n 5)
      COMPREPLY=( $(compgen -W "${keys} ${WWID}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') )
    elif [[ "${cmd}" == vm || "${cmd}" == attach || "${cmd}" == resize ]]; then
#      compopt -o nospace
      LIST=`sudo lun vm ${DC} list "${cur}" --cached`
      COMPREPLY=( $(compgen -W "${LIST}" -- "${cur}" ) )
    else
      COMPREPLY=( $(compgen -W "${keys}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') )
    fi
  • Про nospace будет ниже.

  • Каждый модуль обучен выводить список принимаемых аргументов по ключу args. Его и показываем, в общем случае.

  • list-dc вместо аббревиатуры датацентра выводит список известных ДЦ. Нечего дальше дополнять, return.

  • Для команд get|add|edit дополнительно показываем список из пяти последних добавленных LUN. Не супер удачное решение, т.к. в процессе вытаскивает листинг всех LUN в ДЦ. Правильнее было бы не-юниксвейно протащить ограничение в lun get.

  • Вот этот фокус с кавычками для WWN (кому не ясно что тут написано, гуглит "bash escape quotes"): {print "'''"$0"'''"}. Он для того чтобы автодополнялось разделённое ":". Т.к. ":" входит в список разделителей слов по умолчанию. Глубже копать в этом месте я поленился.

  • Для команд vm, attach, resize дополняем имя ВМ из закэшированного в локальной БД списка. Выше сравнение было через "=~", а здесь вот так. Просто потому что сначала команда была одна.

  • Автодополнять ну очень желательно откуда-то из "быстрого" кэша. Не уподобляйтесь писателям yum/dnf и иже с ним. Долгие запросы через ssh, так и вовсе отваливаются по таймауту. Я не нашёл этого места в bash-completion, но не сильно и старался.

  • Для всех прочих команд, выводим только список ключей.

Остальные аргументы:

  else
    # other level -> options
    if [ "${COMP_WORDS[COMP_CWORD]}" = "=" ]; then
      key=$((COMP_CWORD - 1))
    elif [ "${COMP_WORDS[COMP_CWORD-1]}" = "=" ]; then
      key=$((COMP_CWORD - 2))
    fi

Для не-булевых аргументов, ключи вида --key=value, без пробелов. С пробелами я ниасилил. Будем считать это "домашним заданием", для лучшего овладения материалом.

Поехали дополнять:

    if [ ! -z "${key}" ]; then
      if [ "${COMP_WORDS[$key]}" = "--fields" ]; then
        val="alias host vm scsi blkdeviotune ,"
        local list=$(echo "${cur}" | egrep -o '([a-z]+,)+')
        cur="${cur/[[:alpha:]]*,/}"
        COMPREPLY=( "${list}"$(compgen -W "${val}" -- "${cur}") )
        compopt -o nospace
      elif [ "${COMP_WORDS[$key]}" = "--file" ]; then # [ "${cmd}" = "edit" ]
        compopt -o filenames
        COMPREPLY=( $(compgen -f -- "${cur}") )
      elif [ "${COMP_WORDS[$key]}" = "--vm" ]; then # [ "${cmd}" = "get" ]
        LUN=`sudo lun vm ${DC} list --cached`
        COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) )
      elif [ "${COMP_WORDS[$key]}" = "--lun" ]; then # [ "${cmd}" = "attach"|"resize" ]
        local vm="${COMP_WORDS[3]}"
        LUN=`sudo lun vm ${DC} ${vm} --cached --luns --json | jq -r ".luns[]"`
        COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) )
      else
        val="<`echo ${COMP_WORDS[$key]} | tr -d '=-'`>"
        COMPREPLY=( $(compgen -W "${val}" -- "${cur}") )
        compopt -o nospace
      fi
    else
      keys=`sudo lun "${COMP_WORDS[1]}" args`
      COMPREPLY=( $(compgen -W "${keys}" -- "${cur}") )
    fi
  • Если ключ не начали вводить, показываем список ключей для модуля (нижний блок else). Правильно было бы исключить уже́ задействованные ключи из списка. Внутри — питоновский "argparse", он тупо возьмёт последний попавшийся.

  • Ключ --fields принимает на вход список полей через ",". Такое решение опять-таки поперёк дефолтных настроек разделителей для libreadline, поэтому дальше — фокус: cur="${cur/[[:alpha:]]*,/}". Срезаем всё до последней "запятой". Я вообще в баш-портянках стараюсь ограничиваться средствами баша. Потому что самому потом грозит (в случае чего) разматывать логи аудита.

  • В этом месте также следовало бы исключать все ранее перечисленные через "," поля из автодополнения.

  • Для --file нужно автодополнять имена файлов из текущего каталога. Для этого заказываем compopt -o filenames, иначе не даст "проваливаться" в подкаталоги. Где-то написано что это можно вставлять прямо в вызов compgen -f. Но так (у меня, bash 4.4.20 из Oracle Linux 8) не работает.

  • Для --lun берём список LUN, относящихся к указанной ВМ. Запрос jq -r ".luns[]" достаёт значения (имена LUN) из отданного в json словаря. JSON и "jq" — вообще довольно удобно при парсинге отдаваемого в CLI. Для тех утилит, которые умеют в JSON.

  • Всё остальное (после else) — не знаем как дополнять. По "табулятору" выводим название ключа в угловых скобках (--key=<key>).

Чтобы автодополнялось после "=", просим не добавлять пробел:

  if [[ "${COMPREPLY[@]}" =~ =$ ]]; then
    # Add space, if there is not a '=' in suggestions
    compopt -o nospace
  fi

Всё. Как смог, рассказал. Удачи в улучшении UI/UX для инструментов командной строки!

  • Это мой первый опыт написания (а не подачи в виде лекции) обучающего материала для взрослых тётей и дядей. Буду крайне признателен за конструктивную критику. И, по мере поступления таковой, постараюсь доработать эту статью для улучшения читаемости.

Комментарии (2)


  1. pvzh
    18.04.2024 20:33

    Не очень понятен сценарий использования инженером ТП. Возможно, проще было бы оформить в виде мастера, когда все варианты и подстановки делаются уже после запуска, внутри баш-скрипта, а не на его входе, в оболочке. Типа того, как работают git init или npm init.


  1. PnDx Автор
    18.04.2024 20:33

    Задачу манипуляции с LUN успешно решают примерно все эксплуатанты ДЦ. Нюансы, традиционно, в контроле сложности. Кому-то хватает коммерческих вариантов, типа vSAN от vmWare. Кто-то сразу кидается делать GUI.
    В моём сценарии, абстракции добавляются по мере необходимости. Потому что "внутренние" задачи являются расходной частью бизнеса, ресурсы на них выделяются соответственно.

    • Есть "физический" слой, linux+multipath+qemu(libvirt). На первых порах его хватает, раскидать конфиги можно и каким-нибудь ansible/pdsh. И там же тяп-ляп пересканировать SAN.

    • Когда понятно что "всё", есть смысл обернуть конфигурацию каким-нибудь DSL. И организовать транспорт поприличнее, с контролем compliance. Это кардинально снизит "ошибки оператора" и позволит дотянуть до следующего "порога" (если бизнес не развалится).

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

    А вот вместо того чтобы тратить время на написание кастомного шелла, я его потратил на вкручивание json-интерфейсов ко всем модулям. На па́ру с обеспечением функциональности автодополнения/автозаполнения везде где это возможно.
    Когда/если встанет задача обернуть всё в GUI, программисту останется прокинуть сдизайненные (вот тут боюсь что им же) "окошки" к готовым и документированным интерфейсам.

    Но это всё лирика. Системотехнике есть и без меня кому учить. Я просто иногда стараюсь показать интересные (ПММ) случаи.
    А в данном случае интересно как раз практическое применение bash-completion. Не на "всю катушку", но около того. При наличии как позиционных, так и "свободных" аргументов, включая "--ключ=значение".

    • Побуждения тут вполне корыстные. Когда очередной коллега попытается отпетлять от организации подсказок в CLI с аргументацией "отвяжись, не умею я в bash-completion", теперь можно ткнуть в ссылку. "Ну вот же «рыба», с разъяснениями."