dust-deploy 0.12.2 → 0.13.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
@@ -8,6 +8,7 @@ require 'erb'
8
8
  require 'fileutils'
9
9
  require 'ipaddress'
10
10
  require 'dust'
11
+ require 'colorize'
11
12
 
12
13
  module Dust
13
14
  class Deploy < Thor::Runner
@@ -15,72 +16,167 @@ module Dust
15
16
  default_task :list
16
17
  check_unknown_options!
17
18
 
18
- desc 'deploy [--yaml server.yaml] [--filter key=value,value2] [--recipes recipe1 recipe2] [--proxy host:port]',
19
- 'deploy all recipes to the node(s) specified in server.yaml or to all nodes defined in ./nodes/'
19
+ desc 'deploy', 'deploy all recipes to the node(s) specified in server.yaml or to all nodes defined in ./nodes/'
20
20
 
21
- method_options :yaml => :string, :filter => :hash, :recipes => :array, :proxy => :string,
22
- :restart => :boolean, :reload => :boolean
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)'
23
29
 
24
30
  def deploy
25
31
  return unless check_dust_dir
26
32
  initialize_thorfiles
27
33
  Dust.print_failed 'no servers match this filter' if load_servers.empty?
28
34
 
29
- run_recipes 'deploy'
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
+ run_recipes node, 'deploy'
45
+ Thread.current['hostname'] = node['hostname']
46
+ end
47
+ else
48
+ run_recipes node, 'deploy'
49
+ end
50
+ end
51
+
52
+ if $parallel
53
+ print 'waiting for servers: '
54
+ threads.each { |t| t.join; print t['hostname'].blue + ' ' }
55
+ puts
56
+ end
57
+
58
+ display_summary($summary) if $summary
30
59
  end
31
60
 
32
61
 
33
- desc 'status [--yaml server.yaml] [--filter key=value,value2] [--recipes recipe1 recipe2] [--proxy host:port]',
34
- 'display status of recipes specified by filter'
62
+ desc 'status', 'display status of recipes specified by filter'
35
63
 
36
- method_options :yaml => :string, :filter => :hash, :recipes => :array, :proxy => :string
64
+ method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
65
+ method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
66
+ method_option 'recipes', :type => :array, :desc => 'only deploy these recipes'
67
+ method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
68
+ method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
69
+ method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
37
70
 
38
71
  def status
39
72
  return unless check_dust_dir
40
73
  initialize_thorfiles
41
74
  Dust.print_failed 'no servers match this filter' if load_servers.empty?
42
75
 
43
- run_recipes 'status'
76
+ # set global variables
77
+ $summary = options['summary']
78
+ $parallel = options['parallel']
79
+ $summary = 'all' if $parallel and not $summary
80
+
81
+ threads = []
82
+ @nodes.each_with_index do |node, i|
83
+ if $parallel
84
+ threads[i] = Thread.new do
85
+ run_recipes node, 'status'
86
+ Thread.current['hostname'] = node['hostname']
87
+ end
88
+ else
89
+ run_recipes node, 'status'
90
+ end
91
+ end
92
+
93
+ if $parallel
94
+ print 'waiting for servers: '
95
+ threads.each { |t| t.join; print t['hostname'].blue + ' ' }
96
+ puts
97
+ end
98
+
99
+ display_summary($summary) if $summary
44
100
  end
45
101
 
46
102
 
47
- desc 'system_update [--yaml server.yaml] [--filter key=value,vale2] [--proxy host:port]',
48
- 'perform a full system upgrade (using aptitude, emerge, yum)'
103
+ desc 'system_update', 'perform a full system upgrade (using aptitude, emerge, yum)'
49
104
 
50
- method_options :yaml => :string, :filter => :hash, :proxy => :string
105
+ method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
106
+ method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
107
+ method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
108
+ method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
109
+ method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
51
110
 
52
111
  def system_update
53
112
  return unless check_dust_dir
54
113
  initialize_thorfiles
55
114
  Dust.print_failed 'no servers match this filter' if load_servers.empty?
56
115
 
57
- @nodes.each do |node|
58
- # connect to server
59
- server = Server.new node
60
- next unless server.connect
61
- server.system_update
62
- server.disconnect
116
+ # set global variables
117
+ $summary = options['summary']
118
+ $parallel = options['parallel']
119
+ $summary = 'all' if $parallel and not $summary
120
+
121
+ threads = []
122
+ @nodes.each_with_index do |node, i|
123
+ if $parallel
124
+ threads[i] = Thread.new do
125
+ run_system_update(node)
126
+ Thread.current['hostname'] = node['hostname']
127
+ end
128
+ else
129
+ run_system_update(node)
130
+ end
131
+ end
132
+
133
+ if $parallel
134
+ print 'waiting for servers: '
135
+ threads.each { |t| t.join; print t['hostname'].blue + ' ' }
136
+ puts
63
137
  end
