Linux kernel hacking: real-time backup con i kernel tracepoints

I tracepoint (Documentation/trace/tracepoints.txt) sono degli hook “lightweight” che possono essere piazzati in determinati punti nel codice del kernel per registrare l’occorrenza di determinati eventi.

Un tracepoint definisce automaticamente la “probe function” che potrà essere piazzata in punti desiderati all’interno del codice del kernel (instrumentation).

Ogni volta che l’esecuzione passa da tali punti la “probe function” viene invocata e l’infrastruttura di tracing provvede a salvare il contesto dell’evento e le informazioni desiderate all’interno di un tracing buffer. Il tracing buffer sarà poi accessibile dallo userspace tramite il debugfs (/sys/kernel/debug/tracing/trace).

I kernel tracepoint sono utilizzati tipicamente come strumento di debug per tracciare in tempo reale l’occorrenza di particolari eventi, raccogliere statistiche, ecc.

In questo articolo vedremo un utilizzo dei tracepoint un po’ diverso dal solito e sfrutteremo la capacità di tracking in tempo reale e la leggerezza dell’infrastruttura dei tracepoint per realizzare un sistema di notifiche efficace e soprattutto a basso overhead.

Definizione di un tracepoint event

Il modo più rapido per definire dei tracepoint è quello di usare la event tracing infrastructure. Il prototipo di un evento che caratterizza il tracepoint è formato da:

  • una definizione, piazzata in un header file
  • uno statement (probe function), da piazzare nel codice C del kernel per intercettare l’evento

La definizione di un tracepoint viene specificata con la seguente macro di preprocessore:

TRACE_EVENT(eventname,
       TP_PROTO(int first_arg, const char *second_arg),

       TP_ARGS(first_arg, second_arg),

       TP_STRUCT__entry(
               __field(int, num)
               __string(str, second_arg)
       ),

       TP_fast_assign(
               __entry->num = dev;
               __assign_str(str, second_arg);
       ),

       TP_printk("%i %s", __entry->num, __get_str(str))
);

Dove:

  • eventname: è un identificatore univoco che identifica il particolare evento che vogliamo tracciare;
  • la macro TP_PROTO() è usata per definire il prototipo della “probe function” invocata dal tracepoint;
  • la macro TP_ARGS() è usata per definire i nomi dei parametri dichiarati nel prototipo;
  • la macro TP_fast_assign() contiene lo statement C che si occupa di assegnare la entry ad ogni record della trace (il record della trace viene riferito con il nome ‘__entry’);
  • la macro TP_printk() definisce il formato di come verrà stampato ogni record della trace (in stile formato della printf()).

In questo esempio ogni entry nel trace buffer conterrà un attributo numerico (num) e un attributo stringa (str).

Con questa dichiarazione il prototipo della probe function viene definito come:

void trace_eventname(int first_arg, const char *second_arg);

Una dichiarazione di questo tipo è sufficiente per poter piazzare la nostra “probe function” in tutti i punti che vogliamo all’interno del codice del kernel per intercettare l’evento che abbiamo nominato “eventname”. Ogni volta che verrà invocata il sistema di tracing provvederà a registrare una nuova entry nel trace buffer.

Da userspace è possibile attivare o disattivare il tracepoint tramite debugfs, scrivendo rispettivamente 1 o 0 nel file:

/sys/kernel/debug/tracing/events/TRACE_SYSTEM_NAME/eventname/enable

Assumiamo che il debugfs sia montato su /sys/kernel/debug, come di default. Inoltre, TRACE_SYSTEM_NAME è un nome che viene definito dalla macro di preprocessore ”#define TRACE_SYSTEM …” all’interno del codice e serve a raggruppare i vari eventi all’interno di un ulteriore livello gerarchico.

Una volta attivato il tracing dell’evento ogni volta che la probe function viene invocata il kernel registra una entry nel trace buffer.

Da userspace si potrà reperire il contenuto del trace buffer leggendo il file:

/sys/kernel/debug/tracing/trace

Esempio:

$ cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
#           TASK-PID    CPU#    TIMESTAMP  FUNCTION
#              | |       |          |         |
           <...>-3004  [000]  1402.258401: eventname: 0 string
           <...>-3004  [000]  1407.994688: eventname: 0 string
