Rakefile 19.1 KB
Newer Older
1
# -*- coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# Tails: The Amnesic Incognito Live System
# Copyright © 2012 Tails developers <tails@boum.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

21
require 'date'
22
require 'libvirt'
23
require 'open3'
24
require 'rbconfig'
25
require 'uri'
26

27
require_relative 'vagrant/lib/tails_build_settings'
28

29 30 31
# Path to the directory which holds our Vagrantfile
VAGRANT_PATH = File.expand_path('../vagrant', __FILE__)

32 33 34
# Branches that are considered 'stable' (used to select SquashFS compression)
STABLE_BRANCH_NAMES = ['stable', 'testing']

35 36
EXPORTED_VARIABLES = [
  'MKSQUASHFS_OPTIONS',
37
  'TAILS_BUILD_FAILURE_RESCUE',
38
  'TAILS_DATE_OFFSET',
39
  'TAILS_MERGE_BASE_BRANCH',
40
  'TAILS_OFFLINE_MODE',
41 42 43
  'TAILS_PROXY',
  'TAILS_PROXY_TYPE',
  'TAILS_RAM_BUILD',
44 45 46
  'GIT_COMMIT',
  'GIT_REF',
  'BASE_BRANCH_GIT_COMMIT',
47
]
48
ENV['EXPORTED_VARIABLES'] = EXPORTED_VARIABLES.join(' ')
49

50 51
EXTERNAL_HTTP_PROXY = ENV['http_proxy']

52
# In-VM proxy URL
anonym's avatar
anonym committed
53
INTERNAL_HTTP_PROXY = "http://#{VIRTUAL_MACHINE_HOSTNAME}:3142"
54

55 56
ENV['ARTIFACTS'] ||= '.'

57
class CommandError < StandardError
anonym's avatar
anonym committed
58 59 60 61 62 63 64 65
  attr_reader :status, :stderr

  def initialize(message = nil, opts = {})
    opts[:status] ||= nil
    opts[:stderr] ||= nil
    @status = opts[:status]
    @stderr = opts[:stderr]
    super(message % {status: @status, stderr: @stderr})
66 67 68 69 70 71
  end
end

def run_command(*args)
  Process.wait Kernel.spawn(*args)
  if $?.exitstatus != 0
72
    raise CommandError.new("command #{args} failed with exit status " +
anonym's avatar
anonym committed
73
                           "%{status}", status: $?.exitstatus)
74 75 76 77
  end
end

def capture_command(*args)
anonym's avatar
anonym committed
78
  stdout, stderr, proc_status = Open3.capture3(*args)
79 80
  if proc_status.exitstatus != 0
    raise CommandError.new("command #{args} failed with exit status " +
anonym's avatar
anonym committed
81 82
                           "%{status}: %{stderr}",
                           stderr: stderr, status: proc_status.exitstatus)
83 84
  end
  return stdout, stderr
85 86
end

87
def git_helper(*args)
88 89
  question = args.first.end_with?('?')
  args.first.sub!(/\?$/, '')
anonym's avatar
anonym committed
90 91 92 93 94 95 96
  status = 0
  stdout = ''
  begin
    stdout, _ = capture_command('auto/scripts/utils.sh', *args)
  rescue CommandError => e
    status = e.status
  end
97
  if question
anonym's avatar
anonym committed
98
    return status == 0
99
  else
100
    return stdout.chomp
101 102 103
  end
end

104
class VagrantCommandError < CommandError
105 106
end

anonym's avatar
anonym committed
107 108
# Runs the vagrant command, letting stdout/stderr through. Throws an
# exception unless the vagrant command succeeds.
109
def run_vagrant(*args)
anonym's avatar
anonym committed
110 111
  run_command('vagrant', *args, :chdir => './vagrant')
rescue CommandError => e
112
  raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " +
anonym's avatar
anonym committed
113
                             "status #{e.status}")
114 115
end

116
# Runs the vagrant command, not letting stdout/stderr through, and
anonym's avatar
anonym committed
117
# returns [stdout, stderr, Preocess:Status].
118
def capture_vagrant(*args)
anonym's avatar
anonym committed
119
  capture_command('vagrant', *args, :chdir => './vagrant')
