]> git.proxmox.com Git - pve-storage.git/commitdiff
test: add tests for 'ceph.conf' parser and writer
authorMax Carrara <m.carrara@proxmox.com>
Tue, 2 Apr 2024 14:55:14 +0000 (16:55 +0200)
committerFabian Grünbichler <f.gruenbichler@proxmox.com>
Thu, 11 Apr 2024 09:53:37 +0000 (11:53 +0200)
These tests attempt to cover most of Ceph's config parser's grammar,
including all of its syntax quirks [0].

Each case is tested against two testing subroutines:

  1. The parser's output is compared with the expected output.

  2. The writer's output is parsed again ant then compared with the
     expected output.

[0]: https://git.proxmox.com/?p=ceph.git;a=blob;f=ceph/src/common/ConfUtils.cc;h=2f78fd02bf9e27467275752e6f3bca0c5e3946ce;hb=e9fe820e7fffd1b7cde143a9f77653b73fcec748#l144

Signed-off-by: Max Carrara <m.carrara@proxmox.com>
Tested-by: Friedrich Weber <f.weber@proxmox.com>
src/Makefile
src/PVE/Makefile
src/PVE/test/Makefile [new file with mode: 0644]
src/PVE/test/ceph_conf_parse_write_test.pl [new file with mode: 0755]

index 1eba51ed18210f3f4ce77a378363892a1c5dd39c..a322f46ee134b7400b904cad6a9f880ae28d599f 100644 (file)
@@ -15,6 +15,7 @@ install: PVE bin udev-rbd
 test:
        perl -I. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->verify_api();"
        $(MAKE) -C test
+       $(MAKE) -C PVE test
 
 .PHONY: clean
 clean:
index 5fe4a0ab36538fb4c3731eeb2822bcbdbd8a7909..d4388043a224059396a97bb31f0b2b1fb8a6801b 100644 (file)
@@ -9,4 +9,8 @@ install:
        make -C API2 install
        make -C CLI install
 
+.PHONY: test
+test:
+       $(MAKE) -C test test
+
 clean:
diff --git a/src/PVE/test/Makefile b/src/PVE/test/Makefile
new file mode 100644 (file)
index 0000000..f9c6c79
--- /dev/null
@@ -0,0 +1,9 @@
+all:
+
+.PHONY: test
+test: test_ceph_conf_parse_write
+
+.PHONY: test_ceph_conf_parse_write
+test_ceph_conf_parse_write: ceph_conf_parse_write_test.pl
+       ./ceph_conf_parse_write_test.pl
+
diff --git a/src/PVE/test/ceph_conf_parse_write_test.pl b/src/PVE/test/ceph_conf_parse_write_test.pl
new file mode 100755 (executable)
index 0000000..1d5e506
--- /dev/null
@@ -0,0 +1,794 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use lib qw(../..);
+
+use Test::More;
+
+use PVE::CephConfig;
+
+
+# An array of test cases.
+# Each test case is comprised of the following keys:
+#   description          => to identify a single test
+#   expected_cfg  => the hash that parse_ceph_config should return
+#   raw                  => the raw content of the file to test
+my $tests = [
+    {
+       description => 'empty file',
+       expected_cfg => {},
+       raw => <<~EOF,
+       EOF
+    },
+    {
+       description => 'file without section',
+       expected_cfg => {},
+       raw => <<~EOF,
+       While Ceph's format doesn't allow this, we do, because it makes things simpler
+       foo = bar
+       arbitrary text can go here
+
+       Rust is better than Perl
+       EOF
+    },
+    {
+       description => 'single section',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = baz
+       EOF
+    },
+    {
+       description => 'single section, no key-value pairs',
+       expected_cfg => {
+           foo => {},
+       },
+       raw => <<~EOF,
+       [foo]
+       EOF
+    },
+    {
+       description => 'single section, whitespace before key',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+            \t     bar = baz
+       EOF
+    },
+    {
+       description => 'single section, section header with preceding whitespace',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+         \t    [foo]
+       bar = baz
+       EOF
+    },
+    {
+       description => 'single section, section header with comment',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       [foo] # some comment
+       bar = baz
+       EOF
+    },
+    {
+       description => 'single section, section header ' .
+           'with preceding whitespace and comment',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+         \t  [foo] ; some comment
+       bar = baz
+       EOF
+    },
+    {
+       description => 'single section, arbitrary text before section',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       Rust is better than Perl
+
+       This text is ignored by our parser, because it makes things simpler
+       [foo]
+       bar = baz
+       EOF
+    },
+    {
+       description => 'single section, invalid key-value pairs',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       this here will cause a warning and is ignored
+       bar = baz
+       as well as this
+       EOF
+    },
+    {
+       description => 'single section, multiple key-value pairs',
+       expected_cfg => {
+           foo => {
+               one => 1,
+               two => 2,
+               three => 3,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       one = 1
+       two = 2
+
+       three = 3
+       EOF
+    },
+    {
+       description => 'multiple sections with whitespace in section headers',
+       expected_cfg => {
+           'foo bar' => {},
+           ' quo  qux ' => {},
+       },
+       raw => <<~EOF,
+       [foo bar]
+       [ quo  qux ]
+       EOF
+    },
+    {
+       description => 'single section with whitespace in section header '
+           . 'and multiple key-value pair',
+       expected_cfg => {
+           'foo bar' => {
+               one => 1,
+               two => 2,
+           },
+           '  quo  ' => {
+               three => 3,
+           },
+       },
+       raw => <<~EOF,
+       [foo bar]
+       one = 1
+       two = 2
+
+       [  quo  ]
+       three = 3
+       EOF
+    },
+    {
+       description => 'single section, numeric key-value pairs',
+       expected_cfg => {
+           foo => {
+               '0' => 0,
+               '1' => 1,
+               '2' => 0,
+               '3' => 1,
+               '3.14' => 1.414,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       0 = 0
+       1 = 1
+       2 = 0
+       3 = 1
+       3.14 = 1.414
+       EOF
+    },
+    {
+       description => 'single section, keys with single-quoted values',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = 'baz'
+       quo = 'qux'
+       EOF
+    },
+    {
+       description => 'single section, keys with double-quoted values',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = "baz"
+       quo = "qux"
+       EOF
+    },
+    {
+       description => 'single section, keys with quoted values, '
+           . 'comment literals within quotes',
+       expected_cfg => {
+           foo => {},
+       },
+       raw => <<~EOF,
+       [foo]
+       one = "1;1"
+       two = "2#2"
+       three = '3;3'
+       four = '4#4'
+       EOF
+    },
+    {
+       description => 'single section, keys with quoted values, '
+           . 'escaped comment literals within quotes',
+       expected_cfg => {
+           foo => {
+               one => '1;1',
+               two => '2#2',
+               three => '3;3',
+               four => '4#4',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       one = "1\\;1"
+       two = "2\\#2"
+       three = '3\\;3'
+       four = '4\\#4'
+       EOF
+    },
+    {
+       description => 'single section, keys with quoted values, '
+           . 'comments after values',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = "baz" ; some comment
+       quo = 'qux'     # another comment
+       EOF
+    },
+    {
+       description => 'single section, keys with quoted values, '
+           . 'continued lines after quotes',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = "baz"\\
+
+       quo = 'qux'\\
+
+       EOF
+    },
+    {
+       description => 'single section, keys with quoted values, '
+           . 'continued lines with comments after quotes',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = "baz"\\
+       # believe it or not, this is valid syntax
+
+       quo = 'qux'\\
+       \\
+       \\
+       \\
+           \t \t ; and it "supports" trailing whitespace for some reason
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with whitespace',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+              \t  bar  \t   =\t \tbaz\t
+
+       quo\t=\tqux
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs without whitespace',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar=baz
+       quo=qux
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with repeated whitespace '
+           . 'in key names',
+       expected_cfg => {
+           foo => {
+               'one_space' => 1,
+               'two_spaces' => 2,
+               'three_spaces' => 3,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       one space = 1
+       two  spaces = 2
+       three  spaces = 3
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with whitespace and '
+           . 'complex whitespace in key names',
+       expected_cfg => {
+           foo => {
+               'one_two' => 'foo',
+               'three_four' => 'bar',
+               'five_six' => 'baz',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+              \t  one two  \t   =\t \tfoo\t
+
+       three   \t  four\t=\tbar\t
+       five     six=baz
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with repeated whitespace '
+           . 'and underlines in key names',
+       expected_cfg => {
+           foo => {
+               'one_ul' => 2,
+               'two_ul' => 0,
+               'two__ul' => 0,
+               'odd___name' => 4,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       # these are equivalent
+       one ul = 0
+       one_ul = 1
+       one             ul = 2
+
+       # these are not
+       two  ul = 0
+       two__ul = 0
+
+       # these are equivalent
+       odd _ name = 0
+       odd      _     name = 1
+       odd__ name = 2
+       odd __name = 3
+       odd___name = 4
+
+       EOF
+    },
+    {
+       description => 'single section with line continuations, multiple key-value pairs',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [\\
+       f\\
+       o\\
+       o\\
+       ]\\
+
+       bar = baz
+       quo = qux
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with continued lines in keys',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar\\
+       = baz
+       \\
+       quo\\
+       \\
+       \\
+       \\
+       \\
+       \\
+       \\
+       = qux
+       EOF
+    },
+    {
+       description => 'multiple sections with escaped comment literals, '
+           . 'multiple key-value pairs',
+       expected_cfg => {
+           'f;oo' => {
+               one => 1,
+               two => 2,
+           },
+           'b#ar' => {
+               three => 3,
+               four => 4,
+           },
+           '###' => {
+               five => 5,
+           },
+           ';;;' => {
+               six => 6,
+           },
+       },
+       raw => <<~EOF,
+       [f\\;oo]
+       one = 1
+       two = 2
+
+       [b\\#ar]
+       three = 3
+       four = 4
+
+       [\\#\\#\\#]
+       five = 5
+
+       [\\;\\;\\;]
+       six = 6
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with comments',
+       expected_cfg => {
+           foo => {
+               bar => 'baz',
+               quo => 'qux',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       ; preceding comment
+       bar = baz # some comment
+       ### comment inbetween
+       ;; another one for good measure
+       quo = qux ; another comment
+       # trailing comment
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with continued lines',
+       expected_cfg => {
+           foo => {
+               bar => 'baz continued baz',
+               quo => "qux continued \tqux",
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = baz \\
+       continued baz
+
+       quo =\\
+       qux \\
+       continued \\
+       \tqux
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with ' .
+           'continued lines and comments',
+       expected_cfg => {
+           foo => {
+               bar => 'baz continued baz',
+               quo => 'qux continued qux',
+               key => 'value',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = baz \\
+       continued baz # comments are allowed here
+
+       quo =\\
+       qux \\
+       continued \\
+       qux # but this continuation will be ignored, because it's in a comment: \\
+       key = value\\
+       # really weird comment
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with ' .
+           'escaped commment literals in values',
+       expected_cfg => {
+           foo => {
+               bar => 'baz#escaped',
+               quo => 'qux;escaped',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = baz\\#escaped
+       quo = qux\\;escaped
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with ' .
+           'continued lines and escaped commment literals in values',
+       expected_cfg => {
+           foo => {
+               bar => 'baz#escaped',
+               quo => 'qux;escaped continued# escaped done',
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       bar = baz\\#escaped
+
+       quo = qux\\;escaped\\
+        continued\\# escaped \\
+       done
+       EOF
+    },
+    {
+       description => 'single section, key-value pairs with escaped comment '
+           . 'literals in key names',
+       expected_cfg => {
+           foo => {
+               'b#a#r' => 'baz',
+               ';q;uo' => 'qux',
+               '#' => 1,
+               '##' => 2,
+               '###' => 3,
+               ';' => 1,
+               ';;' => 2,
+               ';;;' => 3,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       b\\#a\\#r = baz
+       \\;q\\;uo = qux
+
+       \\# = 1
+       \\#\\# = 2
+       \\#\\#\\# = 3
+
+       \\; = 1
+       \\;\\; = 2
+       \\;\\;\\; = 3
+       EOF
+    },
+    {
+       description => 'multiple sections, multiple key-value pairs',
+       expected_cfg => {
+           foo => {
+               one => 1,
+               two => 2,
+           },
+           bar => {
+               three => 3,
+               four => 4,
+           },
+       },
+       raw => <<~EOF,
+       [foo]
+       one = 1
+       two = 2
+       [bar]
+       three = 3
+       four = 4
+       EOF
+    },
+    {
+       description => 'multiple sections, multiple key-value pairs, '
+           . 'comments inline and inbetween, escaped comment literals, '
+           . 'continued lines, arbitrary whitespace',
+       # NOTE: We don't use '/etc/pve/priv/$cluster.$name.keyring' as value for
+       # 'keyring' below, because `ceph-conf` will actually substitute those.
+       # Because we don't care for that (not the parser's or writer's job) we
+       # just omit the dollar signs.
+       expected_cfg => {
+           global => {
+               auth_client_required => 'cephx',
+               auth_cluster_required => 'cephx',
+               auth_service_required => 'cephx',
+               cluster_network => '172.16.65.0/24',
+               fsid => '0e2f72eb-ffff-ffff-ffff-f480790a5b07',
+               mon_allow_pool_delete => 'true',
+               mon_host => '172.16.65.12 172.16.65.13 172.16.65.11',
+               ms_bind_ipv4 => 'true',
+               osd_pool_default_min_size => '2',
+               osd_pool_default_size => '3',
+               public_network => '172.16.65.0/24',
+           },
+           client => {
+               keyring => '/etc/pve/priv/cluster.name.keyring',
+           },
+           'mon.ceph-01' => {
+               public_addr => '172.16.65.11',
+           },
+           'mon.ceph-02' => {
+               public_addr => '172.16.65.12',
+           },
+           'mon.ceph-03' => {
+               public_addr => '172.16.65.13',
+           },
+           'some arbitrary section' => {
+               some_key => 'foo;bar;baz',
+           },
+       },
+       raw => <<~EOF,
+       [global]
+       auth_client_required = cephx
+               auth_cluster_required = \\
+       cephx
+       auth_service_required =           cephx
+                       cluster_network =        172.16.65.0/24
+       fsid = 0e2f72eb-ffff-ffff-ffff-f480790a5b07
+           mon_allow_pool_delete = true
+       mon_host = \\
+       172.16.65.12 \\
+       172.16.65.13 \\
+       172.16.65.11 # why is this one last? nobody knows for sure!
+
+       ms_bind_ipv4 = true
+       osd_pool_default_min_size =\\
+       \\
+       \\
+       \\
+       \\
+       \\
+       \\
+       \\
+       \\
+       2
+       osd_pool_default_size =\\
+                                           3
+       public_network = 172.16.65.0/24 # some comment
+
+       [client] ### another comment
+       keyring = /etc/pve/priv/cluster.name.keyring# cheeky trailing comment
+
+       ## mon config ##
+       [mon.ceph-01]
+       public_addr = 172.16.65.11 ; foo
+
+       ;; another arbitrary comment ;;
+       [mon.ceph-02] ;; very important comment here
+       public_addr = "172.16.65.12" # bar
+
+       [mon.ceph-03]
+       public_addr = '172.16.65.13'                    # baz
+
+       [some arbitrary section]
+       some key = "foo\\;bar\\;baz"    # I am a comment
+       EOF
+    },
+];
+
+sub test_parse_ceph_config {
+    my ($case) = @_;
+
+    my $desc = "parse_ceph_config: $case->{description}";
+
+    my $parse_result = eval { PVE::CephConfig::parse_ceph_config(undef, $case->{raw}) };
+    if ($@) {
+       fail($desc);
+       diag('Failed to parse config:');
+       diag($@);
+       return;
+    }
+
+    if (!is_deeply($parse_result, $case->{expected_cfg}, $desc)) {
+       diag("=== Expected ===");
+       diag(explain($case->{expected_cfg}));
+       diag("=== Got ===");
+       diag(explain($parse_result));
+    }
+}
+
+sub test_write_ceph_config {
+    my ($case) = @_;
+
+    my $desc = "write_ceph_config: $case->{description}";
+
+    my $write_result = eval { PVE::CephConfig::write_ceph_config(undef, $case->{expected_cfg}) };
+    if ($@) {
+       fail($desc);
+       diag('Failed to write config:');
+       diag($@);
+       return;
+    }
+
+    my $parse_result = eval { PVE::CephConfig::parse_ceph_config(undef, $write_result) };
+    if ($@) {
+       fail($desc);
+       diag('Failed to parse previously written config:');
+       diag($@);
+       return;
+    }
+
+    if (!is_deeply($parse_result, $case->{expected_cfg}, $desc)) {
+       diag("=== Expected ===");
+       diag(explain($case->{expected_cfg}));
+       diag("=== Got ===");
+       diag(explain($parse_result));
+       diag("=== Write Output ===");
+       diag($write_result);
+    }
+}
+
+sub main {
+    my $test_subs = [
+       \&test_parse_ceph_config,
+       \&test_write_ceph_config,
+    ];
+
+    plan(tests => scalar($tests->@*) * scalar($test_subs->@*));
+
+    for my $case ($tests->@*) {
+       for my $test_sub ($test_subs->@*) {
+           eval {
+               # suppress warnings here to make output less noisy for certain tests
+               local $SIG{__WARN__} = sub {};
+               $test_sub->($case);
+           };
+           warn "$@\n" if $@;
+       };
+    }
+
+    done_testing();
+}
+
+main();