...

Un caso pratico: real-time backup con i tracepoint

Il nostro sistema di backup in real-time sarà caratterizzato da:

A) un componente che terrà traccia in tempo reale di tutte le modifiche ai filesystem presenti su un sistema
B) un componente che si occuperà di raccogliere periodicamente la lista di modifiche e di invocare il comando di backup vero e proprio

I due componenti agiranno in modalità produttore-consumatore. Si può intuire facilmente che il produttore (componente A) è il componente più critico dal punto di vista delle performance. Difatti dovrà essere il più leggero possibile e dovrà riuscire a tenere traccia in modo affidabile di tutte le modifiche ai filesystem senza introdurre un overhead eccessivo nel sistema.

Il componente B potrà essere eseguito periodicamente (es. in cron o come demone) a bassa priorità. Difatti, a differenza del componente A, non avrà particolari requisiti di reattività e potrà essere trattato come un generico processo batch.

A: componente di tracing (produttore)

Il componente A sembra quindi essere il candidato perfetto per essere implementato in kernel space utlizzando il meccanismo dei tracepoint visto prima.

L’evento che ci interessa intercettare è ogni generica modifica a qualsiasi filesystem presente nel sistema. In realtà, nel nostro caso, non ci interessano veramente tutti i filesystem di sistema, ma solo quelli definiti su block device “reali”, cioè che hanno una backing store definita su un disco fisico. Tutto ciò che ha una backing store in memoria, es. filesystem virtuali come procfs, sysfs, etc. o filesystem in RAM, come tmpfs, non verranno presi in considerazione. Questo ci consente di ridurre ulteriormente l’overhead del sistema di tracing.

Per ogni modifica si provvederà a tracciare il filename coinvolto e il block device su cui tale filename è definito.

B: componente di backup (consumatore)

Il sistema di backup vero e proprio potrà essere implementato totalmente in userspace, come demone. Tale demone si occuperà di leggere periodicamente dal trace buffer la lista dei file e i rispettivi block device. Per ogni file verrà poi effettuato il backup vero e proprio, es. lanciando un rsync o utilizzando un qualsiasi comando/script che si occupi di salvare il file su un dispositivo di backup.

Implementazione del componente di tracing (A)

Partiamo con la definizione dell’evento di tracing, analogamente a quanto abbiamo visto nell’introduzione ai tracepoint event.

Chiameremo semplicemente “backup”, con un notevole sforzo di fantasia 🙂 , l’evento che rappresenta ogni modifica ai filesystem; ad ogni occorrenza di tale evento provvederemo a registrare il block device (come combinazione di major e minor number, cioè il dev_t) e il filename (come stringa).

TRACE_EVENT(backup,

       TP_PROTO(dev_t dev, const char *filename),

       TP_ARGS(dev, filename),

       TP_STRUCT__entry(
               __field(dev_t, dev)
               __string(filename, filename)
       ),

       TP_fast_assign(
               __entry->dev = dev;
               __assign_str(filename, filename);
       ),

       TP_printk("%u:%u %s", MAJOR(__entry->dev), MINOR(__entry->dev),
                       __get_str(filename))
);

Abbiamo visto che tale definizione è sufficiente a metterci automaticamente a disposizione la funzione:

void trace_backup(dev_t dev, const char *filename);

da piazzare nei punti opportuni all’interno del codice del kernel per registrare l’occorrenza del nostro evento.

Prima di ricorrere all’istrumentazione vera e propria dobbiamo definire una funzione di più alto livello che filtri via gli eventi che vogliamo ignorare, e che solo nei casi di interesse invochi la funzione trace_backup().

La “probe function” effettiva sarà quindi la seguente:

void trace_backup_path(struct dentry *dentry)
{
       struct block_device *bdev = dentry->d_sb->s_bdev;
       char *buf, *fname;

       /* Do nothing if the tracepoint event is not enabled */
       if (likely(!__tracepoint_backup.state))
               return;
       /* The backing store of this dentry is in memory, so ignore it */
       if (bdev == NULL)
               return;
       buf = (char *)__get_free_page(GFP_NOWAIT);
       if (!buf)
               return;
       fname = dentry_path(dentry, buf, PAGE_SIZE);
       if (!IS_ERR(fname))
               trace_backup(bdev->bd_dev, fname);
       free_page((unsigned long)buf);
}