120 121
rescue CommandError => e
  raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " +
anonym's avatar
anonym committed
122
                             "status #{e.status}: #{e.stderr}")
123 124
end

125 126
[:run_vagrant, :capture_vagrant].each do |m|
  define_method "#{m}_ssh" do |*args|
anonym's avatar
anonym committed
127
    method(m).call('ssh', '-c', *args, '--', '-q')
128
  end
129 130
end

anonym's avatar
anonym committed
131
def vagrant_ssh_config(key)
anonym's avatar
anonym committed
132
  # Cache results
anonym's avatar
anonym committed
133 134 135 136 137 138 139 140 141 142
  if $vagrant_ssh_config.nil?
    $vagrant_ssh_config = capture_vagrant('ssh-config').first.split("\n") \
                           .map { |line| line.strip.split(/\s+/, 2) } .to_h
    # The path in the ssh-config output is quoted, which is not what
    # is expected outside of a shell, so let's get rid of the quotes.
    $vagrant_ssh_config['IdentityFile'].gsub!(/^"|"$/, '')
  end
  $vagrant_ssh_config[key]
end

143
def current_vm_cpus
144
  capture_vagrant_ssh('grep -c "^processor\s*:" /proc/cpuinfo').first.chomp.to_i
145 146
end

147
def vm_state
148
  out, _ = capture_vagrant('status')
149 150 151 152 153 154 155
  status_line = out.split("\n")[2]
  if    status_line['not created']
    return :not_created
  elsif status_line['shutoff']
    return :poweroff
  elsif status_line['running']
    return :running
156
  else
157
    raise "could not determine VM state"
158 159 160
  end
end

161
def enough_free_host_memory_for_ram_build?
162 163 164
  return false unless RbConfig::CONFIG['host_os'] =~ /linux/i

  begin
165
    usable_free_mem = `free`.split[12].to_i
166 167 168 169 170 171
    usable_free_mem > VM_MEMORY_FOR_RAM_BUILDS * 1024
  rescue
    false
  end
end

172
def free_vm_memory
173
  capture_vagrant_ssh('free').first.chomp.split[12].to_i
174 175 176 177 178 179 180
end

def enough_free_vm_memory_for_ram_build?
  free_vm_memory > BUILD_SPACE_REQUIREMENT * 1024
end

def enough_free_memory_for_ram_build?
181
  if vm_state == :running
182 183 184 185 186 187
    enough_free_vm_memory_for_ram_build?
  else
    enough_free_host_memory_for_ram_build?
  end
end

188
def is_release?
189
  git_helper('git_on_a_tag?')
190 191
end

192 193 194 195 196 197 198 199 200 201
def system_cpus
  return nil unless RbConfig::CONFIG['host_os'] =~ /linux/i

  begin
    File.read('/proc/cpuinfo').scan(/^processor\s+:/).count
  rescue
    nil
  end
end

202
task :parse_build_options do
203
  options = []
204

205
  # Default to in-memory builds if there is enough RAM available
206
  options << 'ram' if enough_free_memory_for_ram_build?
207

208
  # Default to build using the in-VM proxy
209
  options << 'vmproxy'
210

211
  # Default to fast compression on development branches
212
  options << 'gzipcomp' unless is_release?
213

214 215
  # Default to the number of system CPUs when we can figure it out
  cpus = system_cpus
216
  options << "cpus=#{cpus}" if cpus
217

218
  options += ENV['TAILS_BUILD_OPTIONS'].split if ENV['TAILS_BUILD_OPTIONS']
219

220
  options.uniq.each do |opt|
221
    case opt
222 223 224 225 226
    # Memory build settings
    when 'ram'
      ENV['TAILS_RAM_BUILD'] = '1'
    when 'noram'
      ENV['TAILS_RAM_BUILD'] = nil
227
    # Bootstrap cache settings
228 229 230
    # HTTP proxy settings
    when 'extproxy'
      abort "No HTTP proxy set, but one is required by TAILS_BUILD_OPTIONS. Aborting." unless EXTERNAL_HTTP_PROXY
231
      ENV['TAILS_PROXY'] = EXTERNAL_HTTP_PROXY
232
      ENV['TAILS_PROXY_TYPE'] = 'extproxy'
233
    when 'vmproxy'
