- PVSM.RU - https://www.pvsm.ru -

Pautomount — демон автоматического монтирования, запуска скриптов и всего такого прочего

image

Возникла передо мной задача — автоматически выполнять действие при вставке какого-нибудь storage device в Дебиане. К примеру, просто автоматически монтировать его. А может, монтировать и синхронизировать данные, если устройство известно. А может, проверять его clamav на всякую фигню и запускать на нём что-нибудь типа USB-вакцины. Может, и включить сирену, если владельца рядом с компьютером нет =)

К примеру, есть у меня флешки, штук этак 5-7. На каждой записано что-то своё, одна — загрузочная, одна с документами, одна с программами, одна с музыкой и с фотками… И стоит дома сервер, на котором все-все копии того, что на флешках, есть. Хотелось бы мне, чтобы с одной флешкой синхронизировались документы, с другой — музыка и фотки, с третьей — загрузочные образы, и так далее. Только вот нужно это дело как-то автоматически запускать, потому как не годится каждый раз через SSH на сервер лезть и ручками всё править. Поэтому нужно что-то, где можно было бы флешки прописать и действия, нужные при подключении, задать. А там один раз скрипт синхронизации написал — и готово.

Только вот ничего готового и полностью подходящего в Гугле не нашлось. Часть решений тянут за собой кучу зависимостей и не рассчитаны на headless-установку, часть устарели, часть не имеют всего необходимого функционала, а часть позволяют это сделать, только вот слишком муторно, настраивать трудно, и вообще, NIH ;-) Поэтому написал я свой демон автоматического монтирования, ну и выкладываю его сюда, может, пригодится кому-то ещё, может, такому же владельцу домашнего сервера, который точно так же хочет синхронизировать флешки с домашним NAS. Да и хочется, чтобы кто-нибудь код покритиковал, на ошибки указал и решения некоторых проблем подсказал — смотреть в конце топика.

Был у меня как-то iPod. Иногда возникала необходимость его заряжать от компьютера с Линуксом. Но как только подключаешь его к компьютеру, он сразу включает режим синхронизации. Для того, чтобы он прекратил заряжаться, требуется либо хардварное решение (переходник папа-мама USB с по-особому замкнутыми контактами), либо выполнить eject /dev/sdx1, что выключает режим синхронизации и переводит в режим зарядки. Соответственно, нужно выполнять эту команду каждый раз — паять переходник ведь так лень… тогда и родился такой скрипт — umipod.sh: (UnMount-iPOD)

umipod.sh

#!/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 и проще =) Поэтому решил писать демона, которого можно спокойно сунуть в автозапуск.

Собственно, вот он [1]. Об устройстве и возможностях легче всего будет рассказать, описывая то, как его настраивать. Но всё же вкратце опишу принцип работы:

  1. Демон каждые n секунд проверяет /dev/disk/by-uuid, затем по данным оттуда составляет список доступных разделов
  2. Этот список сравнивается со списком, сделанным за n секунд до этого, определяются разделы, которых до этого не было
  3. При наличии свежеподключенных разделов по записям из конфига определяется, какой раздел проигнорировать, а для какого выполнить какое-то действие.
  4. 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():

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":"Ты куда монтируешься, а ну пропуск покажи"
  }
}
Зачем?

В итоге — что позволяет этот демон? Кроме того, что он помогает автоматически монтировать разделы на подключаемых устройствах, он может быть использован для:

  1. Синхронизации файлов на съёмных носителях с, к примеру, домашним сервером на Linux
  2. Своеобразного usb-modeswitch, вспомнить ту же ситуацию с iPod
  3. Проверки носителей на вирусы
  4. Сливания данных с чьих-нибудь iPhone/Android/флешек/фотоаппаратов втихаря =)
  5. Создания Samba-шар для доступа к носителям по сети

Сейчас, когда всё написано и полностью равботает, но до конца ещё не отполировано, у меня есть несколько вопросов по улучшению безопасности и соответствию каким-нибудь там выработавшимся за многие годы традициям в написании демонов.

  1. Можно ли постоянно держать этот демон под рутом? Проблема в том, что нужно иметь привилегии для запуска всех команд — как монтирования, так и исполнения внешних скриптов, среди которых может быть тот же rsync, к примеру. Безопасность файла конфигурации — из той же оперы. Если скрипт запускается под рутом, а в конфиге прописана команда, которая случайно стирает MBR&MFT, будет весело.
  2. Для выполнения скриптов и команд, определённо, в ближайшее время нужно будет сделать обязательное выполнение в фоне, в отдельном потоке — а не то любая команда, которая будет работать дольше пары секунд, застопорит весь демон. В принципе, такая же ситуация может возникнуть при монтировании. Но тут дилемма — если выполнять монтирование в фоне, то команда или скрипт могут быть выполнены раньше монтирования, такой вот race condition =( Думаю выделить в отдельный поток обработку каждого раздела, так не будет ни race condition, ни повисания, если mount виснет из-за того, что нужно ещё пофиксить ФС.
  3. Нужно ли демону уходить в фоновый режим самому или это необязательно? Пока что устроено так — демон сам в фоновый режим уходить не умеет, за него это делает start-stop-daemon в init-скрипте.
  4. Как по пути к блочному устройству (/dev/sdxZ) проще и быстрее всего проверить, примонтировано ли оно? Желательно используя стандартные модули Python, ну, или в крайнем случае — используя внешние команды. Тогда можно будет избавиться от проблем, связанных с двукратным монтированием одного и того же раздела. Парсить mtab и сопоставлять UUID с путям к блочным устройствам — задача ещё та ;-)
  5. Стоит ли генерировать из fstab список исключений до первого запуска или можно положиться на пользователя, который один раз вобьёт это ручками?
  6. При запуске внешних скриптов нужно иметь в виду возможные спецсимволы в partition label, типа случайно попавших туда "&&rm -rf /" (сработает при запуске внешнего скрипта) и "../../etc/" (может смонтировать раздел вместо "/etc") ;-) Вопрос — какие спецсимволы нужно фильтровать для полной защиты этой дыры? На ум сразу приходят "&", "/" и ";", но может быть больше.

GitHub [1]
Кстати, эту статью можно считать продолжением мной обещанного, но на уже почти год как остановленного курса статей по настройке своего переносного сервера, используя Debian. [2] Собирался сделать что-то такое в рамках реализации своего файлового сервера, только вот тогда не смог найти готового решения… А сейчас написал своё =) В ближайшее время думаю написать ещё пару статей, которые подойдут под тематику этого курса.

Автор: CRImier

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/60336

Ссылки в тексте:

[1] вот он: https://github.com/CRImier/pautomount

[2] курса статей по настройке своего переносного сервера, используя Debian.: http://habrahabr.ru/post/188108/

[3] Источник: http://habrahabr.ru/post/220301/