138
+
139
+ display_summary($summary) if $summary
64
140
  end
65
141
 
66
142
 
67
- desc 'exec <command> [--yaml server.yaml] [--filter key=value,vale2] [--proxy host:port]',
68
- 'run a command on the server'
143
+ desc 'exec <command>', 'run a command on the server'
69
144
 
70
- method_options :yaml => :string, :filter => :hash, :proxy => :string
145
+ method_option 'yaml', :type => :string, :desc => 'use only this server.yaml'
146
+ method_option 'filter', :type => :hash, :desc => 'only deploy to these hosts (e.g. environment:staging)'
147
+ method_option 'proxy', :type => :string, :desc => 'socks proxy to use'
148
+ method_option 'parallel', :type => :boolean, :desc => 'deploy to all hosts at the same time using threads'
149
+ method_option 'summary', :type => :string, :desc => 'print summary of all events (all, warning, failed)'
71
150
 
72
151
  def exec cmd, yaml=''
73
152
  return unless check_dust_dir
74
153
  initialize_thorfiles
75
154
  Dust.print_failed 'no servers match this filter' if load_servers.empty?
76
155
 
77
- @nodes.each do |node|
78
- # connect to server
79
- server = Server.new node
80
- next unless server.connect
81
- server.exec cmd, :live => true
82
- server.disconnect
156
+ # set global variables
157
+ $summary = options['summary']
158
+ $parallel = options['parallel']
159
+ $summary = 'all' if $parallel and not $summary
160
+
161
+ threads = []
162
+ @nodes.each_with_index do |node, i|
163
+ if $parallel
164
+ threads[i] = Thread.new do
165
+ run_exec(node, cmd)
166
+ Thread.current['hostname'] = node['hostname']
167
+ end
168
+ else
169
+ run_exec(node, cmd)
170
+ end
171
+ end
172
+
173
+ if $parallel
174
+ print 'waiting for servers: '
175
+ threads.each { |t| t.join; print t['hostname'].blue + ' ' }
176
+ puts
83
177
  end
178
+
179
+ display_summary($summary) if $summary
84
180
  end
85
181
 
86
182
 
@@ -111,53 +207,83 @@ module Dust
111
207
  end
112
208
 
113
209
  # run specified recipes in the given context
114
- def run_recipes context
115
- @nodes.each do |node|
116
- # skip this node if there are no recipes found
117
- next unless node['recipes']
118
-
119
- recipes = generate_recipes node, context
120
-
121
- # skip this node unless we're actually having recipes to cook
122
- next if recipes.empty?
123
-
124
- # connect to server
125
- server = Server.new node
126
- next unless server.connect
127
-
128
- # runs the method with the recipe name, defined and included in recipe/*.rb
129
- # call recipes for each recipe that is defined for this node
130
- recipes.each do |recipe, config|
131
- ::Dust.print_recipe recipe
132
- send recipe, 'prepare', server, recipe, context, config, options
133
- puts
134
- end
210
+ def run_recipes(node, context)
211
+ # skip this node if there are no recipes found
212
+ return unless node['recipes']
213
+
214
+ recipes = generate_recipes node, context
215
+
216
+ # skip this node unless we're actually having recipes to cook
217
+ return if recipes.empty?
135
218
 
136
- server.disconnect
219
+ # connect to server
220
+ node['server'] = Server.new node
221
+ return unless node['server'].connect
222
+
223
+ # runs the method with the recipe name, defined and included in recipe/*.rb
224
+ # call recipes for each recipe that is defined for this node
225
+ recipes.each do |recipe, config|
226
+ send recipe, 'prepare', node['server'], recipe, context, config, options
137
227
  end
228
+
229
+ node['server'].disconnect
230
+ end
231
+
232
+ def run_system_update(node)
233
+ node['server'] = Server.new(node)
234
+ return unless node['server'].connect
235
+ node['server'].system_update
236
+ node['server'].disconnect
237
+ end
238
+
239
+ def run_exec(node, cmd)
240
+ node['server'] = Server.new(node)
241
+ return unless node['server'].connect
242
+ node['server'].exec(cmd, :live => true)
243
+ node['server'].disconnect
138
244
  end
139
-
245
+
140
246
  # generate list of recipes for this node
141
247
  def generate_recipes node, context
142
- recipes = {}
248
+ recipes = {}
143
249
  node['recipes'].each do |recipe, config|
144
-
250
+
145
251
  # in case --recipes was set, skip unwanted recipes
