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 +4 -4
- data/VERSION +1 -1
- data/gaptool-client.gemspec +1 -1
- data/lib/gaptool_client/api.rb +37 -0
- data/lib/gaptool_client/commands.rb +404 -0
- data/lib/gaptool_client/helpers.rb +63 -0
- data/lib/gaptool_client/host.rb +42 -0
- data/lib/gaptool_client/runner.rb +93 -0
- data/lib/gaptool_client/ssh.rb +135 -0
- metadata +7 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c7cf10926c2fbafec0a7d1a219d91563d0ef67c
|
4
|
+
data.tar.gz: b76e3252e52ff2d6ed3eeee06092c584fd97df5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f66c772bbf3fc7f69473f10994421a2342840e7ea0da3a31c27974c845558cffcd525ffb58d8d6ae9a97f801476a3660b52eafdcac575c2142a9651bf6f04aea
|
7
|
+
data.tar.gz: a459d7f3e039c50962a3f098dde5d4e3703c2708edc9b643d81405ee30325be76b0c4e304c2ab18c8fe510ccb52230a796addc064471dc4af38191f7dab99e9b
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.8.0-
|
1
|
+
0.8.0-alpha5
|
data/gaptool-client.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
|
11
11
|
s.rubyforge_project = 'gaptool-client'
|
12
12
|
|
13
|
-
s.files = Dir['
|
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.
|
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
|