elastic_beans 0.10.0.alpha6 → 0.10.0.alpha7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4aa4fc0dc734f2eeee83efeed1dd7b288f3f9904
4
- data.tar.gz: 703e6b50540326e18a48ad9abb79da92e940a8c3
3
+ metadata.gz: 2fec2d342af0cabded810ab2d37bf6f810b52a67
4
+ data.tar.gz: 6c2c698d6b72232de2461ec76f96084488a4e8f6
5
5
  SHA512:
6
- metadata.gz: f6cbb95eb184b7a85f247c45062dc3a5f2bebf2af8480bd32802f698bcc2fdcaa084fb8ecf12ef4f1b072cc313c76fcdaf610d3a6caf9b03816cc4cf54a0006e
7
- data.tar.gz: 48648ab0283f6130b6d54a611ae36034a818e3625510893384ee74bf77c189154137c6d1a48c1674be3b7e71bad8b03904b55171f52e95669e3306cfe73c4ec0
6
+ metadata.gz: 3b0b71d95a2282b86822c3675246e62604ff488f7ff17db594899d82593455b09782dadad20a5d3a01daab80992ffc5b58c52832d8ecc17dab69ebd5dee0a571
7
+ data.tar.gz: d9213516af9141f51372bbe55a57067dc28b2ab8f186a9b4c0e281ef3184427e80c7683c2c0e65541fd1fc28b8fbab93e86d0eae38d6a6001a3a2d3068cc3429
data/README.md CHANGED
@@ -51,12 +51,16 @@ As the SDK documentation suggests, using environment variables is recommended.
51
51
  beans deploy -a myapp
52
52
 
53
53
  # Run one-off tasks
54
- beans exec -a myapp rake db:migrate
54
+ beans exec rake db:migrate -a myapp
55
55
 
56
56
  # SSH to an instance for debugging, tunneling through a bastion instance to reach the private network
57
57
  beans ssh -a myapp webserver [-n INDEX] [-i IDENTITY_FILE] [-u USERNAME] \
58
58
  [-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]
59
59
 
60
+ # Run interactive commands via SSH
61
+ beans exec rails console -a myapp --interactive [-i IDENTITY_FILE] [-u USERNAME] \
62
+ [-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]
63
+
60
64
  # Update all existing environments and configuration
61
65
  beans configure -n myapp-networking -a myapp \
62
66
  [-b SECRET_KEY_BASE] [-d DATABASE_URL] [-k KEYPAIR] \
@@ -107,7 +107,7 @@ module ElasticBeans
107
107
  # Raises an error if the exec environment or queue cannot be found.
108
108
  def enqueue_command(command)
109
109
  if environments.none? { |environment| environment.is_a?(Environment::Exec) }
110
- raise MissingExecEnvironmentError
110
+ raise MissingExecEnvironmentError.new(application: self)
111
111
  end
112
112
 
113
113
  if command.to_s == command
@@ -269,11 +269,15 @@ module ElasticBeans
269
269
  # :nodoc: all
270
270
  # @!visibility private
271
271
  class MissingExecEnvironmentError < ElasticBeans::Error
272
+ def initialize(application:)
273
+ @application = application
274
+ end
275
+
272
276
  def message
273
277
  <<-MESSAGE
274
278
  A one-off command cannot be executed because the "exec" environment does not exist. Please create it:
275
279
 
276
- #{command_as_string "-a APPLICATION exec"}
280
+ #{command_as_string "create -a #{@application.name} exec"}
277
281
  MESSAGE
278
282
  end
279
283
  end
@@ -86,12 +86,25 @@ class ElasticBeans::CLI < Thor
86
86
  desc ElasticBeans::Command::Exec::USAGE, ElasticBeans::Command::Exec::DESC
87
87
  long_desc ElasticBeans::Command::Exec::LONG_DESC
88
88
  option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
89
+ option :interactive, aliases: %w(-t), type: :boolean, desc: "Run this command interactively, via SSH. Set SSH options as necessary."
90
+ option :bastion_host, aliases: %w(-b), desc: "The hostname of the bastion server in the VPC"
91
+ option :bastion_identity_file, desc: "The SSH private key to use to connect to the bastion server"
92
+ option :bastion_username, desc: "The username to use to connect to the bastion server"
93
+ option :identity_file, aliases: %w(-i), desc: "The SSH private key associated with the keypair used when creating the environment"
94
+ option :username, aliases: %w(-u), desc: "The username to log into the instance with, e.g. `ec2-user`"
89
95
  def exec(*command_parts)
