elastic_beans 0.10.0.alpha6 → 0.10.0.alpha7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/lib/elastic_beans/application.rb +6 -2
- data/lib/elastic_beans/cli.rb +16 -3
- data/lib/elastic_beans/command/exec.rb +163 -5
- data/lib/elastic_beans/command/ps.rb +1 -0
- data/lib/elastic_beans/command/ssh.rb +21 -56
- data/lib/elastic_beans/environment.rb +7 -3
- data/lib/elastic_beans/exec/ebextension.yml +5 -1
- data/lib/elastic_beans/exec/run_command.sh +2 -0
- data/lib/elastic_beans/scheduler/ebextension.yml +1 -1
- data/lib/elastic_beans/ssh.rb +110 -0
- data/lib/elastic_beans/version.rb +1 -1
- data/lib/elastic_beans.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fec2d342af0cabded810ab2d37bf6f810b52a67
|
4
|
+
data.tar.gz: 6c2c698d6b72232de2461ec76f96084488a4e8f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
280
|
+
#{command_as_string "create -a #{@application.name} exec"}
|
277
281
|
MESSAGE
|
278
282
|
end
|
279
283
|
end
|
data/lib/elastic_beans/cli.rb
CHANGED
@@ -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::
|
219
|
-
long_desc ElasticBeans::Command::
|
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::
|
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
|
-
|
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 =
|
52
|
+
command = command_from_parts(command_parts)
|
25
53
|
ui.info("Running `#{command.command_string}' on #{application.name}... (ID=#{command.id})")
|
26
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
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(
|
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
|
293
|
-
Timeout.timeout(
|
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
|
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,4 +1,4 @@
|
|
1
1
|
container_commands:
|
2
2
|
remove_scheduler_file:
|
3
|
-
command: "rm -f
|
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
|
data/lib/elastic_beans.rb
CHANGED
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.
|
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-
|
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
|