From e34e05462a0d45c9548d3ee915324e37b905c297 Mon Sep 17 00:00:00 2001 From: Jacob Bachmeyer Date: Fri, 12 May 2023 21:36:24 -0500 Subject: [PATCH] Add "split-zone" keymaster command --- keymaster.pl | 189 +++++++++- testsuite/keymaster.all/31_zonesplit.exp | 457 +++++++++++++++++++++++ testsuite/lib/envutils.exp | 12 + testsuite/lib/tool/keymaster.exp | 9 +- 4 files changed, 663 insertions(+), 4 deletions(-) create mode 100644 testsuite/keymaster.all/31_zonesplit.exp diff --git a/keymaster.pl b/keymaster.pl index c91f619..67b7546 100755 --- a/keymaster.pl +++ b/keymaster.pl @@ -50,6 +50,8 @@ keymaster.pl B keymaster.pl B [--B] +keymaster.pl B I + =head1 OPTIONS =over @@ -100,8 +102,9 @@ backend logic across all of the utilities. =cut +use Cwd qw(realpath); use Errno; -use Fcntl qw(O_RDWR O_WRONLY O_CREAT O_EXCL +use Fcntl qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL LOCK_SH LOCK_EX LOCK_UN); use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG); use FindBin; @@ -255,6 +258,16 @@ sub command_usage_error () { close $config; +=item assert_zones_configured + +Ensure that zones are configured. + +=cut + + sub assert_zones_configured () { + die 'no zones configured' unless @zonelist; + } + =item assert_zone_argument_given_if_needed Ensure that a zone was specified on the command line if zones are @@ -293,6 +306,24 @@ sub assert_config_set { unless defined $ZoneConfig{$item} } } +=item assert_zone_config_set $zone, @items + +Ensure that all ITEMS are set in the zone configuration for ZONE. + +=cut + +sub assert_zone_config_set { + my $zone = shift; + + my $OtherConfig = $Config{'zone.'.$zone}; + die "zone $zone not configured\n" + unless $OtherConfig; + + foreach my $item (@_) + { die "configuration key $item not set for zone $zone\n" + unless defined $OtherConfig->{$item} } +} + =back =head2 Filesystem Utilities @@ -1673,6 +1704,162 @@ sub cmd_rebuild_key_index { return 0; } +=head2 Zone Management Utility + +=head3 split-zone + +=over + +=item split-zone I + +=back + +Transfer configuration and state information about packages that have been +moved to the DESTINATION zone. See the manual for more information. + +=cut + +sub cmd_split_zone { + assert_zones_configured; + assert_zone_argument_given_if_needed; + assert_config_set qw(pkgconfdir pkgstatedir publicdir); + GetOptions() or command_usage_error; + + my $dest = shift @ARGV; + assert_zone_config_set $dest, qw(pkgconfdir pkgstatedir publicdir); + my %DestConfig = %{$Config{'zone.'.$dest}}; + + my %shared = map + { $_ => (realpath($ZoneConfig{$_}) eq realpath($DestConfig{$_})) } + qw(pkgconfdir pkgstatedir publicdir); + + # check destination zone configuration + die "cannot split zone into itself\n" if $OPT{zone} eq $dest; + die "cannot split zones that share publicdir\n" if $shared{publicdir}; + die "nothing to split: zones use same pkgconfdir and pkgstatedir\n" + if $shared{pkgconfdir} && $shared{pkgstatedir}; + + # note packages in both zones + my @source_packages = map(((File::Spec->splitpath($_))[2]), + grep_dir { -d _ && m/^$RE_filename_here$/ } + $ZoneConfig{publicdir}); + my @dest_packages = map(((File::Spec->splitpath($_))[2]), + grep_dir { -d _ && m/^$RE_filename_here$/ } + $DestConfig{publicdir}); + my %dest_packages = map { $_ => 1 } @dest_packages; + my @common_packages = grep $dest_packages{$_}, @source_packages; + my %common_packages = map { $_ => 1 } @common_packages; + + # skip any packages found in both zones, since their configuration and/or + # state information may have diverged + if (@common_packages) { + @source_packages = grep !$common_packages{$_}, @source_packages; + @dest_packages = grep !$common_packages{$_}, @dest_packages; + warn join("\n",'keymaster: packages found in both zones:', + map(' - '.$_, @common_packages), + ' The above packages will be ignored.', ''); + } + + # move configuration for packages + unless ($shared{pkgconfdir}) { + my @move_conf = + map File::Spec->catdir($ZoneConfig{pkgconfdir}, $_), + grep -d File::Spec->catdir($ZoneConfig{pkgconfdir}, $_) + && !-d File::Spec->catdir($DestConfig{pkgconfdir}, $_), + @dest_packages; + mkdir_p $DestConfig{pkgconfdir}; + die 'moving configuration trees failed' + if system ('/bin/mv', @move_conf, $DestConfig{pkgconfdir}) != 0; + } + + # move timestamp records + unless ($shared{pkgstatedir}) { + # This is done last, since it will throw an error if the source serials + # list does not exist. That list should always exist, but the error is + # harmless if all other work has already been done. + my $source_serials_file_name = + File::Spec->catfile($ZoneConfig{pkgstatedir}, 'serials'); + my $dest_serials_file_name = + File::Spec->catfile($DestConfig{pkgstatedir}, 'serials'); + my $source_scratch_file_name = + File::Spec->catfile($ZoneConfig{pkgstatedir}, 'serials.scratch'); + + my $source_serials_flag_name = + File::Spec->catfile($ZoneConfig{pkgstatedir}, 'serials.flag'); + my $dest_serials_flag_name = + File::Spec->catfile($DestConfig{pkgstatedir}, 'serials.flag'); + + # acquire the serials list locks + open my $source_serials_flag, '>', $source_serials_flag_name + or die "open flag file $source_serials_flag_name: $!"; + open my $dest_serials_flag, '>', $dest_serials_flag_name + or die "open flag file $dest_serials_flag_name: $!"; + flock $source_serials_flag, LOCK_EX or die "lock source flag: $!"; + flock $dest_serials_flag, LOCK_EX or die "lock destination flag: $!"; + + # open the serials list files, creating destination if needed + sysopen my $source_serials, $source_serials_file_name, O_RDONLY + or die "open source serials list: $!"; + sysopen my $dest_serials, $dest_serials_file_name, + O_RDWR|O_CREAT|O_APPEND, 0666 + or die "open destination serials list: $!"; + + # read headers and verify matching format + my $source_header = ''; my $dest_header = ''; + my $source_first_record; + while (<$source_serials>) { + if (/^#/) { $source_header .= $_ } + else { $source_first_record = $_; last } + } + while (<$dest_serials>) { + if (/^#/) { $dest_header .= $_ } + else { last } # leaves first record of destination in $_ + } + if ($dest_header eq '' && !$_) { + # destination file is empty; copy header from source + print $dest_serials $source_header; + } elsif ($dest_header ne $source_header) { + die "format mismatch between serials list files"; + } + + # open a scratchpad file in the source zone + sysopen my $scratchpad, $source_scratch_file_name, + O_WRONLY|O_CREAT|O_EXCL, 0666 + or die "open scratchpad $source_scratch_file_name: $!"; + + # split the source serials list between the destination and a scratchpad + die "invalid line in source serials list: $source_first_record" + unless ($source_first_record + =~ m{(?:\A|(?) { + die "invalid line in source serials list: $_" + unless m{(?:\A|(?. + +# ---------------------------------------- + +array set configuration_file { + plain {# test configuration + pkgconfdir = [file join $tenv packages] + keypubdir = [file join $tenv keyrings] + } + partial {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv foo packages] + pkgstatedir = [file join $tenv foo state] + publicdir = [file join $tenv foo pub] + + \[zone.bar\] + pkgconfdir = [file join $tenv bar packages] + publicdir = [file join $tenv bar pub] + } + shared-public {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv foo packages] + pkgstatedir = [file join $tenv foo state] + publicdir = [file join $tenv pub] + keypubdir = [file join $tenv foo keyrings] + + \[zone.bar\] + pkgconfdir = [file join $tenv bar packages] + pkgstatedir = [file join $tenv bar state] + publicdir = [file join $tenv pub] + keypubdir = [file join $tenv bar keyrings] + } + shared-configuration {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv packages] + pkgstatedir = [file join $tenv foo state] + publicdir = [file join $tenv foo pub] + keypubdir = [file join $tenv foo keyrings] + + \[zone.bar\] + pkgconfdir = [file join $tenv packages] + pkgstatedir = [file join $tenv bar state] + publicdir = [file join $tenv bar pub] + keypubdir = [file join $tenv bar keyrings] + } + shared-state {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv foo packages] + pkgstatedir = [file join $tenv state] + publicdir = [file join $tenv foo pub] + keypubdir = [file join $tenv foo keyrings] + + \[zone.bar\] + pkgconfdir = [file join $tenv bar packages] + pkgstatedir = [file join $tenv state] + publicdir = [file join $tenv bar pub] + keypubdir = [file join $tenv bar keyrings] + } + shared-both {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv packages] + pkgstatedir = [file join $tenv state] + publicdir = [file join $tenv foo pub] + keypubdir = [file join $tenv foo keyrings] + + \[zone.bar\] + pkgconfdir = [file join $tenv packages] + pkgstatedir = [file join $tenv state] + publicdir = [file join $tenv bar pub] + keypubdir = [file join $tenv bar keyrings] + } + full-split {# test configuration + \[zone.foo\] + pkgconfdir = [file join $tenv foo packages] + pkgstatedir = [file join $tenv foo state] + publicdir = [file join $tenv foo pub] + keypubdir = [file join $tenv foo keyrings] + + \[zone.bar\] + pkgconfdir = [file join $tenv bar packages] + pkgstatedir = [file join $tenv bar state] + publicdir = [file join $tenv bar pub] + keypubdir = [file join $tenv bar keyrings] + } +} + +# ---------------------------------------- + +with_test_environment tenv $configuration_file(plain) { + run_keymaster "split-zone: zones not configured" \ + {2 {{^keymaster: no zones configured}}} $tenv \ + split-zone +} + +with_test_environment tenv $configuration_file(partial) { + run_keymaster "split-zone: destination not configured" \ + {2 {{^keymaster: zone baz not configured}}} $tenv \ + -z foo split-zone baz + + run_keymaster "split-zone: partial configuration for destination" \ + {2 {{^keymaster: configuration key pkgstatedir not set for zone bar}}}\ + $tenv \ + -z foo split-zone bar +} + +with_test_environment tenv $configuration_file(full-split) { + run_keymaster "split-zone: unknown option" \ + {2 {{^Unknown option:}}} $tenv \ + -z foo split-zone --bogus-argument-for-testing-error + + run_keymaster "bogus: split foo into foo" \ + {2 {"cannot split zone into itself"}} $tenv \ + -z foo split-zone foo +} + +with_test_environment tenv $configuration_file(shared-public) { + run_keymaster "bogus: split zones with same publicdir" \ + {2 {"cannot split zones that share publicdir"}} $tenv \ + -z foo split-zone bar +} + +with_test_environment tenv $configuration_file(shared-both) { + run_keymaster "bogus: nothing to split" \ + {2 {"nothing to split"}} $tenv \ + -z foo split-zone bar +} + +# ---------------------------------------- + +# The "full split" and "shared configuration" tests are run both with and +# without an initial serials file. (The initial serials file should always +# exist in practice, but the error if it does not is harmless, as the other +# work has already been done.) + +# test full split without initial serials file +with_test_environment tenv $configuration_file(full-split) { + register_test_packages [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the configuration. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 } + } + + run_keymaster "split-zone: full split, no serials" \ + {2 {{open source serials list}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: full split, no serials: foo remains" \ + [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + } + analyze_test_packages \ + "split-zone: full split, no serials: bar moved" \ + [file join $tenv bar] { + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } +} + + +# test full split with initial serials file +with_test_environment tenv $configuration_file(full-split) { + register_test_packages [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + write_serials_v0 [file join $tenv foo state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the configuration. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 "May 11 21:16:44 CDT 2023" } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 "May 10 20:00:44 CDT 2023" } + } + + run_keymaster "split-zone: full split, with serials" \ + {0 {{^$}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: full split, with serials: foo remains" \ + [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + } + check_serials_v0 \ + "split-zone: full split, with serials: foo serials remain" \ + [file join $tenv foo state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + } + analyze_test_packages \ + "split-zone: full split, with serials: bar moved" \ + [file join $tenv bar] { + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + check_serials_v0 \ + "split-zone: full split, with serials: bar serials moved" \ + [file join $tenv bar state serials] { + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } +} + +# test shared configuration without initial serials file +with_test_environment tenv $configuration_file(shared-configuration) { + register_test_packages $tenv { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the state records. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 } + } + + run_keymaster "split-zone: common config, no serials" \ + {2 {{open source serials list}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: common config, no serials: config" \ + $tenv { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } +} + + +# test shared configuration with initial serials file +with_test_environment tenv $configuration_file(shared-configuration) { + register_test_packages $tenv { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + write_serials_v0 [file join $tenv foo state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the state records. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 "May 11 21:16:44 CDT 2023" } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 "May 10 20:00:44 CDT 2023" } + } + + run_keymaster "split-zone: common config, with serials" \ + {0 {{^$}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: common config, with serials: config" \ + $tenv { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + check_serials_v0 \ + "split-zone: common config, with serials: foo serials remain" \ + [file join $tenv foo state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + } + check_serials_v0 \ + "split-zone: common config, with serials: bar serials moved" \ + [file join $tenv bar state serials] { + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } +} + +# The "shared state" tests are also run both with and without in initial +# serials file, although its presence or absence has no effect in these +# cases and the keymaster does not check. + +# test shared state without initial serials file +with_test_environment tenv $configuration_file(shared-state) { + register_test_packages [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the configuration. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 } + } + + run_keymaster "split-zone: common state, no serials" \ + {0 {{^$}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: common state, no serials: foo remains" \ + [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + } + analyze_test_packages \ + "split-zone: common state, no serials: bar moved" \ + [file join $tenv bar] { + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } +} + + +# test shared state with initial serials file +with_test_environment tenv $configuration_file(shared-state) { + register_test_packages [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + + write_serials_v0 [file join $tenv state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } + + # The public files for zone "bar" have already been moved. This is + # used as the indication to also move the configuration. + make_dummy_files [file join $tenv foo pub] { + foo/foo-1.0.bin { good 01 1000 "May 11 21:16:44 CDT 2023" } + } + make_dummy_files [file join $tenv bar pub] { + bar/bar-1.0.bin { good 02 1001 "May 10 20:00:44 CDT 2023" } + } + + run_keymaster "split-zone: common state, with serials" \ + {0 {{^$}}} $tenv \ + -z foo split-zone bar + + analyze_test_packages \ + "split-zone: common state, with serials: foo remains" \ + [file join $tenv foo] { + foo { + email { foo@example.org } + keys { { id 1000 name "foo " } } + } + } + analyze_test_packages \ + "split-zone: common state, with serials: bar moved" \ + [file join $tenv bar] { + bar { + email { bar@example.org } + keys { { id 1001 name "bar " } } + } + } + check_serials_v0 "split-zone: common state, with serials: serials" \ + [file join $tenv state serials] { + {foo/foo-1.0.bin "May 11 21:16:48 CDT 2023"} + {bar/bar-1.0.bin "May 10 20:00:48 CDT 2023"} + } +} + +# ---------------------------------------- + +#EOF diff --git a/testsuite/lib/envutils.exp b/testsuite/lib/envutils.exp index 6779941..b65fc44 100644 --- a/testsuite/lib/envutils.exp +++ b/testsuite/lib/envutils.exp @@ -137,4 +137,16 @@ proc register_test_packages { base_dir packlist } { } } +proc make_dummy_files { base_dir filelist } { + foreach {file sig} $filelist { + file mkdir [file dirname [file join $base_dir $file]] + put_file [file join $base_dir $file] "${file}\n" + put_file [file join $base_dir "${file}.sig"] \ + [sign_test_file [file tail $file] \ + [eval [list make_test_signature] $sig]] + age_file [file join $base_dir $file] "10 minutes ago" + age_file [file join $base_dir "${file}.sig"] "10 minutes ago" + } +} + # EOF diff --git a/testsuite/lib/tool/keymaster.exp b/testsuite/lib/tool/keymaster.exp index 0bd0a07..6cb4502 100644 --- a/testsuite/lib/tool/keymaster.exp +++ b/testsuite/lib/tool/keymaster.exp @@ -56,6 +56,7 @@ proc with_test_environment { env_name config body } { load_lib mockgpg.exp load_lib envutils.exp load_lib keyindex.exp +load_lib serials.exp proc run_keymaster { test expected base_dir args } { global KEYMASTER_TOOL PERL CHECK_COVERAGE @@ -127,9 +128,11 @@ proc run_keymaster { test expected base_dir args } { proc check_list_file_contents { test file items } { lappend linelist - set chan [open $file] - while { [gets $chan line] >= 0 } { lappend linelist $line } - close $chan + if { [catch { open $file } chan] == 0 } { + while { [gets $chan line] >= 0 } { lappend linelist $line } + close $chan + } + # a list file that does not exist is treated as empty if { [lsort $items] eq [lsort $linelist] } { pass $test -- 2.25.1