foreplay 0.1.5 → 0.1.6

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.
@@ -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