reachlocal/boxci

View on GitHub
lib/boxci/tester.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require "yaml"
require "net/ssh"
require "net/scp"

module Boxci
  class Tester
    include Thor::Base
    include Thor::Actions

    source_root(File.dirname(__FILE__))

    def test(options)
      File.open('/tmp/boxci.log', 'w') do |f|
        f.write('')
      end

      # NOTE: The Signal.trap('SIGTERM') is required for Bamboo's "Stop
      # Build" functionality because Bamboo basically sends a SIGTERM to
      # Boxci and then immediately closes its stdout and stderr pipes
      # before Boxci has had a chance to cleanup. Therefore, it causes
      # Errno::EPIPE exceptions to be raised. It does seem that in general
      # the use of SIGTERM is correct however one would hope that stdout and
      # stderr pipes would stay open until Boxci exits, but sadly they
      # do not.
      Signal.trap('SIGTERM') do
        File.open('/tmp/boxci.log', 'a+') { |f| f.write("Got SIGTERM, going to cleanup...\n") }

        begin
          cleanup
        rescue Errno::EPIPE => e
          File.open('/tmp/boxci.log', 'a+') { |f| f.write("SIGTERM handler swallowed Errno::EPIPE exception\n") }
        rescue => e
          File.open('/tmp/boxci.log', 'a+') do |f|
            f.write("SIGTERM handler caught exception")
            f.write("#{e.class}\n")
            f.write("#{e.message}\n")
            f.write("#{e.backtrace.join("\n")}\n")
          end
          raise e
        end

        File.open('/tmp/boxci.log', 'a+') { |f| f.write("Finished cleanup process from SIGTERM\n") }
        exit 255
      end

      Signal.trap('SIGINT') do
        cleanup
        exit 255
      end

      # TODO: I don't believe this is necessary as I think Ruby's default
      # handler for SIGPIPE is to ignore it. I need to test this though to
      # verify.
      Signal.trap('SIGPIPE', 'SIG_IGN')

      begin
        @tester_exit_code = 0
        # depencency_checker = Boxci::DependencyChecker.new
        # depencency_checker.verify_all
        initial_config(options)

        create_project_folder
        create_project_archive
        write_vagrant_file
        write_test_runner
        if @provider_object.requires_plugin?
          install_vagrant_plugin
          add_provider_box
        end
        spin_up_box
        setup_ssh_config
        install_puppet_on_box
        provision_box
        create_artifact_directory
        upload_test_runner
        run_tests
        download_artifacts
        say "Finished!", :green
      rescue Errno::EPIPE => e
        File.open('/tmp/boxci.log', 'a+') do |f|
          f.write("test() method swallowed Errno::EPIPE exception\n")
        end
      ensure
        cleanup
      end

      exit @tester_exit_code
    end

    def initial_config(options)
      @gem_path = File.expand_path(File.dirname(__FILE__) + "/../..")
      @puppet_path = File.join(Boxci.project_path, "puppet")
      @project_uid = "#{rand(1000..9000)}-#{rand(1000..9000)}-#{rand(1000..9000)}-#{rand(1000..9000)}"
      @project_workspace_folder = File.join(File.expand_path(ENV['HOME']), '.boxci', @project_uid)
      @options = options
      @provider_config = Boxci.provider_config(provider)
      @project_config = Boxci.project_config
      @provider_object = Boxci::ProviderFactory.build(provider)
    end

    def provider
      @options["provider"]
    end

    def verbose?
      @options["verbose"] == true
    end

    def create_project_folder
      empty_directory @project_workspace_folder, :verbose => verbose?
    end

    def create_project_archive
      inside Boxci.project_path do
        run "git checkout #{@options["revision"]}", :verbose => verbose?
        run "git submodule update --init", :verbose => verbose?
        run "tar cf #{File.join(@project_workspace_folder, "project.tar")} --exclude \"*.log\" --exclude node_modules .", :verbose => verbose?
      end
    end

    def write_vagrant_file
      erb_template = File.join("templates", "providers", provider, "Vagrantfile.erb")
      destination = File.join(@project_workspace_folder, "Vagrantfile")

      template erb_template, destination, :verbose => verbose?
    end

    def write_test_runner
      destination = File.join(@project_workspace_folder, "test_runner.sh")
      test_runner = Boxci::TestRunner.new(Boxci::LanguageFactory.build(Boxci.project_config.language))
      File.open(destination, 'w+') do |f|
        f.write(test_runner.generate_script)
      end
    end

    def install_vagrant_plugin
      inside @project_workspace_folder do
        plugin = @provider_object.plugin
        # check for vagrant plugin
        if !system("vagrant plugin list | grep -q #{plugin}")
          # if vagrant plugin is missing
          say "You are missing the Vagrant plugin for #{provider}", :yellow
          run "vagrant plugin install #{plugin}", :verbose => verbose?
        else # if vagrant plugin is found
          say "Provider plugin #{plugin} found", :green
        end
      end
    end

    def add_provider_box
      dummy_box_url = @provider_object.dummy_box_url
      if dummy_box_url
        inside @project_workspace_folder do
          # check for box
          if !system("vagrant box list | grep dummy | grep -q \"(#{provider})\"")
            # if box is missing
            say "No box found for #{provider}, installing now...", :blue
            run "vagrant box add dummy #{dummy_box_url}", :verbose => verbose?
          else # if vagrant plugin is found
            say "Provider box found", :green
          end
        end
      end
    end

    def spin_up_box
      inside @project_workspace_folder do
        if verbose?
          if !run "vagrant up --no-provision --provider #{provider}", :verbose => verbose?
            raise Boxci::CommandFailed, "Failed to successfully run vagrant up --no-provision --provider #{provider}"
          end
        else
          if !run "vagrant up --no-provision --provider #{provider}"
            raise Boxci::CommandFailed, "Failed to successfully run vagrant up --no-provision --provider #{provider}"
          end
        end
      end
    end

    def setup_ssh_config
      inside @project_workspace_folder do
        if verbose?
          if !run "vagrant ssh-config > ssh-config.local", :verbose => verbose?
            raise Boxci::CommandFailed, "Failed to successfully run vagrant ssh-config > ssh-config.local"
          end
        else
          if !run "vagrant ssh-config > ssh-config.local"
            raise Boxci::CommandFailed, "Failed to successfully run vagrant ssh-config > ssh-config.local"
          end
        end
      end
    end

    def install_puppet_on_box
      say "Opening SSH tunnel into the box...", :blue if verbose?
      Net::SSH.start("default", nil, {:config => File.join(@project_workspace_folder, "ssh-config.local")}) do |ssh|
        puppet = ssh.exec! "which puppet"
        unless puppet
          say "Running: sudo apt-get --yes update", :blue if verbose?
          ssh.exec! "sudo apt-get --yes update"
          say "Running: sudo apt-get --yes install puppet", :blue if verbose?
          ssh.exec! "sudo apt-get --yes install puppet"
        end
      end
    end

    def provision_box
      say "Provisioning the box with puppet...", :blue if verbose?
      inside @project_workspace_folder do
        run "vagrant provision", :verbose => verbose?
      end
    end

    def create_artifact_directory
      say "Creating the artifact directory on the box...", :blue if verbose?
      Net::SSH.start("default", nil, {:config => File.join(@project_workspace_folder, "ssh-config.local")}) do |ssh|
        ssh.exec! "mkdir -p #{@project_config.artifact_path}"
      end
    end

    def upload_test_runner
      Net::SSH.start("default", nil, {:config => File.join(@project_workspace_folder, "ssh-config.local")}) do |ssh|
        say "Uploading test_runner.sh to the box...", :blue if verbose?
        remote_file_location = File.join(@project_config.base_directory, "test_runner.sh")
        ssh.scp.upload! File.join(@project_workspace_folder, "test_runner.sh"), remote_file_location
        say "Running: chmod a+x #{remote_file_location}", :blue if verbose?
        puts ssh.exec! "chmod a+x #{remote_file_location}"
      end
    end

    def run_tests
      exit_code = nil
      exit_signal = nil
      Net::SSH.start("default", nil, {:config => File.join(@project_workspace_folder, "ssh-config.local")}) do |session|
        say "Running the test steps on the box...", :blue if verbose?
        session.open_channel do |channel|
          channel.on_data do |ch, data|
            $stdout.write(data)
          end

          channel.on_extended_data do |ch, type, data|
            $stderr.write(data)
          end

          channel.on_request("exit-status") do |ch, data|
            exit_code = data.read_long
            @tester_exit_code = exit_code
          end

          channel.exec File.join(@project_config.base_directory, "test_runner.sh")
        end
        session.loop
      end
    end

    def download_artifacts
      Net::SSH.start("default", nil, {:config => File.join(@project_workspace_folder, "ssh-config.local")}) do |ssh|
        say "Downloading the reports...", :blue if verbose?
        artifact_file = File.join(@project_config.base_directory, 'boxci_artifacts.tar')
        puts ssh.exec! "cd #{@project_config.artifact_path} && tar cf #{artifact_file} ."
        ssh.scp.download! artifact_file, '.'
      end
    end

    def cleanup
      if @project_workspace_folder && File.directory?(@project_workspace_folder)
        # NOTE: The begin rescue for Errno::EPIPE and the &>>
        # /tmp/boxci.log in the backtick execution ARE required for
        # Bamboo's "Stop Build" functionality because Bamboo basically sends
        # a SIGTERM to Boxci and then immediately closes its stdout and
        # stderr pipes before Boxci has had a chance to cleanup. Therefore,
        # it causes Errno::EPIPE exceptions to be raised.
        begin
          say "Cleaning up...", :blue
        rescue Errno::EPIPE => e
          File.open('/tmp/boxci.log', 'a+') { |f| f.write("Cleaning up...\n") }
        end
        inside @project_workspace_folder do
          `vagrant destroy -f >> /tmp/boxci.log 2>&1`
          # run "vagrant destroy -f", :verbose => verbose?, :capture => true
        end
        `rm -rf #{@project_workspace_folder} >> /tmp/boxci.log 2>&1`
        # remove_dir @project_workspace_folder, :verbose => verbose?, :capture => true
      end
    end
  end
end