/* * virsystemd.c: helpers for using systemd APIs * * Copyright (C) 2013, 2014 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 * . * */ #include #define LIBVIRT_VIRSYSTEMDPRIV_H_ALLOW #include "virsystemdpriv.h" #include "virsystemd.h" #include "virbuffer.h" #include "virgdbus.h" #include "virstring.h" #include "viralloc.h" #include "virutil.h" #include "virlog.h" #include "virerror.h" #include "virfile.h" #include "virhash.h" #include "virsocketaddr.h" #define VIR_FROM_THIS VIR_FROM_SYSTEMD VIR_LOG_INIT("util.systemd"); #ifndef MSG_NOSIGNAL # 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) { static const char hextable[16] = "0123456789abcdef"; #define ESCAPE(c) \ do { \ virBufferAddChar(buf, '\\'); \ virBufferAddChar(buf, 'x'); \ virBufferAddChar(buf, hextable[(c >> 4) & 15]); \ virBufferAddChar(buf, hextable[c & 15]); \ } while (0) #define VALID_CHARS \ "0123456789" \ "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ ":-_.\\" if (*name == '.') { ESCAPE(*name); name++; } while (*name) { if (*name == '/') virBufferAddChar(buf, '-'); else if (*name == '-' || *name == '\\' || !strchr(VALID_CHARS, *name)) ESCAPE(*name); else virBufferAddChar(buf, *name); name++; } #undef ESCAPE #undef VALID_CHARS } char *virSystemdMakeScopeName(const char *name, const char *drivername, bool legacy_behaviour) { g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER; virBufferAddLit(&buf, "machine-"); if (legacy_behaviour) { virSystemdEscapeName(&buf, drivername); virBufferAddLit(&buf, "\\x2d"); } virSystemdEscapeName(&buf, name); virBufferAddLit(&buf, ".scope"); return virBufferContentAndReset(&buf); } char *virSystemdMakeSliceName(const char *partition) { g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER; if (*partition == '/') partition++; virSystemdEscapeName(&buf, partition); virBufferAddLit(&buf, ".slice"); return virBufferContentAndReset(&buf); } static int virSystemdHasMachinedCachedValue = -1; static int virSystemdHasLogindCachedValue = -1; /* Reset the cache from tests for testing the underlying dbus calls * as well */ void virSystemdHasMachinedResetCachedValue(void) { virSystemdHasMachinedCachedValue = -1; } void virSystemdHasLogindResetCachedValue(void) { virSystemdHasLogindCachedValue = -1; } /* -2 = machine1 is not supported on this machine * -1 = error * 0 = machine1 is available */ static int virSystemdHasMachined(void) { int ret; int val; val = g_atomic_int_get(&virSystemdHasMachinedCachedValue); if (val != -1) return val; if ((ret = virGDBusIsServiceEnabled("org.freedesktop.machine1")) < 0) { if (ret == -2) g_atomic_int_set(&virSystemdHasMachinedCachedValue, -2); return ret; } if ((ret = virGDBusIsServiceRegistered("org.freedesktop.systemd1")) == -1) return ret; g_atomic_int_set(&virSystemdHasMachinedCachedValue, ret); return ret; } int virSystemdHasLogind(void) { int ret; int val; val = g_atomic_int_get(&virSystemdHasLogindCachedValue); if (val != -1) return val; ret = virGDBusIsServiceEnabled("org.freedesktop.login1"); if (ret < 0) { if (ret == -2) g_atomic_int_set(&virSystemdHasLogindCachedValue, -2); return ret; } if ((ret = virGDBusIsServiceRegistered("org.freedesktop.login1")) == -1) return ret; g_atomic_int_set(&virSystemdHasLogindCachedValue, ret); return ret; } char * virSystemdGetMachineNameByPID(pid_t pid) { GDBusConnection *conn; g_autoptr(GVariant) message = NULL; g_autoptr(GVariant) reply = NULL; g_autoptr(GVariant) gvar = NULL; g_autofree char *object = NULL; char *name = NULL; if (virSystemdHasMachined() < 0) return NULL; if (!(conn = virGDBusGetSystemBus())) return NULL; message = g_variant_new("(u)", pid); if (virGDBusCallMethod(conn, &reply, G_VARIANT_TYPE("(o)"), NULL, "org.freedesktop.machine1", "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", "GetMachineByPID", message) < 0) return NULL; g_variant_get(reply, "(o)", &object); g_variant_unref(reply); reply = NULL; VIR_DEBUG("Domain with pid %lld has object path '%s'", (long long) pid, object); g_variant_unref(message); message = g_variant_new("(ss)", "org.freedesktop.machine1.Machine", "Name"); if (virGDBusCallMethod(conn, &reply, G_VARIANT_TYPE("(v)"), NULL, "org.freedesktop.machine1", object, "org.freedesktop.DBus.Properties", "Get", message) < 0) return NULL; g_variant_get(reply, "(v)", &gvar); g_variant_get(gvar, "s", &name); VIR_DEBUG("Domain with pid %lld has machine name '%s'", (long long) pid, name); return name; } /** * virSystemdCreateMachine: * @name: driver unique name of the machine * @drivername: name of the virt driver * @privileged: whether driver is running privileged or per user * @uuid: globally unique UUID of the machine * @rootdir: root directory of machine filesystem * @pidleader: PID of the leader process * @iscontainer: true if a container, false if a VM * @nnicindexes: number of network interface indexes in list * @nicindexes: list of network interface indexes * @partition: name of the slice to place the machine in * * Returns 0 on success, -1 on fatal error, or -2 if systemd-machine is not available */ int virSystemdCreateMachine(const char *name, const char *drivername, const unsigned char *uuid, const char *rootdir, pid_t pidleader, bool iscontainer, size_t nnicindexes, int *nicindexes, const char *partition, unsigned int maxthreads) { int rc; GDBusConnection *conn; GVariant *guuid; GVariant *gnicindexes; GVariant *gprops; GVariant *message; g_autofree char *creatorname = NULL; g_autofree char *slicename = NULL; g_autofree char *scopename = NULL; static int hasCreateWithNetwork = 1; if ((rc = virSystemdHasMachined()) < 0) return rc; if (!(conn = virGDBusGetSystemBus())) return -1; creatorname = g_strdup_printf("libvirt-%s", drivername); if (partition) { if (!(slicename = virSystemdMakeSliceName(partition))) return -1; } else { slicename = g_strdup(""); } /* * The systemd DBus APIs we're invoking have the * following signature(s) * * CreateMachineWithNetwork(in s name, * in ay id, * in s service, * in s class, * in u leader, * in s root_directory, * in ai nicindexes * in a(sv) scope_properties, * out o path); * * CreateMachine(in s name, * in ay id, * in s service, * in s class, * in u leader, * in s root_directory, * in a(sv) scope_properties, * out o path); * * @name a host unique name for the machine. shows up * in 'ps' listing & similar * * @id: a UUID of the machine, ideally matching /etc/machine-id * for containers * * @service: identifier of the client ie "libvirt-lxc" * * @class: either the string "container" or "vm" depending * on the type of machine * * @leader: main PID of the machine, either the host emulator * process, or the 'init' PID of the container * * @root_directory: the root directory of the container, if * this is known & visible in the host filesystem, or empty string * * @nicindexes: list of network interface indexes for the * host end of the VETH device pairs. * * @scope_properties:an array (not a dict!) of properties that are * passed on to PID 1 when creating a scope unit for your machine. * Will allow initial settings for the cgroup & similar. * * @path: a bus path returned for the machine object created, to * allow further API calls to be made against the object. * */ VIR_DEBUG("Attempting to create machine via systemd"); if (g_atomic_int_get(&hasCreateWithNetwork)) { g_autoptr(virError) error = NULL; error = g_new0(virError, 1); guuid = g_variant_new_fixed_array(G_VARIANT_TYPE("y"), uuid, 16, sizeof(unsigned char)); gnicindexes = g_variant_new_fixed_array(G_VARIANT_TYPE("i"), nicindexes, nnicindexes, sizeof(int)); gprops = g_variant_new_parsed("[('Slice', <%s>)," " ('After', <['libvirtd.service']>)," " ('Before', <['virt-guest-shutdown.target']>)]", slicename); message = g_variant_new("(s@ayssus@ai@a(sv))", name, guuid, creatorname, iscontainer ? "container" : "vm", (unsigned int)pidleader, NULLSTR_EMPTY(rootdir), gnicindexes, gprops); rc = virGDBusCallMethod(conn, NULL, NULL, error, "org.freedesktop.machine1", "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", "CreateMachineWithNetwork", message); g_variant_unref(message); if (rc < 0) return -1; if (error->level == VIR_ERR_ERROR) { if (virGDBusErrorIsUnknownMethod(error)) { VIR_INFO("CreateMachineWithNetwork isn't supported, switching " "to legacy CreateMachine method for systemd-machined"); virResetError(error); g_atomic_int_set(&hasCreateWithNetwork, 0); /* Could re-structure without Using goto, but this * avoids another atomic read which would trigger * another memory barrier */ goto fallback; } virReportErrorObject(error); virResetError(error); return -1; } } else { fallback: guuid = g_variant_new_fixed_array(G_VARIANT_TYPE("y"), uuid, 16, sizeof(unsigned char)); gprops = g_variant_new_parsed("[('Slice', <%s>)," " ('After', <['libvirtd.service']>)," " ('Before', <['virt-guest-shutdown.target']>)]", slicename); message = g_variant_new("(s@ayssus@a(sv))", name, guuid, creatorname, iscontainer ? "container" : "vm", (unsigned int)pidleader, NULLSTR_EMPTY(rootdir), gprops); rc = virGDBusCallMethod(conn, NULL, NULL, NULL, "org.freedesktop.machine1", "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", "CreateMachine", message); g_variant_unref(message); if (rc < 0) return -1; } if (maxthreads > 0) { if (!(scopename = virSystemdMakeScopeName(name, drivername, false))) return -1; gprops = g_variant_new_parsed("[('TasksMax', <%llu>)]", (uint64_t)maxthreads); message = g_variant_new("(sb@a(sv))", scopename, true, gprops); rc = virGDBusCallMethod(conn, NULL, NULL, NULL, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "SetUnitProperties", message); g_variant_unref(message); if (rc < 0) return -1; } return 0; } int virSystemdTerminateMachine(const char *name) { int rc; GDBusConnection *conn; g_autoptr(GVariant) message = NULL; g_autoptr(virError) error = NULL; if (!name) return 0; if ((rc = virSystemdHasMachined()) < 0) return rc; if (!(conn = virGDBusGetSystemBus())) return -1; error = g_new0(virError, 1); /* * The systemd DBus API we're invoking has the * following signature * * TerminateMachine(in s name); * * @name a host unique name for the machine. shows up * in 'ps' listing & similar */ message = g_variant_new("(s)", name); VIR_DEBUG("Attempting to terminate machine via systemd"); if (virGDBusCallMethod(conn, NULL, NULL, error, "org.freedesktop.machine1", "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", "TerminateMachine", message) < 0) return -1; if (error->level == VIR_ERR_ERROR && STRNEQ_NULLABLE("org.freedesktop.machine1.NoSuchMachine", error->str1)) { virReportErrorObject(error); return -1; } return 0; } void virSystemdNotifyStartup(void) { #ifndef WIN32 const char *path; const char *msg = "READY=1"; int fd; struct sockaddr_un un = { .sun_family = AF_UNIX, }; struct iovec iov = { .iov_base = (char *)msg, .iov_len = strlen(msg), }; struct msghdr mh = { .msg_name = &un, .msg_iov = &iov, .msg_iovlen = 1, }; if (!(path = getenv("NOTIFY_SOCKET"))) { VIR_DEBUG("Skipping systemd notify, not requested"); return; } /* NB sun_path field is *not* NUL-terminated, hence >, not >= */ if (strlen(path) > sizeof(un.sun_path)) { VIR_WARN("Systemd notify socket path '%s' too long", path); return; } memcpy(un.sun_path, path, strlen(path)); if (un.sun_path[0] == '@') un.sun_path[0] = '\0'; mh.msg_namelen = offsetof(struct sockaddr_un, sun_path) + strlen(path); fd = socket(AF_UNIX, SOCK_DGRAM, 0); if (fd < 0) { VIR_WARN("Unable to create socket FD"); return; } if (sendmsg(fd, &mh, MSG_NOSIGNAL) < 0) VIR_WARN("Failed to notify systemd"); VIR_FORCE_CLOSE(fd); #endif /* !WIN32 */ } static int virSystemdPMSupportTarget(const char *methodName, bool *result) { int rc; GDBusConnection *conn; g_autoptr(GVariant) reply = NULL; char *response; if ((rc = virSystemdHasLogind()) < 0) return rc; if (!(conn = virGDBusGetSystemBus())) return -1; if (virGDBusCallMethod(conn, &reply, G_VARIANT_TYPE("(s)"), NULL, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", methodName, NULL) < 0) return -1; g_variant_get(reply, "(&s)", &response); *result = STREQ("yes", response) || STREQ("challenge", response); return 0; } int virSystemdCanSuspend(bool *result) { return virSystemdPMSupportTarget("CanSuspend", result); } int virSystemdCanHibernate(bool *result) { return virSystemdPMSupportTarget("CanHibernate", result); } int virSystemdCanHybridSleep(bool *result) { return virSystemdPMSupportTarget("CanHybridSleep", result); } static void virSystemdActivationEntryFree(void *data) { virSystemdActivationEntryPtr ent = data; size_t i; VIR_DEBUG("Closing activation FDs"); 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) { ent = g_new0(virSystemdActivationEntry, 1); ent->fds = g_new0(int, 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); 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; } VIR_DEBUG("Got socket family %d for FD %d", addr.data.sa.sa_family, nextfd); 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) { VIR_DEBUG("Expect %d got %d", map[i].port, ntohs(addr.data.inet4.sin_port)); if (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) { VIR_DEBUG("Expect %d got %d", map[i].port, ntohs(addr.data.inet6.sin6_port)); if (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) { VIR_DEBUG("Expect %s got %s", map[i].path, addr.data.un.sun_path); if (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; } #ifndef WIN32 /** * virSystemdGetListenFDs: * * Parse LISTEN_PID and LISTEN_FDS passed from caller. * * Returns number of passed FDs. */ static unsigned int virSystemdGetListenFDs(void) { const char *pidstr; const char *fdstr; size_t i = 0; unsigned long long procid; unsigned int nfds; VIR_DEBUG("Setting up networking from caller"); if (!(pidstr = getenv("LISTEN_PID"))) { VIR_DEBUG("No LISTEN_PID from caller"); return 0; } if (virStrToLong_ull(pidstr, NULL, 10, &procid) < 0) { VIR_DEBUG("Malformed LISTEN_PID from caller %s", pidstr); return 0; } if ((pid_t)procid != getpid()) { VIR_DEBUG("LISTEN_PID %s is not for us %lld", pidstr, (long long) getpid()); return 0; } if (!(fdstr = getenv("LISTEN_FDS"))) { VIR_DEBUG("No LISTEN_FDS from caller"); return 0; } if (virStrToLong_ui(fdstr, NULL, 10, &nfds) < 0) { VIR_DEBUG("Malformed LISTEN_FDS from caller %s", fdstr); return 0; } g_unsetenv("LISTEN_PID"); g_unsetenv("LISTEN_FDS"); VIR_DEBUG("Got %u file descriptors", nfds); for (i = 0; i < nfds; i++) { int fd = STDERR_FILENO + i + 1; VIR_DEBUG("Disabling inheritance of passed FD %d", fd); if (virSetInherit(fd, false) < 0) VIR_WARN("Couldn't disable inheritance of passed FD %d", fd); } return nfds; } #else /* WIN32 */ static unsigned int virSystemdGetListenFDs(void) { return 0; } #endif /* WIN32 */ static virSystemdActivationPtr virSystemdActivationNew(virSystemdActivationMap *map, size_t nmap, int nfds) { virSystemdActivationPtr act; const char *fdnames; VIR_DEBUG("Activated with %d FDs", nfds); act = g_new0(virSystemdActivation, 1); if (!(act->fds = virHashCreate(10, virSystemdActivationEntryFree))) goto error; fdnames = getenv("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 service 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 = virSystemdGetListenFDs()) < 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); }