Revert "Test Suite: Use Dogtail for some Greeter steps"
[tails.git] / features / step_definitions / usb.rb
blobdead7516746a848de22f93a9aee3d6a52b7db9e7
1 require 'securerandom'
3 # Returns a hash that for each persistence preset the running Tails is aware of,
4 # for each of the corresponding configuration lines,
5 # maps the source to the destination.
6 def get_persistence_presets_config(skip_links = false)
7   # Python script that prints all persistence configuration lines (one per
8   # line) in the form: <mount_point>\t<comma-separated-list-of-options>
9   script = [
10     'from tps.configuration import features',
11     'for feature in features.get_classes():',
12     '    for mount in feature.Mounts:',
13     '        print(mount)',
14   ]
15   c = RemoteShell::PythonCommand.new($vm, script.join("\n"))
16   assert(c.success?, 'Python script for get_persistence_presets_config failed')
17   presets_configs = c.stdout.chomp.split("\n")
18   assert presets_configs.size >= 10,
19          "Got #{presets_configs.size} persistence preset configuration " \
20          'lines, which is too few'
21   persistence_mapping = {}
22   presets_configs.each do |line|
23     destination, options_str = line.split("\t")
24     options = options_str.split(',')
25     is_link = options.include? 'link'
26     next if is_link && skip_links
28     source_str = options.find { |option| /^source=/.match(option) }
29     # If no source is given as an option, live-boot's persistence
30     # feature defaults to the destination minus the initial "/".
31     source = if source_str.nil?
32                destination.partition('/').last
33              else
34                source_str.split('=')[1]
35              end
36     persistence_mapping[source] = destination
37   end
38   persistence_mapping
39 end
41 def persistent_dirs
42   get_persistence_presets_config
43 end
45 def persistent_mounts
46   get_persistence_presets_config(true)
47 end
49 def persistent_volumes_mountpoints
50   $vm.execute('ls -1 -d /live/persistence/*_unlocked/').stdout.chomp.split
51 end
53 # Returns the list of mountpoints which are configured in persistence.conf
54 def configured_persistent_mountpoints
55   $vm.file_content(
56     '/live/persistence/TailsData_unlocked/persistence.conf'
57   ).split("\n").map do |line|
58     line.split[0]
59   end
60 end
62 def persistent_storage_frontend
63   Dogtail::Application.new('tps-frontend')
64 end
66 def persistent_storage_main_frame
67   persistent_storage_frontend.child('Persistent Storage', roleName: 'frame')
68 end
70 def persistent_directory_is_active(**opts)
71   opts[:user] = "root"
72   opts[:use_system_bus] = true
73   dbus_send(
74     'org.boum.tails.PersistentStorage',
75     '/org/boum/tails/PersistentStorage/Features/PersistentDirectory',
76     "org.freedesktop.DBus.Properties.Get",
77     "org.boum.tails.PersistentStorage.Feature",
78     "IsActive",
79     **opts
80   )
81 end
83 def recover_from_upgrader_failure
84   $vm.execute('pkill --full tails-upgrade-frontend-wrapper')
85   $vm.execute('killall tails-upgrade-frontend zenity')
86   # Do not sleep when retrying
87   $vm.spawn('tails-upgrade-frontend-wrapper --no-wait', user: LIVE_USER)
88 end
90 Given /^I clone USB drive "([^"]+)" to a (new|temporary) USB drive "([^"]+)"$/ do |from, mode, to|
91   $vm.storage.clone_to_new_disk(from, to)
92   if mode == 'temporary'
93     add_after_scenario_hook { $vm.storage.delete_volume(to) }
94   end
95 end
97 Given /^I unplug USB drive "([^"]+)"$/ do |name|
98   $vm.unplug_drive(name)
99 end
101 Given /^the computer is set to boot in UEFI mode$/ do
102   $vm.set_os_loader('UEFI')
103   @os_loader = 'UEFI'
106 def tails_installer_selected_device
107   @installer.child('Target USB stick:', roleName: 'label').parent
108             .child('', roleName: 'combo box', recursive: false).name
111 def tails_installer_is_device_selected?(name)
112   device = $vm.disk_dev(name)
113   tails_installer_selected_device[/\(#{device}\d*\)$/]
116 def tails_installer_match_status(pattern)
117   @installer.child('', roleName: 'text').text[pattern]
120 When /^I start Tails Installer$/ do
121   @installer_log_path = '/tmp/tails-installer.log'
122   command = "/usr/local/bin/tails-installer --verbose  2>&1 | tee #{@installer_log_path} | logger -t tails-installer"
123   step "I run \"#{command}\" in GNOME Terminal"
124   @installer = Dogtail::Application.new('tails-installer')
125   @installer.child('Tails Installer', roleName: 'frame')
126   # Sometimes Dogtail will find the Installer and click its window
127   # before it is shown (searchShowingOnly is not perfect) which
128   # generally means clicking somewhere on the Terminal => the click is
129   # lost *and* the installer does not go to the foreground. So let's
130   # wait a bit extra.
131   sleep 3
132   @screen.wait('TailsInstallerWindow.png', 10).click
135 When /^I am told by Tails Installer that.*"([^"]+)".*$/ do |status|
136   try_for(10) do
137     tails_installer_match_status(status)
138   end
141 Then /^a suitable USB device is (?:still )?not found$/ do
142   @installer.child(
143     'No device suitable to install Tails could be found', roleName: 'label'
144   )
147 Then /^(no|the "([^"]+)") USB drive is selected$/ do |mode, name|
148   try_for(30) do
149     if mode == 'no'
150       tails_installer_selected_device == ''
151     else
152       tails_installer_is_device_selected?(name)
153     end
154   end
157 def persistence_exists?(name)
158   data_part_dev = $vm.persistent_storage_dev_on_disk(name)
159   $vm.execute("test -b #{data_part_dev}").success?
162 When /^I (install|reinstall|upgrade) Tails (?:to|on) USB drive "([^"]+)" by cloning$/ do |action, name|
163   step 'I start Tails Installer'
164   # If the device was plugged *just* before this step, it might not be
165   # completely ready (so it's shown) at this stage.
166   try_for(10) { tails_installer_is_device_selected?(name) }
167   begin
168     label = if action == 'reinstall'
169               'Reinstall (delete all data)'
170             else
171               action.capitalize
172             end
173     # Despite being a normal "push button" this button doesn't respond
174     # to the "press" action. It has a "click" action, which works, but
175     # after that the installer is inaccessible for Dogtail.
176     @installer.button(label).grabFocus
177     @screen.press('Enter')
178     unless action == 'upgrade'
179       confirmation_label = if persistence_exists?(name)
180                              'Delete Persistent Storage and Reinstall'
181                            else
182                              'Delete All Data and Install'
183                            end
184       @installer.child('Question',
185                        roleName: 'alert').button(confirmation_label).click
186     end
187     try_for(15 * 60, delay: 10) do
188       @installer
189         .child('Information', roleName: 'alert')
190         .child('Installation complete!', roleName: 'label')
191       true
192     end
193   rescue StandardError => e
194     debug_log("Tails Installer debug log:\n" +
195               $vm.file_content(@installer_log_path))
196     raise e
197   end
200 Given(/^I plug and mount a USB drive containing a Tails USB image$/) do
201   usb_image_dir = share_host_files(TAILS_IMG)
202   @usb_image_path = "#{usb_image_dir}/#{File.basename(TAILS_IMG)}"
205 def enable_all_persistence_presets
206   assert persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
207   switches = persistent_storage_main_frame.children(roleName: 'toggle button')
208   switches.each do |switch|
209     if switch.checked
210       debug_log("#{switch.name} is already enabled, skipping")
211     else
212       debug_log("enabling #{switch.name}")
213       # To avoid having to bother with scrolling the window we just
214       # send an AT-SPI action instead of clicking.
215       switch.toggle
216       try_for(10) { switch.checked }
217     end
218   end
221 When /^I disable the first persistence preset$/ do
222   step 'I start "Persistent Storage" via GNOME Activities Overview'
223   assert persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
224   persistent_folder_switch = persistent_storage_main_frame.child(
225     'Activate Persistent Folder',
226     roleName: 'toggle button'
227   )
228   assert persistent_folder_switch.checked
229   persistent_folder_switch.toggle
230   try_for(10) do
231     assert !persistent_folder_switch.checked
232     # GtkSwitch does not expose its underlying state via AT-SPI (the
233     # accessible has the "check" state when the switch is on but the
234     # underlying state is false) so we check via D-Bus that the
235     # Persistent Directory feature is inactive.
236     !persistent_directory_is_active
237   end
238   @screen.press('alt', 'F4')
241 Given(/^I enable persistence creation in Tails Greeter$/) do
242   @screen.wait('TailsGreeterPersistenceCreate.png', 10).click
245 Given /^I create a persistent partition( with the default settings| for Additional Software)?( using the wizard that was already open)?$/ do |mode, dontrun|
246   # XXX: the wording here could be misleading. Pay attention when reading it! (or, please improve it)
247   default_settings = mode
248   asp = mode == ' for Additional Software'
249   unless asp || dontrun
250     step 'I start "Persistent Storage" via GNOME Activities Overview'
251   end
252   persistent_storage_main_frame.button('Co_ntinue').click
253   persistent_storage_main_frame
254     .child('Passphrase:', roleName: 'label')
255     .labelee
256     .grabFocus
257   @screen.type(@persistence_password)
258   persistent_storage_main_frame
259     .child('Confirm:', roleName: 'label')
260     .labelee
261     .grabFocus
262   @screen.type(@persistence_password)
263   persistent_storage_main_frame.button('_Create Persistent Storage').click
264   try_for(300) do
265     persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
266   end
267   enable_all_persistence_presets unless default_settings
270 def check_disk_integrity(name, dev, scheme)
271   info = $vm.execute("udisksctl info --block-device '#{dev}'").stdout
272   info_split = info.split("\n  org\.freedesktop\.UDisks2\.PartitionTable:\n")
273   part_table_info = info_split[1]
274   assert_match(/^    Type: +#{scheme}/, part_table_info,
275                "Unexpected partition scheme on USB drive '#{name}', '#{dev}'")
278 def check_part_integrity(name, dev, usage, fs_type,
279                          part_label: nil, part_type: nil)
280   info = $vm.execute("udisksctl info --block-device '#{dev}'").stdout
281   info_split = info.split("\n  org\.freedesktop\.UDisks2\.Partition:\n")
282   dev_info = info_split[0]
283   part_info = info_split[1]
284   assert_match(/^    IdUsage: +#{usage}$/, dev_info,
285                "Unexpected device field 'usage' on drive '#{name}', '#{dev}'")
286   assert_match(/^    IdType: +#{fs_type}$/, dev_info,
287                "Unexpected device field 'IdType' on drive '#{name}', '#{dev}'")
288   if part_label
289     assert_match(/^    Name: +#{part_label}$/, part_info,
290                  "Unexpected partition label on drive '#{name}', '#{dev}'")
291   end
292   if part_type
293     assert_match(/^    Type: +#{part_type}$/, part_info,
294                  "Unexpected partition type on drive '#{name}', '#{dev}'")
295   end
298 def tails_is_installed_helper(name, tails_root, loader)
299   disk_dev = $vm.disk_dev(name)
300   part_dev = disk_dev + '1'
301   check_disk_integrity(name, disk_dev, 'gpt')
302   check_part_integrity(name, part_dev, 'filesystem', 'vfat',
303                        part_label: 'Tails', part_type: ESP_GUID)
305   target_root = '/mnt/new'
306   $vm.execute("mkdir -p #{target_root}")
307   $vm.execute("mount #{part_dev} #{target_root}")
309   c = $vm.execute("diff -qr '#{tails_root}/live' '#{target_root}/live'")
310   assert(
311     c.success?,
312     "USB drive '#{name}' has differences in /live:\n#{c.stdout}\n#{c.stderr}"
313   )
315   syslinux_files = $vm.execute("ls -1 #{target_root}/syslinux")
316                       .stdout.chomp.split
317   # We deal with these files separately
318   ignores = ['syslinux.cfg', 'exithelp.cfg', 'ldlinux.c32', 'ldlinux.sys']
319   (syslinux_files - ignores).each do |f|
320     assert_vmcommand_success(
321       $vm.execute("diff -q '#{tails_root}/#{loader}/#{f}' " \
322                   "'#{target_root}/syslinux/#{f}'"),
323       "USB drive '#{name}' has differences in '/syslinux/#{f}'"
324     )
325   end
327   # The main .cfg is named differently vs isolinux
328   assert_vmcommand_success(
329     $vm.execute("diff -q '#{tails_root}/#{loader}/#{loader}.cfg' " \
330                 "'#{target_root}/syslinux/syslinux.cfg'"),
331     "USB drive '#{name}' has differences in '/syslinux/syslinux.cfg'"
332   )
334   $vm.execute("umount #{target_root}")
335   $vm.execute('sync')
338 Then /^the running Tails is installed on USB drive "([^"]+)"$/ do |target_name|
339   loader = boot_device_type == 'usb' ? 'syslinux' : 'isolinux'
340   tails_is_installed_helper(target_name, '/lib/live/mount/medium', loader)
343 Then /^there is no persistence partition on USB drive "([^"]+)"$/ do |name|
344   data_part_dev = $vm.persistent_storage_dev_on_disk(name)
345   assert($vm.execute("test -b #{data_part_dev}").failure?,
346          "USB drive #{name} has a partition '#{data_part_dev}'")
349 Then /^a Tails persistence partition exists on USB drive "([^"]+)"$/ do |name|
350   dev = $vm.persistent_storage_dev_on_disk(name)
351   check_part_integrity(name, dev, 'crypto', 'crypto_LUKS',
352                        part_label: 'TailsData')
354   luks_dev = nil
355   # The LUKS container may already be opened, e.g. by udisks after
356   # we've created the Persistent Storage.
357   c = $vm.execute("ls -1 --hide 'control' /dev/mapper/")
358   if c.success?
359     c.stdout.split("\n").each do |candidate|
360       luks_info = $vm.execute("cryptsetup status '#{candidate}'")
361       if luks_info.success? && luks_info.stdout.match("^\s+device:\s+#{dev}$")
362         luks_dev = "/dev/mapper/#{candidate}"
363         break
364       end
365     end
366   end
367   if luks_dev.nil?
368     assert_vmcommand_success(
369       $vm.execute("echo #{@persistence_password} | " \
370                   "cryptsetup luksOpen #{dev} #{name}"),
371       "Couldn't open LUKS device '#{dev}' on  drive '#{name}'"
372     )
373     luks_dev = "/dev/mapper/#{name}"
374   end
376   # Adapting check_part_integrity() seems like a bad idea so here goes
377   info = $vm.execute("udisksctl info --block-device '#{luks_dev}'").stdout
378   assert_match(%r{^    CryptoBackingDevice: +'/[a-zA-Z0-9_/]+'$}, info)
379   assert_match(/^    IdUsage: +filesystem$/, info)
380   assert_match(/^    IdType: +ext[34]$/, info)
381   assert_match(/^    IdLabel: +TailsData$/, info)
383   mount_dir = "/mnt/#{name}"
384   $vm.execute("mkdir -p #{mount_dir}")
385   assert_vmcommand_success($vm.execute("mount '#{luks_dev}' #{mount_dir}"),
386                            "Couldn't mount opened LUKS device '#{dev}' " \
387                            "on drive '#{name}'")
389   $vm.execute("umount #{mount_dir}")
390   $vm.execute('sync')
391   $vm.execute("cryptsetup luksClose #{name}")
394 Given /^I enable persistence$/ do
395   @screen.wait('TailsGreeterPersistencePassphrase.png', 60).click
396   sleep 1
397   @screen.type(@persistence_password, ['Return'])
398   @screen.wait_any(['TailsGreeterPersistenceUnlocked.png', 'TailsGreeterPersistenceUnlockedGerman.png'], 30)
401 def tails_persistence_enabled?
402   libtps_file = '/usr/local/lib/tails-shell-library/libtps.sh'
403   $vm.execute(". '#{libtps_file}' && " \
404               'tps_is_unlocked').success?
407 Given /^all persistence presets(| from the old Tails version)(| but the first one) are enabled$/ do |old_tails, except_first|
408   assert(old_tails.empty? || except_first.empty?, 'Unsupported case.')
409   try_for(120, msg: 'Persistence is disabled') do
410     tails_persistence_enabled?
411   end
412   unexpected_mounts = []
413   # Check that all persistent directories are mounted
414   if old_tails.empty?
415     expected_mounts = persistent_mounts
416     unless except_first.empty?
417       first_expected_mount_source      = expected_mounts.keys[0]
418       first_expected_mount_destination = expected_mounts[
419         first_expected_mount_source
420       ]
421       expected_mounts.delete(first_expected_mount_source)
422       unexpected_mounts = [first_expected_mount_destination]
423     end
424   else
425     assert_not_nil($remembered_persistence_mounts)
426     expected_mounts = $remembered_persistence_mounts
427   end
428   mount = $vm.execute('mount').stdout.chomp
429   expected_mounts.each do |_, dir|
430     assert(mount.include?("on #{dir} "),
431            "Persistent directory '#{dir}' is not mounted")
432   end
433   unexpected_mounts.each do |dir|
434     assert(!mount.include?("on #{dir} "),
435            "Persistent directory '#{dir}' is mounted")
436   end
439 Given /^persistence is disabled$/ do
440   assert(!tails_persistence_enabled?, 'Persistence is enabled')
443 def boot_device
444   # Approach borrowed from
445   # config/chroot_local_includes/lib/live/config/998-permissions
446   boot_dev_id = $vm.execute(
447     'udevadm info --device-id-of-file=/lib/live/mount/medium'
448   ).stdout.chomp
449   $vm.execute("readlink -f /dev/block/'#{boot_dev_id}'").stdout.chomp
452 def device_info(dev)
453   # Approach borrowed from
454   # config/chroot_local_includes/lib/live/config/998-permissions
455   info = $vm.execute("udevadm info --query=property --name='#{dev}'")
456             .stdout.chomp
457   info.split("\n").map { |e| e.split('=') }.to_h
460 def boot_device_type
461   device_info(boot_device)['ID_BUS']
464 # Turn udisksctl info output into something more manipulable:
465 def parse_udisksctl_info(input)
466   tree = {}
467   section = nil
468   key = nil
469   input.chomp.split("\n").each do |line|
470     case line
471     when %r{^/org/freedesktop/UDisks2/block_devices/}
472       true
473     when /^  (org\.freedesktop\.UDisks2\..+):$/
474       section = Regexp.last_match(1)
475       tree[section] = {}
476     when /^\s+(.+?):\s+(.+)$/
477       key = Regexp.last_match(1)
478       value = Regexp.last_match(2)
479       tree[section][key] = value
480     else
481       # XXX: Best effort = consider this a continuation from previous
482       # line (e.g. Symlinks), and add the whole line, without
483       # stripping anything (e.g. leading whitespaces)
484       tree[section][key] += line
485     end
486   end
487   tree
490 Then /^Tails is running from (.*) drive "([^"]+)"$/ do |bus, name|
491   bus = bus.downcase
492   expected_bus = bus == 'sata' ? 'ata' : bus
493   assert_equal(expected_bus, boot_device_type)
494   actual_dev = boot_device
495   # The boot partition differs between an using Tails installer and
496   # isohybrids. There's also a strange case isohybrids are thought to
497   # be booting from the "raw" device, and not a partition of it
498   # (#10504).
499   expected_devs = ['', '1', '4'].map { |e| $vm.disk_dev(name) + e }
500   assert(expected_devs.include?(actual_dev),
501          "We are running from device #{actual_dev}, but for #{bus} drive " \
502          "'#{name}' we expected to run from one of #{expected_devs}")
505 Then /^the boot device has safe access rights$/ do
506   super_boot_dev = boot_device.sub(/[[:digit:]]+$/, '')
507   devs = $vm.file_glob("#{super_boot_dev}*")
508   assert(!devs.empty?, 'Could not determine boot device')
509   all_users = $vm.file_content('/etc/passwd')
510                  .split("\n")
511                  .map { |line| line.split(':')[0] }
512   all_users_with_groups = all_users.map do |user|
513     groups = $vm.execute("groups #{user}").stdout.chomp.sub(/^#{user} : /,
514                                                             '').split(' ')
515     [user, groups]
516   end
517   devs.each do |dev|
518     dev_owner = $vm.execute("stat -c %U #{dev}").stdout.chomp
519     dev_group = $vm.execute("stat -c %G #{dev}").stdout.chomp
520     dev_perms = $vm.execute("stat -c %a #{dev}").stdout.chomp
521     assert_equal('root', dev_owner)
522     assert(['disk', 'root'].include?(dev_group),
523            "Boot device '#{dev}' owned by group '#{dev_group}', expected " \
524            "'disk' or 'root'.")
525     assert_equal('660', dev_perms)
526     all_users_with_groups.each do |user, groups|
527       next if user == 'root'
529       assert(!groups.include?(dev_group),
530              "Unprivileged user '#{user}' is in group '#{dev_group}' which " \
531              "owns boot device '#{dev}'")
532     end
533   end
535   info = $vm.execute("udisksctl info --block-device '#{super_boot_dev}'").stdout
536   assert_match(/^    HintSystem: +true$/, info,
537                "Boot device '#{super_boot_dev}' is not system internal " \
538                'for udisks')
541 Then /^all persistent filesystems have safe access rights$/ do
542   persistent_volumes_mountpoints.each do |mountpoint|
543     fs_owner = $vm.execute("stat -c %U #{mountpoint}").stdout.chomp
544     fs_group = $vm.execute("stat -c %G #{mountpoint}").stdout.chomp
545     fs_perms = $vm.execute("stat -c %a #{mountpoint}").stdout.chomp
546     assert_equal('root', fs_owner)
547     assert_equal('root', fs_group)
548     # This ensures the amnesia user cannot write to the root of the
549     # persistent storage, which in turns ensures this user cannot
550     # create a .Trash-1000 folder in there, which is our current best
551     # workaround for the lack of proper trash support in Persistent
552     # Storage: then the user is not offered to send files to the
553     # trash, and they can only delete files permanently (#18118).
554     assert_equal('770', fs_perms)
555   end
558 Then /^all persistence configuration files have safe access rights$/ do
559   persistent_volumes_mountpoints.each do |mountpoint|
560     assert_vmcommand_success(
561       $vm.execute("test -e #{mountpoint}/persistence.conf"),
562       "#{mountpoint}/persistence.conf does not exist, while it should"
563     )
564     assert_vmcommand_success(
565       $vm.execute("test ! -e #{mountpoint}/live-persistence.conf"),
566       "#{mountpoint}/live-persistence.conf does exist, while it should not"
567     )
568     $vm.file_glob(
569       "#{mountpoint}/persistence.conf* #{mountpoint}/live-*.conf"
570     ).each do |f|
571       file_owner = $vm.execute("stat -c %U '#{f}'").stdout.chomp
572       file_group = $vm.execute("stat -c %G '#{f}'").stdout.chomp
573       file_perms = $vm.execute("stat -c %a '#{f}'").stdout.chomp
574       assert_equal('tails-persistent-storage', file_owner)
575       assert_equal('tails-persistent-storage', file_group)
576       case f
577       when %r{.*/live-additional-software.conf$}
578         assert_equal('644', file_perms)
579       else
580         assert_equal('600', file_perms)
581       end
582     end
583   end
586 Then /^all persistent directories(| from the old Tails version) have safe access rights$/ do |old_tails|
587   if old_tails.empty?
588     expected_dirs = persistent_dirs
589   else
590     assert_not_nil($remembered_persistence_dirs)
591     expected_dirs = $remembered_persistence_dirs
592   end
593   persistent_volumes_mountpoints.each do |mountpoint|
594     expected_dirs.each do |src, dest|
595       full_src = "#{mountpoint}/#{src}"
596       assert_vmcommand_success $vm.execute("test -d #{full_src}")
597       dir_perms = $vm.execute_successfully("stat -c %a '#{full_src}'")
598                      .stdout.chomp
599       dir_owner = $vm.execute_successfully("stat -c %U '#{full_src}'")
600                      .stdout.chomp
601       if dest.start_with?("/home/#{LIVE_USER}")
602         expected_perms = '700'
603         expected_owner = LIVE_USER
604       elsif File.basename(src) == 'greeter-settings'
605         expected_perms = '700'
606         expected_owner = 'Debian-gdm'
607       elsif File.basename(src) == 'tca'
608         expected_perms = '700'
609         expected_owner = 'root'
610       else
611         expected_perms = '755'
612         expected_owner = 'root'
613       end
614       assert_equal(expected_perms, dir_perms,
615                    "Persistent source #{full_src} has permission " \
616                    "#{dir_perms}, expected #{expected_perms}")
617       assert_equal(expected_owner, dir_owner,
618                    "Persistent source #{full_src} has owner " \
619                    "#{dir_owner}, expected #{expected_owner}")
620     end
621   end
624 When /^I write some files expected to persist$/ do
625   persistent_mounts.each do |_, dir|
626     owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
627     assert_vmcommand_success(
628       $vm.execute("touch #{dir}/XXX_persist", user: owner),
629       "Could not create file in persistent directory #{dir}"
630     )
631   end
634 When /^I write some dotfile expected to persist$/ do
635   assert_vmcommand_success(
636     $vm.execute(
637       'touch /live/persistence/TailsData_unlocked/dotfiles/.XXX_persist',
638       user: LIVE_USER
639     ),
640     'Could not create a file in the dotfiles persistence.'
641   )
644 When /^I remove some files expected to persist$/ do
645   persistent_mounts.each do |_, dir|
646     owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
647     assert_vmcommand_success(
648       $vm.execute("rm #{dir}/XXX_persist", user: owner),
649       "Could not remove file in persistent directory #{dir}"
650     )
651   end
654 When /^I write some files not expected to persist$/ do
655   persistent_mounts.each do |_, dir|
656     owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
657     assert_vmcommand_success(
658       $vm.execute("touch #{dir}/XXX_gone", user: owner),
659       "Could not create file in persistent directory #{dir}"
660     )
661   end
664 When /^I take note of which persistence presets are available$/ do
665   $remembered_persistence_mounts = persistent_mounts
666   $remembered_persistence_dirs = persistent_dirs
669 Then /^the expected persistent files(| created with the old Tails version) are present in the filesystem$/ do |old_tails|
670   if old_tails.empty?
671     expected_mounts = persistent_mounts
672   else
673     assert_not_nil($remembered_persistence_mounts)
674     expected_mounts = $remembered_persistence_mounts
675   end
676   expected_mounts.each do |_, dir|
677     assert_vmcommand_success(
678       $vm.execute("test -e #{dir}/XXX_persist"),
679       "Could not find expected file in persistent directory #{dir}"
680     )
681     assert(
682       $vm.execute("test -e #{dir}/XXX_gone").failure?,
683       "Found file that should not have persisted in persistent directory #{dir}"
684     )
685   end
688 Then /^the expected persistent dotfile is present in the filesystem$/ do
689   expected_dirs = persistent_dirs
690   assert_vmcommand_success(
691     $vm.execute("test -L #{expected_dirs['dotfiles']}/.XXX_persist"),
692     'Could not find expected persistent dotfile link.'
693   )
694   assert_vmcommand_success(
695     $vm.execute(
696       "test -e $(readlink -f #{expected_dirs['dotfiles']}/.XXX_persist)"
697     ),
698     'Could not find expected persistent dotfile link target.'
699   )
702 Then /^only the expected files are present on the persistence partition on USB drive "([^"]+)"$/ do |name|
703   assert(!$vm.running?)
704   disk = {
705     path: $vm.storage.disk_path(name),
706     opts: {
707       format:   $vm.storage.disk_format(name),
708       readonly: true,
709     },
710   }
711   $vm.storage.guestfs_disk_helper(disk) do |g, disk_handle|
712     partitions = g.part_list(disk_handle).map do |part_desc|
713       disk_handle + part_desc['part_num'].to_s
714     end
715     partition = partitions.find do |part|
716       g.blkid(part)['PART_ENTRY_NAME'] == 'TailsData'
717     end
718     assert_not_nil(partition, "Could not find the 'TailsData' partition " \
719                               "on disk '#{disk_handle}'")
720     luks_mapping = File.basename(partition) + '_unlocked'
721     g.cryptsetup_open(partition, @persistence_password, luks_mapping)
722     luks_dev = "/dev/mapper/#{luks_mapping}"
723     mount_point = '/'
724     g.mount(luks_dev, mount_point)
725     assert_not_nil($remembered_persistence_mounts)
726     $remembered_persistence_mounts.each do |dir, _|
727       # Guestfs::exists may have a bug; if the file exists, 1 is
728       # returned, but if it doesn't exist false is returned. It seems
729       # the translation of C types into Ruby types is glitchy.
730       assert(g.exists("/#{dir}/XXX_persist") == 1,
731              "Could not find expected file in persistent directory #{dir}")
732       assert(
733         g.exists("/#{dir}/XXX_gone") != 1,
734         'Found file that should not have persisted in persistent directory ' +
735         dir
736       )
737     end
738     g.umount(mount_point)
739     g.cryptsetup_close(luks_dev)
740   end
743 When /^I delete the persistent partition$/ do
744   step 'I start "Persistent Storage" via GNOME Activities Overview'
746   delete_btn = persistent_storage_main_frame.button('Delete Persistent Storage')
747   assert delete_btn
749   # If we just do delete_btn.click, then dogtail won't find tps-frontend anymore.
750   # That's probably a bug somewhere, and this is a simple workaround
751   delete_btn.grabFocus
752   @screen.press('Return')
754   persistent_storage_frontend
755     .child('Warning', roleName: 'alert')
756     .button('Delete Persistent Storage').click
757   assert persistent_storage_main_frame.child(
758     'The Persistent Storage was successfully deleted.',
759     roleName: 'label'
760   )
763 Then /^Tails has started in UEFI mode$/ do
764   assert_vmcommand_success($vm.execute('test -d /sys/firmware/efi'),
765                            '/sys/firmware/efi does not exist')
768 Given /^I create a ([[:alpha:]]+) label on disk "([^"]+)"$/ do |type, name|
769   $vm.storage.disk_mklabel(name, type)
772 # The (crude) bin/create-test-iuks script can be used to generate the IUKs,
773 # meant to apply these exact changes, that are used by the test suite.
774 # It's nice to keep that script updated when updating the list of expected
775 # changes here and uploading new test IUKs.
776 def iuk_changes(version) # rubocop:disable Metrics/MethodLength
777   changes = [
778     {
779       filesystem:  :rootfs,
780       path:        'some_new_file',
781       status:      :added,
782       new_content: <<~CONTENT,
783         Some content
784       CONTENT
785     },
786     {
787       filesystem:  :rootfs,
788       path:        'etc/amnesia/version',
789       status:      :modified,
790       new_content: <<~CONTENT,
791         #{version} - 20380119
792         ffffffffffffffffffffffffffffffffffffffff
793         live-build: 3.0.5+really+is+2.0.12-0.tails2
794         live-boot: 4.0.2-1
795         live-config: 4.0.4-1
796       CONTENT
797     },
798     {
799       filesystem:  :rootfs,
800       path:        'etc/os-release',
801       status:      :modified,
802       new_content: <<~CONTENT,
803         TAILS_PRODUCT_NAME="Tails"
804         TAILS_VERSION_ID="#{version}"
805       CONTENT
806     },
807     {
808       filesystem: :rootfs,
809       path:       'usr/share/common-licenses/BSD',
810       status:     :removed,
811     },
812     {
813       filesystem: :rootfs,
814       path:       'usr/share/doc/tor',
815       status:     :removed,
816     },
817     {
818       filesystem: :medium,
819       path:       'utils/linux/syslinux',
820       status:     :removed,
821     },
822   ]
824   case version
825   when '2.2~testoverlayfsng'
826     changes
827   when '2.3~testoverlayfsng'
828     changes + [
829       {
830         filesystem:  :rootfs,
831         path:        'some_new_file_2.3',
832         status:      :added,
833         new_content: <<~CONTENT,
834           Some content 2.3
835         CONTENT
836       },
837       {
838         filesystem: :rootfs,
839         path:       'usr/share/common-licenses/MPL-1.1',
840         status:     :removed,
841       },
842       {
843         filesystem: :medium,
844         path:       'utils/mbr/mbr.bin',
845         status:     :removed,
846       },
847     ]
848   else
849     raise "Test suite implementation error: unsupported version #{version}"
850   end
853 Given /^the file system changes introduced in version (.+) are (not )?present(?: in the (\S+) Browser's chroot)?$/ do |version, not_present, chroot_browser|
854   assert(['2.2~testoverlayfsng', '2.3~testoverlayfsng'].include?(version))
855   upgrade_applied = not_present.nil?
856   chroot_browser = "#{chroot_browser.downcase}-browser" if chroot_browser
857   changes = iuk_changes(version)
858   changes.each do |change|
859     case change[:filesystem]
860     when :rootfs
861       path = '/'
862       path += "var/lib/#{chroot_browser}/chroot/" if chroot_browser
863       path += change[:path]
864     when :medium
865       path = '/lib/live/mount/medium/' + change[:path]
866     else
867       raise "Unknown filesystem '#{change[:filesystem]}'"
868     end
869     case change[:status]
870     when :removed
871       assert_equal(!upgrade_applied, $vm.file_exist?(path))
872     when :added
873       assert_equal(upgrade_applied, $vm.file_exist?(path))
874       if upgrade_applied && change[:new_content]
875         assert_equal(change[:new_content], $vm.file_content(path))
876       end
877     when :modified
878       assert($vm.file_exist?(path))
879       if upgrade_applied
880         assert_not_nil(change[:new_content])
881         assert_equal(change[:new_content], $vm.file_content(path))
882       end
883     else
884       raise "Unknown status '#{change[:status]}'"
885     end
886   end
889 Then /^I am proposed to install an incremental upgrade to version (.+)$/ do |version|
890   recovery_proc = proc do
891     recover_from_upgrader_failure
892   end
893   failure_pic = 'TailsUpgraderFailure.png'
894   success_pic = "TailsUpgraderUpgradeTo#{version}.png"
895   retry_tor(recovery_proc) do
896     found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60)[:found_pattern]
897     assert_equal(success_pic, found_pic)
898   end
901 When /^I agree to install the incremental upgrade$/ do
902   @orig_syslinux_cfg = $vm.file_content(
903     '/lib/live/mount/medium/syslinux/syslinux.cfg'
904   )
905   @screen.click('TailsUpgraderUpgradeNowButton.png')
908 Then /^I can successfully install the incremental upgrade to version (.+)$/ do |version|
909   step 'I agree to install the incremental upgrade'
910   recovery_proc = proc do
911     recover_from_upgrader_failure
912     step "I am proposed to install an incremental upgrade to version #{version}"
913     step 'I agree to install the incremental upgrade'
914   end
915   failure_pic = 'TailsUpgraderFailure.png'
916   success_pic = 'TailsUpgraderDownloadComplete.png'
917   retry_tor(recovery_proc) do
918     found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60)[:found_pattern]
919     assert_equal(success_pic, found_pic)
920   end
921   @screen.wait('TailsUpgraderApplyUpgradeButton.png', 5).click
922   @screen.wait('TailsUpgraderDone.png', 60)
923   # Restore syslinux.cfg: our test IUKs replace it with something
924   # that would break the next boot
925   $vm.file_overwrite(
926     '/lib/live/mount/medium/syslinux/syslinux.cfg',
927     @orig_syslinux_cfg
928   )
931 def default_squash
932   'filesystem.squashfs'
935 def installed_squashes
936   live = '/lib/live/mount/medium/live'
937   listed_squashes = $vm.file_content("#{live}/Tails.module").chomp.split("\n")
938   assert_equal(
939     default_squash,
940     listed_squashes.first,
941     "Tails.module does not list #{default_squash} on the first line"
942   )
943   present_squashes = $vm.file_glob("#{live}/*.squashfs").map do |f|
944     f.sub('/lib/live/mount/medium/live/', '')
945   end
946   # Sanity check
947   assert_equal(
948     listed_squashes.sort,
949     present_squashes.sort,
950     'Tails.module does not match the present .squashfs files'
951   )
952   listed_squashes
955 Given /^Tails is fooled to think a (.+) SquashFS delta is installed$/ do |version|
956   old_squashes = installed_squashes
957   medium = '/lib/live/mount/medium'
958   live = "#{medium}/live"
959   new_squash = "#{version}.squashfs"
960   $vm.execute_successfully("mount -o remount,rw #{medium}")
961   $vm.execute_successfully("touch #{live}/#{new_squash}")
962   $vm.file_append("#{live}/Tails.module", new_squash + "\n")
963   $vm.execute_successfully("mount -o remount,ro #{medium}")
964   assert_equal(
965     old_squashes + [new_squash],
966     installed_squashes,
967     'Implementation error, alert the test suite maintainer!'
968   )
969   $vm.execute_successfully(
970     "sed --regexp-extended -i '1s/^\S+ /#{version}/' /etc/amnesia/version"
971   )
972   $vm.execute_successfully(
973     "sed -i 's/^TAILS_VERSION_ID=.*/TAILS_VERSION_ID=#{version}/' " \
974     '/etc/amnesia/version'
975   )
978 Then /^the Upgrader considers the system as up-to-date$/ do
979   try_for(120, delay: 10) do
980     $vm.execute_successfully(
981       'systemctl --user status tails-upgrade-frontend.service',
982       user: LIVE_USER
983     )
984     up_to_date_regexp = 'tails-upgrade-frontend-wrapper\[[0-9]+\]: ' \
985                         'The system is up-to-date'
986     $vm.execute_successfully(
987       "journalctl | grep -q -E '#{up_to_date_regexp}'"
988     )
989   end
992 def upgrader_trusted_signing_subkeys
993   $vm.execute_successfully(
994     'sudo -u tails-upgrade-frontend ' \
995     'gpg --batch --list-keys --with-colons ' + TAILS_SIGNING_KEY
996   ).stdout.split("\n")
997      .select { |line| /^sub:/.match(line) }
998      .map { |line| line[/^sub:.:\d+:\d+:(?<subkeyid>[A-F0-9]+):/, 'subkeyid'] }
1001 Given /^the signing key used by the Upgrader is outdated$/ do
1002   upgrader_trusted_signing_subkeys.each do |subkeyid|
1003     $vm.execute_successfully(
1004       'sudo -u tails-upgrade-frontend ' \
1005       "gpg --batch --yes --delete-keys '#{subkeyid}!'"
1006     )
1007   end
1008   assert_equal(0, upgrader_trusted_signing_subkeys.length)
1011 Given /^a current signing key is available on our website$/ do
1012   # We already check this via features/keys.feature so let's not bother here
1013   # ⇒ this step is only here to improve the Gherkin scenario.
1014   true
1017 Then /^(?:no|only the (.+)) SquashFS delta is installed$/ do |version|
1018   expected_squashes = [default_squash]
1019   expected_squashes << "#{version}.squashfs" if version
1020   assert_equal(
1021     expected_squashes,
1022     installed_squashes,
1023     'Unexpected .squashfs files encountered'
1024   )
1027 Then /^the label of the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1028   assert($vm.running?)
1029   disk_dev = $vm.disk_dev(name)
1030   part_dev = disk_dev + '1'
1031   check_disk_integrity(name, disk_dev, 'gpt')
1032   check_part_integrity(name, part_dev, 'filesystem', 'vfat', part_label: label)
1035 Then /^the system partition on "([^"]+)" is an EFI system partition$/ do |name|
1036   assert($vm.running?)
1037   disk_dev = $vm.disk_dev(name)
1038   part_dev = disk_dev + '1'
1039   check_disk_integrity(name, disk_dev, 'gpt')
1040   check_part_integrity(name, part_dev, 'filesystem', 'vfat',
1041                        part_type: ESP_GUID)
1044 Then /^the FAT filesystem on the system partition on "([^"]+)" is at least (\d+)(.+) large$/ do |name, size, unit|
1045   # Let's use bytes all the way:
1046   wanted_size = convert_to_bytes(size.to_i, unit)
1048   disk_dev = $vm.disk_dev(name)
1049   part_dev = disk_dev + '1'
1051   udisks_info = $vm.execute_successfully(
1052     "udisksctl info --block-device #{part_dev}"
1053   ).stdout
1054   partition_size = parse_udisksctl_info(udisks_info)[
1055     'org.freedesktop.UDisks2.Partition'
1056   ]['Size'].to_i
1058   # Partition size:
1059   assert(
1060     partition_size >= wanted_size,
1061     "FAT partition is too small: #{partition_size} is less than #{wanted_size}"
1062   )
1064   # -B 1 forces size to be expressed in bytes rather than (1K) blocks:
1065   fs_size = $vm.execute_successfully(
1066     "df --output=size -B 1 '/lib/live/mount/medium'"
1067   ).stdout.split("\n")[1].to_i
1068   assert(fs_size >= wanted_size,
1069          "FAT filesystem is too small: #{fs_size} is less than #{wanted_size}")
1072 Then /^the UUID of the FAT filesystem on the system partition on "([^"]+)" was randomized$/ do |name|
1073   disk_dev = $vm.disk_dev(name)
1074   part_dev = disk_dev + '1'
1076   # Get the UUID from the block area:
1077   udisks_info = $vm.execute_successfully(
1078     "udisksctl info --block-device #{part_dev}"
1079   ).stdout
1080   fs_uuid = parse_udisksctl_info(
1081     udisks_info
1082   )['org.freedesktop.UDisks2.Block']['IdUUID']
1084   static_uuid = 'A690-20D2'
1085   assert(fs_uuid != static_uuid,
1086          "FS UUID on #{name} wasn't randomized, it's still: #{fs_uuid}")
1089 Then /^the label of the FAT filesystem on the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1090   disk_dev = $vm.disk_dev(name)
1091   part_dev = disk_dev + '1'
1093   # Get FS label from the block area:
1094   udisks_info = $vm.execute_successfully(
1095     "udisksctl info --block-device #{part_dev}"
1096   ).stdout
1097   fs_label = parse_udisksctl_info(
1098     udisks_info
1099   )['org.freedesktop.UDisks2.Block']['IdLabel']
1101   assert(label == fs_label,
1102          "FS label on #{part_dev} is #{fs_label} " \
1103          "instead of the expected #{label}")
1106 Then /^the system partition on "([^"]+)" has the expected flags$/ do |name|
1107   disk_dev = $vm.disk_dev(name)
1108   part_dev = disk_dev + '1'
1110   # Look at the flags from the partition area:
1111   udisks_info = $vm.execute_successfully(
1112     "udisksctl info --block-device #{part_dev}"
1113   ).stdout
1114   flags = parse_udisksctl_info(
1115     udisks_info
1116   )['org.freedesktop.UDisks2.Partition']['Flags']
1118   # See SYSTEM_PARTITION_FLAGS in create-usb-image-from-iso: 0xd000000000000005,
1119   # displayed in decimal (14987979559889010693) in udisksctl's output:
1120   expected_flags = 0xd000000000000005
1121   assert(flags == expected_flags.to_s,
1122          "Got #{flags} as partition flags on #{part_dev} (for #{name}), " \
1123          "instead of the expected #{expected_flags}")
1126 Given /^I install a Tails USB image to the (\d+) MiB disk with GNOME Disks$/ do |size_in_MiB_of_destination_disk|
1127   # GNOME Disks displays devices sizes in GB, with 1 decimal digit precision
1128   size_in_GB_of_destination_disk = convert_from_bytes(
1129     convert_to_bytes(size_in_MiB_of_destination_disk.to_i, 'MiB'),
1130     'GB'
1131   ).round(1).to_s
1132   debug_log('Expected size of destination disk: ' +
1133             size_in_GB_of_destination_disk)
1135   step 'I start "Disks" via GNOME Activities Overview'
1136   disks = gnome_disks_app
1137   destination_disk_label_regexp = /^#{size_in_GB_of_destination_disk} GB Drive/
1138   disks.children(roleName: 'table cell')
1139        .find { |row| destination_disk_label_regexp.match(row.name) }
1140        .grabFocus
1141   @screen.wait('GnomeDisksDriveMenuButton.png', 5).click
1142   disks.child('Restore Disk Image…',
1143               roleName:    'push button',
1144               showingOnly: true)
1145        .click
1146   restore_dialog = disks.child('Restore Disk Image',
1147                                roleName:    'dialog',
1148                                showingOnly: true)
1149   # Open the file chooser
1150   @screen.press('Enter')
1151   select_disk_image_dialog = disks.child('Select Disk Image to Restore',
1152                                          roleName:    'file chooser',
1153                                          showingOnly: true)
1154   @screen.paste(
1155     @usb_image_path,
1156     app: :gtk_file_chooser
1157   )
1158   sleep 2 # avoid ENTER being eaten by the auto-completion system
1159   @screen.press('Enter')
1160   try_for(10) do
1161     !select_disk_image_dialog.showing
1162   end
1163   # Clicking this button using Dogtail works, but afterwards GNOME
1164   # Disks becomes inaccessible.
1165   restore_dialog.child('Start Restoring…',
1166                        roleName:    'push button',
1167                        showingOnly: true).grabFocus
1168   @screen.press('Return')
1169   disks.child('Information', roleName: 'alert', showingOnly: true)
1170        .child('Restore', roleName: 'push button', showingOnly: true)
1171        .grabFocus
1172   @screen.press('Return')
1173   # Wait until the restoration job is finished
1174   job = disks.child('Job', roleName: 'label', showingOnly: true)
1175   try_for(120) do
1176     !job.showing
1177   end
1180 Given /^I set all Greeter options to non-default values$/ do
1181   # We sleep between each option to give the UI time to update,
1182   # otherwise we might detect the + button or language entry before it
1183   # has been readjusted, so while we try to click it, it moves so we
1184   # miss it.
1185   step 'I disable the Unsafe Browser'
1186   sleep 2
1187   step 'I disable networking in Tails Greeter'
1188   sleep 2
1189   step 'I disable MAC spoofing in Tails Greeter'
1190   sleep 2
1191   # Administration password needs to be done last because its image has blue background (selected)
1192   # while the others have no such background.
1193   step 'I set an administration password'
1194   sleep 2
1196   # We should change language, too, but we'll not: in fact, changing the language would change labels in the
1197   # UI, so we would need to keep images (see #19420) in both languages, making the test suite harder to
1198   # maintain.
1199   # The "I log in to a new session" step can change language at the very last moment, which is a good
1200   # workaround to the problem.
1203 Then /^all Greeter options are set to (non-)?default values$/ do |non_default|
1204   settings = $vm.execute_successfully(
1205     'grep -h "^TAILS_" /var/lib/gdm3/settings/persistent/tails.* | ' \
1206     'grep -v "^TAILS_.*PASSWORD" | LC_ALL=C sort'
1207   ).stdout
1208   if non_default
1209     expected = <<~EXPECTED
1210       TAILS_FORMATS=de_DE
1211       TAILS_LOCALE_NAME=de_DE
1212       TAILS_MACSPOOF_ENABLED=false
1213       TAILS_NETWORK=false
1214       TAILS_UNSAFE_BROWSER_ENABLED=false
1215       TAILS_XKBLAYOUT=de
1216       TAILS_XKBMODEL=pc105
1217       TAILS_XKBVARIANT=
1218     EXPECTED
1219     $vm.execute_successfully(
1220       'grep "^TAILS_USER_PASSWORD=\'.\+\'$" ' \
1221       '/var/lib/gdm3/settings/persistent/tails.password'
1222     )
1223     $vm.execute_successfully(
1224       'grep "^TAILS_PASSWORD_HASH_FUNCTION=SHA512$" ' \
1225       '/var/lib/gdm3/settings/persistent/tails.password'
1226     )
1227   else
1228     expected = <<~EXPECTED
1229       TAILS_FORMATS=en_US
1230       TAILS_LOCALE_NAME=en_US
1231       TAILS_MACSPOOF_ENABLED=true
1232       TAILS_NETWORK=true
1233       TAILS_UNSAFE_BROWSER_ENABLED=true
1234       TAILS_XKBLAYOUT=us
1235       TAILS_XKBMODEL=pc105
1236       TAILS_XKBVARIANT=
1237     EXPECTED
1238     assert(!$vm.file_exist?('/var/lib/gdm3/settings/persistent/tails.password'))
1239   end
1240   assert_equal(expected, settings)
1243 Then /^(no )?persistent Greeter options were restored$/ do |no|
1244   if no
1245     assert(!@screen.exists('TailsGreeterToast.png'))
1246   else
1247     $language = 'German'
1248     @screen.wait('TailsGreeterPersistentSettingsRestoredGerman.png', 10)
1249   end
1252 Then /^(.*) is (?:still )?configured to persist$/ do |dir|
1253   assert(configured_persistent_mountpoints.include?(dir))
1256 Then /^(.*) is not configured to persist$/ do |dir|
1257   assert(!configured_persistent_mountpoints.include?(dir))
1260 Then /^the Tails Persistent Storage behave tests pass$/ do
1261   $vm.execute_successfully('/usr/lib/python3/dist-packages/tps/configuration/behave-tests/run-tests.sh')
1264 When /^I give the Persistent Storage on drive "([^"]+)" its own UUID$/ do |name|
1265   # Rationale: udisks cannot unlock 2 devices with the same UUID.
1266   dev = $vm.persistent_storage_dev_on_disk(name)
1267   uuid = SecureRandom.uuid
1268   $vm.execute_successfully("cryptsetup luksUUID --uuid #{uuid} #{dev}")