engineyard/engineyard

View on GitHub
lib/engineyard/cli.rb

Summary

Maintainability
F
3 days
Test Coverage
require 'engineyard'
require 'engineyard/error'
require 'engineyard/thor'
require 'engineyard/deploy_config'
require 'engineyard/serverside_runner'
require 'launchy'
require 'fileutils'

module EY
  class CLI < EY::Thor
    require 'engineyard/cli/recipes'
    require 'engineyard/cli/web'
    require 'engineyard/cli/api'
    require 'engineyard/cli/ui'
    require 'engineyard/error'
    require 'engineyard-cloud-client/errors'

    include Thor::Actions

    def self.start(given_args=ARGV, config={})
      Thor::Base.shell = EY::CLI::UI
      ui = EY::CLI::UI.new
      super(given_args, {shell: ui}.merge(config))
    rescue Thor::Error, EY::Error, EY::CloudClient::Error => e
      ui.print_exception(e)
      raise
    rescue Interrupt => e
      puts
      ui.print_exception(e)
      ui.say("Quitting...")
      raise
    rescue SystemExit, Errno::EPIPE
      # don't print a message for safe exits
      raise
    rescue Exception => e
      ui.print_exception(e)
      raise
    end

    class_option :api_token, type: :string, desc: "Use API_TOKEN to authenticate this command"
    class_option :serverside_version, type: :string, desc: "Please use with care! Override deploy system version (same as ENV variable ENGINEYARD_SERVERSIDE_VERSION)"
    class_option :quiet, aliases: %w[-q], type: :boolean, desc: "Quieter CLI output."

    desc "init",
      "Initialize the current directory with an ey.yml configuration file."
    long_desc <<-DESC
      Initialize the current directory with an ey.yml configuration file.

      Please read the generated file and make adjustments.
      Many applications will need only the default behavior.
      For reference, many available options are explained in the generated file.

      IMPORTANT: THE GENERATED FILE '#{EY::Config.pathname_for_write}'
      MUST BE COMMITTED TO YOUR REPOSITORY OR OPTIONS WILL NOT BE LOADED.
    DESC
    method_option :path, type: :string, aliases: %w(-p),
      desc: "Path for ey.yml (supported paths: #{EY::Config::CONFIG_FILES.join(', ')})"
    def init
      unless EY::Repo.exist?
        raise EY::Error, "Working directory is not a repository. Aborting."
      end

      path = Pathname.new(options['path'] || EY::Config.pathname_for_write)

      existing = {}
      if path.exist?
        ui.warn "Reinitializing existing file: #{path}"
        existing = EY::Config.load_config
      end

      template = EY::Templates::EyYml.new(existing)
      template.write(path)

      ui.info <<-GIT

Configuration generated: #{path}
Go look at it, then add it to your repository!

