gaptool-client 0.8.0.pre.alpha4 → 0.8.0.pre.alpha5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3528f94e83fb0e8209a053b24683729e830b61ef
4
- data.tar.gz: 1f768dc1e8b37c94b289d00cce7e5935506adf13
3
+ metadata.gz: 0c7cf10926c2fbafec0a7d1a219d91563d0ef67c
4
+ data.tar.gz: b76e3252e52ff2d6ed3eeee06092c584fd97df5c
5
5
  SHA512:
6
- metadata.gz: 55aaebc78a856cfd8b3d92789507855c3b394a04c3d66ce2d6c3ad1283a80a5b5ab73773747a502b01f28f661a1574d36d199affb7f59855301c3f964a9d5310
7
- data.tar.gz: 63fbd7d33485fba03a7de11c19db639bf096cd6286ffa31085481fedeed1c62016ae616ad5da7ed2f2363455eaef24618163bc1ff215b51a672eae5a7f506886
6
+ metadata.gz: f66c772bbf3fc7f69473f10994421a2342840e7ea0da3a31c27974c845558cffcd525ffb58d8d6ae9a97f801476a3660b52eafdcac575c2142a9651bf6f04aea
7
+ data.tar.gz: a459d7f3e039c50962a3f098dde5d4e3703c2708edc9b643d81405ee30325be76b0c4e304c2ab18c8fe510ccb52230a796addc064471dc4af38191f7dab99e9b
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.0-alpha4
1
+ 0.8.0-alpha5
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
 
11
11
  s.rubyforge_project = 'gaptool-client'
12
12
 
13
- s.files = Dir['lib/*'] + %w(LICENSE.TXT README.md VERSION Rakefile gaptool-client.gemspec)
13
+ s.files = Dir['bin/*'] + Dir['lib/**/*'] + %w(LICENSE.TXT README.md VERSION Rakefile gaptool-client.gemspec)
14
14
  s.test_files = Dir['test/*']
