Merge branch 'master' into web/18079-migrate-blueprints
[tails/test.git] / Rakefile
blob2d897dfb5a9a073c3b0055ed7d302fd232bffa94
1 # -*- mode: ruby -*-
2 # vi: set ft=ruby :
4 # Tails: The Amnesic Incognito Live System
5 # Copyright © 2012 Tails developers <tails@boum.org>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 require 'date'
21 require 'English'
22 require 'libvirt'
23 require 'open3'
24 require 'rbconfig'
25 require 'uri'
27 require_relative 'vagrant/lib/tails_build_settings'
29 # Path to the directory which holds our Vagrantfile
30 VAGRANT_PATH = File.expand_path('vagrant', __dir__)
32 # Branches that are considered 'stable' (used to select SquashFS compression)
33 STABLE_BRANCH_NAMES = ['stable', 'testing'].freeze
35 EXPORTED_VARIABLES = [
36   'MKSQUASHFS_OPTIONS',
37   'APT_SNAPSHOTS_SERIALS',
38   'TAILS_ACNG_PROXY',
39   'TAILS_BUILD_FAILURE_RESCUE',
40   'TAILS_DATE_OFFSET',
41   'TAILS_MERGE_BASE_BRANCH',
42   'TAILS_OFFLINE_MODE',
43   'TAILS_PROXY',
44   'TAILS_PROXY_TYPE',
45   'TAILS_RAM_BUILD',
46   'TAILS_WEBSITE_CACHE',
47   'GIT_COMMIT',
48   'GIT_REF',
49   'BASE_BRANCH_GIT_COMMIT',
50 ].freeze
51 ENV['EXPORTED_VARIABLES'] = EXPORTED_VARIABLES.join(' ')
53 EXTERNAL_HTTP_PROXY = ENV['http_proxy']
55 # In-VM proxy URL
56 INTERNAL_HTTP_PROXY = "http://#{VIRTUAL_MACHINE_HOSTNAME}:3142".freeze
58 ENV['ARTIFACTS'] ||= '.'
60 ENV['APT_SNAPSHOTS_SERIALS'] ||= ''
62 class CommandError < StandardError
63   attr_reader :status, :stderr
65   def initialize(message, **opts)
66     opts[:status] ||= nil
67     opts[:stderr] ||= nil
68     @status = opts[:status]
69     @stderr = opts[:stderr]
70     super(format(message, status: @status, stderr: @stderr))
71   end
72 end
74 def run_command(*args)
75   Process.wait Kernel.spawn(*args)
76   return if $CHILD_STATUS.exitstatus.zero?
78   raise CommandError.new("command #{args} failed with exit status %<status>s",
79                          status: $CHILD_STATUS.exitstatus)
80 end
82 def capture_command(*args)
83   stdout, stderr, proc_status = Open3.capture3(*args)
84   if proc_status.exitstatus != 0
85     raise CommandError.new("command #{args} failed with exit status " \
86                            '%<status>s: %<stderr>s',
87                            stderr: stderr, status: proc_status.exitstatus)
88   end
89   [stdout, stderr]
90 end
92 def git_helper(*args)
93   question = args.first.end_with?('?')
94   args.first.sub!(/\?$/, '')
95   status = 0
96   stdout = ''
97   begin
98     stdout, = capture_command('auto/scripts/utils.sh', *args)
99   rescue CommandError => e
100     status = e.status
101   end
102   question ? status.zero? : stdout.chomp
105 class VagrantCommandError < CommandError
108 # Runs the vagrant command, letting stdout/stderr through. Throws an
109 # exception unless the vagrant command succeeds.
110 def run_vagrant(*args)
111   run_command('vagrant', *args, chdir: './vagrant')
112 rescue CommandError => e
113   raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " \
114                              "status #{e.status}")
117 # Runs the vagrant command, not letting stdout/stderr through, and
118 # returns [stdout, stderr, Process::Status].
119 def capture_vagrant(*args)
120   capture_command('vagrant', *args, chdir: './vagrant')
121 rescue CommandError => e
122   raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " \
123                              "status #{e.status}: #{e.stderr}")
126 [:run_vagrant, :capture_vagrant].each do |m|
127   define_method "#{m}_ssh" do |*args|
128     method(m).call('ssh', '-c', *args, '--', '-q')
129   end
132 def vagrant_ssh_config(key)
133   # Cache results
134   if $vagrant_ssh_config.nil?
135     $vagrant_ssh_config = capture_vagrant('ssh-config')
136                           .first.split("\n") \
137                           .map { |line| line.strip.split(/\s+/, 2) } .to_h
138     # The path in the ssh-config output is quoted, which is not what
139     # is expected outside of a shell, so let's get rid of the quotes.
140     $vagrant_ssh_config['IdentityFile'].gsub!(/^"|"$/, '')
141   end
142   $vagrant_ssh_config[key]
145 def current_vm_cpus
146   capture_vagrant_ssh('grep -c "^processor\s*:" /proc/cpuinfo').first.chomp.to_i
149 def vm_state
150   out, = capture_vagrant('status')
151   status_line = out.split("\n")[2]
152   if    status_line['not created']
153     :not_created
154   elsif status_line['shutoff']
155     :poweroff
156   elsif status_line['running']
157     :running
158   else
159     raise 'could not determine VM state'
160   end
163 def enough_free_host_memory_for_ram_build?
164   return false unless RbConfig::CONFIG['host_os'] =~ /linux/i
166   begin
167     usable_free_mem = `free`.split[12].to_i
168     usable_free_mem > VM_MEMORY_FOR_RAM_BUILDS * 1024
169   rescue StandardError
170     false
171   end
174 def free_vm_memory
175   capture_vagrant_ssh('free').first.chomp.split[12].to_i
178 def enough_free_vm_memory_for_ram_build?
179   free_vm_memory > BUILD_SPACE_REQUIREMENT * 1024
182 def enough_free_memory_for_ram_build?
183   if vm_state == :running
184     enough_free_vm_memory_for_ram_build?
185   else
186     enough_free_host_memory_for_ram_build?
187   end
190 def releasing?
191   git_helper('git_on_a_tag?')
194 def system_cpus
195   return unless RbConfig::CONFIG['host_os'] =~ /linux/i
197   begin
198     File.read('/proc/cpuinfo').scan(/^processor\s+:/).count
199   rescue StandardError
200     nil
201   end
204 ENV['TAILS_WEBSITE_CACHE'] = releasing? ? '0' : '1'
206 task :parse_build_options do
207   options = []
209   # Default to in-memory builds if there is enough RAM available
210   options << 'ram' if enough_free_memory_for_ram_build?
211   # Default to build using the in-VM proxy
212   options << 'vmproxy'
213   # Default to fast compression on development branches
214   options << 'fastcomp' unless releasing?
215   # Default to the number of system CPUs when we can figure it out
216   cpus = system_cpus
217   options << "cpus=#{cpus}" if cpus
219   options += ENV['TAILS_BUILD_OPTIONS'].split if ENV['TAILS_BUILD_OPTIONS']
221   options.uniq.each do |opt| # rubocop:disable Metrics/BlockLength
222     case opt
223     # Memory build settings
224     when 'ram'
225       ENV['TAILS_RAM_BUILD'] = '1'
226     when 'noram'
227       ENV['TAILS_RAM_BUILD'] = nil
228     # Bootstrap cache settings
229     # HTTP proxy settings
230     when 'extproxy'
231       unless EXTERNAL_HTTP_PROXY
232         abort 'No HTTP proxy set, but one is required by ' \
233               'TAILS_BUILD_OPTIONS. Aborting.'
234       end
235       ENV['TAILS_PROXY'] = EXTERNAL_HTTP_PROXY
236       ENV['TAILS_PROXY_TYPE'] = 'extproxy'
237     when 'vmproxy', 'vmproxy+extproxy'
238       ENV['TAILS_PROXY'] = INTERNAL_HTTP_PROXY
239       ENV['TAILS_PROXY_TYPE'] = 'vmproxy'
240       if opt == 'vmproxy+extproxy'
241         unless EXTERNAL_HTTP_PROXY
242           abort 'No HTTP proxy set, but one is required by ' \
243                 'TAILS_BUILD_OPTIONS. Aborting.'
244         end
245         ENV['TAILS_ACNG_PROXY'] = EXTERNAL_HTTP_PROXY
246       end
247     when 'noproxy'
248       ENV['TAILS_PROXY'] = nil
249       ENV['TAILS_PROXY_TYPE'] = 'noproxy'
250     when 'offline'
251       ENV['TAILS_OFFLINE_MODE'] = '1'
252     when /cachewebsite(?:=([a-z]+))?/
253       value = Regexp.last_match(1)
254       if releasing?
255         warn "Building a release ⇒ ignoring #{opt} build option"
256         ENV['TAILS_WEBSITE_CACHE'] = '0'
257       else
258         value = 'yes' if value.nil?
259         case value
260         when 'yes'
261           ENV['TAILS_WEBSITE_CACHE'] = '1'
262         when 'no'
263           ENV['TAILS_WEBSITE_CACHE'] = '0'
264         else
265           raise "Unsupported value for cachewebsite option: #{value}"
266         end
267       end
268     # SquashFS compression settings
269     when 'fastcomp', 'gzipcomp'
270       if releasing?
271         warn "Building a release ⇒ ignoring #{opt} build option"
272         ENV['MKSQUASHFS_OPTIONS'] = nil
273       else
274         ENV['MKSQUASHFS_OPTIONS'] = '-comp xz -no-exports'
275       end
276     when 'defaultcomp'
277       ENV['MKSQUASHFS_OPTIONS'] = nil
278     # Virtual hardware settings
279     when /machinetype=([a-zA-Z0-9_.-]+)/
280       ENV['TAILS_BUILD_MACHINE_TYPE'] = Regexp.last_match(1)
281     when /cpus=(\d+)/
282       ENV['TAILS_BUILD_CPUS'] = Regexp.last_match(1)
283     when /cpumodel=([a-zA-Z0-9_-]+)/
284       ENV['TAILS_BUILD_CPU_MODEL'] = Regexp.last_match(1)
285     # Git settings
286     when 'ignorechanges'
287       ENV['TAILS_BUILD_IGNORE_CHANGES'] = '1'
288     when /dateoffset=([-+]\d+)/
289       ENV['TAILS_DATE_OFFSET'] = Regexp.last_match(1)
290     # Developer convenience features
291     when 'keeprunning'
292       $keep_running = true
293       $force_cleanup = false
294     when 'forcecleanup'
295       $force_cleanup = true
296       $keep_running = false
297     when 'rescue'
298       $keep_running = true
299       ENV['TAILS_BUILD_FAILURE_RESCUE'] = '1'
300     # Jenkins
301     when 'mergebasebranch'
302       ENV['TAILS_MERGE_BASE_BRANCH'] = '1'
303     else
304       raise "Unknown Tails build option '#{opt}'"
305     end
306   end
308   if ENV['TAILS_OFFLINE_MODE'] == '1'
309     if ENV['TAILS_PROXY'].nil?
310       abort 'You must use a caching proxy when building offline'
311     end
312   end
315 task :ensure_clean_repository do
316   git_status = `git status --porcelain`
317   unless git_status.empty?
318     if ENV['TAILS_BUILD_IGNORE_CHANGES']
319       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
321         You have uncommitted changes in the Git repository. They will
322         be ignored for the upcoming build:
323         #{git_status}
325       END_OF_MESSAGE
326     else
327       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
329         You have uncommitted changes in the Git repository. Due to limitations
330         of the build system, you need to commit them before building Tails:
331         #{git_status}
333         If you don't care about those changes and want to build Tails nonetheless,
334         please add `ignorechanges` to the TAILS_BUILD_OPTIONS environment
335         variable.
337       END_OF_MESSAGE
338       abort 'Uncommitted changes. Aborting.'
339     end
340   end
343 def list_artifacts
344   user = vagrant_ssh_config('User')
345   stdout = capture_vagrant_ssh("find '/home/#{user}/amnesia/' -maxdepth 1 " \
346                                         "-name 'tails-amd64-*' " \
347                                         '-o -name tails-build-env.list').first
348   stdout.split("\n")
349 rescue VagrantCommandError
350   []
353 def remove_artifacts
354   list_artifacts.each do |artifact|
355     run_vagrant_ssh("sudo rm -f '#{artifact}'")
356   end
359 task ensure_clean_home_directory: ['vm:up'] do
360   remove_artifacts
363 task :validate_http_proxy do
364   if ENV['TAILS_PROXY']
365     proxy_host = URI.parse(ENV['TAILS_PROXY']).host
367     if proxy_host.nil?
368       ENV['TAILS_PROXY'] = nil
369       warn 'Ignoring invalid HTTP proxy.'
370       return
371     end
373     if ['localhost', '[::1]'].include?(proxy_host) \
374        || proxy_host.start_with?('127.0.0.')
375       abort 'Using an HTTP proxy listening on the loopback is doomed ' \
376             'to fail. Aborting.'
377     end
379     warn "Using HTTP proxy: #{ENV['TAILS_PROXY']}"
380   else
381     warn 'No HTTP proxy set.'
382   end
385 task :validate_git_state do
386   if git_helper('git_in_detached_head?') && !git_helper('git_on_a_tag?')
387     raise 'We are in detached head but the current commit is not tagged'
388   end
391 task setup_environment: ['validate_git_state'] do
392   ENV['GIT_COMMIT'] ||= git_helper('git_current_commit')
393   ENV['GIT_REF'] ||= git_helper('git_current_head_name')
394   if on_jenkins?
395     jenkins_branch = (ENV['GIT_BRANCH'] || '').sub(%r{^origin/}, '')
396     if !releasing? && jenkins_branch != ENV['GIT_REF']
397       raise "We expected to build the Git ref '#{ENV['GIT_REF']}', " \
398             "but GIT_REF in the environment says '#{jenkins_branch}'. Aborting!"
399     end
400   end
402   ENV['BASE_BRANCH_GIT_COMMIT'] ||= git_helper('git_base_branch_head')
403   ['GIT_COMMIT', 'GIT_REF', 'BASE_BRANCH_GIT_COMMIT'].each do |var|
404     next unless ENV[var].empty?
406     raise "Variable '#{var}' is empty, which should not be possible: " \
407           "either validate_git_state is buggy or the 'origin' remote " \
408           'does not point to the official Tails Git repository.'
409   end
412 task :maybe_clean_up_builder_vms do
413   clean_up_builder_vms if $force_cleanup
416 task :ensure_correct_permissions do
417   FileUtils.chmod('go+x', '.')
418   FileUtils.chmod_R('go+rX', ['.git', 'submodules', 'vagrant'])
420   # Changing permissions outside of the working copy, in particular on
421   # parent directories such as $HOME, feels too blunt and can have
422   # problematic security consequences, so we don't forcibly do that.
423   # Instead, when the permissions are not OK, display a nicer error
424   # message than "Virtio-9p Failed to initialize fs-driver […]"
425   begin
426     capture_command('sudo', '-u', 'libvirt-qemu', 'stat', '.git')
427   rescue CommandError
428     abort <<-END_OF_MESSAGE.gsub(/^      /, '')
430       Incorrect permissions: the libvirt-qemu user needs to be allowed
431       to traverse the filesystem up to #{ENV['PWD']}.
433       To fix this, you can for example run the following command
434       on every parent directory of #{ENV['PWD']} up to #{ENV['HOME']}
435       (inclusive):
437         chmod g+rx DIR && setfacl -m user:libvirt-qemu:rx DIR
439     END_OF_MESSAGE
440   end
443 desc 'Build Tails'
444 task build: [
445   'parse_build_options',
446   'ensure_clean_repository',
447   'maybe_clean_up_builder_vms',
448   'validate_git_state',
449   'setup_environment',
450   'validate_http_proxy',
451   'ensure_correct_permissions',
452   'vm:up',
453   'ensure_clean_home_directory',
454 ] do
455   begin
456     if ENV['TAILS_RAM_BUILD'] && !enough_free_memory_for_ram_build?
457       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
459         The virtual machine is not currently set with enough memory to
460         perform an in-memory build. Either remove the `ram` option from
461         the TAILS_BUILD_OPTIONS environment variable, or shut the
462         virtual machine down using `rake vm:halt` before trying again.
464       END_OF_MESSAGE
465       abort 'Not enough memory for the virtual machine to run an in-memory ' \
466             'build. Aborting.'
467     end
469     if ENV['TAILS_BUILD_CPUS'] \
470        && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
471       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
473         The virtual machine is currently running with #{current_vm_cpus}
474         virtual CPU(s). In order to change that number, you need to
475         stop the VM first, using `rake vm:halt`. Otherwise, please
476         adjust the `cpus` options accordingly.
478       END_OF_MESSAGE
479       abort 'The virtual machine needs to be reloaded to change the number ' \
480             'of CPUs. Aborting.'
481     end
483     exported_env = EXPORTED_VARIABLES
484                    .select { |k| ENV[k] }
485                    .map    { |k| "#{k}='#{ENV[k]}'" }.join(' ')
487     begin
488       retrieved_artifacts = false
489       run_vagrant_ssh("#{exported_env} build-tails")
490     rescue VagrantCommandError
491       retrieve_artifacts(missing_ok: true)
492       retrieved_artifacts = true
493     ensure
494       retrieve_artifacts(missing_ok: false) unless retrieved_artifacts
495       clean_up_builder_vms unless $keep_running
496     end
497   ensure
498     clean_up_builder_vms if $force_cleanup
499   end
502 desc 'Retrieve build artifacts from the Vagrant box'
503 task :retrieve_artifacts do
504   retrieve_artifacts
507 def retrieve_artifacts(missing_ok: false)
508   artifacts = list_artifacts
509   if artifacts.empty?
510     msg = 'No build artifacts were found!'
511     raise msg unless missing_ok
513     warn msg
514     return
515   end
516   user = vagrant_ssh_config('User')
517   hostname = vagrant_ssh_config('HostName')
518   key_file = vagrant_ssh_config('IdentityFile')
519   warn 'Retrieving artifacts from Vagrant build box.'
520   run_vagrant_ssh(
521     "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" } .join(' ')
522   )
523   fetch_command = [
524     'scp',
525     '-i', key_file,
526     # We need this since the user will not necessarily have a
527     # known_hosts entry. It is safe since an attacker must
528     # compromise libvirt's network config or the user running the
529     # command to modify the #{hostname} below.
530     '-o', 'StrictHostKeyChecking=no',
531     '-o', 'UserKnownHostsFile=/dev/null',
532     # Speed up the copy
533     '-o', 'Compression=no',
534   ]
535   fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
536   fetch_command << ENV['ARTIFACTS']
537   run_command(*fetch_command)
540 def box?
541   !capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?
544 def domain_name
545   "#{box_name}_default"
548 # XXX: giving up on a few worst offenders for now
549 # rubocop:disable Metrics/AbcSize
550 # rubocop:disable Metrics/MethodLength
551 def clean_up_builder_vms
552   libvirt = Libvirt.open('qemu:///system')
554   clean_up_domain = proc do |domain|
555     next if domain.nil?
557     domain.destroy if domain.active?
558     domain.undefine
559     begin
560       libvirt
561         .lookup_storage_pool_by_name('default')
562         .lookup_volume_by_name("#{domain.name}.img")
563         .delete
564     rescue Libvirt::RetrieveError
565       # Expected if the pool or disk does not exist
566     end
567   end
569   # Let's ensure that the VM we are about to create is cleaned up ...
570   previous_domain = libvirt.list_all_domains.find { |d| d.name == domain_name }
571   if previous_domain&.active?
572     begin
573       run_vagrant_ssh('mountpoint -q /var/cache/apt-cacher-ng')
574     rescue VagrantCommandError
575     # Nothing to unmount.
576     else
577       run_vagrant_ssh('sudo systemctl stop apt-cacher-ng.service')
578       run_vagrant_ssh('sudo umount /var/cache/apt-cacher-ng')
579       run_vagrant_ssh('sudo sync')
580     end
581     begin
582       run_vagrant_ssh('mountpoint -q /var/cache/tails-website')
583     rescue VagrantCommandError
584     # Nothing to unmount.
585     else
586       run_vagrant_ssh('sudo umount /var/cache/tails-website')
587       run_vagrant_ssh('sudo sync')
588     end
589   end
590   clean_up_domain.call(previous_domain)
592   # ... and the same for any residual VM based on another box (=>
593   # another domain name) that Vagrant still keeps track of.
594   old_domain =
595     begin
596       old_domain_uuid =
597         open('vagrant/.vagrant/machines/default/libvirt/id', 'r', &:read)
598         .strip
599       libvirt.lookup_domain_by_uuid(old_domain_uuid)
600     rescue Errno::ENOENT, Libvirt::RetrieveError
601       # Expected if we don't have vagrant/.vagrant, or if the VM was
602       # undefined for other reasons (e.g. manually).
603       nil
604     end
605   clean_up_domain.call(old_domain)
607   # We could use `vagrant destroy` here but due to vagrant-libvirt's
608   # upstream issue #746 we then risk losing the apt-cacher-ng data.
609   # Since we essentially implement `vagrant destroy` without this bug
610   # above, but in a way so it works even if `vagrant/.vagrant` does
611   # not exist, let's just do what is safest, i.e. avoiding `vagrant
612   # destroy`. For details, see the upstream issue:
613   #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
614   FileUtils.rm_rf('vagrant/.vagrant')
615 ensure
616   libvirt.close
618 # rubocop:enable Metrics/AbcSize
619 # rubocop:enable Metrics/MethodLength
621 desc 'Remove all libvirt volumes named tails-builder-* (run at your own risk!)'
622 task :clean_up_libvirt_volumes do
623   libvirt = Libvirt.open('qemu:///system')
624   begin
625     pool = libvirt.lookup_storage_pool_by_name('default')
626   rescue Libvirt::RetrieveError
627     # Expected if the pool does not exist
628   else
629     pool.list_volumes.each do |disk|
630       next unless /^tails-builder-/.match(disk)
632       begin
633         pool.lookup_volume_by_name(disk).delete
634       rescue Libvirt::RetrieveError
635         # Expected if the disk does not exist
636       end
637     end
638   ensure
639     libvirt.close
640   end
643 def on_jenkins?
644   !ENV['JENKINS_URL'].nil?
647 desc 'Clean up all build related files'
648 task clean_all: ['vm:destroy', 'basebox:clean_all']
650 namespace :vm do
651   desc 'Start the build virtual machine'
652   task up: [
653     'parse_build_options',
654     'validate_http_proxy',
655     'setup_environment',
656     'basebox:create',
657   ] do
658     case vm_state
659     when :not_created
660       clean_up_builder_vms
661     end
662     begin
663       run_vagrant('up', '--provision')
664     rescue VagrantCommandError => e
665       clean_up_builder_vms if $force_cleanup
666       raise e
667     end
668   end
670   desc 'SSH into the builder VM'
671   task :ssh do
672     run_vagrant('ssh')
673   end
675   desc 'Stop the build virtual machine'
676   task :halt do
677     run_vagrant('halt')
678   end
680   desc 'Re-run virtual machine setup'
681   task provision: [
682     'parse_build_options',
683     'validate_http_proxy',
684     'setup_environment',
685   ] do
686     run_vagrant('provision')
687   end
689   desc 'Destroy build virtual machine (clean up all files except the ' \
690        "vmproxy's apt-cacher-ng data and the website cache)"
691   task :destroy do
692     clean_up_builder_vms
693   end
696 namespace :basebox do
697   desc 'Create and import the base box unless already done'
698   task :create do
699     next if box?
701     warn <<-END_OF_MESSAGE.gsub(/^      /, '')
703       This is the first time we are using this Vagrant base box so we
704       will have to bootstrap by building it from scratch. This will
705       take around 20 minutes (depending on your hardware) plus the
706       time needed for downloading around 250 MiB of Debian packages.
708     END_OF_MESSAGE
709     box_dir = VAGRANT_PATH + '/definitions/tails-builder'
710     run_command("#{box_dir}/generate-tails-builder-box.sh")
711     # Let's use an absolute path since run_vagrant changes the working
712     # directory but File.delete doesn't
713     box_path = "#{box_dir}/#{box_name}.box"
714     run_vagrant('box', 'add', '--name', box_name, box_path)
715     File.delete(box_path)
716   end
718   def basebox_date(box)
719     Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
720   end
722   def baseboxes
723     capture_vagrant('box', 'list')
724       .first.lines
725       .grep(/^tails-builder-.*/)
726       .map { |x| x.chomp.sub(/\s.*$/, '') }
727   end
729   def clean_up_basebox(box)
730     run_vagrant('box', 'remove', '--force', box)
731     begin
732       libvirt = Libvirt.open('qemu:///system')
733       libvirt
734         .lookup_storage_pool_by_name('default')
735         .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
736         .delete
737     rescue Libvirt::RetrieveError
738       # Expected if the pool or disk does not exist
739     ensure
740       libvirt.close
741     end
742   end
744   desc 'Remove all base boxes'
745   task :clean_all do
746     baseboxes.each { |box| clean_up_basebox(box) }
747   end
749   desc 'Remove all base boxes older than six months'
750   task :clean_old do
751     boxes = baseboxes
752     # We always want to keep the newest basebox
753     boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
754     boxes.pop
755     boxes.each do |box|
756       clean_up_basebox(box) if basebox_date(box) < Date.today - 365.0 / 2.0
757     end
758   end