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

Sign up to get free protection for your applications and to get access to all the features.
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