dust-deploy 0.13.18 → 0.14.0
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.
- data/bin/dust +1 -405
- data/changelog.md +9 -0
- data/lib/dust.rb +1 -0
- data/lib/dust/recipe.rb +2 -0
- data/lib/dust/recipes/nginx.rb +0 -1
- data/lib/dust/recipes/ntpd.rb +1 -1
- data/lib/dust/recipes/sudoers.rb +18 -9
- data/lib/dust/runner.rb +396 -0
- data/lib/dust/server.rb +31 -23
- data/lib/dust/version.rb +1 -1
- metadata +3 -2
data/bin/dust
CHANGED
@@ -1,408 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'rubygems'
|
4
|
-
require 'thor/runner'
|
5
|
-
require 'thor/util'
|
6
|
-
require 'yaml'
|
7
|
-
require 'erb'
|
8
|
-
require 'fileutils'
|
9
|
-
require 'ipaddress'
|
10
3
|
require 'dust'
|
11
|
-
|
12
|
-
|
13
|
-
module Dust
|
14
|
-
class Deploy < Thor::Runner
|
15
|
-
|
16
|
-
default_task :list
|
17
|
-
check_unknown_options!
|
18
|
-
|
19
|
-
desc 'deploy', 'deploy all recipes to the node(s) specified in server.yaml or to all nodes defined in ./nodes/'
|
20
|
-
|
21
|
-
method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
|
22
|
-
method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
|
23
|
-
method_option 'recipes', :type => :array, :desc => 'only deploy these recipes'
|
24
|
-
method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
|
25
|
-
method_option 'restart', :type => :boolean, :desc => 'restart services after deploy'
|
26
|
-
method_option 'reload', :type => :boolean, :desc => 'reload services after deploy'
|
27
|
-
method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
|
28
|
-
method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
|
29
|
-
|
30
|
-
def deploy
|
31
|
-
return unless check_dust_dir
|
32
|
-
initialize_thorfiles
|
33
|
-
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
34
|
-
|
35
|
-
# set global variables
|
36
|
-
$summary = options['summary']
|
37
|
-
$parallel = options['parallel']
|
38
|
-
$summary = 'all' if $parallel and not $summary
|
39
|
-
|
40
|
-
threads = []
|
41
|
-
@nodes.each_with_index do |node, i|
|
42
|
-
if $parallel
|
43
|
-
threads[i] = Thread.new do
|
44
|
-
Thread.current['hostname'] = node['hostname'] if run_recipes node, 'deploy'
|
45
|
-
end
|
46
|
-
else
|
47
|
-
run_recipes node, 'deploy'
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
if $parallel
|
52
|
-
print 'waiting for servers: '
|
53
|
-
threads.each do |t|
|
54
|
-
t.join # wait for thread
|
55
|
-
print t['hostname'].blue + ' ' if t['hostname']
|
56
|
-
end
|
57
|
-
puts
|
58
|
-
end
|
59
|
-
|
60
|
-
display_summary($summary) if $summary
|
61
|
-
end
|
62
|
-
|
63
|
-
|
64
|
-
desc 'status', 'display status of recipes specified by filter'
|
65
|
-
|
66
|
-
method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
|
67
|
-
method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
|
68
|
-
method_option 'recipes', :type => :array, :desc => 'only deploy these recipes'
|
69
|
-
method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
|
70
|
-
method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
|
71
|
-
method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
|
72
|
-
|
73
|
-
def status
|
74
|
-
return unless check_dust_dir
|
75
|
-
initialize_thorfiles
|
76
|
-
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
77
|
-
|
78
|
-
# set global variables
|
79
|
-
$summary = options['summary']
|
80
|
-
$parallel = options['parallel']
|
81
|
-
$summary = 'all' if $parallel and not $summary
|
82
|
-
|
83
|
-
threads = []
|
84
|
-
@nodes.each_with_index do |node, i|
|
85
|
-
if $parallel
|
86
|
-
threads[i] = Thread.new do
|
87
|
-
Thread.current['hostname'] = node['hostname'] if run_recipes node, 'status'
|
88
|
-
end
|
89
|
-
else
|
90
|
-
run_recipes node, 'status'
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
if $parallel
|
95
|
-
print 'waiting for servers: '
|
96
|
-
threads.each do |t|
|
97
|
-
t.join # wait for thread
|
98
|
-
print t['hostname'].blue + ' ' if t['hostname']
|
99
|
-
end
|
100
|
-
puts
|
101
|
-
end
|
102
|
-
|
103
|
-
display_summary($summary) if $summary
|
104
|
-
end
|
105
|
-
|
106
|
-
|
107
|
-
desc 'system_update', 'perform a full system upgrade (using aptitude, emerge, yum)'
|
108
|
-
|
109
|
-
method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
|
110
|
-
method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
|
111
|
-
method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
|
112
|
-
method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
|
113
|
-
method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
|
114
|
-
|
115
|
-
def system_update
|
116
|
-
return unless check_dust_dir
|
117
|
-
initialize_thorfiles
|
118
|
-
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
119
|
-
|
120
|
-
# set global variables
|
121
|
-
$summary = options['summary']
|
122
|
-
$parallel = options['parallel']
|
123
|
-
$summary = 'all' if $parallel and not $summary
|
124
|
-
|
125
|
-
threads = []
|
126
|
-
@nodes.each_with_index do |node, i|
|
127
|
-
if $parallel
|
128
|
-
threads[i] = Thread.new do
|
129
|
-
run_system_update(node)
|
130
|
-
Thread.current['hostname'] = node['hostname']
|
131
|
-
end
|
132
|
-
else
|
133
|
-
run_system_update(node)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
if $parallel
|
138
|
-
print 'waiting for servers: '
|
139
|
-
threads.each do |t|
|
140
|
-
t.join # wait for thread
|
141
|
-
print t['hostname'].blue + ' ' if t['hostname']
|
142
|
-
end
|
143
|
-
puts
|
144
|
-
end
|
145
|
-
|
146
|
-
display_summary($summary) if $summary
|
147
|
-
end
|
148
|
-
|
149
|
-
|
150
|
-
desc 'exec <command>', 'run a command on the server'
|
151
|
-
|
152
|
-
method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
|
153
|
-
method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
|
154
|
-
method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
|
155
|
-
method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
|
156
|
-
method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
|
157
|
-
|
158
|
-
def exec cmd, yaml=''
|
159
|
-
return unless check_dust_dir
|
160
|
-
initialize_thorfiles
|
161
|
-
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
162
|
-
|
163
|
-
# set global variables
|
164
|
-
$summary = options['summary']
|
165
|
-
$parallel = options['parallel']
|
166
|
-
$summary = 'all' if $parallel and not $summary
|
167
|
-
|
168
|
-
threads = []
|
169
|
-
@nodes.each_with_index do |node, i|
|
170
|
-
if $parallel
|
171
|
-
threads[i] = Thread.new do
|
172
|
-
run_exec(node, cmd)
|
173
|
-
Thread.current['hostname'] = node['hostname']
|
174
|
-
end
|
175
|
-
else
|
176
|
-
run_exec(node, cmd)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
if $parallel
|
181
|
-
print 'waiting for servers: '
|
182
|
-
threads.each do |t|
|
183
|
-
t.join # wait for thread
|
184
|
-
print t['hostname'].blue + ' ' if t['hostname']
|
185
|
-
end
|
186
|
-
puts
|
187
|
-
end
|
188
|
-
|
189
|
-
display_summary($summary) if $summary
|
190
|
-
end
|
191
|
-
|
192
|
-
|
193
|
-
# creates directory skeleton for a dust setup
|
194
|
-
desc 'new <name>', 'creates a dust directory skeleton for your network'
|
195
|
-
def new name
|
196
|
-
Dust.print_msg "spawning new dust directory skeleton with examples into '#{name}.dust'"
|
197
|
-
FileUtils.cp_r File.dirname(__FILE__) + '/../lib/dust/examples', "#{name}.dust"
|
198
|
-
Dust.print_ok
|
199
|
-
end
|
200
|
-
|
201
|
-
desc 'version', 'displays version number'
|
202
|
-
def version
|
203
|
-
puts "dust-deploy-#{Dust::VERSION}, running on ruby-#{RUBY_VERSION}"
|
204
|
-
end
|
205
|
-
|
206
|
-
|
207
|
-
private
|
208
|
-
|
209
|
-
def check_dust_dir
|
210
|
-
if Dir.pwd.split('.').last != 'dust'
|
211
|
-
Dust.print_failed 'current directory does not end with .dust, are you in your dust directory?'
|
212
|
-
Dust.print_msg "try running 'dust new mynetwork' to let me create one for you with tons of examples!\n", :indent => 0
|
213
|
-
return false
|
214
|
-
end
|
215
|
-
|
216
|
-
unless File.directory? './nodes'
|
217
|
-
Dust.print_failed 'could not find \'nodes\' folder in your dust directory. cannot continue.'
|
218
|
-
return false
|
219
|
-
end
|
220
|
-
|
221
|
-
true
|
222
|
-
end
|
223
|
-
|
224
|
-
# run specified recipes in the given context
|
225
|
-
# returns false if no recipes where found
|
226
|
-
# true if recipes were run (doesn't indicate, whether the run was sucessful or not)
|
227
|
-
def run_recipes(node, context)
|
228
|
-
# skip this node if there are no recipes found
|
229
|
-
return false unless node['recipes']
|
230
|
-
|
231
|
-
recipes = generate_recipes node, context
|
232
|
-
|
233
|
-
# skip this node unless we're actually having recipes to cook
|
234
|
-
return false if recipes.empty?
|
235
|
-
|
236
|
-
# connect to server
|
237
|
-
node['server'] = Server.new(node)
|
238
|
-
return true unless node['server'].connect
|
239
|
-
|
240
|
-
# runs the method with the recipe name, defined and included in recipe/*.rb
|
241
|
-
# call recipes for each recipe that is defined for this node
|
242
|
-
recipes.each do |recipe, config|
|
243
|
-
send recipe, 'prepare', node['server'], recipe, context, config, options
|
244
|
-
end
|
245
|
-
|
246
|
-
node['server'].disconnect
|
247
|
-
true
|
248
|
-
end
|
249
|
-
|
250
|
-
def run_system_update(node)
|
251
|
-
node['server'] = Server.new(node)
|
252
|
-
return unless node['server'].connect
|
253
|
-
node['server'].system_update
|
254
|
-
node['server'].disconnect
|
255
|
-
end
|
256
|
-
|
257
|
-
def run_exec(node, cmd)
|
258
|
-
node['server'] = Server.new(node)
|
259
|
-
return unless node['server'].connect
|
260
|
-
node['server'].exec(cmd, :live => true)
|
261
|
-
node['server'].disconnect
|
262
|
-
end
|
263
|
-
|
264
|
-
# generate list of recipes for this node
|
265
|
-
def generate_recipes node, context
|
266
|
-
recipes = {}
|
267
|
-
node['recipes'].each do |recipe, config|
|
268
|
-
|
269
|
-
# in case --recipes was set, skip unwanted recipes
|
270
|
-
next unless options['recipes'].include?(recipe) if options['recipes']
|
271
|
-
|
272
|
-
# skip disabled recipes
|
273
|
-
next if config == 'disabled' or config.is_a? FalseClass
|
274
|
-
|
275
|
-
# check if method and thor task actually exist
|
276
|
-
k = Thor::Util.find_by_namespace recipe
|
277
|
-
next unless k
|
278
|
-
next unless k.method_defined? context
|
279
|
-
|
280
|
-
recipes[recipe] = config
|
281
|
-
end
|
282
|
-
recipes
|
283
|
-
end
|
284
|
-
|
285
|
-
def display_summary(level)
|
286
|
-
puts "\n\n------------------------------ SUMMARY ------------------------------".red unless $parallel
|
287
|
-
|
288
|
-
@nodes.each do |node|
|
289
|
-
next unless node['server']
|
290
|
-
|
291
|
-
messages = node['server'].messages.collect(level)
|
292
|
-
next if messages.empty?
|
293
|
-
|
294
|
-
node['server'].messages.print_hostname_header(node['hostname'])
|
295
|
-
|
296
|
-
# display non-recipe messages first
|
297
|
-
msgs = messages.delete '_node'
|
298
|
-
msgs.each { |m| print m } if msgs
|
299
|
-
|
300
|
-
# display messages from recipes
|
301
|
-
messages.each do |recipe, msgs|
|
302
|
-
node['server'].messages.print_recipe_header(recipe)
|
303
|
-
msgs.each { |m| print m }
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
# overwrite thorfiles to look for tasks in the recipes directories
|
309
|
-
def thorfiles(relevant_to=nil, skip_lookup=false)
|
310
|
-
Dir[File.dirname(__FILE__) + '/../lib/dust/recipes/*.rb'] | Dir['recipes/*.rb']
|
311
|
-
end
|
312
|
-
|
313
|
-
# loads servers
|
314
|
-
def load_servers
|
315
|
-
@nodes = []
|
316
|
-
|
317
|
-
# if the argument is empty, load all yaml files in the ./nodes/ directory
|
318
|
-
# if the argument is a directory, load yaml files in this directory
|
319
|
-
# if the argument is a file, load the file.
|
320
|
-
if options['yaml']
|
321
|
-
if File.directory? options['yaml']
|
322
|
-
yaml_files = Dir["#{options['yaml']}/**/*.yaml"]
|
323
|
-
elsif File.exists? options['yaml']
|
324
|
-
yaml_files = options['yaml']
|
325
|
-
end
|
326
|
-
else
|
327
|
-
yaml_files = Dir['./nodes/**/*.yaml']
|
328
|
-
end
|
329
|
-
|
330
|
-
unless yaml_files
|
331
|
-
Dust.print_failed "#{yaml} doesn't exist. exiting."
|
332
|
-
exit
|
333
|
-
end
|
334
|
-
|
335
|
-
yaml_files.to_array.each do |file|
|
336
|
-
node = YAML.load ERB.new( File.read(file), nil, '%<>').result
|
337
|
-
|
338
|
-
# if the file is empty, just skip it
|
339
|
-
next unless node
|
340
|
-
|
341
|
-
# if there is not hostname field in the yaml file,
|
342
|
-
# treat this node file as a template, and skip to the next one
|
343
|
-
next unless node['hostname']
|
344
|
-
|
345
|
-
# look for the inherits field in the yaml file,
|
346
|
-
# and merge the templates recursively into this node
|
347
|
-
if node['inherits']
|
348
|
-
inherited = {}
|
349
|
-
node.delete('inherits').each do |file|
|
350
|
-
template = YAML.load ERB.new( File.read("./nodes/#{file}.yaml"), nil, '%<>').result
|
351
|
-
inherited.deep_merge! template
|
352
|
-
end
|
353
|
-
node = inherited.deep_merge node
|
354
|
-
end
|
355
|
-
|
356
|
-
# if more than one hostname is specified, create a node
|
357
|
-
# with the same settings for each hostname
|
358
|
-
node['hostname'].to_array.each do |hostname|
|
359
|
-
n = node.clone
|
360
|
-
|
361
|
-
# overwrite hostname with single hostname (in case there are multiple)
|
362
|
-
n['hostname'] = hostname
|
363
|
-
|
364
|
-
# create a new field with the fully qualified domain name
|
365
|
-
n['fqdn'] = hostname
|
366
|
-
|
367
|
-
# if hostname is a valid ip address, don't add domain
|
368
|
-
# so we can connect via ip address only
|
369
|
-
unless IPAddress.valid? hostname
|
370
|
-
n['fqdn'] += '.' + n['domain'] if n['domain']
|
371
|
-
end
|
372
|
-
|
373
|
-
# pass command line proxy option
|
374
|
-
n['proxy'] = options['proxy'] if options['proxy']
|
375
|
-
|
376
|
-
# add this node to the global node array
|
377
|
-
@nodes.push n unless filtered? n
|
378
|
-
end
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
# checks if this node was filtered out by command line argument
|
383
|
-
# e.g. --filter environment:staging filters out all machines but
|
384
|
-
# those in the environment staging
|
385
|
-
def filtered? node
|
386
|
-
|
387
|
-
# if filter is not specified, instantly return false
|
388
|
-
return false unless options['filter']
|
389
|
-
|
390
|
-
# remove items if other filter arguments don't match
|
391
|
-
options['filter'].each do |k, v|
|
392
|
-
next unless v # skip empty filters
|
393
|
-
|
394
|
-
# filter if this node doesn't even have the attribute
|
395
|
-
return true unless node[k]
|
396
|
-
|
397
|
-
# allow multiple filters of the same type, divided by ','
|
398
|
-
# e.g. --filter environment:staging,production
|
399
|
-
return true unless v.split(',').include? node[k]
|
400
|
-
end
|
401
|
-
|
402
|
-
# no filter matched, so this host is not filtered.
|
403
|
-
false
|
404
|
-
end
|
405
|
-
end
|
406
|
-
|
407
|
-
Deploy.start
|
408
|
-
end
|
4
|
+
Dust::Runner.start
|
data/changelog.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
Changelog
|
2
2
|
=============
|
3
3
|
|
4
|
+
0.14.0
|
5
|
+
------------
|
6
|
+
|
7
|
+
- migrates to new runner.rb
|
8
|
+
- fixes bug in print_service_status
|
9
|
+
- checks if sudo password is wrong (and raises an error)
|
10
|
+
- fixes sudoers recipe when using with sudo (was deleting its own rules)
|
11
|
+
|
12
|
+
|
4
13
|
0.13.18
|
5
14
|
------------
|
6
15
|
|
data/lib/dust.rb
CHANGED
data/lib/dust/recipe.rb
CHANGED
data/lib/dust/recipes/nginx.rb
CHANGED
data/lib/dust/recipes/ntpd.rb
CHANGED
@@ -4,7 +4,7 @@ class Ntpd < Recipe
|
|
4
4
|
# warn if other ntp package is installed
|
5
5
|
[ 'openntpd', 'chrony' ].each do |package|
|
6
6
|
if @node.package_installed? package, :quiet => true
|
7
|
-
@node.messages.add("#{package} installed, might conflict with ntpd, might
|
7
|
+
@node.messages.add("#{package} installed, might conflict with ntpd, might get deleted").warning
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
data/lib/dust/recipes/sudoers.rb
CHANGED
@@ -3,8 +3,6 @@ class Sudoers < Recipe
|
|
3
3
|
def deploy
|
4
4
|
return unless @node.install_package 'sudo'
|
5
5
|
|
6
|
-
remove_rules
|
7
|
-
|
8
6
|
@config.each do |name, rule|
|
9
7
|
@node.messages.add("deploying sudo rules '#{name}'\n")
|
10
8
|
|
@@ -25,21 +23,32 @@ class Sudoers < Recipe
|
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
28
|
-
deploy_rule
|
26
|
+
deploy_rule(name, file)
|
29
27
|
end
|
30
28
|
|
29
|
+
remove_other_rules
|
31
30
|
end
|
32
31
|
|
33
32
|
|
34
33
|
private
|
35
34
|
|
36
|
-
def
|
37
|
-
@node.
|
35
|
+
def remove_other_rules
|
36
|
+
@node.messages.add("deleting old rules\n")
|
37
|
+
ret = @node.exec('ls /etc/sudoers.d/* |cat')
|
38
|
+
if ret[:exit_code] != 0
|
39
|
+
return @node.messages.add('couldn\'t get installed rule list, skipping deletion of old rules').warning
|
40
|
+
end
|
41
|
+
|
42
|
+
# delete file if not in config
|
43
|
+
ret[:stdout].each_line do |file|
|
44
|
+
file.chomp!
|
45
|
+
@node.rm(file, :indent => 2) unless @config.keys.include?(File.basename(file))
|
46
|
+
end
|
38
47
|
end
|
39
48
|
|
40
|
-
def deploy_rule
|
41
|
-
@node.write
|
42
|
-
@node.chmod
|
43
|
-
@node.chown
|
49
|
+
def deploy_rule(name, file)
|
50
|
+
@node.write("/etc/sudoers.d/#{name}", file, :indent => 2)
|
51
|
+
@node.chmod('0440', "/etc/sudoers.d/#{name}", :indent => 2)
|
52
|
+
@node.chown('root:root', "/etc/sudoers.d/#{name}", :indent => 2)
|
44
53
|
end
|
45
54
|
end
|
data/lib/dust/runner.rb
ADDED
@@ -0,0 +1,396 @@
|
|
1
|
+
require 'thor/runner'
|
2
|
+
require 'thor/util'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'ipaddress'
|
7
|
+
require 'colorize'
|
8
|
+
|
9
|
+
module Dust
|
10
|
+
class Runner < Thor::Runner
|
11
|
+
|
12
|
+
default_task :list
|
13
|
+
check_unknown_options!
|
14
|
+
|
15
|
+
# default options for all tasks
|
16
|
+
def self.default_options
|
17
|
+
method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
|
18
|
+
method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
|
19
|
+
method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
|
20
|
+
method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
|
21
|
+
method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.recipe_options
|
25
|
+
method_option 'recipes', :type => :array, :desc => 'only deploy these recipes'
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
desc 'deploy', 'deploy all recipes to the node(s) specified in server.yaml or to all nodes defined in ./nodes/'
|
30
|
+
default_options
|
31
|
+
recipe_options
|
32
|
+
method_option 'restart', :type => :boolean, :desc => 'restart services after deploy'
|
33
|
+
method_option 'reload', :type => :boolean, :desc => 'reload services after deploy'
|
34
|
+
|
35
|
+
def deploy
|
36
|
+
return unless check_dust_dir
|
37
|
+
initialize_thorfiles
|
38
|
+
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
39
|
+
|
40
|
+
# set global variables
|
41
|
+
$summary = options['summary']
|
42
|
+
$parallel = options['parallel']
|
43
|
+
$summary = 'all' if $parallel and not $summary
|
44
|
+
|
45
|
+
threads = []
|
46
|
+
@nodes.each_with_index do |node, i|
|
47
|
+
if $parallel
|
48
|
+
threads[i] = Thread.new do
|
49
|
+
Thread.current['hostname'] = node['hostname'] if run_recipes node, 'deploy'
|
50
|
+
end
|
51
|
+
else
|
52
|
+
run_recipes node, 'deploy'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if $parallel
|
57
|
+
print 'waiting for servers: '
|
58
|
+
threads.each do |t|
|
59
|
+
t.join # wait for thread
|
60
|
+
print t['hostname'].blue + ' ' if t['hostname']
|
61
|
+
end
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
|
65
|
+
display_summary($summary) if $summary
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
desc 'status', 'display status of recipes specified by filter'
|
70
|
+
default_options
|
71
|
+
recipe_options
|
72
|
+
|
73
|
+
def status
|
74
|
+
return unless check_dust_dir
|
75
|
+
initialize_thorfiles
|
76
|
+
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
77
|
+
|
78
|
+
# set global variables
|
79
|
+
$summary = options['summary']
|
80
|
+
$parallel = options['parallel']
|
81
|
+
$summary = 'all' if $parallel and not $summary
|
82
|
+
|
83
|
+
threads = []
|
84
|
+
@nodes.each_with_index do |node, i|
|
85
|
+
if $parallel
|
86
|
+
threads[i] = Thread.new do
|
87
|
+
Thread.current['hostname'] = node['hostname'] if run_recipes node, 'status'
|
88
|
+
end
|
89
|
+
else
|
90
|
+
run_recipes node, 'status'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if $parallel
|
95
|
+
print 'waiting for servers: '
|
96
|
+
threads.each do |t|
|
97
|
+
t.join # wait for thread
|
98
|
+
print t['hostname'].blue + ' ' if t['hostname']
|
99
|
+
end
|
100
|
+
puts
|
101
|
+
end
|
102
|
+
|
103
|
+
display_summary($summary) if $summary
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
desc 'system_update', 'perform a full system upgrade (using aptitude, emerge, yum)'
|
108
|
+
default_options
|
109
|
+
|
110
|
+
def system_update
|
111
|
+
return unless check_dust_dir
|
112
|
+
initialize_thorfiles
|
113
|
+
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
114
|
+
|
115
|
+
# set global variables
|
116
|
+
$summary = options['summary']
|
117
|
+
$parallel = options['parallel']
|
118
|
+
$summary = 'all' if $parallel and not $summary
|
119
|
+
|
120
|
+
threads = []
|
121
|
+
@nodes.each_with_index do |node, i|
|
122
|
+
if $parallel
|
123
|
+
threads[i] = Thread.new do
|
124
|
+
run_system_update(node)
|
125
|
+
Thread.current['hostname'] = node['hostname']
|
126
|
+
end
|
127
|
+
else
|
128
|
+
run_system_update(node)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
if $parallel
|
133
|
+
print 'waiting for servers: '
|
134
|
+
threads.each do |t|
|
135
|
+
t.join # wait for thread
|
136
|
+
print t['hostname'].blue + ' ' if t['hostname']
|
137
|
+
end
|
138
|
+
puts
|
139
|
+
end
|
140
|
+
|
141
|
+
display_summary($summary) if $summary
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
desc 'exec <command>', 'run a command on the server'
|
146
|
+
default_options
|
147
|
+
|
148
|
+
def exec cmd, yaml=''
|
149
|
+
return unless check_dust_dir
|
150
|
+
initialize_thorfiles
|
151
|
+
Dust.print_failed 'no servers match this filter' if load_servers.empty?
|
152
|
+
|
153
|
+
# set global variables
|
154
|
+
$summary = options['summary']
|
155
|
+
$parallel = options['parallel']
|
156
|
+
$summary = 'all' if $parallel and not $summary
|
157
|
+
|
158
|
+
threads = []
|
159
|
+
@nodes.each_with_index do |node, i|
|
160
|
+
if $parallel
|
161
|
+
threads[i] = Thread.new do
|
162
|
+
run_exec(node, cmd)
|
163
|
+
Thread.current['hostname'] = node['hostname']
|
164
|
+
end
|
165
|
+
else
|
166
|
+
run_exec(node, cmd)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
if $parallel
|
171
|
+
print 'waiting for servers: '
|
172
|
+
threads.each do |t|
|
173
|
+
t.join # wait for thread
|
174
|
+
print t['hostname'].blue + ' ' if t['hostname']
|
175
|
+
end
|
176
|
+
puts
|
177
|
+
end
|
178
|
+
|
179
|
+
display_summary($summary) if $summary
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# creates directory skeleton for a dust setup
|
184
|
+
desc 'new <name>', 'creates a dust directory skeleton for your network'
|
185
|
+
def new name
|
186
|
+
Dust.print_msg "spawning new dust directory skeleton with examples into '#{name}.dust'"
|
187
|
+
FileUtils.cp_r File.dirname(__FILE__) + '/examples', "#{name}.dust"
|
188
|
+
Dust.print_ok
|
189
|
+
end
|
190
|
+
|
191
|
+
desc 'version', 'displays version number'
|
192
|
+
def version
|
193
|
+
puts "dust-deploy-#{Dust::VERSION}, running on ruby-#{RUBY_VERSION}"
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def check_dust_dir
|
200
|
+
if Dir.pwd.split('.').last != 'dust'
|
201
|
+
Dust.print_failed 'current directory does not end with .dust, are you in your dust directory?'
|
202
|
+
Dust.print_msg "try running 'dust new mynetwork' to let me create one for you with tons of examples!\n", :indent => 0
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
|
206
|
+
unless File.directory? './nodes'
|
207
|
+
Dust.print_failed 'could not find \'nodes\' folder in your dust directory. cannot continue.'
|
208
|
+
return false
|
209
|
+
end
|
210
|
+
|
211
|
+
true
|
212
|
+
end
|
213
|
+
|
214
|
+
# run specified recipes in the given context
|
215
|
+
# returns false if no recipes where found
|
216
|
+
# true if recipes were run (doesn't indicate, whether the run was sucessful or not)
|
217
|
+
def run_recipes(node, context)
|
218
|
+
# skip this node if there are no recipes found
|
219
|
+
return false unless node['recipes']
|
220
|
+
|
221
|
+
recipes = generate_recipes node, context
|
222
|
+
|
223
|
+
# skip this node unless we're actually having recipes to cook
|
224
|
+
return false if recipes.empty?
|
225
|
+
|
226
|
+
# connect to server
|
227
|
+
node['server'] = Server.new(node)
|
228
|
+
return true unless node['server'].connect
|
229
|
+
|
230
|
+
# runs the method with the recipe name, defined and included in recipe/*.rb
|
231
|
+
# call recipes for each recipe that is defined for this node
|
232
|
+
recipes.each do |recipe, config|
|
233
|
+
send recipe, 'prepare', node['server'], recipe, context, config, options
|
234
|
+
end
|
235
|
+
|
236
|
+
node['server'].disconnect
|
237
|
+
true
|
238
|
+
end
|
239
|
+
|
240
|
+
def run_system_update(node)
|
241
|
+
node['server'] = Server.new(node)
|
242
|
+
return unless node['server'].connect
|
243
|
+
node['server'].system_update
|
244
|
+
node['server'].disconnect
|
245
|
+
end
|
246
|
+
|
247
|
+
def run_exec(node, cmd)
|
248
|
+
node['server'] = Server.new(node)
|
249
|
+
return unless node['server'].connect
|
250
|
+
node['server'].exec(cmd, :live => true)
|
251
|
+
node['server'].disconnect
|
252
|
+
end
|
253
|
+
|
254
|
+
# generate list of recipes for this node
|
255
|
+
def generate_recipes node, context
|
256
|
+
recipes = {}
|
257
|
+
node['recipes'].each do |recipe, config|
|
258
|
+
|
259
|
+
# in case --recipes was set, skip unwanted recipes
|
260
|
+
next unless options['recipes'].include?(recipe) if options['recipes']
|
261
|
+
|
262
|
+
# skip disabled recipes
|
263
|
+
next if config == 'disabled' or config.is_a? FalseClass
|
264
|
+
|
265
|
+
# check if method and thor task actually exist
|
266
|
+
k = Thor::Util.find_by_namespace recipe
|
267
|
+
next unless k
|
268
|
+
next unless k.method_defined? context
|
269
|
+
|
270
|
+
recipes[recipe] = config
|
271
|
+
end
|
272
|
+
recipes
|
273
|
+
end
|
274
|
+
|
275
|
+
def display_summary(level)
|
276
|
+
puts "\n\n------------------------------ SUMMARY ------------------------------".red unless $parallel
|
277
|
+
|
278
|
+
@nodes.each do |node|
|
279
|
+
next unless node['server']
|
280
|
+
|
281
|
+
messages = node['server'].messages.collect(level)
|
282
|
+
next if messages.empty?
|
283
|
+
|
284
|
+
node['server'].messages.print_hostname_header(node['hostname'])
|
285
|
+
|
286
|
+
# display non-recipe messages first
|
287
|
+
msgs = messages.delete '_node'
|
288
|
+
msgs.each { |m| print m } if msgs
|
289
|
+
|
290
|
+
# display messages from recipes
|
291
|
+
messages.each do |recipe, msgs|
|
292
|
+
node['server'].messages.print_recipe_header(recipe)
|
293
|
+
msgs.each { |m| print m }
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# overwrite thorfiles to look for tasks in the recipes directories
|
299
|
+
def thorfiles(relevant_to=nil, skip_lookup=false)
|
300
|
+
Dir[File.dirname(__FILE__) + '/recipes/*.rb'] | Dir['recipes/*.rb']
|
301
|
+
end
|
302
|
+
|
303
|
+
# loads servers
|
304
|
+
def load_servers
|
305
|
+
@nodes = []
|
306
|
+
|
307
|
+
# if the argument is empty, load all yaml files in the ./nodes/ directory
|
308
|
+
# if the argument is a directory, load yaml files in this directory
|
309
|
+
# if the argument is a file, load the file.
|
310
|
+
if options['yaml']
|
311
|
+
if File.directory? options['yaml']
|
312
|
+
yaml_files = Dir["#{options['yaml']}/**/*.yaml"]
|
313
|
+
elsif File.exists? options['yaml']
|
314
|
+
yaml_files = options['yaml']
|
315
|
+
end
|
316
|
+
else
|
317
|
+
yaml_files = Dir['./nodes/**/*.yaml']
|
318
|
+
end
|
319
|
+
|
320
|
+
unless yaml_files
|
321
|
+
Dust.print_failed "#{yaml} doesn't exist. exiting."
|
322
|
+
exit
|
323
|
+
end
|
324
|
+
|
325
|
+
yaml_files.to_array.each do |file|
|
326
|
+
node = YAML.load ERB.new( File.read(file), nil, '%<>').result
|
327
|
+
|
328
|
+
# if the file is empty, just skip it
|
329
|
+
next unless node
|
330
|
+
|
331
|
+
# if there is not hostname field in the yaml file,
|
332
|
+
# treat this node file as a template, and skip to the next one
|
333
|
+
next unless node['hostname']
|
334
|
+
|
335
|
+
# look for the inherits field in the yaml file,
|
336
|
+
# and merge the templates recursively into this node
|
337
|
+
if node['inherits']
|
338
|
+
inherited = {}
|
339
|
+
node.delete('inherits').each do |file|
|
340
|
+
template = YAML.load ERB.new( File.read("./nodes/#{file}.yaml"), nil, '%<>').result
|
341
|
+
inherited.deep_merge! template
|
342
|
+
end
|
343
|
+
node = inherited.deep_merge node
|
344
|
+
end
|
345
|
+
|
346
|
+
# if more than one hostname is specified, create a node
|
347
|
+
# with the same settings for each hostname
|
348
|
+
node['hostname'].to_array.each do |hostname|
|
349
|
+
n = node.clone
|
350
|
+
|
351
|
+
# overwrite hostname with single hostname (in case there are multiple)
|
352
|
+
n['hostname'] = hostname
|
353
|
+
|
354
|
+
# create a new field with the fully qualified domain name
|
355
|
+
n['fqdn'] = hostname
|
356
|
+
|
357
|
+
# if hostname is a valid ip address, don't add domain
|
358
|
+
# so we can connect via ip address only
|
359
|
+
unless IPAddress.valid? hostname
|
360
|
+
n['fqdn'] += '.' + n['domain'] if n['domain']
|
361
|
+
end
|
362
|
+
|
363
|
+
# pass command line proxy option
|
364
|
+
n['proxy'] = options['proxy'] if options['proxy']
|
365
|
+
|
366
|
+
# add this node to the global node array
|
367
|
+
@nodes.push n unless filtered? n
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# checks if this node was filtered out by command line argument
|
373
|
+
# e.g. --filter environment:staging filters out all machines but
|
374
|
+
# those in the environment staging
|
375
|
+
def filtered? node
|
376
|
+
|
377
|
+
# if filter is not specified, instantly return false
|
378
|
+
return false unless options['filter']
|
379
|
+
|
380
|
+
# remove items if other filter arguments don't match
|
381
|
+
options['filter'].each do |k, v|
|
382
|
+
next unless v # skip empty filters
|
383
|
+
|
384
|
+
# filter if this node doesn't even have the attribute
|
385
|
+
return true unless node[k]
|
386
|
+
|
387
|
+
# allow multiple filters of the same type, divided by ','
|
388
|
+
# e.g. --filter environment:staging,production
|
389
|
+
return true unless v.split(',').include? node[k]
|
390
|
+
end
|
391
|
+
|
392
|
+
# no filter matched, so this host is not filtered.
|
393
|
+
false
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
data/lib/dust/server.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'rubygems'
|
2
1
|
require 'net/ssh'
|
3
2
|
require 'net/scp'
|
4
3
|
require 'net/ssh/proxy/socks5'
|
@@ -83,15 +82,21 @@ module Dust
|
|
83
82
|
abort "FAILED: couldn't execute command (ssh.channel.exec)" unless success
|
84
83
|
|
85
84
|
channel.on_data do |ch, data|
|
86
|
-
|
87
85
|
# only send password if sudo mode is enabled,
|
88
86
|
# and only send password once in a session (trying to prevent attacks reading out the password)
|
89
|
-
if @node['
|
90
|
-
|
91
|
-
|
87
|
+
if data =~ /\[sudo\] password for #{@node['user']}/
|
88
|
+
|
89
|
+
raise 'password requested, but none given in config!' if @node['password'].empty?
|
90
|
+
raise 'already sent password, but sudo requested the password again. (wrong password?)' if sudo_authenticated
|
91
|
+
|
92
|
+
# we're not authenticated yet, send password
|
92
93
|
channel.send_data "#{@node['password']}\n"
|
93
94
|
sudo_authenticated = true
|
95
|
+
|
94
96
|
else
|
97
|
+
# skip everything util authenticated (if sudo is used and password given in config)
|
98
|
+
next if @node['sudo'] and not @node['password'].empty? and not sudo_authenticated
|
99
|
+
|
95
100
|
stdout += data
|
96
101
|
messages.add(data.green, :indent => 0) if options[:live] and not data.empty?
|
97
102
|
end
|
@@ -141,11 +146,11 @@ module Dust
|
|
141
146
|
msg.parse_result(write(destination, content, :quiet => true))
|
142
147
|
end
|
143
148
|
|
144
|
-
def scp
|
149
|
+
def scp(source, destination, options = {})
|
145
150
|
options = default_options.merge options
|
146
151
|
|
147
152
|
# make sure scp is installed on client
|
148
|
-
install_package
|
153
|
+
install_package('openssh-clients', :quiet => true) if uses_rpm?
|
149
154
|
|
150
155
|
msg = messages.add("deploying #{File.basename source}", options)
|
151
156
|
|
@@ -153,9 +158,9 @@ module Dust
|
|
153
158
|
is_dir = dir_exists?(destination, :quiet => true)
|
154
159
|
|
155
160
|
# save permissions if the file already exists
|
156
|
-
ret = exec
|
161
|
+
ret = exec("stat -c %a:%u:%g #{destination}")
|
157
162
|
if ret[:exit_code] == 0 and not is_dir
|
158
|
-
permissions, user, group = ret[:stdout].chomp.split
|
163
|
+
permissions, user, group = ret[:stdout].chomp.split(':')
|
159
164
|
else
|
160
165
|
# files = 644, dirs = 755
|
161
166
|
permissions = 'ug-x,o-wx,u=rwX,g=rX,o=rX'
|
@@ -168,9 +173,12 @@ module Dust
|
|
168
173
|
|
169
174
|
# allow user to write file without sudo (for scp)
|
170
175
|
# then change file back to root, and copy to the destination
|
171
|
-
chown
|
172
|
-
@ssh.scp.upload!
|
173
|
-
|
176
|
+
chown(@node['user'], tmpfile, :quiet => true)
|
177
|
+
@ssh.scp.upload!(source, tmpfile)
|
178
|
+
|
179
|
+
# set file permissions
|
180
|
+
chown("#{user}:#{group}", tmpfile, :quiet => true) if user and group
|
181
|
+
chmod(permissions, tmpfile, :quiet => true)
|
174
182
|
|
175
183
|
# if destination is a directory, append real filename
|
176
184
|
destination = "#{destination}/#{File.basename(source)}" if is_dir
|
@@ -179,15 +187,15 @@ module Dust
|
|
179
187
|
msg.parse_result(exec("mv -f #{tmpfile} #{destination}")[:exit_code])
|
180
188
|
|
181
189
|
else
|
182
|
-
@ssh.scp.upload!
|
190
|
+
@ssh.scp.upload!(source, destination)
|
183
191
|
msg.ok
|
184
|
-
end
|
185
192
|
|
186
|
-
|
187
|
-
|
188
|
-
|
193
|
+
# set file permissions
|
194
|
+
chown("#{user}:#{group}", destination, :quiet => true) if user and group
|
195
|
+
chmod(permissions, destination, :quiet => true)
|
196
|
+
end
|
189
197
|
|
190
|
-
restorecon
|
198
|
+
restorecon(destination, options) # restore SELinux labels
|
191
199
|
end
|
192
200
|
|
193
201
|
# download a file (sudo not yet supported)
|
@@ -368,7 +376,7 @@ module Dust
|
|
368
376
|
elsif uses_opkg?
|
369
377
|
exec "opkg install #{package}"
|
370
378
|
else
|
371
|
-
return msg.failed("
|
379
|
+
return msg.failed("\ninstall_package only supports apt, emerge and yum systems at the moment")
|
372
380
|
end
|
373
381
|
|
374
382
|
# check if package actually was installed
|
@@ -690,10 +698,10 @@ module Dust
|
|
690
698
|
service service, 'reload', options
|
691
699
|
end
|
692
700
|
|
693
|
-
def print_service_status
|
694
|
-
options = default_options.merge options
|
695
|
-
ret = service
|
696
|
-
messages.
|
701
|
+
def print_service_status(service, options = {})
|
702
|
+
options = default_options.merge(:indent => 0).merge(options)
|
703
|
+
ret = service(service, 'status', options)
|
704
|
+
messages.add('', options).print_output(ret)
|
697
705
|
ret
|
698
706
|
end
|
699
707
|
|
data/lib/dust/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dust-deploy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -205,6 +205,7 @@ files:
|
|
205
205
|
- lib/dust/recipes/sudoers.rb
|
206
206
|
- lib/dust/recipes/sysctl.rb
|
207
207
|
- lib/dust/recipes/zabbix_agent.rb
|
208
|
+
- lib/dust/runner.rb
|
208
209
|
- lib/dust/server.rb
|
209
210
|
- lib/dust/version.rb
|
210
211
|
homepage: ''
|