[PATCH 09/10] Add a test case for the socket APIs

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

 



Start of a trivial test case for the socket APIs. Only tests
simple server setup & client connect for UNIX sockets so far

* tests/Makefile.am: Add socket test
* tests/virnetsockettest.c: New test case
* tests/testutils.c: Avoid overriding LIBVIRT_DEBUG settings
* tests/ssh.c: Dumb helper program for SSH tunnelling tests
---
 configure.ac             |    2 +-
 tests/.gitignore         |    2 +
 tests/Makefile.am        |   14 ++-
 tests/ssh.c              |   54 +++++
 tests/testutils.c        |    8 +-
 tests/virnetsockettest.c |  529 ++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 604 insertions(+), 5 deletions(-)
 create mode 100644 tests/ssh.c
 create mode 100644 tests/virnetsockettest.c

diff --git a/configure.ac b/configure.ac
index 81bad91..737a476 100644
--- a/configure.ac
+++ b/configure.ac
@@ -134,7 +134,7 @@ LIBS=$old_libs
 dnl Availability of various common headers (non-fatal if missing).
 AC_CHECK_HEADERS([pwd.h paths.h regex.h sys/syslimits.h sys/un.h \
   sys/poll.h syslog.h mntent.h net/ethernet.h linux/magic.h \
-  sys/un.h sys/syscall.h netinet/tcp.h fnmatch.h])
+  sys/un.h sys/syscall.h netinet/tcp.h fnmatch.h ifaddrs.h])
 
 AC_CHECK_LIB([intl],[gettext],[])
 
diff --git a/tests/.gitignore b/tests/.gitignore
index e3906f0..e272cf6 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -1,6 +1,7 @@
 *.exe
 .deps
 .libs
+ssh
 commandhelper
 commandhelper.log
 commandhelper.pid
@@ -30,6 +31,7 @@ statstest
 storagepoolxml2xmltest
 storagevolxml2xmltest
 virbuftest
+virnetsockettest
 virshtest
 vmx2xmltest
 xencapstest
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 5896442..abf8f30 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -77,7 +77,12 @@ EXTRA_DIST =		\
 
 check_PROGRAMS = virshtest conftest sockettest \
 	nodeinfotest qparamtest virbuftest \
-	commandtest commandhelper seclabeltest
+	commandtest commandhelper seclabeltest \
+	virnetsockettest ssh
+
+# This is a fake SSH we use from virnetsockettest
+ssh_SOURCES = ssh.c
+ssh_LDADD = $(COVERAGE_LDFLAGS)
 
 if WITH_XEN
 check_PROGRAMS += xml2sexprtest sexpr2xmltest \
@@ -159,6 +164,7 @@ TESTS = virshtest \
 	sockettest \
 	commandtest \
 	seclabeltest \
+	virnetsockettest \
 	$(test_scripts)
 
 if WITH_XEN
@@ -361,6 +367,12 @@ commandhelper_SOURCES = \
 commandhelper_CFLAGS = -Dabs_builddir="\"`pwd`\""
 commandhelper_LDADD = $(LDADDS)
 
+virnetsockettest_SOURCES = \
+	virnetsockettest.c testutils.h testutils.c
+virnetsockettest_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\""
+virnetsockettest_LDADD = $(LDADDS)
+
+
 seclabeltest_SOURCES = \
 	seclabeltest.c
 seclabeltest_LDADD = ../src/libvirt_driver_security.la $(LDADDS)