Questa funzione accetta come argomento una “struct dentry”, struttura definita dal kernel e utilizzata per effettuare traduzioni da inode a filename. Questa struttura contiene anche un puntatore al superblocco del filesystem su cui tale dentry (e quindi il file) è definita. Con un unico argomento siamo quindi in grado di tradurre l’inode modificato nel rispettivo filename e identificare il block device su cui tale inode è definito. Se il block device non esiste (NULL) vuol dire che abbiamo intercettato una modifica ad un filesystem con una backing store in memoria e quindi possiamo evitare di registrare tale evento nel trace buffer (vedi controllo bdev == NULL).

Se invece il block device esiste, possiamo utilizzare la funzione dentry_path() per tradurre la dentry nel filename e registrare la entry nel trace buffer con la nostra probe function trace_backup().

Il nostro sistema di tracing real-time è quasi completo, resta solo da piazzare la trace_backup_path() nei punti opportuni.

L’implementazione completa è riportata come patch al termine dell’articolo (vedi rt-backup-tracer.patch).

Implementazione del componente di backup (B)

Dato che il contesto di questo articolo è il “Linux kernel hacking” tralasceremo i dettagli sull’implementazione di tale componente, che come abbiamo detto sarà totalmente in userspace.

Come esempio mi limito a riportare un semplice script python che si occuperà di:

  • montare il debugfs;
  • reperire periodicamente il contenuto del trace buffer da /sys/kernel/debug/tracing/trace;
  • formattarlo opportunamente traducendo le coppie (device, filename) nei rispettivi filename con path assoluto;
  • infine registrare tutto in un file /var/backups/rtbackupd.bak contenente la lista effettiva dei file da backuppare.

Per i dettagli vedere rt-backupd.py riportato al termine dell’articolo.

Risultati: validazione del sistema di real-time backup

Per verificare la correttezza del nostro sistema di backup basta semplicemente lanciare il demone rt-backupd.py e dopo un po’ di tempo controllare la lista dei file che sono stati modificati leggendo il contenuto di /var/backups/rtbackupd.bak:

Esempio:

$ sudo ./rt-backupd.py
Sun, 06 Sep 2009 00:36:14: retrieving all the filesystem changes
Sun, 06 Sep 2009 00:36:16: saving changed files to /var/backups/rtbackupd.bak
...

Dopo qualche minuto:

$ cat /var/backups/rtbackupd.bak
/var/log/wpa_supplicant.log
/var/log/auth.log
/var/log/ConsoleKit/history
/var/log/syslog
...

Il sistema di tracing ha rilevato una lista di file che sono stati modificati. A questo punto possiamo dare in pasto questo file ad un rsync (per esempio) che si occuperà di effettuare il backup vero e proprio.

Il nostro sistema di backup sembra funzionare correttamente.

Risultati: overhead introdotto

Per valutare l’overhead introdotto abbiamo preso un semplice testcase che provvede semplicemente a creare e rimuovere 1000 file.

Come metrica misureremo il tempo totale impiegato ad effettuare queste operazioni.

Il test viene riportato di seguito:

$ cat test.sh
#!/bin/sh
for i in `seq 1 1000`; do
    echo i > $i
done
for i in `seq 1 1000`; do
    rm -f $i
done
$ for i in `seq 1 10`; do sudo chrt -f 99 ionice -c1 -n0 time -p sh test.sh; done
...

Su un Intel Core 2 U7600 con disco SSD MOBI MTRON 3000 e filesystem ext4 otteniamo i seguenti risultati (media su un run di 10 ripetizioni):

kernel 2.6.31-rc8 (vanilla):            6860ms
kernel 2.6.31-rc8 (with rt-backup-tracer):    6870ms
                        ——-
                    diff:      10ms

Possiamo vedere che l’overhead introdotto su questo caso di particolare impatto sui metadati è di soltanto 10ms, pari circa allo 0.15% del tempo di esecuzione, valore che potrebbe essere anche semplicemente imputabile all’errore di misurazione.

