Re: [PATCH 12/12] Add a virtlockd client as a lock driver impl

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

 



On 12.09.2012 18:29, Daniel P. Berrange wrote:
> From: "Daniel P. Berrange" <berrange@xxxxxxxxxx>
> 
> This adds a 'lockd' lock driver which is just a client which
> talks to the lockd daemon to perform all locking. This will
> be the default lock driver for any hypervisor which needs one.
> 
> * src/Makefile.am: Add lockd.so plugin
> * src/locking/lock_driver_lockd.c: Lockd driver impl
> 
> Signed-off-by: Daniel P. Berrange <berrange@xxxxxxxxxx>
> ---
>  po/POTFILES.in                  |   1 +
>  src/Makefile.am                 |  26 +-
>  src/locking/lock_driver_lockd.c | 561 ++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 584 insertions(+), 4 deletions(-)
>  create mode 100644 src/locking/lock_driver_lockd.c
> 

ACK with one nit

> diff --git a/po/POTFILES.in b/po/POTFILES.in
> index 6b9a7af..663e37b 100644
> --- a/po/POTFILES.in
> +++ b/po/POTFILES.in
> @@ -46,6 +46,7 @@ src/libvirt.c
>  src/libvirt-qemu.c
>  src/locking/lock_daemon.c
>  src/locking/lock_daemon_dispatch.c
> +src/locking/lock_driver_lockd.c
>  src/locking/lock_driver_sanlock.c
>  src/locking/lock_manager.c
>  src/lxc/lxc_cgroup.c
> diff --git a/src/Makefile.am b/src/Makefile.am
> index b402297..ec5014a 100644
> --- a/src/Makefile.am
> +++ b/src/Makefile.am
> @@ -158,6 +158,10 @@ LOCK_DAEMON_GENERATED = \
>  BUILT_SOURCES += $(LOCK_DAEMON_GENERATED)
>  MAINTAINERCLEANFILES += $(LOCK_DAEMON_GENERATED)
>  
> +LOCK_DRIVER_LOCKD_SOURCES = \
> +		locking/lock_driver_lockd.c \
> +		$(NULL)
> +
>  LOCK_DAEMON_SOURCES = \
>  		locking/lock_daemon.h \
>  		locking/lock_daemon.c \
> @@ -1501,7 +1505,22 @@ libvirt_qemu_la_CFLAGS = $(AM_CFLAGS)
>  libvirt_qemu_la_LIBADD = libvirt.la $(CYGWIN_EXTRA_LIBADD)
>  EXTRA_DIST += $(LIBVIRT_QEMU_SYMBOL_FILE)
>  
> +lockdriverdir = $(libdir)/libvirt/lock-driver
> +lockdriver_LTLIBRARIES =
> +
>  if WITH_LIBVIRTD
> +lockdriver_LTLIBRARIES += lockd.la
> +lockd_la_SOURCES = \
> +		$(LOCK_DRIVER_LOCKD_SOURCES) \
> +		$(LOCK_PROTOCOL_GENERATED) \
> +		$(NULL)
> +lockd_la_CFLAGS = $(AM_CFLAGS)
> +lockd_la_LDFLAGS = -module -avoid-version
> +lockd_la_LIBADD = ../gnulib/lib/libgnu.la libvirt-net-rpc.la libvirt-net-rpc-client.la
> +if WITH_DTRACE_PROBES
> +lockd_la_LIBADD += libvirt_probes.lo
> +endif
> +
>  sbin_PROGRAMS = virtlockd
>  
>  virtlockd_SOURCES = \
> @@ -1529,7 +1548,8 @@ virtlockd_LDADD += libvirt_probes.lo
>  endif
>  
>  else
> -EXTRA_DIST += $(LOCK_DAEMON_SOURCES)
> +EXTRA_DIST += $(LOCK_DAEMON_SOURCES) \
> +		$(LOCK_DRIVER_LOCKD_SOURCES)

indentation

