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 +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
|