Fuse и Python

1. Вводная

Fuse - это проект, предоставляющий программисту функционал для написания драйвера для произвольной файловой системы. Fuse (в Linux) включает в себя модуль ядра, используемый различными программами для связи с драйвером ФС (который является, по существу, исполняемым файлом).

Стратегия разработки драйвера проста - необходимо реализовать некоторый набор функций с определенными прототипами, указатели на которых передать модулю ядра (с помощью вызова некоторой функции - т.н. fuse_main) - после чего при обращении к ФС будут вызываться именно те функции из драйвера, указатели на которые были переданы в fuse_main. Нетрудно догадаться, что диапазон применения fuse очень и очень широк - с ее помощью можно разработать самые "неожиданные" ФС :-)

Плюсы fuse:
  • Код драйвера ФС выполняется в user-space - сразу исчезают многие проблемы, возникающие при отладке модуля ядра

  • У fuse есть много "привязок" к разным ЯП (кроме C, это еще и Python, например)

  • Простые принципы разработки драйверов (простое API)

  • Стабильность, надежность и защищенность



Подробности разработки драйверов ФС на C/C++ для fuse можно узнать в статье: "Разработка собственной файловой системы с помощью FUSE" (ibm.com Россия).

Однако в этой статье я расскажу, как написать драйвер, используя python.

2. Установка "привязок" к fuse для python

Все просто. Для начала получим исходный код "привязок":

Проследуйте на страницу FUSEWiki - FusePython и выберите понравившийся вам способ загрузки.

Я использовал Mercurial:

hg clone http://mercurial.creo.hu/repos/fuse-python-hg

После загрузки перейдите в каталог с исходниками fuse-python и сделайте:

python setup.py build
su -c 'python setup.py install'


Вы получите модуль fuse, в котором содержаться вся необходимые классы.

3. Небольшой пример

Давайте напишем драйвер для "игрушечной" виртуальной ФС.

В начале - каркас драйвера:
  1. Первым делом мы импортируем нужные модули:

    import os,stat
    import fuse

  2. Теперь необходимо сообщить fuse-python какую ее версию мы используем:

    fuse.fuse_python_api = (0, 2)

  3. Описываем базовый для нашего драйвера класс (он будет наследовать класс fuse.Fuse):

    class simpleFS(fuse.Fuse):
      #функции
      #TODO

  4. Описываем функцию, которая будет "запускать" наш драйвер:

    def runSimpleFS():
      usage='Simple FS ' + fuse.Fuse.fusage
      fs = simpleFS(version="%prog " + fuse.__version__,usage=usage,dash_s_do='setsingle')
      fs.parse(errex=1)
      fs.main()


    Как вы могли заметить, принципиально важными являются вызов конструктора fuse.Fuse (с правильным списком аргументов), вызов fs.parse() и вызов fs.main(). fs.main() это и есть как раз та самая fuse_main. Указатели на функцию в fuse-python подменяются функциями-членами_класса fuse.Fuse.

    Важное замечание: после вызова fs.main() драйверу перестает быть доступной консоль, из которой, собственно, драйвер и был запущен. В частности, теряется связь с STDIN, STDOUT, STDERR - т.е. мы сможем догадаться об исключенях только по результатам работы драйвера.

  5. Вызываем runSimpleFS():

    runSimpleFS()


Каркас готов.

Теперь давайте определимся с тем, что будет представлять из себя наша ФС:
  1. 2 каталога - '/' и '/simple' - оба с правами 0555 и root.root в качестве пользователя.группы_пользователя

  2. 1 файл - '/README' (0444 root.root)