\tgit add #{path}
\tgit commit -m "Add generated #{path} from ey init"

      GIT
    end


    desc "deploy [--environment ENVIRONMENT] [--ref GIT-REF]",
      "Deploy specified branch, tag, or sha to specified environment."
    long_desc <<-DESC
      This command must be run with the current directory containing the app to be
      deployed. If ey.yml specifies a default branch then the ref parameter can be
      omitted. Furthermore, if a default branch is specified but a different command
      is supplied the deploy will fail unless -R or --force-ref is used.

      Migrations are run based on the settings in your ey.yml file.
      With each deploy the default migration setting can be overriden by
      specifying --migrate or --migrate 'rake db:migrate'.
      Migrations can also be skipped by using --no-migrate.
    DESC
    method_option :ignore_bad_master, type: :boolean, aliases: %w(--ignore-bad-bridge),
      desc: "Force a deploy even if the master is in a bad state"
    method_option :migrate, type: :string, aliases: %w(-m),
      lazy_default: true,
      desc: "Run migrations via [MIGRATE]; use --no-migrate to avoid running migrations"
    method_option :ref, type: :string, aliases: %w(-r --branch --tag),
      required: true, default: '',
      desc: "Git ref to deploy. May be a branch, a tag, or a SHA. Use -R to deploy a different ref if a default is set."
    method_option :force_ref, type: :string, aliases: %w(--ignore-default-branch -R),
      lazy_default: true,
      desc: "Force a deploy of the specified git ref even if a default is set in ey.yml."
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: false,
      desc: "Environment in which to deploy this application"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application to deploy"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    method_option :verbose, type: :boolean, aliases: %w(-v),
      desc: "Be verbose"
    method_option :config, type: :hash, default: {}, aliases: %w(--extra-deploy-hook-options),
      desc: "Hash made available in deploy hooks (in the 'config' hash), can also override some ey.yml settings."
    def deploy
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])

      env_config    = config.environment_config(app_env.environment_name)
      deploy_config = EY::DeployConfig.new(options, env_config, repo, ui)

      deployment = app_env.new_deployment({
        ref:                deploy_config.ref,
        migrate:            deploy_config.migrate,
        migrate_command:    deploy_config.migrate_command,
        extra_config:       deploy_config.extra_config,
        serverside_version: serverside_version,
      })

      runner = serverside_runner(app_env, deploy_config.verbose, deployment.serverside_version, options[:ignore_bad_master])

      out = EY::CLI::UI::Tee.new(ui.out, deployment.output)
      err = EY::CLI::UI::Tee.new(ui.err, deployment.output)

      ui.info  "Beginning deploy...", :green
      begin
        deployment.start
      rescue
        ui.error "Error encountered before deploy. Deploy not started."
        raise
      end

      begin
        ui.show_deployment(deployment)
        out << "Deploy initiated.\n"

        runner.deploy do |args|
          args.config  = deployment.config          if deployment.config
          if deployment.migrate
            args.migrate = deployment.migrate_command
          else
            args.migrate = false
          end
          args.ref     = deployment.resolved_ref
        end
        deployment.successful = runner.call(out, err)
      rescue Interrupt
        Signal.trap(:INT) { # The fingers you have used to dial are too fat...
          ui.info "\nRun `ey timeout-deploy` to mark an unfinished deployment as failed."
          exit 1
        }
        err << "Interrupted. Deployment halted.\n"
        ui.warn <<-WARN
Recording interruption of this unfinished deployment in Engine Yard Cloud...

