Class: ElasticBeans::Command::Exec

Inherits:
Object
  • Object
show all
Defined in:
lib/elastic_beans/command/exec.rb

Overview

:nodoc: all

Defined Under Namespace

Classes: BastionAuthenticationError, ExecEnvironmentBusyError, TerminatedInstanceError

Constant Summary collapse

USAGE =
"exec COMMAND STRING"
DESC =
"Run an arbitrary command in the context of your application"
LONG_DESC =
<<-LONG_DESC
Run an arbitrary command in the context of your application.
The command is run in an "exec" environment, separate from your webserver or worker environments.
You must create the exec environment prior to this command being run: `beans create exec -a APPLICATION`.
Output from the command is appended to /var/log/elastic_beans/exec/command.log.

If interactive, runs the command via SSH, tunneling through a bastion server if specified.

Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
LONG_DESC
COMMAND_LOGFILE =

:category: internal

"/var/log/elastic_beans/exec/command.log"
COMMAND_SCRIPT =

:category: internal

"/opt/elastic_beans/exec/run_command.sh"
TAKEN_WAIT_PERIOD =

:category: internal

5
TAKEN_WAIT_TIMEOUT =

:category: internal

120

Instance Method Summary collapse

Constructor Details

#initialize(application:, bastion_host:, bastion_identity_file:, bastion_username:, identity_file:, interactive:, username:, ec2:, ui:) ⇒ Exec

Returns a new instance of Exec.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/elastic_beans/command/exec.rb', line 29

def initialize(
  application:,
  bastion_host:,
  bastion_identity_file:,
  bastion_username:,
  identity_file:,
  interactive:,
  username:,
  ec2:,
  ui:
)
  @application = application
  @bastion_host = bastion_host
  @bastion_identity_file = bastion_identity_file
  @bastion_username = bastion_username
  @identity_file = identity_file
  @interactive = interactive
  @username = username || "ec2-user"
  @ec2 = ec2
  @ui = ui
end

Instance Method Details

#run(*command_parts) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/elastic_beans/command/exec.rb', line 51

def run(*command_parts)
  command = command_from_parts(command_parts)
  ui.info("Running `#{command.command_string}' on #{application.name}... (ID=#{command.id})")

  if interactive?
    ui.info("Finding an exec instance...")
    freeze_command = ::ElasticBeans::Exec::Command.freeze_instance

    begin
      ui.debug { "Freezing exec instance... (ID=#{freeze_command.id})" }
      application.enqueue_command(freeze_command)
      application.register_command(command)

      freeze_command = wait_until_command_taken(freeze_command)
      ui.debug { "Frozen exec instance: '#{freeze_command.instance_id}'" }
      command.instance_id = freeze_command.instance_id
      command.start_time = freeze_command.start_time
      application.register_command(command)

      begin
        instance_ip = ec2.describe_instances(instance_ids: [freeze_command.instance_id]).reservations[0].instances[0].private_ip_address
      rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
        raise TerminatedInstanceError.new(instance_id: freeze_command.instance_id)
      end
      # It's possible a retry would just work, but I'd like to see this happen in reality before I assume that.
      if instance_ip.nil?
        raise TerminatedInstanceError.new(instance_id: freeze_command.instance_id)
      end

      ui.info("Connecting to #{username}@#{instance_ip}...")
      ElasticBeans::SSH.new(
        hostname: instance_ip,
        username: username,
        identity_file: identity_file,
        bastion_host: bastion_host,
        bastion_username: bastion_username,
        bastion_identity_file: bastion_identity_file,
        ssh_options: %w(-t),
        command: [
          # Do not lose command exit status
          *%w(set -o pipefail &&),
          # Log command start just like SQSConsumer
          "echo", %("Executing command ID=#{command.id} \\`#{command.command_string}' on host `hostname` pid $$..."),
            ">>", COMMAND_LOGFILE, "&&",
          # Load application environment
          "sudo", COMMAND_SCRIPT, *command_parts,
          # Log command execution
            "|", "tee", "-a", COMMAND_LOGFILE, ";",
          # Save command exit status for final exit
          *%w(status=$? ;),
          # Log command end just like SQSConsumer
          "echo", %("Command ID=#{command.id} \\`#{command.command_string}' exited on host `hostname`: pid $$ exit $status"),
            ">>", COMMAND_LOGFILE, "&&",
          # Propagate command exit status
          *%w(exit $status),
        ],
        logger: ui,
      ).connect
    rescue ElasticBeans::SSH::BastionAuthenticationError => e
      raise BastionAuthenticationError.new(cause: e)
    ensure
      begin
        ui.info("Cleaning up, please do not interrupt!")
        application.kill_command(freeze_command)
        application.deregister_command(command)
      rescue Interrupt
        retry
      end
    end
  else
    application.enqueue_command(command)
  end
end