234
      ENV['TAILS_PROXY'] = INTERNAL_HTTP_PROXY
235
      ENV['TAILS_PROXY_TYPE'] = 'vmproxy'
236
    when 'noproxy'
237
      ENV['TAILS_PROXY'] = nil
238
      ENV['TAILS_PROXY_TYPE'] = 'noproxy'
239 240
    when 'offline'
      ENV['TAILS_OFFLINE_MODE'] = '1'
241 242
    # SquashFS compression settings
    when 'gzipcomp'
243
      ENV['MKSQUASHFS_OPTIONS'] = '-comp gzip -Xcompression-level 1'
244 245 246
      if is_release?
        raise 'We must use the default compression when building releases!'
      end
247 248
    when 'defaultcomp'
      ENV['MKSQUASHFS_OPTIONS'] = nil
249 250 251
    # Virtual hardware settings
    when /machinetype=([a-zA-Z0-9_.-]+)/
      ENV['TAILS_BUILD_MACHINE_TYPE'] = $1
252 253
    when /cpus=(\d+)/
      ENV['TAILS_BUILD_CPUS'] = $1
254 255
    when /cpumodel=([a-zA-Z0-9_-]+)/
      ENV['TAILS_BUILD_CPU_MODEL'] = $1
256 257 258
    # Git settings
    when 'ignorechanges'
      ENV['TAILS_BUILD_IGNORE_CHANGES'] = '1'
259 260
    when /dateoffset=([-+]\d+)/
      ENV['TAILS_DATE_OFFSET'] = $1
261 262 263
    # Developer convenience features
    when 'keeprunning'
      $keep_running = true
264 265 266 267
      $force_cleanup = false
    when 'forcecleanup'
      $force_cleanup = true
      $keep_running = false
268 269 270
    when 'rescue'
      $keep_running = true
      ENV['TAILS_BUILD_FAILURE_RESCUE'] = '1'
271 272
    # Jenkins
    when 'mergebasebranch'
273
      ENV['TAILS_MERGE_BASE_BRANCH'] = '1'
274 275
    else
      raise "Unknown Tails build option '#{opt}'"
276 277
    end
  end
278 279

  if ENV['TAILS_OFFLINE_MODE'] == '1'
280
    if ENV['TAILS_PROXY'].nil?
281 282 283
      abort "You must use a caching proxy when building offline"
    end
  end
284 285 286
end

task :ensure_clean_repository do
287 288
  git_status = `git status --porcelain`
  unless git_status.empty?
289 290 291
    if ENV['TAILS_BUILD_IGNORE_CHANGES']
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')

Austin English's avatar
Austin English committed
292
        You have uncommitted changes in the Git repository. They will
293 294
        be ignored for the upcoming build:
        #{git_status}
295 296 297 298 299

      END_OF_MESSAGE
    else
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')

Austin English's avatar
Austin English committed
300
        You have uncommitted changes in the Git repository. Due to limitations
301 302
        of the build system, you need to commit them before building Tails:
        #{git_status}
303 304 305 306 307 308

        If you don't care about those changes and want to build Tails nonetheless,
        please add `ignorechanges` to the TAILS_BUILD_OPTIONS environment
        variable.

      END_OF_MESSAGE
Austin English's avatar
Austin English committed
309
      abort 'Uncommitted changes. Aborting.'
310 311 312 313
    end
  end
end

anonym's avatar
anonym committed
314 315
def list_artifacts
  user = vagrant_ssh_config('User')
316
  stdout = capture_vagrant_ssh("find '/home/#{user}/amnesia/' -maxdepth 1 " +
anonym's avatar
anonym committed
317 318
                                        "-name 'tails-*.iso*'").first
  stdout.split("\n")
319 320
rescue VagrantCommandError
  return Array.new
anonym's avatar
anonym committed
321 322 323
end

def remove_artifacts
324
  list_artifacts.each do |artifact|
325
    run_vagrant_ssh("sudo rm -f '#{artifact}'")
326
  end
anonym's avatar
anonym committed
327 328
end

329
task :ensure_clean_home_directory => ['vm:up'] do
anonym's avatar
anonym committed
330
  remove_artifacts
331 332
end