WARNING: Interrupting again may prevent Engine Yard Cloud from recording this
         failed deployment. Unfinished deployments can block future deploys.
        WARN
        raise
      rescue StandardError => e
        deployment.err << "Error encountered during deploy.\n#{e.class} #{e}\n"
        ui.print_exception(e)
        raise
      ensure
        ui.info "Saving log... ", :green
        deployment.finished

        if deployment.successful?
          ui.info "Successful deployment recorded on Engine Yard Cloud.", :green
          ui.info "Run `ey launch` to open the application in a browser."
        else
          ui.info "Failed deployment recorded on Engine Yard Cloud", :green
          raise EY::Error, "Deploy failed"
        end
      end
    end

    desc "timeout-deploy [--environment ENVIRONMENT]",
      "Fail a stuck unfinished deployment."
    long_desc <<-DESC
      NOTICE: Timing out a deploy does not stop currently running deploy
      processes.

      This command must be run in the current directory containing the app.
      The latest running deployment will be marked as failed, allowing a
      new deployment to be run. It is possible to mark a potentially successful
      deployment as failed. Only run this when a deployment is known to be
      wrongly unfinished/stuck and when further deployments are blocked.
    DESC
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: false,
      desc: "Environment in which to deploy this application"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application to deploy"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    def timeout_deploy
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
      deployment = app_env.last_deployment
      if deployment && !deployment.finished?
        begin
          ui.info  "Marking last deployment failed...", :green
          deployment.timeout
          ui.deployment_status(deployment)
        rescue EY::CloudClient::RequestFailed => e
          ui.error "Error encountered attempting to timeout previous deployment."
          raise
        end
      else
        raise EY::Error, "No unfinished deployment was found for #{app_env.hierarchy_name}."
      end
    end

    desc "status", "Show the deployment status of the app"
    long_desc <<-DESC
      Show the current status of most recent deployment of the specified
      application and environment.
    DESC
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment where the application is deployed"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the application can be found"
    def status
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
      deployment = app_env.last_deployment
      if deployment
        ui.deployment_status(deployment)
      else
        raise EY::Error, "Application #{app_env.app.name} has not been deployed on #{app_env.environment.name}."
      end
    end

    desc "environments [--all]", "List environments for this app; use --all to list all environments."
    long_desc <<-DESC
      By default, environments for this app are displayed. The --all option will
      display all environments, including those for this app.
    DESC

    method_option :all, type: :boolean, aliases: %(-A),
      desc: "Show all environments (ignores --app, --account, and --environment arguments)"
    method_option :simple, type: :boolean, aliases: %(-s),
      desc: "Display one environment per line with no extra output"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Show environments for this application"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Show environments in this account"
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Show environments matching environment name"
    def environments
      if options[:all] && options[:simple]
        ui.print_simple_envs api.environments
      elsif options[:all]
        ui.print_envs api.apps
      else
        remotes = nil
        if options[:app] == ''
          repo.fail_on_no_remotes!
          remotes = repo.remotes
        end

        resolver = api.resolve_app_environments({
          account_name:     options[:account],
          app_name:         options[:app],
          environment_name: options[:environment],
          remotes:          remotes,
        })

        resolver.no_matches do |errors|
          messages = errors
          messages << "Use #{self.class.send(:banner_base)} environments --all to see all environments."
          raise EY::NoMatchesError.new(messages.join("\n"))
        end

        apps = resolver.matches.map { |app_env| app_env.app }.uniq

        if options[:simple]
          if apps.size > 1
            message = "# This app matches multiple Applications in Engine Yard Cloud:\n"
            apps.each { |app| message << "#\t#{app.name}\n" }
            message << "# The following environments contain those applications:\n\n"
            ui.warn(message)
          end
          ui.print_simple_envs(apps.map{ |app| app.environments }.flatten)
        else
          ui.print_envs(apps, config.default_environment)
        end
      end
    end
    map "envs" => :environments

    desc "servers", "List servers for an environment."
    long_desc <<-DESC
      Display a list of all servers on an environment.
      Specify -s (--simple) to make parsing the output easier
      or -uS (--user --host) to output bash loop friendly "user@hostname"
    DESC

    method_option :simple, type: :boolean, aliases: %(-s),
      desc: "Display all information in a simplified format without extra text or column alignment"
    method_option :host, type: :boolean, aliases: %(-S),
      desc: "Display only hostnames, one per newline (use options -uS (--user --host) for user@hostname)"
    method_option :user, type: :boolean, aliases: %w(-u),
      desc: "Include the ssh username in front of the hostname for easy SSH scripting"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Find environment in this account"
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Show servers in environment matching environment name"
    method_option :all, type: :boolean, aliases: %(-A),
      desc: "Show all servers (for compatibility only, this is the default for this command)"
    method_option :app_master, type: :boolean,
      desc: "Show only app master server"
    method_option :app_servers, type: :boolean, aliases: %w(--app),
      desc: "Show only application servers"
    method_option :db_servers, type: :boolean, aliases: %w(--db),
      desc: "Show only database servers"
    method_option :db_master, type: :boolean,
      desc: "Show only the master database server"
    method_option :db_slaves, type: :boolean,
      desc: "Show only the slave database servers"
    method_option :utilities, type: :array, lazy_default: true, aliases: %w(--util),
      desc: "Show only utility servers or only utility servers with the given names"
    def servers
      if options[:environment] == '' && options[:account] == ''
        repo.fail_on_no_remotes!
      end

      environment = nil
      ui.mute_if(options[:simple] || options[:host]) do
        environment = fetch_environment(options[:environment], options[:account])
      end

      username = options[:user] && environment.username

      servers = filter_servers(environment, options, default: {all: true})

      if options[:host]
        ui.print_hostnames(servers, username)
      elsif options[:simple]
        ui.print_simple_servers(servers, username)
      else
        ui.print_servers(servers, environment.hierarchy_name, username)
      end
    end

    desc "rebuild [--environment ENVIRONMENT]", "Rebuild specified environment."
    long_desc <<-DESC
      Engine Yard's main configuration run occurs on all servers. Mainly used to fix
      failed configuration of new or existing servers, or to update servers to latest
      Engine Yard stack (e.g. to apply an Engine Yard supplied security
      patch).

      Note that uploaded recipes are also run after the main configuration run has
      successfully completed.
    DESC

    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment to rebuild"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    def rebuild
      environment = fetch_environment(options[:environment], options[:account])
      ui.info "Updating instances on #{environment.hierarchy_name}"
      environment.rebuild
    end
    map "update" => :rebuild

    desc "rollback [--environment ENVIRONMENT]", "Rollback to the previous deploy."
    long_desc <<-DESC
      Uses code from previous deploy in the "/data/APP_NAME/releases" directory on
      remote server(s) to restart application servers.
    DESC

    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment in which to roll back the application"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application to roll back"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    method_option :verbose, type: :boolean, aliases: %w(-v),
      desc: "Be verbose"
    method_option :config, type: :hash, default: {}, aliases: %w(--extra-deploy-hook-options),
      desc: "Hash made available in deploy hooks (in the 'config' hash), can also override some ey.yml settings."
    def rollback
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
      env_config    = config.environment_config(app_env.environment_name)
      deploy_config = EY::DeployConfig.new(options, env_config, repo, ui)

      ui.info "Rolling back #{app_env.hierarchy_name}"

      runner = serverside_runner(app_env, deploy_config.verbose)
      runner.rollback do |args|
        args.config = {'deployed_by' => api.current_user.name, 'input_ref' => 'N/A'}.merge(deploy_config.extra_config || {})
      end

      if runner.call(ui.out, ui.err)
        ui.info "Rollback complete"
      else
        raise EY::Error, "Rollback failed"
      end
    end

    desc "ssh [COMMAND] [--all] [--environment ENVIRONMENT]", "Open an ssh session to the master app server, or run a command."
    long_desc <<-DESC
      If a command is supplied, it will be run, otherwise a session will be
      opened. The bridge server (app master) is used for environments with multiple instances.

      Option --all requires a command to be supplied and runs it on all servers or
      pass --each to connect to each server one after another.

      Note: this command is a bit picky about its ordering. To run a command with arguments on
      all servers, like "rm -f /some/file", you need to order it like so:

      $ #{banner_base} ssh "rm -f /some/file" -e my-environment --all
    DESC
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment to ssh into"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    method_option :all, type: :boolean, aliases: %(-A),
      desc: "Run command on all servers"
    method_option :app_servers, type: :boolean,
      desc: "Run command on all application servers"
    method_option :db_servers, type: :boolean,
      desc: "Run command on the database servers"
    method_option :db_master, type: :boolean,
      desc: "Run command on the master database server"
    method_option :db_slaves, type: :boolean,
      desc: "Run command on the slave database servers"
    method_option :utilities, type: :array, lazy_default: true,
      desc: "Run command on the utility servers with the given names. If no names are given, run on all utility servers."
    method_option :shell, type: :string, default: 'bash', aliases: %w(-s),
      desc: "Run command in a shell other than bash. Use --no-shell to run the command without a shell."
    method_option :pty, type: :boolean, default: false, aliases: %w(-t),
      desc: "If a command is given, run in a pty. Required for interactive commands like sudo."
    method_option :bind_address, type: :string, aliases: %w(-L),
      desc: "When a command is not given, pass -L to the ssh command."
    method_option :each, type: :boolean, default: false,
      desc: "If no command is given, connect to multiple servers each one after another, instead of exiting with an error."

    def ssh(cmd=nil)
      environment = fetch_environment(options[:environment], options[:account])
      instances = filter_servers(environment, options, default: {app_master: true})
      user = environment.username
      ssh_opts = []

      if cmd
        if options[:shell]
          cmd = Escape.shell_command([options[:shell],'-lc',cmd])
        end

        if options[:pty]
          ssh_opts = ["-t"]
        elsif cmd =~ /sudo/
          ui.warn "sudo commands often need a tty to run correctly. Use -t option to spawn a tty."
        end
      else
        if instances.size != 1 && options[:each] == false
          raise NoCommandError.new
        end

        if options[:bind_address]
          ssh_opts = ["-L", options[:bind_address]]
        end
      end

      ssh_cmd = ["ssh"]
      ssh_cmd += ssh_opts

      trap(:INT) { abort "Aborting..." }

      exits = []
      instances.each do |instance|
        host = instance.public_hostname
        name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
        ui.info "\nConnecting to #{name} #{host}..."
        unless cmd
          ui.info "Ctrl + C to abort"
          sleep 1.3
        end
        sshcmd = Escape.shell_command((ssh_cmd + ["#{user}@#{host}"] + [cmd]).compact)
        ui.debug "$ #{sshcmd}"
        system sshcmd
        exits << $?.exitstatus
      end

      exit exits.detect {|status| status != 0 } || 0
    end

    desc "console [--app APP] [--environment ENVIRONMENT] [--account ACCOUNT]", "Open a Rails console session to the master app server."
    long_desc <<-DESC
      Opens a Rails console session on app master.
    DESC
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment to console into"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"

    def console
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
      instances = filter_servers(app_env.environment, options, default: {app_master: true})
      user = app_env.environment.username
      cmd = "cd /data/#{app_env.app.name}/current && bundle exec rails console"
      cmd = Escape.shell_command(['bash','-lc',cmd])

      ssh_cmd = ["ssh"]
      ssh_cmd += ["-t"]

      trap(:INT) { abort "Aborting..." }

      exits = []
      instances.each do |instance|
        host = instance.public_hostname
        name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
        ui.info "\nConnecting to #{name} #{host}..."
        unless cmd
          ui.info "Ctrl + C to abort"
          sleep 1.3
        end
        sshcmd = Escape.shell_command((ssh_cmd + ["#{user}@#{host}"] + [cmd]).compact)
        ui.debug "$ #{sshcmd}"
        system sshcmd
        exits << $?.exitstatus
      end

      exit exits.detect {|status| status != 0 } || 0
    end

    desc "scp [FROM_PATH] [TO_PATH] [--all] [--environment ENVIRONMENT]", "scp a file to/from multiple servers in an environment"
    long_desc <<-DESC
      Use the system `scp` command to copy files to some or all of the servers.

      If `HOST:` is found in the FROM_PATH or TO_PATH, the server name will be
      substituted in place of `HOST:` when scp is run. This allows you to scp in
      either direction by putting `HOST:` in the FROM_PATH or TO_PATH, as follows:

      $ #{banner_base} scp example.json HOST:/data/app_name/current/config/ -e env --app-servers

      $ #{banner_base} scp HOST:/data/app_name/current/config/example.json ./ -e env --app-servers

      If `HOST:` is not specified, TO_PATH will be used as the remote path.
      Be sure to escape shell words so they don't expand locally (e.g. '~').

      Note: this command is a bit picky about its ordering. FROM_PATH TO_PATH
      must follow immediately after `ey scp` with no flags in between.
    DESC
    method_option :environment, :type => :string, :aliases => %w(-e),
      :required => true, :default => '',
      :desc => "Name of the destination environment"
    method_option :account, :type => :string, :aliases => %w(-c),
      :required => true, :default => '',
      :desc => "Name of the account in which the environment can be found"
    method_option :all, :type => :boolean, :aliases => %(-A),
      :desc => "scp to all servers"
    method_option :app_servers, :type => :boolean,
      :desc => "scp to all application servers"
    method_option :db_servers, :type => :boolean,
      :desc => "scp to database servers"
    method_option :db_master, :type => :boolean,
      :desc => "scp to the master database server"
    method_option :db_slaves, :type => :boolean,
      :desc => "scp to the slave database servers"
    method_option :utilities, :type => :array, :lazy_default => true,
      :desc => "scp to all utility servers or only those with the given names"

    def scp(from_path, to_path)
      environment = fetch_environment(options[:environment], options[:account])
      instances   = filter_servers(environment, options, default: {app_master: true})
      user        = environment.username

      ui.info "Copying '#{from_path}' to '#{to_path}' on #{instances.count} server#{instances.count == 1 ? '' : 's'} serially..."

      # default to `scp FROM_PATH HOST:TO_PATH`
      unless [from_path, to_path].detect { |path| path =~ /HOST:/ }
        to_path = "HOST:#{to_path}"
      end

      exits = []
      instances.each do |instance|
        host = instance.public_hostname
        authority = "#{user}@#{host}:"

        name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
        ui.info "# #{name} #{host}"

        from = from_path.sub(/^HOST:/, authority)
        to   =   to_path.sub(/^HOST:/, authority)

        cmd  = Escape.shell_command(["scp", from, to])
        ui.debug "$ #{cmd}"
        system cmd
        exits << $?.exitstatus
      end

      exit exits.detect {|status| status != 0 } || 0
    end

    no_tasks do
      OPT_TO_ROLES = {
        all:         %w[all],
        app_master:  %w[solo app_master],
        app_servers: %w[solo app app_master],
        db_servers:  %w[solo db_master db_slave],
        db_master:   %w[solo db_master],
        db_slaves:   %w[db_slave],
        utilities:   %w[util],
      }

      def filter_servers(environment, cli_opts, filter_opts)
        if (cli_opts.keys.map(&:to_sym) & OPT_TO_ROLES.keys).any?
          options = cli_opts.dup
        else
          options = filter_opts[:default].dup
        end

        options.keep_if {|k,v| OPT_TO_ROLES.has_key?(k.to_sym) }

        if options[:all]
          instances = environment.instances
        else
          roles = {}
          options.each do |cli_opt,cli_val|
            if cli_val && OPT_TO_ROLES.has_key?(cli_opt.to_sym)
              OPT_TO_ROLES[cli_opt.to_sym].each do |role|
                roles[role] = cli_val # val is true or an array of strings
              end
            end
          end
          instances = environment.select_instances(roles)
        end

        if instances.empty?
          raise NoInstancesError.new(environment.name)
        end

        return instances
      end
    end

    desc "logs [--environment ENVIRONMENT]", "Retrieve the latest logs for an environment."
    long_desc <<-DESC
      Displays Engine Yard configuration logs for all servers in the environment. If
      recipes were uploaded to the environment & run, their logs will also be
      displayed beneath the main configuration logs.
    DESC
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment with the interesting logs"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the environment can be found"
    def logs
      environment = fetch_environment(options[:environment], options[:account])
      environment.logs.each do |log|
        ui.say "Instance: #{log.instance_name}"

        if log.main
          ui.say "Main logs for #{environment.name}:", :green
          ui.say  log.main
        end

        if log.custom
          ui.say "Custom logs for #{environment.name}:", :green
          ui.say  log.custom
        end
      end
    end

    desc "recipes", "Commands related to chef recipes."
    subcommand "recipes", EY::CLI::Recipes

    desc "web", "Commands related to maintenance pages."
    subcommand "web", EY::CLI::Web

    desc "version", "Print version number."
    def version
      ui.say %{engineyard version #{EY::VERSION}}
    end
    map ["-v", "--version"] => :version

    desc "help [COMMAND]", "Describe all commands or one specific command."
    def help(*cmds)
      if cmds.empty?
        base = self.class.send(:banner_base)
        list = self.class.printable_tasks

        ui.say "Usage:"
        ui.say "  #{base} [--help] [--version] COMMAND [ARGS]"
        ui.say

        ui.say "Deploy commands:"
        deploy_cmds = %w(deploy environments logs rebuild rollback status)
        deploy_cmds.map! do |name|
          list.find{|task| task[0] =~ /^#{base} #{name}/ }
        end
        list -= deploy_cmds

        ui.print_help(deploy_cmds)
        ui.say

        self.class.subcommands.each do |name|
          klass = self.class.subcommand_class_for(name)
          list.reject!{|cmd| cmd[0] =~ /^#{base} #{name}/}
          ui.say "#{name.capitalize} commands:"
          tasks = klass.printable_tasks.reject{|t| t[0] =~ /help$/ }
          ui.print_help(tasks)
          ui.say
        end

        %w(help version).each{|n| list.reject!{|c| c[0] =~ /^#{base} #{n}/ } }
        if list.any?
          ui.say "Other commands:"
          ui.print_help(list)
          ui.say
        end

        self.class.send(:class_options_help, shell)
        ui.say "See '#{base} help COMMAND' for more information on a specific command."
      elsif klass = self.class.subcommand_class_for(cmds.first)
        klass.new.help(*cmds[1..-1])
      else
        super
      end
    end

    desc "launch [--app APP] [--environment ENVIRONMENT] [--account ACCOUNT]", "Open application in browser."
    method_option :environment, type: :string, aliases: %w(-e),
      required: true, default: '',
      desc: "Environment where the application is deployed"
    method_option :app, type: :string, aliases: %w(-a),
      required: true, default: '',
      desc: "Name of the application"
    method_option :account, type: :string, aliases: %w(-c),
      required: true, default: '',
      desc: "Name of the account in which the application can be found"
    def launch
      app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
      Launchy.open(app_env.uri)
    end

    desc "whoami", "Who am I logged in as?"
    def whoami
      current_user = api.current_user
      ui.say "#{current_user.name} (#{current_user.email})"
    end

    desc "login", "Log in and verify access to Engine Yard Cloud."
    long_desc <<-DESC
      You may run this command to log in to EY Cloud without performing
      any other action.

      Once you are logged in, a file will be stored at ~/.eyrc with your
      API token. You may override the location of this file using the
      $EYRC environment variable.

      Instead of logging in, you may specify a token on the command line
      with --api-token or using the $ENGINEYARD_API_TOKEN environment
      variable.
    DESC
    def login
      whoami
    end

    desc "logout", "Remove the current API key from ~/.eyrc or env variable $EYRC"
    def logout
      eyrc = EYRC.load
      if eyrc.delete_api_token
        ui.info "API token removed: #{eyrc.path}"
        ui.info "Run any other command to login again."
      else
        ui.info "Already logged out. Run any other command to login again."
      end
    end

  end # CLI
end # EY