When receiving multiple FDs from systemd during service activation it is
neccessary to identify which purpose each FD is used for. While this
could be inferred by looking for the specific IP ports or UNIX socket
paths, this requires the systemd config to always match what is expected
by the code. Using systemd FD names we can remove this restriction and
simply identify FDs based on an arbitrary name.
The FD names are passed by systemd in the LISTEN_FDNAMES env variable
which is populated with the socket unit file names, unless overriden
by using the FileDescriptorName setting.
This is supported since the system 227 release and unfortunately RHEL7
lacks this version. Thus the code has some back compat support whereby
we look at the TCP ports or the UNIX socket paths to identify what
socket maps to which name. This back compat code is written such that
is it easly deleted when we are able to mandate newer systemd.
Reviewed-by: Ján Tomko <jtomko(a)redhat.com>
Signed-off-by: Daniel P. Berrangé <berrange(a)redhat.com>
---
src/libvirt_private.syms | 5 +
src/util/virsystemd.c | 362 +++++++++++++++++++++++++++++++++++++++
src/util/virsystemd.h | 32 ++++
tests/virsystemdtest.c | 169 ++++++++++++++++++
4 files changed, 568 insertions(+)
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index 02d5b7acce..a19ba1d798 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -3086,10 +3086,15 @@ virSysinfoReadS390;
# util/virsystemd.h
+virSystemdActivationClaimFDs;
+virSystemdActivationComplete;
+virSystemdActivationFree;
+virSystemdActivationHasName;
virSystemdCanHibernate;
virSystemdCanHybridSleep;
virSystemdCanSuspend;
virSystemdCreateMachine;
+virSystemdGetActivation;
virSystemdGetMachineNameByPID;
virSystemdHasMachinedResetCachedValue;
virSystemdMakeScopeName;
diff --git a/src/util/virsystemd.c b/src/util/virsystemd.c
index 3f03e3bd63..ae8401343d 100644
--- a/src/util/virsystemd.c
+++ b/src/util/virsystemd.c
@@ -39,6 +39,8 @@
#include "virlog.h"
#include "virerror.h"
#include "virfile.h"
+#include "virhash.h"
+#include "virsocketaddr.h"
#define VIR_FROM_THIS VIR_FROM_SYSTEMD
@@ -48,6 +50,18 @@ VIR_LOG_INIT("util.systemd");
# define MSG_NOSIGNAL 0
#endif
+struct _virSystemdActivation {
+ virHashTablePtr fds;
+};
+
+typedef struct _virSystemdActivationEntry virSystemdActivationEntry;
+typedef virSystemdActivationEntry *virSystemdActivationEntryPtr;
+
+struct _virSystemdActivationEntry {
+ int *fds;
+ size_t nfds;
+};
+
static void virSystemdEscapeName(virBufferPtr buf,
const char *name)
{
@@ -561,3 +575,351 @@ int virSystemdCanHybridSleep(bool *result)
{
return virSystemdPMSupportTarget("CanHybridSleep", result);
}
+
+
+static void
+virSystemdActivationEntryFree(void *data, const void *name)
+{
+ virSystemdActivationEntryPtr ent = data;
+ size_t i;
+
+ VIR_DEBUG("Closing activation FDs for %s", (const char *)name);
+ for (i = 0; i < ent->nfds; i++) {
+ VIR_DEBUG("Closing activation FD %d", ent->fds[i]);
+ VIR_FORCE_CLOSE(ent->fds[i]);
+ }
+
+ VIR_FREE(ent->fds);
+ VIR_FREE(ent);
+}
+
+
+static int
+virSystemdActivationAddFD(virSystemdActivationPtr act,
+ const char *name,
+ int fd)
+{
+ virSystemdActivationEntryPtr ent = virHashLookup(act->fds, name);
+
+ if (!ent) {
+ if (VIR_ALLOC(ent) < 0)
+ return -1;
+
+ if (VIR_ALLOC_N(ent->fds, 1) < 0) {
+ virSystemdActivationEntryFree(ent, name);
+ return -1;
+ }
+
+ ent->fds[ent->nfds++] = fd;
+
+ VIR_DEBUG("Record first FD %d with name %s", fd, name);
+ if (virHashAddEntry(act->fds, name, ent) < 0) {
+ virSystemdActivationEntryFree(ent, name);
+ return -1;
+ }
+
+ return 0;
+ }
+
+ if (VIR_EXPAND_N(ent->fds, ent->nfds, 1) < 0)
+ return -1;
+
+ VIR_DEBUG("Record extra FD %d with name %s", fd, name);
+ ent->fds[ent->nfds - 1] = fd;
+
+ return 0;
+}
+
+
+static int
+virSystemdActivationInitFromNames(virSystemdActivationPtr act,
+ int nfds,
+ const char *fdnames)
+{
+ VIR_AUTOSTRINGLIST fdnamelistptr = NULL;
+ char **fdnamelist;
+ size_t nfdnames;
+ size_t i;
+ int nextfd = STDERR_FILENO + 1;
+
+ VIR_DEBUG("FD names %s", fdnames);
+
+ if (!(fdnamelistptr = virStringSplitCount(fdnames, ":", 0,
&nfdnames)))
+ goto error;
+
+ if (nfdnames != nfds) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Expecting %d FD names but got %zu"),
+ nfds, nfdnames);
+ goto error;
+ }
+
+ fdnamelist = fdnamelistptr;
+ while (nfds) {
+ if (virSystemdActivationAddFD(act, *fdnamelist, nextfd) < 0)
+ goto error;
+
+ fdnamelist++;
+ nextfd++;
+ nfds--;
+ }
+
+ return 0;
+
+ error:
+ for (i = 0; i < nfds; i++) {
+ int fd = nextfd + i;
+ VIR_FORCE_CLOSE(fd);
+ }
+ return -1;
+}
+
+
+/*
+ * Back compat for systemd < v227 which lacks LISTEN_FDNAMES.
+ * Delete when min systemd is increased ie RHEL7 dropped
+ */
+static int
+virSystemdActivationInitFromMap(virSystemdActivationPtr act,
+ int nfds,
+ virSystemdActivationMap *map,
+ size_t nmap)
+{
+ int nextfd = STDERR_FILENO + 1;
+ size_t i;
+
+ while (nfds) {
+ virSocketAddr addr;
+ const char *name = NULL;
+
+ memset(&addr, 0, sizeof(addr));
+
+ addr.len = sizeof(addr.data);
+ if (getsockname(nextfd, &addr.data.sa, &addr.len) < 0) {
+ virReportSystemError(errno, "%s", _("Unable to get local
socket name"));
+ goto error;
+ }
+
+ for (i = 0; i < nmap && !name; i++) {
+ if (map[i].name == NULL)
+ continue;
+
+ if (addr.data.sa.sa_family == AF_INET) {
+ if (map[i].family == AF_INET &&
+ addr.data.inet4.sin_port == htons(map[i].port))
+ name = map[i].name;
+ } else if (addr.data.sa.sa_family == AF_INET6) {
+ /* NB use of AF_INET here is correct. The "map" struct
+ * only refers to AF_INET. The socket may be AF_INET
+ * or AF_INET6
+ */
+ if (map[i].family == AF_INET &&
+ addr.data.inet6.sin6_port == htons(map[i].port))
+ name = map[i].name;
+#ifndef WIN32
+ } else if (addr.data.sa.sa_family == AF_UNIX) {
+ if (map[i].family == AF_UNIX &&
+ STREQLEN(map[i].path,
+ addr.data.un.sun_path,
+ sizeof(addr.data.un.sun_path)))
+ name = map[i].name;
+#endif
+ } else {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Unexpected socket family %d"),
+ addr.data.sa.sa_family);
+ goto error;
+ }
+ }
+
+ if (!name) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Cannot find name for FD %d socket family %d"),
+ nextfd, addr.data.sa.sa_family);
+ goto error;
+ }
+
+ if (virSystemdActivationAddFD(act, name, nextfd) < 0)
+ goto error;
+
+ nfds--;
+ nextfd++;
+ }
+
+ return 0;
+
+ error:
+ for (i = 0; i < nfds; i++) {
+ int fd = nextfd + i;
+ VIR_FORCE_CLOSE(fd);
+ }
+ return -1;
+}
+
+
+static virSystemdActivationPtr
+virSystemdActivationNew(virSystemdActivationMap *map,
+ size_t nmap,
+ int nfds)
+{
+ virSystemdActivationPtr act;
+ const char *fdnames;
+
+ VIR_DEBUG("Activated with %d FDs", nfds);
+ if (VIR_ALLOC(act) < 0)
+ return NULL;
+
+ if (!(act->fds = virHashCreate(10, virSystemdActivationEntryFree)))
+ goto error;
+
+ fdnames = virGetEnvAllowSUID("LISTEN_FDNAMES");
+ if (fdnames) {
+ if (virSystemdActivationInitFromNames(act, nfds, fdnames) < 0)
+ goto error;
+ } else {
+ if (virSystemdActivationInitFromMap(act, nfds, map, nmap) < 0)
+ goto error;
+ }
+
+ VIR_DEBUG("Created activation object for %d FDs", nfds);
+ return act;
+
+ error:
+ virSystemdActivationFree(&act);
+ return NULL;
+}
+
+
+/**
+ * virSystemdGetActivation:
+ * @map: mapping of socket addresses to names
+ * @nmap: number of entries in @map
+ * @act: filled with allocated activation object
+ *
+ * Acquire an object for handling systemd activation.
+ * If no activation FDs have been provided the returned object
+ * will be NULL, indicating normal sevice setup can be performed
+ * If the returned object is non-NULL then at least one file
+ * descriptor will be present. No normal service setup should
+ * be performed.
+ *
+ * Returns: 0 on success, -1 on failure
+ */
+int
+virSystemdGetActivation(virSystemdActivationMap *map,
+ size_t nmap,
+ virSystemdActivationPtr *act)
+{
+ int nfds = 0;
+
+ if ((nfds = virGetListenFDs()) < 0)
+ return -1;
+
+ if (nfds == 0) {
+ VIR_DEBUG("No activation FDs present");
+ *act = NULL;
+ return 0;
+ }
+
+ *act = virSystemdActivationNew(map, nmap, nfds);
+ return 0;
+}
+
+
+/**
+ * virSystemdActivationHasName:
+ * @act: the activation object
+ * @name: the file descriptor name
+ *
+ * Check whether there is a file descriptor present
+ * for the requested name.
+ *
+ * Returns: true if a FD is present, false otherwise
+ */
+bool
+virSystemdActivationHasName(virSystemdActivationPtr act,
+ const char *name)
+{
+ return virHashLookup(act->fds, name) != NULL;
+}
+
+
+/**
+ * virSystemdActivationComplete:
+ * @act: the activation object
+ *
+ * Indicate that processing of activation has been
+ * completed. All provided file descriptors should
+ * have been claimed. If any are unclaimed then
+ * an error will be reported
+ *
+ * Returns: 0 on success, -1 if some FDs are unclaimed
+ */
+int
+virSystemdActivationComplete(virSystemdActivationPtr act)
+{
+ if (virHashSize(act->fds) != 0) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Some activation file descriptors are unclaimed"));
+ return -1;
+ }
+
+ return 0;
+}
+
+
+/**
+ * virSystemdActivationClaimFDs:
+ * @act: the activation object
+ * @name: the file descriptor name
+ * @fds: to be filled with claimed FDs
+ * @nfds: to be filled with number of FDs in @fds
+ *
+ * Claims the file descriptors associated with
+ * @name.
+ *
+ * The caller is responsible for closing all
+ * returned file descriptors when they are no
+ * longer required. The caller must also free
+ * the array memory in @fds.
+ */
+void
+virSystemdActivationClaimFDs(virSystemdActivationPtr act,
+ const char *name,
+ int **fds,
+ size_t *nfds)
+{
+ virSystemdActivationEntryPtr ent = virHashSteal(act->fds, name);
+
+ if (!ent) {
+ *fds = NULL;
+ *nfds = 0;
+ VIR_DEBUG("No FD with name %s", name);
+ return;
+ }
+
+ VIR_DEBUG("Found %zu FDs with name %s", ent->nfds, name);
+ *fds = ent->fds;
+ *nfds = ent->nfds;
+
+ VIR_FREE(ent);
+}
+
+
+/**
+ * virSystemdActivationFree:
+ * @act: the activation object
+ *
+ * Free memory and close unclaimed file descriptors
+ * associated with the activation object
+ */
+void
+virSystemdActivationFree(virSystemdActivationPtr *act)
+{
+ if (!*act)
+ return;
+
+ virHashFree((*act)->fds);
+
+ VIR_FREE(*act);
+}
diff --git a/src/util/virsystemd.h b/src/util/virsystemd.h
index db4ecbff60..5d56c78835 100644
--- a/src/util/virsystemd.h
+++ b/src/util/virsystemd.h
@@ -23,6 +23,20 @@
#include "internal.h"
+typedef struct _virSystemdActivation virSystemdActivation;
+typedef virSystemdActivation *virSystemdActivationPtr;
+
+/*
+ * Back compat for systemd < v227 which lacks LISTEN_FDNAMES.
+ * Delete when min systemd is increased ie RHEL7 dropped
+ */
+typedef struct _virSystemdActivationMap {
+ const char *name;
+ int family;
+ int port; /* if family == AF_INET/AF_INET6 */
+ const char *path; /* if family == AF_UNIX */
+} virSystemdActivationMap;
+
char *virSystemdMakeScopeName(const char *name,
const char *drivername,
bool legacy_behaviour);
@@ -49,3 +63,21 @@ int virSystemdCanHibernate(bool *result);
int virSystemdCanHybridSleep(bool *result);
char *virSystemdGetMachineNameByPID(pid_t pid);
+
+int virSystemdGetActivation(virSystemdActivationMap *map,
+ size_t nmap,
+ virSystemdActivationPtr *act);
+
+bool virSystemdActivationHasName(virSystemdActivationPtr act,
+ const char *name);
+
+int virSystemdActivationComplete(virSystemdActivationPtr act);
+
+void virSystemdActivationClaimFDs(virSystemdActivationPtr act,
+ const char *name,
+ int **fds,
+ size_t *nfds);
+
+void virSystemdActivationFree(virSystemdActivationPtr *act);
+
+#define virSystemdActivationAutoPtrFree virSystemdActivationFree
diff --git a/tests/virsystemdtest.c b/tests/virsystemdtest.c
index 82c02decd1..586c512fca 100644
--- a/tests/virsystemdtest.c
+++ b/tests/virsystemdtest.c
@@ -31,6 +31,8 @@
# include "virdbus.h"
# include "virlog.h"
# include "virmock.h"
+# include "rpc/virnetsocket.h"
+# include "intprops.h"
# define VIR_FROM_THIS VIR_FROM_NONE
VIR_LOG_INIT("tests.systemdtest");
@@ -507,6 +509,166 @@ static int testPMSupportSystemdNotRunning(const void *opaque)
return 0;
}
+
+static int
+testActivationCreateFDs(virNetSocketPtr *sockUNIX,
+ virNetSocketPtr **sockIP,
+ size_t *nsockIP)
+{
+ *sockUNIX = NULL;
+ *sockIP = NULL;
+ *nsockIP = 0;
+
+ if (virNetSocketNewListenUNIX("virsystemdtest.sock",
+ 0777,
+ 0,
+ 0,
+ sockUNIX) < 0)
+ return -1;
+
+ if (virNetSocketNewListenTCP("localhost",
+ NULL,
+ AF_UNSPEC,
+ sockIP,
+ nsockIP) < 0) {
+ virObjectUnref(*sockUNIX);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static int
+testActivation(bool useNames)
+{
+ virNetSocketPtr sockUNIX;
+ virNetSocketPtr *sockIP;
+ size_t nsockIP;
+ int ret = -1;
+ size_t i;
+ const char *names2 = "demo-unix.socket:demo-ip.socket";
+ const char *names3 = "demo-unix.socket:demo-ip.socket:demo-ip.socket";
+ char nfdstr[INT_BUFSIZE_BOUND(size_t)];
+ char pidstr[INT_BUFSIZE_BOUND(pid_t)];
+ virSystemdActivationMap map[2];
+ int *fds = NULL;
+ size_t nfds = 0;
+ VIR_AUTOPTR(virSystemdActivation) act = NULL;
+
+ if (testActivationCreateFDs(&sockUNIX, &sockIP, &nsockIP) < 0)
+ return -1;
+
+ if (nsockIP != 1 && nsockIP != 2) {
+ fprintf(stderr, "Got %zu IP sockets but expected only 1 or 2\n",
nsockIP);
+ goto cleanup;
+ }
+
+ snprintf(nfdstr, sizeof(nfdstr), "%zu", 1 + nsockIP);
+ snprintf(pidstr, sizeof(pidstr), "%lld", (long long)getpid());
+
+ setenv("LISTEN_FDS", nfdstr, 1);
+ setenv("LISTEN_PID", pidstr, 1);
+
+ if (useNames)
+ setenv("LISTEN_FDNAMES", nsockIP == 1 ? names2 : names3, 1);
+ else
+ unsetenv("LISTEN_FDNAMES");
+
+ map[0].name = "demo-unix.socket";
+ map[0].family = AF_UNIX;
+ map[0].path = virNetSocketGetPath(sockUNIX);
+
+ map[1].name = "demo-ip.socket";
+ map[1].family = AF_INET;
+ map[1].port = virNetSocketGetPort(sockIP[0]);
+
+ if (virSystemdGetActivation(map, ARRAY_CARDINALITY(map), &act) < 0)
+ goto cleanup;
+
+ if (act == NULL) {
+ fprintf(stderr, "Activation object was not created: %s",
virGetLastErrorMessage());
+ goto cleanup;
+ }
+
+ if (virSystemdActivationComplete(act) == 0) {
+ fprintf(stderr, "Activation did not report unclaimed FDs");
+ goto cleanup;
+ }
+
+ virSystemdActivationClaimFDs(act, "demo-unix.socket", &fds,
&nfds);
+
+ if (nfds != 1) {
+ fprintf(stderr, "Expected 1 UNIX fd, but got %zu\n", nfds);
+ goto cleanup;
+ }
+ VIR_FREE(fds);
+
+ virSystemdActivationClaimFDs(act, "demo-ip.socket", &fds, &nfds);
+
+ if (nfds != nsockIP) {
+ fprintf(stderr, "Expected %zu IP fd, but got %zu\n", nsockIP, nfds);
+ goto cleanup;
+ }
+ VIR_FREE(fds);
+
+ virSystemdActivationClaimFDs(act, "demo-ip-alt.socket", &fds,
&nfds);
+
+ if (nfds != 0) {
+ fprintf(stderr, "Expected 0 IP fd, but got %zu\n", nfds);
+ goto cleanup;
+ }
+
+ if (virSystemdActivationComplete(act) < 0) {
+ fprintf(stderr, "Action was not complete: %s\n",
virGetLastErrorMessage());
+ goto cleanup;
+ }
+
+ ret = 0;
+ cleanup:
+ virObjectUnref(sockUNIX);
+ for (i = 0; i < nsockIP; i++)
+ virObjectUnref(sockIP[i]);
+ VIR_FREE(sockIP);
+ VIR_FREE(fds);
+ return ret;
+}
+
+
+static int
+testActivationEmpty(const void *opaque ATTRIBUTE_UNUSED)
+{
+ virSystemdActivationPtr act;
+
+ unsetenv("LISTEN_FDS");
+
+ if (virSystemdGetActivation(NULL, 0, &act) < 0)
+ return -1;
+
+ if (act != NULL) {
+ fprintf(stderr, "Unexpectedly got activation object");
+ virSystemdActivationFree(&act);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static int
+testActivationFDNames(const void *opaque ATTRIBUTE_UNUSED)
+{
+ return testActivation(true);
+}
+
+
+static int
+testActivationFDAddrs(const void *opaque ATTRIBUTE_UNUSED)
+{
+ return testActivation(false);
+}
+
+
static int
mymain(void)
{
@@ -598,6 +760,13 @@ mymain(void)
TESTS_PM_SUPPORT_HELPER("canHibernate", &virSystemdCanHibernate);
TESTS_PM_SUPPORT_HELPER("canHybridSleep", &virSystemdCanHybridSleep);
+ if (virTestRun("Test activation empty", testActivationEmpty, NULL) < 0)
+ ret = -1;
+ if (virTestRun("Test activation names", testActivationFDNames, NULL) <
0)
+ ret = -1;
+ if (virTestRun("Test activation addrs", testActivationFDAddrs, NULL) <
0)
+ ret = -1;
+
return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
--
2.21.0