From a18452d0d2aedf65b83b24983ce5746cfe68e71a Mon Sep 17 00:00:00 2001 From: Eric Blake Date: Wed, 13 Feb 2013 11:04:05 -0700 Subject: [PATCH] storage: test backing chain traversal Testing our backing chain handling will make it much easier to ensure that we avoid issues in the future. If only I had written this test before I first caused several regressions... * tests/virstoragetest.c: New test. * tests/Makefile.am (test_programs): Build it. * .gitignore: Ignore new files. --- .gitignore | 1 + tests/Makefile.am | 6 + tests/virstoragetest.c | 554 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 561 insertions(+) create mode 100644 tests/virstoragetest.c diff --git a/.gitignore b/.gitignore index 8afbf3343a..23fdc91b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -184,6 +184,7 @@ /tests/virnet*test /tests/virportallocatortest /tests/virshtest +/tests/virstoragetest /tests/virstringtest /tests/virtimetest /tests/viruritest diff --git a/tests/Makefile.am b/tests/Makefile.am index 7d0a88ede9..cafdae0bb9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -100,6 +100,7 @@ test_programs = virshtest sockettest \ virstringtest \ virportallocatortest \ sysinfotest \ + virstoragetest \ $(NULL) if WITH_GNUTLS @@ -565,6 +566,11 @@ virstringtest_SOURCES = \ virstringtest_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\"" $(AM_CFLAGS) virstringtest_LDADD = $(LDADDS) +virstoragetest_SOURCES = \ + virstoragetest.c testutils.h testutils.c +virstoragetest_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\"" $(AM_CFLAGS) +virstoragetest_LDADD = $(LDADDS) + virlockspacetest_SOURCES = \ virlockspacetest.c testutils.h testutils.c virlockspacetest_CFLAGS = -Dabs_builddir="\"$(abs_builddir)\"" $(AM_CFLAGS) diff --git a/tests/virstoragetest.c b/tests/virstoragetest.c new file mode 100644 index 0000000000..9da58f3823 --- /dev/null +++ b/tests/virstoragetest.c @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2013 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 + * . + * + * Author: Eric Blake + */ + +#include + +#include + +#include "testutils.h" +#include "vircommand.h" +#include "virerror.h" +#include "virlog.h" +#include "virstoragefile.h" + +#define VIR_FROM_THIS VIR_FROM_NONE + +#define datadir abs_builddir "/virstoragedata" + +/* This test creates the following files, all in datadir: + + * raw: 1024-byte raw file + * qcow2: qcow2 file with 'raw' as backing + * wrap: qcow2 file with 'qcow2' as backing + * qed: qed file with 'raw' as backing + * sub/link1: symlink to qcow2 + * sub/link2: symlink to wrap + * + * Relative names to these files are known at compile time, but absolute + * and canonical names depend on where the test is run; for convenience, + * we pre-populate the computation of these names for use during the test. +*/ + +static char *qemuimg; +static char *absraw; +static char *canonraw; +static char *absqcow2; +static char *canonqcow2; +static char *abswrap; +static char *absqed; +static char *abslink2; + +static void +testCleanupImages(void) +{ + virCommandPtr cmd; + + VIR_FREE(qemuimg); + VIR_FREE(absraw); + VIR_FREE(canonraw); + VIR_FREE(absqcow2); + VIR_FREE(canonqcow2); + VIR_FREE(abswrap); + VIR_FREE(absqed); + VIR_FREE(abslink2); + + if (chdir(abs_builddir) < 0) { + fprintf(stderr, "unable to return to correct directory, refusing to " + "clean up %s\n", datadir); + return; + } + + cmd = virCommandNewArgList("rm", "-rf", datadir, NULL); + ignore_value(virCommandRun(cmd, NULL)); + virCommandFree(cmd); +} + +static int +testPrepImages(void) +{ + int ret = EXIT_FAILURE; + virCommandPtr cmd = NULL; + + qemuimg = virFindFileInPath("kvm-img"); + if (!qemuimg) + qemuimg = virFindFileInPath("qemu-img"); + if (!qemuimg) { + fprintf(stderr, "qemu-img missing or too old; skipping this test\n"); + return EXIT_AM_SKIP; + } + + if (virAsprintf(&absraw, "%s/raw", datadir) < 0 || + virAsprintf(&absqcow2, "%s/qcow2", datadir) < 0 || + virAsprintf(&abswrap, "%s/wrap", datadir) < 0 || + virAsprintf(&absqed, "%s/qed", datadir) < 0 || + virAsprintf(&abslink2, "%s/sub/link2", datadir) < 0) { + virReportOOMError(); + goto cleanup; + } + + if (virFileMakePath(datadir "/sub") < 0) { + fprintf(stderr, "unable to create directory %s\n", datadir "/sub"); + goto cleanup; + } + + if (chdir(datadir) < 0) { + fprintf(stderr, "unable to test relative backing chains\n"); + goto cleanup; + } + + /* I'm lazy enough to use a shell one-liner instead of open/write/close */ + virCommandFree(cmd); + cmd = virCommandNewArgList("sh", "-c", "printf %1024d 0 > raw", NULL); + if (virCommandRun(cmd, NULL) < 0) + goto cleanup; + if (!(canonraw = canonicalize_file_name(absraw))) { + virReportOOMError(); + goto cleanup; + } + + /* Create a qcow2 wrapping relative raw; later on, we modify its + * metadata to test other configurations */ + virCommandFree(cmd); + cmd = virCommandNewArgList("qemu-img", "create", "-f", "qcow2", + "-obacking_file=raw,backing_fmt=raw", "qcow2", + NULL); + if (virCommandRun(cmd, NULL) < 0) + goto cleanup; + /* Make sure our later uses of 'qemu-img rebase' will work */ + virCommandFree(cmd); + cmd = virCommandNewArgList("qemu-img", "rebase", "-u", "-f", "qcow2", + "-F", "raw", "-b", "raw", "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) { + fprintf(stderr, "qemu-img is too old; skipping this test\n"); + ret = EXIT_AM_SKIP; + goto cleanup; + } + if (!(canonqcow2 = canonicalize_file_name(absqcow2))) { + virReportOOMError(); + goto cleanup; + } + + /* Create a second qcow2 wrapping the first, to be sure that we + * can correctly avoid insecure probing. */ + virCommandFree(cmd); + cmd = virCommandNewArgList("qemu-img", "create", "-f", "qcow2", NULL); + virCommandAddArgFormat(cmd, "-obacking_file=%s,backing_fmt=qcow2", + absqcow2); + virCommandAddArg(cmd, "wrap"); + if (virCommandRun(cmd, NULL) < 0) + goto cleanup; + + /* Create a qed file. */ + virCommandFree(cmd); + cmd = virCommandNewArgList("qemu-img", "create", "-f", "qed", NULL); + virCommandAddArgFormat(cmd, "-obacking_file=%s,backing_fmt=raw", + absraw); + virCommandAddArg(cmd, "qed"); + if (virCommandRun(cmd, NULL) < 0) + goto cleanup; + + /* Create some symlinks in a sub-directory. */ + if (symlink("../qcow2", datadir "/sub/link1") < 0 || + symlink("../wrap", datadir "/sub/link2") < 0) { + fprintf(stderr, "unable to create symlink"); + goto cleanup; + } + + ret = 0; +cleanup: + virCommandFree(cmd); + if (ret) + testCleanupImages(); + return ret; +} + +typedef struct _testFileData testFileData; +struct _testFileData +{ + const char *expBackingStore; + const char *expBackingStoreRaw; + const char *expDirectory; + enum virStorageFileFormat expFormat; + bool expIsFile; + unsigned long long expCapacity; + bool expEncrypted; +}; + +enum { + EXP_PASS = 0, + EXP_FAIL = 1, + EXP_WARN = 2, + ALLOW_PROBE = 4, +}; + +struct testChainData +{ + const char *start; + enum virStorageFileFormat format; + const testFileData *files; + int nfiles; + unsigned int flags; +}; + +static int +testStorageChain(const void *args) +{ + const struct testChainData *data = args; + int ret = -1; + virStorageFileMetadataPtr meta; + virStorageFileMetadataPtr elt; + int i = 0; + + meta = virStorageFileGetMetadata(data->start, data->format, -1, -1, + (data->flags & ALLOW_PROBE) != 0); + if (!meta) { + if (data->flags & EXP_FAIL) { + virResetLastError(); + ret = 0; + } + goto cleanup; + } else if (data->flags & EXP_FAIL) { + fprintf(stderr, "call should have failed\n"); + goto cleanup; + } + if (data->flags & EXP_WARN) { + if (!virGetLastError()) { + fprintf(stderr, "call should have warned\n"); + goto cleanup; + } + virResetLastError(); + } else if (virGetLastError()) { + fprintf(stderr, "call should not have warned\n"); + goto cleanup; + } + + elt = meta; + while (elt) { + char *expect = NULL; + char *actual = NULL; + + if (i == data->nfiles) { + fprintf(stderr, "probed chain was too long\n"); + goto cleanup; + } + + if (virAsprintf(&expect, + "store:%s\nraw:%s\ndirectory:%s\nother:%d %d %lld %d", + NULLSTR(data->files[i].expBackingStore), + NULLSTR(data->files[i].expBackingStoreRaw), + NULLSTR(data->files[i].expDirectory), + data->files[i].expFormat, + data->files[i].expIsFile, + data->files[i].expCapacity, + data->files[i].expEncrypted) < 0 || + virAsprintf(&actual, + "store:%s\nraw:%s\ndirectory:%s\nother:%d %d %lld %d", + NULLSTR(elt->backingStore), + NULLSTR(elt->backingStoreRaw), + NULLSTR(elt->directory), + elt->backingStoreFormat, elt->backingStoreIsFile, + elt->capacity, elt->encrypted) < 0) { + virReportOOMError(); + VIR_FREE(expect); + VIR_FREE(actual); + goto cleanup; + } + if (STRNEQ(expect, actual)) { + virtTestDifference(stderr, expect, actual); + VIR_FREE(expect); + VIR_FREE(actual); + goto cleanup; + } + VIR_FREE(expect); + VIR_FREE(actual); + elt = elt->backingMeta; + i++; + } + if (i != data->nfiles) { + fprintf(stderr, "probed chain was too short\n"); + goto cleanup; + } + + ret = 0; +cleanup: + virStorageFileFreeMetadata(meta); + return ret; +} + +static int +mymain(void) +{ + int ret; + virCommandPtr cmd = NULL; + + /* Prep some files with qemu-img; if that is not found on PATH, or + * if it lacks support for qcow2 and qed, skip this test. */ + if ((ret = testPrepImages()) != 0) + return ret; + +#define TEST_ONE_CHAIN(id, start, format, chain, flags) \ + do { \ + struct testChainData data = { \ + start, format, chain, ARRAY_CARDINALITY(chain), flags, \ + }; \ + if (virtTestRun("Storage backing chain " id, 1, \ + testStorageChain, &data) < 0) \ + ret = -1; \ + } while (0) + +#define TEST_CHAIN(id, relstart, absstart, format, chain1, flags1, \ + chain2, flags2, chain3, flags3, chain4, flags4) \ + do { \ + TEST_ONE_CHAIN(#id "a", relstart, format, chain1, flags1); \ + TEST_ONE_CHAIN(#id "b", relstart, format, chain2, flags2); \ + TEST_ONE_CHAIN(#id "c", absstart, format, chain3, flags3); \ + TEST_ONE_CHAIN(#id "d", absstart, format, chain4, flags4); \ + } while (0) + + /* Expected details about files in chains */ + const testFileData raw = { + NULL, NULL, NULL, VIR_STORAGE_FILE_NONE, false, 0, false, + }; + const testFileData qcow2_relback_relstart = { + canonraw, "raw", ".", VIR_STORAGE_FILE_RAW, true, 1024, false, + }; + const testFileData qcow2_relback_absstart = { + canonraw, "raw", datadir, VIR_STORAGE_FILE_RAW, true, 1024, false, + }; + const testFileData qcow2_absback = { + canonraw, absraw, datadir, VIR_STORAGE_FILE_RAW, true, 1024, false, + }; + const testFileData qcow2_as_probe = { + canonraw, absraw, datadir, VIR_STORAGE_FILE_AUTO, true, 1024, false, + }; + const testFileData qcow2_bogus = { + NULL, datadir "/bogus", datadir, VIR_STORAGE_FILE_NONE, + false, 1024, false, + }; + const testFileData qcow2_protocol = { + "nbd:example.org:6000", NULL, NULL, VIR_STORAGE_FILE_RAW, + false, 1024, false, + }; + const testFileData wrap = { + canonqcow2, absqcow2, datadir, VIR_STORAGE_FILE_QCOW2, + true, 1024, false, + }; + const testFileData wrap_as_raw = { + canonqcow2, absqcow2, datadir, VIR_STORAGE_FILE_RAW, + true, 1024, false, + }; + const testFileData wrap_as_probe = { + canonqcow2, absqcow2, datadir, VIR_STORAGE_FILE_AUTO, + true, 1024, false, + }; + const testFileData qed = { + canonraw, absraw, datadir, VIR_STORAGE_FILE_RAW, + true, 1024, false, + }; + const testFileData link1_rel = { + canonraw, "../raw", "sub/../sub/..", VIR_STORAGE_FILE_RAW, + true, 1024, false, + }; + const testFileData link1_abs = { + canonraw, "../raw", datadir "/sub/../sub/..", VIR_STORAGE_FILE_RAW, + true, 1024, false, + }; + const testFileData link2_rel = { + canonqcow2, "../sub/link1", "sub/../sub", VIR_STORAGE_FILE_QCOW2, + true, 1024, false, + }; + const testFileData link2_abs = { + canonqcow2, "../sub/link1", datadir "/sub/../sub", + VIR_STORAGE_FILE_QCOW2, true, 1024, false, + }; + + /* The actual tests, in several groups. */ + + /* Missing file */ + const testFileData chain0[] = { }; + TEST_ONE_CHAIN("0", "bogus", VIR_STORAGE_FILE_RAW, chain0, EXP_FAIL); + + /* Raw image, whether with right format or no specified format */ + const testFileData chain1[] = { raw }; + TEST_CHAIN(1, "raw", absraw, VIR_STORAGE_FILE_RAW, + chain1, EXP_PASS, + chain1, ALLOW_PROBE | EXP_PASS, + chain1, EXP_PASS, + chain1, ALLOW_PROBE | EXP_PASS); + TEST_CHAIN(2, "raw", absraw, VIR_STORAGE_FILE_AUTO, + chain1, EXP_PASS, + chain1, ALLOW_PROBE | EXP_PASS, + chain1, EXP_PASS, + chain1, ALLOW_PROBE | EXP_PASS); + + /* Qcow2 file with relative raw backing, format provided */ + const testFileData chain3a[] = { qcow2_relback_relstart, raw }; + const testFileData chain3c[] = { qcow2_relback_absstart, raw }; + const testFileData chain4a[] = { raw }; + TEST_CHAIN(3, "qcow2", absqcow2, VIR_STORAGE_FILE_QCOW2, + chain3a, EXP_PASS, + chain3a, ALLOW_PROBE | EXP_PASS, + chain3c, EXP_PASS, + chain3c, ALLOW_PROBE | EXP_PASS); + TEST_CHAIN(4, "qcow2", absqcow2, VIR_STORAGE_FILE_AUTO, + chain4a, EXP_PASS, + chain3a, ALLOW_PROBE | EXP_PASS, + chain4a, EXP_PASS, + chain3c, ALLOW_PROBE | EXP_PASS); + + /* Rewrite qcow2 file to use absolute backing name */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-F", "raw", "-b", absraw, "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Qcow2 file with raw as absolute backing, backing format provided */ + const testFileData chain5[] = { qcow2_absback, raw }; + const testFileData chain6[] = { raw }; + TEST_CHAIN(5, "qcow2", absqcow2, VIR_STORAGE_FILE_QCOW2, + chain5, EXP_PASS, + chain5, ALLOW_PROBE | EXP_PASS, + chain5, EXP_PASS, + chain5, ALLOW_PROBE | EXP_PASS); + TEST_CHAIN(6, "qcow2", absqcow2, VIR_STORAGE_FILE_AUTO, + chain6, EXP_PASS, + chain5, ALLOW_PROBE | EXP_PASS, + chain6, EXP_PASS, + chain5, ALLOW_PROBE | EXP_PASS); + + /* Wrapped file access */ + const testFileData chain7[] = { wrap, qcow2_absback, raw }; + TEST_CHAIN(7, "wrap", abswrap, VIR_STORAGE_FILE_QCOW2, + chain7, EXP_PASS, + chain7, ALLOW_PROBE | EXP_PASS, + chain7, EXP_PASS, + chain7, ALLOW_PROBE | EXP_PASS); + + /* Rewrite qcow2 and wrap file to omit backing file type */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-b", absraw, "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-b", absqcow2, "wrap", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Qcow2 file with raw as absolute backing, backing format omitted */ + const testFileData chain8a[] = { wrap_as_raw, raw }; + const testFileData chain8b[] = { wrap_as_probe, qcow2_as_probe, raw }; + TEST_CHAIN(8, "wrap", abswrap, VIR_STORAGE_FILE_QCOW2, + chain8a, EXP_PASS, + chain8b, ALLOW_PROBE | EXP_PASS, + chain8a, EXP_PASS, + chain8b, ALLOW_PROBE | EXP_PASS); + + /* Rewrite qcow2 to a missing backing file, with backing type */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-F", "qcow2", "-b", datadir "/bogus", + "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Qcow2 file with missing backing file but specified type */ + const testFileData chain9[] = { qcow2_bogus }; + TEST_CHAIN(9, "qcow2", absqcow2, VIR_STORAGE_FILE_QCOW2, + chain9, EXP_WARN, + chain9, ALLOW_PROBE | EXP_WARN, + chain9, EXP_WARN, + chain9, ALLOW_PROBE | EXP_WARN); + + /* Rewrite qcow2 to a missing backing file, without backing type */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-b", datadir "/bogus", "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Qcow2 file with missing backing file and no specified type */ + const testFileData chain10[] = { qcow2_bogus }; + TEST_CHAIN(10, "qcow2", absqcow2, VIR_STORAGE_FILE_QCOW2, + chain10, EXP_WARN, + chain10, ALLOW_PROBE | EXP_WARN, + chain10, EXP_WARN, + chain10, ALLOW_PROBE | EXP_WARN); + + /* Rewrite qcow2 to use an nbd: protocol as backend */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-F", "raw", "-b", "nbd:example.org:6000", + "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Qcow2 file with backing protocol instead of file */ + const testFileData chain11[] = { qcow2_protocol }; + TEST_CHAIN(11, "qcow2", absqcow2, VIR_STORAGE_FILE_QCOW2, + chain11, EXP_PASS, + chain11, ALLOW_PROBE | EXP_PASS, + chain11, EXP_PASS, + chain11, ALLOW_PROBE | EXP_PASS); + + /* qed file */ + const testFileData chain12a[] = { raw }; + const testFileData chain12b[] = { qed, raw }; + TEST_CHAIN(12, "qed", absqed, VIR_STORAGE_FILE_AUTO, + chain12a, EXP_PASS, + chain12b, ALLOW_PROBE | EXP_PASS, + chain12a, EXP_PASS, + chain12b, ALLOW_PROBE | EXP_PASS); + + /* Rewrite qcow2 and wrap file to use backing names relative to a + * symlink from a different directory */ + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-F", "raw", "-b", "../raw", "qcow2", NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + virCommandFree(cmd); + cmd = virCommandNewArgList(qemuimg, "rebase", "-u", "-f", "qcow2", + "-F", "qcow2", "-b", "../sub/link1", "wrap", + NULL); + if (virCommandRun(cmd, NULL) < 0) + ret = -1; + + /* Behavior of symlinks to qcow2 with relative backing files */ + const testFileData chain13a[] = { link2_rel, link1_rel, raw }; + const testFileData chain13c[] = { link2_abs, link1_abs, raw }; + TEST_CHAIN(13, "sub/link2", abslink2, VIR_STORAGE_FILE_QCOW2, + chain13a, EXP_PASS, + chain13a, ALLOW_PROBE | EXP_PASS, + chain13c, EXP_PASS, + chain13c, ALLOW_PROBE | EXP_PASS); + + /* Final cleanup */ + testCleanupImages(); + virCommandFree(cmd); + + return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +VIRT_TEST_MAIN(mymain) -- GitLab