[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

Re: [Libvir] PATCH: 10/16: general purpose helper APIs



On Tue, Feb 12, 2008 at 04:37:08AM +0000, Daniel P. Berrange wrote:
> This patch provides a couple of helper APIs used by the forthcoming
> driver backends.
> 
>    - virStorageBackendUpdateVolInfo - take a filename and virStorageVolPtr
>      and update the capacity/allocation information based on the file
>      stat() results. Also update the permissions data on owner/mode
>      and SELinux label.
> 
>    - virStorageBackendUpdateVolInfoFD - same as above but with a pre-opened
>      filehandle
> 
>    - virStorageBackendStablePath - given a /dev/XXX node, and a directory
>      of symlinks (eg /dev/disk/by-path), attempts to find a symlink pointing
>      to the desired file.
> 
>    - virStorageBackendRunProgRegex - given one or more regexes and a command
>      line argv[], execute the program and attempt to match its STDOUT against
>      the regex. When complete matches are found invoke a callback.
> 
>    - virStorageBackendRunProgNul - given a command line argv[] and an expected
>      number of fields per record, tokenize the STDOUT into records splitting
>      on NULL, and invoke the callback per record.
> 
> The SELinux code is optional, and can be replaced with calls to any other
> library which can provide MAC file labels, or just disabled completely..


 configure.in          |   39 ++++
 libvirt.spec.in       |    1 
 src/Makefile.am       |    3 
 src/storage_backend.c |  470 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/storage_backend.h |   38 ++++
 tests/Makefile.am     |    2 
 6 files changed, 552 insertions(+), 1 deletion(-)


diff -r afe06dcc2b64 configure.in
--- a/configure.in	Tue Feb 19 16:59:10 2008 -0500
+++ b/configure.in	Tue Feb 19 17:04:59 2008 -0500
@@ -473,6 +473,40 @@ AC_SUBST(AVAHI_CFLAGS)
 AC_SUBST(AVAHI_CFLAGS)
 AC_SUBST(AVAHI_LIBS)
 
+dnl SELinux
+AC_ARG_WITH(selinux,
+  [  --with-selinux         use SELinux to manage security],
+  [],
+  [with_selinux=check])
+
+SELINUX_CFLAGS=
+SELINUX_LIBS=
+if test "$with_selinux" != "no"; then
+  old_cflags="$CFLAGS"
+  old_libs="$LIBS"
+  if test "$with_selinux" = "check"; then
+    AC_CHECK_HEADER([selinux/selinux.h],[],[with_selinux=no])
+    AC_CHECK_LIB(selinux, fgetfilecon,[],[with_selinux=no])
+    if test "$with_selinux" != "no"; then
+      with_selinux="yes"
+    fi
+  else
+    AC_CHECK_HEADER([selinux/selinux.h],[],
+       [AC_MSG_ERROR([You must install the SELinux development package in order to compile libvirt])])
+    AC_CHECK_LIB(selinux, fgetfilecon,[],
+       [AC_MSG_ERROR([You must install the SELinux development package in order to compile and run libvirt])])
+  fi
+  CFLAGS="$old_cflags"
+  LIBS="$old_libs"
+fi
+if test "$with_selinux" = "yes"; then
+  SELINUX_LIBS="-lselinux"
+  AC_DEFINE_UNQUOTED(HAVE_SELINUX, 1, [whether SELinux is available for security])
+fi
+AM_CONDITIONAL(HAVE_SELINUX, [test "$with_selinux" != "no"])
+AC_SUBST(SELINUX_CFLAGS)
+AC_SUBST(SELINUX_LIBS)
+
 dnl virsh libraries
 AC_CHECK_HEADERS([readline/readline.h])
 
@@ -745,6 +779,11 @@ else
 else
 AC_MSG_NOTICE([  polkit: no])
 fi
+if test "$with_selinux" = "yes" ; then
+AC_MSG_NOTICE([  selinux: $SELINUX_CFLAGS $SELINUX_LIBS])
+else
+AC_MSG_NOTICE([  selinux: no])
+fi
 AC_MSG_NOTICE([])
 AC_MSG_NOTICE([Miscellaneous])
 AC_MSG_NOTICE([])
diff -r afe06dcc2b64 libvirt.spec.in
--- a/libvirt.spec.in	Tue Feb 19 16:59:10 2008 -0500
+++ b/libvirt.spec.in	Tue Feb 19 17:04:59 2008 -0500
@@ -41,6 +41,7 @@ BuildRequires: gettext
 BuildRequires: gettext
 BuildRequires: gnutls-devel
 BuildRequires: avahi-devel
+BuildRequires: libselinux-devel
 BuildRequires: dnsmasq
 BuildRequires: bridge-utils
 BuildRequires: qemu
diff -r afe06dcc2b64 src/Makefile.am
--- a/src/Makefile.am	Tue Feb 19 16:59:10 2008 -0500
+++ b/src/Makefile.am	Tue Feb 19 17:04:59 2008 -0500
@@ -8,6 +8,7 @@ INCLUDES = \
 	   $(LIBXML_CFLAGS) \
 	   $(GNUTLS_CFLAGS) \
 	   $(SASL_CFLAGS) \
+	   $(SELINUX_CFLAGS) \
 	   -DBINDIR=\""$(libexecdir)"\" \
 	   -DSBINDIR=\""$(sbindir)"\" \
 	   -DSYSCONF_DIR="\"$(sysconfdir)\"" \
@@ -67,7 +68,7 @@ SERVER_SOURCES = 						\
 		../qemud/remote_protocol.c ../qemud/remote_protocol.h
 
 libvirt_la_SOURCES = $(CLIENT_SOURCES) $(SERVER_SOURCES)
-libvirt_la_LIBADD = $(LIBXML_LIBS) $(GNUTLS_LIBS) $(SASL_LIBS) \
+libvirt_la_LIBADD = $(LIBXML_LIBS) $(GNUTLS_LIBS) $(SASL_LIBS) $(SELINUX_LIBS) \
 		    @CYGWIN_EXTRA_LIBADD@ ../gnulib/lib/libgnu.la
 libvirt_la_LDFLAGS = -Wl,--version-script=$(srcdir)/libvirt_sym.version \
                      -version-info @LIBVIRT_VERSION_INFO@ \
diff -r afe06dcc2b64 src/storage_backend.c
--- a/src/storage_backend.c	Tue Feb 19 16:59:10 2008 -0500
+++ b/src/storage_backend.c	Tue Feb 19 17:04:59 2008 -0500
@@ -28,6 +28,14 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <sys/stat.h>
+#include <dirent.h>
+
+#if HAVE_SELINUX
+#include <selinux/selinux.h>
+#endif
 
 #include "util.h"
 
@@ -73,6 +81,468 @@ virStorageBackendToString(int type) {
 }
 
 
+int
+virStorageBackendUpdateVolInfo(virConnectPtr conn,
+                               virStorageVolDefPtr vol,
+                               int withCapacity)
+{
+    int ret, fd;
+
+    if ((fd = open(vol->target.path, O_RDONLY)) < 0) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot open volume '%s': %s"),
+                              vol->target.path, strerror(errno));
+        return -1;
+    }
+
+    ret = virStorageBackendUpdateVolInfoFD(conn,
+                                           vol,
+                                           fd,
+                                           withCapacity);
+
+    close(fd);
+
+    return ret;
+}
+
+int
+virStorageBackendUpdateVolInfoFD(virConnectPtr conn,
+                                 virStorageVolDefPtr vol,
+                                 int fd,
+                                 int withCapacity)
+{
+    struct stat sb;
+#if HAVE_SELINUX
+    security_context_t filecon = NULL;
+#endif
+
+    if (fstat(fd, &sb) < 0) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot stat file '%s': %s"),
+                              vol->target.path, strerror(errno));
+        return -1;
+    }
+
+    if (!S_ISREG(sb.st_mode) &&
+        !S_ISCHR(sb.st_mode) &&
+        !S_ISBLK(sb.st_mode))
+        return -2;
+
+    if (S_ISREG(sb.st_mode)) {
+        vol->allocation = (unsigned long long)sb.st_blocks *
+            (unsigned long long)sb.st_blksize;
+        /* Regular files may be sparse, so logical size (capacity) is not same
+         * as actual allocation above
+         */
+        if (withCapacity)
+            vol->capacity = sb.st_size;
+    } else {
+        off_t end;
+        /* XXX this is POSIX compliant, but doesn't work for for CHAR files,
+         * only BLOCK. There is a Linux specific ioctl() for getting
+         * size of both CHAR / BLOCK devices we should check for in
+         * configure
+         */
+        end = lseek(fd, 0, SEEK_END);
+        if (end == (off_t)-1) {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  _("cannot seek to end of file '%s':%s"),
+                                  vol->target.path, strerror(errno));
+            return -1;
+        }
+        vol->allocation = end;
+        if (withCapacity) vol->capacity = end;
+    }
+
+    vol->target.perms.mode = sb.st_mode;
+    vol->target.perms.uid = sb.st_uid;
+    vol->target.perms.gid = sb.st_gid;
+
+    free(vol->target.perms.label);
+    vol->target.perms.label = NULL;
+
+#if HAVE_SELINUX
+    if (fgetfilecon(fd, &filecon) == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot get file context of %s: %s"),
+                              vol->target.path, strerror(errno));
+        return -1;
+    }
+    vol->target.perms.label = strdup(filecon);
+    if (vol->target.perms.label == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, _("context"));
+        return -1;
+    }
+    freecon(filecon);
+#else
+    vol->target.perms.label = NULL;
+#endif
+
+    return 0;
+}
+
+/*
+ * Given a volume path directly in /dev/XXX, iterate over the
+ * entries in the directory pool->def->target.path and find the
+ * first symlink pointing to the volume path.
+ *
+ * If, the target.path is /dev/, then return the original volume
+ * path.
+ *
+ * If no symlink is found, then return the original volume path
+ *
+ * Typically target.path is one of the /dev/disk/by-XXX dirs
+ * with stable paths.
+ */
+char *
+virStorageBackendStablePath(virConnectPtr conn,
+                            virStoragePoolObjPtr pool,
+                            char *devpath)
+{
+    DIR *dh;
+    struct dirent *dent;
+
+    /* Short circuit if pool has no target, or if its /dev */
+    if (pool->def->target.path == NULL ||
+        STREQ(pool->def->target.path, "/dev") ||
+        STREQ(pool->def->target.path, "/dev/"))
+        return devpath;
+
+    /* The pool is pointing somewhere like /dev/disk/by-path
+     * or /dev/disk/by-id, so we need to check all symlinks in
+     * the target directory and figure out which one points
+     * to this device node
+     */
+    if ((dh = opendir(pool->def->target.path)) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot read dir %s: %s"),
+                              pool->def->target.path,
+                              strerror(errno));
+        return NULL;
+    }
+
+    while ((dent = readdir(dh)) != NULL) {
+        char *stablepath;
+        if (dent->d_name[0] == '.')
+            continue;
+
+        stablepath = malloc(strlen(pool->def->target.path) +
+                            1 + strlen(dent->d_name) + 1);
+        if (stablepath == NULL) {
+            virStorageReportError(conn, VIR_ERR_NO_MEMORY, _("path"));
+            closedir(dh);
+            return NULL;
+        }
+
+        strcpy(stablepath, pool->def->target.path);
+        strcat(stablepath, "/");
+        strcat(stablepath, dent->d_name);
+
+        if (virFileLinkPointsTo(stablepath, devpath)) {
+            closedir(dh);
+            return stablepath;
+        }
+
+        free(stablepath);
+    }
+
+    closedir(dh);
+
+    /* Couldn't find any matching stable link so give back
+     * the original non-stable dev path
+     */
+    return devpath;
+}
+
+/*
+ * Run an external program.
+ *
+ * Read its output and apply a series of regexes to each line
+ * When the entire set of regexes has matched consequetively
+ * then run a callback passing in all the matches
+ */
+int
+virStorageBackendRunProgRegex(virConnectPtr conn,
+                              virStoragePoolObjPtr pool,
+                              const char **prog,
+                              int nregex,
+                              const char **regex,
+                              int *nvars,
+                              virStorageBackendListVolRegexFunc func,
+                              void *data)
+{
+    int child = 0, fd = -1, exitstatus, err, failed = 1;
+    FILE *list = NULL;
+    regex_t *reg;
+    regmatch_t *vars = NULL;
+    char line[1024];
+    int maxReg = 0, i, j;
+    int totgroups = 0, ngroup = 0, maxvars = 0;
+    char **groups;
+
+    /* Compile all regular expressions */
+    if ((reg = calloc(nregex, sizeof(*reg))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, _("regex"));
+        return -1;
+    }
+
+    for (i = 0 ; i < nregex ; i++) {
+        err = regcomp(&reg[i], regex[i], REG_EXTENDED);
+        if (err != 0) {
+            char error[100];
+            regerror(err, &reg[i], error, sizeof(error));
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  _("Failed to compile regex %s"), error);
+            for (j = 0 ; j <= i ; j++)
+                regfree(&reg[j]);
+            free(reg);
+            return -1;
+        }
+
+        totgroups += nvars[i];
+        if (nvars[i] > maxvars)
+            maxvars = nvars[i];
+
+    }
+
+    /* Storage for matched variables */
+    if ((groups = calloc(totgroups, sizeof(*groups))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY,
+                              _("regex groups"));
+        goto cleanup;
+    }
+    if ((vars = calloc(maxvars+1, sizeof(*vars))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY,
+                              _("regex groups"));
+        goto cleanup;
+    }
+
+
+    /* Run the program and capture its output */
+    if (virExec(conn, (char**)prog, &child, -1, &fd, NULL) < 0) {
+        goto cleanup;
+    }
+
+    if ((list = fdopen(fd, "r")) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot read fd"));
+        goto cleanup;
+    }
+
+    while (fgets(line, sizeof(line), list) != NULL) {
+        /* Strip trailing newline */
+        int len = strlen(line);
+        if (len && line[len-1] == '\n')
+            line[len-1] = '\0';
+
+        for (i = 0 ; i <= maxReg && i < nregex ; i++) {
+            if (regexec(&reg[i], line, nvars[i]+1, vars, 0) == 0) {
+                maxReg++;
+
+                if (i == 0)
+                    ngroup = 0;
+
+                /* NULL terminate each captured group in the line */
+                for (j = 0 ; j < nvars[i] ; j++) {
+                    /* NB vars[0] is the full pattern, so we offset j by 1 */
+                    line[vars[j+1].rm_eo] = '\0';
+                    if ((groups[ngroup++] =
+                         strdup(line + vars[j+1].rm_so)) == NULL) {
+                        virStorageReportError(conn, VIR_ERR_NO_MEMORY,
+                                              _("regex groups"));
+                        goto cleanup;
+                    }
+                }
+
+                /* We're matching on the last regex, so callback time */
+                if (i == (nregex-1)) {
+                    if (((*func)(conn, pool, groups, data)) < 0)
+                        goto cleanup;
+
+                    /* Release matches & restart to matching the first regex */
+                    for (j = 0 ; j < totgroups ; j++) {
+                        free(groups[j]);
+                        groups[j] = NULL;
+                    }
+                    maxReg = 0;
+                    ngroup = 0;
+                }
+            }
+        }
+    }
+
+    failed = 0;
+
+ cleanup:
+    if (groups) {
+        for (j = 0 ; j < totgroups ; j++)
+            free(groups[j]);
+        free(groups);
+    }
+    free(vars);
+
+    for (i = 0 ; i < nregex ; i++)
+        regfree(&reg[i]);
+
+    free(reg);
+
+    if (list)
+        fclose(list);
+    else
+        close(fd);
+
+    while ((err = waitpid(child, &exitstatus, 0) == -1) && errno == EINTR);
+
+    /* Don't bother checking exit status if we already failed */
+    if (failed)
+        return -1;
+
+    if (err == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("failed to wait for command: %s"),
+                              strerror(errno));
+        return -1;
+    } else {
+        if (WIFEXITED(exitstatus)) {
+            if (WEXITSTATUS(exitstatus) != 0) {
+                virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                      _("non-zero exit status from command %d"),
+                                      WEXITSTATUS(exitstatus));
+                return -1;
+            }
+        } else {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  _("command did not exit cleanly"));
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+/*
+ * Run an external program and read from its standard output
+ * a stream of tokens from IN_STREAM, applying FUNC to
+ * each successive sequence of N_COLUMNS tokens.
+ * If FUNC returns < 0, stop processing input and return -1.
+ * Return -1 if N_COLUMNS == 0.
+ * Return -1 upon memory allocation error.
+ * If the number of input tokens is not a multiple of N_COLUMNS,
+ * then the final FUNC call will specify a number smaller than N_COLUMNS.
+ * If there are no input tokens (empty input), call FUNC with N_COLUMNS == 0.
+ */
+int
+virStorageBackendRunProgNul(virConnectPtr conn,
+                            virStoragePoolObjPtr pool,
+                            const char **prog,
+                            size_t n_columns,
+                            virStorageBackendListVolNulFunc func,
+                            void *data)
+{
+    size_t n_tok = 0;
+    int child = 0, fd = -1, exitstatus;
+    FILE *fp = NULL;
+    char **v;
+    int err = -1;
+    int w_err;
+    int i;
+
+    if (n_columns == 0)
+        return -1;
+
+    if (n_columns > SIZE_MAX / sizeof *v
+        || (v = malloc (n_columns * sizeof *v)) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY,
+                              _("n_columns too large"));
+        return -1;
+    }
+    for (i = 0; i < n_columns; i++)
+        v[i] = NULL;
+
+    /* Run the program and capture its output */
+    if (virExec(conn, (char**)prog, &child, -1, &fd, NULL) < 0) {
+        goto cleanup;
+    }
+
+    if ((fp = fdopen(fd, "r")) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("cannot read fd"));
+        goto cleanup;
+    }
+
+    while (1) {
+        char *buf = NULL;
+        size_t buf_len = 0;
+        /* Be careful: even when it returns -1,
+           this use of getdelim allocates memory.  */
+        ssize_t tok_len = getdelim (&buf, &buf_len, 0, fp);
+        v[n_tok] = buf;
+        if (tok_len < 0) {
+            /* Maybe EOF, maybe an error.
+               If n_tok > 0, then we know it's an error.  */
+            if (n_tok && func (conn, pool, n_tok, v, data) < 0)
+                goto cleanup;
+            break;
+        }
+        ++n_tok;
+        if (n_tok == n_columns) {
+            if (func (conn, pool, n_tok, v, data) < 0)
+                goto cleanup;
+            n_tok = 0;
+            for (i = 0; i < n_columns; i++) {
+                free (v[i]);
+                v[i] = NULL;
+            }
+        }
+    }
+
+    if (feof (fp))
+        err = 0;
+    else
+        virStorageReportError (conn, VIR_ERR_INTERNAL_ERROR,
+                               _("read error: %s"), strerror (errno));
+
+ cleanup:
+    for (i = 0; i < n_columns; i++)
+        free (v[i]);
+    free (v);
+
+    if (fp)
+        fclose (fp);
+    else
+        close (fd);
+
+    while ((w_err = waitpid (child, &exitstatus, 0) == -1) && errno == EINTR)
+        /* empty */ ;
+
+    /* Don't bother checking exit status if we already failed */
+    if (err < 0)
+        return -1;
+
+    if (w_err == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              _("failed to wait for command: %s"),
+                              strerror(errno));
+        return -1;
+    } else {
+        if (WIFEXITED(exitstatus)) {
+            if (WEXITSTATUS(exitstatus) != 0) {
+                virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                      _("non-zero exit status from command %d"),
+                                      WEXITSTATUS(exitstatus));
+                return -1;
+            }
+        } else {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  _("command did not exit cleanly"));
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+
 /*
  * vim: set tabstop=4:
  * vim: set shiftwidth=4:
diff -r afe06dcc2b64 src/storage_backend.h
--- a/src/storage_backend.h	Tue Feb 19 16:59:10 2008 -0500
+++ b/src/storage_backend.h	Tue Feb 19 17:04:59 2008 -0500
@@ -103,6 +103,44 @@ int virStorageBackendFromString(const ch
 int virStorageBackendFromString(const char *type);
 const char *virStorageBackendToString(int type);
 
+int virStorageBackendUpdateVolInfo(virConnectPtr conn,
+                                   virStorageVolDefPtr vol,
+                                   int withCapacity);
+
+int virStorageBackendUpdateVolInfoFD(virConnectPtr conn,
+                                     virStorageVolDefPtr vol,
+                                     int fd,
+                                     int withCapacity);
+
+char *virStorageBackendStablePath(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  char *devpath);
+
+typedef int (*virStorageBackendListVolRegexFunc)(virConnectPtr conn,
+                                                 virStoragePoolObjPtr pool,
+                                                 char **const groups,
+                                                 void *data);
+typedef int (*virStorageBackendListVolNulFunc)(virConnectPtr conn,
+                                               virStoragePoolObjPtr pool,
+                                               size_t n_tokens,
+                                               char **const groups,
+                                               void *data);
+
+int virStorageBackendRunProgRegex(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  const char **prog,
+                                  int nregex,
+                                  const char **regex,
+                                  int *nvars,
+                                  virStorageBackendListVolRegexFunc func,
+                                  void *data);
+
+int virStorageBackendRunProgNul(virConnectPtr conn,
+                                virStoragePoolObjPtr pool,
+                                const char **prog,
+                                size_t n_columns,
+                                virStorageBackendListVolNulFunc func,
+                                void *data);
 
 #endif /* __VIR_STORAGE_BACKEND_H__ */
 
