Merge branch 'stable' into devel
[tails.git] / Rakefile
blobc10b7bc7af4127b876f4d556fb2cfdcec6d7de68
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 enough_free_host_memory_for_ram_build?
165   return false unless RbConfig::CONFIG['host_os'] =~ /linux/i
167   begin
168     usable_free_mem = `free`.split[12].to_i
169     usable_free_mem > VM_MEMORY_FOR_RAM_BUILDS * 1024
170   rescue StandardError
171     false
172   end
175 def free_vm_memory
176   capture_vagrant_ssh('free').first.chomp.split[12].to_i
179 def enough_free_vm_memory_for_ram_build?
180   free_vm_memory > BUILD_SPACE_REQUIREMENT * 1024
183 def enough_free_memory_for_ram_build?
184   if vm_state == :running
185     enough_free_vm_memory_for_ram_build?
186   else
187     enough_free_host_memory_for_ram_build?
188   end
191 def releasing?
192   git_helper('git_on_a_tag?')
195 def system_cpus
196   return unless RbConfig::CONFIG['host_os'] =~ /linux/i
198   begin
199     File.read('/proc/cpuinfo').scan(/^processor\s+:/).count
200   rescue StandardError
201     nil
202   end
205 ENV['TAILS_WEBSITE_CACHE'] = releasing? ? '0' : '1'
207 task :parse_build_options do
208   options = []
210   # Default to in-memory builds if there is enough RAM available
211   options << 'ram' if enough_free_memory_for_ram_build?
212   # Default to build using the in-VM proxy
213   options << 'vmproxy'
214   # Default to fast compression on development branches
215   options << 'fastcomp' unless releasing?
216   # Default to the number of system CPUs when we can figure it out
217   cpus = system_cpus
218   options << "cpus=#{cpus}" if cpus
220   options += ENV['TAILS_BUILD_OPTIONS'].split if ENV['TAILS_BUILD_OPTIONS']
222   options.uniq.each do |opt| # rubocop:disable Metrics/BlockLength
223     case opt
224     # Memory build settings
225     when 'ram'
226       ENV['TAILS_RAM_BUILD'] = '1'
227     when 'noram'
228       ENV['TAILS_RAM_BUILD'] = nil
229     # Bootstrap cache settings
230     # HTTP proxy settings
231     when 'extproxy'
232       unless EXTERNAL_HTTP_PROXY
233         abort 'No HTTP proxy set, but one is required by ' \
234               'TAILS_BUILD_OPTIONS. Aborting.'
235       end
236       ENV['TAILS_PROXY'] = EXTERNAL_HTTP_PROXY
237       ENV['TAILS_PROXY_TYPE'] = 'extproxy'
238     when 'vmproxy', 'vmproxy+extproxy'
239       ENV['TAILS_PROXY'] = INTERNAL_HTTP_PROXY
240       ENV['TAILS_PROXY_TYPE'] = 'vmproxy'
241       if opt == 'vmproxy+extproxy'
242         unless EXTERNAL_HTTP_PROXY
243           abort 'No HTTP proxy set, but one is required by ' \
244                 'TAILS_BUILD_OPTIONS. Aborting.'
245         end
246         ENV['TAILS_ACNG_PROXY'] = EXTERNAL_HTTP_PROXY
247       end
248     when 'noproxy'
249       ENV['TAILS_PROXY'] = nil
250       ENV['TAILS_PROXY_TYPE'] = 'noproxy'
251     when 'offline'
252       ENV['TAILS_OFFLINE_MODE'] = '1'
253     when /cachewebsite(?:=([a-z]+))?/
254       value = Regexp.last_match(1)
255       if releasing?
256         warn "Building a release ⇒ ignoring #{opt} build option"
257         ENV['TAILS_WEBSITE_CACHE'] = '0'
258       else
259         value = 'yes' if value.nil?
260         case value
261         when 'yes'
262           ENV['TAILS_WEBSITE_CACHE'] = '1'
263         when 'no'
264           ENV['TAILS_WEBSITE_CACHE'] = '0'
265         else
266           raise "Unsupported value for cachewebsite option: #{value}"
267         end
268       end
269     # SquashFS compression settings
270     when 'fastcomp', 'gzipcomp'
271       if releasing?
272         warn "Building a release ⇒ ignoring #{opt} build option"
273         ENV['MKSQUASHFS_OPTIONS'] = nil
274       else
275         ENV['MKSQUASHFS_OPTIONS'] = '-comp zstd -no-exports'
276       end
277     when 'defaultcomp'
278       ENV['MKSQUASHFS_OPTIONS'] = nil
279     # Virtual hardware settings
280     when /machinetype=([a-zA-Z0-9_.-]+)/
281       ENV['TAILS_BUILD_MACHINE_TYPE'] = Regexp.last_match(1)
282     when /cpus=(\d+)/
283       ENV['TAILS_BUILD_CPUS'] = Regexp.last_match(1)
284     when /cpumodel=([a-zA-Z0-9_-]+)/
285       ENV['TAILS_BUILD_CPU_MODEL'] = Regexp.last_match(1)
286     # Git settings
287     when 'ignorechanges'
288       ENV['TAILS_BUILD_IGNORE_CHANGES'] = '1'
289     when /dateoffset=([-+]\d+)/
290       ENV['TAILS_DATE_OFFSET'] = Regexp.last_match(1)
291     # Developer convenience features
292     when 'keeprunning'
293       $keep_running = true
294       $force_cleanup = false
295     when 'forcecleanup'
296       $force_cleanup = true
297       $keep_running = false
298     when 'rescue'
299       $keep_running = true
300       ENV['TAILS_BUILD_FAILURE_RESCUE'] = '1'
301     # Jenkins
302     when 'nomergebasebranch'
303       $skip_mergebasebranch = true
304     else
305       raise "Unknown Tails build option '#{opt}'"
306     end
307   end
309   if ENV['TAILS_OFFLINE_MODE'] == '1' && ENV['TAILS_PROXY'].nil?
310     abort 'You must use a caching proxy when building offline'
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_RAM_BUILD'] && !enough_free_memory_for_ram_build?
495     warn <<-END_OF_MESSAGE.gsub(/^        /, '')
497         The virtual machine is not currently set with enough memory to
498         perform an in-memory build. Either remove the `ram` option from
499         the TAILS_BUILD_OPTIONS environment variable, or shut the
500         virtual machine down using `rake vm:halt` before trying again.
502     END_OF_MESSAGE
503     abort 'Not enough memory for the virtual machine to run an in-memory ' \
504           'build. Aborting.'
505   end
507   if ENV['TAILS_BUILD_CPUS'] \
508      && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
509     warn <<-END_OF_MESSAGE.gsub(/^        /, '')
511         The virtual machine is currently running with #{current_vm_cpus}
512         virtual CPU(s). In order to change that number, you need to
513         stop the VM first, using `rake vm:halt`. Otherwise, please
514         adjust the `cpus` options accordingly.
516     END_OF_MESSAGE
517     abort 'The virtual machine needs to be reloaded to change the number ' \
518           'of CPUs. Aborting.'
519   end
521   exported_env = EXPORTED_VARIABLES
522                  .select { |k| ENV[k] }
523                  .map    { |k| "#{k}='#{ENV[k]}'" }.join(' ')
525   begin
526     retrieved_artifacts = false
527     run_vagrant_ssh("#{exported_env} build-tails")
528   rescue VagrantCommandError
529     retrieve_artifacts(missing_ok: true)
530     retrieved_artifacts = true
531   ensure
532     retrieve_artifacts(missing_ok: false) unless retrieved_artifacts
533     clean_up_builder_vms unless $keep_running
534   end
535 ensure
536   clean_up_builder_vms if $force_cleanup
539 desc 'Retrieve build artifacts from the Vagrant box'
540 task :retrieve_artifacts do
541   retrieve_artifacts
544 def retrieve_artifacts(missing_ok: false)
545   artifacts = list_artifacts
546   if artifacts.empty?
547     msg = 'No build artifacts were found!'
548     raise msg unless missing_ok
550     warn msg
551     return
552   end
553   user = vagrant_ssh_config('User')
554   hostname = vagrant_ssh_config('HostName')
555   key_file = vagrant_ssh_config('IdentityFile')
556   warn 'Retrieving artifacts from Vagrant build box.'
557   run_vagrant_ssh(
558     "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" }.join(' ')
559   )
560   fetch_command = [
561     'scp',
562     '-i', key_file,
563     # We don't want to use any identity saved in ssh agent'
564     '-o', 'IdentityAgent=none',
565     # We need this since the user will not necessarily have a
566     # known_hosts entry. It is safe since an attacker must
567     # compromise libvirt's network config or the user running the
568     # command to modify the #{hostname} below.
569     '-o', 'StrictHostKeyChecking=no',
570     '-o', 'UserKnownHostsFile=/dev/null',
571     # Speed up the copy
572     '-o', 'Compression=no',
573   ]
574   fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
575   fetch_command << ENV['ARTIFACTS']
576   run_command(*fetch_command)
579 def box?
580   !capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?
583 def domain_name
584   "#{box_name}_default"
587 def clean_up_builder_vms
588   libvirt = Libvirt.open('qemu:///system')
590   clean_up_domain = proc do |domain|
591     next if domain.nil?
593     domain.destroy if domain.active?
594     domain.undefine
595     begin
596       libvirt
597         .lookup_storage_pool_by_name('default')
598         .lookup_volume_by_name("#{domain.name}.img")
599         .delete
600     rescue Libvirt::RetrieveError
601       # Expected if the pool or disk does not exist
602     end
603   end
605   # Let's ensure that the VM we are about to create is cleaned up ...
606   previous_domain = libvirt.list_all_domains.find { |d| d.name == domain_name }
607   if previous_domain&.active?
608     begin
609       run_vagrant_ssh('mountpoint -q /var/cache/apt-cacher-ng')
610     rescue VagrantCommandError
611     # Nothing to unmount.
612     else
613       run_vagrant_ssh('sudo systemctl stop apt-cacher-ng.service')
614       run_vagrant_ssh('sudo umount /var/cache/apt-cacher-ng')
615       run_vagrant_ssh('sudo sync')
616     end
617     begin
618       run_vagrant_ssh('mountpoint -q /var/cache/tails-website')
619     rescue VagrantCommandError
620     # Nothing to unmount.
621     else
622       run_vagrant_ssh('sudo umount /var/cache/tails-website')
623       run_vagrant_ssh('sudo sync')
624     end
625   end
626   clean_up_domain.call(previous_domain)
628   # ... and the same for any residual VM based on another box (=>
629   # another domain name) that Vagrant still keeps track of.
630   old_domain =
631     begin
632       old_domain_uuid =
633         open('vagrant/.vagrant/machines/default/libvirt/id', 'r', &:read)
634         .strip
635       libvirt.lookup_domain_by_uuid(old_domain_uuid)
636     rescue Errno::ENOENT, Libvirt::RetrieveError
637       # Expected if we don't have vagrant/.vagrant, or if the VM was
638       # undefined for other reasons (e.g. manually).
639       nil
640     end
641   clean_up_domain.call(old_domain)
643   # We could use `vagrant destroy` here but due to vagrant-libvirt's
644   # upstream issue #746 we then risk losing the apt-cacher-ng data.
645   # Since we essentially implement `vagrant destroy` without this bug
646   # above, but in a way so it works even if `vagrant/.vagrant` does
647   # not exist, let's just do what is safest, i.e. avoiding `vagrant
648   # destroy`. For details, see the upstream issue:
649   #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
650   FileUtils.rm_rf('vagrant/.vagrant')
651 ensure
652   libvirt.close
655 desc 'Remove all libvirt volumes named tails-builder-* (run at your own risk!)'
656 task :clean_up_libvirt_volumes do
657   libvirt = Libvirt.open('qemu:///system')
658   begin
659     pool = libvirt.lookup_storage_pool_by_name('default')
660   rescue Libvirt::RetrieveError
661     # Expected if the pool does not exist
662   else
663     pool.list_volumes.each do |disk|
664       next unless /^tails-builder-/.match(disk)
666       begin
667         pool.lookup_volume_by_name(disk).delete
668       rescue Libvirt::RetrieveError
669         # Expected if the disk does not exist
670       end
671     end
672   ensure
673     libvirt.close
674   end
677 def on_jenkins?
678   !ENV['JENKINS_URL'].nil?
681 desc 'Clean up all build related files'
682 task clean_all: ['vm:destroy', 'basebox:clean_all']
684 namespace :vm do
685   desc 'Start the build virtual machine'
686   task up: [
687     'parse_build_options',
688     'validate_http_proxy',
689     'setup_environment',
690     'basebox:create',
691   ] do
692     case vm_state
693     when :not_created
694       clean_up_builder_vms
695     end
696     begin
697       run_vagrant('up', '--provision')
698     rescue VagrantCommandError => e
699       clean_up_builder_vms if $force_cleanup
700       raise e
701     end
702   end
704   desc 'SSH into the builder VM'
705   task :ssh do
706     run_vagrant('ssh')
707   end
709   desc 'Stop the build virtual machine'
710   task :halt do
711     run_vagrant('halt')
712   end
714   desc 'Re-run virtual machine setup'
715   task provision: [
716     'parse_build_options',
717     'validate_http_proxy',
718     'setup_environment',
719   ] do
720     run_vagrant('provision')
721   end
723   desc 'Destroy build virtual machine (clean up all files except the ' \
724        "vmproxy's apt-cacher-ng data and the website cache)"
725   task :destroy do
726     clean_up_builder_vms
727   end
730 namespace :basebox do
731   desc 'Create and import the base box unless already done'
732   task :create do
733     next if box?
735     warn <<-END_OF_MESSAGE.gsub(/^      /, '')
737       This is the first time we are using this Vagrant base box so we
738       will have to bootstrap by building it from scratch. This will
739       take around 20 minutes (depending on your hardware) plus the
740       time needed for downloading around 250 MiB of Debian packages.
742     END_OF_MESSAGE
743     run_command(
744       "#{VAGRANT_PATH}/definitions/tails-builder/generate-tails-builder-box.sh"
745     )
746     box_dir = Dir.pwd
747     # Let's use an absolute path since run_vagrant changes the working
748     # directory but File.delete doesn't
749     box_path = "#{box_dir}/#{box_name}.box"
750     run_vagrant('box', 'add', '--name', box_name, box_path)
751     File.delete(box_path)
752   end
754   def basebox_date(box)
755     Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
756   end
758   def baseboxes
759     capture_vagrant('box', 'list')
760       .first.lines
761       .grep(/^tails-builder-.*/)
762       .map { |x| x.chomp.sub(/\s.*$/, '') }
763   end
765   def clean_up_basebox(box)
766     run_vagrant('box', 'remove', '--force', box)
767     begin
768       libvirt = Libvirt.open('qemu:///system')
769       libvirt
770         .lookup_storage_pool_by_name('default')
771         .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
772         .delete
773     rescue Libvirt::RetrieveError
774       # Expected if the pool or disk does not exist
775     ensure
776       libvirt.close
777     end
778   end
780   desc 'Remove all base boxes'
781   task :clean_all do
782     baseboxes.each { |box| clean_up_basebox(box) }
783   end
785   desc 'Remove all base boxes older than six months'
786   task :clean_old do
787     boxes = baseboxes
788     # We always want to keep the newest basebox
789     boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
790     boxes.pop
791     boxes.each do |box|
792       clean_up_basebox(box) if basebox_date(box) < Date.today - 365.0 / 2.0
793     end
794   end