90
96
  @verbose = options[:verbose]
91
97
  ElasticBeans::Command::Exec.new(
92
98
  application: application(
93
99
  name: options[:application],
94
100
  ),
101
+ bastion_host: options[:bastion_host],
102
+ bastion_identity_file: options[:bastion_identity_file],
103
+ bastion_username: options[:bastion_username],
104
+ identity_file: options[:identity_file],
105
+ interactive: options[:interactive],
106
+ username: options[:username],
107
+ ec2: ec2_client,
95
108
  ui: ui,
96
109
  ).run(*command_parts)
97
110
  rescue StandardError => e
@@ -215,8 +228,8 @@ class ElasticBeans::CLI < Thor
215
228
 
216
229
  map ["delvar", "rmvar"] => "unsetenv"
217
230
 
218
- desc ElasticBeans::Command::Ssh::USAGE, ElasticBeans::Command::Ssh::DESC
219
- long_desc ElasticBeans::Command::Ssh::LONG_DESC
231
+ desc ElasticBeans::Command::SSH::USAGE, ElasticBeans::Command::SSH::DESC
232
+ long_desc ElasticBeans::Command::SSH::LONG_DESC
220
233
  option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
221
234
  option :bastion_host, aliases: %w(-b), desc: "The hostname of the bastion server in the VPC"
222
235
  option :bastion_identity_file, desc: "The SSH private key to use to connect to the bastion server"
@@ -227,7 +240,7 @@ class ElasticBeans::CLI < Thor
227
240
  option :username, aliases: %w(-u), desc: "The username to log into the instance with, e.g. `ec2-user`"
228
241
  def ssh(environment_type)
229
242
  @verbose = options[:verbose]