diff -r afe06dcc2b64 tests/Makefile.am
--- a/tests/Makefile.am	Tue Feb 19 16:59:10 2008 -0500
+++ b/tests/Makefile.am	Tue Feb 19 17:04:59 2008 -0500
@@ -20,6 +20,7 @@ INCLUDES = \
 	$(LIBXML_CFLAGS) \
 	$(GNUTLS_CFLAGS) \
 	$(SASL_CFLAGS) \
+	$(SELINUX_CFLAGS) \
         -D_XOPEN_SOURCE=600 -D_POSIX_C_SOURCE=199506L \
         -DGETTEXT_PACKAGE=\"$(PACKAGE)\" \
          $(COVERAGE_CFLAGS) \
@@ -31,6 +32,7 @@ LDADDS = \
 	$(LIBXML_LIBS) \
         $(GNUTLS_LIBS) \
         $(SASL_LIBS) \
+        $(SELINUX_LIBS) \
         $(WARN_CFLAGS) \
 	$(LIBVIRT) \
 	../gnulib/lib/libgnu.la \


-- 
|=- Red Hat, Engineering, Emerging Technologies, Boston.  +1 978 392 2496 -=|
|=-           Perl modules: http://search.cpan.org/~danberr/              -=|
|=-               Projects: http://freshmeat.net/~danielpb/               -=|
|=-  GnuPG: 7D3B9505   F3C9 553F A1DA 4AC2 5648 23C1 B3DF F742 7D3B 9505  -=| 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]