Conclusioni

In questo articolo abbiamo visto un possibile utilizzo dei kernel tracepoint un po’ diverso dal solito, e abbiamo fatto vedere come un’ottima infrastruttura tipicamente usata per attività di debug possa essere sfruttata per implementare uno strumento di produzione efficace e performante.

L’infrastruttura dei tracepoint ci ha permesso di realizzare un sistema di notifiche sui cambiamenti che avvengono su qualsiasi filesystem, senza aggiungere un overhead percepibile.

Questo ci ha permesso di rilevare in tempo reale tutti i file che vengono modificati su un generico sistema. Tali informazioni possono essere poi passate ad un generico strumento di backup, che può così risparmiarsi la fatica di dover monitorare periodicamente tutti i file presenti su un sistema per capire chi realmente deve essere sottoposto a backup e chi no, facendoci risparmiare a noi un fastidioso carico di I/O di fondo, tipico di ogni classico sistema di backup.

Molti avranno notato che la vera potenzialità della nostra soluzione sta nel sistema di notifiche real-time implementato in kernel-space. E` d’obbligo quindi un paragone con un altro ben noto file notification system messo a disposizione dal kernel: inotify (Documentation/filesystems/inotify.txt).

Infatti, lo stesso progetto poteva essere benissimo implementato on-top-of inotify, sfruttando un’infrastruttura già presente, senza quindi dover applicare nessuna patch al kernel.

Tuttavia, oltre ad essere un valido argomento di kernel hacking 🙂 , il nostro sistema di notifiche ha il vantaggio di avere un costo O(1) sia per il monitoring degli eventi (come inotify), sia per il costo di startup (basta scrivere un “1” su un file del sysfs per attivarsi globalmente su tutto il sistema).

Un sistema come inotify, invece, richiede di definire un “watch” per ogni file descriptor monitorato, introducendo di fatto una complessità O(N) per lo startup. Ciò lo rendende di fatto un sistema poco adeguato a tenere traccia di un elevato numero di file e directory; in certi casi, infatti, solo la creazione di tutti watch può richiedere tempi di startup non proprio trascurabili (pensate ad un server con migliaia di utenti, ciascuno con una propria $HOME, quanto tempo ci vuole per lanciare anche un semplice “find” su tutto il filesystem?).

E gli svantaggi del nostro sistema? beh, dobbiamo tener presente che il trace buffer è un buffer circolare, quindi il sistema di raccolta dati (il consumatore rtbackupd.py) deve essere sicuro di poter leggere tutti i dati presenti nella traccia prima che il buffer ruoti sovrascrivendo le vecchie entry. Ma questo si può evitare facilmente dimensionando opportunamente la size del trace buffer (/sys/kernel/debug/tracing/buffer_size_kb) o aumentando la frequenza con cui il consumatore reperisce periodicamente le informazioni dalla traccia.

rt-backup-tracer.patch (versione completa)

Trace all the filesystem changes using a kernel tracepoint event.

Signed-off-by: Andrea Righi <arighi@develer.com>
—-
 fs/attr.c                 |    4 +++-
 fs/btrfs/ioctl.c          |    1 +
 fs/compat.c               |    4 +++-
 fs/namei.c                |   22 ++++++++++++++++++——
 fs/open.c                 |   22 ++++++++++++++++++++++
 fs/read_write.c           |    5 ++++-
 include/linux/fs.h        |    2 ++
 include/trace/events/fs.h |   34 ++++++++++++++++++++++++++++++++++
 8 files changed, 87 insertions(+), 7 deletions(-)

diff —git a/fs/attr.c b/fs/attr.c
index 9fe1b1b..a5cd6fa 100644
—- a/fs/attr.c
+++ b/fs/attr.c
@@ -183,8 +183,10 @@ int notify_change(struct dentry * dentry, struct iattr * attr)
     if (ia_valid & ATTR_SIZE)
         up_write(&dentry->d_inode->i_alloc_sem);

-    if (!error)
+    if (!error) {
+        trace_backup_path(dentry);
         fsnotify_change(dentry, ia_valid);
+    }

     return error;
 }
diff —git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c
index bd88f25..8146475 100644
—- a/fs/btrfs/ioctl.c
+++ b/fs/btrfs/ioctl.c
@@ -520,6 +520,7 @@ create:
     if (error)
         goto out_drop_write;

+    trace_backup_path(dentry);
     fsnotify_mkdir(parent->dentry->d_inode, dentry);
 out_drop_write:
     mnt_drop_write(parent->mnt);
diff —git a/fs/compat.c b/fs/compat.c
index 94502da..4a1b4ae 100644
—- a/fs/compat.c
+++ b/fs/compat.c
@@ -1180,8 +1180,10 @@ out:
         struct dentry *dentry = file->f_path.dentry;
         if (type == READ)
             fsnotify_access(dentry);
-        else
+        else {
+            trace_backup_path(dentry);
             fsnotify_modify(dentry);
+        }
     }
     return ret;
 }
diff —git a/fs/namei.c b/fs/namei.c
index f3c5b27..e933bdc 100644
—- a/fs/namei.c
+++ b/fs/namei.c
@@ -1497,8 +1497,10 @@ int vfs_create(struct inode *dir, struct dentry *dentry, int mode,
         return error;
     vfs_dq_init(dir);
     error = dir->i_op->create(dir, dentry, mode, nd);
-    if (!error)
+    if (!error) {
+        trace_backup_path(dentry);
         fsnotify_create(dir, dentry);
+    }
     return error;
 }

@@ -1993,8 +1995,10 @@ int vfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)

     vfs_dq_init(dir);
     error = dir->i_op->mknod(dir, dentry, mode, dev);
-    if (!error)
+    if (!error) {
+        trace_backup_path(dentry);
         fsnotify_create(dir, dentry);
+    }
     return error;
 }

@@ -2092,8 +2096,10 @@ int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)

     vfs_dq_init(dir);
     error = dir->i_op->mkdir(dir, dentry, mode);
-    if (!error)
+    if (!error) {
+        trace_backup_path(dentry);
         fsnotify_mkdir(dir, dentry);
+    }
     return error;
 }

@@ -2178,6 +2184,7 @@ int vfs_rmdir(struct inode *dir, struct dentry *dentry)

     vfs_dq_init(dir);

+    trace_backup_path(dentry);
     mutex_lock(&dentry->d_inode->i_mutex);
     dentry_unhash(dentry);
     if (d_mountpoint(dentry))
@@ -2265,6 +2272,7 @@ int vfs_unlink(struct inode *dir, struct dentry *dentry)

     vfs_dq_init(dir);

+    trace_backup_path(dentry);
     mutex_lock(&dentry->d_inode->i_mutex);
     if (d_mountpoint(dentry))
         error = -EBUSY;
@@ -2376,8 +2384,10 @@ int vfs_symlink(struct inode *dir, struct dentry *dentry, const char *oldname)

     vfs_dq_init(dir);
     error = dir->i_op->symlink(dir, dentry, oldname);
-    if (!error)
+    if (!error) {
+        trace_backup_path(dentry);
         fsnotify_create(dir, dentry);
+    }
     return error;
 }

@@ -2660,6 +2670,7 @@ int vfs_rename(struct inode *old_dir, struct dentry *old_dentry,
     vfs_dq_init(old_dir);
     vfs_dq_init(new_dir);

+    trace_backup_path(old_dentry);
     old_name = fsnotify_oldname_init(old_dentry->d_name.name);

     if (is_dir)
@@ -2668,6 +2679,9 @@ int vfs_rename(struct inode *old_dir, struct dentry *old_dentry,
         error = vfs_rename_other(old_dir,old_dentry,new_dir,new_dentry);
     if (!error) {
         const char *new_name = old_dentry->d_name.name;
+
+        /* old_dentry contains the new name here */
+        trace_backup_path(old_dentry);
         fsnotify_move(old_dir, new_dir, old_name, new_name, is_dir,
                   new_dentry->d_inode, old_dentry);
     }
diff —git a/fs/open.c b/fs/open.c
index dd98e80..292d568 100644
—- a/fs/open.c
+++ b/fs/open.c
@@ -31,6 +31,9 @@
 #include <linux/falloc.h>
 #include <linux/fs_struct.h>

+#define CREATE_TRACE_POINTS
+#include <trace/events/fs.h>
+
 int vfs_statfs(struct dentry *dentry, struct kstatfs *buf)
 {
     int retval = -ENODEV;
@@ -1025,6 +1028,25 @@ void fd_install(unsigned int fd, struct file *file)

 EXPORT_SYMBOL(fd_install);

+void trace_backup_path(struct dentry *dentry)
+{
+    struct block_device *bdev = dentry->d_sb->s_bdev;
+    char *buf, *fname;
+
+    if (likely(!__tracepoint_backup.state))
+        return;
+    /* The backing store of this dentry is in memory, so ignore it */
+    if (bdev == NULL)
+        return;
+    buf = (char *)__get_free_page(GFP_NOWAIT);
+    if (!buf)
+        return;
+    fname = dentry_path(dentry, buf, PAGE_SIZE);
+    if (!IS_ERR(fname))
+        trace_backup(bdev->bd_dev, fname);
+    free_page((unsigned long)buf);
+}
+
 long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
 {
     char *tmp = getname(filename);
diff —git a/fs/read_write.c b/fs/read_write.c
index 6c8c55d..d36baee 100644
—- a/fs/read_write.c
+++ b/fs/read_write.c
@@ -348,6 +348,7 @@ ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_
         else
             ret = do_sync_write(file, buf, count, pos);
         if (ret > 0) {
+            trace_backup_path(file->f_path.dentry);
             fsnotify_modify(file->f_path.dentry);
             add_wchar(current, ret);
         }
@@ -657,8 +658,10 @@ out:
     if ((ret + (type == READ)) > 0) {
         if (type == READ)
             fsnotify_access(file->f_path.dentry);
-        else
+        else {
+            trace_backup_path(file->f_path.dentry);
             fsnotify_modify(file->f_path.dentry);
+        }
     }
     return ret;
 }
diff —git a/include/linux/fs.h b/include/linux/fs.h
index 73e9b64..cfe9381 100644
—- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2459,5 +2459,7 @@ int proc_nr_files(struct ctl_table *table, int write, struct file *filp,

 int __init get_filesystem_list(char *buf);

+void trace_backup_path(struct dentry *dentry);
+
 #endif /* __KERNEL__ */
 #endif /* _LINUX_FS_H */
diff —git a/include/trace/events/fs.h b/include/trace/events/fs.h
new file mode 100644
index 0000000..882f972
—- /dev/null
+++ b/include/trace/events/fs.h
@@ -0,0 +1,34 @@
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM fs
+
+#if !defined(_TRACE_FS_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_FS_H
+
+#include <linux/fs.h>
+#include <linux/kdev_t.h>
+#include <linux/tracepoint.h>
+
+TRACE_EVENT(backup,
+
+    TP_PROTO(dev_t dev, const char *filename),
+
+    TP_ARGS(dev, filename),
+
+    TP_STRUCT__entry(
+        __field(dev_t, dev)
+        __string(filename, filename)
+    ),
+
+    TP_fast_assign(
+        __entry->dev = dev;
+        __assign_str(filename, filename);
+    ),
+
+    TP_printk("%u:%u %s", MAJOR(__entry->dev), MINOR(__entry->dev),
+            __get_str(filename))
+);
+
+#endif /* _TRACE_FS_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>

rt-backupd.py (componente userspace per reperire la lista dei file da backuppare)

#!/usr/bin/env python
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Library General Public License as published by
# the Free Software Foundation; version 2 only
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2009 Andrea Righi <arighi@develer.com>
#
# Description:
#
#   Monitor all the files changed in the system and log them into a file.
#
#   NOTE: this program requires the rt-backup-tracer kernel patch to work.

import sys, os, time, logging, string, re

# Time to poll the trace file (default is 10 sec)
POLLING = 10

trace_file = '/sys/kernel/debug/tracing/trace'
state_file = '/var/backups/rtbackupd.bak'

files = {}
devs = {}

# Check if we run as root
def check_root():
    if os.geteuid():
        logging.critical("root permission required");
        sys.exit(1)

# Configure the kernel backup tracer
def init_fs_tracer():
    if os.path.exists(trace_file):
        return
    event_file = '/sys/kernel/debug/tracing/events/fs/backup/enable'
    logging.debug("mounting debugfs on /sys/kernel/debug")
    if os.system("mount -t debugfs none /sys/kernel/debug"):
        logging.critical("couldn't mount debugfs")
        sys.exit(1)
    logging.debug("enabling filesystem tracer")
    try:
        fd = open(event_file, 'w')
        fd.write('1')
        fd.close()
    except Exception, e:
        logging.critical(str(e))
        logging.critical("couldn't write to %s" % event_file)
        logging.critical("probably you need to apply the rt-backup kernel patch")
        sys.exit(1)

# Translate a device name into the equivalent mount point
# (i.e. /dev/sda1 -> /)
#
# XXX: this doesn't work if any device can be mounted elsewhere
def devname_to_mp(devname):
    file = "/etc/mtab" 
    try:
        fd = open(file, 'r')
        for line in fd.readlines():
            res = re.match(r'^%s (\S+)' % devname, line)
            if res is not None:
                mp = res.group(1)
                break
        fd.close()
    except Exception, e:
        logging.critical(str(e))
        logging.critical("ERROR: couldn't read mount point from %s" %
                file)
        sys.exit(1)
    return mp

# Translate a dev coordinates into the equivalent device name
# (i.e. 8:1 -> /dev/sda1)
def dev_to_devname(dev):
    file = "/sys/dev/block/%s/uevent" % dev
    try:
        fd = open(file, 'r')
        for line in fd.readlines():
            res = re.match(r'DEVNAME=(.*)', line)
            if res is not None:
                devname = res.group(1)
                break
        fd.close()
    except Exception, e:
        logging.critical(str(e))
        logging.critical("ERROR: couldn't read device name from %s" %
                file)
        sys.exit(1)
    return "/dev/" + devname

# Translate a dev coordinates into the current mountpoint
# (i.e. 8:1 -> /)
def dev_to_mp(dev):
    if not devs.has_key(dev):
        devs[dev] = devname_to_mp(dev_to_devname(dev))
    return devs[dev]

# Retrieve the list of files that have been modified in the system and fill the
# "files" dictionary with them
def read_files():
    global files
    logging.debug("retrieving all the filesystem changes")
    try:
        fd = open(trace_file, 'r')
        for line in fd.readlines():
            if line.startswith("#"):
                continue
            dev = line.split()[4]
            file = line.split()[5]
            filename = re.sub(r'/+', '/', dev_to_mp(dev) + file)
            files[file] = True
        fd.close()
    except Exception, e:
        logging.critical(str(e))
        logging.critical("ERROR: couldn't read from %s" % trace_file)
        sys.exit(1)

# Print the list of changed files that need to be backed-up
#
# NOTE: file removed from the system are not considered anymore
def write_files():
    logging.debug("saving changed files to %s" % state_file)
    tmpfile = state_file + '.tmp'
    try:
        fd = open(tmpfile, 'w')
    except Exception, e:
        logging.critical(str(e))
        logging.critical("ERROR: couldn't open %s for writing" %
                tmpfile)
        sys.exit(1)
    list = []
    for file in files.keys():
        list.append(file)
    try:
        for file in list:
            if (file == tmpfile):
                continue
            if os.path.exists(file):
                fd.write(file + '\n')
    except Exception, e:
        logging.critical(str(e))
        logging.critical("ERROR: couldn't write to %s" % tmpfile)
        sys.exit(1)
    # Be sure all the data have been committed to disk before renaming into
    # the actual state file
    os.fsync(fd)
    fd.close()
    os.rename(tmpfile, state_file)

def main():
    logging.basicConfig(
        level = logging.DEBUG,
        datefmt = '%a, %d %b %Y %H:%M:%S',
        format = '%(asctime)s: %(message)s')
    check_root()
    init_fs_tracer()

    while True:
        read_files()
        write_files()
        time.sleep(POLLING)

if __name__ == '__main__':
    main()

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.


Speak Your Mind

*

Time limit is exhausted. Please reload CAPTCHA.