Возникла передо мной задача — автоматически выполнять действие при вставке какого-нибудь storage device в Дебиане. К примеру, просто автоматически монтировать его. А может, монтировать и синхронизировать данные, если устройство известно. А может, проверять его clamav на всякую фигню и запускать на нём что-нибудь типа USB-вакцины. Может, и включить сирену, если владельца рядом с компьютером нет =)
К примеру, есть у меня флешки, штук этак 5-7. На каждой записано что-то своё, одна — загрузочная, одна с документами, одна с программами, одна с музыкой и с фотками… И стоит дома сервер, на котором все-все копии того, что на флешках, есть. Хотелось бы мне, чтобы с одной флешкой синхронизировались документы, с другой — музыка и фотки, с третьей — загрузочные образы, и так далее. Только вот нужно это дело как-то автоматически запускать, потому как не годится каждый раз через SSH на сервер лезть и ручками всё править. Поэтому нужно что-то, где можно было бы флешки прописать и действия, нужные при подключении, задать. А там один раз скрипт синхронизации написал — и готово.
Только вот ничего готового и полностью подходящего в Гугле не нашлось. Часть решений тянут за собой кучу зависимостей и не рассчитаны на headless-установку, часть устарели, часть не имеют всего необходимого функционала, а часть позволяют это сделать, только вот слишком муторно, настраивать трудно, и вообще, NIH ;-) Поэтому написал я свой демон автоматического монтирования, ну и выкладываю его сюда, может, пригодится кому-то ещё, может, такому же владельцу домашнего сервера, который точно так же хочет синхронизировать флешки с домашним NAS. Да и хочется, чтобы кто-нибудь код покритиковал, на ошибки указал и решения некоторых проблем подсказал — смотреть в конце топика.
Был у меня как-то iPod. Иногда возникала необходимость его заряжать от компьютера с Линуксом. Но как только подключаешь его к компьютеру, он сразу включает режим синхронизации. Для того, чтобы он прекратил заряжаться, требуется либо хардварное решение (переходник папа-мама USB с по-особому замкнутыми контактами), либо выполнить eject /dev/sdx1, что выключает режим синхронизации и переводит в режим зарядки. Соответственно, нужно выполнять эту команду каждый раз — паять переходник ведь так лень… тогда и родился такой скрипт — umipod.sh: (UnMount-iPOD)
#!/bin/bash
#Intended to run in background and eject device specified by UUID if plugged
# in.Check is performed every N seconds where N variable is set in script
# header part.
N=10 #Yes, this is delay between checks, best recommended is 10 seconds
# which is just N=10.
UUID="4C25-4DC2"
BEACON="/usr/local/bin/umipod/a.flg"
if [[ $1 = "-a" ]]
then
touch $BEACON
echo "Mounting temporarily allowed"
exit 0
fi
if [[ $1 = "-d" ]]
then
rm -f $BEACON
echo "Device will be ejected "
exit 0
fi
while [ true ]
do
if [[ ! -e $BEACON && -e /dev/disk/by-uuid/$UUID ]]
then
DEVPART=`blkid -U $UUID`
eject $DEVPART
echo "Device ejected"
fi
sleep $N
done
Прошу сильно не плеваться на качество кода, это было написано два года назад, да и дальше в статье будет ещё много кода — берегите слюну =) Суть проста — в цикле проверяем, подключено ли устройство с конкретным UUID, если да, то находим, какое блочное устройство eject'ить и делаем это. Подключил плеер к компьютеру, через пару секунд он автоматически отключается, но продолжает заряжаться — можно дальше слушать музыку. Было достаточно удобно… Пока не потерял плеер =( С тех пор надобность в таком устройстве практически отпала.
А сейчас столкнулся с некоторыми задачами, для которых требуются автоматические действия при подключении носителя. Вот и решил это исправить, только теперь и близко не подходя к Bash =) Язык программирования долго выбирать не пришлось — да и не из чего, честно говоря, я только Python в последнее время и занимаюсь. Дальше пришлось подумать — писать ли демон, который в цикле проверяет, не появилось ли новых ещё необработанных разделов, или цепляться через udev, чтобы программа вызывалась как callback при подключении любой флешки. Второй вариант вроде потребляет меньше ресурсов — но первый не зависит от udev и проще =) Поэтому решил писать демона, которого можно спокойно сунуть в автозапуск.
Собственно, вот он. Об устройстве и возможностях легче всего будет рассказать, описывая то, как его настраивать. Но всё же вкратце опишу принцип работы:
- Демон каждые n секунд проверяет /dev/disk/by-uuid, затем по данным оттуда составляет список доступных разделов
- Этот список сравнивается со списком, сделанным за n секунд до этого, определяются разделы, которых до этого не было
- При наличии свежеподключенных разделов по записям из конфига определяется, какой раздел проигнорировать, а для какого выполнить какое-то действие.
- GOTO 1
Дальше — уже требует описания конфига =) Конфиг обычно состоит из четырёх секций, любую из которых можно безболезненно пропустить.
{
"globals": {
"interval":3,
"debug":false,
"noexecute":true,
"comment":"Everything that is in this section will be exported as a global variable in the script, replacing if there's something to replace."
},
"exceptions": [
{"uuid":"ceb62844-7cc8-4dcc-8127-105253a081fc", "comment":"System boot partition"},
{"uuid":"6d1a8448-10c2-4d42-b8f6-ee790a849228", "comment":"System root partition"},
{"uuid":"9b0bb1fc-8720-4793-ab35-8a028a475d1e", "comment":"System swap partition"}
],
"rules": [
{"uuid":"E02C8F0E2C8EDEC2", "mount":{"mountpoint":"/media/16G-DT100G2"}},
{"uuid":"7F22-AD64", "mount":{"mountpoint":"/media/16G-DT100G3"}},
{"uuid":"406C9EEE6C9EDE4A", "mount":{"mountpoint":"/media/80G-Music"}},
{"uuid":"52663FC01BD35EA4", "mount":{"mountpoint":"/media/32G-Data"}}
],
"default": {
"mount":true,
"comment":"Configuration section for the actions that are taken if drive isn't in either exception or rule list."
}
}
1) Секция «globals»
Тут всё просто — любая переменная в этой секции экспортируется в global namespace демона такой незатейливой функцией:
def export_globals():
log("Exporting globals from config file")
for variable in config["globals"].keys():
if debug:
log("Exporting variable "+variable+" from config")
globals()[variable] = config["globals"][variable]
Просто и быстро, не нужно составлять всякие списки переменных к экспорту или запихивать все переменные в словарь.
Полезные переменные:
main_mount_dir — основная директория для монтирования, когда путь для монтирования неизвестен либо указан в виде относительного пути. По умолчанию — "/media".
default_mount_option — опции при монтировании по умолчанию, когда другие не указаны. Я оставляю там «rw», хоть и не уверен, не прописано ли это уже по умолчанию в драйверах для файловых систем =)
logfile — путь к лог-файлу. Если надо поменять, лучше указывать абсолютный — чёрт его знает, куда попадут логи, если указать относительный =) Скорее всего — в .
interval — интервал между проверками на предмет появления новых разделов. Как по мне — даже интервал в одну секунду не загружает процессор сколь-либо значительно, даже на одноядерном Celeron 900MHz =)
Не столь полезные переменные:
debug — тут всё понятно. Значительно убыстряет увеличение размера лог-файла, в реальной жизни вряд ли понадобится =)
super_debug — убыстряет расширение лог-файла ещё больше. Вероятность того, что понадобится, ещё меньше.
noexecute — опция, которая включена по умолчанию и при которой демон только имитирует вызовы внешних команд — таким образом, с момента первого запуска и до тех пор, пока не будет убрана опция, ничего монтироваться/выполняться не будет. Сделана для того, чтобы до настройки демон не монтировал разделы, которые уже монтируются, используя fstab.
2) Секция «exceptions»
Любой раздел, чей UUID/Label указан в этой секции, будет жестоко проигнорирован. Собственно, основной use case — разделы, которые автоматически монтируются при загрузке системы из fstab. То есть обычные записи в этой секции будут выглядеть так:
"exceptions": [
{"uuid":"ceb62844-7cc8-4dcc-8127-105253a081fc", "comment":"System boot partition"},
{"uuid":"6d1a8448-10c2-4d42-b8f6-ee790a849228", "comment":"System root partition"},
{"uuid":"9b0bb1fc-8720-4793-ab35-8a028a475d1e", "comment":"System swap partition"}
]
Кстати — всякие там ключи типа «comment», естественно, игнорируются. Если не считать трюк с повторным использованием ключей в словаре, это единственная возможность комментировать конфиг-файл в формате JSON =)
3) Секция «rules»
Тут описываются правила для особых случаев. К примеру, здесь можно указать правило типа:
{"uuid":"E02C8F0E2C8EDEC2", "mount":{"mountpoint":"/media/16G-DT100G2", "options":"rw,uid=1002,gid=1002"}},
При наличии такого правила раздел с UUID E02C8F0E2C8EDEC2 будет всегда монтироваться по пути "/media/16G-DT100G2", используя опции «rw,uid=1002,gid=1002».
4) Секция «default»
Тут всё просто — если раздел не соответствует записям в двух предыдущих секциях, то именно эта секция отвечает за действие по умолчанию. Обычно содержит просто «mount»:true, ну или «mount»:false.
Правила
Как можно видеть, во последних трёх секциях в правилах есть два типа переменных.
Первый тип — переменные для идентификации какого-то конкретного раздела. Их пока три типа — «uuid», «label» и «label_regex».
- «uuid» — тут всё понятно.
- «label» — позволяет задать метку раздела, при совпадении которой будет принято действие.
- «label_regex» — сопоставляет метку диска с указанным регулярным выражением, используя re.match().
Второй тип — переменные для обозначения действия. Их тоже три:
- «mount» может иметь следующие значения: true (автоматически монтировать), false ( не монтировать) или словарь с дополнительными аргументами для монтирования (подразумевается true). Пока что допустимые ключи для словаря — «mountpoint» (задание точки монтирования) и «options» (дополнительные опции для монтирования).
- «command» — строка, содержащая путь к команде, которую следует выполнить. Тут всё ясно =)
- «script» — какой-нибудь кастомный скрипт, который вызывается с аргументами «DEVICE_PATH UUID MOUNTPOINT LABEL». Вместо последних двух, если нет точки монтирования или метки раздела, будет None.
Естественно, при обработке секции «exceptions» никакие действия, даже если будут указаны, приняты не будут, а в секции «default» не будут обрабатываться переменные идентификации — смысла нет =)
Добавлю пару примеров правил в конфиге:
{
"globals": {
"interval":1,
"debug":false,
"default_mount_option":"noexec,noatime,rw"
},
"exceptions": [
{"label":"Root", "comment":"System boot partition"},
{"uuid":"6d1a8448-10c2-4d42-b8f6-ee790a849228", "comment":"System root partition"},
{"uuid":"9b0bb1fc-8720-4793-ab35-8a028a475d1e", "comment":"System swap partition"}
],
"rules": [
{"label":"MULTISYSTEM", "mount":{"mountpoint":"/media/MULTISYSTEM", "comment":"Будет конфликтовать, если будет два раздела с одинаковой меткой =( "}},
{"uuid":"7F22-AD64", "mount":{"mountpoint":"/media/16G-DT100G3"}, "command":"/usr/local/bin/sync_dt100g3.sh"},
{"uuid":"406C9EEE6C9EDE4A", "mount":{"mountpoint":"/media/80G-Music"}, "command":"mocp --server; mocp -P", "comment":"Автоматическое проигрывание музыки при "},
{"label_regex":"*iPhone*", "comment":"За правильность регулярки вообще не ручаюсь", "mount":true, "script":"/usr/loca/bin/iphone_factory_restore.sh", "comment":"Учимся делать бекапы ;-) "}
],
"default": {
"mount":false,
"comment":"Ты куда монтируешься, а ну пропуск покажи"
}
}
Зачем?
В итоге — что позволяет этот демон? Кроме того, что он помогает автоматически монтировать разделы на подключаемых устройствах, он может быть использован для:
- Синхронизации файлов на съёмных носителях с, к примеру, домашним сервером на Linux
- Своеобразного usb-modeswitch, вспомнить ту же ситуацию с iPod
- Проверки носителей на вирусы
- Сливания данных с чьих-нибудь iPhone/Android/флешек/фотоаппаратов втихаря =)
- Создания Samba-шар для доступа к носителям по сети
Сейчас, когда всё написано и полностью равботает, но до конца ещё не отполировано, у меня есть несколько вопросов по улучшению безопасности и соответствию каким-нибудь там выработавшимся за многие годы традициям в написании демонов.
- Можно ли постоянно держать этот демон под рутом? Проблема в том, что нужно иметь привилегии для запуска всех команд — как монтирования, так и исполнения внешних скриптов, среди которых может быть тот же rsync, к примеру. Безопасность файла конфигурации — из той же оперы. Если скрипт запускается под рутом, а в конфиге прописана команда, которая случайно стирает MBR&MFT, будет весело.
- Для выполнения скриптов и команд, определённо, в ближайшее время нужно будет сделать обязательное выполнение в фоне, в отдельном потоке — а не то любая команда, которая будет работать дольше пары секунд, застопорит весь демон. В принципе, такая же ситуация может возникнуть при монтировании. Но тут дилемма — если выполнять монтирование в фоне, то команда или скрипт могут быть выполнены раньше монтирования, такой вот race condition =( Думаю выделить в отдельный поток обработку каждого раздела, так не будет ни race condition, ни повисания, если mount виснет из-за того, что нужно ещё пофиксить ФС.
- Нужно ли демону уходить в фоновый режим самому или это необязательно? Пока что устроено так — демон сам в фоновый режим уходить не умеет, за него это делает start-stop-daemon в init-скрипте.
- Как по пути к блочному устройству (/dev/sdxZ) проще и быстрее всего проверить, примонтировано ли оно? Желательно используя стандартные модули Python, ну, или в крайнем случае — используя внешние команды. Тогда можно будет избавиться от проблем, связанных с двукратным монтированием одного и того же раздела. Парсить mtab и сопоставлять UUID с путям к блочным устройствам — задача ещё та ;-)
- Стоит ли генерировать из fstab список исключений до первого запуска или можно положиться на пользователя, который один раз вобьёт это ручками?
- При запуске внешних скриптов нужно иметь в виду возможные спецсимволы в partition label, типа случайно попавших туда "&&rm -rf /" (сработает при запуске внешнего скрипта) и "../../etc/" (может смонтировать раздел вместо "/etc") ;-) Вопрос — какие спецсимволы нужно фильтровать для полной защиты этой дыры? На ум сразу приходят "&", "/" и ";", но может быть больше.
GitHub
Кстати, эту статью можно считать продолжением мной обещанного, но на уже почти год как остановленного курса статей по настройке своего переносного сервера, используя Debian. Собирался сделать что-то такое в рамках реализации своего файлового сервера, только вот тогда не смог найти готового решения… А сейчас написал своё =) В ближайшее время думаю написать ещё пару статей, которые подойдут под тематику этого курса.
Автор: CRImier