Test Suite: Add scenario "Creating a Persistent Storage"
[tails.git] / Rakefile
blobce03ec77d4c08f2be388a1eb1b344ab2bcef0b7f
1 # -*- mode: ruby -*-
2 # vi: set ft=ruby :
4 # Tails: https://tails.boum.org/
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_OFFLINE_MODE',
42   'TAILS_PROXY',
43   'TAILS_PROXY_TYPE',
44   'TAILS_RAM_BUILD',
45   'TAILS_WEBSITE_CACHE',
46   'GIT_COMMIT',
47   'GIT_REF',
48   'BASE_BRANCH_GIT_COMMIT',
49   'FEATURE_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://127.0.0.1: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, **kwargs)
75   Process.wait Kernel.spawn(*args, **kwargs)
76   return if $CHILD_STATUS.exitstatus.zero?
78   raise CommandError.new("command #{args}, #{kwargs} failed with exit status %<status>s",
79                          status: $CHILD_STATUS.exitstatus)
80 end
82 def capture_command(*args, **kwargs)
83   stdout, stderr, proc_status = Open3.capture3(*args, **kwargs)
84   if proc_status.exitstatus != 0
85     raise CommandError.new("command #{args}, #{kwargs} 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, **kwargs)
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, **kwargs)
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, **kwargs)
111   run_command('vagrant', *args, chdir: './vagrant', **kwargs)
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, **kwargs)
120   capture_command('vagrant', *args, chdir: './vagrant', **kwargs)
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 zstd -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 'nomergebasebranch'
302       $skip_mergebasebranch = true
303     else
304       raise "Unknown Tails build option '#{opt}'"
305     end
306   end
308   if ENV['TAILS_OFFLINE_MODE'] == '1' && ENV['TAILS_PROXY'].nil?
309     abort 'You must use a caching proxy when building offline'
310   end
313 task :ensure_clean_repository do
314   git_status = `git status --porcelain`
315   unless git_status.empty?
316     if ENV['TAILS_BUILD_IGNORE_CHANGES']
317       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
319         You have uncommitted changes in the Git repository. They will
320         be ignored for the upcoming build:
321         #{git_status}
323       END_OF_MESSAGE
324     else
325       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
327         You have uncommitted changes in the Git repository. Due to limitations
328         of the build system, you need to commit them before building Tails:
329         #{git_status}
331         If you don't care about those changes and want to build Tails nonetheless,
332         please add `ignorechanges` to the TAILS_BUILD_OPTIONS environment
333         variable.
335       END_OF_MESSAGE
336       abort 'Uncommitted changes. Aborting.'
337     end
338   end
341 def list_artifacts
342   user = vagrant_ssh_config('User')
343   stdout = capture_vagrant_ssh("find '/home/#{user}/amnesia/' -maxdepth 1 " \
344                                         "-name 'tails-amd64-*' " \
345                                         '-o -name tails-build-env.list').first
346   stdout.split("\n")
347 rescue VagrantCommandError
348   []
351 def remove_artifacts
352   list_artifacts.each do |artifact|
353     run_vagrant_ssh("sudo rm -f '#{artifact}'")
354   end
357 task ensure_clean_home_directory: ['vm:up'] do
358   remove_artifacts
361 task :validate_http_proxy do
362   if ENV['TAILS_PROXY']
363     proxy_host = URI.parse(ENV['TAILS_PROXY']).host
365     if proxy_host.nil?
366       ENV['TAILS_PROXY'] = nil
367       abort "Invalid HTTP proxy: #{ENV['TAILS_PROXY']}"
368     end
370     if ENV['TAILS_PROXY_TYPE'] == 'vmproxy'
371       warn 'Using the internal VM proxy'
372     else
373       if ['localhost', '[::1]'].include?(proxy_host) \
374          || proxy_host.start_with?('127.0.0.')
375         abort 'Using an HTTP proxy listening on the host\'s loopback ' \
376               'is doomed to fail. Aborting.'
377       end
378       warn "Using HTTP proxy: #{ENV['TAILS_PROXY']}"
379     end
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 merge_base_branch: ['parse_build_options', 'setup_environment'] do
413   next if $skip_mergebasebranch
415   branch = git_helper('git_current_branch')
416   base_branch = git_helper('base_branch')
417   source_date_faketime = `date --utc --date="$(dpkg-parsechangelog --show-field=Date)" '+%Y-%m-%d %H:%M:%S'`.chomp
418   next if releasing? || branch == base_branch
420   commit_before_merge = git_helper('git_current_commit')
421   warn "Merging base branch '#{base_branch}' (at commit " \
422        "#{ENV['BASE_BRANCH_GIT_COMMIT']}) ..."
423   begin
424     run_command('faketime', '-f', source_date_faketime, \
425                 'git', 'merge', '--no-edit', ENV['BASE_BRANCH_GIT_COMMIT'])
426   rescue CommandError
427     run_command('git', 'merge', '--abort')
428     raise <<-END_OF_MESSAGE.gsub(/^        /, '')
430           There were conflicts when merging the base branch; either
431           merge it yourself and resolve conflicts, or skip this merge
432           by rebuilding with the 'nomergebasebranch' option.
434     END_OF_MESSAGE
435   end
436   run_command('git', 'submodule', 'update', '--init')
438   # If we actually merged anything we'll re-run rake in the new Git
439   # state in order to avoid subtle build errors due to mixed state.
440   next if commit_before_merge == git_helper('git_current_commit')
442   ENV['GIT_COMMIT'] = git_helper('git_current_commit')
443   ENV['FEATURE_BRANCH_GIT_COMMIT'] = commit_before_merge
444   ENV['TAILS_BUILD_OPTIONS'] = (ENV['TAILS_BUILD_OPTIONS'] || '') + \
445                                ' nomergebasebranch'
446   Kernel.exec('rake', *ARGV)
449 task :maybe_clean_up_builder_vms do
450   clean_up_builder_vms if $force_cleanup
453 task :ensure_correct_permissions do
454   FileUtils.chmod('go+x', '.')
455   FileUtils.chmod_R('go+rX', ['.git', 'submodules', 'vagrant'])
457   # Changing permissions outside of the working copy, in particular on
458   # parent directories such as $HOME, feels too blunt and can have
459   # problematic security consequences, so we don't forcibly do that.
460   # Instead, when the permissions are not OK, display a nicer error
461   # message than "Virtio-9p Failed to initialize fs-driver […]"
462   begin
463     capture_command('sudo', '-u', 'libvirt-qemu', 'stat', '.git')
464   rescue CommandError
465     abort <<-END_OF_MESSAGE.gsub(/^      /, '')
467       Incorrect permissions: the libvirt-qemu user needs to be allowed
468       to traverse the filesystem up to #{ENV['PWD']}.
470       To fix this, you can for example run the following command
471       on every parent directory of #{ENV['PWD']} up to #{ENV['HOME']}
472       (inclusive):
474         chmod g+rx DIR && setfacl -m user:libvirt-qemu:rx DIR
476     END_OF_MESSAGE
477   end
480 desc 'Build Tails'
481 task build: [
482   'parse_build_options',
483   'ensure_clean_repository',
484   'maybe_clean_up_builder_vms',
485   'validate_git_state',
486   'setup_environment',
487   'merge_base_branch',
488   'validate_http_proxy',
489   'ensure_correct_permissions',
490   'vm:up',
491   'ensure_clean_home_directory',
492 ] do
493   if ENV['TAILS_RAM_BUILD'] && !enough_free_memory_for_ram_build?
494     warn <<-END_OF_MESSAGE.gsub(/^        /, '')
496         The virtual machine is not currently set with enough memory to
497         perform an in-memory build. Either remove the `ram` option from
498         the TAILS_BUILD_OPTIONS environment variable, or shut the
499         virtual machine down using `rake vm:halt` before trying again.
501     END_OF_MESSAGE
502     abort 'Not enough memory for the virtual machine to run an in-memory ' \
503           'build. Aborting.'
504   end
506   if ENV['TAILS_BUILD_CPUS'] \
507      && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
508     warn <<-END_OF_MESSAGE.gsub(/^        /, '')
510         The virtual machine is currently running with #{current_vm_cpus}
511         virtual CPU(s). In order to change that number, you need to
512         stop the VM first, using `rake vm:halt`. Otherwise, please
513         adjust the `cpus` options accordingly.
515     END_OF_MESSAGE
516     abort 'The virtual machine needs to be reloaded to change the number ' \
517           'of CPUs. Aborting.'
518   end
520   exported_env = EXPORTED_VARIABLES
521                  .select { |k| ENV[k] }
522                  .map    { |k| "#{k}='#{ENV[k]}'" }.join(' ')
524   begin
525     retrieved_artifacts = false
526     run_vagrant_ssh("#{exported_env} build-tails")
527   rescue VagrantCommandError
528     retrieve_artifacts(missing_ok: true)
529     retrieved_artifacts = true
530   ensure
531     retrieve_artifacts(missing_ok: false) unless retrieved_artifacts
532     clean_up_builder_vms unless $keep_running
533   end
534 ensure
535   clean_up_builder_vms if $force_cleanup
538 desc 'Retrieve build artifacts from the Vagrant box'
539 task :retrieve_artifacts do
540   retrieve_artifacts
543 def retrieve_artifacts(missing_ok: false)
544   artifacts = list_artifacts
545   if artifacts.empty?
546     msg = 'No build artifacts were found!'
547     raise msg unless missing_ok
549     warn msg
550     return
551   end
552   user = vagrant_ssh_config('User')
553   hostname = vagrant_ssh_config('HostName')
554   key_file = vagrant_ssh_config('IdentityFile')
555   warn 'Retrieving artifacts from Vagrant build box.'
556   run_vagrant_ssh(
557     "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" }.join(' ')
558   )
559   fetch_command = [
560     'scp',
561     '-i', key_file,
562     # We don't want to use any identity saved in ssh agent'
563     '-o', 'IdentityAgent=none',
564     # We need this since the user will not necessarily have a
565     # known_hosts entry. It is safe since an attacker must
566     # compromise libvirt's network config or the user running the
567     # command to modify the #{hostname} below.
568     '-o', 'StrictHostKeyChecking=no',
569     '-o', 'UserKnownHostsFile=/dev/null',
570     # Speed up the copy
571     '-o', 'Compression=no',
572   ]
573   fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
574   fetch_command << ENV['ARTIFACTS']
575   run_command(*fetch_command)
578 def box?
579   !capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?
582 def domain_name
583   "#{box_name}_default"
586 # XXX: giving up on a few worst offenders for now
587 # rubocop:disable Metrics/AbcSize
588 # rubocop:disable Metrics/MethodLength
589 def clean_up_builder_vms
590   libvirt = Libvirt.open('qemu:///system')
592   clean_up_domain = proc do |domain|
593     next if domain.nil?
595     domain.destroy if domain.active?
596     domain.undefine
597     begin
598       libvirt
599         .lookup_storage_pool_by_name('default')
600         .lookup_volume_by_name("#{domain.name}.img")
601         .delete
602     rescue Libvirt::RetrieveError
603       # Expected if the pool or disk does not exist
604     end
605   end
607   # Let's ensure that the VM we are about to create is cleaned up ...
608   previous_domain = libvirt.list_all_domains.find { |d| d.name == domain_name }
609   if previous_domain&.active?
610     begin
611       run_vagrant_ssh('mountpoint -q /var/cache/apt-cacher-ng')
612     rescue VagrantCommandError
613     # Nothing to unmount.
614     else
615       run_vagrant_ssh('sudo systemctl stop apt-cacher-ng.service')
616       run_vagrant_ssh('sudo umount /var/cache/apt-cacher-ng')
617       run_vagrant_ssh('sudo sync')
618     end
619     begin
620       run_vagrant_ssh('mountpoint -q /var/cache/tails-website')
621     rescue VagrantCommandError
622     # Nothing to unmount.
623     else
624       run_vagrant_ssh('sudo umount /var/cache/tails-website')
625       run_vagrant_ssh('sudo sync')
626     end
627   end
628   clean_up_domain.call(previous_domain)
630   # ... and the same for any residual VM based on another box (=>
631   # another domain name) that Vagrant still keeps track of.
632   old_domain =
633     begin
634       old_domain_uuid =
635         open('vagrant/.vagrant/machines/default/libvirt/id', 'r', &:read)
636         .strip
637       libvirt.lookup_domain_by_uuid(old_domain_uuid)
638     rescue Errno::ENOENT, Libvirt::RetrieveError
639       # Expected if we don't have vagrant/.vagrant, or if the VM was
640       # undefined for other reasons (e.g. manually).
641       nil
642     end
643   clean_up_domain.call(old_domain)
645   # We could use `vagrant destroy` here but due to vagrant-libvirt's
646   # upstream issue #746 we then risk losing the apt-cacher-ng data.
647   # Since we essentially implement `vagrant destroy` without this bug
648   # above, but in a way so it works even if `vagrant/.vagrant` does
649   # not exist, let's just do what is safest, i.e. avoiding `vagrant
650   # destroy`. For details, see the upstream issue:
651   #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
652   FileUtils.rm_rf('vagrant/.vagrant')
653 ensure
654   libvirt.close
656 # rubocop:enable Metrics/AbcSize
657 # rubocop:enable Metrics/MethodLength
659 desc 'Remove all libvirt volumes named tails-builder-* (run at your own risk!)'
660 task :clean_up_libvirt_volumes do
661   libvirt = Libvirt.open('qemu:///system')
662   begin
663     pool = libvirt.lookup_storage_pool_by_name('default')
664   rescue Libvirt::RetrieveError
665     # Expected if the pool does not exist
666   else
667     pool.list_volumes.each do |disk|
668       next unless /^tails-builder-/.match(disk)
670       begin
671         pool.lookup_volume_by_name(disk).delete
672       rescue Libvirt::RetrieveError
673         # Expected if the disk does not exist
674       end
675     end
676   ensure
677     libvirt.close
678   end
681 def on_jenkins?
682   !ENV['JENKINS_URL'].nil?
685 desc 'Clean up all build related files'
686 task clean_all: ['vm:destroy', 'basebox:clean_all']
688 namespace :vm do
689   desc 'Start the build virtual machine'
690   task up: [
691     'parse_build_options',
692     'validate_http_proxy',
693     'setup_environment',
694     'basebox:create',
695   ] do
696     case vm_state
697     when :not_created
698       clean_up_builder_vms
699     end
700     begin
701       run_vagrant('up', '--provision')
702     rescue VagrantCommandError => e
703       clean_up_builder_vms if $force_cleanup
704       raise e
705     end
706   end
708   desc 'SSH into the builder VM'
709   task :ssh do
710     run_vagrant('ssh')
711   end
713   desc 'Stop the build virtual machine'
714   task :halt do
715     run_vagrant('halt')
716   end
718   desc 'Re-run virtual machine setup'
719   task provision: [
720     'parse_build_options',
721     'validate_http_proxy',
722     'setup_environment',
723   ] do
724     run_vagrant('provision')
725   end
727   desc 'Destroy build virtual machine (clean up all files except the ' \
728        "vmproxy's apt-cacher-ng data and the website cache)"
729   task :destroy do
730     clean_up_builder_vms
731   end
734 namespace :basebox do
735   desc 'Create and import the base box unless already done'
736   task :create do
737     next if box?
739     warn <<-END_OF_MESSAGE.gsub(/^      /, '')
741       This is the first time we are using this Vagrant base box so we
742       will have to bootstrap by building it from scratch. This will
743       take around 20 minutes (depending on your hardware) plus the
744       time needed for downloading around 250 MiB of Debian packages.
746     END_OF_MESSAGE
747     run_command("#{VAGRANT_PATH}/definitions/tails-builder/generate-tails-builder-box.sh")
748     box_dir = Dir.pwd
749     # Let's use an absolute path since run_vagrant changes the working
750     # directory but File.delete doesn't
751     box_path = "#{box_dir}/#{box_name}.box"
752     run_vagrant('box', 'add', '--name', box_name, box_path)
753     File.delete(box_path)
754   end
756   def basebox_date(box)
757     Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
758   end
760   def baseboxes
761     capture_vagrant('box', 'list')
762       .first.lines
763       .grep(/^tails-builder-.*/)
764       .map { |x| x.chomp.sub(/\s.*$/, '') }
765   end
767   def clean_up_basebox(box)
768     run_vagrant('box', 'remove', '--force', box)
769     begin
770       libvirt = Libvirt.open('qemu:///system')
771       libvirt
772         .lookup_storage_pool_by_name('default')
773         .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
774         .delete
775     rescue Libvirt::RetrieveError
776       # Expected if the pool or disk does not exist
777     ensure
778       libvirt.close
779     end
780   end
782   desc 'Remove all base boxes'
783   task :clean_all do
784     baseboxes.each { |box| clean_up_basebox(box) }
785   end
787   desc 'Remove all base boxes older than six months'
788   task :clean_old do
789     boxes = baseboxes
790     # We always want to keep the newest basebox
791     boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
792     boxes.pop
793     boxes.each do |box|
794       clean_up_basebox(box) if basebox_date(box) < Date.today - 365.0 / 2.0
795     end
796   end