zpatten/cucumber-chef

View on GitHub
bin/cucumber-chef

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby

require 'thor'
require 'cucumber-chef'


# $logger = Cucumber::Chef.logger

class CucumberChef < Thor
  include Thor::Actions

  no_tasks do

    def initalize_config
      source_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "cucumber", "chef", "templates", "cucumber-chef"))
      destination_dir = File.expand_path(File.join(Cucumber::Chef.locate_parent(".chef"), ".cucumber-chef"))
      FileUtils.mkdir_p(destination_dir)

      CucumberChef.source_root(source_dir)

      templates = {
        "config-rb.erb" => "config.rb"
      }

      templates.each do |source, destination|
        template(source, File.join(destination_dir, destination))
      end
      puts
      say "Ucanhaz Cucumber-Chef now! Rock on.", :green
    end

    def create_project(project)
      @project = project
      source_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "cucumber", "chef", "templates", "cucumber"))
      destination_dir = Cucumber::Chef.locate_parent(".chef")

      CucumberChef.source_root source_dir
      templates = {
        "readme.erb" => "features/#{project}/README.md",
        "example_feature.erb" => "features/#{project}/#{project}.feature",
        "example_steps.erb" => "features/#{project}/step_definitions/#{project}_steps.rb",
        "example_labfile.erb" => "Labfile",
        "env.rb" => "features/support/env.rb",
        "cc-hooks.rb" => "features/support/cc-hooks.rb",
        "readme-data_bags.erb" => "features/support/data_bags/README.md",
        "readme-roles.erb" => "features/support/roles/README.md",
        "readme-keys.erb" => "features/support/keys/README.md",
        "readme-environments.erb" => "features/support/environments/README.md"
      }

      templates.each do |source, destination|
        template(source, File.join(destination_dir, destination))
      end
    end

    def boot
      tag = Cucumber::Chef.tag("cucumber-chef")
      puts(tag)
      Cucumber::Chef.boot(tag)
      $logger = Cucumber::Chef.logger

      @is_rc = Cucumber::Chef.is_rc?

      @options.test? and Cucumber::Chef::Config.test
    end

    def fatal(message)
      puts(set_color(message, :red, :bold))
      exit(255)
    end

  end

################################################################################

  desc "init", "Initalize cucumber-chef configuration"
  def init
    initalize_config
  end

################################################################################
# SETUP
################################################################################

  desc "setup", "Setup the cucumber-chef test lab"
  method_option :test, :type => :boolean, :desc => "INTERNAL USE ONLY"
  def setup
    boot

    if (test_lab = Cucumber::Chef::TestLab.new)
      if (provider = test_lab.create)
        if (provisioner = Cucumber::Chef::Provisioner.new(test_lab))

          provisioner.build

          puts
          puts("If you are using AWS, be sure to log into the chef-server webui and change the default admin password at least.")
          puts
          puts("Your test lab has now been provisioned!  Enjoy!")
          puts
          test_lab.status

        else
          puts(set_color("Could not create the provisioner!", :red, true))
        end
      else
        puts(set_color("Could not create the server!", :red, true))
      end
    else
      puts(set_color("Could not create a new instance of test lab!", :red, true))
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e.message)
  end

################################################################################
# DESTROY
################################################################################

  desc "destroy [container] [...]", "Destroy the cucumber-chef test lab or a single or multiple containers if specified"
  method_option :test, :type => :boolean, :desc => "INTERNAL USE ONLY"
  def destroy(*args)
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.exists?
      if args.count == 0
        test_lab.status

        if yes?(set_color("Are you sure you want to destroy the test lab?", :red, true))
          puts
          puts(set_color("You have 5 seconds to abort!", :red, true))
          puts
          5.downto(1) do |x|
            print("#{x}...")
            sleep(1)
          end
          puts("BOOM!")
          puts

          ZTK::Benchmark.bench(:message => "Destroy #{Cucumber::Chef::Config.provider.upcase} instance '#{test_lab.id}'", :mark => "completed in %0.4f seconds.") do
            test_lab.destroy
          end
        else
          puts
          puts(set_color("Whew! That was close!", :green, true))
        end
      else
        if yes?(set_color("Are you sure you want to destroy the container#{args.count > 1 ? 's' : nil} #{args.collect{|a| "'#{a}'"}.join(', ')}?", :red, true))
          puts
          puts(set_color("You have 5 seconds to abort!", :red, true))
          puts
          5.downto(1) do |x|
            print("#{x}...")
            sleep(1)
          end
          puts("BOOM!")
          puts

          args.each do |container|
            ZTK::Benchmark.bench(:message => "Destroy container '#{container}'", :mark => "completed in %0.4f seconds.") do
              test_lab.containers.destroy(container)
            end
          end

        else
          puts
          puts(set_color("Whew! That was close!", :green, true))
        end

      end

    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# UP
