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 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: ''