333
task :validate_http_proxy do
334 335
  if ENV['TAILS_PROXY']
    proxy_host = URI.parse(ENV['TAILS_PROXY']).host
336

337
    if proxy_host.nil?
338
      ENV['TAILS_PROXY'] = nil
339 340 341 342
      $stderr.puts "Ignoring invalid HTTP proxy."
      return
    end

343 344 345 346
    if ['localhost', '[::1]'].include?(proxy_host) || proxy_host.start_with?('127.0.0.')
      abort 'Using an HTTP proxy listening on the loopback is doomed to fail. Aborting.'
    end

347
    $stderr.puts "Using HTTP proxy: #{ENV['TAILS_PROXY']}"
348 349 350 351 352
  else
    $stderr.puts "No HTTP proxy set."
  end
end

353
task :validate_git_state do
354
  if git_helper('git_in_detached_head?') && not(git_helper('git_on_a_tag?'))
355 356 357 358 359
    raise 'We are in detached head but the current commit is not tagged'
  end
end

task :setup_environment => ['validate_git_state'] do
360
  ENV['GIT_COMMIT'] ||= git_helper('git_current_commit')
361 362 363 364 365 366 367
  ENV['GIT_REF'] ||= git_helper('git_current_head_name')
  if on_jenkins?
    jenkins_branch = (ENV['GIT_BRANCH'] || '').sub(/^origin\//, '')
    if not(is_release?) && jenkins_branch != ENV['GIT_REF']
      raise "We expected to build the Git ref '#{ENV['GIT_REF']}', but GIT_REF in the environment says '#{jenkins_branch}'. Aborting!"
    end
  end
368

anonym's avatar
anonym committed
369
  ENV['BASE_BRANCH_GIT_COMMIT'] = git_helper('git_base_branch_head')
370
  ['GIT_COMMIT', 'GIT_REF', 'BASE_BRANCH_GIT_COMMIT'].each do |var|
371 372 373 374 375 376 377
    if ENV[var].empty?
      raise "Variable '#{var}' is empty, which should not be possible" +
            "(validate_git_state must be buggy)"
    end
  end
end

378 379
task :maybe_clean_up_builder_vms do
  clean_up_builder_vms if $force_cleanup
380 381
end

382
desc 'Build Tails'
383
task :build => ['parse_build_options', 'ensure_clean_repository', 'maybe_clean_up_builder_vms', 'validate_git_state', 'setup_environment', 'validate_http_proxy', 'vm:up', 'ensure_clean_home_directory'] do
384

anonym's avatar
anonym committed
385 386 387
  begin
    if ENV['TAILS_RAM_BUILD'] && not(enough_free_memory_for_ram_build?)
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')
388

anonym's avatar
anonym committed
389 390 391 392
        The virtual machine is not currently set with enough memory to
        perform an in-memory build. Either remove the `ram` option from
        the TAILS_BUILD_OPTIONS environment variable, or shut the
        virtual machine down using `rake vm:halt` before trying again.
393

anonym's avatar
anonym committed
394 395 396
      END_OF_MESSAGE
      abort 'Not enough memory for the virtual machine to run an in-memory build. Aborting.'
    end
397

anonym's avatar
anonym committed
398 399
    if ENV['TAILS_BUILD_CPUS'] && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')
400

anonym's avatar
anonym committed
401 402 403 404
        The virtual machine is currently running with #{current_vm_cpus}
        virtual CPU(s). In order to change that number, you need to
        stop the VM first, using `rake vm:halt`. Otherwise, please
        adjust the `cpus` options accordingly.
405

anonym's avatar
anonym committed
406 407 408
      END_OF_MESSAGE
      abort 'The virtual machine needs to be reloaded to change the number of CPUs. Aborting.'
    end
409

anonym's avatar
anonym committed
410 411
    exported_env = EXPORTED_VARIABLES.select { |k| ENV[k] }.
                   collect { |k| "#{k}='#{ENV[k]}'" }.join(' ')
412
    run_vagrant_ssh("#{exported_env} build-tails")
anonym's avatar
anonym committed
413 414

    artifacts = list_artifacts
anonym's avatar
anonym committed
415
    raise 'No build artifacts were found!' if artifacts.empty?
anonym's avatar
anonym committed
416 417 418 419
    user     = vagrant_ssh_config('User')
    hostname = vagrant_ssh_config('HostName')
    key_file = vagrant_ssh_config('IdentityFile')
    $stderr.puts "Retrieving artifacts from Vagrant build box."
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
    run_vagrant_ssh(
      "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" } .join(' ')
    )
    fetch_command = [
      'scp',
      '-i', key_file,
      # We need this since the user will not necessarily have a
      # known_hosts entry. It is safe since an attacker must
      # compromise libvirt's network config or the user running the
      # command to modify the #{hostname} below.
      '-o', 'StrictHostKeyChecking=no',
      '-o', 'UserKnownHostsFile=/dev/null',
    ]
    fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
    fetch_command << ENV['ARTIFACTS']
    run_command(*fetch_command)