################################################################################

  desc "up", "Power up the cucumber-chef test lab"
  def up
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.dead?
      ZTK::Benchmark.bench(:message => "Booting #{Cucumber::Chef::Config.provider.upcase} instance '#{test_lab.id}'", :mark => "completed in %0.4f seconds.") do
        test_lab.up
      end
    else
      raise Cucumber::Chef::Error, "We could not find a powered off test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# DOWN
################################################################################

  desc "down", "Power off the cucumber-chef test lab"
  def down
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.alive?
      ZTK::Benchmark.bench(:message => "Downing #{Cucumber::Chef::Config.provider.upcase} instance '#{test_lab.id}'", :mark => "completed in %0.4f seconds.") do
        test_lab.down
      end
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# RELOAD
################################################################################

  desc "reload", "Reload the cucumber-chef test lab"
  def reload
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.alive?
      ZTK::Benchmark.bench(:message => "Reloading #{Cucumber::Chef::Config.provider.upcase} instance '#{test_lab.id}'", :mark => "completed in %0.4f seconds.") do
        test_lab.reload
      end
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

  desc "genmac", "Generate an RFC compliant private MAC address"
  def genmac
    boot

    puts Cucumber::Chef::Containers.generate_mac
  end

  desc "genip", "Generate an RFC compliant private IP address"
  def genip
    boot

    puts Cucumber::Chef::Containers.generate_ip
  end