146
- next unless options[:recipes].include?(recipe) if options[:recipes]
147
-
252
+ next unless options['recipes'].include?(recipe) if options['recipes']
253
+
148
254
  # skip disabled recipes
149
255
  next if config == 'disabled' or config.is_a? FalseClass
150
-
256
+
151
257
  # check if method and thor task actually exist
152
258
  k = Thor::Util.find_by_namespace recipe
153
259
  next unless k
154
260
  next unless k.method_defined? context
155
-
261
+
156
262
  recipes[recipe] = config
157
263
  end
158
264
  recipes
159
265
  end
160
-
266
+
267
+ def display_summary(level)
268
+ puts "\n\n------------------------------ SUMMARY ------------------------------".red unless $parallel
269
+
270
+ @nodes.each do |node|
271
+ messages = node['server'].messages.collect(level)
272
+ next if messages.empty?
273
+
274
+ node['server'].messages.print_hostname_header(node['hostname'])
275
+
276
+ # display non-recipe messages first
277
+ msgs = messages.delete '_node'
278
+ msgs.each { |m| print m } if msgs
279
+
280
+ # display messages from recipes
281
+ messages.each do |recipe, msgs|
282
+ node['server'].messages.print_recipe_header(recipe)
283
+ msgs.each { |m| print m }
284
+ end
285
+ end
286
+ end
161
287
 
162
288
  # overwrite thorfiles to look for tasks in the recipes directories
163
289
  def thorfiles(relevant_to=nil, skip_lookup=false)
@@ -171,11 +297,11 @@ module Dust
171
297
  # if the argument is empty, load all yaml files in the ./nodes/ directory
172
298
  # if the argument is a directory, load yaml files in this directory
173
299
  # if the argument is a file, load the file.
174
- if options[:yaml]
175
- if File.directory? options[:yaml]
176
- yaml_files = Dir["#{options[:yaml]}/**/*.yaml"]
177
- elsif File.exists? options[:yaml]
178
- yaml_files = options[:yaml]
300
+ if options['yaml']
301
+ if File.directory? options['yaml']
302
+ yaml_files = Dir["#{options['yaml']}/**/*.yaml"]
303
+ elsif File.exists? options['yaml']
304
+ yaml_files = options['yaml']
179
305
  end
180
306
  else
181
307
  yaml_files = Dir['./nodes/**/*.yaml']
@@ -195,7 +321,7 @@ module Dust
195
321
  # if there is not hostname field in the yaml file,
196
322
  # treat this node file as a template, and skip to the next one
197
323
  next unless node['hostname']
198
-
324
+
199
325
  # look for the inherits field in the yaml file,
200
326
  # and merge the templates recursively into this node
201
327
  if node['inherits']
@@ -225,7 +351,7 @@ module Dust
225
351
  end
226
352
 
227
353
  # pass command line proxy option
228
- n['proxy'] = options[:proxy] if options[:proxy]
354
+ n['proxy'] = options['proxy'] if options['proxy']
229
355
 
230
356
  # add this node to the global node array
231
357
  @nodes.push n unless filtered? n
@@ -239,10 +365,10 @@ module Dust
239
365
  def filtered? node
240
366
 
241
367
  # if filter is not specified, instantly return false
242
- return false unless options[:filter]
368
+ return false unless options['filter']
243
369
 
244
370
  # remove items if other filter arguments don't match
245
- options[:filter].each do |k, v|
371
+ options['filter'].each do |k, v|
246
372
  next unless v # skip empty filters
247
373
 
248
374
  # filter if this node doesn't even have the attribute
@@ -1,6 +1,43 @@
1
1
  Changelog
2
2
  =============
3
3
 
4
+ 0.13.0
5
+ ------------
6
+
7
+ - introduces --parallel and --summary
8
+ --summary -> warning e.g. shows a summary with all errors and warnings after completion
9
+
10
+ --parallel -> deploy to all hosts in parallel, using threads
11
+
12
+ - switches to new messaging system. using ::Dust.pring_* methods is now deprecated
13
+ please migrate your recipes to the new @node.messages.add() system
14
+
15
+ ::Dust.print_msg('checking something')
16
+ ::Dust.print_ok
17
+
18
+ msg = @node.messages.add('checking something')
19
+ msg.ok
20
+
21
+
22
+ ::Dust.print_ok('this went well')
23
+
24
+ @node.message.add('this went well').ok
25
+
26
+
27
+ ::Dust.print_message('executing something')
28
+ ::Dust.print_result(ret)
29
+
30
+ msg = @node.message.add('executing something')
31
+ msg.parse_result(ret)
32
+
33
+
34
+ - redis doesn't configure sysctl anymore, please use sysctl redis template
35
+
36
+ recipes:
37
+ sysctl:
38
+ templates: redis
39
+
40
+
4
41
  0.12.2