436
    clean_up_builder_vms unless $keep_running
anonym's avatar
anonym committed
437
  ensure
438
    clean_up_builder_vms if $force_cleanup
439
  end
440 441
end

anonym's avatar
anonym committed
442 443
def has_box?
  not(capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?)
444
end
445

anonym's avatar
anonym committed
446 447
def domain_name
  "#{box_name}_default"
448
end
449

450
def clean_up_builder_vms
451
  $virt = Libvirt::open("qemu:///system")
452 453 454

  clean_up_domain = Proc.new do |domain|
    next if domain.nil?
455
    domain.destroy if domain.active?
456
    domain.undefine
457 458 459 460 461 462 463 464
    begin
      $virt
        .lookup_storage_pool_by_name('default')
        .lookup_volume_by_name("#{domain.name}.img")
        .delete
    rescue Libvirt::RetrieveError
      # Expected if the pool or disk does not exist
    end
465
  end
466

467 468
  # Let's ensure that the VM we are about to create is cleaned up ...
  previous_domain = $virt.list_all_domains.find { |d| d.name == domain_name }
469
  if previous_domain && previous_domain.active?
470 471 472 473 474 475 476 477 478 479
    begin
      run_vagrant_ssh("mountpoint -q /var/cache/apt-cacher-ng")
    rescue VagrantCommandError
    # Nothing to unmount.
    else
      run_vagrant_ssh("sudo systemctl stop apt-cacher-ng.service")
      run_vagrant_ssh("sudo umount /var/cache/apt-cacher-ng")
      run_vagrant_ssh("sudo sync")
    end
  end
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
  clean_up_domain.call(previous_domain)

  # ... and the same for any residual VM based on another box (=>
  # another domain name) that Vagrant still keeps track of.
  old_domain =
    begin
      old_domain_uuid =
        open('vagrant/.vagrant/machines/default/libvirt/id', 'r') { |f| f.read }
        .strip
      $virt.lookup_domain_by_uuid(old_domain_uuid)
    rescue Errno::ENOENT, Libvirt::RetrieveError
      # Expected if we don't have vagrant/.vagrant, or if the VM was
      # undefined for other reasons (e.g. manually).
      nil
    end
  clean_up_domain.call(old_domain)

  # We could use `vagrant destroy` here but due to vagrant-libvirt's
  # upstream issue #746 we then risk losing the apt-cacher-ng data.
  # Since we essentially implement `vagrant destroy` without this bug
intrigeri's avatar
intrigeri committed
500
  # above, but in a way so it works even if `vagrant/.vagrant` does
anonym's avatar
anonym committed
501
  # not exist, let's just do what is safest, i.e. avoiding `vagrant
502 503 504
  # destroy`. For details, see the upstream issue:
  #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
  FileUtils.rm_rf('vagrant/.vagrant')
505 506
ensure
  $virt.close
507
end
508

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
desc "Remove all libvirt volumes named tails-builder-* (run at your own risk!)"
task :clean_up_libvirt_volumes do
  $virt = Libvirt::open("qemu:///system")
  begin
    pool = $virt.lookup_storage_pool_by_name('default')
  rescue Libvirt::RetrieveError
    # Expected if the pool does not exist
  else
    for disk in pool.list_volumes do
      if /^tails-builder-/.match(disk)
        begin
          pool.lookup_volume_by_name(disk).delete
        rescue Libvirt::RetrieveError
          # Expected if the disk does not exist
        end
      end
    end
  ensure
    $virt.close
  end