>  endif
>  
>  EXTRA_DIST += locking/virtlockd.sysconf
> @@ -1623,9 +1643,7 @@ virtlockd.socket: locking/virtlockd.socket.in $(top_builddir)/config.status
>  
>  
>  if HAVE_SANLOCK
> -lockdriverdir = $(libdir)/libvirt/lock-driver
> -lockdriver_LTLIBRARIES = sanlock.la
> -
> +lockdriver_LTLIBRARIES += sanlock.la
>  sanlock_la_SOURCES = $(LOCK_DRIVER_SANLOCK_SOURCES)
>  sanlock_la_CFLAGS = $(AM_CFLAGS)
>  sanlock_la_LDFLAGS = -module -avoid-version
> diff --git a/src/locking/lock_driver_lockd.c b/src/locking/lock_driver_lockd.c
> new file mode 100644
> index 0000000..462996b
> --- /dev/null
> +++ b/src/locking/lock_driver_lockd.c
> @@ -0,0 +1,561 @@
> +/*
> + * lock_driver_lockd.c: A lock driver which locks nothing
> + *
> + * Copyright (C) 2010-2011 Red Hat, Inc.
> + *
> + * This library is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU Lesser General Public
> + * License as published by the Free Software Foundation; either
> + * version 2.1 of the License, or (at your option) any later version.
> + *
> + * This library 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
> + * Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public
> + * License along with this library;  If not, see
> + * <http://www.gnu.org/licenses/>.
> + *
> + */
> +
> +#include <config.h>
> +
> +#include "lock_driver.h"
> +#include "memory.h"
> +#include "logging.h"
> +#include "uuid.h"
> +#include "util.h"
> +#include "virfile.h"
> +#include "virterror_internal.h"
> +#include "rpc/virnetclient.h"
> +#include "lock_protocol.h"
> +#include "configmake.h"
> +
> +#define VIR_FROM_THIS VIR_FROM_LOCKING
> +
> +#define virLockError(code, ...)                                     \
> +    virReportErrorHelper(VIR_FROM_THIS, code, __FILE__,             \
> +                         __FUNCTION__, __LINE__, __VA_ARGS__)
> +
> +typedef struct _virLockManagerLockDaemonPrivate virLockManagerLockDaemonPrivate;
> +typedef virLockManagerLockDaemonPrivate *virLockManagerLockDaemonPrivatePtr;
> +
> +typedef struct _virLockManagerLockDaemonResource virLockManagerLockDaemonResource;
> +typedef virLockManagerLockDaemonResource *virLockManagerLockDaemonResourcePtr;
> +
> +struct _virLockManagerLockDaemonResource {
> +    char *lockspace;
> +    char *name;
> +    unsigned int flags;
> +};
> +
> +struct _virLockManagerLockDaemonPrivate {
> +    unsigned char uuid[VIR_UUID_BUFLEN];
> +    char *name;
> +    int id;
> +    pid_t pid;
> +
> +    size_t nresources;
> +    virLockManagerLockDaemonResourcePtr resources;
> +};
> +
> +
> +#define VIRTLOCKD_PATH SBINDIR "/virtlockd"
> +
> +static const char *
> +virLockManagerLockDaemonFindDaemon(void)
> +{
> +    const char *customDaemon = getenv("VIRTLOCKD_PATH");
> +
> +    if (customDaemon)
> +        return customDaemon;
> +
> +    if (virFileIsExecutable(VIRTLOCKD_PATH))
> +        return VIRTLOCKD_PATH;
> +
> +    return NULL;
> +}
> +
> +static int virLockManagerLockDaemonInit(unsigned int version,
> +                                        const char *configFile,
> +                                        unsigned int flags)
> +{
> +    VIR_DEBUG("version=%u configFile=%s flags=%x", version, NULLSTR(configFile), flags);
> +
> +    return 0;
> +}
> +
> +static int virLockManagerLockDaemonDeinit(void)
> +{
> +    VIR_DEBUG(" ");
> +
> +    return 0;
> +}
> +
> +static void virLockManagerLockDaemonFree(virLockManagerPtr lock)
> +{
> +    virLockManagerLockDaemonPrivatePtr priv = lock->privateData;
> +    size_t i;
> +
> +    if (!priv)
> +        return;
> +
> +    lock->privateData = NULL;
> +
> +    for (i = 0 ; i < priv->nresources ; i++) {
> +        VIR_FREE(priv->resources[i].lockspace);
> +        VIR_FREE(priv->resources[i].name);
> +    }
> +    VIR_FREE(priv->resources);
> +
> +    VIR_FREE(priv->name);
> +
> +    VIR_FREE(priv);
> +}
> +
> +
> +static char *virLockManagerLockDaemonPath(bool privileged)
> +{
> +    char *path;
> +    if (privileged) {
> +        if (!(path = strdup(LOCALSTATEDIR "/run/libvirt/virtlockd/virtlockd.sock"))) {
> +            virReportOOMError();
> +            return NULL;
> +        }
> +    } else {
> +        char *userdir;
> +        if (!(userdir = virGetUserDirectory()))
> +            return NULL;
> +
> +        if (virAsprintf(&path, "%s/.libvirt/virtlockd/virtlockd.sock", userdir) < 0) {
> +            virReportOOMError();
> +        }
> +        VIR_FREE(userdir);
> +    }
> +    return path;
> +}
> +
> +
> +static int virLockManagerLockDaemonNew(virLockManagerPtr lock,
> +                                       unsigned int type,
> +                                       size_t nparams,
> +                                       virLockManagerParamPtr params,
> +                                       unsigned int flags)
> +{
> +    virLockManagerLockDaemonPrivatePtr priv;
> +    size_t i;
> +
> +    virCheckFlags(VIR_LOCK_MANAGER_USES_STATE, -1);
> +
> +    if (VIR_ALLOC(priv) < 0) {
> +        virReportOOMError();
> +        return -1;
> +    }
> +    lock->privateData = priv;
> +
> +    switch (type) {
> +    case VIR_LOCK_MANAGER_OBJECT_TYPE_DOMAIN:
> +        for (i = 0 ; i < nparams ; i++) {
> +            if (STREQ(params[i].key, "uuid")) {
> +                memcpy(priv->uuid, params[i].value.uuid, VIR_UUID_BUFLEN);
> +            } else if (STREQ(params[i].key, "name")) {
> +                if (!(priv->name = strdup(params[i].value.str))) {
> +                    virReportOOMError();
> +                    return -1;
> +                }
> +            } else if (STREQ(params[i].key, "id")) {
> +                priv->id = params[i].value.i;
> +            } else if (STREQ(params[i].key, "pid")) {
> +                priv->pid = params[i].value.i;
> +            } else {
> +                virReportError(VIR_ERR_INTERNAL_ERROR,
> +                               _("Unexpected parameter %s for object"),
> +                               params[i].key);
> +            }
> +        }
> +        if (priv->id == 0) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Missing ID parameter for domain object"));
> +            return -1;
> +        }
> +        if (priv->pid == 0) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Missing PID parameter for domain object"));
> +            return -1;
> +        }
> +        if (!priv->name) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Missing name parameter for domain object"));
> +            return -1;
> +        }
> +        if (!virUUIDIsValid(priv->uuid)) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Missing UUID parameter for domain object"));
> +            return -1;
> +        }
> +        break;
> +
> +    default:
> +        virReportError(VIR_ERR_INTERNAL_ERROR,
> +                       _("Unknown lock manager object type %d"),
> +                       type);
> +        return -1;
> +    }
> +
> +    return 0;
> +}
> +
> +
> +static int virLockManagerLockDaemonAddResource(virLockManagerPtr lock,
> +                                               unsigned int type,
> +                                               const char *name,
> +                                               size_t nparams,
> +                                               virLockManagerParamPtr params,
> +                                               unsigned int flags)
> +{
> +    virLockManagerLockDaemonPrivatePtr priv = lock->privateData;
> +    char *newName;
> +    char *newLockspace = NULL;
> +
> +    virCheckFlags(VIR_LOCK_MANAGER_RESOURCE_READONLY |
> +                  VIR_LOCK_MANAGER_RESOURCE_SHARED, -1);
> +
> +    if (flags & VIR_LOCK_MANAGER_RESOURCE_READONLY)
> +        return 0;
> +
> +    switch (type) {
> +    case VIR_LOCK_MANAGER_RESOURCE_TYPE_DISK:
> +        if (params || nparams) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Unexpected parameters for disk resource"));
> +            return -1;
> +        }
> +        if (!(newLockspace = strdup(""))) {
> +            virReportOOMError();
> +            return -1;
> +        }
> +        break;
> +    case VIR_LOCK_MANAGER_RESOURCE_TYPE_LEASE: {
> +        size_t i;
> +        char *path = NULL;
> +        char *lockspace = NULL;
> +        for (i = 0 ; i < nparams ; i++) {
> +            if (STREQ(params[i].key, "offset")) {
> +                if (params[i].value.ul != 0) {
> +                    virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                                   _("Offset must be zero for this lock manager"));
> +                    return -1;
> +                }
> +            } else if (STREQ(params[i].key, "lockspace")) {
> +                lockspace = params[i].value.str;
> +            } else if (STREQ(params[i].key, "path")) {
> +                path = params[i].value.str;
> +            } else {
> +                virReportError(VIR_ERR_INTERNAL_ERROR,
> +                               _("Unexpected parameter %s for lease resource"),
> +                               params[i].key);
> +                return -1;
> +            }
> +        }
> +        if (!path || !lockspace) {
> +            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                           _("Missing path or lockspace for lease resource"));
> +            return -1;
> +        }
> +        if (virAsprintf(&newLockspace, "%s/%s",
> +                        path, lockspace) < 0) {
> +            virReportOOMError();
> +            return -1;
> +        }
> +    }   break;
> +    default:
> +        virReportError(VIR_ERR_INTERNAL_ERROR,
> +                       _("Unknown lock manager object type %d"),
> +                       type);
> +        return -1;
> +    }
> +
> +    if (!(newName = strdup(name)))
> +        goto no_memory;
> +
> +    if (VIR_EXPAND_N(priv->resources, priv->nresources, 1) < 0)
> +        goto no_memory;
> +
> +    priv->resources[priv->nresources-1].lockspace = newLockspace;
> +    priv->resources[priv->nresources-1].name = newName;
> +
> +    if (flags & VIR_LOCK_MANAGER_RESOURCE_SHARED)
> +        priv->resources[priv->nresources-1].flags |=
> +            VIR_LOCK_SPACE_PROTOCOL_ACQUIRE_RESOURCE_SHARED;
> +
> +    return 0;
> +
> +no_memory:
> +    virReportOOMError();
> +    VIR_FREE(newName);
> +    return -1;
> +}
> +
> +
> +static int
> +virLockManagerLockDaemonConnectionRegister(virLockManagerPtr lock,
> +                                           virNetClientPtr client,
> +                                           virNetClientProgramPtr program,
> +                                           int *counter)
> +{
> +    virLockManagerLockDaemonPrivatePtr priv = lock->privateData;
> +    virLockSpaceProtocolRegisterArgs args;
> +    int rv = -1;
> +
> +    memset(&args, 0, sizeof(args));
> +
> +    args.flags = 0;
> +    memcpy(args.owner.uuid, priv->uuid, VIR_UUID_BUFLEN);
> +    args.owner.name = priv->name;
> +    args.owner.id = priv->id;
> +    args.owner.pid = priv->pid;
> +
> +    if (virNetClientProgramCall(program,
> +                                client,
> +                                (*counter)++,
> +                                VIR_LOCK_SPACE_PROTOCOL_PROC_REGISTER,
> +                                0, NULL, NULL, NULL,
> +                                (xdrproc_t)xdr_virLockSpaceProtocolRegisterArgs, (char*)&args,
> +                                (xdrproc_t)xdr_void, NULL) < 0)
> +        goto cleanup;
> +
> +    rv = 0;
> +
> +cleanup:
> +    return rv;
> +}
> +
> +
> +static int
> +virLockManagerLockDaemonConnectionRestrict(virLockManagerPtr lock ATTRIBUTE_UNUSED,
> +                                           virNetClientPtr client,
> +                                           virNetClientProgramPtr program,
> +                                           int *counter)
> +{
> +    virLockSpaceProtocolRestrictArgs args;
> +    int rv = -1;
> +
> +    memset(&args, 0, sizeof(args));
> +
> +    args.flags = 0;
> +
> +    if (virNetClientProgramCall(program,
> +                                client,
> +                                (*counter)++,
> +                                VIR_LOCK_SPACE_PROTOCOL_PROC_RESTRICT,
> +                                0, NULL, NULL, NULL,
> +                                (xdrproc_t)xdr_virLockSpaceProtocolRestrictArgs, (char*)&args,
> +                                (xdrproc_t)xdr_void, NULL) < 0)
> +        goto cleanup;
> +
> +    rv = 0;
> +
> +cleanup:
> +    return rv;
> +}
> +
> +
> +static virNetClientPtr virLockManagerLockDaemonConnectionNew(bool privileged,
> +                                                             virNetClientProgramPtr *prog)
> +{
> +    virNetClientPtr client = NULL;
> +    char *lockdpath;
> +    const char *daemonPath = NULL;
> +
> +    *prog = NULL;
> +
> +    if (!(lockdpath = virLockManagerLockDaemonPath(privileged)))
> +        goto error;
> +
> +    if (!privileged)
> +        daemonPath = virLockManagerLockDaemonFindDaemon();
> +
> +    if (!(client = virNetClientNewUNIX(lockdpath,
> +                                       daemonPath != NULL,
> +                                       daemonPath)))
> +        goto error;
> +
> +    if (!(*prog = virNetClientProgramNew(VIR_LOCK_SPACE_PROTOCOL_PROGRAM,
> +                                         VIR_LOCK_SPACE_PROTOCOL_PROGRAM_VERSION,
> +                                         NULL,
> +                                         0,
> +                                         NULL)))
> +        goto error;
> +
> +    if (virNetClientAddProgram(client, *prog) < 0)
> +        goto error;
> +
> +    VIR_FREE(lockdpath);
> +
> +    return client;
> +
> +error:
> +    VIR_FREE(lockdpath);
> +    virNetClientClose(client);
> +    virObjectUnref(client);
> +    virObjectUnref(*prog);
> +    return NULL;
> +}
> +
> +
> +static virNetClientPtr
> +virLockManagerLockDaemonConnect(virLockManagerPtr lock,
> +                                virNetClientProgramPtr *program,
> +                                int *counter)
> +{
> +    virNetClientPtr client;
> +
> +    if (!(client = virLockManagerLockDaemonConnectionNew(getuid() == 0, program)))
> +        return NULL;
> +
> +    if (virLockManagerLockDaemonConnectionRegister(lock,
> +                                                   client,
> +                                                   *program,
> +                                                   counter) < 0)
> +        goto error;
> +
> +    return client;
> +
> +error:
> +    virNetClientClose(client);
> +    virObjectUnref(client);
> +    return NULL;
> +}
> +
> +
> +static int virLockManagerLockDaemonAcquire(virLockManagerPtr lock,
> +                                           const char *state ATTRIBUTE_UNUSED,
> +                                           unsigned int flags,
> +                                           int *fd)
> +{
> +    virNetClientPtr client = NULL;
> +    virNetClientProgramPtr program = NULL;
> +    int counter = 0;
> +    int rv = -1;
> +    virLockManagerLockDaemonPrivatePtr priv = lock->privateData;
> +
> +    virCheckFlags(VIR_LOCK_MANAGER_ACQUIRE_REGISTER_ONLY |
> +                  VIR_LOCK_MANAGER_ACQUIRE_RESTRICT, -1);
> +
> +    if (!(client = virLockManagerLockDaemonConnect(lock, &program, &counter)))
> +        goto cleanup;
> +
> +    if (fd &&
> +        (*fd = virNetClientDupFD(client, false)) < 0)
> +        goto cleanup;
> +
> +    if (!(flags & VIR_LOCK_MANAGER_ACQUIRE_REGISTER_ONLY)) {
> +        size_t i;
> +        for (i = 0 ; i < priv->nresources ; i++) {
> +            virLockSpaceProtocolAcquireResourceArgs args;
> +
> +            memset(&args, 0, sizeof(args));
> +
> +            if (priv->resources[i].lockspace)
> +            args.path = priv->resources[i].lockspace;
> +            args.name = priv->resources[i].name;
> +            args.flags = priv->resources[i].flags;
> +
> +            if (virNetClientProgramCall(program,
> +                                        client,
> +                                        counter++,
> +                                        VIR_LOCK_SPACE_PROTOCOL_PROC_ACQUIRE_RESOURCE,
> +                                        0, NULL, NULL, NULL,
> +                                        (xdrproc_t)xdr_virLockSpaceProtocolAcquireResourceArgs, &args,
> +                                        (xdrproc_t)xdr_void, NULL) < 0)
> +                goto cleanup;
> +        }
> +    }
> +
> +    if ((flags & VIR_LOCK_MANAGER_ACQUIRE_RESTRICT) &&
> +        virLockManagerLockDaemonConnectionRestrict(lock, client, program, &counter) < 0)
> +        goto cleanup;
> +
> +    rv = 0;
> +
> +cleanup:
> +    if (rv != 0 && fd)
> +        VIR_FORCE_CLOSE(*fd);
> +    virNetClientClose(client);
> +    virObjectUnref(client);
> +    virObjectUnref(program);
> +
> +    return rv;
> +}
> +
> +static int virLockManagerLockDaemonRelease(virLockManagerPtr lock,
> +                                           char **state,
> +                                           unsigned int flags)
> +{
> +    virNetClientPtr client = NULL;
> +    virNetClientProgramPtr program = NULL;
> +    int counter = 0;
> +    virLockSpaceProtocolReleaseResourceArgs args;
> +    int rv = -1;
> +
> +    memset(&args, 0, sizeof(args));
> +
> +    if (state)
> +        *state = NULL;
> +
> +    if (!(client = virLockManagerLockDaemonConnect(lock, &program, &counter)))
> +        goto cleanup;
> +
> +    args.flags = flags;
> +
> +    if (virNetClientProgramCall(program,
> +                                client,
> +                                counter++,
> +                                VIR_LOCK_SPACE_PROTOCOL_PROC_RELEASE_RESOURCE,
> +                                0, NULL, NULL, NULL,
> +                                (xdrproc_t)xdr_virLockSpaceProtocolReleaseResourceArgs, &args,
> +                                (xdrproc_t)xdr_void, NULL) < 0)
> +        goto cleanup;
> +
> +    rv = 0;
> +
> +cleanup:
> +    virNetClientClose(client);
> +    virObjectUnref(client);
> +    virObjectUnref(program);
> +
> +    return rv;
> +}
> +
> +
> +static int virLockManagerLockDaemonInquire(virLockManagerPtr lock ATTRIBUTE_UNUSED,
> +                                           char **state,
> +                                           unsigned int flags)
> +{
> +    virCheckFlags(0, -1);
> +
> +    if (state)
> +        *state = NULL;
> +
> +    return 0;
> +}
> +
> +virLockDriver virLockDriverImpl =
> +{
> +    .version = VIR_LOCK_MANAGER_VERSION,
> +    .flags = 0,
> +
> +    .drvInit = virLockManagerLockDaemonInit,
> +    .drvDeinit = virLockManagerLockDaemonDeinit,
> +
> +    .drvNew = virLockManagerLockDaemonNew,
> +    .drvFree = virLockManagerLockDaemonFree,
> +
> +    .drvAddResource = virLockManagerLockDaemonAddResource,
> +
> +    .drvAcquire = virLockManagerLockDaemonAcquire,
> +    .drvRelease = virLockManagerLockDaemonRelease,
> +
> +    .drvInquire = virLockManagerLockDaemonInquire,
> +};
> 

--
libvir-list mailing list
libvir-list@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/libvir-list


[Index of Archives]     [Virt Tools]     [Libvirt Users]     [Lib OS Info]     [Fedora Users]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]