Pautomount —

от автора

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

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

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

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

Прошу сильно не плеваться на качество кода, это было написано два года назад, да и дальше в статье будет ещё много кода — берегите слюну =) Суть проста — в цикле проверяем, подключено ли устройство с конкретным UUID, если да, то находим, какое блочное устройство eject’ить и делаем это. Подключил плеер к компьютеру, через пару секунд он автоматически отключается, но продолжает заряжаться. Было достаточно удобно… Пока не потерял плеер =( С тех пор надобность в таком устройстве практически отпала.

А сейчас судьба столкнула меня с некоторыми задачами, для которых требуются автоматические действия при подключении носителя. Вот и решил это исправить, только теперь и близко не подходя к Bash =) Язык программирования, впрочем, долго выбирать не пришлось — да и не из чего, честно говоря, я только Python в последнее время и занимаюсь. Дальше пришлось подумать — писать ли демон, который в цикле проверяет, не появилось ли новых ещё необработанных разделов, или цепляться через udev, чтобы программа вызывалась как callback при подключении любой флешки. Второй вариант вроде потребляет меньше ресурсов — но первый не зависит от udev и проще =) Поэтому решил писать демона, которого можно спокойно сунуть в автозапуск.

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

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():     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] 

Просто и быстро, не нужно составлять всякие списки переменных к экспорту или запихивать все переменные в словарь.

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» не будут обрабатываться переменные идентификации — смысла нет =)

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/220301/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *