dust-deploy 0.12.2 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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