################################################################################
# STATUS
################################################################################

  desc "status", "Displays the current status of the test lab."
  method_option :containers, :type => :boolean, :desc => "Display container status.", :default => false
  method_option :attributes, :type => :boolean, :desc => "Display chef-client attributes for containers.", :default => false
  method_option :test, :type => :boolean, :desc => "INTERNAL USE ONLY"
  def status
    boot

    if (test_lab = Cucumber::Chef::TestLab.new)
      if @options.containers?
        if test_lab.alive?

          if test_lab.containers.count > 0
            headers = [:name, :alive, :distro, :ip, :mac, :"chef version", :persist]
            results = ZTK::Report.new.spreadsheet(Cucumber::Chef::Container.all, headers) do |container|
              chef_version = "N/A"
              alive = (test_lab.bootstrap_ssh(:ignore_exit_status => true).exec(%(ping -n -c 1 -W 1 #{container.ip}), :silence => true).exit_code == 0)
              if alive
                chef_version = test_lab.proxy_ssh(container.id, :ignore_exit_status => true).exec(%(/usr/bin/env chef-client -v), :silence => true).output.chomp
              end

              OpenStruct.new(
                :name => container.id,
                :ip => container.ip,
                :mac => container.mac,
                :distro => container.distro,
                :alive => alive,
                :"chef version" => chef_version,
                :persist => container.persist,
                :chef_attributes => container.chef_client
              )
            end

            if @options.attributes?
              results.rows.each do |result|
                puts
                puts("-" * results.width)
                puts("Chef-Client attributes for '#{result.name.to_s.downcase}':")
                puts("-" * results.width)
                puts(JSON.pretty_generate(result.chef_attributes))
              end
            end
          else
            raise Cucumber::Chef::Error, "We could not find any containers!"
          end

        else
          raise Cucumber::Chef::Error, "We could not find a running test lab."
        end

      else
        test_lab.status
      end
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e.message)
  end

################################################################################
# SSH
################################################################################

  desc "ssh [container]", "SSH to cucumber-chef test lab or [container] if specified"
  method_option :bootstrap, :type => :boolean, :desc => "Use the bootstrap settings", :default => false
  def ssh(*args)
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.alive?
      if @options.bootstrap?
        puts([set_color("Attempting bootstrap SSH connection to cucumber-chef '", :blue, true), set_color("test lab", :cyan, true), set_color("'...", :blue, true)].join)
        test_lab.bootstrap_ssh.console
      elsif args.size == 0
        puts([set_color("Attempting SSH connection to the '", :blue, true), set_color("test lab", :cyan, true), set_color("'...", :blue, true)].join)
        test_lab.ssh.console
      elsif args.size > 0
        container = args[0]
        puts([set_color("Attempting proxy SSH connection to the container '", :blue, true), set_color(container, :cyan, true), set_color("'...", :blue, true)].join)
        test_lab.proxy_ssh(container).console
      else
        raise Cucumber::Chef::Error, "You did not specify a valid combination of options."
      end
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# PS
################################################################################

  desc "ps [ps-options]", "Snapshot of the current cucumber-chef test lab container processes."
  def ps(*args)
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.alive?
      puts("-" * 80)
      test_lab.ssh.exec("lxc-ps --lxc -- #{args.join(" ")}")
      puts("-" * 80)
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# LOG
################################################################################

  desc "log", "Streams the cucumber-chef local and test lab logs to the terminal."
  def log
    boot

    if ($test_lab = Cucumber::Chef::TestLab.new) && $test_lab.exists? && $test_lab.alive?
      $tail_thread_remote = Thread.new do
        $test_lab.ssh.exec("tail -n 0 -f /home/#{$test_lab.ssh.config.user}/.cucumber-chef/cucumber-chef.log")
      end

      log_file = File.open(Cucumber::Chef.log_file, "r")
      log_file.seek(0, ::IO::SEEK_END)
      loop do
        if !(data = (log_file.readline rescue nil)).nil?
          print(data)
        else
          sleep(1)
        end
      end

      $tail_thread_remote.join
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# DIAGNOSE
################################################################################

  desc "diagnose <container>", "Provide diagnostics from the chef-client on the specified container."
  method_option :strace, :type => :boolean, :desc => "output the chef-client 'chef-stacktrace.out'", :aliases => "-s", :default => true
  method_option :log, :type => :boolean, :desc => "output the chef-client 'chef.log'", :aliases => "-l", :default => true
  method_option :lines, :type => :numeric, :desc => "output the last N lines of the chef-client 'chef.log'", :aliases => "-n", :default => 1
  def diagnose(container)
    boot

    if (test_lab = Cucumber::Chef::TestLab.new) && test_lab.alive?
      puts([set_color("Attempting to collect diagnostic information on cucumber-chef container '", :blue, true), set_color(container, :cyan, true), set_color("'...", :blue, true)].join)
      if @options.strace?
        puts
        puts("chef-stacktrace.out:")
        puts(set_color("============================================================================", :bold))
        test_lab.proxy_ssh(container).exec("[[ -e /var/chef/cache/chef-stacktrace.out ]] && cat /var/chef/cache/chef-stacktrace.out")
        print("\n")
      end
      if @options.log?
        puts
        puts("chef.log:")
        puts(set_color("============================================================================", :bold))
        test_lab.proxy_ssh(container).exec("[[ -e /var/log/chef/client.log ]] && tail -n #{@options.lines} /var/log/chef/client.log")
      end
    else
      raise Cucumber::Chef::Error, "We could not find a running test lab."
    end
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# DISPLAYCONFIG
################################################################################

  desc "displayconfig", "Display the current cucumber-chef config."
  method_option :test, :type => :boolean, :desc => "INTERNAL USE ONLY"
  def displayconfig
    boot

    details = {
      "root_dir" => Cucumber::Chef.root_dir,
      "home_dir" => Cucumber::Chef.home_dir,
      "log_file" => Cucumber::Chef.log_file,
      "artifacts_dir" => Cucumber::Chef.artifacts_dir,
      "config_rb" => Cucumber::Chef.config_rb,
      "labfile" => Cucumber::Chef.labfile,
      "chef_repo" => Cucumber::Chef.chef_repo,
      "chef_user" => Cucumber::Chef.chef_user,
      "chef_identity" => Cucumber::Chef.chef_identity,
      "bootstrap_user" => Cucumber::Chef.bootstrap_user,
      "bootstrap_user_home_dir" => Cucumber::Chef.bootstrap_user_home_dir,
      "bootstrap_identity" => Cucumber::Chef.bootstrap_identity,
      "lab_user" => Cucumber::Chef.lab_user,
      "lab_user_home_dir" => Cucumber::Chef.lab_user_home_dir,
      "lab_identity" => Cucumber::Chef.lab_identity,
      "lxc_user" => Cucumber::Chef.lxc_user,
      "lxc_user_home_dir" => Cucumber::Chef.lxc_user_home_dir,
      "lxc_identity" => Cucumber::Chef.lxc_identity,
      "chef_pre_11" => Cucumber::Chef.chef_pre_11
    }
    max_key_length = details.collect{ |k,v| k.to_s.length }.max

    puts("-" * 80)
    say(Cucumber::Chef::Config.configuration.to_yaml, :bold)
    puts("-" * 80)
    details.each do |key,value|
      puts("%#{max_key_length}s = %s" % [key.downcase, value.inspect])
    end
    puts("-" * 80)
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e.message)
  end

################################################################################
# CREATE
################################################################################

  desc "create <project>" , "Create a project template for testing an infrastructure."
  def create(project)

    create_project(project)
    root_dir = Cucumber::Chef.locate_parent(".chef")
    features_dir = File.join(root_dir, "features")
    feature = File.join(features_dir, "#{project}.feature")
    steps = File.join(features_dir, "step_definitions", "#{project}.steps")

    puts
    puts(set_color("Project created!", :green, true))
    say("Please look at the README in '#{features_dir}/#{project}/', and the example features (#{File.basename(feature)}) and steps (#{File.basename(steps)}), which I have autogenerated for you.", :green)
    puts

  rescue Cucumber::Chef::Error => e
    $logger.fatal { e.backtrace.join("\n") }
    fatal(e)
  end

################################################################################
# DEPRECATED TASKS
################################################################################

  deprecated_tasks = {
    "teardown" => "You should execute the 'destroy' task instead.",
    "info" => "You should execute the 'status' task instead.",
    "test" => "You should execute 'cucumber' or 'rspec' directly."
  }

  deprecated_tasks.each do |old_task, message|
    desc old_task, "*DEPRECATED* - #{message}"
    define_method(old_task) do
      puts
      puts(set_color("The '#{old_task}' task is *DEPRECIATED* - #{message}", :red, true))
      puts
    end
  end

################################################################################

end

CucumberChef.start