En esta receta voy a contar cómo utilizar los «descriptores» de Python para poder crear atributos (variables de instancia) que no puedan cambiar de tipo durante la vida del objeto. Por supuesto, también es una excusa para aprender algo sobre los descriptores en sí.
Buenas crysoleros,antes de empezar quiero dar la enhorabuena a todos los componentes,por su maravillosa organización el día del software libre cuando asistió R.stallman,no pude asistir anteriores por falta de transporte y estudios,pero aquí estaba la ansiosa y esperada foto http://arco.esi.uclm.es/~cleto.martin/fotos_jornadas/IMG_4103_900x677.html que muchas gracias por publicarla bueno,al grano;
Ya podéis ver algunas de las fotos de las Jornadas en:
http://arco.esi.uclm.es/~cleto.martin/fotos_jornadas/
Aún me quedan por conseguir algunas, por lo que lo actualizaré en breve.
Gracias a Sergio Pérez, Francisco Sánchez y Cayetano J. Solana por las fotos.
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:
A la hora de utilizar un FS determinado FUSE se encargará de realizar todas las tareas comunes y cuando haya que realizar algo específico de nuestro FS se invocará a alguno de los métodos escritos por nosotros.
FUSE está diseñado para ofrecer soporte absoluto a FS's que cumplan todas las normas Posix. Evidentemente no es necesario para el funcionamiento de cualquier FS que se implementen todas esas funciones (como veremos más adelante), así que si alguna acción no la hemos implementado FUSE se las arreglará con lo que tenga. Si finalmente no se puede realizar la acción se eleva un error que nos mostrará el sistema operativo (por ejemplo: sistema de ficheros de sólo lectura).
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.
Vaya guarrería de sistema de ficheros... ¡pues claro! la primera versión está escrita en unas horas de tiempo libre además pretende ser muy simple para que sirva de ejemplo. Nosotros implementaremos lo siguiente:
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)
¿Cómo implementaremos nuestro FS? pues de una forma muy simple: un directorio será un diccionario en python, la clave será el nombre (del fichero o directorio) y el valor será:
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.
La clase principal, la que utilizará FUSE deberá heredar de la clase Fuse y cuando creemos una instacia de nuestro FS le pasaremos unos parámetros que indicarán a FUSE la clase de FS que va a manejar, si el módulo permite acceso concurrente, cómo debe tratar el carácter de separación de directorios, etc.
Así de buenas a primeras el esqueleto de aplicación FUSE podría ser como sigue:
Como véis en el constructor nos hemos creado el directorio raíz de nuestro FS. Si no hubiésemos necesitado nada, podríamos habernos ahorrado el método completo.
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:
Vamos a hacer esto porque en nuestro ejemplo siempre vamos a devolver un objeto de estos, pero modificando algunos atributos según sea el caso.
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)
El resto de parámetros podemos dejarlos intactos, ya hemos dicho que no trataremos al dueño del fichero ni las fechas de modificacion, acceso, etc. Además, para los directorios se asumen que tienen tamaño 0.
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:
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:
Este método creo que es un poco spaguetti porque el caso especial del root realmente no existiría. A parte de esto el funcionamiento es simple: creamos un objeto stats. Cuando nos preguntan por un elemento, si existe y es un directorio (un diccionario) ponemos unos valores a los atributos y lo retornamos. Si era un archivo (una cadena) pues ponemos otros valores y lo retornamos. Si la función no retorna nada obtendremos un parámetro inválido y la operación sobre nuestro FS fallará. Si retornamos -errno.ENOENT obtendremos un fichero no encontrado.
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.
Este método recibe el path sobre el que obtener el contenido y un offset que indica cual es el primer elemento de la lista a devolver (en todas mis pruebas siempre valía 0). Como véis hay que añadir a pelo los directorios "," (por eso en directorios st_nlink = 2) y el enlace al padre (esto es: ".."). En vez de retornar una lista pasamos directamente el iterador. Usamos un método de python-fuse que construye una entrada de directorio a partir del nombre.
Con este método podremos hacer ahora ls en nuestro FS que no dará error... pero claro, tampoco mostrará nada porque nuestro FS está vacío... Vamos a permitir la creación de directorios, para ello implementamos el siguiente método:
Es tan fácil que no hay nada que explicar... ;) Si ahora montamos nuestro FS (al final tenéis un ejemplo de cómo) veréis que podemos crear directorios, ir a ellos y listarlos... ¡todo un avance! Si intentamos borrarlos... ¡fail! así que añadimos esa posibilidad:
Ahora también podremos hacer rmdir o rm sobre un directorio... pero cuidado que si un directorio no está vacío, se eliminará también (aquí no comprobamos que no lo esté). No es demasiado grave puesto que python tiene garbage collector y no se nos quedarán por ahí directorios sin enlazar ocupando memoria...
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:
Nuestro método no retornará nada (ni elevará ninguna excepción) lo cual indicará a FUSE que todo ha ido bien. Del modo de creación pasamos completamente (ya que no mantenemos un objeto stats por cada elemento del FS). El parámetro dev es un identificador interno del kernel que representa al manejador de dispositivo asociado al FS... también pasaremos de él :).
El segundo método tampoco es complicado:
Símplemente verificamos que exista o no el fichero. Daos cuenta que en un FS real esto es más complicado puesto que deberíamos comprobar los flags con los permisos del fichero. También mantendríamos una lista de ficheros abiertos si quisiéramos controlar la concurrencia, etc.
En este momento el touch crearía ficheros vacíos... pero después daría un error extraño. ¡Pues claro! porque hemos implementado el open() pero no el close()... que en este caso se llama release(path):
Todo lo dicho para el open() es válido ahora para release() así que poco más que comentar.
En este punto ya podemos trabajar con directorios en nuestro FS y crear archivos vacíos con touch... pero si creamos un fichero con emacs por ejemplo y le damos a guardar... ¡error! pues claro... para leer y escribir de un fichero necesitamos dos métodos nuevos: read(path, length, offset) y write(path, buf, offset).
La función read() debe retornar un buffer (una cadena, en python) de como mucholength bytes, leídos del elemento path a partir del byte offset. O dicho en pythonés:
El método es bastante simple, pero hay que tener cuidado con los rangos y demás. Con el método write() tenemos menos problemas:
¡Y listo! ya podemos leer y escribir dentro de los ficheros de nuestro FS... ahora abrimos un fichero existente, le añadimos algunos bytes, le damos a aguardar y... ¡¡error!! ¿y esto? pues... ¿qué va a ser? esa operación también hay que implementarla y se llama truncate(path, size): permite modificar el tamaño de un fichero existente. El método es sencillo: truncar la cadena o concatenarla según sea necesario...
Y ahora sí... montad el FS y perrear con él... veréis que casi todo funciona. ¿Todo? pues si intentáis mover/renombrar un fichero o directorio... ¡¡FAIL!!... aún nos queda un método más: rename(oldPath, newPath). Implementar el movimiento/renombrado de ficheros en nuestro FS tampoco es muy difícil :P. A ver qué os parece:
Tampoco hay mucho que comentar así que ya os dejo de dar la lata... ¡ahora a probarlo!
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:
Y ya está... ¡montadito! (hombres de poca fé, tecleen mount para verificarlo!). Para desmontar pues el umount de toda la vida ;)
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í):
Va con comentarios del director y logging, muy útil... ;)
Muchas veces se habla sobre cómo deberían hacerse las cosas, pero a veces no se muestran los contraejemplos. A raíz de una conversación en la lista de correo se me ha ocurrido que podría ser útil tener una lista de ejemplos de malas prácticas de programación, cosas a evitar, desde fallos de novato hasta cosas más complejas.