Merge pull request #8620 from poettering/portablectl

an implementation of the "portable services" concept
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2018-05-25 10:48:12 +02:00
committed by GitHub
98 changed files with 6332 additions and 716 deletions
+86 -42
View File
@@ -21,18 +21,28 @@
#include "macro.h"
#include "missing.h"
#include "path-util.h"
#include "set.h"
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "util.h"
static int files_add(Hashmap *h, const char *suffix, const char *root, unsigned flags, const char *path) {
static int files_add(
Hashmap *h,
Set *masked,
const char *suffix,
const char *root,
unsigned flags,
const char *path) {
_cleanup_closedir_ DIR *dir = NULL;
const char *dirpath;
struct dirent *de;
int r;
assert(h);
assert((flags & CONF_FILES_FILTER_MASKED) == 0 || masked);
assert(path);
dirpath = prefix_roota(root, path);
@@ -41,62 +51,89 @@ static int files_add(Hashmap *h, const char *suffix, const char *root, unsigned
if (!dir) {
if (errno == ENOENT)
return 0;
return -errno;
return log_debug_errno(errno, "Failed to open directory '%s': %m", dirpath);
}
FOREACH_DIRENT(de, dir, return -errno) {
char *p;
struct stat st;
char *p, *key;
if (!dirent_is_file_with_suffix(de, suffix)) {
log_debug("Ignoring %s/%s, because it's not a regular file with suffix %s.", dirpath, de->d_name, strna(suffix));
/* Does this match the suffix? */
if (suffix && !endswith(de->d_name, suffix))
continue;
/* Has this file already been found in an earlier directory? */
if (hashmap_contains(h, de->d_name)) {
log_debug("Skipping overridden file '%s/%s'.", dirpath, de->d_name);
continue;
}
if (flags & CONF_FILES_EXECUTABLE) {
struct stat st;
/* As requested: check if the file is marked exectuable. Note that we don't check access(X_OK)
* here, as we care about whether the file is marked executable at all, and not whether it is
* executable for us, because if such errors are stuff we should log about. */
/* Has this been masked in an earlier directory? */
if ((flags & CONF_FILES_FILTER_MASKED) && set_contains(masked, de->d_name)) {
log_debug("File '%s/%s' is masked by previous entry.", dirpath, de->d_name);
continue;
}
/* Read file metadata if we shall validate the check for file masks, for node types or whether the node is marked executable. */
if (flags & (CONF_FILES_FILTER_MASKED|CONF_FILES_REGULAR|CONF_FILES_DIRECTORY|CONF_FILES_EXECUTABLE))
if (fstatat(dirfd(dir), de->d_name, &st, 0) < 0) {
log_debug_errno(errno, "Failed to stat %s/%s, ignoring: %m", dirpath, de->d_name);
log_debug_errno(errno, "Failed to stat '%s/%s', ignoring: %m", dirpath, de->d_name);
continue;
}
if (!null_or_empty(&st)) {
/* A mask is a symlink to /dev/null or an empty file. It does not even
* have to be executable. Other entries must be regular executable files
* or symlinks to them. */
if (S_ISREG(st.st_mode)) {
if ((st.st_mode & 0111) == 0) { /* not executable */
log_debug("Ignoring %s/%s, as it is not marked executable.",
dirpath, de->d_name);
continue;
}
} else {
log_debug("Ignoring %s/%s, as it is neither a regular file nor a mask.",
dirpath, de->d_name);
continue;
}
/* Is this a masking entry? */
if ((flags & CONF_FILES_FILTER_MASKED))
if (null_or_empty(&st)) {
/* Mark this one as masked */
r = set_put_strdup(masked, de->d_name);
if (r < 0)
return r;
log_debug("File '%s/%s' is a mask.", dirpath, de->d_name);
continue;
}
/* Does this node have the right type? */
if (flags & (CONF_FILES_REGULAR|CONF_FILES_DIRECTORY))
if (!((flags & CONF_FILES_DIRECTORY) && S_ISDIR(st.st_mode)) &&
!((flags & CONF_FILES_REGULAR) && S_ISREG(st.st_mode))) {
log_debug("Ignoring '%s/%s', as it is not a of the right type.", dirpath, de->d_name);
continue;
}
/* Does this node have the executable bit set? */
if (flags & CONF_FILES_EXECUTABLE)
/* As requested: check if the file is marked exectuable. Note that we don't check access(X_OK)
* here, as we care about whether the file is marked executable at all, and not whether it is
* executable for us, because if so, such errors are stuff we should log about. */
if ((st.st_mode & 0111) == 0) { /* not executable */
log_debug("Ignoring '%s/%s', as it is not marked executable.", dirpath, de->d_name);
continue;
}
if (flags & CONF_FILES_BASENAME) {
p = strdup(de->d_name);
if (!p)
return -ENOMEM;
key = p;
} else {
p = strjoin(dirpath, "/", de->d_name);
if (!p)
return -ENOMEM;
key = basename(p);
}
p = strjoin(dirpath, "/", de->d_name);
if (!p)
return -ENOMEM;
r = hashmap_put(h, basename(p), p);
if (r == -EEXIST) {
log_debug("Skipping overridden file: %s.", p);
free(p);
} else if (r < 0) {
free(p);
return r;
} else if (r == 0) {
log_debug("Duplicate file %s", p);
r = hashmap_put(h, key, p);
if (r < 0) {
free(p);
return log_debug_errno(r, "Failed to add item to hashmap: %m");
}
assert(r > 0);
}
return 0;
@@ -112,6 +149,7 @@ static int base_cmp(const void *a, const void *b) {
static int conf_files_list_strv_internal(char ***strv, const char *suffix, const char *root, unsigned flags, char **dirs) {
_cleanup_hashmap_free_ Hashmap *fh = NULL;
_cleanup_set_free_free_ Set *masked = NULL;
char **files, **p;
int r;
@@ -121,12 +159,18 @@ static int conf_files_list_strv_internal(char ***strv, const char *suffix, const
if (!path_strv_resolve_uniq(dirs, root))
return -ENOMEM;
fh = hashmap_new(&string_hash_ops);
fh = hashmap_new(&path_hash_ops);
if (!fh)
return -ENOMEM;
if (flags & CONF_FILES_FILTER_MASKED) {
masked = set_new(&path_hash_ops);
if (!masked)
return -ENOMEM;
}
STRV_FOREACH(p, dirs) {
r = files_add(fh, suffix, root, flags, *p);
r = files_add(fh, masked, suffix, root, flags, *p);
if (r == -ENOMEM)
return r;
if (r < 0)
+5 -1
View File
@@ -9,7 +9,11 @@
***/
enum {
CONF_FILES_EXECUTABLE = 1,
CONF_FILES_EXECUTABLE = 1U << 0,
CONF_FILES_REGULAR = 1U << 1,
CONF_FILES_DIRECTORY = 1U << 2,
CONF_FILES_BASENAME = 1U << 3,
CONF_FILES_FILTER_MASKED = 1U << 4,
};
int conf_files_list(char ***ret, const char *suffix, const char *root, unsigned flags, const char *dir, ...);
+1 -1
View File
@@ -89,7 +89,7 @@ static int do_execute(
* If callbacks is nonnull, execution is serial. Otherwise, we default to parallel.
*/
r = conf_files_list_strv(&paths, NULL, NULL, CONF_FILES_EXECUTABLE, (const char* const*) directories);
r = conf_files_list_strv(&paths, NULL, NULL, CONF_FILES_EXECUTABLE|CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char* const*) directories);
if (r < 0)
return r;
+199
View File
@@ -12,10 +12,13 @@
#include <sys/stat.h>
#include <unistd.h>
#include "alloc-util.h"
#include "copy.h"
#include "dirent-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
#include "io-util.h"
#include "macro.h"
#include "memfd-util.h"
#include "missing.h"
@@ -562,6 +565,202 @@ try_dev_shm_without_o_tmpfile:
return -EOPNOTSUPP;
}
/* When the data is smaller or equal to 64K, try to place the copy in a memfd/pipe */
#define DATA_FD_MEMORY_LIMIT (64U*1024U)
/* If memfd/pipe didn't work out, then let's use a file in /tmp up to a size of 1M. If it's large than that use /var/tmp instead. */
#define DATA_FD_TMP_LIMIT (1024U*1024U)
int fd_duplicate_data_fd(int fd) {
_cleanup_close_ int copy_fd = -1, tmp_fd = -1;
_cleanup_free_ void *remains = NULL;
_cleanup_free_ char *t = NULL;
size_t remains_size = 0;
const char *td;
struct stat st;
int r;
/* Creates a 'data' fd from the specified source fd, containing all the same data in a read-only fashion, but
* independent of it (i.e. the source fd can be closed and unmounted after this call succeeded). Tries to be
* somewhat smart about where to place the data. In the best case uses a memfd(). If memfd() are not supported
* uses a pipe instead. For larger data will use an unlinked file in /tmp, and for even larger data one in
* /var/tmp. */
if (fstat(fd, &st) < 0)
return -errno;
/* For now, let's only accept regular files, sockets, pipes and char devices */
if (S_ISDIR(st.st_mode))
return -EISDIR;
if (S_ISLNK(st.st_mode))
return -ELOOP;
if (!S_ISREG(st.st_mode) && !S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode) && !S_ISCHR(st.st_mode))
return -EBADFD;
/* If we have reason to believe the data is bounded in size, then let's use memfds or pipes as backing fd. Note
* that we use the reported regular file size only as a hint, given that there are plenty special files in
* /proc and /sys which report a zero file size but can be read from. */
if (!S_ISREG(st.st_mode) || st.st_size < DATA_FD_MEMORY_LIMIT) {
/* Try a memfd first */
copy_fd = memfd_new("data-fd");
if (copy_fd >= 0) {
off_t f;
r = copy_bytes(fd, copy_fd, DATA_FD_MEMORY_LIMIT, 0);
if (r < 0)
return r;
f = lseek(copy_fd, 0, SEEK_SET);
if (f != 0)
return -errno;
if (r == 0) {
/* Did it fit into the limit? If so, we are done. */
r = memfd_set_sealed(copy_fd);
if (r < 0)
return r;
return TAKE_FD(copy_fd);
}
/* Hmm, pity, this didn't fit. Let's fall back to /tmp then, see below */
} else {
_cleanup_(close_pairp) int pipefds[2] = { -1, -1 };
int isz;
/* If memfds aren't available, use a pipe. Set O_NONBLOCK so that we will get EAGAIN rather
* then block indefinitely when we hit the pipe size limit */
if (pipe2(pipefds, O_CLOEXEC|O_NONBLOCK) < 0)
return -errno;
isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
if (isz < 0)
return -errno;
/* Try to enlarge the pipe size if necessary */
if ((size_t) isz < DATA_FD_MEMORY_LIMIT) {
(void) fcntl(pipefds[1], F_SETPIPE_SZ, DATA_FD_MEMORY_LIMIT);
isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
if (isz < 0)
return -errno;
}
if ((size_t) isz >= DATA_FD_MEMORY_LIMIT) {
r = copy_bytes_full(fd, pipefds[1], DATA_FD_MEMORY_LIMIT, 0, &remains, &remains_size);
if (r < 0 && r != -EAGAIN)
return r; /* If we get EAGAIN it could be because of the source or because of
* the destination fd, we can't know, as sendfile() and friends won't
* tell us. Hence, treat this as reason to fall back, just to be
* sure. */
if (r == 0) {
/* Everything fit in, yay! */
(void) fd_nonblock(pipefds[0], false);
return TAKE_FD(pipefds[0]);
}
/* Things didn't fit in. But we read data into the pipe, let's remember that, so that
* when writing the new file we incorporate this first. */
copy_fd = TAKE_FD(pipefds[0]);
}
}
}
/* If we have reason to believe this will fit fine in /tmp, then use that as first fallback. */
if ((!S_ISREG(st.st_mode) || st.st_size < DATA_FD_TMP_LIMIT) &&
(DATA_FD_MEMORY_LIMIT + remains_size) < DATA_FD_TMP_LIMIT) {
off_t f;
tmp_fd = open_tmpfile_unlinkable(NULL /* NULL as directory means /tmp */, O_RDWR|O_CLOEXEC);
if (tmp_fd < 0)
return tmp_fd;
if (copy_fd >= 0) {
/* If we tried a memfd/pipe first and it ended up being too large, then copy this into the
* temporary file first. */
r = copy_bytes(copy_fd, tmp_fd, UINT64_MAX, 0);
if (r < 0)
return r;
assert(r == 0);
}
if (remains_size > 0) {
/* If there were remaining bytes (i.e. read into memory, but not written out yet) from the
* failed copy operation, let's flush them out next. */
r = loop_write(tmp_fd, remains, remains_size, false);
if (r < 0)
return r;
}
r = copy_bytes(fd, tmp_fd, DATA_FD_TMP_LIMIT - DATA_FD_MEMORY_LIMIT - remains_size, COPY_REFLINK);
if (r < 0)
return r;
if (r == 0)
goto finish; /* Yay, it fit in */
/* It didn't fit in. Let's not forget to use what we already used */
f = lseek(tmp_fd, 0, SEEK_SET);
if (f != 0)
return -errno;
safe_close(copy_fd);
copy_fd = TAKE_FD(tmp_fd);
remains = mfree(remains);
remains_size = 0;
}
/* As last fallback use /var/tmp */
r = var_tmp_dir(&td);
if (r < 0)
return r;
tmp_fd = open_tmpfile_unlinkable(td, O_RDWR|O_CLOEXEC);
if (tmp_fd < 0)
return tmp_fd;
if (copy_fd >= 0) {
/* If we tried a memfd/pipe first, or a file in /tmp, and it ended up being too large, than copy this
* into the temporary file first. */
r = copy_bytes(copy_fd, tmp_fd, UINT64_MAX, COPY_REFLINK);
if (r < 0)
return r;
assert(r == 0);
}
if (remains_size > 0) {
/* Then, copy in any read but not yet written bytes. */
r = loop_write(tmp_fd, remains, remains_size, false);
if (r < 0)
return r;
}
/* Copy in the rest */
r = copy_bytes(fd, tmp_fd, UINT64_MAX, COPY_REFLINK);
if (r < 0)
return r;
assert(r == 0);
finish:
/* Now convert the O_RDWR file descriptor into an O_RDONLY one (and as side effect seek to the beginning of the
* file again */
return fd_reopen(tmp_fd, O_RDONLY|O_CLOEXEC);
}
int fd_move_above_stdio(int fd) {
int flags, copy;
PROTECT_ERRNO;
+2
View File
@@ -81,6 +81,8 @@ enum {
int acquire_data_fd(const void *data, size_t size, unsigned flags);
int fd_duplicate_data_fd(int fd);
/* Hint: ENETUNREACH happens if we try to connect to "non-existing" special IP addresses, such as ::5 */
#define ERRNO_IS_DISCONNECT(r) \
IN_SET(r, ENOTCONN, ECONNRESET, ECONNREFUSED, ECONNABORTED, EPIPE, ENETUNREACH)
+44 -18
View File
@@ -263,29 +263,35 @@ int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
}
int read_full_stream(FILE *f, char **contents, size_t *size) {
size_t n, l;
_cleanup_free_ char *buf = NULL;
struct stat st;
size_t n, l;
int fd;
assert(f);
assert(contents);
if (fstat(fileno(f), &st) < 0)
return -errno;
n = LINE_MAX;
if (S_ISREG(st.st_mode)) {
fd = fileno(f);
if (fd >= 0) { /* If the FILE* object is backed by an fd (as opposed to memory or such, see fmemopen(), let's
* optimize our buffering) */
/* Safety check */
if (st.st_size > READ_FULL_BYTES_MAX)
return -E2BIG;
if (fstat(fileno(f), &st) < 0)
return -errno;
/* Start with the right file size, but be prepared for files from /proc which generally report a file
* size of 0. Note that we increase the size to read here by one, so that the first read attempt
* already makes us notice the EOF. */
if (st.st_size > 0)
n = st.st_size + 1;
if (S_ISREG(st.st_mode)) {
/* Safety check */
if (st.st_size > READ_FULL_BYTES_MAX)
return -E2BIG;
/* Start with the right file size, but be prepared for files from /proc which generally report a file
* size of 0. Note that we increase the size to read here by one, so that the first read attempt
* already makes us notice the EOF. */
if (st.st_size > 0)
n = st.st_size + 1;
}
}
l = 0;
@@ -676,21 +682,41 @@ static int parse_env_file_push(
return 0;
}
int parse_env_file(
int parse_env_filev(
FILE *f,
const char *fname,
const char *newline, ...) {
const char *newline,
va_list ap) {
va_list ap;
int r, n_pushed = 0;
va_list aq;
if (!newline)
newline = NEWLINE;
va_copy(aq, ap);
r = parse_env_file_internal(f, fname, newline, parse_env_file_push, &aq, &n_pushed);
va_end(aq);
if (r < 0)
return r;
return n_pushed;
}
int parse_env_file(
FILE *f,
const char *fname,
const char *newline,
...) {
va_list ap;
int r;
va_start(ap, newline);
r = parse_env_file_internal(NULL, fname, newline, parse_env_file_push, &ap, &n_pushed);
r = parse_env_filev(f, fname, newline, ap);
va_end(ap);
return r < 0 ? r : n_pushed;
return r;
}
static int load_env_file_push(
+2 -1
View File
@@ -45,7 +45,8 @@ int read_full_stream(FILE *f, char **contents, size_t *size);
int verify_file(const char *fn, const char *blob, bool accept_extra_nl);
int parse_env_file(const char *fname, const char *separator, ...) _sentinel_;
int parse_env_filev(FILE *f, const char *fname, const char *separator, va_list ap);
int parse_env_file(FILE *f, const char *fname, const char *separator, ...) _sentinel_;
int load_env_file(FILE *f, const char *fname, const char *separator, char ***l);
int load_env_file_pairs(FILE *f, const char *fname, const char *separator, char ***l);
+2
View File
@@ -131,6 +131,8 @@ basic_sources = files('''
ordered-set.h
pager.c
pager.h
os-util.c
os-util.h
parse-util.c
parse-util.h
path-util.c
+117
View File
@@ -0,0 +1,117 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "alloc-util.h"
#include "fd-util.h"
#include "fs-util.h"
#include "macro.h"
#include "os-util.h"
#include "strv.h"
#include "fileio.h"
#include "string-util.h"
int path_is_os_tree(const char *path) {
int r;
assert(path);
/* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
* always results in -ENOENT, and we can properly distuingish the case where the whole root doesn't exist from
* the case where just the os-release file is missing. */
if (laccess(path, F_OK) < 0)
return -errno;
/* We use {/etc|/usr/lib}/os-release as flag file if something is an OS */
r = open_os_release(path, NULL, NULL);
if (r == -ENOENT) /* We got nothing */
return 0;
if (r < 0)
return r;
return 1;
}
int open_os_release(const char *root, char **ret_path, int *ret_fd) {
_cleanup_free_ char *q = NULL;
const char *p;
int k;
FOREACH_STRING(p, "/etc/os-release", "/usr/lib/os-release") {
k = chase_symlinks(p, root, CHASE_PREFIX_ROOT|(ret_fd ? CHASE_OPEN : 0), (ret_path ? &q : NULL));
if (k != -ENOENT)
break;
}
if (k < 0)
return k;
if (ret_fd) {
int real_fd;
/* Convert the O_PATH fd into a proper, readable one */
real_fd = fd_reopen(k, O_RDONLY|O_CLOEXEC|O_NOCTTY);
safe_close(k);
if (real_fd < 0)
return real_fd;
*ret_fd = real_fd;
}
if (ret_path)
*ret_path = TAKE_PTR(q);
return 0;
}
int fopen_os_release(const char *root, char **ret_path, FILE **ret_file) {
_cleanup_free_ char *p = NULL;
_cleanup_close_ int fd = -1;
FILE *f;
int r;
if (!ret_file)
return open_os_release(root, ret_path, NULL);
r = open_os_release(root, ret_path ? &p : NULL, &fd);
if (r < 0)
return r;
f = fdopen(fd, "re");
if (!f)
return -errno;
fd = -1;
*ret_file = f;
if (ret_path)
*ret_path = TAKE_PTR(p);
return 0;
}
int parse_os_release(const char *root, ...) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
va_list ap;
int r;
r = fopen_os_release(root, &p, &f);
if (r < 0)
return r;
va_start(ap, root);
r = parse_env_filev(f, p, NEWLINE, ap);
va_end(ap);
return r;
}
int load_os_release_pairs(const char *root, char ***ret) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
int r;
r = fopen_os_release(root, &p, &f);
if (r < 0)
return r;
return load_env_file_pairs(f, p, NEWLINE, ret);
}
+12
View File
@@ -0,0 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <stdio.h>
int path_is_os_tree(const char *path);
int open_os_release(const char *root, char **ret_path, int *ret_fd);
int fopen_os_release(const char *root, char **ret_path, FILE **ret_file);
int parse_os_release(const char *root, ...);
int load_os_release_pairs(const char *root, char ***ret);
+11
View File
@@ -17,6 +17,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/personality.h>
#include <sys/prctl.h>
#include <sys/types.h>
@@ -1344,6 +1345,16 @@ int safe_fork_full(
}
}
if ((flags & (FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE)) == (FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE)) {
/* Optionally, make sure we never propagate mounts to the host. */
if (mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) < 0) {
log_full_errno(prio, errno, "Failed to remount root directory as MS_SLAVE: %m");
_exit(EXIT_FAILURE);
}
}
if (flags & FORK_CLOSE_ALL_FDS) {
/* Close the logs here in case it got reopened above, as close_all_fds() would close them for us */
log_close();
+1
View File
@@ -163,6 +163,7 @@ typedef enum ForkFlags {
FORK_LOG = 1U << 5,
FORK_WAIT = 1U << 6,
FORK_NEW_MOUNTNS = 1U << 7,
FORK_MOUNTNS_SLAVE = 1U << 8,
} ForkFlags;
int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid);
-26
View File
@@ -132,32 +132,6 @@ int path_is_read_only_fs(const char *path) {
return false;
}
int path_is_os_tree(const char *path) {
int r;
assert(path);
/* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
* always results in -ENOENT, and we can properly distuingish the case where the whole root doesn't exist from
* the case where just the os-release file is missing. */
if (laccess(path, F_OK) < 0)
return -errno;
/* We use /usr/lib/os-release as flag file if something is an OS */
r = chase_symlinks("/usr/lib/os-release", path, CHASE_PREFIX_ROOT, NULL);
if (r == -ENOENT) {
/* Also check for the old location in /etc, just in case. */
r = chase_symlinks("/etc/os-release", path, CHASE_PREFIX_ROOT, NULL);
if (r == -ENOENT)
return 0; /* We got nothing */
}
if (r < 0)
return r;
return 1;
}
int files_same(const char *filea, const char *fileb, int flags) {
struct stat a, b;
-1
View File
@@ -35,7 +35,6 @@ int null_or_empty_path(const char *fn);
int null_or_empty_fd(int fd);
int path_is_read_only_fs(const char *path);
int path_is_os_tree(const char *path);
int files_same(const char *filea, const char *fileb, int flags);
+1 -1
View File
@@ -264,7 +264,7 @@ int container_get_leader(const char *machine, pid_t *pid) {
return -EINVAL;
p = strjoina("/run/systemd/machines/", machine);
r = parse_env_file(p, NEWLINE, "LEADER", &s, "CLASS", &class, NULL);
r = parse_env_file(NULL, p, NEWLINE, "LEADER", &s, "CLASS", &class, NULL);
if (r == -ENOENT)
return -EHOSTDOWN;
if (r < 0)