Реализация нашего класса:

  1. Конструктор. В нем принципиально важно вызвать конструктор базового класса со всеми переданными в конструктор simpleFS аргументами (а self.README - содержимое файла '/README'):

    def __init__(self, *args, **kw):
      fuse.Fuse.__init__(self, *args, **kw)
      self.README = 'This is simple FS\n'

  2. Пользовательские действия с ФС (системные вызовы и прочее). На каждой такой функции в fuse.Fuse стоит "заглушка". Нам остается только переопределить нужные нам функции:


    # getattr вызывается при получении информации об объекте ФС. Например, при использовании команды ls

    def getattr(self, path):
      # В объекте fuse.Stat() вернем интересующую информацию
      st = fuse.Stat()
      # "Режим" - права доступа, тип объекта
      st.st_mode = 0
      # Номер inode
      st.st_ino = 0
      st.st_dev = 0
      # Количество ссылок на объект
      st.st_nlink = 0
      # ID владельца объекта
      st.st_uid = 0
      # ID группы владельца объекта
      st.st_gid = 0
      # Размер объекта
      st.st_size = 0
      # Временные штампы
      st.st_atime = 0
      st.st_mtime = 0
      st.st_ctime = 0
      if path == '/' or path == '/simple':
        # Каталоги
        st.st_mode = stat.S_IFDIR | 0755
        st.st_nlink = 3
      elif path == '/README':
        # Файлы
        st.st_mode = stat.S_IFREG | 0444
        st.st_nlink = 1
        st.st_size = len(self.README)
      else:
        # path не существует
        return -errno.ENOENT
      return st

    # readdir вызывается при попытке просмотра содержимого каталога. Например, при использовании ls

    def readdir(self, path, offset):
      # В каждом каталоге есть '.' и '..'
      yield fuse.Direntry('.')
      yield fuse.Direntry('..')
      if path == '/':
        # Кроме того, в '/' есть еще и 'README' и 'simple'
        yield fuse.Direntry('README')
        yield fuse.Direntry('simple')

    # open вызывается при попытки открыть файл. Мы должны проверить флаги доступа - наш единственный файл '/README' доступен только на чтение

    def open(self, path, flags):
      if path != '/README':
        return -errno.ENOENT
      accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
      if (flags & accmode) != os.O_RDONLY:
        # Ошибка доступа
        return -errno.EACCES

    # read вызывается при попытки прочитать данные из файла
    # offset - смещение в читаемом файле
    # size - размер считываемого ("запрощенного") блока
    # read возвращает считанные символы

    def read(self, path, size, offset):
      if path != '/README':
        return -errno.ENOENT
      slen = len(self.README)
      if offset < slen:
        if offset + size > slen:
          size = slen - offset
        buf = self.README[offset:offset+size]
      else:
        buf = ''
      return buf

    # statfs вызывается в ответ на запрос информации о ФС

    def statfs(self):
      # Вернем информацию в объекте класса fuse.StatVfs
      st = fuse.StatVfs()
      # Размер блока
      st.f_bsize = 1024
      st.f_frsize = 1024
      st.f_bfree = 0
      st.f_bavail = 0
      # Количество файлов
      st.f_files = 2
      # Количество блоков
      # Если f_blocks == 0, то 'df' не включит ФС в свой список - понадобится сделать 'df -a'
      st.f_blocks = 4
      st.f_ffree = 0
      st.f_favail = 0
      st.f_namelen = 255
      return st


Вот и все. Драйвер готов (исходный код драйвера лежит здесь).

Проверка:

mkdir smpfs
python simplefs.py smpfs
ls -la smpfs/
ls -la smpfs/simple
cat smpfs/README
df
su -c 'umount smpfs'


Заметьте, что первым параметром в скрипт передается точка монтирования (каталог 'smpfs' в нашем случае).

В случае, если этого примера вам показалось мало - посмотрите в каталог 'example' дистрибутива fuse-python. Там есть кое-что интересное.

4. Почитать



5. BashOrgFS

Здесь лежит моя поделка на заданную тему. bash.org.ru представлен в виде файловой системы.

Монтировать так:

./bashorg ТОЧКА_МОНТИРОВАНИЯ.

Заранее - это просто пример. Используйте на свой страх и риск. Предварительно читайте README ;-)

6. За сим все. Успехов!

4 комментария:

rdx комментирует...

Полезная информация. Спасибо. Статья стала стимулом написать для себя ФС на базе FUSE. :)

Анонимный комментирует...

хотелось бы найти что-нибудь про настройки и т.д. находящиеся в монтируемой папке типа autorun.inf, Trash, Trash-1000 и т.д. У смих авторов Fuse не найти толком доков, приходится самому ручками изучать чего хочет система по ошибкам в логе

Анонимный комментирует...

Привет.
Можете еще раз выложить исходный код?

Я пютаюсь скачать тот, что Вы выложили:
Несуществующая страница
Страница, которую вы читаете, не существует.

Анонимный комментирует...

5. BashOrgFS
Здесь лежит моя поделка на заданную тему. bash.org.ru представлен в виде файловой системы.

Тоже не могу скачать.