15
15
  s.executables = Dir['bin/*'].map {|x| x.sub(/^bin\//, '')}
16
16
  s.require_paths = ['lib']
@@ -0,0 +1,37 @@
1
+ require 'gaptool-api'
2
+
3
+ module Gaptool
4
+ module API
5
+ def self.client
6
+ @client ||= GTAPI::GaptoolServer.new(
7
+ ENV['GT_USER'], ENV['GT_KEY'],
8
+ ENV['GT_URL'], ENV['GT_AWS_ZONE']
9
+ )
10
+ end
11
+
12
+ def self.query_nodes(opts)
13
+ instance = opts[:instance]
14
+ role = opts[:role]
15
+ environment = opts[:environment]
16
+ params = opts[:params]
17
+
18
+ if instance
19
+ puts Rainbow('Ignoring role and environment as instance is set').red \
20
+ if role || environment
21
+ [Gaptool::API.client.getonenode(instance)]
22
+ elsif role && environment
23
+ Gaptool::API.client.getenvroles(role, environment, params)
24
+ elsif role
25
+ Gaptool::API.client.getrolenodes(role, params)
26
+ elsif environment
27
+ Gaptool::API.client.getenvnodes(environment, params)
28
+ else
29
+ Gaptool::API.client.getallnodes(params)
30
+ end
31
+ end
32
+
33
+ def self.get_host(node)
34
+ "#{node['role']}-#{node['environment']}-#{node['instance']}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,404 @@
1
+ # coding: utf-8
2
+ # rubocop:disable Metrics/LineLength, Lint/Eval
3
+ #
4
+ require 'rainbow'
5
+ require 'json'
6
+ require 'clamp'
7
+ require 'set'
8
+ require 'gaptool_client/api'
9
+ require 'gaptool_client/helpers'
10
+ require 'gaptool_client/ssh'
11
+
12
+ module Gaptool
13
+ class InitCommand < Clamp::Command
14
+ option ['-r', '--role'], 'ROLE', 'Resource name to initilize', required: true
15
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true
16
+ option ['-z', '--zone'], 'ZONE', 'AWS availability zone to put node in', default: 'us-west-2c'
17
+ option ['-t', '--type'], 'TYPE', 'Type of instance, e.g. m1.large', required: true
18
+ option ['-s', '--security-group'], 'SECURITY_GROUP', 'Security group name. Defaults to $role-$environment'
19
+ option ['-a', '--ami'], 'AMI_ID', 'Use a specific AMI for the instance (i.e. ami-xxxxxxx)'
20
+ option ['-C', '--chef-repo'], 'GITURL', 'git url for the chef repository'
21
+ option ['-b', '--chef-branch'], 'BRANCH', 'branch of the chef repository to use'
22
+ option(['-R', '--chef_runlist'], 'RECIPE|ROLE',
23
+ 'override chef run_list. recipe[cb::recipe] or role[myrole]. Can be specified multiple times',
24
+ multivalued: true, attribute_name: 'chef_runlist')
25
+ option ['--no-terminate'], :flag, 'Add terminate protection'
26
+ def execute
27
+ no_terminate = no_terminate? ? true : nil
28
+ Gaptool::API.client.addnode(zone, type, role, environment, nil, security_group,
29
+ ami, chef_repo, chef_branch, chef_runlist,
30
+ no_terminate)
31
+ end
32
+ end
33
+
34
+ class TerminateCommand < Clamp::Command
35
+ option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678', required: true
36
+ option ['-z', '--zone'], 'ZONE', 'AWS region of the node (deprecated/ignored)'
37
+ option ['-r', '--role'], 'ROLE', 'Resource name to initilize', required: true
38
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true
39
+
40
+ def execute
41
+ node = Gaptool::API.client.getonenode(instance)
42
+ nodes = [node]
43
+ Gaptool::Helpers.info(nodes, false, false)
44
+ zone = node['zone'][0..-2]
45
+ if node['environment'] != environment || node['role'] != role
46
+ puts Rainbow("'#{node['role']}-#{node['environment']}' do not match provided value (#{role}-#{environment})").red
47
+ exit 1
48
+ end
49
+ if node['terminate'] == 'false'
50
+ puts Rainbow('"terminate" command is disabled for this instance').red
51
+ exit 2
52
+ end
53
+ print Rainbow('Terminate instance? [type yes to confirm]: ').green
54
+ res = $stdin.gets.chomp
55
+ return 0 unless res.downcase == 'yes'
56
+ puts "Terminating instance #{node['role']}:#{node['environment']}:#{node['instance']} in region #{zone}"
57
+ begin
58
+ Gaptool::API.client.terminatenode(instance, zone)
59
+ rescue
60
+ puts Rainbow('Cannot terminate instance').red
61
+ end
62
+ end
63
+ end
64
+
65
+ class RuncmdCommand < Clamp::Command
66
+ option ['-r', '--role'], 'ROLE', 'Instance role'
67
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment'
68
+ option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)'
69
+ option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.'
70
+ option ['-x', '--exclude-hidden'], :flag, 'Exclude hidden hosts'
71
+ option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail'
72
+ option ['-b', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE
73
+ parameter 'COMMAND ...', 'Command to run', attribute_name: :commands
74
+
75
+ def execute
76
+ params = exclude_hidden? ? {} : { hidden: true }
77
+ nodes = Gaptool::API.query_nodes(params.merge(instance: instance,
78
+ role: role, environment: environment))
79
+ res = Gaptool::SSH.exec(nodes, [commands.join(' ')],
80
+ serial: serial?, continue_on_errors: continue_on_errors?,
81
+ batch_size: batch_size)
82
+ exit res
83
+ end
84
+ end
85
+
86
+ class SSHConfigCommand < Clamp::Command
87
+ option ['-r', '--remove'], :flag, 'Remove ssh configuration'
88
+
89
+ def execute
90
+ config, content = Gaptool::SSH.config
91
+
92
+ if remove?
93
+ data = ''
94
+ else
95
+ puts Rainbow('Getting nodes from API').green
96
+ data = [Gaptool::SSH::START_MARKER, '# to remove this block, run gt ssh-config --remove']
97
+ Gaptool::API.client.getallnodes(hidden: true).sort_by { |n| n['instance'] }.each do |node|
98
+ host = Gaptool::API.get_host(node)
99
+ puts " - #{Rainbow(host).blue}"
100
+ data << Gaptool::SSH.config_for(node)
101
+ end
102
+ data << Gaptool::SSH::STOP_MARKER
103
+ data = data.join("\n")
104
+ end
105
+
106
+ if Regexp.new(Gaptool::SSH::START_MARKER).match(content)
107
+ content.gsub!(/#{Gaptool::SSH::START_MARKER}.*?#{Gaptool::SSH::STOP_MARKER}/m, data)
108
+ elsif !remove?
109
+ content = content + "\n" + data
110
+ end
111
+ File.open(config, 'w') { |f| f.write(content) }
112
+ end
113
+ end
114
+
115
+ class SSHCommand < Clamp::Command
116
+ option ['-r', '--role'], 'ROLE', 'Instance role'
117
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment'
118
+ option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)'
119
+ option ['-f', '--first'], :flag, 'Just connect to first available instance'
120
+ option ['-t', '--tmux'], :flag, 'No-op, DEPRECATED'
121
+
122
+ def execute
123
+ puts Rainbow('tmux support has been removed').yellow if tmux?
124
+ nodes = Gaptool::API.query_nodes(hidden: true,
125
+ instance: instance,
126
+ environment: environment,
127
+ role: role)
128
+
129
+ if first? || (nodes.length == 1 && !instance)
130
+ puts Rainbow('No instance specified, but only one instance in cluster or first forced').green
131
+ node = nodes.first
132
+ elsif !instance
133
+ nodes.each_index do |i|
134
+ puts "#{i}: #{nodes[i]['instance']}"
135
+ end
136
+ print Rainbow('Select a node: ').cyan
137
+ node = nodes[$stdin.gets.chomp.to_i]
138
+ error 'Invalid selection' if node.nil?
139
+ else
140
+ node = nodes.first
141
+ end
142
+ Gaptool::SSH.update_config_for(node)
143
+ system "ssh #{node['instance']}"
144
+ end
145
+ end
146
+
147
+ class InfoCommand < Clamp::Command
148
+ option ['-r', '--role'], 'ROLE', 'Role name, e.g. frontend'
149
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production'
150
+ option ['-i', '--instance'], 'INSTANCE', 'Node instance, leave blank to query avilable nodes'
151
+ option ['-s', '--short'], :flag, 'Only show summary for hosts'
152
+ option ['-p', '--parseable'], :flag, 'Display in non-pretty parseable JSON'
153
+ option ['-g', '--grepable'], :flag, 'Display in non-pretty grep-friendly text'
154
+ option ['-H', '--hidden'], :flag, 'Display hidden hosts'
155
+
156
+ def execute
157
+ params = hidden? ? { hidden: true } : {}
158
+ nodes = Gaptool::API.query_nodes(params.merge(instance: instance,
159
+ role: role,
160
+ environment: environment))
161
+ Gaptool::Helpers.info(nodes, parseable?, grepable?, short?)
162
+ end
163
+ end
164
+
165
+ class SetCommand < Clamp::Command
166
+ option ['-i', '--instance'], 'INSTANCE', 'Node instance, required', required: true
167
+ option ['-p', '--parseable'], :flag, 'Display in non-pretty parseable JSON'
168
+ option ['-g', '--grepable'], :flag, 'Display in non-pretty grep-friendly text'
169
+ option ['-k', '--parameter'], 'NAME', 'Set parameter for the node', required: true, multivalued: true
170
+ option ['-v', '--value'], 'VALUE', 'Value for parameter', required: true, multivalued: true
171
+
172
+ def convert_bool(v)
173
+ case v.downcase
174
+ when 'true'
175
+ true
176
+ when 'false'
177
+ false
178
+ else
179
+ v
180
+ end
181
+ end
182
+
183
+ def execute
184
+ if parameter_list.length != value_list.length
185
+ puts Rainbow('parameter and value length mismatch').red
186
+ end
187
+ params = Hash[parameter_list.each_with_index.map { |p, i| [p, convert_bool(value_list[i])] }]
188
+ Gaptool::Helpers.info([Gaptool::API.client.setparameters(instance, params)], parseable?, grepable?)
189
+ end
190
+ end
191
+
192
+ class ChefrunCommand < Clamp::Command
193
+ option ['-r', '--role'], 'ROLE', 'Role name to ssh to'
194
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production'
195
+ option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678'
196
+ option ['-H', '--hidden'], :flag, 'Include hidden hosts'
197
+ option(['-A', '--attribute'], 'ATTRIBUTE',
198
+ 'Pass one or more parameters to the deploy recipe in recipe.attr=value format',
199
+ multivalued: true)
200
+ option(['-b', '--chef-branch'], 'BRANCH',
201
+ 'branch of the chef repository to use (defaults to last branch used during init/chefrun)',
202
+ default: nil)
203
+ option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.'
204
+ option ['-W', '--whyrun'], :flag, 'Whyrun, like dry-run but different.'
205
+ option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail'
206
+ option ['-b', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE
207
+
208
+ def execute
209
+ attrs = Gaptool::Helpers.split_attrs(attribute_list)
210
+ nodes = Gaptool::API.query_nodes(hidden: hidden? ? true : nil,
211
+ role: role,
212
+ instance: instance,
213
+ environment: environment)
214
+
215
+ nodes = nodes.map do |x|
216
+ x['whyrun'] = whyrun?
217
+ x['chef_branch'] = chef_branch
218
+ x['attrs'] = attrs
219
+ x
220
+ end
221
+
222
+ pre_hook = proc do |node|
223
+ if node['chef_runlist'].nil?
224
+ runlist = ['recipe[main]']
225
+ elsif node['chef_runlist'].is_a? Array
226
+ runlist = node['chef_runlist']
227
+ else
228
+ runlist = eval(node['chef_runlist'])
229
+ end
230
+ json = {
231
+ 'this_server' => "#{node['role']}-#{node['environment']}-#{node['instance']}",
232
+ 'role' => node['role'],
233
+ 'environment' => node['environment'],
234
+ 'app_user' => node['appuser'],
235
+ 'run_list' => runlist,
236
+ 'hostname' => node['hostname'],
237
+ 'instance' => node['instance'],
238
+ 'zone' => node['zone'],
239
+ 'itype' => node['itype'],
240
+ 'apps' => eval(node['apps'] || '[]'),
241
+ 'gaptool' => {
242
+ 'user' => ENV['GT_USER'],
243
+ 'key' => ENV['GT_KEY'],
244
+ 'url' => ENV['GT_URL']
245
+ }
246
+ }.merge(node['attrs'])
247
+ git = 'sudo -u admin git'
248
+ pull = "#{git} fetch --all; #{git} reset --hard origin/`#{git} rev-parse --abbrev-ref HEAD`"
249
+ wopts = node['whyrun'] ? ' -W ' : ''
250
+
251
+ unless node['chef_branch'].nil?
252
+ json['chefbranch'] = node['chef_branch']
253
+ pull = "#{git} checkout -f #{node['chef_branch']}; #{git} fetch --all; #{git} reset --hard origin/#{node['chef_branch']}"
254
+ end
255
+ upload!(StringIO.new(json.to_json), '/tmp/chef.json')
256
+ script = <<-EOS
257
+ cd /var/data/admin/ops
258
+ #{pull}
259
+ sudo chef-solo -c /var/data/admin/ops/cookbooks/solo.rb -j /tmp/chef.json -E #{node['environment']}#{wopts}
260
+ rm -f /tmp/chef.json
261
+ EOS
262
+ upload!(StringIO.new(script), '/tmp/chef.sh')
263
+ end
264
+ res = Gaptool::SSH.exec(
265
+ nodes, ['chmod +x /tmp/chef.sh', '/tmp/chef.sh', 'rm -f /tmp/chef.sh'],
266
+ pre_hooks: [pre_hook], serial: serial?, continue_on_errors: continue_on_errors?,
267
+ batch_size: batch_size
268
+ )
269
+ exit res
270
+ end
271
+ end
272
+
273
+ class DeployCommand < Clamp::Command
274
+ option(['-a', '--app'], 'APP',
275
+ 'Application(s) to deploy (can be set multiple times)',
276
+ required: true, multivalued: true)
277
+ option ['-m', '--migrate'], :flag, 'Toggle running migrations'
278
+ option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true
279
+ option ['-b', '--branch'], 'BRANCH', 'Git branch to deploy, default is master'
280
+ option ['-r', '--rollback'], :flag, 'Toggle this to rollback last deploy'
281
+ option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678. If set, all applications MUST be hosted on this node.'
282
+ option ['-A', '--attribute'], 'ATTRIBUTE', 'Pass one or more parameters to the deploy recipe in recipe.attr=value format', multivalued: true
283
+ option ['-H', '--hidden'], :flag, 'Display hidden hosts'
284
+ option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.'
285
+ option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail'
286
+ option ['-b', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE
287
+
288
+ def execute # rubocop:disable Metrics/MethodLength
289
+ attrs = Gaptool::Helpers.split_attrs(attribute_list)
290
+ if instance
291
+ n = Gaptool::API.client.getonenode(instance)
292
+ if n['environment'] != environment
293
+ Gaptool::Helpers.error "Instance #{instance} is not in environment #{environment}"
294
+ else
295
+ app_list.each do |app|
296
+ Gaptool::Helpers.error "Instance #{instance} does not host #{app} in env #{environment}" \
297
+ unless n['apps'].include?(app)
298
+ end
299
+ end
300
+ nodes = [n]
301
+
302
+ else
303
+ params = hidden? ? { hidden: true } : {}
304
+ nodes = []
305
+ app_list.each do |app|
306
+ nodes.concat(Gaptool::API.client.getappnodes(app, environment, params))
307
+ end
308
+ end
309
+
310
+ # dedup nodes
311
+ seen = Set.new
312
+ app_set = Set.new(app_list)
313
+ nodes = nodes.select do |x|
314
+ res = !seen.include?(x['instance'])
315
+ seen << x['instance']
316
+ res
317
+ end
318
+ nodes = nodes.map do |x|
319
+ x['apps'] = eval(x['apps'])
320
+ x['apps_to_deploy'] = (Set.new(x['apps']) & app_set).to_a
321
+ x['rollback'] = rollback?
322
+ x['branch'] = branch || 'master'
323
+ x['migrate'] = migrate?
324
+ x['attrs'] = attrs
325
+ x
326
+ end
327
+
328
+ pre_hook = proc do |node|
329
+ host = "#{node['role']}:#{node['environment']}:#{node['instance']}"
330
+ puts "#{Rainbow('Deploying apps').cyan} '" + \
331
+ Rainbow(node['apps_to_deploy'].join(' ')).green + \
332
+ "' #{Rainbow('on').cyan} " + \
333
+ Rainbow(host).green
334
+
335
+ if node['chef_runlist'].nil?
336
+ runlist = ['recipe[deploy]']
337
+ elsif node['chef_runlist'].is_a? Array
338
+ runlist = node['chef_runlist']
339
+ else
340
+ runlist = eval(node['chef_runlist'])
341
+ end
342
+ json = {
343
+ 'this_server' => "#{node['role']}-#{node['environment']}-#{node['instance']}",
344
+ 'role' => node['role'],
345
+ 'environment' => node['environment'],
346
+ 'app_user' => node['appuser'],
347
+ 'run_list' => runlist,
348
+ 'hostname' => node['hostname'],
349
+ 'instance' => node['instance'],
350
+ 'zone' => node['zone'],
351
+ 'itype' => node['itype'],
352
+ 'apps' => node['apps'],
353
+ 'deploy_apps' => node['apps_to_deploy'],
354
+ 'rollback' => node['rollback'],
355
+ 'branch' => node['branch'],
356
+ 'migrate' => node['migrate'],
357
+ 'gaptool' => {
358
+ 'user' => ENV['GT_USER'],
359
+ 'key' => ENV['GT_KEY'],
360
+ 'url' => ENV['GT_URL']
361
+ }
362
+ }.merge(node['attrs']).to_json
363
+ upload!(StringIO.new(json), '/tmp/chef.json')
364
+ script = <<-EOS
365
+ cd /var/data/admin/ops
366
+ sudo -u admin git pull
367
+ sudo chef-solo -c /var/data/admin/ops/cookbooks/solo.rb -j /tmp/chef.json -E #{node['environment']}
368
+ rm -f /tmp/chef.json
369
+ EOS
370
+ upload!(StringIO.new(script), '/tmp/deploy.sh')
371
+ end
372
+
373
+ res = Gaptool::SSH.exec(
374
+ nodes,
375
+ ['chmod +x /tmp/deploy.sh', '/tmp/deploy.sh', 'rm -f /tmp/deploy.sh'],
376
+ pre_hooks: [pre_hook], serial: serial?, continue_on_errors: continue_on_errors?,
377
+ batch_size: batch_size
378
+ )
379
+ exit res
380
+ end
381
+ end
382
+
383
+ class RehashCommand < Clamp::Command
384
+ option ['-y', '--yes'], :flag, 'YES I REALLY WANT TO DO THIS'
385
+ def execute
386
+ if yes?
387
+ puts Gaptool::API.client.rehash
388
+ else
389
+ puts "You need to run this with -y\nIf you don't know what this does or aren't sure, DO NOT RUN IT\nThis will regenerate all host metadata on gaptool-server\nand can break in-progress operations."
390
+ end
391
+ end
392
+ end
393
+
394
+ class VersionCommand < Clamp::Command
395
+ option ['-r', '--remote'], :flag, 'Include remote API version'
396
+ def execute
397
+ version = File.read(File.realpath(File.join(File.dirname(__FILE__), '..', 'VERSION'))).strip
398
+ puts "gaptool-client #{version} using gaptool-api #{Gaptool::API.client.version}"
399
+ return 0 unless remote?
400
+ vinfo = Gaptool::API.client.api_version
401
+ puts "gaptool-server #{vinfo['server_version']}"
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,63 @@
1
+ require 'rainbow'
2
+
3
+ module Gaptool
4
+ module Helpers
5
+ def self.info(nodes, parseable, grepable, short = false)
6
+ if parseable && !short
7
+ puts({ nodes: nodes }.to_json)
8
+ elsif parseable
9
+ puts({ nodes: nodes.map { |node| node.select { |k, _v| %w(role environment instance).include?(k) } } }.to_json)
10
+ else
11
+ nodes.each do |node|
12
+ host = "#{node['role']}:#{node['environment']}:#{node['instance']}"
13
+ if grepable && short
14
+ puts host
15
+ elsif !grepable
16
+ puts Rainbow(host).green
17
+ end
18
+ next if short
19
+ keys = node.keys.sort
20
+ keys.each do |key|
21
+ value = node[key]
22
+ if grepable
23
+ puts "#{host}|#{key}|#{value}"
24
+ else
25
+ value = Time.at(node[key].to_i) if key == 'launch_time'
26
+ if key == keys.last
27
+ puts " ┖ #{Rainbow(key).cyan}: #{value}\n\n"
28
+ else
29
+ puts " ┠ #{Rainbow(key).cyan}: #{value}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.error(message, opts = {})
38
+ code = opts[:code] || 1
39
+ color = opts[:color] || :red
40
+ STDERR.puts(Rainbow(message).send(color))
41
+ exit code
42
+ end
43
+
44
+ def self.split_attrs(attribute_list)
45
+ opts = {}
46
+ attribute_list.each do |attr_|
47
+ key, value = attr_.split('=', 2)
48
+ split = key.split('.')
49
+ cur = opts
50
+ split.each_with_index do |part, idx|
51
+ if idx == split.size - 1
52
+ # leaf, add the value
53
+ cur[part] = value
54
+ else
55
+ cur[part] ||= {}
56
+ end
57
+ cur = cur[part]
58
+ end
59
+ end
60
+ opts
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ require 'rainbow'
2
+ require 'sshkit'
3
+ require 'gaptool_client/api'
4
+
5
+ module Gaptool
6
+ class InteractionHandler
7
+ attr_reader :host
8
+ def initialize(host)
9
+ @host = host
10
+ end
11
+
12
+ def on_data(_command, stream_name, data, _channel)
13
+ case stream_name
14
+ when :stdout
15
+ puts "#{Rainbow("#{@host}").yellow}> #{data}"
16
+ when :stderr
17
+ STDERR.puts "#{Rainbow("#{@host}").red}> #{data}"
18
+ end
19
+ end
20
+ end
21
+
22
+ class Host < SSHKit::Host
23
+ attr_reader :info
24
+
25
+ def initialize(node)
26
+ super(node['hostname'])
27
+ @info = node
28
+ end
29
+
30
+ def method_missing(attr)
31
+ @info[attr]
32
+ end
33
+
34
+ def name
35
+ @name ||= Gaptool::API.get_host(@info)
36
+ end
37
+
38
+ def handler
39
+ InteractionHandler.new(name)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,93 @@
1
+ require 'sshkit'
2
+ require 'thread'
3
+
4
+ module SSHKit
5
+ module Runner
6
+ class SafeSequential < Sequential
7
+ attr_reader :failed, :succeeded
8
+
9
+ def initialize(hosts, options = nil, &block)
10
+ options ||= {}
11
+ @on_errors = options.delete(:on_errors) || :exit
12
+ super(hosts, options, &block)
13
+ @failed = []
14
+ @succeeded = []
15
+ end
16
+
17
+ def execute
18
+ super
19
+ rescue
20
+ return false
21
+ else
22
+ return @failed.length == 0
23
+ end
24
+
25
+ private
26
+
27
+ def run_backend(host, &block)
28
+ backend(host, &block).run
29
+ rescue => e
30
+ @failed << { host: host, error: e }
31
+ raise if @on_errors == :exit
32
+ else
33
+ @succeeded << host
34
+ end
35
+ end
36
+
37
+ class SafeParallel < SafeSequential
38
+ attr_writer :group_size
39
+
40
+ def initialize(hosts, options = nil, &block)
41
+ super(hosts, options, &block)
42
+ @failed = Queue.new
43
+ @succeeded = Queue.new
44
+ end
45
+
46
+ def execute
47
+ hosts.each_slice(group_size).each do |slice|
48
+ threads = []
49
+ slice.each do |host|
50
+ threads << Thread.new(host) do |h|
51
+ begin
52
+ backend(h, &block).run
53
+ succeeded << host
54
+ rescue => e
55
+ failed << { host: host, error: e }
56
+ end
57
+ end
58
+ end
59
+ threads.map(&:join)
60
+ return convert_results if failed.length > 0 && @on_errors == :exit
61
+ sleep wait_interval
62
+ end
63
+ convert_results
64
+ end
65
+
66
+ private
67
+
68
+ def group_size
69
+ @group_size || options[:limit] || 2
70
+ end
71
+
72
+ def convert_results
73
+ @failed = queue_to_array(failed)
74
+ @succeeded = queue_to_array(succeeded)
75
+ return true if failed.length == 0
76
+ end
77
+
78
+ def queue_to_array(queue)
79
+ res = []
80
+ begin
81
+ loop do
82
+ obj = queue.pop(true)
83
+ break unless obj
84
+ res << obj
85
+ end
86
+ rescue ThreadError
87
+ return res
88
+ end
89
+ res
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,135 @@
1
+ require 'logger'
2
+ require 'rainbow'
3
+ require 'sshkit'
4
+ require 'gaptool_client/runner'
5
+ require 'gaptool_client/host'
6
+ # rubocop:disable Metrics/LineLength
7
+
8
+ module Gaptool
9
+ module SSH
10
+ BATCH_SIZE = 10
11
+ START_MARKER = '###### GAPTOOL ######'
12
+ STOP_MARKER = '###### END GAPTOOL ######'
13
+
14
+ def self.config_for(node)
15
+ host = Gaptool::API.get_host(node)
16
+ <<-EOF
17
+ # -- #{node['instance']}
18
+ Host #{host} #{node['instance']}
19
+ Hostname #{node['hostname']}
20
+ User #{ENV['GT_USER']}
21
+ LogLevel FATAL
22
+ PreferredAuthentications publickey
23
+ CheckHostIP no
24
+ StrictHostKeyChecking no
25
+ UserKnownHostsFile /dev/null
26
+ # -- end #{node['instance']}
27
+ EOF
28
+ end
29
+
30
+ def self.config
31
+ config = File.join(Dir.home, '.ssh', 'config')
32
+ dir = File.dirname(config)
33
+ parent = File.join(dir, '..')
34
+ if File.exist?(config)
35
+ Gaptool::Helpers.error "#{config}: not writable" unless File.writable?(config)
36
+ content = File.read(config)
37
+ File.open("#{config}.bck", 'w') { |f| f.write(content) }
38
+ else
39
+ if !File.exist?(dir)
40
+ Gaptool::Helpers.error "Home directory #{parent} does not exists"\
41
+ unless Dir.exist?(parent)
42
+ Dir.mkdir(dir, 0700)
43
+ elsif !File.directory?(dir)
44
+ Gaptool::Helpers.error "#{dir}: not a directory"
45
+ end
46
+ content = ''
47
+ end
48
+ [config, content]
49
+ end
50
+
51
+ def self.update_config_for(nodes, verbose = true)
52
+ nodes = [nodes] unless nodes.is_a?(Array)
53
+ config, content = Gaptool::SSH.config
54
+ nodes.each do |node|
55
+ snip = Gaptool::SSH.config_for(node)
56
+ mark = "# -- #{node['instance']}"
57
+ emark = "# -- end #{node['instance']}"
58
+
59
+ if Regexp.new(mark).match(content)
60
+ puts Rainbow("Updating ssh config for #{get_host(node)}").green if verbose
61
+ content.gsub!(/#{mark}.*?#{emark}/m, snip.strip)
62
+ elsif Regexp.new(STOP_MARKER).match(content)
63
+ puts Rainbow("Adding ssh config for #{get_host(node)}").green if verbose
64
+ content.gsub!(/#{STOP_MARKER}/m, snip + "\n" + STOP_MARKER)
65
+ else
66
+ puts Rainbow('No gt ssh config found: please run gt ssh-config').yellow
67
+ puts Rainbow("Adding ssh config for #{get_host(node)}").green if verbose
68
+ content = <<-EOF
69
+ #{content}
70
+ #{START_MARKER}
71
+ #{snip}
72
+ #{STOP_MARKER}
73
+ EOF
74
+ end
75
+ end
76
+ File.open(config, 'w') { |f| f.write(content) }
77
+ end
78
+
79
+ def self.configure_sshkit
80
+ SSHKit.config.output_verbosity = Logger::WARN
81
+ SSHKit::Backend::Netssh.configure do |ssh|
82
+ ssh.connection_timeout = 30
83
+ ssh.ssh_options = {
84
+ forward_agent: true,
85
+ global_known_hosts_file: '/dev/null',
86
+ keys_only: true,
87
+ port: 22,
88
+ user: ENV['GT_USER']
89
+ }
90
+ end
91
+ end
92
+
93
+ def self.exec(nodes, commands, opts = {})
94
+ logger = SSHKit::Formatter::Pretty.new(STDERR)
95
+ serial = opts[:serial] || nodes.length == 1
96
+ if opts[:update_ssh_config].nil? || opts[:update_ssh_config]
97
+ nodes.each { |n| Gaptool::SSH.update_config_for(n, false) }
98
+ end
99
+ pre = opts[:pre_hooks] || []
100
+ post = opts[:post_hooks] || []
101
+ opts = {
102
+ limit: opts[:batch_size].to_i || BATCH_SIZE,
103
+ on_errors: opts[:continue_on_errors] ? :continue : :exit,
104
+ wait: opts[:wait] || 0
105
+ }
106
+ Gaptool::SSH.configure_sshkit
107
+ runner_cls = if serial
108
+ SSHKit::Runner::SafeSequential
109
+ else
110
+ SSHKit::Runner::SafeParallel
111
+ end
112
+ hosts = nodes.map { |n| Gaptool::Host.new(n) }
113
+ runner = runner_cls.new(hosts, opts) do |host|
114
+ pre.each { |h| instance_exec(host, &h) }
115
+ commands.each do |cmd|
116
+ execute(:bash, "-l -c '#{cmd}'",
117
+ interaction_handler: host.handler)
118
+ end
119
+ post.each { |h| instance_exec(host, &h) }
120
+ end
121
+ res = runner.execute
122
+
123
+ unless res
124
+ logger.error("#{runner.failed.length}/#{nodes.length} hosts failed")
125
+ runner.failed.each do |desc|
126
+ host = desc[:host]
127
+ error = desc[:error]
128
+ logger.error("#{host.name}> #{error.message}")
129
+ end
130
+ return runner.failed.length + 100
131
+ end
132
+ 0
133
+ end
134
+ end
135
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gaptool-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0.pre.alpha4
4
+ version: 0.8.0.pre.alpha5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francesco Laurita
@@ -103,6 +103,12 @@ files:
103
103
  - bin/gt
104
104
  - gaptool-client.gemspec
105
105
  - lib/gaptool_client.rb
106
+ - lib/gaptool_client/api.rb
107
+ - lib/gaptool_client/commands.rb
108
+ - lib/gaptool_client/helpers.rb
109
+ - lib/gaptool_client/host.rb
110
+ - lib/gaptool_client/runner.rb
111
+ - lib/gaptool_client/ssh.rb
106
112
  homepage: http://www.gild.com
107
113
  licenses:
108
114
  - MIT