230
- ElasticBeans::Command::Ssh.new(
243
+ ElasticBeans::Command::SSH.new(
231
244
  application: application(
232
245
  name: options[:application],
233
246
  ),
@@ -12,28 +12,186 @@ The command is run in an "exec" environment, separate from your webserver or wor
12
12
  You must create the exec environment prior to this command being run: `beans create exec -a APPLICATION`.
13
13
  Output from the command is appended to /var/log/elastic_beans/exec/command.log.
14
14
 
15
+ If interactive, runs the command via SSH, tunneling through a bastion server if specified.
16
+
15
17
  Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
16
18
  LONG_DESC
17
19
 
18
- def initialize(application:, ui:)
20
+ # :category: internal
21
+ COMMAND_LOGFILE = "/var/log/elastic_beans/exec/command.log"
22
+ # :category: internal
23
+ COMMAND_SCRIPT = "/opt/elastic_beans/exec/run_command.sh"
24
+ # :category: internal
25
+ TAKEN_WAIT_PERIOD = 5
26
+ # :category: internal
27
+ TAKEN_WAIT_TIMEOUT = 120
28
+
29
+ def initialize(
30
+ application:,
31
+ bastion_host:,
32
+ bastion_identity_file:,
33
+ bastion_username:,
34
+ identity_file:,
35
+ interactive:,
36
+ username:,
37
+ ec2:,
38
+ ui:
39
+ )
19
40
  @application = application
41
+ @bastion_host = bastion_host
42
+ @bastion_identity_file = bastion_identity_file
43
+ @bastion_username = bastion_username
44
+ @identity_file = identity_file
45
+ @interactive = interactive
46
+ @username = username || "ec2-user"
47
+ @ec2 = ec2
20
48
  @ui = ui
21
49
  end
22
50
 
23
51
  def run(*command_parts)
24
- command = command(command_parts)
52
+ command = command_from_parts(command_parts)
25
53
  ui.info("Running `#{command.command_string}' on #{application.name}... (ID=#{command.id})")
26
- application.enqueue_command(command)
54
+
55
+ if interactive?
56
+ ui.info("Finding an exec instance...")
57
+ freeze_command = ::ElasticBeans::Exec::Command.freeze_instance
58
+
59
+ begin
60
+ ui.debug { "Freezing exec instance... (ID=#{freeze_command.id})" }
61
+ application.enqueue_command(freeze_command)
62
+ freeze_command = wait_until_command_taken(freeze_command)
63
+ ui.debug { "Frozen exec instance: '#{freeze_command.instance_id}'" }
64
+
65
+ begin
66
+ instance_ip = ec2.describe_instances(instance_ids: [freeze_command.instance_id]).reservations[0].instances[0].private_ip_address
67
+ rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
68
+ raise TerminatedInstanceError.new(instance_id: freeze_command.instance_id)
69
+ end
70
+ # It's possible a retry would just work, but I'd like to see this happen in reality before I assume that.
71
+ if instance_ip.nil?
72
+ raise TerminatedInstanceError.new(instance_id: freeze_command.instance_id)
73
+ end
74
+
75
+ ui.info("Connecting to #{username}@#{instance_ip}...")
76
+ ElasticBeans::SSH.new(
77
+ hostname: instance_ip,
78
+ username: username,
79
+ identity_file: identity_file,
80
+ bastion_host: bastion_host,
81
+ bastion_username: bastion_username,
82
+ bastion_identity_file: bastion_identity_file,
83
+ ssh_options: %w(-t),
84
+ command: [
85
+ # Do not lose command exit status
86
+ *%w(set -o pipefail &&),
87
+ # Log command start just like SQSConsumer
88
+ "sudo", "echo", %("Executing command ID=#{command.id} \\`#{command.command_string}' on host `hostname` pid $$..."),
89
+ ">>", COMMAND_LOGFILE, "&&",
90
+ # Load application environment
91
+ "sudo", COMMAND_SCRIPT, *command_parts,
92
+ # Log command execution
93
+ "|", "tee", "-a", COMMAND_LOGFILE, ";",
94
+ # Save command exit status for final exit
95
+ *%w(status=$? ;),
96
+ # Log command end just like SQSConsumer
97
+ "sudo", "echo", %("Command ID=#{command.id} \\`#{command.command_string}' exited on host `hostname`: pid $$ exit $status"),
98
+ ">>", COMMAND_LOGFILE, "&&",
99
+ # Propagate command exit status
100
+ *%w(exit $status),
101
+ ],
102
+ logger: ui,
103
+ ).connect
104
+ rescue ElasticBeans::SSH::BastionAuthenticationError => e
105
+ raise BastionAuthenticationError.new(cause: e)
106
+ ensure
107
+ application.kill_command(freeze_command)
108
+ end
109
+ else
110
+ application.enqueue_command(command)
111
+ end
27
112
  end
28
113
 
29
114
  private
30
115
 
31
- attr_reader :application, :ui
116
+ attr_reader(
117
+ :application,
118
+ :bastion_host,
119
+ :bastion_identity_file,
120
+ :bastion_username,
121
+ :identity_file,
122
+ :username,
123
+ :ec2,
124
+ :ui,
125
+ )
32
126
 
33
- def command(command_parts)
127
+ def command_from_parts(command_parts)
34
128
  command_string = command_parts.map { |word| Shellwords.escape(word) }.join(" ")
35
129
  ::ElasticBeans::Exec::Command.new(command_string: command_string)
36
130
  end
131
+
132
+ def interactive?
133
+ @interactive
134
+ end
135
+
136
+ def wait_until_command_taken(command)
137
+ taken_command = nil
138
+ Timeout.timeout(TAKEN_WAIT_TIMEOUT) do
139
+ until taken_command
140
+ sleep(TAKEN_WAIT_PERIOD)
141
+ taken_command = application.enqueued_commands.find { |cmd| cmd == command && !cmd.instance_id.nil? }
142
+ end
143
+ end
144
+ taken_command
145
+ rescue Timeout::Error
146
+ raise ExecEnvironmentBusyError.new(timeout: TAKEN_WAIT_TIMEOUT)
147
+ end
148
+
149
+ class BastionAuthenticationError < ElasticBeans::Error
150
+ def initialize(cause:)
151
+ @cause = cause
152
+ end
153
+
154
+ def backtrace
155
+ @cause.backtrace
156
+ end
157
+
158
+ def message
159
+ <<-MESSAGE
160
+ #{@cause.message}
161
+
162
+ Please check the bastion options and try again.
163
+
164
+ #{command_help "exec"}
165
+ MESSAGE
166
+ end
167
+ end
168
+
169
+ class ExecEnvironmentBusyError < ElasticBeans::Error
170
+ def initialize(timeout:)
171
+ @timeout = timeout
172
+ end
173
+
174
+ def message
175
+ <<-MESSAGE
176
+ The exec environment is too busy to reserve an instance for this interactive session (we waited #{@timeout} seconds).
177
+ Please scale it up and try again.
178
+ MESSAGE
179
+ end
180
+ end
181
+
182
+ class TerminatedInstanceError < ElasticBeans::Error
183
+ def initialize(instance_id:)
184
+ @instance_id = instance_id
185
+ end
186
+
187
+ def message
188
+ <<-MESSAGE
189
+ The exec instance '#{@instance_id}' was terminated.
190
+ Please check the exec environment status, wait for the environment to stabilize, and try again.
191
+ MESSAGE
192
+ msg
193
+ end
194
+ end
37
195
  end
38
196
  end
39
197
  end
@@ -37,6 +37,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
37
37
  def run
38
38
  ui.debug { "Fetching enqueued commands from #{application.name}..." }
39
39
  enqueued_commands = application.enqueued_commands
40
+ enqueued_commands.reject!(&:freeze_instance?)
40
41
 
41
42
  enqueued_commands, running_commands = enqueued_commands.partition { |cmd| cmd.start_time.nil? }
42
43
  running_commands.sort_by!(&:start_time)
@@ -4,7 +4,7 @@ require "elastic_beans/error/environments_not_ready"
4
4
  module ElasticBeans
5
5
  module Command
6
6
  # :nodoc: all
7
- class Ssh
7
+ class SSH
8
8
  USAGE = "ssh ENVIRONMENT_TYPE"
9
9
  DESC = "Connect to an instance for debugging, tunneling through a bastion server"
10
10
  LONG_DESC = <<-LONG_DESC
@@ -53,57 +53,26 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
53
53
  raise NoInstanceError.new(environment: environment, index: index)
54
54
  end
55
55
 
56
- begin
57
- instance_ip = ec2.describe_instances(instance_ids: [instance_id]).reservations[0].instances[0].private_ip_address
58
- rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
59
- raise NoInstanceError.new(environment: environment, index: index)
60
- end
56
+ instance_ip = ec2.describe_instances(instance_ids: [instance_id]).reservations[0].instances[0].private_ip_address
61
57
  # It's possible a retry would just work, but I'd like to see this happen in reality before I assume that.
62
58
  if instance_ip.nil?
63
59
  raise TerminatedInstanceError.new(instance_id: instance_id, environment: environment)
64
60
  end
65
61
 
66
- ssh_args = ["ssh"]
67
-
68
- if identity_file
69
- ssh_args << "-i"
70
- ssh_args << identity_file
71
- end
72
-
73
- gateway = nil
74
- if bastion_host
75
- ssh_args << "#{username}@localhost"
76
- ssh_args += ["-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"]
77
- begin
78
- bastion_options = {auth_methods: ["publickey"]}
79
- if bastion_identity_file
80
- bastion_options[:key_data] = File.read(bastion_identity_file)
81
- end
82
-
83
- ui.info("Connecting to '#{bastion_username}@#{bastion_host}'...")
84
- gateway = Net::SSH::Gateway.new(
85
- bastion_host,
86
- bastion_username,
87
- bastion_options
88
- )
89
- port = gateway.open(instance_ip, 22)
90
- ui.debug { "Opened gateway to '#{instance_ip}' on port '#{port}'" }
91
- ssh_args += ["-p", port.to_s]
92
- rescue Net::SSH::AuthenticationFailed => e
93
- raise BastionAuthenticationError.new(cause: e)
94
- end
95
- else
96
- ssh_args << "#{username}@#{instance_ip}"
97
- end
98
-
99
- ui.info("Connecting to '#{username}@#{instance_ip}'...")
100
- pid = Process.spawn(*ssh_args)
101
- _, status = Process.waitpid2(pid)
102
- unless status.success?
103
- raise SshFailedError
104
- end
105
- ensure
106
- gateway.close(port) if gateway
62
+ ui.info("Connecting to #{username}@#{instance_ip}...")
63
+ ElasticBeans::SSH.new(
64
+ hostname: instance_ip,
65
+ username: username,
66
+ identity_file: identity_file,
67
+ bastion_host: bastion_host,
68
+ bastion_username: bastion_username,
69
+ bastion_identity_file: bastion_identity_file,
70
+ logger: ui,
71
+ ).connect
72
+ rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
73
+ raise NoInstanceError.new(environment: environment, index: index)
74
+ rescue ElasticBeans::SSH::BastionAuthenticationError => e
75
+ raise BastionAuthenticationError.new(cause: e)
107
76
  end
108
77
 
109
78
  private
@@ -127,11 +96,15 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
127
96
  @cause = cause
128
97
  end
129
98
 
99
+ def backtrace
100
+ @cause.backtrace
101
+ end
102
+
130
103
  def message
131
104
  <<-MESSAGE
132
105
  #{@cause.message}
133
106
 
134
- Please check the --bastion-identity-file or --bastion-username options and try again.
107
+ Please check the bastion options and try again.
135
108
 
136
109
  #{command_help "ssh"}
137
110
  MESSAGE
@@ -159,12 +132,6 @@ Please check the --bastion-identity-file or --bastion-username options and try a
159
132
  end
160
133
  end
161
134
 
162
- class SshFailedError < ElasticBeans::Error
163
- def message
164
- ""
165
- end
166
- end
167
-
168
135
  class TerminatedInstanceError < ElasticBeans::Error
169
136
  def initialize(instance_id:, environment:)
170
137
  @instance_id = instance_id
@@ -175,8 +142,6 @@ Please check the --bastion-identity-file or --bastion-username options and try a
175
142
  <<-MESSAGE
176
143
  The instance '#{@instance_id}' in the environment '#{@environment.name}' has been terminated.
177
144
  Please try again in a moment.
178
-
179
- #{command_help "ssh"}
180
145
  MESSAGE
181
146
  end
182
147
  end
@@ -21,6 +21,10 @@ module ElasticBeans
21
21
  TIER_TYPE = "Standard"
22
22
  # :category: Internal
23
23
  WORKER_TEMPLATE_NAME_PATTERN = /worker-(?<queue>\w+)/
24
+ # :category: Internal
25
+ WAIT_TIMEOUT = 3600
26
+ # :category: Internal
27
+ DEGRADED_TIMEOUT = 90
24
28
 
25
29
  attr_reader :name
26
30
 
@@ -279,7 +283,7 @@ module ElasticBeans
279
283
  status = wait_status[0]
280
284
  health = "Grey"
281
285
  health_status = wait_health_status[0]
282
- Timeout.timeout(3600) do
286
+ Timeout.timeout(WAIT_TIMEOUT) do
283
287
  while wait_status.include?(status) || wait_health_status.include?(health_status)
284
288
  sleep 5
285
289
  environment = environment_description
@@ -289,8 +293,8 @@ module ElasticBeans
289
293
  end
290
294
  end
291
295
 
292
- # Wait a little bit longer. Sometimes apps are Ready but still Degraded for a few seconds.
293
- Timeout.timeout(30) do
296
+ # Wait a little bit longer. Sometimes apps are Ready but still Degraded for a few moments.
297
+ Timeout.timeout(DEGRADED_TIMEOUT) do
294
298
  while status == "Ready" && health != "Green"
295
299
  sleep 5
296
300
  environment = environment_description
@@ -1,8 +1,12 @@
1
1
  container_commands:
2
2
  00_copy_files:
3
- command: "mkdir -vp /opt/elastic_beans && cp -vR /var/app/ondeck/.elastic_beans/exec /opt/elastic_beans/"
3
+ command: "mkdir -vp /opt/elastic_beans && cp -vR .elastic_beans/exec /opt/elastic_beans/"
4
4
  01_permissions:
5
5
  command: "chmod 755 /opt/elastic_beans/exec/run_command.sh"
6
+ 05_log_files:
7
+ command: |
8
+ mkdir -vp /var/log/elastic_beans/exec
9
+ touch /var/log/elastic_beans/exec/command.log
6
10
  09_logrotate:
7
11
  command: "cp -v /opt/elastic_beans/exec/logrotate /etc/logrotate.d/elastic_beans_exec"
8
12
  test: "/opt/elasticbeanstalk/bin/get-config meta -k sqsdconfig --output YAML | grep -q '^environment_name: .*-exec$'"
@@ -1,5 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
+ set -e
4
+
3
5
  # execute in the context of the application
4
6
  EB_APP_DIR="`/opt/elasticbeanstalk/bin/get-config container -k app_deploy_dir`"
5
7
  cd "$EB_APP_DIR"
@@ -1,4 +1,4 @@
1
1
  container_commands:
2
2
  remove_scheduler_file:
3
- command: "rm -f /var/app/ondeck/cron.yaml"
3
+ command: "rm -f cron.yaml"
4
4
  test: "/opt/elasticbeanstalk/bin/get-config meta -k sqsdconfig --output YAML | grep '^environment_name: ' | grep -qv '^environment_name: .*-scheduler$'"
@@ -0,0 +1,110 @@
1
+ require 'logger'
2
+
3
+ module ElasticBeans
4
+ # Opens an SSH connection, tunneling through a bastion host if specified.
5
+ class SSH
6
+ def initialize(
7
+ hostname:,
8
+ username:,
9
+ bastion_host: nil,
10
+ bastion_username: nil,
11
+ bastion_identity_file: nil,
12
+ identity_file: nil,
13
+ ssh_options: [],
14
+ command: [],
15
+ logger: Logger.new('/dev/null')
16
+ )
17
+ @hostname = hostname
18
+ @username = username
19
+ @bastion_host = bastion_host
20
+ @bastion_username = bastion_username
21
+ @bastion_identity_file = bastion_identity_file
22
+ @identity_file = identity_file
23
+ @ssh_options = ssh_options
24
+ @command = command
25
+ @logger = logger
26
+ end
27
+
28
+ # Opens an SSH connection, tunneling through a bastion host if specified.
29
+ # Spawns a child process using the ssh command-line utility and waits for it to complete.
30
+ #
31
+ # Raises an error if SSH exits with a non-zero exit status.
32
+ def connect
33
+ ssh_args = ["ssh", *ssh_options]
34
+
35
+ if identity_file
36
+ ssh_args << "-i"
37
+ ssh_args << identity_file
38
+ end
39
+
40
+ gateway = nil
41
+ if bastion_host
42
+ ssh_args << "#{username}@localhost"
43
+ ssh_args += ["-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"]
44
+ begin
45
+ bastion_options = {auth_methods: ["publickey"]}
46
+ if bastion_identity_file
47
+ bastion_options[:key_data] = File.read(bastion_identity_file)
48
+ end
49
+
50
+ logger.info("Connecting to '#{bastion_username}@#{bastion_host}'...")
51
+ gateway = Net::SSH::Gateway.new(
52
+ bastion_host,
53
+ bastion_username,
54
+ bastion_options,
55
+ )
56
+ port = gateway.open(hostname, 22)
57
+ logger.debug { "Opened gateway to '#{hostname}' on port '#{port}'" }
58
+ ssh_args += ["-p", port.to_s]
59
+ rescue Net::SSH::Exception => e
60
+ raise BastionAuthenticationError.new(cause: e)
61
+ end
62
+ else
63
+ ssh_args << "#{username}@#{hostname}"
64
+ end
65
+
66
+ logger.debug { "Connecting: `#{ssh_args.join(' ')}'..." }
67
+ pid = Process.spawn(*ssh_args, *command)
68
+ _, status = Process.waitpid2(pid)
69
+ unless status.success?
70
+ raise SSHFailedError
71
+ end
72
+ ensure
73
+ gateway.close(port) if gateway
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader(
79
+ :bastion_host,
80
+ :bastion_identity_file,
81
+ :bastion_username,
82
+ :command,
83
+ :identity_file,
84
+ :hostname,
85
+ :ssh_options,
86
+ :username,
87
+ :logger,
88
+ )
89
+
90
+ class BastionAuthenticationError < ElasticBeans::Error
91
+ def initialize(cause:)
92
+ @cause = cause
93
+ end
94
+
95
+ def backtrace
96
+ @cause.backtrace
97
+ end
98
+
99
+ def message
100
+ @cause.message
101
+ end
102
+ end
103
+
104
+ class SSHFailedError < ElasticBeans::Error
105
+ def message
106
+ ""
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,3 +1,3 @@
1
1
  module ElasticBeans
2
- VERSION = "0.10.0.alpha6"
2
+ VERSION = "0.10.0.alpha7"
3
3
  end
data/lib/elastic_beans.rb CHANGED
@@ -8,4 +8,5 @@ require "elastic_beans/env_vars"
8
8
  require "elastic_beans/environment"
9
9
  require "elastic_beans/exec"
10
10
  require "elastic_beans/network"
11
+ require "elastic_beans/ssh"
11
12
  require "elastic_beans/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_beans
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0.alpha6
4
+ version: 0.10.0.alpha7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Stegman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-26 00:00:00.000000000 Z
11
+ date: 2017-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -233,6 +233,7 @@ files:
233
233
  - lib/elastic_beans/network.rb
234
234
  - lib/elastic_beans/rack/exec.rb
235
235
  - lib/elastic_beans/scheduler/ebextension.yml
236
+ - lib/elastic_beans/ssh.rb
236
237
  - lib/elastic_beans/ui.rb
237
238
  - lib/elastic_beans/version.rb
238
239
  homepage: https://github.com/onemedical/elastic_beans