foreplay 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,344 +1,361 @@
1
- require 'thor/group'
2
- require 'yaml'
3
- require 'net/ssh'
4
- require 'net/ssh/shell'
5
- require 'active_support/inflector'
6
- require 'active_support/core_ext/object'
7
- require 'active_support/core_ext/hash'
8
- require 'colorize'
9
- require 'foreplay/utility'
10
-
11
- module Foreplay
12
- class Deploy < Thor::Group
13
- include Thor::Actions
14
-
15
- argument :mode, :type => :string, :required => true
16
- argument :environment, :type => :string, :required => true
17
- argument :filters, :type => :hash, :required => false
18
-
19
- DEFAULTS_KEY = 'defaults'
20
- INDENT = ' ' * 4
21
-
22
- def parse
23
- # Explain what we're going to do
24
- puts '%sing %s environment, %s, %s' % [
25
- mode.capitalize,
26
- environment.dup.yellow,
27
- explanatory_text(filters, 'role'),
28
- explanatory_text(filters, 'server')
29
- ]
30
-
31
- config_file = "#{Dir.getwd}/config/foreplay.yml"
32
-
33
- begin
34
- config_yml = File.read config_file
35
- rescue Errno::ENOENT
36
- terminate "Can't find configuration file #{config_file}.\nPlease run foreplay setup or create the file manually."
37
- end
38
-
39
- config_all = YAML.load(config_yml)
40
- config_env = config_all[environment] || {}
41
-
42
- # This environment
43
- terminate("No deployment configuration defined for #{environment} environment.\nCheck #{config_file}") unless config_all.has_key? environment
44
-
45
- # Establish defaults
46
- # First the default defaults
47
- defaults = {
48
- :name => File.basename(Dir.getwd),
49
- :environment => environment,
50
- :env => { 'RAILS_ENV' => environment },
51
- :port => 50000
52
- }
53
-
54
- defaults = Foreplay::Utility::supermerge(defaults, config_all[DEFAULTS_KEY]) if config_all.has_key? DEFAULTS_KEY # Then the global defaults
55
- defaults = Foreplay::Utility::supermerge(defaults, config_env[DEFAULTS_KEY]) if config_env.has_key? DEFAULTS_KEY # Then the defaults for this environment
56
-
57
- config_env.each do |role, additional_instructions|
58
- next if role == DEFAULTS_KEY # 'defaults' is not a role
59
- next if filters.has_key?('role') && filters['role'] != role # Only deploy to the role we've specified (or all roles if none is specified)
60
-
61
- instructions = Foreplay::Utility::supermerge(defaults, additional_instructions).symbolize_keys
62
- instructions[:role] = role
63
- required_keys = [:name, :environment, :role, :servers, :path, :repository]
64
- required_keys.each { |key| terminate("Required key #{key} not found in instructions for #{environment} environment.\nCheck #{config_file}") unless instructions.has_key? key }
65
-
66
- deploy_role instructions
67
- end
68
-
69
- puts mode == :deploy ? 'Finished deployment' : 'Deployment configuration check was successful'
70
- end
71
-
72
- private
73
-
74
- def deploy_role instructions
75
- servers = instructions[:servers]
76
- preposition = mode == :deploy ? 'to' : 'for'
77
-
78
- puts "#{mode.capitalize}ing #{instructions[:name].yellow} #{preposition} #{servers.join(', ').yellow} for the #{instructions[:role].dup.yellow} role in the #{environment.dup.yellow} environment..." if servers.length > 1
79
- servers.each { |server| deploy_to_server server, instructions }
80
- end
81
-
82
- def deploy_to_server server, instructions
83
- name = instructions[:name]
84
- role = instructions[:role]
85
- path = instructions[:path]
86
- repository = instructions[:repository]
87
- user = instructions[:user]
88
- port = instructions[:port]
89
- preposition = mode == :deploy ? 'to' : 'for'
90
-
91
- instructions[:server] = server
92
-
93
- puts "#{mode.capitalize}ing #{name.yellow} #{preposition} #{server.yellow} for the #{role.dup.yellow} role in the #{environment.dup.yellow} environment"
94
-
95
- # Substitute variables in the path
96
- path.gsub! '%u', user
97
- path.gsub! '%a', name
98
-
99
- # Find out which port we're currently running on
100
- current_port_file = ".foreplay/#{name}/current_port"
101
- steps = [ { :command => "mkdir -p .foreplay/#{name} && touch #{current_port_file} && cat #{current_port_file}", :silent => true } ]
102
-
103
- current_port_string = execute_on_server(steps, instructions).strip!
104
- puts current_port_string.blank? ? "#{INDENT}No instance is currently deployed" : "#{INDENT}Current instance is using port #{current_port_string}"
105
-
106
- current_port = current_port_string.to_i
107
-
108
- # Switch ports
109
- if current_port == port
110
- current_port = port + 1000
111
- former_port = port
112
- else
113
- current_port = port
114
- former_port = port + 1000
115
- end
116
-
117
- # Contents of .foreman file
118
- current_service = '%s-%s' % [name, current_port]
119
- former_service = '%s-%s' % [name, former_port]
120
-
121
- instructions[:foreman]['app'] = current_service
122
- instructions[:foreman]['port'] = current_port
123
- instructions[:foreman]['user'] = user
124
-
125
- # Commands to execute on remote server
126
- steps = [
127
- { :command => "mkdir -p #{path} && cd #{path} && rm -rf #{current_port} && git clone #{repository} #{current_port}",
128
- :commentary => "Cloning repository #{repository}" },
129
- { :command => "rvm rvmrc trust #{current_port}",
130
- :commentary => 'Trusting the .rvmrc file for the new instance' },
131
- { :command => "rvm rvmrc warning ignore #{current_port}",
132
- :commentary => 'Ignoring the .rvmrc warning for the new instance' },
133
- { :command => "cd #{current_port}",
134
- :commentary => 'If you have a .rvmrc file there may be a delay now while we install a new ruby' },
135
- { :command => 'if [ -f .ruby-version ] ; then rvm install `cat .ruby-version` ; else echo "No .ruby-version file found" ; fi',
136
- :commentary => 'If you have a .ruby-version file there may be a delay now while we install a new ruby' },
137
- { :command => 'mkdir -p config',
138
- :commentary => "Making sure the config directory exists" },
139
- { :key => :env,
140
- :delimiter => '=',
141
- :prefix => '.',
142
- :commentary => 'Building .env' },
143
- { :key => :foreman,
144
- :delimiter => ': ',
145
- :prefix => '.',
146
- :commentary => 'Building .foreman' },
147
- { :key => :database,
148
- :delimiter => ': ',
149
- :suffix => '.yml',
150
- :commentary => 'Building config/database.yml',
151
- :before => ' ',
152
- :header => "#{environment}:",
153
- :path => 'config/' },
154
- { :key => :resque,
155
- :delimiter => ': ',
156
- :suffix => '.yml',
157
- :commentary => 'Building config/resque.yml',
158
- :before => environment,
159
- :path => 'config/' },
160
- { :command => 'bundle install --deployment --without development test',
161
- :commentary => 'Using bundler to install the required gems in deployment mode' },
162
- { :command => 'sudo ln -f `which foreman` /usr/bin/foreman || echo Using default version of foreman',
163
- :commentary => 'Setting the current version of foreman to be the default' },
164
- { :command => 'echo HOME="$HOME" >> .env',
165
- :commentary => 'Adding home path to .env (foreplay issue #443)' },
166
- { :command => 'echo SHELL="$SHELL" >> .env',
167
- :commentary => 'Adding shell path to .env (foreplay issue #443)' },
168
- { :command => 'echo PATH="$PATH:`which bundle`" >> .env',
169
- :commentary => 'Adding bundler path to .env (foreplay issue #443)' },
170
- { :command => 'sudo foreman export upstart /etc/init',
171
- :commentary => "Converting #{current_service} to an upstart service" },
172
- { :command => "sudo start #{current_service} || sudo restart #{current_service}",
173
- :commentary => 'Starting the service',
174
- :ignore_error => true },
175
- { :command => "echo #{current_port} > #{current_port_file}",
176
- :commentary => "Setting the port for the new instance to #{current_port}" },
177
- { :command => 'sleep 60',
178
- :commentary => 'Waiting 60s to give service time to start' },
179
- { :command => "sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{current_port}",
180
- :commentary => "Adding firewall rule to direct incoming traffic on port 80 to port #{current_port}" },
181
- { :command => "sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{former_port}",
182
- :commentary => "Removing previous firewall rule directing traffic to port #{former_port}",
183
- :ignore_error => true },
184
- { :command => 'sudo iptables-save > /etc/iptables/rules.v4',
185
- :commentary => 'Attempting to save firewall rules to /etc/iptables/rules.v4',
186
- :ignore_error => true },
187
- { :command => 'sudo iptables-save > /etc/iptables.up.rules',
188
- :commentary => 'Attempting to save firewall rules to /etc/iptables.up.rules',
189
- :ignore_error => true },
190
- { :command => 'sudo iptables-save -c | egrep REDIRECT --color=never',
191
- :ignore_error => true,
192
- :commentary => 'Current firewall NAT configuration:' },
193
- { :command => "sudo stop #{former_service} || echo 'No previous instance running'",
194
- :commentary => 'Stopping the previous instance',
195
- :ignore_error => true },
196
- ]
197
-
198
- execute_on_server steps, instructions
199
- end
200
-
201
- def execute_on_server steps, instructions
202
- server_port = instructions[:server]
203
- user = instructions[:user]
204
- password = instructions[:password]
205
- keyfile = instructions[:keyfile]
206
- private_key = instructions[:private_key]
207
-
208
- keyfile.sub! '~', ENV['HOME'] || '/' unless keyfile.blank? # Remote shell won't expand this for us
209
-
210
- # Parse server + port
211
- server, port = server_port.split(':')
212
- port ||= 22
213
-
214
- # SSH authentication methods
215
- options = { :verbose => :warn, :port => port }
216
-
217
- if password.blank?
218
- # If there's no password we must supply a private key
219
- if private_key.blank?
220
- terminate('No authentication methods supplied. You must supply a private key, key file or password in the configuration file') if keyfile.blank?
221
- # Get the key from the key file
222
- puts "#{INDENT}Using private key from #{keyfile}"
223
- private_key = File.read keyfile
224
- else
225
- puts "#{INDENT}Using private key from the configuration file"
226
- end
227
-
228
- options[:key_data] = [private_key]
229
- else
230
- # Use the password supplied
231
- options[:password] = password
232
- end
233
-
234
- # Capture output of last command to return to the calling routine
235
- output = ''
236
-
237
- if mode == :deploy
238
- puts "#{INDENT}Connecting to #{server} on port #{port}"
239
-
240
- # SSH connection
241
- begin
242
- Net::SSH.start(server, user, options) do |session|
243
- puts "#{INDENT}Successfully connected to #{server} on port #{port}"
244
-
245
- session.shell do |sh|
246
- steps.each do |step|
247
- # Output from this step
248
- output = ''
249
- previous = '' # We don't need or want the final CRLF
250
- commands = build_step step, instructions
251
-
252
- commands.each do |command|
253
- process = sh.execute command
254
-
255
- process.on_output do |p, o|
256
- previous = o
257
- output += previous
258
- end
259
-
260
- sh.wait!
261
-
262
- if step[:ignore_error] == true || process.exit_status == 0
263
- print output.gsub!(/^/, INDENT * 2) unless step[:silent] == true || output.blank?
264
- else
265
- terminate(output)
266
- end
267
- end
268
- end
269
- end
270
- end
271
- rescue SocketError => e
272
- terminate "There was a problem starting an ssh session on #{server_port}:\n#{e.message}"
273
- end
274
- else
275
- # Deployment check: just say what we would have done
276
- steps.each do |step|
277
- commands = build_step step, instructions
278
-
279
- commands.each { |command| puts "#{INDENT * 2}#{command}" unless step[:silent] }
280
- end
281
- end
282
-
283
- output
284
- end
285
-
286
- def build_step step, instructions
287
- puts "#{INDENT}#{(step[:commentary] || step[:command]).yellow}" unless step[:silent] == true
288
-
289
- # Each step can be (1) a command or (2) a series of values to add to a file
290
- if step.has_key?(:key)
291
- if instructions.has_key?(step[:key])
292
- build_commands step, instructions
293
- else
294
- []
295
- end
296
- else
297
- # ...or just execute the command specified
298
- [step[:command]]
299
- end
300
- end
301
-
302
- def build_commands step, instructions
303
- # Add values from the config file to a file on the remote machine
304
- key = step[:key]
305
- prefix = step[:prefix] || ''
306
- suffix = step[:suffix] || ''
307
- path = step[:path] || ''
308
- before = step[:before] || ''
309
- delimiter = step[:delimiter] || ''
310
- after = step[:after] || ''
311
-
312
- step[:silent] = true
313
- filename = '%s%s%s%s' % [path, prefix, key, suffix]
314
-
315
- if step.has_key?(:header)
316
- commands = ['echo "%s" > %s' % [step[:header], filename]]
317
- redirect = '>>'
318
- else
319
- commands = []
320
- redirect = '>'
321
- end
322
-
323
- if instructions[key].kind_of?(Hash)
324
- instructions[key].each do |k, v|
325
- commands << 'echo "%s%s%s%s%s" %s %s' % [before, k, delimiter, v, after, redirect, filename]
326
- redirect = '>>'
327
- end
328
- else
329
- commands << 'echo "%s%s%s%s" %s %s' % [before, delimiter, instructions[key], after, redirect, filename]
330
- redirect = '>>'
331
- end
332
-
333
- commands
334
- end
335
-
336
- def explanatory_text(hsh, key)
337
- hsh.has_key?(key) ? "#{hsh[key].dup.yellow} #{key}" : "all #{key.pluralize}"
338
- end
339
-
340
- def terminate(message)
341
- raise message
342
- end
343
- end
344
- end
1
+ require 'thor/group'
2
+ require 'yaml'
3
+ require 'net/ssh'
4
+ require 'net/ssh/shell'
5
+ require 'active_support/inflector'
6
+ require 'active_support/core_ext/object'
7
+ require 'active_support/core_ext/hash'
8
+ require 'colorize'
9
+ require 'foreplay/utility'
10
+
11
+ module Foreplay
12
+ class Deploy < Thor::Group
13
+ include Thor::Actions
14
+
15
+ argument :mode, type: :string, required: true
16
+ argument :environment, type: :string, required: true
17
+ argument :filters, type: :hash, required: false
18
+
19
+ DEFAULTS_KEY = 'defaults'
20
+ INDENT = ' ' * 4
21
+
22
+ def parse
23
+ # Explain what we're going to do
24
+ message = "#{mode.capitalize}ing #{environment.dup.yellow} environment, "
25
+ message += "#{explanatory_text(filters, 'role')}, #{explanatory_text(filters, 'server')}"
26
+ puts message
27
+
28
+ config_file = "#{Dir.getwd}/config/foreplay.yml"
29
+
30
+ begin
31
+ config_yml = File.read config_file
32
+ rescue Errno::ENOENT
33
+ terminate "Can't find configuration file #{config_file}.\nPlease run foreplay setup or create the file manually."
34
+ end
35
+
36
+ config_all = YAML.load(config_yml)
37
+ config_env = config_all[environment] || {}
38
+
39
+ # This environment
40
+ unless config_all.key? environment
41
+ terminate("No deployment configuration defined for #{environment} environment.\nCheck #{config_file}")
42
+ end
43
+
44
+ # Establish defaults
45
+ # First the default defaults
46
+ defaults = {
47
+ name: File.basename(Dir.getwd),
48
+ environment: environment,
49
+ env: { 'RAILS_ENV' => environment },
50
+ port: 50_000
51
+ }
52
+
53
+ defaults = Foreplay::Utility.supermerge(defaults, config_all[DEFAULTS_KEY]) if config_all.key? DEFAULTS_KEY
54
+ defaults = Foreplay::Utility.supermerge(defaults, config_env[DEFAULTS_KEY]) if config_env.key? DEFAULTS_KEY
55
+
56
+ config_env.each do |role, additional_instructions|
57
+ next if role == DEFAULTS_KEY # 'defaults' is not a role
58
+ # Only deploy to the role we've specified (or all roles if none is specified)
59
+ next if filters.key?('role') && filters['role'] != role
60
+
61
+ instructions = Foreplay::Utility.supermerge(defaults, additional_instructions).symbolize_keys
62
+ instructions[:role] = role
63
+ required_keys = [:name, :environment, :role, :servers, :path, :repository]
64
+
65
+ required_keys.each do |key|
66
+ next if instructions.key? key
67
+ terminate("Required key #{key} not found in instructions for #{environment} environment.\nCheck #{config_file}")
68
+ end
69
+
70
+ deploy_role instructions
71
+ end
72
+
73
+ puts mode == :deploy ? 'Finished deployment' : 'Deployment configuration check was successful'
74
+ end
75
+
76
+ private
77
+
78
+ def deploy_role(instructions)
79
+ servers = instructions[:servers]
80
+ preposition = mode == :deploy ? 'to' : 'for'
81
+
82
+ if servers.length > 1
83
+ message = "#{mode.capitalize}ing #{instructions[:name].yellow} #{preposition} #{servers.join(', ').yellow} for the "
84
+ message += "#{instructions[:role].dup.yellow} role in the #{environment.dup.yellow} environment..."
85
+ puts message
86
+ end
87
+
88
+ servers.each { |server| deploy_to_server server, instructions }
89
+ end
90
+
91
+ def deploy_to_server(server, instructions)
92
+ name = instructions[:name]
93
+ role = instructions[:role]
94
+ path = instructions[:path]
95
+ repository = instructions[:repository]
96
+ user = instructions[:user]
97
+ port = instructions[:port]
98
+ preposition = mode == :deploy ? 'to' : 'for'
99
+
100
+ instructions[:server] = server
101
+
102
+ message = "#{mode.capitalize}ing #{name.yellow} #{preposition} #{server.yellow} "
103
+ message += "for the #{role.dup.yellow} role in the #{environment.dup.yellow} environment"
104
+ puts message
105
+
106
+ # Substitute variables in the path
107
+ path.gsub! '%u', user
108
+ path.gsub! '%a', name
109
+
110
+ # Find out which port we're currently running on
111
+ current_port_file = ".foreplay/#{name}/current_port"
112
+ steps = [{ command: "mkdir -p .foreplay/#{name} && touch #{current_port_file} && cat #{current_port_file}", silent: true }]
113
+
114
+ current_port_string = execute_on_server(steps, instructions).strip!
115
+
116
+ if current_port_string.blank?
117
+ puts "#{INDENT}No instance is currently deployed"
118
+ else
119
+ "#{INDENT}Current instance is using port #{current_port_string}"
120
+ end
121
+
122
+ current_port = current_port_string.to_i
123
+
124
+ # Switch ports
125
+ if current_port == port
126
+ current_port = port + 1000
127
+ former_port = port
128
+ else
129
+ current_port = port
130
+ former_port = port + 1000
131
+ end
132
+
133
+ # Contents of .foreman file
134
+ current_service = "#{name}-#{current_port}"
135
+ former_service = "#{name}-#{former_port}"
136
+
137
+ instructions[:foreman]['app'] = current_service
138
+ instructions[:foreman]['port'] = current_port
139
+ instructions[:foreman]['user'] = user
140
+
141
+ # Commands to execute on remote server
142
+ steps = [
143
+ { command: "mkdir -p #{path} && cd #{path} && rm -rf #{current_port} && git clone #{repository} #{current_port}",
144
+ commentary: "Cloning repository #{repository}" },
145
+ { command: "rvm rvmrc trust #{current_port}",
146
+ commentary: 'Trusting the .rvmrc file for the new instance' },
147
+ { command: "rvm rvmrc warning ignore #{current_port}",
148
+ commentary: 'Ignoring the .rvmrc warning for the new instance' },
149
+ { command: "cd #{current_port}",
150
+ commentary: 'If you have a .rvmrc file there may be a delay now while we install a new ruby' },
151
+ { command: 'if [ -f .ruby-version ] ; then rvm install `cat .ruby-version` ; else echo "No .ruby-version file found" ; fi',
152
+ commentary: 'If you have a .ruby-version file there may be a delay now while we install a new ruby' },
153
+ { command: 'mkdir -p config',
154
+ commentary: 'Making sure the config directory exists' },
155
+ { key: :env,
156
+ delimiter: '=',
157
+ prefix: '.',
158
+ commentary: 'Building .env' },
159
+ { key: :foreman,
160
+ delimiter: ': ',
161
+ prefix: '.',
162
+ commentary: 'Building .foreman' },
163
+ { key: :database,
164
+ delimiter: ': ',
165
+ suffix: '.yml',
166
+ commentary: 'Building config/database.yml',
167
+ before: ' ',
168
+ header: "#{environment}:",
169
+ path: 'config/' },
170
+ { key: :resque,
171
+ delimiter: ': ',
172
+ suffix: '.yml',
173
+ commentary: 'Building config/resque.yml',
174
+ before: environment,
175
+ path: 'config/' },
176
+ { command: 'bundle install --deployment --without development test',
177
+ commentary: 'Using bundler to install the required gems in deployment mode' },
178
+ { command: 'sudo ln -f `which foreman` /usr/bin/foreman || echo Using default version of foreman',
179
+ commentary: 'Setting the current version of foreman to be the default' },
180
+ { command: 'echo HOME="$HOME" >> .env',
181
+ commentary: 'Adding home path to .env (foreplay issue #443)' },
182
+ { command: 'echo SHELL="$SHELL" >> .env',
183
+ commentary: 'Adding shell path to .env (foreplay issue #443)' },
184
+ { command: 'echo PATH="$PATH:`which bundle`" >> .env',
185
+ commentary: 'Adding bundler path to .env (foreplay issue #443)' },
186
+ { command: 'sudo foreman export upstart /etc/init',
187
+ commentary: "Converting #{current_service} to an upstart service" },
188
+ { command: "sudo start #{current_service} || sudo restart #{current_service}",
189
+ commentary: 'Starting the service',
190
+ ignore_error: true },
191
+ { command: "echo #{current_port} > $HOME/#{current_port_file}",
192
+ commentary: "Setting the port for the new instance to #{current_port}" },
193
+ { command: 'sleep 60',
194
+ commentary: 'Waiting 60s to give service time to start' },
195
+ { command: "sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{current_port}",
196
+ commentary: "Adding firewall rule to direct incoming traffic on port 80 to port #{current_port}" },
197
+ { command: "sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{former_port}",
198
+ commentary: "Removing previous firewall rule directing traffic to port #{former_port}",
199
+ ignore_error: true },
200
+ { command: 'sudo iptables-save > /etc/iptables/rules.v4',
201
+ commentary: 'Attempting to save firewall rules to /etc/iptables/rules.v4',
202
+ ignore_error: true },
203
+ { command: 'sudo iptables-save > /etc/iptables.up.rules',
204
+ commentary: 'Attempting to save firewall rules to /etc/iptables.up.rules',
205
+ ignore_error: true },
206
+ { command: 'sudo iptables-save -c | egrep REDIRECT --color=never',
207
+ ignore_error: true,
208
+ commentary: 'Current firewall NAT configuration:' },
209
+ { command: "sudo stop #{former_service} || echo 'No previous instance running'",
210
+ commentary: 'Stopping the previous instance',
211
+ ignore_error: true }
212
+ ]
213
+
214
+ execute_on_server steps, instructions
215
+ end
216
+
217
+ def execute_on_server(steps, instructions)
218
+ server_port = instructions[:server]
219
+ user = instructions[:user]
220
+ password = instructions[:password]
221
+ keyfile = instructions[:keyfile]
222
+ private_key = instructions[:private_key]
223
+
224
+ keyfile.sub! '~', ENV['HOME'] || '/' unless keyfile.blank? # Remote shell won't expand this for us
225
+
226
+ # Parse server + port
227
+ server, port = server_port.split(':')
228
+ port ||= 22
229
+
230
+ # SSH authentication methods
231
+ options = { verbose: :warn, port: port }
232
+
233
+ if password.blank?
234
+ # If there's no password we must supply a private key
235
+ if private_key.blank?
236
+ message = 'No authentication methods supplied. You must supply a private key, key file or password in the configuration file'
237
+ terminate(message) if keyfile.blank?
238
+ # Get the key from the key file
239
+ puts "#{INDENT}Using private key from #{keyfile}"
240
+ private_key = File.read keyfile
241
+ else
242
+ puts "#{INDENT}Using private key from the configuration file"
243
+ end
244
+
245
+ options[:key_data] = [private_key]
246
+ else
247
+ # Use the password supplied
248
+ options[:password] = password
249
+ end
250
+
251
+ # Capture output of last command to return to the calling routine
252
+ output = ''
253
+
254
+ if mode == :deploy
255
+ puts "#{INDENT}Connecting to #{server} on port #{port}"
256
+
257
+ # SSH connection
258
+ begin
259
+ Net::SSH.start(server, user, options) do |session|
260
+ puts "#{INDENT}Successfully connected to #{server} on port #{port}"
261
+
262
+ session.shell do |sh|
263
+ steps.each do |step|
264
+ # Output from this step
265
+ output = ''
266
+ previous = '' # We don't need or want the final CRLF
267
+ commands = build_step step, instructions
268
+
269
+ commands.each do |command|
270
+ process = sh.execute command
271
+
272
+ process.on_output do |_, o|
273
+ previous = o
274
+ output += previous
275
+ end
276
+
277
+ sh.wait!
278
+
279
+ if step[:ignore_error] == true || process.exit_status == 0
280
+ print output.gsub!(/^/, INDENT * 2) unless step[:silent] == true || output.blank?
281
+ else
282
+ terminate(output)
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+ rescue SocketError => e
289
+ terminate "There was a problem starting an ssh session on #{server_port}:\n#{e.message}"
290
+ end
291
+ else
292
+ # Deployment check: just say what we would have done
293
+ steps.each do |step|
294
+ commands = build_step step, instructions
295
+
296
+ commands.each { |command| puts "#{INDENT * 2}#{command}" unless step[:silent] }
297
+ end
298
+ end
299
+
300
+ output
301
+ end
302
+
303
+ def build_step(step, instructions)
304
+ puts "#{INDENT}#{(step[:commentary] || step[:command]).yellow}" unless step[:silent] == true
305
+
306
+ # Each step can be (1) a command or (2) a series of values to add to a file
307
+ if step.key?(:key)
308
+ if instructions.key?(step[:key])
309
+ build_commands step, instructions
310
+ else
311
+ []
312
+ end
313
+ else
314
+ # ...or just execute the command specified
315
+ [step[:command]]
316
+ end
317
+ end
318
+
319
+ def build_commands(step, instructions)
320
+ # Add values from the config file to a file on the remote machine
321
+ key = step[:key]
322
+ prefix = step[:prefix] || ''
323
+ suffix = step[:suffix] || ''
324
+ path = step[:path] || ''
325
+ before = step[:before] || ''
326
+ delimiter = step[:delimiter] || ''
327
+ after = step[:after] || ''
328
+
329
+ step[:silent] = true
330
+ filename = "#{path}#{prefix}#{key}#{suffix}"
331
+
332
+ if step.key?(:header)
333
+ commands = ["echo \"#{step[:header]}\" > #{filename}"]
334
+ redirect = '>>'
335
+ else
336
+ commands = []
337
+ redirect = '>'
338
+ end
339
+
340
+ if instructions[key].kind_of?(Hash)
341
+ instructions[key].each do |k, v|
342
+ commands << "echo \"#{before}#{k}#{delimiter}#{v}#{after}\" #{redirect} #{filename}"
343
+ redirect = '>>'
344
+ end
345
+ else
346
+ commands << "echo \"#{before}#{delimiter}#{instructions[key]}#{after}\" #{redirect} #{filename}"
347
+ redirect = '>>'
348
+ end
349
+
350
+ commands
351
+ end
352
+
353
+ def explanatory_text(hsh, key)
354
+ hsh.key?(key) ? "#{hsh[key].dup.yellow} #{key}" : "all #{key.pluralize}"
355
+ end
356
+
357
+ def terminate(message)
358
+ fail message
359
+ end
360
+ end
361
+ end