dust-deploy 0.13.18 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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
- require 'colorize'
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
@@ -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
 
@@ -1,3 +1,4 @@
1
+ require 'dust/runner'
1
2
  require 'dust/version'
2
3
  require 'dust/helper'
3
4
  require 'dust/print_status'
@@ -1,3 +1,5 @@
1
+ require 'thor'
2
+
1
3
  class Recipe < Thor
2
4
 
3
5
  desc 'prepare', 'prepare recipe (do not use manually)'
@@ -40,7 +40,6 @@ class Nginx < Recipe
40
40
 
41
41
  desc 'nginx:status', 'displays nginx status'
42
42
  def status
43
- return unless @node.package_installed? 'nginx'
44
43
  @node.print_service_status 'nginx'
45
44
  end
46
45
  end
@@ -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 be deleted").warning
7
+ @node.messages.add("#{package} installed, might conflict with ntpd, might get deleted").warning
8
8
  end
9
9
  end
10
10
 
@@ -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 name, file
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 remove_rules
37
- @node.rm '/etc/sudoers.d/*'
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 name, file
41
- @node.write "/etc/sudoers.d/#{name}", file, :indent => 2
42
- @node.chmod '0440', "/etc/sudoers.d/#{name}", :indent => 2
43
- @node.chown 'root:root', "/etc/sudoers.d/#{name}", :indent => 2
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
@@ -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
@@ -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['sudo'] and not sudo_authenticated
90
- # skip everything till password is prompted
91
- next unless data =~ /^\[sudo\] password for #{@node['user']}/
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 source, destination, options = {}
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 'openssh-clients', :quiet => true if uses_rpm?
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 "stat -c %a:%u:%g #{destination}"
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 @node['user'], tmpfile, :quiet => true
172
- @ssh.scp.upload! source, tmpfile
173
- chown 'root', tmpfile, :quiet => true
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! source, destination
190
+ @ssh.scp.upload!(source, destination)
183
191
  msg.ok
184
- end
185
192
 
186
- # set file permissions
187
- chown "#{user}:#{group}", destination, :quiet => true if user and group
188
- chmod permissions, destination, :quiet => true
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 destination, options # restore SELinux labels
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("install_package only supports apt, emerge and yum systems at the moment")
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 service, options = {}
694
- options = default_options.merge options
695
- ret = service service, 'status', options
696
- messages.print_output(ret, options)
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
 
@@ -1,3 +1,3 @@
1
1
  module Dust
2
- VERSION = "0.13.18"
2
+ VERSION = "0.14.0"
3
3
  end
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.13.18
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-13 00:00:00.000000000 Z
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: ''