diff --git a/tests/ssh.c b/tests/ssh.c
new file mode 100644
index 0000000..ceaa29f
--- /dev/null
+++ b/tests/ssh.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2006-2010 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+ *
+ * Author: Daniel P. Berrange <berrange@xxxxxxxxxx>
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include "internal.h"
+
+int main(int argc, char **argv)
+{
+    int i;
+    int failConnect = 0; /* Exit -1, with no data on stdout, msg on stderr */
+    int dieEarly = 0;    /* Exit -1, with partial data on stdout, msg on stderr */
+
+    for (i = 1 ; i < argc ; i++) {
+        if (STREQ(argv[i], "nosuchhost"))
+            failConnect = 1;
+        else if (STREQ(argv[i], "crashinghost"))
+            dieEarly = 1;
+    }
+
+    if (failConnect) {
+        fprintf(stderr, "%s", "Cannot connect to host nosuchhost\n");
+        return -1;
+    }
+
+    if (dieEarly) {
+        printf("%s\n", "Hello World");
+        fprintf(stderr, "%s", "Hangup from host\n");
+        return -1;
+    }
+
+    for (i = 1 ; i < argc ; i++)
+        printf("%s%c", argv[i], i == (argc -1) ? '\n' : ' ');
+
+    return 0;
+}
diff --git a/tests/testutils.c b/tests/testutils.c
index 3110457..9b3cf59 100644
--- a/tests/testutils.c
+++ b/tests/testutils.c
@@ -495,9 +495,11 @@ int virtTestMain(int argc,
         return 1;
 
     virLogSetFromEnv();
-    if (virLogDefineOutput(virtTestLogOutput, virtTestLogClose, &testLog,
-                           0, 0, NULL, 0) < 0)
-        return 1;
+    if (!getenv("LIBVIRT_DEBUG") && !virLogGetNbOutputs()) {
+        if (virLogDefineOutput(virtTestLogOutput, virtTestLogClose, &testLog,
+                               0, 0, NULL, 0) < 0)
+            return 1;
+    }
 
 #if TEST_OOM
     if ((oomStr = getenv("VIR_TEST_OOM")) != NULL) {
diff --git a/tests/virnetsockettest.c b/tests/virnetsockettest.c
new file mode 100644
index 0000000..39d496a
--- /dev/null
+++ b/tests/virnetsockettest.c
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2006-2010 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+ *
+ * Author: Daniel P. Berrange <berrange@xxxxxxxxxx>
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <signal.h>
+#ifdef HAVE_IFADDRS_H
+#include <ifaddrs.h>
+#endif
+
+#include "testutils.h"
+#include "util.h"
+#include "virterror_internal.h"
+#include "memory.h"
+#include "logging.h"
+#include "ignore-value.h"
+#include "files.h"
+
+#include "rpc/virnetsocket.h"
+
+#define VIR_FROM_THIS VIR_FROM_RPC
+
+static char *argv0;
+static char cwd[PATH_MAX];
+
+#if HAVE_IFADDRS_H
+#define BASE_PORT 5672
+
+static int
+checkProtocols(bool *hasIPv4, bool *hasIPv6,
+               int *freePort)
+{
+    struct ifaddrs *ifaddr = NULL, *ifa;
+    struct sockaddr_in in4;
+    struct sockaddr_in6 in6;
+    int s4 = -1, s6 = -1;
+    int i;
+    int ret = -1;
+
+    *hasIPv4 = *hasIPv6 = false;
+    *freePort = 0;
+
+    if (getifaddrs(&ifaddr) < 0)
+        goto cleanup;
+
+    for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
+        if (!ifa->ifa_addr)
+            continue;
+
+        if (ifa->ifa_addr->sa_family == AF_INET)
+            *hasIPv4 = true;
+        if (ifa->ifa_addr->sa_family == AF_INET6)
+            *hasIPv6 = true;
+    }
+
+    VIR_DEBUG("Protocols: v4 %d v6 %d\n", *hasIPv4, *hasIPv6);
+
+    freeifaddrs(ifaddr);
+
+    for (i = 0 ; i < 50 ; i++) {
+        int only = 1;
+        if ((s4 = socket(AF_INET, SOCK_STREAM, 0)) < 0)
+            goto cleanup;
+
+        if ((s6 = socket(AF_INET6, SOCK_STREAM, 0)) < 0)
+            goto cleanup;
+
+        if (setsockopt(s6, IPPROTO_IPV6, IPV6_V6ONLY, &only, sizeof(only)) < 0)
+            goto cleanup;
+
+        memset(&in4, 0, sizeof(in4));
+        memset(&in6, 0, sizeof(in6));
+
+        in4.sin_family = AF_INET;
+        in4.sin_port = htons(BASE_PORT + i);
+        in4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+        in6.sin6_family = AF_INET6;
+        in6.sin6_port = htons(BASE_PORT + i);
+        in6.sin6_addr = in6addr_loopback;
+
+        if (bind(s4, (struct sockaddr *)&in4, sizeof(in4)) < 0) {
+            if (errno == EADDRINUSE) {
+                VIR_FORCE_CLOSE(s4);
+                VIR_FORCE_CLOSE(s6);
+                continue;
+            }
+            goto cleanup;
+        }
+        if (bind(s6, (struct sockaddr *)&in6, sizeof(in6)) < 0) {
+            if (errno == EADDRINUSE) {
+                VIR_FORCE_CLOSE(s4);
+                VIR_FORCE_CLOSE(s6);
+                continue;
+            }
+            goto cleanup;
+        }
+
+        *freePort = BASE_PORT + i;
+        break;
+    }
+
+    VIR_DEBUG("Choose port %d\n", *freePort);
+
+    ret = 0;
+
+cleanup:
+    VIR_FORCE_CLOSE(s4);
+    VIR_FORCE_CLOSE(s6);
+    return ret;
+}
+
+
+struct testTCPData {
+    const char *lnode;
+    int port;
+    const char *cnode;
+};
+
+static int testSocketTCPAccept(const void *opaque)
+{
+    virNetSocketPtr *lsock = NULL; /* Listen socket */
+    size_t nlsock = 0, i;
+    virNetSocketPtr ssock = NULL; /* Server socket */
+    virNetSocketPtr csock = NULL; /* Client socket */
+    const struct testTCPData *data = opaque;
+    int ret = -1;
+    char portstr[100];
+
+    snprintf(portstr, sizeof(portstr), "%d", data->port);
+
+    if (virNetSocketNewListenTCP(data->lnode, portstr, &lsock, &nlsock) < 0)
+        goto cleanup;
+
+    for (i = 0 ; i < nlsock ; i++) {
+        if (virNetSocketListen(lsock[i]) < 0)
+            goto cleanup;
+    }
+
+    if (virNetSocketNewConnectTCP(data->cnode, portstr, &csock) < 0)
+        goto cleanup;
+
+    virNetSocketFree(csock);
+
+    for (i = 0 ; i < nlsock ; i++) {
+        if (virNetSocketAccept(lsock[i], &ssock) != -1 && ssock) {
+            char c = 'a';
+            if (virNetSocketWrite(ssock, &c, 1) != -1 &&
+                virNetSocketRead(ssock, &c, 1) != -1) {
+                VIR_DEBUG0("Unexpected client socket present");
+                goto cleanup;
+            }
+        }
+        virNetSocketFree(ssock);
+        ssock = NULL;
+    }
+
+    ret = 0;
+
+cleanup:
+    virNetSocketFree(ssock);
+    for (i = 0 ; i < nlsock ; i++)
+        virNetSocketFree(lsock[i]);
+    VIR_FREE(lsock);
+    return ret;
+}
+#endif
+
+
+#ifndef WIN32
+static int testSocketUNIXAccept(const void *data ATTRIBUTE_UNUSED)
+{
+    virNetSocketPtr lsock = NULL; /* Listen socket */
+    virNetSocketPtr ssock = NULL; /* Server socket */
+    virNetSocketPtr csock = NULL; /* Client socket */
+    int ret = -1;
+
+    char *path;
+    if (argv0[0] == '/') {
+        if (virAsprintf(&path, "%s-test.sock", argv0) < 0) {
+            virReportOOMError();
+            goto cleanup;
+        }
+    } else {
+        if (virAsprintf(&path, "%s/%s-test.sock", cwd, argv0) < 0) {
+            virReportOOMError();
+            goto cleanup;
+        }
+    }
+
+    if (virNetSocketNewListenUNIX(path, 0700, getgid(), &lsock) < 0)
+        goto cleanup;
+
+    if (virNetSocketListen(lsock) < 0)
+        goto cleanup;
+
+    if (virNetSocketNewConnectUNIX(path, false, NULL, &csock) < 0)
+        goto cleanup;
+
+    virNetSocketFree(csock);
+
+    if (virNetSocketAccept(lsock, &ssock) != -1) {
+        char c = 'a';
+        if (virNetSocketWrite(ssock, &c, 1) != -1) {
+            VIR_DEBUG0("Unexpected client socket present");
+            goto cleanup;
+        }
+    }
+
+    ret = 0;
+
+cleanup:
+    VIR_FREE(path);
+    virNetSocketFree(lsock);
+    virNetSocketFree(ssock);
+    return ret;
+}
+
+
+static int testSocketUNIXAddrs(const void *data ATTRIBUTE_UNUSED)
+{
+    virNetSocketPtr lsock = NULL; /* Listen socket */
+    virNetSocketPtr ssock = NULL; /* Server socket */
+    virNetSocketPtr csock = NULL; /* Client socket */
+    int ret = -1;
+
+    char *path;
+    if (argv0[0] == '/') {
+        if (virAsprintf(&path, "%s-test.sock", argv0) < 0) {
+            virReportOOMError();
+            goto cleanup;
+        }
+    } else {
+        if (virAsprintf(&path, "%s/%s-test.sock", cwd, argv0) < 0) {
+            virReportOOMError();
+            goto cleanup;
+        }
+    }
+
+    if (virNetSocketNewListenUNIX(path, 0700, getgid(), &lsock) < 0)
+        goto cleanup;
+
+    if (STRNEQ(virNetSocketLocalAddrString(lsock), "127.0.0.1;0")) {
+        VIR_DEBUG0("Unexpected local address");
+        goto cleanup;
+    }
+
+    if (virNetSocketRemoteAddrString(lsock) != NULL) {
+        VIR_DEBUG0("Unexpected remote address");
+        goto cleanup;
+    }
+
+    if (virNetSocketListen(lsock) < 0)
+        goto cleanup;
+
+    if (virNetSocketNewConnectUNIX(path, false, NULL, &csock) < 0)
+        goto cleanup;
+
+    if (STRNEQ(virNetSocketLocalAddrString(csock), "127.0.0.1;0")) {
+        VIR_DEBUG0("Unexpected local address");
+        goto cleanup;
+    }
+
+    if (STRNEQ(virNetSocketRemoteAddrString(csock), "127.0.0.1;0")) {
+        VIR_DEBUG0("Unexpected local address");
+        goto cleanup;
+    }
+
+
+    if (virNetSocketAccept(lsock, &ssock) < 0) {
+        VIR_DEBUG0("Unexpected client socket missing");
+        goto cleanup;
+    }
+
+
+    if (STRNEQ(virNetSocketLocalAddrString(ssock), "127.0.0.1;0")) {
+        VIR_DEBUG0("Unexpected local address");
+        goto cleanup;
+    }
+
+    if (STRNEQ(virNetSocketRemoteAddrString(ssock), "127.0.0.1;0")) {
+        VIR_DEBUG0("Unexpected local address");
+        goto cleanup;
+    }
+
+
+    ret = 0;
+
+cleanup:
+    VIR_FREE(path);
+    virNetSocketFree(lsock);
+    virNetSocketFree(ssock);
+    virNetSocketFree(csock);
+    return ret;
+}
+
+static int testSocketCommandNormal(const void *data ATTRIBUTE_UNUSED)
+{
+    virNetSocketPtr csock = NULL; /* Client socket */
+    char buf[100];
+    size_t i;
+    int ret = -1;
+    virCommandPtr cmd = virCommandNewArgList("/bin/cat", "/dev/zero", NULL);
+    virCommandAddEnvPassCommon(cmd);
+
+    if (virNetSocketNewConnectCommand(cmd, &csock) < 0)
+        goto cleanup;
+
+    virNetSocketSetBlocking(csock, true);
+
+    if (virNetSocketRead(csock, buf, sizeof(buf)) < 0)
+        goto cleanup;
+
+    for (i = 0 ; i < sizeof(buf) ; i++)
+        if (buf[i] != '\0')
+            goto cleanup;
+
+    ret = 0;
+
+cleanup:
+    virNetSocketFree(csock);
+    return ret;
+}
+
+static int testSocketCommandFail(const void *data ATTRIBUTE_UNUSED)
+{
+    virNetSocketPtr csock = NULL; /* Client socket */
+    char buf[100];
+    int ret = -1;
+    virCommandPtr cmd = virCommandNewArgList("/bin/cat", "/dev/does-not-exist", NULL);
+    virCommandAddEnvPassCommon(cmd);
+
+    if (virNetSocketNewConnectCommand(cmd, &csock) < 0)
+        goto cleanup;
+
+    virNetSocketSetBlocking(csock, true);
+
+    if (virNetSocketRead(csock, buf, sizeof(buf)) == 0)
+        goto cleanup;
+
+    ret = 0;
+
+cleanup:
+    virNetSocketFree(csock);
+    return ret;
+}
+
+struct testSSHData {
+    const char *nodename;
+    const char *service;
+    const char *binary;
+    const char *username;
+    bool noTTY;
+    const char *netcat;
+    const char *path;
+
+    const char *expectOut;
+    bool failConnect;
+    bool dieEarly;
+};
+
+static int testSocketSSH(const void *opaque)
+{
+    const struct testSSHData *data = opaque;
+    virNetSocketPtr csock = NULL; /* Client socket */
+    int ret = -1;
+    char buf[1024];
+
+    if (virNetSocketNewConnectSSH(data->nodename,
+                                  data->service,
+                                  data->binary,
+                                  data->username,
+                                  data->noTTY,
+                                  data->netcat,
+                                  data->path,
+                                  &csock) < 0)
+        goto cleanup;
+
+    virNetSocketSetBlocking(csock, true);
+
+    if (data->failConnect) {
+        if (virNetSocketRead(csock, buf, sizeof(buf)-1) >= 0)
+            goto cleanup;
+    } else {
+        ssize_t rv;
+        if ((rv = virNetSocketRead(csock, buf, sizeof(buf)-1)) < 0)
+            goto cleanup;
+        buf[rv] = '\0';
+
+        if (!STREQ(buf, data->expectOut)) {
+            virtTestDifference(stderr, data->expectOut, buf);
+            goto cleanup;
+        }
+
+        if (data->dieEarly &&
+            virNetSocketRead(csock, buf, sizeof(buf)-1) >= 0)
+            goto cleanup;
+    }
+
+    ret = 0;
+
+cleanup:
+    virNetSocketFree(csock);
+    return ret;
+}
+
+#endif
+
+
+static int
+mymain(int argc, char **argv)
+{
+    int ret = 0;
+#ifdef HAVE_IFADDRS_H
+    bool hasIPv4, hasIPv6;
+    int freePort;
+#endif
+
+    argv0 = argv[0];
+
+    if (argc > 1) {
+        fprintf(stderr, "Usage: %s\n", argv0);
+        return (EXIT_FAILURE);
+    }
+
+    signal(SIGPIPE, SIG_IGN);
+
+    if (!(getcwd(cwd, sizeof(cwd))))
+        return (EXIT_FAILURE);
+
+#ifdef HAVE_IFADDRS_H
+    if (checkProtocols(&hasIPv4, &hasIPv6, &freePort) < 0) {
+        fprintf(stderr, "Cannot identify IPv4/6 availability\n");
+        return (EXIT_FAILURE);
+    }
+
+    if (hasIPv4) {
+        struct testTCPData tcpData = { "127.0.0.1", freePort, "127.0.0.1" };
+        if (virtTestRun("Socket TCP/IPv4 Accept", 1, testSocketTCPAccept, &tcpData) < 0)
+            ret = -1;
+    }
+    if (hasIPv6) {
+        struct testTCPData tcpData = { "::1", freePort, "::1" };
+        if (virtTestRun("Socket TCP/IPv6 Accept", 1, testSocketTCPAccept, &tcpData) < 0)
+            ret = -1;
+    }
+    if (hasIPv6 && hasIPv4) {
+        struct testTCPData tcpData = { NULL, freePort, "127.0.0.1" };
+        if (virtTestRun("Socket TCP/IPv4+IPv6 Accept", 1, testSocketTCPAccept, &tcpData) < 0)
+            ret = -1;
+
+        tcpData.cnode = "::1";
+        if (virtTestRun("Socket TCP/IPv4+IPv6 Accept", 1, testSocketTCPAccept, &tcpData) < 0)
+            ret = -1;
+    }
+#endif
+
+#ifndef WIN32
+    if (virtTestRun("Socket UNIX Accept", 1, testSocketUNIXAccept, NULL) < 0)
+        ret = -1;
+
+    if (virtTestRun("Socket UNIX Addrs", 1, testSocketUNIXAddrs, NULL) < 0)
+        ret = -1;
+
+    if (virtTestRun("Socket External Command /dev/zero", 1, testSocketCommandNormal, NULL) < 0)
+        ret = -1;
+    if (virtTestRun("Socket External Command /dev/does-not-exist", 1, testSocketCommandFail, NULL) < 0)
+        ret = -1;
+
+    struct testSSHData sshData1 = {
+        .nodename = "somehost",
+        .path = "/tmp/socket",
+        .expectOut = "somehost nc -U /tmp/socket\n",
+    };
+    if (virtTestRun("SSH test 1", 1, testSocketSSH, &sshData1) < 0)
+        ret = -1;
+
+    struct testSSHData sshData2 = {
+        .nodename = "somehost",
+        .service = "9000",
+        .username = "fred",
+        .netcat = "netcat",
+        .noTTY = true,
+        .path = "/tmp/socket",
+        .expectOut = "-p 9000 -l fred -T -o BatchMode=yes -e none somehost netcat -U /tmp/socket\n",
+    };
+    if (virtTestRun("SSH test 2", 1, testSocketSSH, &sshData2) < 0)
+        ret = -1;
+
+    struct testSSHData sshData3 = {
+        .nodename = "nosuchhost",
+        .path = "/tmp/socket",
+        .failConnect = true,
+    };
+    if (virtTestRun("SSH test 3", 1, testSocketSSH, &sshData3) < 0)
+        ret = -1;
+
+    struct testSSHData sshData4 = {
+        .nodename = "crashyhost",
+        .path = "/tmp/socket",
+        .expectOut = "crashyhost nc -U /tmp/socket\n",
+        .dieEarly = true,
+    };
+    if (virtTestRun("SSH test 4", 1, testSocketSSH, &sshData4) < 0)
+        ret = -1;
+
+#endif
+
+    return (ret==0 ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+VIRT_TEST_MAIN(mymain)
-- 
1.7.4

--
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]