Merge branch '20515-test-suite-connectivity-check-with-dnsmasq' into 'stable'
[tails.git] / Rakefile
blob2be7cbdb477e8037d065e87544208c16de1a83b1
1 # -*- mode: ruby -*-
2 # vi: set ft=ruby :
4 # Tails: https://tails.net/
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 " \
79                          'with exit status %<status>s',
80                          status: $CHILD_STATUS.exitstatus)
81 end
83 def capture_command(*args, **kwargs)
84   stdout, stderr, proc_status = Open3.capture3(*args, **kwargs)
85   if proc_status.exitstatus != 0
86     raise CommandError.new("command #{args}, #{kwargs} failed with exit status " \
87                            '%<status>s: %<stderr>s',
88                            stderr:, status: proc_status.exitstatus)
89   end
90   [stdout, stderr]
91 end
93 def git_helper(*args, **kwargs)
94   question = args.first.end_with?('?')
95   args.first.sub!(/\?$/, '')
96   status = 0
97   stdout = ''
98   begin
99     stdout, = capture_command('auto/scripts/utils.sh', *args, **kwargs)
100   rescue CommandError => e
101     status = e.status
102   end
103   question ? status.zero? : stdout.chomp
106 class VagrantCommandError < CommandError
109 # Runs the vagrant command, letting stdout/stderr through. Throws an
110 # exception unless the vagrant command succeeds.
111 def run_vagrant(*args, **kwargs)
112   run_command('vagrant', *args, chdir: './vagrant', **kwargs)
113 rescue CommandError => e
114   raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " \
115                              "status #{e.status}")
118 # Runs the vagrant command, not letting stdout/stderr through, and
119 # returns [stdout, stderr, Process::Status].
120 def capture_vagrant(*args, **kwargs)
121   capture_command('vagrant', *args, chdir: './vagrant', **kwargs)
122 rescue CommandError => e
123   raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " \
124                              "status #{e.status}: #{e.stderr}")
127 [:run_vagrant, :capture_vagrant].each do |m|
128   define_method "#{m}_ssh" do |*args|
129     method(m).call('ssh', '-c', *args, '--', '-q')
130   end
133 def vagrant_ssh_config(key)
134   # Cache results
135   if $vagrant_ssh_config.nil?
136     $vagrant_ssh_config = capture_vagrant('ssh-config')
137                           .first.split("\n") \
138                           .map { |line| line.strip.split(/\s+/, 2) }.to_h
139     # The path in the ssh-config output is quoted, which is not what
140     # is expected outside of a shell, so let's get rid of the quotes.
141     $vagrant_ssh_config['IdentityFile'].gsub!(/^"|"$/, '')
142   end
143   $vagrant_ssh_config[key]
146 def current_vm_cpus
147   capture_vagrant_ssh('grep -c "^processor\s*:" /proc/cpuinfo').first.chomp.to_i
150 def vm_state
151   out, = capture_vagrant('status')
152   status_line = out.split("\n")[2]
153   if    status_line['not created']
154     :not_created
155   elsif status_line['shutoff']
156     :poweroff
157   elsif status_line['running']
158     :running
159   else
160     raise 'could not determine VM state'
161   end
164 def releasing?
165   git_helper('git_on_a_tag?')
168 def system_cpus
169   File.read('/proc/cpuinfo').scan(/^processor\s+:/).count
172 ENV['TAILS_WEBSITE_CACHE'] = releasing? ? '0' : '1'
174 task :parse_build_options do
175   options = []
177   # Default to in-memory builds
178   options << 'ram'
179   # Default to build using the in-VM proxy
180   options << 'vmproxy'
181   # Default to fast compression on development branches
182   options << 'fastcomp' unless releasing?
183   # Default to use all host CPUs
184   options << "cpus=#{system_cpus}"
186   options += ENV['TAILS_BUILD_OPTIONS'].split if ENV['TAILS_BUILD_OPTIONS']
188   options.uniq.each do |opt| # rubocop:disable Metrics/BlockLength
189     case opt
190     # Memory build settings
191     when 'ram'
192       ENV['TAILS_RAM_BUILD'] = '1'
193     when 'noram'
194       ENV['TAILS_RAM_BUILD'] = nil
195     # Bootstrap cache settings
196     # HTTP proxy settings
197     when 'extproxy'
198       unless EXTERNAL_HTTP_PROXY
199         abort 'No HTTP proxy set, but one is required by ' \
200               'TAILS_BUILD_OPTIONS. Aborting.'
201       end
202       ENV['TAILS_PROXY'] = EXTERNAL_HTTP_PROXY
203       ENV['TAILS_PROXY_TYPE'] = 'extproxy'
204     when 'vmproxy', 'vmproxy+extproxy'
205       ENV['TAILS_PROXY'] = INTERNAL_HTTP_PROXY
206       ENV['TAILS_PROXY_TYPE'] = 'vmproxy'
207       if opt == 'vmproxy+extproxy'
208         unless EXTERNAL_HTTP_PROXY
209           abort 'No HTTP proxy set, but one is required by ' \
210                 'TAILS_BUILD_OPTIONS. Aborting.'
211         end
212         ENV['TAILS_ACNG_PROXY'] = EXTERNAL_HTTP_PROXY
213       end
214     when 'noproxy'
215       ENV['TAILS_PROXY'] = nil
216       ENV['TAILS_PROXY_TYPE'] = 'noproxy'
217     when 'offline'
218       ENV['TAILS_OFFLINE_MODE'] = '1'
219     when /cachewebsite(?:=([a-z]+))?/
220       value = Regexp.last_match(1)
221       if releasing?
222         warn "Building a release ⇒ ignoring #{opt} build option"
223         ENV['TAILS_WEBSITE_CACHE'] = '0'
224       else
225         value = 'yes' if value.nil?
226         case value
227         when 'yes'
228           ENV['TAILS_WEBSITE_CACHE'] = '1'
229         when 'no'
230           ENV['TAILS_WEBSITE_CACHE'] = '0'
231         else
232           raise "Unsupported value for cachewebsite option: #{value}"
233         end
234       end
235     # SquashFS compression settings
236     when 'fastcomp', 'gzipcomp'
237       if releasing?
238         warn "Building a release ⇒ ignoring #{opt} build option"
239         ENV['MKSQUASHFS_OPTIONS'] = nil
240       else
241         ENV['MKSQUASHFS_OPTIONS'] = '-comp zstd -no-exports'
242       end
243     when 'defaultcomp'
244       ENV['MKSQUASHFS_OPTIONS'] = nil
245     # Virtual hardware settings
246     when /machinetype=([a-zA-Z0-9_.-]+)/
247       ENV['TAILS_BUILD_MACHINE_TYPE'] = Regexp.last_match(1)
248     when /cpus=(\d+)/
249       ENV['TAILS_BUILD_CPUS'] = Regexp.last_match(1)
250     when /cpumodel=([a-zA-Z0-9_-]+)/
251       ENV['TAILS_BUILD_CPU_MODEL'] = Regexp.last_match(1)
252     # Git settings
253     when 'ignorechanges'
254       ENV['TAILS_BUILD_IGNORE_CHANGES'] = '1'
255     when /dateoffset=([-+]\d+)/
256       ENV['TAILS_DATE_OFFSET'] = Regexp.last_match(1)
257     # Developer convenience features
258     when 'keeprunning'
259       $keep_running = true
260       $force_cleanup = false
261     when 'forcecleanup'
262       $force_cleanup = true
263       $keep_running = false
264     when 'rescue'
265       $keep_running = true
266       ENV['TAILS_BUILD_FAILURE_RESCUE'] = '1'
267     # Jenkins
268     when 'nomergebasebranch'
269       $skip_mergebasebranch = true
270     else
271       raise "Unknown Tails build option '#{opt}'"
272     end
273   end
275   if ENV['TAILS_OFFLINE_MODE'] == '1' && ENV['TAILS_PROXY'].nil?
276     abort 'You must use a caching proxy when building offline'
277   end
280 task :ensure_enough_free_memory do
281   next unless vm_state == :not_created
283   cpus = ENV['TAILS_BUILD_CPUS'].to_i
284   free_memory = capture_command('free', '--mebi').first.split[12].to_i
285   required_memory = if ENV['TAILS_RAM_BUILD']
286                       vm_memory_for_ram_builds(cpus)
287                     else
288                       vm_memory_base(cpus)
289                     end
291   if free_memory < required_memory
292     message = <<-END_OF_MESSAGE.gsub(/^ */, '')
294       There is not enough free memory to start the virtual machine: only
295       #{free_memory} MB is free but #{required_memory} MB is required.
297     END_OF_MESSAGE
298     message += if ENV['TAILS_RAM_BUILD']
299                  <<-END_OF_MESSAGE.gsub(/^ */, '')
300       Try again with the `noram` option added to the TAILS_BUILD_OPTIONS
301       environment variable to force a slower on-disk build.
303                  END_OF_MESSAGE
304                else
305                  <<-END_OF_MESSAGE.gsub(/^ */, '')
306       Try freeing up some system memory before attempting to build again.
308                  END_OF_MESSAGE
309                end
310     abort message
311   end
314 task :ensure_clean_repository do
315   git_status = `git status --porcelain`
316   unless git_status.empty?
317     if ENV['TAILS_BUILD_IGNORE_CHANGES']
318       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
320         You have uncommitted changes in the Git repository. They will
321         be ignored for the upcoming build:
322         #{git_status}
324       END_OF_MESSAGE
325     else
326       warn <<-END_OF_MESSAGE.gsub(/^        /, '')
328         You have uncommitted changes in the Git repository. Due to limitations
329         of the build system, you need to commit them before building Tails:
330         #{git_status}
332         If you don't care about those changes and want to build Tails nonetheless,
333         please add `ignorechanges` to the TAILS_BUILD_OPTIONS environment
334         variable.
336       END_OF_MESSAGE
337       abort 'Uncommitted changes. Aborting.'
338     end
339   end
342 def list_artifacts
343   user = vagrant_ssh_config('User')
344   stdout = capture_vagrant_ssh("find '/home/#{user}/amnesia/' -maxdepth 1 " \
345                                         "-name 'tails-amd64-*' " \
346                                         '-o -name tails-build-env.list').first
347   stdout.split("\n")
348 rescue VagrantCommandError
349   []
352 def remove_artifacts
353   list_artifacts.each do |artifact|
354     run_vagrant_ssh("sudo rm -f '#{artifact}'")
355   end
358 task ensure_clean_home_directory: ['vm:up'] do
359   remove_artifacts
362 task :validate_http_proxy do
363   if ENV['TAILS_PROXY']
364     proxy_host = URI.parse(ENV['TAILS_PROXY']).host
366     if proxy_host.nil?
367       ENV['TAILS_PROXY'] = nil
368       abort "Invalid HTTP proxy: #{ENV['TAILS_PROXY']}"
369     end
371     if ENV['TAILS_PROXY_TYPE'] == 'vmproxy'
372       warn 'Using the internal VM proxy'
373     else
374       if ['localhost', '[::1]'].include?(proxy_host) \
375          || proxy_host.start_with?('127.0.0.')
376         abort 'Using an HTTP proxy listening on the host\'s loopback ' \
377               'is doomed to fail. Aborting.'
378       end
379       warn "Using HTTP proxy: #{ENV['TAILS_PROXY']}"
380     end
381   else
382     warn 'No HTTP proxy set.'
383   end
386 task :validate_git_state do
387   if git_helper('git_in_detached_head?') && !git_helper('git_on_a_tag?')
388     raise 'We are in detached head but the current commit is not tagged'
389   end
392 task setup_environment: ['validate_git_state'] do
393   ENV['GIT_COMMIT'] ||= git_helper('git_current_commit')
394   ENV['GIT_REF'] ||= git_helper('git_current_head_name')
395   if on_jenkins?
396     jenkins_branch = (ENV['GIT_BRANCH'] || '').sub(%r{^origin/}, '')
397     if !releasing? && jenkins_branch != ENV['GIT_REF']
398       raise "We expected to build the Git ref '#{ENV['GIT_REF']}', " \
399             "but GIT_REF in the environment says '#{jenkins_branch}'. Aborting!"
400     end
401   end
403   ENV['BASE_BRANCH_GIT_COMMIT'] ||= git_helper('git_base_branch_head')
404   ['GIT_COMMIT', 'GIT_REF', 'BASE_BRANCH_GIT_COMMIT'].each do |var|
405     next unless ENV[var].empty?
407     raise "Variable '#{var}' is empty, which should not be possible: " \
408           "either validate_git_state is buggy or the 'origin' remote " \
409           'does not point to the official Tails Git repository.'
410   end
413 task merge_base_branch: ['parse_build_options', 'setup_environment'] do
414   next if $skip_mergebasebranch
416   branch = git_helper('git_current_branch')
417   base_branch = git_helper('base_branch')
418   source_date_faketime = `date --utc --date="$(dpkg-parsechangelog --show-field=Date)" \
419                                '+%Y-%m-%d %H:%M:%S'`.chomp
420   next if releasing? || branch == base_branch
422   commit_before_merge = git_helper('git_current_commit')
423   warn "Merging base branch '#{base_branch}' (at commit " \
424        "#{ENV['BASE_BRANCH_GIT_COMMIT']}) ..."
425   begin
426     run_command('faketime', '-f', source_date_faketime, \
427                 'git', 'merge', '--no-edit', ENV['BASE_BRANCH_GIT_COMMIT'])
428   rescue CommandError
429     run_command('git', 'merge', '--abort')
430     raise <<-END_OF_MESSAGE.gsub(/^        /, '')
432           There were conflicts when merging the base branch; either
433           merge it yourself and resolve conflicts, or skip this merge
434           by rebuilding with the 'nomergebasebranch' option.
436     END_OF_MESSAGE
437   end
438   run_command('git', 'submodule', 'update', '--init')
440   # If we actually merged anything we'll re-run rake in the new Git
441   # state in order to avoid subtle build errors due to mixed state.
442   next if commit_before_merge == git_helper('git_current_commit')
444   ENV['GIT_COMMIT'] = git_helper('git_current_commit')
445   ENV['FEATURE_BRANCH_GIT_COMMIT'] = commit_before_merge
446   ENV['TAILS_BUILD_OPTIONS'] = "#{ENV['TAILS_BUILD_OPTIONS'] || ''} nomergebasebranch"
447   Kernel.exec('rake', *ARGV)
450 task :maybe_clean_up_builder_vms do
451   clean_up_builder_vms if $force_cleanup
454 task :ensure_correct_permissions do
455   FileUtils.chmod('go+x', '.')
456   FileUtils.chmod_R('go+rX', ['.git', 'submodules', 'vagrant'])
458   # Changing permissions outside of the working copy, in particular on
459   # parent directories such as $HOME, feels too blunt and can have
460   # problematic security consequences, so we don't forcibly do that.
461   # Instead, when the permissions are not OK, display a nicer error
462   # message than "Virtio-9p Failed to initialize fs-driver […]"
463   begin
464     capture_command('sudo', '-u', 'libvirt-qemu', 'stat', '.git')
465   rescue CommandError
466     abort <<-END_OF_MESSAGE.gsub(/^      /, '')
468       Incorrect permissions: the libvirt-qemu user needs to be allowed
469       to traverse the filesystem up to #{ENV['PWD']}.
471       To fix this, you can for example run the following command
472       on every parent directory of #{ENV['PWD']} up to #{ENV['HOME']}
473       (inclusive):
475         chmod g+rx DIR && setfacl -m user:libvirt-qemu:rx DIR
477     END_OF_MESSAGE
478   end
481 desc 'Build Tails'
482 task build: [
483   'parse_build_options',
484   'ensure_clean_repository',
485   'maybe_clean_up_builder_vms',
486   'validate_git_state',
487   'setup_environment',
488   'merge_base_branch',
489   'validate_http_proxy',
490   'ensure_correct_permissions',
491   'vm:up',
492   'ensure_clean_home_directory',
493 ] do
494   if ENV['TAILS_BUILD_CPUS'] \
495      && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
496     warn <<-END_OF_MESSAGE.gsub(/^        /, '')
498         The virtual machine is currently running with #{current_vm_cpus}
499         virtual CPU(s). In order to change that number, you need to
500         stop the VM first, using `rake vm:halt`. Otherwise, please
501         adjust the `cpus` options accordingly.
503     END_OF_MESSAGE
504     abort 'The virtual machine needs to be reloaded to change the number ' \
505           'of CPUs. Aborting.'
506   end
508   exported_env = EXPORTED_VARIABLES
509                  .select { |k| ENV[k] }
510                  .map    { |k| "#{k}='#{ENV[k]}'" }.join(' ')
512   begin
513     retrieved_artifacts = false
514     run_vagrant_ssh("#{exported_env} build-tails")
515   rescue VagrantCommandError
516     retrieve_artifacts(missing_ok: true)
517     retrieved_artifacts = true
518   ensure
519     retrieve_artifacts(missing_ok: false) unless retrieved_artifacts
520     clean_up_builder_vms unless $keep_running
521   end
522 ensure
523   clean_up_builder_vms if $force_cleanup
526 desc 'Retrieve build artifacts from the Vagrant box'
527 task :retrieve_artifacts do
528   retrieve_artifacts
531 def retrieve_artifacts(missing_ok: false)
532   artifacts = list_artifacts
533   if artifacts.empty?
534     msg = 'No build artifacts were found!'
535     raise msg unless missing_ok
537     warn msg
538     return
539   end
540   user = vagrant_ssh_config('User')
541   hostname = vagrant_ssh_config('HostName')
542   key_file = vagrant_ssh_config('IdentityFile')
543   warn 'Retrieving artifacts from Vagrant build box.'
544   run_vagrant_ssh(
545     "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" }.join(' ')
546   )
547   fetch_command = [
548     'scp',
549     '-i', key_file,
550     # We don't want to use any identity saved in ssh agent'
551     '-o', 'IdentityAgent=none',
552     # We need this since the user will not necessarily have a
553     # known_hosts entry. It is safe since an attacker must
554     # compromise libvirt's network config or the user running the
555     # command to modify the #{hostname} below.
556     '-o', 'StrictHostKeyChecking=no',
557     '-o', 'UserKnownHostsFile=/dev/null',
558     # Speed up the copy
559     '-o', 'Compression=no',
560   ]
561   fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
562   fetch_command << ENV['ARTIFACTS']
563   run_command(*fetch_command)
566 def box?
567   !capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?
570 def domain_name
571   "#{box_name}_default"
574 def clean_up_builder_vms
575   libvirt = Libvirt.open('qemu:///system')
577   clean_up_domain = proc do |domain|
578     next if domain.nil?
580     domain.destroy if domain.active?
581     domain.undefine
582     begin
583       libvirt
584         .lookup_storage_pool_by_name('default')
585         .lookup_volume_by_name("#{domain.name}.img")
586         .delete
587     rescue Libvirt::RetrieveError
588       # Expected if the pool or disk does not exist
589     end
590   end
592   # Let's ensure that the VM we are about to create is cleaned up ...
593   previous_domain = libvirt.list_all_domains.find { |d| d.name == domain_name }
594   if previous_domain&.active?
595     begin
596       run_vagrant_ssh('mountpoint -q /var/cache/apt-cacher-ng')
597     rescue VagrantCommandError
598     # Nothing to unmount.
599     else
600       run_vagrant_ssh('sudo systemctl stop apt-cacher-ng.service')
601       run_vagrant_ssh('sudo umount /var/cache/apt-cacher-ng')
602       run_vagrant_ssh('sudo sync')
603     end
604     begin
605       run_vagrant_ssh('mountpoint -q /var/cache/tails-website')
606     rescue VagrantCommandError
607     # Nothing to unmount.
608     else
609       run_vagrant_ssh('sudo umount /var/cache/tails-website')
610       run_vagrant_ssh('sudo sync')
611     end
612   end
613   clean_up_domain.call(previous_domain)
615   # ... and the same for any residual VM based on another box (=>
616   # another domain name) that Vagrant still keeps track of.
617   old_domain =
618     begin
619       old_domain_uuid =
620         open('vagrant/.vagrant/machines/default/libvirt/id', 'r', &:read)
621         .strip
622       libvirt.lookup_domain_by_uuid(old_domain_uuid)
623     rescue Errno::ENOENT, Libvirt::RetrieveError
624       # Expected if we don't have vagrant/.vagrant, or if the VM was
625       # undefined for other reasons (e.g. manually).
626       nil
627     end
628   clean_up_domain.call(old_domain)
630   # We could use `vagrant destroy` here but due to vagrant-libvirt's
631   # upstream issue #746 we then risk losing the apt-cacher-ng data.
632   # Since we essentially implement `vagrant destroy` without this bug
633   # above, but in a way so it works even if `vagrant/.vagrant` does
634   # not exist, let's just do what is safest, i.e. avoiding `vagrant
635   # destroy`. For details, see the upstream issue:
636   #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
637   FileUtils.rm_rf('vagrant/.vagrant')
638 ensure
639   libvirt.close
642 desc 'Remove all libvirt volumes named tails-builder-* (run at your own risk!)'
643 task :clean_up_libvirt_volumes do
644   libvirt = Libvirt.open('qemu:///system')
645   begin
646     pool = libvirt.lookup_storage_pool_by_name('default')
647   rescue Libvirt::RetrieveError
648     # Expected if the pool does not exist
649   else
650     pool.list_volumes.each do |disk|
651       next unless /^tails-builder-/.match(disk)
653       begin
654         pool.lookup_volume_by_name(disk).delete
655       rescue Libvirt::RetrieveError
656         # Expected if the disk does not exist
657       end
658     end
659   ensure
660     libvirt.close
661   end
664 def on_jenkins?
665   !ENV['JENKINS_URL'].nil?
668 desc 'Clean up all build related files'
669 task clean_all: ['vm:destroy', 'basebox:clean_all']
671 namespace :vm do
672   desc 'Start the build virtual machine'
673   task up: [
674     'parse_build_options',
675     'ensure_enough_free_memory',
676     'validate_http_proxy',
677     'setup_environment',
678     'basebox:create',
679   ] do
680     case vm_state
681     when :not_created
682       clean_up_builder_vms
683     end
684     begin
685       run_vagrant('up', '--provision')
686     rescue VagrantCommandError => e
687       clean_up_builder_vms if $force_cleanup
688       raise e
689     end
690   end
692   desc 'SSH into the builder VM'
693   task :ssh do
694     run_vagrant('ssh')
695   end
697   desc 'Stop the build virtual machine'
698   task :halt do
699     run_vagrant('halt')
700   end
702   desc 'Re-run virtual machine setup'
703   task provision: [
704     'parse_build_options',
705     'validate_http_proxy',
706     'setup_environment',
707   ] do
708     run_vagrant('provision')
709   end
711   desc 'Destroy build virtual machine (clean up all files except the ' \
712        "vmproxy's apt-cacher-ng data and the website cache)"
713   task :destroy do
714     clean_up_builder_vms
715   end
718 namespace :basebox do
719   desc 'Create and import the base box unless already done'
720   task :create do
721     next if box?
723     warn <<-END_OF_MESSAGE.gsub(/^      /, '')
725       This is the first time we are using this Vagrant base box so we
726       will have to bootstrap by building it from scratch. This will
727       take around 20 minutes (depending on your hardware) plus the
728       time needed for downloading around 250 MiB of Debian packages.
730     END_OF_MESSAGE
731     run_command(
732       "#{VAGRANT_PATH}/definitions/tails-builder/generate-tails-builder-box.sh"
733     )
734     box_dir = Dir.pwd
735     # Let's use an absolute path since run_vagrant changes the working
736     # directory but File.delete doesn't
737     box_path = "#{box_dir}/#{box_name}.box"
738     run_vagrant('box', 'add', '--name', box_name, box_path)
739     File.delete(box_path)
740   end
742   def basebox_date(box)
743     Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
744   end
746   def baseboxes
747     capture_vagrant('box', 'list')
748       .first.lines
749       .grep(/^tails-builder-.*/)
750       .map { |x| x.chomp.sub(/\s.*$/, '') }
751   end
753   def clean_up_basebox(box)
754     run_vagrant('box', 'remove', '--force', box)
755     begin
756       libvirt = Libvirt.open('qemu:///system')
757       libvirt
758         .lookup_storage_pool_by_name('default')
759         .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
760         .delete
761     rescue Libvirt::RetrieveError
762       # Expected if the pool or disk does not exist
763     ensure
764       libvirt.close
765     end
766   end
768   desc 'Remove all base boxes'
769   task :clean_all do
770     baseboxes.each { |box| clean_up_basebox(box) }
771   end
773   desc 'Remove all base boxes older than six months'
774   task :clean_old do
775     boxes = baseboxes
776     # We always want to keep the newest basebox
777     boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
778     boxes.pop
779     boxes.each do |box|
780       clean_up_basebox(box) if basebox_date(box) < Date.today - 365.0 / 2.0
781     end
782   end