end

531 532 533
def on_jenkins?
  !!ENV['JENKINS_URL']
end
534

535 536 537 538 539 540 541 542 543
desc 'Test Tails'
task :test do
  args = ARGV.drop_while { |x| x == 'test' || x == '--' }
  if on_jenkins?
    args += ['--'] unless args.include? '--'
    if not(is_release?)
      args += ['--tag', '~@fragile']
    end
    base_branch = git_helper('base_branch')
544
    if git_helper('git_only_doc_changes_since?', "origin/#{base_branch}") then
545 546 547 548 549
      args += ['--tag', '@doc']
    end
  end
  run_command('./run_test_suite', *args)
end
550

anonym's avatar
anonym committed
551 552
desc 'Clean up all build related files'
task :clean_all => ['vm:destroy', 'basebox:clean_all']
553 554 555

namespace :vm do
  desc 'Start the build virtual machine'
556
  task :up => ['parse_build_options', 'validate_http_proxy', 'setup_environment', 'basebox:create'] do
557
    case vm_state
558
    when :not_created
559
      clean_up_builder_vms
560
    end
561 562 563 564 565
    begin
      run_vagrant('up')
    rescue VagrantCommandError => e
      clean_up_builder_vms if $force_cleanup
      raise e
566 567 568
    end
  end

569 570 571 572 573
  desc 'SSH into the builder VM'
  task :ssh do
    run_vagrant('ssh')
  end

574 575
  desc 'Stop the build virtual machine'
  task :halt do
576
    run_vagrant('halt')
577 578 579
  end

  desc 'Re-run virtual machine setup'
580
  task :provision => ['parse_build_options', 'validate_http_proxy', 'setup_environment'] do
581
    run_vagrant('provision')
582 583
  end

584
  desc "Destroy build virtual machine (clean up all files except the vmproxy's apt-cacher-ng data)"
585
  task :destroy do
586
    clean_up_builder_vms
587 588
  end
end
589 590 591

namespace :basebox do

592
  desc 'Create and import the base box unless already done'
593
  task :create do
594
    next if has_box?
595 596
    $stderr.puts <<-END_OF_MESSAGE.gsub(/^      /, '')

597 598 599 600
      This is the first time we are using this Vagrant base box so we
      will have to bootstrap by building it from scratch. This will
      take around 20 minutes (depending on your hardware) plus the
      time needed for downloading around 250 MiB of Debian packages.
601 602

    END_OF_MESSAGE
603
    box_dir = VAGRANT_PATH + '/definitions/tails-builder'
604
    run_command("#{box_dir}/generate-tails-builder-box.sh")
605 606
    # Let's use an absolute path since run_vagrant changes the working
    # directory but File.delete doesn't
607
    box_path = "#{box_dir}/#{box_name}.box"
608 609 610
    run_vagrant('box', 'add', '--name', box_name, box_path)
    File.delete(box_path)
    end
611

612 613 614
  def basebox_date(box)
    Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
  end
615

616 617 618 619 620
  def baseboxes
    capture_vagrant('box', 'list').first.lines
      .grep(/^tails-builder-.*/)
      .map { |x| x.chomp.sub(/\s.*$/, '') }
  end
621

622 623 624 625 626 627 628 629 630 631 632 633 634 635
  def clean_up_basebox(box)
    run_vagrant('box', 'remove', '--force', box)
    begin
      $virt = Libvirt::open("qemu:///system")
      $virt
        .lookup_storage_pool_by_name('default')
        .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
        .delete
    rescue Libvirt::RetrieveError
      # Expected if the pool or disk does not exist
    ensure
      $virt.close
    end
  end
636

637 638
  desc 'Remove all base boxes'
  task :clean_all do
639
    baseboxes.each { |box| clean_up_basebox(box) }
640 641
  end

642 643
  desc 'Remove all base boxes older than six months'
  task :clean_old do
644
    boxes = baseboxes
645
    # We always want to keep the newest basebox
646
    boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
647 648
    boxes.pop
    boxes.each do |box|
649
      if basebox_date(box) < Date.today - 365.0/3.0
650
        clean_up_basebox(box)
651 652 653
      end
    end
  end
654
end