IUK test suite: preserve xattrs when unpacking test SquashFS
[tails.git] / config / chroot_local-includes / usr / src / iuk / features / create / step_definitions / Create_steps.pl
blob6ce73c38addf190afb11b8a7c12eb696b4ed8482
1 #!perl
3 use strictures 2;
5 use lib "lib";
7 use Carp;
8 use Carp::Assert;
9 use Cwd;
10 use Data::Dumper;
11 use English qw{-no_match_vars};
12 use Function::Parameters;
13 use IPC::System::Simple qw{capture capturex systemx $EXITVAL EXIT_ANY};
14 use Test::More;
15 use Test::BDD::Cucumber::StepFile;
17 use Path::Tiny;
18 use Types::Path::Tiny qw{Path};
20 use Tails::IUK;
21 use Tails::IUK::Read;
22 use Tails::IUK::Utils qw{run_as_root};
25 my $bindir = path(__FILE__)->parent->parent->parent->parent->child('bin')->absolute;
26 use Env qw{@PATH};
27 unshift @PATH, $bindir;
29 Given qr{^a usable temporary directory$}, fun ($c) {
30 my $dirname = Path::Tiny->tempdir(CLEANUP => 0);
31 $c->{stash}->{scenario}->{tempdir} = $dirname;
32 ok(defined($dirname));
35 fun inject_new_bootloader_bits_into($dir) {
36 for (qw{EFI/BOOT/bootx64.efi utils/linux/syslinux}) {
37 my $injected_file = path($dir, $_);
38 $injected_file->parent->mkpath;
39 $injected_file->touch;
40 ok($injected_file->is_file);
44 fun geniso($srcdir, $outfile) {
45 path($srcdir, 'isolinux')->mkpath;
46 assert(path($srcdir, 'isolinux')->is_dir);
47 path($srcdir, 'isolinux', 'isolinux.cfg')->spew(
48 "bla\n bli/isolinux/blu\n\n\n bla/isolinux/"
50 assert(path($srcdir, 'isolinux', 'isolinux.cfg')->exists);
52 path($srcdir, 'live')->mkpath;
53 if (! -e path($srcdir, 'live', 'filesystem.squashfs')) {
54 my $squashfs_tempdir = Path::Tiny->tempdir;
55 # an empty SquashFS is invalid
56 path($squashfs_tempdir, '.placeholder')->touch;
57 capture("gensquashfs --quiet --pack-dir '$squashfs_tempdir' '$srcdir/live/filesystem.squashfs' 2>/dev/null");
59 capture(EXIT_ANY,
60 "genisoimage --quiet -J -l -cache-inodes -allow-multidot -o '$outfile' '$srcdir' 2>/dev/null");
61 $EXITVAL == 0
64 Given qr{^(an old|a new) ISO image(?: that does not contain file "([^"]+)")?$}, fun ($c) {
65 my $generation = $c->matches->[0] eq 'an old' ? 'old' : 'new';
66 my $file = $c->matches->[1];
67 my $basename = $generation eq 'old' ? 'old.iso' : 'new.iso';
68 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
69 my $iso_tempdir = Path::Tiny->tempdir;
70 if (defined $file) {
71 assert(length $file);
72 ok(! -e path($iso_tempdir, $file));
74 inject_new_bootloader_bits_into($iso_tempdir) if $generation eq 'new';
75 ok(geniso($iso_tempdir, $filename));
78 Given qr{^(an old|a new) ISO image that contains file "([^"]+)"$}, fun ($c) {
79 my $generation = $c->matches->[0];
80 my $file = $c->matches->[1];
81 my $basename = $generation eq 'an old' ? 'old.iso' : 'new.iso';
82 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
83 my $iso_tempdir = Path::Tiny->tempdir;
84 my $file_in_iso = path($iso_tempdir, $file);
85 $file_in_iso->parent->mkpath();
86 $file_in_iso->touch;
87 ok($file_in_iso->is_file);
88 inject_new_bootloader_bits_into($iso_tempdir) if $generation eq 'new';
89 ok(geniso($iso_tempdir, $filename));
92 Given qr{^(?:two identical ISO images|two ISO images that contain the same set of kernels|two ISO images that contain the same bootloader configuration)$}, fun ($c) {
93 for my $generation (qw{old new}) {
94 my $basename = $generation.'.iso';
95 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
96 my $iso_tempdir = Path::Tiny->tempdir;
97 path($iso_tempdir, 'live')->mkpath;
98 inject_new_bootloader_bits_into($iso_tempdir);
99 geniso($iso_tempdir, $filename) or croak "Failed to generate ISO image.";
102 -f path($c->{stash}->{scenario}->{tempdir}, 'old.iso')
103 && -f path($c->{stash}->{scenario}->{tempdir}, 'new.iso')
107 Given qr{^two ISO images when the kernel was upgraded$}, fun ($c) {
108 for my $generation (qw{old new}) {
109 my $basename = $generation.'.iso';
110 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
111 my $iso_tempdir = Path::Tiny->tempdir;
112 path($iso_tempdir, 'live')->mkpath;
113 for (qw{vmlinuz initrd.img}) {
114 path($iso_tempdir, 'live', $_)->spew($generation);
116 inject_new_bootloader_bits_into($iso_tempdir) if $generation eq 'new';
117 geniso($iso_tempdir, $filename) or croak "Failed to generate ISO image.";
121 -f path($c->{stash}->{scenario}->{tempdir}, 'old.iso')
122 && -f path($c->{stash}->{scenario}->{tempdir}, 'new.iso')
126 Given qr{^two ISO images when a new kernel was added$}, fun ($c) {
127 for my $generation (qw{old new}) {
128 my $basename = $generation.'.iso';
129 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
130 my $iso_tempdir = Path::Tiny->tempdir;
131 path($iso_tempdir, 'live')->mkpath;
132 for (qw{vmlinuz initrd.img}) {
133 path($iso_tempdir, 'live', $_)->spew("same content");
135 if ($generation eq 'new') {
136 for (qw{vmlinuz2 initrd2.img}) {
137 path($iso_tempdir, 'live', $_)->spew($generation);
139 inject_new_bootloader_bits_into($iso_tempdir);
141 geniso($iso_tempdir, $filename) or croak "Failed to generate ISO image.";
145 -f path($c->{stash}->{scenario}->{tempdir}, 'old.iso')
146 && -f path($c->{stash}->{scenario}->{tempdir}, 'new.iso')
150 Given qr{^(an old|a new) ISO image whose filesystem.squashfs( does not|) contains? file "([^"]+)"(?:| modified at ([0-9]+)| owned by ([a-z-]+))$}, fun ($c) {
151 my $generation = $c->matches->[0] eq 'an old' ? 'old' : 'new';
152 my $contains = $c->matches->[1] eq "" ? 1 : 0;
153 my $file = $c->matches->[2];
154 my ($mtime, $owner);
155 if (defined $c->matches->[3] && $c->matches->[3] =~ m{\A[0-9]+\z}) {
156 $mtime = $c->matches->[3];
157 } elsif (defined $c->matches->[4] && $c->matches->[4] =~ m{\A[a-z-]+\z}) {
158 $owner = $c->matches->[4];
161 my $iso_basename = $generation eq 'old' ? 'old.iso' : 'new.iso';
162 my $iso_filename = path($c->{stash}->{scenario}->{tempdir}, $iso_basename);
163 my $iso_tempdir = Path::Tiny->tempdir;
164 my $squashfs_tempdir = Path::Tiny->tempdir;
165 # an empty SquashFS is invalid
166 path($squashfs_tempdir, '.placeholder')->touch;
167 if ($contains) {
168 path($squashfs_tempdir, $file)->parent->mkpath();
169 path($squashfs_tempdir, $file)->touch;
170 utime($mtime, $mtime, path($squashfs_tempdir, $file)) if defined($mtime);
171 run_as_root('chown', $owner, path($squashfs_tempdir, $file)) if defined($owner);
173 path($iso_tempdir, 'live')->mkpath();
174 systemx('sudo', 'chmod', '-R', 'go+rwX', $squashfs_tempdir);
175 systemx(
176 'gensquashfs', '--quiet', '--keep-time',
177 '--pack-dir', $squashfs_tempdir,
178 $iso_tempdir->child('live/filesystem.squashfs')
180 inject_new_bootloader_bits_into($iso_tempdir) if $generation eq 'new';
181 ok(geniso($iso_tempdir, $iso_filename));
184 Given qr{^two ISO images that do not contain the same bootloader configuration$}, fun ($c) {
185 for my $generation (qw{old new}) {
186 my $basename = $generation.'.iso';
187 my $filename = path($c->{stash}->{scenario}->{tempdir}, $basename);
188 my $iso_tempdir = Path::Tiny->tempdir;
189 path($iso_tempdir, 'isolinux')->mkpath;
190 for (qw{live.cfg}) {
191 path($iso_tempdir, 'isolinux', $_)->spew($generation);
193 inject_new_bootloader_bits_into($iso_tempdir) if $generation eq 'new';
194 geniso($iso_tempdir, $filename) or croak "Failed to generate ISO image.";
198 -f path($c->{stash}->{scenario}->{tempdir}, 'old.iso')
199 && -f path($c->{stash}->{scenario}->{tempdir}, 'new.iso')
203 When qr{^I create an IUK$}, fun ($c) {
204 my %args;
205 $c->{stash}->{scenario}->{squashfs_diff_name} =
206 'Tails_amd64_0.11.1_to_0.11.2.squashfs';
208 my $iuk_path = path($c->{stash}->{scenario}->{tempdir}, 'test.iuk');
210 my $cmdline =
211 # on overlayfs, deleted files are stored using character devices,
212 # that one needs to be root to create
213 "sudo SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH} " .
214 path($bindir, "tails-create-iuk") .
215 ' --old_iso "' .
216 path($c->{stash}->{scenario}->{tempdir}, 'old.iso') . '" ' .
217 ' --new_iso "' .
218 path($c->{stash}->{scenario}->{tempdir}, 'new.iso') . '"' .
219 ' --squashfs_diff_name "'.$c->{stash}->{scenario}->{squashfs_diff_name}.'"' .
220 ' --outfile "' . $iuk_path . '"'
223 if (exists $c->{stash}->{scenario}->{squashfs_diff}) {
224 $cmdline .=
225 ' --squashfs_diff "' . $c->{stash}->{scenario}->{squashfs_diff} . '"';
228 $c->{stash}->{scenario}->{create_output} = capture(
229 EXIT_ANY,
230 "umask 077 && $cmdline 2>&1"
232 $c->{stash}->{scenario}->{create_exit_code} = $EXITVAL;
234 $c->{stash}->{scenario}->{create_exit_code} == 0
235 or warn $c->{stash}->{scenario}->{create_output};
237 $c->{stash}->{scenario}->{iuk_path} = $iuk_path;
239 my $iuk_in = Tails::IUK::Read->new_from_file($iuk_path);
241 my @gids = split(/ /, $GID);
242 system(qw{sudo chown}, "$UID:$gids[0]", $iuk_path);
243 ${^CHILD_ERROR_NATIVE} == 0 or croak "Could not chown '$iuk_path': $!";
245 $c->{stash}->{scenario}->{iuk_in} = $iuk_in;
246 ok(defined($iuk_in));
249 Then qr{^the created IUK is a SquashFS image$}, fun ($c) {
250 system('rdsquashfs --list / ' . $c->{stash}->{scenario}->{iuk_path} . '>/dev/null 2>&1');
251 is(${^CHILD_ERROR_NATIVE}, 0, "The generated IUK is not a SquashFS image");
254 Then qr{^the saved IUK contains a "([^"]*)" file$}, fun ($c) {
255 my $file = path($c->matches->[0]);
256 ok($c->{stash}->{scenario}->{iuk_in}->contains_file($file));
259 fun squashfs_contains_only_files_owned_by ($squashfs_filename, $owner, $group) {
260 map { like(
263 \A # at the beginning of the string
264 (?:file|dir) # file type
265 [[:space:]]+
266 .+? # path
267 [[:space:]]+
268 [[:digit:]]+ # permissions
269 [[:space:]]+
270 $owner # UID
271 [[:space:]]+
272 $group # GID
273 }xms,
274 "line looks like a file description with owner $owner and group $group"
275 ) } split(/\n/, `rdsquashfs --quiet --describe '$squashfs_filename'`);
278 Then qr{^all files in the saved IUK belong to owner 0 and group 0$}, fun ($c) {
279 ok(squashfs_contains_only_files_owned_by(
280 $c->{stash}->{scenario}->{iuk_in}->file,
286 Then qr{^the "([^"]+)" file in the saved IUK contains "([^"]*)"$}, fun ($c) {
287 my $file = $c->matches->[0];
288 my $expected_content = $c->matches->[1];
290 $c->{stash}->{scenario}->{iuk_in}->get_content(path($file)),
291 $expected_content
295 fun _file_content_in_iuk_like(
296 $iuk_in, Path $filename, $regexp, $should_match
298 assert(defined $iuk_in && defined $filename);
299 assert(defined $regexp && defined $should_match);
301 unless ($iuk_in->contains_file($filename)) {
302 warn "The IUK does not contain $filename, so we can't check its content.";
303 return;
306 my $content = $iuk_in->get_content($filename);
308 if ($should_match) {
309 return $content =~ m{$regexp}xms;
311 else {
312 return $content !~ m{$regexp}xms;
316 fun file_content_in_iuk_like($iuk_in, Path $filename, $regexp) {
317 assert(defined $iuk_in && defined $regexp);
318 _file_content_in_iuk_like(@_, 1);
321 fun file_content_in_iuk_unlike($iuk_in, Path $filename, $regexp) {
322 assert(defined $iuk_in && defined $regexp);
323 _file_content_in_iuk_like(@_, 0);
326 fun squashfs_in_iuk_contains(:$iuk_in, :$squashfs_name, :$expected_file,
327 :$expected_mtime, :$expected_owner) {
328 my $squashfs_path = path('overlay', 'live', $squashfs_name);
329 die "SquashFS '$squashfs_name' not found in the IUK"
330 unless $iuk_in->contains_file($squashfs_path);
332 my $orig_cwd = getcwd;
333 my $tempdir = Path::Tiny->tempdir;
334 chdir $tempdir;
335 $tempdir->child('squashfs-root')->mkpath;
336 capturex(EXIT_ANY,
337 # on overlayfs, deleted files are stored using character devices,
338 # that one needs to be root to create
339 'sudo',
340 'rdsquashfs', '--quiet', '--set-times', '--set-xattr', '--chown',
341 '--unpack-root', $tempdir->child('squashfs-root'),
342 '--unpack-path', "/",
343 $iuk_in->mountpoint->child($squashfs_path),
345 my $exists = $tempdir->child('squashfs-root', $expected_file)->exists;
346 chdir $orig_cwd;
348 # Ensure $tempdir can be cleaned up and the $expected_mtime test can access
349 # the file it needs to
350 my @gids = split(/ /, $GID);
351 systemx(
352 qw{sudo chmod -R go+rwX},
353 $tempdir->child('squashfs-root')
356 return unless $exists;
358 if (defined $expected_mtime) {
359 $expected_mtime = $ENV{SOURCE_DATE_EPOCH} if $expected_mtime eq 'SOURCE_DATE_EPOCH';
360 return unless $expected_mtime == $tempdir->child('squashfs-root', $expected_file)->stat->mtime
363 if (defined $expected_owner) {
364 return unless $expected_owner eq getpwuid($tempdir->child('squashfs-root', $expected_file)->stat->uid)
367 return 1;
370 fun squashfs_in_iuk_deletes($iuk_in, $squashfs_name, $deleted_file) {
371 my $squashfs_path = path('overlay', 'live', $squashfs_name);
372 my $orig_cwd = getcwd;
373 my $tempdir = Path::Tiny->tempdir;
374 chdir $tempdir;
375 die "SquashFS '$squashfs_name' not found in the IUK"
376 unless $iuk_in->contains_file($squashfs_path);
378 my $old_dir = Path::Tiny->tempdir;
379 path($old_dir, $deleted_file)->touch;
381 my $new_dir = Path::Tiny->tempdir;
382 capturex(
383 # on overlayfs, deleted files are stored using character devices,
384 # that one needs to be root to create
385 'sudo',
386 "rdsquashfs", '--quiet', "--unpack-root", $new_dir,
387 '--unpack-path', '.',
388 $iuk_in->mountpoint->child($squashfs_path),
390 chdir $orig_cwd;
392 # Ensure $new_dir can be cleaned up
393 my @gids = split(/ /, $GID);
394 systemx(qw{sudo chown -R}, "$UID:$gids[0]", $new_dir);
396 my $union_basedir = Path::Tiny->tempdir;
397 my $union_workdir = path($union_basedir, 'work');
398 my $union_mountpoint = path($union_basedir, 'mount');
399 $_->mkpath for ($union_workdir, $union_mountpoint);
400 my @mount_args = (
401 '-t', 'overlay',
402 '-o', sprintf("noatime,lowerdir=%s,upperdir=%s,workdir=%s",
403 $old_dir, $new_dir, $union_workdir),
404 'overlay'
407 capturex(
408 qw{sudo -n mount}, @mount_args,
409 $union_mountpoint
412 my $exists = -e path($union_mountpoint, $deleted_file);
413 system(qw{sudo umount}, "$union_mountpoint");
414 return ! $exists;
417 Then qr{^the saved IUK contains an? "([^"]+)" directory$}, fun ($c) {
418 my $dir = $c->matches->[0];
419 ok($c->{stash}->{scenario}->{iuk_in}->mountpoint->child($dir)->is_dir);
422 fun overlay_directory_in_iuk_contains($iuk_in, $expected_file) {
423 grep { 'overlay/' . $expected_file eq $_->stringify } $iuk_in->list_files;
426 Then qr{^the overlay directory in the saved IUK (contains|does not contain) "([^"]+)"$}, fun ($c) {
427 my $should_contain = $c->matches->[0] eq 'contains';
428 my $expected_file = $c->matches->[1];
430 $should_contain ?
431 ok(overlay_directory_in_iuk_contains(
432 $c->{stash}->{scenario}->{iuk_in},
433 $expected_file,
436 ok(! overlay_directory_in_iuk_contains(
437 $c->{stash}->{scenario}->{iuk_in},
438 $expected_file,
442 Then qr{^the delete_files list (contains|does not contain) "([^"]+)"$}, fun ($c) {
443 my $wanted = $c->matches->[0] eq 'contains' ? 1 : 0;
444 my $expected_filename = $c->matches->[1];
447 scalar(grep {
448 $_ eq $expected_filename
449 } @{$c->{stash}->{scenario}->{iuk_in}->delete_files}),
450 $wanted
454 Then qr{^the delete_files list is empty$}, fun ($c) {
455 is($c->{stash}->{scenario}->{iuk_in}->delete_files_count, 0);
458 Then qr{^the saved IUK contains a SquashFS that contains file "([^"]+)"(?:| modified at ([0-9]+|SOURCE_DATE_EPOCH)| owned by ([a-z-]+))$}, fun ($c) {
459 my $expected_file = $c->matches->[0];
460 my ($expected_mtime, $expected_owner);
461 if (defined $c->matches->[1] && $c->matches->[1] =~ m{\A[0-9]+\z}) {
462 $expected_mtime = $c->matches->[1];
463 } elsif (defined $c->matches->[1] && $c->matches->[1] eq "SOURCE_DATE_EPOCH") {
464 $expected_mtime = $c->matches->[1];
465 } elsif (defined $c->matches->[2] && $c->matches->[2] =~ m{\A[a-z-]+\z}) {
466 $expected_owner = $c->matches->[2];
467 } else {
468 croak "Test suite implementation error";
471 ok(squashfs_in_iuk_contains(
472 iuk_in => $c->{stash}->{scenario}->{iuk_in},
473 squashfs_name => $c->{stash}->{scenario}->{squashfs_diff_name},
474 expected_file => $expected_file,
475 expected_mtime => $expected_mtime,
476 expected_owner => $expected_owner,
480 Then qr{^the overlay directory in the saved IUK contains a SquashFS diff$}, fun ($c) {
481 ok(overlay_directory_in_iuk_contains(
482 $c->{stash}->{scenario}->{iuk_in},
483 path('live', $c->{stash}->{scenario}->{squashfs_diff_name}),
487 Then qr{^the saved IUK contains a SquashFS that deletes file "([^"]+)"$}, fun ($c) {
488 my $deleted_file = $c->matches->[0];
490 ok(squashfs_in_iuk_deletes(
491 $c->{stash}->{scenario}->{iuk_in},
492 $c->{stash}->{scenario}->{squashfs_diff_name},
493 $deleted_file
497 Then qr{^the saved IUK contains the new bootloader configuration$}, fun ($c) {
498 my $live_cfg_path = path('overlay/syslinux/live.cfg');
500 $c->{stash}->{scenario}->{iuk_in}->get_content($live_cfg_path),
501 'new'
505 Then qr{^the overlay directory contains an upgraded syslinux configuration$}, fun ($c) {
506 my @wanted_files = qw{syslinux/syslinux.cfg EFI/BOOT/bootx64.efi utils/linux/syslinux};
507 my @unwanted_files = qw{syslinux/isolinux.cfg};
509 map {
510 ok(overlay_directory_in_iuk_contains(
511 $c->{stash}->{scenario}->{iuk_in}, $_,
512 ), "the overlay directory contains '$_'");
513 } @wanted_files;
515 map {
516 ok(! overlay_directory_in_iuk_contains(
517 $c->{stash}->{scenario}->{iuk_in}, $_,
518 ), "the overlay directory does not contain '$_'");
519 } @unwanted_files;
521 ok(file_content_in_iuk_like(
522 $c->{stash}->{scenario}->{iuk_in},
523 path("overlay/syslinux/syslinux.cfg"),
524 qr{/syslinux/}
525 ), "overlay/syslinux/syslinux.cfg contains /syslinux/");
527 ok(file_content_in_iuk_unlike(
528 $c->{stash}->{scenario}->{iuk_in},
529 path("overlay/syslinux/syslinux.cfg"),
530 qr{/isolinux/}
531 ), "overlay/syslinux/syslinux.cfg does not contain /isolinux/");