5
42
  ------------
6
43
 
@@ -1,5 +1,6 @@
1
1
  require 'dust/helper'
2
2
  require 'dust/print_status'
3
+ require 'dust/messaging'
3
4
  require 'dust/server'
4
5
  require 'dust/recipe'
5
6
 
@@ -0,0 +1,140 @@
1
+ require 'colorize'
2
+
3
+ module Dust
4
+
5
+ class Messages
6
+ attr_reader :current_recipe
7
+
8
+ def initialize
9
+ @store = {}
10
+
11
+ # store non-recipe messages in _node
12
+ start_recipe('_node')
13
+ end
14
+
15
+ def add(msg, options = {})
16
+ m = Message.new(msg, options)
17
+ @store[@current_recipe] << m
18
+ m
19
+ end
20
+
21
+ def start_recipe(recipe)
22
+ @current_recipe = recipe
23
+
24
+ # display recipe header, unless we're starting non-recipe messages
25
+ print_recipe_header(recipe) if recipe != '_node' and not $parallel
26
+
27
+ @store[@current_recipe] = []
28
+ end
29
+
30
+ # print hostname
31
+ def print_hostname_header(host)
32
+ puts "\n\n[ #{host} ]".blue
33
+ end
34
+
35
+ # print recipe name
36
+ def print_recipe_header(recipe)
37
+ puts "\n|#{recipe}|".green
38
+ end
39
+
40
+ def collect(level = 'all')
41
+ case level
42
+ when 'all'
43
+ l = [ 'none', 'ok', 'warning', 'failed' ]
44
+ when 'warning'
45
+ l = [ 'warning', 'failed' ]
46
+ when 'failed'
47
+ l = [ 'failed' ]
48
+ end
49
+
50
+ errors = {}
51
+ @store.each do |recipe, messages|
52
+ messages.each do |msg|
53
+ if l.include? msg.status
54
+ errors[recipe] ||= []
55
+ errors[recipe] << msg.text
56
+ end
57
+ end
58
+ end
59
+
60
+ errors
61
+ end
62
+
63
+ end
64
+
65
+ class Message
66
+ attr_reader :text, :status
67
+
68
+ def initialize(msg = '', options = {})
69
+ # merge default options
70
+ @options = { :quiet => false, :indent => 1 }.merge options
71
+
72
+ # autoflush
73
+ $stdout.sync = true
74
+
75
+ # just return if quiet mode is on
76
+ unless @options[:quiet]
77
+ # default status is 'message'
78
+ @status = 'none'
79
+
80
+ @text = indent + msg
81
+ print @text unless $parallel
82
+ end
83
+ end
84
+
85
+ def ok(msg = '')
86
+ unless @options[:quiet]
87
+ @text << msg + ' [ ok ]'.green + "\n"
88
+ puts msg + ' [ ok ]'.green unless $parallel
89
+ @status = 'ok'
90
+ end
91
+
92
+ true
93
+ end
94
+
95
+ def warning(msg = '')
96
+ unless @options[:quiet]
97
+ @text << msg + ' [ warning ]'.yellow + "\n"
98
+ puts msg + ' [ warning ]'.yellow unless $parallel
99
+ @status = 'warning'
100
+ end
101
+
102
+ true
103
+ end
104
+
105
+ def failed(msg = '')
106
+ unless @options[:quiet]
107
+ @text << msg + ' [ failed ]'.red + "\n"
108
+ puts msg + ' [ failed ]'.red unless $parallel
109
+ @status = 'failed'
110
+ end
111
+
112
+ false
113
+ end
114
+
115
+ def parse_result(ret)
116
+ return ok if ret == 0 or ret.is_a? TrueClass
117
+ failed
118
+ end
119
+
120
+ # prints stdout in grey and stderr in red (if existend)
121
+ def print_output(ret)
122
+ @text << indent + ret[:stdout].chomp.green + "\n" unless ret[:stdout].empty?
123
+ @text << indent + ret[:stderr].chomp.red + "\n" unless ret[:stderr].empty?
124
+
125
+ print @text unless $parallel
126
+ end
127
+
128
+
129
+ private
130
+
131
+ # indent according to @options[:indent]
132
+ # indent 0
133
+ # - indent 1
134
+ # - indent 2
135
+ def indent
136
+ return '' if @options[:quiet] or @options[:indent] == 0
137
+ ' ' + ' ' * (@options[:indent] - 1) + '- '
138
+ end
139
+ end
140
+ end