[libvirt] [PATCH v3 06/16] Add a test suite for cgroups functionality
Eric Blake
eblake at redhat.com
Fri Apr 12 21:24:04 UTC 2013
On 04/10/2013 04:08 AM, Daniel P. Berrange wrote:
> From: "Daniel P. Berrange" <berrange at redhat.com>
>
> Some aspects of the cgroups setup / detection code are quite subtle
> and easy to break. It would greatly benefit from unit testing, but
> this is difficult because the test suite won't have privileges to
> play around with cgroups. The solution is to use monkey patching
> via LD_PRELOAD to override the fopen, open, mkdir, access functions
> to redirect access of cgroups files to some magic stubs in the
> test suite.
>
> Using this we provide custom content for the /proc/cgroup and
> /proc/self/mounts files which report a fixed cgroup setup. We
> then override open/mkdir/access so that access to the cgroups
> filesystem gets redirected into files in a temporary directory
> tree in the test suite build dir.
>
> Signed-off-by: Daniel P. Berrange <berrange at redhat.com>
> ---
> .gitignore | 1 +
> cfg.mk | 11 +-
> tests/Makefile.am | 15 +-
> tests/vircgroupmock.c | 453 ++++++++++++++++++++++++++++++++++++++++++++++++++
> tests/vircgrouptest.c | 249 +++++++++++++++++++++++++++
> 5 files changed, 723 insertions(+), 6 deletions(-)
> create mode 100644 tests/vircgroupmock.c
> create mode 100644 tests/vircgrouptest.c
Now that I'm reading this earlier in the day, I can give the full review
that I promised...
>
> exclude_file_name_regexp--sc_prohibit_asprintf = \
> - ^(bootstrap.conf$$|src/util/virutil\.c$$|examples/domain-events/events-c/event-test\.c$$)
> + ^(bootstrap.conf$$|src/util/virutil\.c$$|examples/domain-events/events-c/event-test\.c$$|tests/vircgroupmock\.c$$)
raw asprintf - yep, all the more reason to make sure this test compiles
only on Linux.
> +++ b/tests/Makefile.am
> @@ -97,7 +97,9 @@ test_programs = virshtest sockettest \
> utiltest shunloadtest \
> virtimetest viruritest virkeyfiletest \
> virauthconfigtest \
> - virbitmaptest virendiantest \
> + virbitmaptest \
> + vircgrouptest \
> + virendiantest \
> viridentitytest \
> virkeycodetest \
> virlockspacetest \
> @@ -247,6 +249,7 @@ EXTRA_DIST += $(test_scripts)
>
> test_libraries = libshunload.la \
> libvirportallocatormock.la \
> + vircgroupmock.la \
> $(NULL)
shunload.c is guarded by #ifdef linux; you should do the same for
vircgroupmock.c.
>
> +vircgrouptest_SOURCES = \
> + vircgrouptest.c testutils.h testutils.c
> +vircgrouptest_LDADD = $(LDADDS)
> +
> +vircgroupmock_la_SOURCES = \
> + vircgroupmock.c
> +vircgroupmock_la_CFLAGS = $(AM_CFLAGS) -DMOCK_HELPER=1
Why do you need -DMOCK_HELPER=1? Too much copy and past from
virportallocatortest.c, which crammed both the mock library and the test
into the same file rather than your approach here of using two files?
> +++ b/tests/vircgroupmock.c
> +#include <config.h>
> +
> +#include "internal.h"
> +
> +#include <stdio.h>
> +#include <dlfcn.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <sys/stat.h>
> +
> +static int (*realopen)(const char *path, int flags, ...);
Don't you need <stdarg.h> to make it possible to implement your
realopen()? [1]
> +/*
> + * Intentionally missing the 'devices' mount.
> + * Co-mounting cpu & cpuacct controllers
> + * An anonymous controller for systemd
Should give good coverage of the various virCgroup actions.
> + */
> +const char *mounts =
> + "rootfs / rootfs rw 0 0\n"
> + "tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0\n"
> + "tmpfs /not/really/sys/fs/cgroup tmpfs rw,seclabel,nosuid,nodev,noexec,mode=755 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0\n"
> + "cgroup /not/really/sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0\n"
> + "/dev/sda1 /boot ext4 rw,seclabel,relatime,data=ordered 0 0\n"
> + "tmpfs /tmp tmpfs rw,seclabel,relatime,size=1024000k 0 0\n";
Resembles a real system; so far so good.
> +
> +const char *cgroups =
> + "115:memory:/\n"
> + "8:blkio:/\n"
> + "6:freezer:/\n"
> + "3:cpuacct,cpu:/system\n"
> + "2:cpuset:/\n"
> + "1:name=systemd:/user/berrange/123\n";
No wonder it resembles a real system :) I'm sure you populated this
based on your own system, then tweaked it into the test.
> +
> +static int make_file(const char *path,
> + const char *name,
> + const char *value)
Indentation is off.
> +{
> + int fd = -1;
> + int ret = -1;
> + char *filepath = NULL;
> +
> + if (asprintf(&filepath, "%s/%s", path, name) < 0)
> + return -1;
> +
> + if ((fd = open(filepath, O_CREAT|O_WRONLY, 0600)) < 0)
Is it okay if this calls our open() instead of realopen()?
> + goto cleanup;
> +
> + if (write(fd, value, strlen(value)) != strlen(value))
Can't embed any NUL in your fake files, but that's probably okay.
> + goto cleanup;
> +
> + ret = 0;
> +cleanup:
> + if (fd != -1 &&close(fd) < 0)
Spacing.
> + ret = -1;
> + free(filepath);
> +
> + return ret;
> +}
> +
> +static int make_controller(const char *path, mode_t mode)
> +{
> + int ret = -1;
> + const char *controller;
> +
> + if (!STRPREFIX(path, fakesysfsdir)) {
> + errno = EINVAL;
> + return -1;
> + }
> + controller = path + strlen(fakesysfsdir) + 1;
> +
> + if (STREQ(controller, "cpu")) {
> + if (symlink("cpu,cpuacct", path) < 0)
> + return -1;
> + return -0;
-0 is cute. Couldn't this just be:
return symlink("cpu,cpuacct", path);
> + }
> + if (STREQ(controller, "cpuacct")) {
> + if (symlink("cpu,cpuacct", path) < 0)
> + return -1;
> + return 0;
> + }
> +
> + if (realmkdir(path, mode) < 0)
> + goto cleanup;
> +
> +#define MAKE_FILE(name, value) \
> + do { \
> + if (make_file(path, name, value) < 0) \
> + goto cleanup; \
> + } while (0)
> +
> + if (STRPREFIX(controller, "cpu,cpuacct")) {
> + MAKE_FILE("cpu.cfs_period_us", "100000\n");
Seems useful; again, the set of files and their contents was probably
copied right off your system, even if newer kernels later on add more
files, this test will still be a reasonable action of at least one
kernel in time that libvirt must deal with.
...
> + MAKE_FILE("blkio.weight_device", "");
> +
> + } else {
> + errno = EINVAL;
> + goto cleanup;
> + }
> +
> + ret = 0;
> +cleanup:
> + return ret;
> +}
> +
> +static void init_syms(void)
> +{
> + if (realfopen)
> + return;
> +
> +#define LOAD_SYM(name) \
> + do { \
> + if (!(real ## name = dlsym(RTLD_NEXT, #name))) { \
> + fprintf(stderr, "Cannot find real '%s' symbol\n", #name); \
> + abort(); \
> + } \
> + } while (0)
> +
> + LOAD_SYM(fopen);
> + LOAD_SYM(access);
> + LOAD_SYM(mkdir);
> + LOAD_SYM(open);
Looks correct for the stubs we are overriding.
> +}
> +
> +static void init_sysfs(void)
> +{
> + if (fakesysfsdir)
> + return;
> +
> + if (!(fakesysfsdir = getenv("LIBVIRT_FAKE_SYSFS_DIR"))) {
> + fprintf(stderr, "Missing LIBVIRT_FAKE_SYSFS_DIR env variable\n");
> + abort();
> + }
> +
> +#define MAKE_CONTROLLER(subpath) \
> + do { \
> + char *path; \
> + if (asprintf(&path,"%s/%s", fakesysfsdir, subpath) < 0) \
> + abort(); \
> + if (make_controller(path, 0755) < 0) { \
> + fprintf(stderr, "Cannot initialize %s\n", path); \
> + abort(); \
> + } \
Odd alignment of \ (half aligned, half not)
> + } while (0)
> +
> + MAKE_CONTROLLER("cpu");
> + MAKE_CONTROLLER("cpuacct");
> + MAKE_CONTROLLER("cpu,cpuacct");
> + MAKE_CONTROLLER("cpu,cpuacct/system");
> + MAKE_CONTROLLER("cpuset");
> + MAKE_CONTROLLER("blkio");
> + MAKE_CONTROLLER("memory");
> + MAKE_CONTROLLER("freezer");
> +}
> +
> +
> +FILE *fopen(const char *path, const char *mode)
> +{
> + init_syms();
> +
> + if (STREQ(path, "/proc/mounts")) {
> + if (STREQ(mode, "r")) {
> + return fmemopen((void *)mounts, strlen(mounts), mode);
fmemopen() - fun stuff. Glad you're fixing this up to be Linux only :)
Is the (void*) cast really needed? Ah yes, to cast away const.
> + } else {
> + errno = EACCES;
> + return NULL;
> + }
> + }
> + if (STREQ(path, "/proc/self/cgroup")) {
> + if (STREQ(mode, "r")) {
> + return fmemopen((void *)cgroups, strlen(cgroups), mode);
> + } else {
> + errno = EACCES;
> + return NULL;
> + }
> + }
And these just happen to be the two files we fopen in libvirt.
> +
> + return realfopen(path, mode);
> +}
> +
> +int access(const char *path, int mode)
> +{
> + int ret;
> +
> + init_syms();
> +
> + if (STRPREFIX(path, SYSFS_PREFIX)) {
> + init_sysfs();
> + char *newpath;
> + if (asprintf(&newpath, "%s/%s",
> + fakesysfsdir,
> + path + strlen(SYSFS_PREFIX)) < 0) {
> + errno = ENOMEM;
> + return -1;
> + }
> + ret = realaccess(newpath, mode);
> + free(newpath);
> + } else {
> + ret = realaccess(path, mode);
> + }
> + return ret;
> +}
> +
> +int mkdir(const char *path, mode_t mode)
> +{
> + int ret;
> +
> + init_syms();
> +
> + if (STRPREFIX(path, SYSFS_PREFIX)) {
> + init_sysfs();
> + char *newpath;
> + if (asprintf(&newpath, "%s/%s",
> + fakesysfsdir,
> + path + strlen(SYSFS_PREFIX)) < 0) {
> + errno = ENOMEM;
> + return -1;
> + }
> + ret = make_controller(newpath, mode);
> + free(newpath);
> + } else {
> + ret = realmkdir(path, mode);
> + }
> + return ret;
> +}
Looks fine.
> +
> +int open(const char *path, int flags, ...)
> +{
> + int ret;
> + char *newpath = NULL;
> +
> + init_syms();
> +
> + if (STRPREFIX(path, SYSFS_PREFIX)) {
> + init_sysfs();
> + if (asprintf(&newpath, "%s/%s",
> + fakesysfsdir,
> + path + strlen(SYSFS_PREFIX)) < 0) {
> + errno = ENOMEM;
> + return -1;
> + }
> + }
> + if (flags & O_CREAT) {
> + va_list ap;
[1] Yep, you DO need to fix the includes to use <stdarg.h>.
> + mode_t mode;
> + va_start(ap, flags);
> + mode = va_arg(ap, mode_t);
> + va_end(ap);
> + ret = realopen(newpath ? newpath : path, flags, mode);
> + } else {
> + ret = realopen(newpath ? newpath : path, flags);
> + }
You know, wouldn't it just work to write the simpler:
int open(const char *path, int flags, mode_t mode)
{
...
ret = realopen(newpath ? newpath : path, flags, mode);
...
}
without any regard to the presence or absence of O_CREAT? That is, are
there any platforms that support Linux but where var-arg passing would
do the wrong thing if our LD_PRELOAD is specified as a three-argument
function instead of a var-arg function, whether or not the calling app
passed 2 or 3 args?
> + free(newpath);
> + return ret;
> +}
> diff --git a/tests/vircgrouptest.c b/tests/vircgrouptest.c
> new file mode 100644
> index 0000000..a68aa88
> --- /dev/null
> +++ b/tests/vircgrouptest.c
> @@ -0,0 +1,249 @@
...
> +
> +static int validateCgroup(virCgroupPtr cgroup,
> + const char *expectPath,
> + const char **expectMountPoint,
> + const char **expectPlacement)
> +{
> + int i;
> +
> + if (STRNEQ(cgroup->path, expectPath)) {
> + fprintf(stderr, "Wrong path '%s', expected '%s'\n",
> + cgroup->path, expectPath);
> + return -1;
> + }
> +
> +const char *mountsSmall[VIR_CGROUP_CONTROLLER_LAST] = {
> + [VIR_CGROUP_CONTROLLER_CPU] = "/not/really/sys/fs/cgroup/cpu,cpuacct",
Is it worth spelling this:
SYSFS_PREFIX "/cgroup/cpu,cpuacct"
> +
> +#define FAKESYSFSDIRTEMPLATE abs_builddir "/fakesysfsdir-XXXXXX"
> +
> +
> +
> +static int
> +mymain(void)
> +{
> + int ret = 0;
> + char *fakesysfsdir;
> +
> + if (!(fakesysfsdir = strdup(FAKESYSFSDIRTEMPLATE))) {
> + fprintf(stderr, "Out of memory\n");
> + abort();
> + }
> +
> + if (!mkdtemp(fakesysfsdir)) {
> + fprintf(stderr, "Cannot create fakesysfsdir");
> + abort();
> + }
> +
> + setenv("LIBVIRT_FAKE_SYSFS_DIR", fakesysfsdir, 1);
> +
> + if (virtTestRun("New cgroup for self", 1, testCgroupNewForSelf, NULL) < 0)
> + ret = -1;
> +
> + if (virtTestRun("New cgroup for driver", 1, testCgroupNewForDriver, NULL) < 0)
> + ret = -1;
> +
> + if (virtTestRun("New cgroup for domain", 1, testCgroupNewForDomain, NULL) < 0)
> + ret = -1;
> +
> + if (getenv("LIBVIRT_SKIP_CLEANUP") == NULL)
> + virFileDeleteTree(fakesysfsdir);
Worth calling VIR_FREE(fakesysfsdir) here, to keep valgrind happy?
(That is, supposing that valgrind can even deal with our mock LD_PRELOAD
tests)
> +
> + return ret==0 ? EXIT_SUCCESS : EXIT_FAILURE;
> +}
> +
> +VIRT_TEST_MAIN_PRELOAD(mymain, abs_builddir "/.libs/libvircgroupmock.so")
>
I know that you have access to a mingw cross-compilation environment -
ACK if you fix the issues I mentioned above and check that things still
compile on mingw.
--
Eric Blake eblake redhat com +1-919-301-3266
Libvirt virtualization library http://libvirt.org
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 621 bytes
Desc: OpenPGP digital signature
URL: <http://listman.redhat.com/archives/libvir-list/attachments/20130412/71fa3864/attachment-0001.sig>
More information about the libvir-list
mailing list