FUSE y python: crea tu propio sistema de ficheros fácilmente
Buenas!Hace un par de días, en uno de mis ratos libres se me ocurrió buscar información sobre cómo crear tu propio sistema de ficheros para Linux (si, el kernel). En los nomerosos foros comentaban que era algo muy díficil y que requería mucho tiempo y esfuerzo. Bien, esto es verdad... pero también es verdad que no tenemos porqué enfrentarnos al problema en todo su esplendor. Existe un modulito para los Linux 2.4 y 2.6 que permite montar sistemas de ficheros en espacio de usuario. Esta receta explicará como crearnos nuestro propio filesystem para montarse mediante fuse y para ello nada mejor que crearnos nuestro propio FS.
Qué es FUSE
Inicialmente FUSE era un componente de AFS. Finalmente se desarrolló como componente independiente y AFS se convirtió en un módulo de AFS. FUSE se compone de un módulo que se carga en el kernel y una biblioteca que facilita el acceso al mismo. Además, para desarrollar módulos en un determinado lenguaje debe existir un wrapper para dicho lenguaje. Afortunadamente para nosotros existe uno para python, en Debian y similares:$ sudo aptitude install python-fuse
Nuestro sistema de ficheros
Nosotros vamos a crear un sistema de ficheros con las siguientes características:- NO es persistente, es decir: cuando desmontemos volarán todos los datos.
- NO soportará gestión de permisos.
- NO soportará fechas (ni de creación, modificación, etc.)
- NO permitirá enlaces simbólicos.
- Creación/eliminación de directorios
- Creación/eliminación de ficheros
- Modificación de ficheros
- Mover/renombrar ficheros y directorios
- Nivel de anidamiento ilimitado (teórico, claro)
- Una cadena si se trata de un fichero
- Otro diccionario si se trata de un subdirectorio
Cómo escribir un módulo para FUSE
Bueno, nuestros módulos van a ser ejecutables que aceptarán opciones similares a mount y mediante los cuales podremos montar directamente nuestro FS en la estructura de directorios de nuestro sistema. A la hora de depurar nuestro FS debéis saber que los print no se van a mostrar por pantalla (se acabó la depuración por chivatos) y que las excepciones no capturadas se las comerá FUSE y como mucho obtendremos por consola un "imposible hacer XXX: argumento no válido". Así que puede ser una buena tarea estudiar un poquito el módulo logging de python ;). A la hora de implementar los métodos a los que invocará FUSE tenemos tres opciones:- No escribir el método: se elevará una excepción que capturará FUSE y obtendremos un mesaje similar a "imposible hacer XXX: no implementado".
- Escribir un método hueco que devuelva error: obtendremos el mensaje de antes, sin embargo nos resultará útil para saber qué acciones en el FS ocasionan llamadas a unos métodos u otros (cosa que supongo podríamos ver estudiando código y documentación).
- Implementar el método y que devuelva un valor correcto o error en caso de fallo: cuantos más de estos tengamos, mejor será nuestro FS :D.
#!/usr/bin/env python
import fuse
from fuse import Fuse
if not hasattr(fuse, '__version__'):
raise RuntimeError, \
"python-fuse doesn't know of fuse.__version__, probably it's too old."
fuse.fuse_python_api = (0, 2)
# My FS, only stored in memory :P
#
class DictFS(Fuse):
"""
"""
def __init__(self, *args, **kw):
Fuse.__init__(self, *args, **kw)
# Root dir
self.root = {}
def main():
usage = """
Userspace filesystem example
""" + Fuse.fusage
fs = DictFS(version = '%prog' + fuse.__version__,
usage = usage,
dash_s_do='setsingle')
fs.parse(errex = 1)
fs.main()
if __name__ == '__main__':
main()
Entradas de directorio y atributos
Como ya sabéis, una entrada de directorio en nuestro FS será un fichero o un directorio. Cada vez que FUSE entre en un directorio o vaya a leer un fichero, preguntará primero por sus atributos. Para ello invocará al método getstats(path) de nuestra clase y le pasará la ruta ruta completa dentro de nuestro FS. El raíz de nuestro FS será '/' que no tiene porqué coincidir con el '/' de nuestro sistema. Este método es básico en nuestro FS y debe retornar un objeto de tipo fuse.stats. Podemos crearnos nosotros nuestra propia clase de atributos:class MyStat(fuse.Stat):
def __init__(self):
self.st_mode = 0
self.st_ino = 0
self.st_dev = 0
self.st_nlink = 0
self.st_uid = 0
self.st_gid = 0
self.st_size = 0
self.st_atime = 0
self.st_mtime = 0
self.st_ctime = 0
Atributos de directorio
Cambiaremos los siguientes valores:- st_mode = stat.S_IFDIR | 0755 (recordad que deben ser ejecutables)
- st_nlink = 2 (numero de enlaces al fichero, debe ser distinto de 0, en directorios se usa 2)
Atributos de archivo
Ahora los valores serán:- st_mode = stat.S_IFREG | 0666 (así impedimos ejecución de ficheros en nuestro FS)
- st_link = 1 (en nuestro FS siempre será 1, 0 indicaría archivo borrado)
- st_size = longitud del fichero (de la cadena en nuestro caso)
Nuestas funciones auxiliares
Bueno, lo suyo es que si nos hacemos un FS nos hagamos una clase a parte que implemente nuestro FS y el modulito de FUSE sirva de wrapper entre nuestra clase y el FUSE. Pero bueno, voy a pasar y como queremos un ejemplo simple lo metemos todo en la misma clase. Un ejemplo de esto serán las funciones auxiliares que nos vamos a crear. La principal es __get_dir(path) que, siendo path una lista de elementos (nombres de directorio), caminará desde el diccionario root hasta llegar al último elemento de la lista (directorio hoja) y nos lo devolverá. Los métodos __join_path(path) y __path_list(path) convierten una lista de elementos en una cadena del tipo "/elemento1/elemento2" y viceversa (si, conozco os.path.join() y tal pero preferí escribirlos yo). Por último está __navigate(path). Este método nos resultará muy útil porque cada vez que FUSE se refiere a un elemento de nuestro FS lo hace utilizando la ruta completa dentro de nuestro FS. Así, si nos indicase "/dir1/dir2/elemento1", este método nos devolvería el directorio dir2 y el nombre de elemento1. Como veréis más adelante, esto nos hará todo el trabajo. El código de las funciones es el siguiente, no hay mucho más que comentar sobre ellas:# Return string path as list of path elements
def __path_list(self, path):
raw_path = path.split('/')
path = []
for entry in raw_path:
if entry != '':
path.append(entry)
return path
# Return list of path elements as string
def __join_path(self, path):
joined_path = '/'
for element in path:
joined_path += (element + '/')
return joined_path[:-1]
# Return dict of a given path
def __get_dir(self, path):
level = self.root
path = self.__path_list(path)
for entry in path:
if level.has_key(entry):
if type(level[entry]) is dict:
level = level[entry]
else:
# Walk over files?
return {}
else:
# Walk over non-existent dirs?
return {}
return level
# Return dict of a given path plus last name of path
def __navigate(self, path):
path = self.__path_list(path)
entry = path.pop()
# Get level
level = self.__get_dir(self.__join_path(path))
return level, entry
Los métodos propios de nuestro FS
Bien, ya tenemos todos los ingredientes, pero si ahora intentásemos montar un directorio con nuestro módulo nos daría error porque FUSE no sería capaz de leer el directorio raíz de nuestro FS. La primera llamada que intenta FUSE es: getattr('/') así pues, lo primero que tenemos que implementar es ese método:def getattr(self, path):
st = MyStat()
# Ask for root dir
if path == '/':
#return self.root.stats
st.st_mode = stat.S_IFDIR | 0755
st.st_nlink = 2
return st
level, entry = self.__navigate(path)
if level.has_key(entry):
# Entry found
# is a directory?
if type(level[entry]) is dict:
st.st_mode = stat.S_IFDIR | 0755
st.st_nlink = 2
return st
# is a file?
if type(level[entry]) is str:
st.st_mode = stat.S_IFREG | 0666
st.st_nlink = 1
st.st_size = len(level[entry])
return st
# File not found
return -errno.ENOENT
Operaciones con directorios
Ahora mismo ya podríamos montar nuestro FS, pero un simple ls sobre él nos daría error. Ahora hay que implementar tres operaciones para poder listar, crear y borrar directorios: readdir(path, offset), mkdir(path, mode) y rmdir(path) respectivamente.def readdir(self, path, offset):
file_entries = ['.','..']
# Get filelist
level = self.__get_dir(path)
if len(level.keys()) > 0:
file_entries += level.keys()
file_entries = file_entries[offset:]
for filename in file_entries:
yield fuse.Direntry(filename)
def mkdir ( self, path, mode ):
level, entry = self.__navigate(path)
# Make new dir
level[entry] = {}
def rmdir ( self, path ):
level, entry = self.__navigate(path)
# File exists?
if not level.has_key(entry):
return -errno.ENOENT
# Delete entry
del(level[entry])
Operaciones con ficheros
Bien, ya podemos trabajar con directorios como con cualquier otro FS... pero... ¿y los ficheros? estos son bastante más chicha... Si montáis nuestro FS y hacéis un touch os dará un unimplemented error, nos hacen falta dos métodos (además del getstat()) para poder realizarlo: mknod(path, mode, dev) y open(path, flags). El primero creará el enlace y el segundo intentará abrirlo (aunque luego no realice operaciones sobre él). Estas dos operaciones no deben devolver error (o elevar una excepción) para que touch funcione. La primera es bastante sencilla:def mknod ( self, path, mode, dev ):
level, filename = self.__navigate(path)
# Make empty file
level[filename] = ''
def open ( self, path, flags ):
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
def release ( self, path, flags ):
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
def read ( self, path, length, offset ):
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
# Check ranges
file_size = len(level[filename])
if offset < file_size:
# Fix size
if offset + length > file_size:
length = file_size - offset
buf = level[filename][offset:offset + length]
else:
# Invalid range returns no data, instead error!
buf = ''
return buf
def write ( self, path, buf, offset ):
level, filename = self.__navigate(path)
# Write data into file
if offset > len(level[filename]):
offset = (offset % len(level[filename]))
# This operation could be truncate the file!!
level[filename] = level[filename][:offset] + str(buf)
# Return written bytes
return len(buf)
def truncate ( self, path, size ):
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
if len(level[filename]) > size:
# Truncate file to specified size
level[filename] = level[filename][:size]
else:
# Add more bytes
level[filename] += ' ' * (size - len(level[filename]))
def rename ( self, oldPath, newPath ):
oldLevel, oldFilename = self.__navigate(oldPath)
# Can't use __navigate() because newPath-filename not exists
newPath = self.__path_list(newPath)
newFilename = newPath.pop()
newLevel = self.__get_dir(self.__join_path(newPath))
# Make new link
newLevel[newFilename] = oldLevel[oldFilename]
# Remove old
self.unlink(oldPath)
Probando el invento
Supongamos que habéis creado el archivo dictfs.py con permisos de ejecución y toda la pesca. Si estáis en el grupo de FUSE o sois sudoers: $ mkdir mymnt
$ sudo ./dictfs.py mymnt/
El código completo
Bueno, tengo el ejemplo en mi github pero aquí os voy a copiar la versión inicial (supongo que si actualizo, también lo haré aquí):#!/usr/bin/env python
#
# Released under GPLv3 license
# Read full text at: gnu.org/licenses/gpl-3.0.html
#
import os
import stat
import errno
import fuse
from fuse import Fuse
if not hasattr(fuse, '__version__'):
raise RuntimeError, \
"python-fuse doesn't know of fuse.__version__, probably it's too old."
fuse.fuse_python_api = (0, 2)
import logging
LOG_FILENAME = 'dictfs.log'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)
# Only make one of this whe getstat() is called. Real FS has one per entry (file
# or directory).
#
class MyStat(fuse.Stat):
def __init__(self):
self.st_mode = 0
self.st_ino = 0
self.st_dev = 0
self.st_nlink = 0
self.st_uid = 0
self.st_gid = 0
self.st_size = 0
self.st_atime = 0
self.st_mtime = 0
self.st_ctime = 0
# My FS, only stored in memory :P
#
class DictFS(Fuse):
"""
"""
def __init__(self, *args, **kw):
Fuse.__init__(self, *args, **kw)
# Root dir
self.root = {}
# Return string path as list of path elements
def __path_list(self, path):
raw_path = path.split('/')
path = []
for entry in raw_path:
if entry != '':
path.append(entry)
return path
# Return list of path elements as string
def __join_path(self, path):
joined_path = '/'
for element in path:
joined_path += (element + '/')
return joined_path[:-1]
# Return dict of a given path
def __get_dir(self, path):
level = self.root
path = self.__path_list(path)
for entry in path:
if level.has_key(entry):
if type(level[entry]) is dict:
level = level[entry]
else:
# Walk over files?
return {}
else:
# Walk over non-existent dirs?
return {}
return level
# Return dict of a given path plus last name of path
def __navigate(self, path):
# Path analysis
path = self.__path_list(path)
entry = path.pop()
# Get level
level = self.__get_dir(self.__join_path(path))
return level, entry
### FILESYSTEM FUNCTIONS ###
def getattr(self, path):
st = MyStat()
logging.debug('*** getattr(%s)', path)
# Ask for root dir
if path == '/':
#return self.root.stats
st.st_mode = stat.S_IFDIR | 0755
st.st_nlink = 2
return st
level, entry = self.__navigate(path)
if level.has_key(entry):
# Entry found
# is a directory?
if type(level[entry]) is dict:
st.st_mode = stat.S_IFDIR | 0755 # rwx r-x r-x
st.st_nlink = 2
logging.debug('*** getattr_dir_found: %s', entry)
return st
# is a file?
if type(level[entry]) is str:
st.st_mode = stat.S_IFREG | 0666 # rw- rw- rw-
st.st_nlink = 1
st.st_size = len(level[entry])
logging.debug('*** getattr_file_found: %s', entry)
return st
# File not found
logging.debug('*** getattr_entry_not_found')
return -errno.ENOENT
def readdir(self, path, offset):
logging.debug('*** readdir(%s, %d)', path, offset)
file_entries = ['.','..']
# Get filelist
level = self.__get_dir(path)
# Get all directory entries
if len(level.keys()) > 0:
file_entries += level.keys()
file_entries = file_entries[offset:]
for filename in file_entries:
yield fuse.Direntry(filename)
def mkdir ( self, path, mode ):
logging.debug('*** mkdir(%s, %d)', path, mode)
level, entry = self.__navigate(path)
# Make new dir
level[entry] = {}
def mknod ( self, path, mode, dev ):
logging.debug('*** mknod(%s, %d, %d)', path, mode, dev)
level, filename = self.__navigate(path)
# Make empty file
level[filename] = ''
# This method could maintain opened (or locked) file list and,
# of course, it could check file permissions.
# For now, only check if file exists...
def open ( self, path, flags ):
logging.debug('*** open(%s, %d)', path, flags)
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
# No exception or no error means OK
# In this example this method is the same as open(). This method
# is called by close() syscall, it's means that if open() maintain
# an opened-file list, or lock files, or something... this method
# must do reverse operation (refresh opened-file list, unlock files...
def release ( self, path, flags ):
logging.debug('*** release(%s, %d)', path, flags)
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
def read ( self, path, length, offset ):
logging.debug('*** read(%s, %d, %d)', path, length, offset)
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
# Check ranges
file_size = len(level[filename])
if offset < file_size:
# Fix size
if offset + length > file_size:
length = file_size - offset
buf = level[filename][offset:offset + length]
else:
# Invalid range returns no data, instead error!
buf = ''
return buf
def rmdir ( self, path ):
logging.debug('*** rmdir(%s)', path)
level, entry = self.__navigate(path)
# File exists?
if not level.has_key(entry):
return -errno.ENOENT
# Delete entry
del(level[entry])
def truncate ( self, path, size ):
logging.debug('*** truncate(%s, %d)', path, size)
level, filename = self.__navigate(path)
# File exists?
if not level.has_key(filename):
return -errno.ENOENT
if len(level[filename]) > size:
# Truncate file to specified size
level[filename] = level[filename][:size]
else:
# Add more bytes
level[filename] += ' ' * (size - len(level[filename]))
def unlink ( self, path ):
logging.debug('*** unlink(%s)', path)
level, entry = self.__navigate(path)
# File exists?
if not level.has_key(entry):
return -errno.ENOENT
# Remove entry
del(level[entry])
def write ( self, path, buf, offset ):
logging.debug('*** write(%s, %s, %d)', path, str(buf), offset)
level, filename = self.__navigate(path)
# Write data into file
if offset > len(level[filename]):
offset = (offset % len(level[filename]))
level[filename] = level[filename][:offset] + str(buf)
# Return written bytes
return len(buf)
def rename ( self, oldPath, newPath ):
logging.debug('*** rename(%s, %s)', oldPath, newPath)
oldLevel, oldFilename = self.__navigate(oldPath)
# Can't use __navigate() because newPath-filename not exists
newPath = self.__path_list(newPath)
newFilename = newPath.pop()
newLevel = self.__get_dir(self.__join_path(newPath))
# Make new link
newLevel[newFilename] = oldLevel[oldFilename]
# Remove old
self.unlink(oldPath)
def main():
usage = """
Userspace filesystem example
""" + Fuse.fusage
fs = DictFS(version = '%prog' + fuse.__version__,
usage = usage,
dash_s_do='setsingle')
fs.parse(errex = 1)
fs.main()
if __name__ == '__main__':
main()
Enlaces interesantes
[ show comments ]
blog comments powered by Disqus