navo 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3adda44205c06f6444cdc4969561ce867498cc6f
4
- data.tar.gz: a8261f98b2e072aab9451df37b558c4b2376308a
3
+ metadata.gz: 6e19118f65db1a7db3490eb3ce679b05d9c93dd4
4
+ data.tar.gz: 28fdb3567c3f9b883e54a88c258e3e5776f6f5b3
5
5
  SHA512:
6
- metadata.gz: ec8ecc5aee52a8c00aeaab4ffed1062f613f71a85b808327d1c0dd62e9d0326793a5e1cbe4f49a1628d46038b0b050e8ddc7884b90a1d9c9cc6dae39a4fa02a3
7
- data.tar.gz: 940a3980f2acae81e2353e7c57d2a79d523bb7fa2d572f6b28a9c28bc8bdc1d96e02339aa55e0e5e0b97fca54387c1ee2bcffd793a2b2e4f5175bad72cc178e4
6
+ metadata.gz: 8df3881c54fbba49a9bcabd1e1fce93728407f7302f8a115091cf89f25897fdfbd189c24212b237207cd5bc3d8499fe69d1dc48bd8f45583890990486ec4bb0c
7
+ data.tar.gz: 59849b6754981ccf3526f10ec0f893b1f8f52018fb29fa5826a163c5a051a4681ef297a49d92354933302dc8b46468e323792e0b5f6b3860d77424473095154d
@@ -1,8 +1,26 @@
1
1
  require 'navo/constants'
2
2
  require 'navo/errors'
3
3
  require 'navo/configuration'
4
+ require 'navo/logger'
4
5
  require 'navo/sandbox'
5
6
  require 'navo/suite'
6
- require 'navo/suite_state'
7
+ require 'navo/berksfile'
8
+ require 'navo/state_file'
7
9
  require 'navo/utils'
8
10
  require 'navo/version'
11
+
12
+ module Navo
13
+ class << self
14
+ attr_accessor :mutex
15
+
16
+ def synchronize
17
+ mutex.synchronize do
18
+ yield
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ Navo.mutex = Mutex.new
25
+
26
+ Excon.defaults[:read_timeout] = 600
@@ -0,0 +1,58 @@
1
+ require 'fileutils'
2
+
3
+ module Navo
4
+ # A global Berksfile to be shared amongst all threads.
5
+ #
6
+ # This synchronizes access so we don't have multiple threads doing the same
7
+ # work resolving cookbooks.
8
+ class Berksfile
9
+ class << self
10
+ attr_accessor :path
11
+ attr_accessor :config
12
+
13
+ def load
14
+ require 'berkshelf' # Lazily require so we don't have to load for every command
15
+ berksfile
16
+ end
17
+
18
+ def install(logger: nil)
19
+ if @installed
20
+ logger.info 'Berksfile cookbooks already resolved'
21
+ return
22
+ end
23
+
24
+ logger.info 'Installing Berksfile...'
25
+ Berkshelf.logger = Celluloid.logger = logger
26
+ Berkshelf.ui.mute { Berkshelf::Installer.new(berksfile).run }
27
+ Celluloid.logger = nil # Ignore annoying shutdown messages
28
+
29
+ @installed = true
30
+ end
31
+
32
+ def vendor(logger:)
33
+ Berkshelf.logger = Celluloid.logger = logger
34
+ Berkshelf.ui.mute { berksfile.vendor(vendor_directory) }
35
+ Celluloid.logger = nil # Ignore annoying shutdown messages
36
+ end
37
+
38
+ def cache_directory
39
+ Berkshelf::CookbookStore.default_path
40
+ end
41
+
42
+ def vendor_directory
43
+ @vendor_directory ||=
44
+ FileUtils.mkdir_p(File.join(config.repo_root, %w[.navo vendored-cookbooks])).first
45
+ end
46
+
47
+ def lockfile_path
48
+ berksfile.lockfile.filepath
49
+ end
50
+
51
+ private
52
+
53
+ def berksfile
54
+ @berksfile ||= Berkshelf::Berksfile.from_file(@path)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,278 @@
1
+ class Chef
2
+ module Formatters
3
+ class NavoFormatter < Formatters::Base
4
+ cli_name(:navo)
5
+
6
+ def initialize(out, err)
7
+ super
8
+
9
+ @up_to_date_resources = 0
10
+ @updated_resources = 0
11
+ @skipped_resources = 0
12
+ @resource_stack = []
13
+ @resource_action_times = Hash.new { |hash, key| hash[key] = [] }
14
+
15
+ @deprecations = {}
16
+ end
17
+
18
+ def total_resources
19
+ @up_to_date_resources + @updated_resources + @skipped_resources
20
+ end
21
+
22
+ def print_deprecations
23
+ return if @deprecations.empty?
24
+ puts_line 'Deprecated features used!'
25
+
26
+ @deprecations.each do |message, locations|
27
+ if locations.size == 1
28
+ puts_line " #{message} at one location:"
29
+ else
30
+ puts_line " #{message} at #{locations.size} locations:"
31
+ end
32
+ locations.each do |location|
33
+ prefix = ' - '
34
+ Array(location).each do |line|
35
+ puts_line "#{prefix}#{line}"
36
+ prefix = ' '
37
+ end
38
+ end
39
+ end
40
+ puts_line ''
41
+ end
42
+
43
+ def run_start(version)
44
+ @start_time = Time.now
45
+ puts_line "Starting Chef client #{version}...", :cyan
46
+ end
47
+
48
+ def ohai_completed(node)
49
+ puts_line ''
50
+ puts_line 'Ohai run completed', :cyan
51
+ end
52
+
53
+ def library_load_start(file_count)
54
+ @load_start_time = Time.now
55
+ puts_line ''
56
+ puts_line 'Loading cookbook libraries...', :cyan
57
+ end
58
+
59
+ def library_load_complete
60
+ elapsed = Time.now - @load_start_time
61
+ puts_line "Loaded cookbook libraries (#{elapsed}s)", :cyan
62
+ end
63
+
64
+ def attribute_load_start(file_count)
65
+ @load_start_time = Time.now
66
+ puts_line ''
67
+ puts_line 'Loading cookbook attributes...', :cyan
68
+ end
69
+
70
+ def attribute_load_complete
71
+ elapsed = Time.now - @load_start_time
72
+ puts_line "Loaded cookbook attributes (#{elapsed}s)", :cyan
73
+ end
74
+
75
+ def lwrp_load_start(file_count)
76
+ @load_start_time = Time.now
77
+ puts_line ''
78
+ puts_line 'Loading custom resources...', :cyan
79
+ end
80
+
81
+ def lwrp_load_complete
82
+ elapsed = Time.now - @load_start_time
83
+ puts_line "Loaded custom resources (#{elapsed}s)", :cyan
84
+ end
85
+
86
+ def definition_load_start(file_count)
87
+ @load_start_time = Time.now
88
+ puts_line ''
89
+ puts_line 'Loading definitions...', :cyan
90
+ end
91
+
92
+ def definition_load_complete
93
+ elapsed = Time.now - @load_start_time
94
+ puts_line "Loaded definitions (#{elapsed}s)", :cyan
95
+ end
96
+
97
+ def recipe_load_start(recipes)
98
+ @load_start_time = Time.now
99
+ puts_line ''
100
+ puts_line "Loading recipes...", :cyan
101
+ end
102
+
103
+ def recipe_load_complete
104
+ elapsed = Time.now - @load_start_time
105
+ puts_line "Recipes loaded (#{elapsed}s)", :cyan
106
+ end
107
+
108
+ def file_loaded(path)
109
+ puts_line "Loaded #{path}"
110
+ end
111
+
112
+ def converge_start(run_context)
113
+ puts_line ''
114
+ puts_line "Converging #{run_context.resource_collection.all_resources.size} resources..."
115
+ end
116
+
117
+ def converge_complete
118
+ unindent while @resource_stack.pop
119
+ puts_line ''
120
+ puts_line 'Converge completed', :green
121
+ end
122
+
123
+ def converge_failed(e)
124
+ unindent while @resource_stack.pop
125
+ puts_line ''
126
+ puts_line "Converge failed: #{e}", :red
127
+ end
128
+
129
+ def resource_action_start(resource, action, notification_type = nil, notifier = nil)
130
+ # Track the current recipe so we update it only when it changes
131
+ # (i.e. when descending into another recipe via include_recipe)
132
+ if resource.cookbook_name && resource.recipe_name
133
+ current_recipe = "#{resource.cookbook_name}::#{resource.recipe_name}"
134
+
135
+ unless current_recipe == @current_recipe
136
+ @current_recipe = current_recipe
137
+ puts_line current_recipe, :magenta
138
+ end
139
+ end
140
+
141
+ # Record the resource and the time we started so we can figure out how
142
+ # long it took to complete
143
+ @resource_stack << [resource, Time.now]
144
+ indent
145
+
146
+ puts_line "#{resource} action #{action}"
147
+ end
148
+
149
+ def resource_failed_retriable(resource, action, retry_count, exception)
150
+ puts_line "#{resource} action #{action} FAILED; retrying...", :yellow
151
+ end
152
+
153
+ # Called when a resource fails and will not be retried.
154
+ def resource_failed(resource, action, exception)
155
+ _, start_time = @resource_stack.pop
156
+ elapsed = Time.now - start_time
157
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
158
+ puts_line "#{resource} action #{action} (#{elapsed}s) FAILED: #{exception}", :red
159
+ unindent
160
+ end
161
+
162
+ def resource_skipped(resource, action, conditional)
163
+ @skipped_resources += 1
164
+ _, start_time = @resource_stack.pop
165
+ elapsed = Time.now - start_time
166
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
167
+ puts_line "#{resource} action #{action} (#{elapsed}s) SKIPPED due to: #{conditional.short_description}"
168
+ unindent
169
+ end
170
+
171
+ def resource_up_to_date(resource, action)
172
+ @up_to_date_resources += 1
173
+ _, start_time = @resource_stack.pop
174
+ elapsed = Time.now - start_time
175
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
176
+ puts_line "#{resource} action #{action} up-to-date (#{elapsed}s)"
177
+ unindent
178
+ end
179
+
180
+ def resource_update_applied(resource, action, update)
181
+ indent
182
+
183
+ Array(update).compact.each do |line|
184
+ if line.is_a?(String)
185
+ puts_line "- #{line}", :green
186
+ elsif line.is_a?(Array)
187
+ # Expanded output delta
188
+ line.each do |detail|
189
+ if detail =~ /^\+(?!\+\+ )/
190
+ color = :green
191
+ elsif detail =~ /^-(?!-- )/
192
+ color = :red
193
+ else
194
+ color = :cyan
195
+ end
196
+ puts_line detail, color
197
+ end
198
+ end
199
+ end
200
+
201
+ unindent
202
+ end
203
+
204
+ def resource_updated(resource, action)
205
+ @updated_resources += 1
206
+ _, start_time = @resource_stack.pop
207
+ elapsed = Time.now - start_time
208
+ @resource_action_times[[resource.to_s, action.to_s]] << elapsed
209
+ puts_line "#{resource} action #{action} updated (#{elapsed}s)"
210
+ unindent
211
+ end
212
+
213
+ def handlers_start(handler_count)
214
+ @handler_count = handler_count
215
+ puts_line ''
216
+ if @handler_count > 0
217
+ puts_line "Running #{handler_count} handlers:", :cyan
218
+ else
219
+ puts_line 'No registered handlers to run', :cyan
220
+ end
221
+ end
222
+
223
+ def handler_executed(handler)
224
+ puts_line "- #{handler.class.name}"
225
+ end
226
+
227
+ def handlers_completed
228
+ puts_line 'Running handlers complete' unless @handler_count == 0
229
+ end
230
+
231
+ def deprecation(message, location=caller(2..2)[0])
232
+ if Chef::Config[:treat_deprecation_warnings_as_errors]
233
+ super
234
+ end
235
+
236
+ # Save deprecations to the screen until the end
237
+ @deprecations[message] ||= Set.new
238
+ @deprecations[message] << location
239
+ end
240
+
241
+ def run_completed(node)
242
+ @end_time = Time.now
243
+
244
+ print_deprecations
245
+
246
+ puts_line ''
247
+ print_resource_summary
248
+ puts_line "Chef client finished in #{@end_time - @start_time} seconds", :cyan
249
+ end
250
+
251
+ private
252
+
253
+ def print_resource_summary
254
+ puts_line "#{@updated_resources}/#{total_resources} resources updated"
255
+
256
+ slowest_resources = @resource_action_times.sort_by do |key, values|
257
+ -values.inject(:+)
258
+ end
259
+
260
+ puts_line 'Slowest resource actions:'
261
+ slowest_resources[0...10].each do |key, values|
262
+ elapsed = '%-.3fs' % values.inject(:+)
263
+ puts_line "#{elapsed.ljust(8)} #{key.first} (#{key[1]})"
264
+ end
265
+
266
+ puts_line ''
267
+ end
268
+
269
+ def indent
270
+ indent_by 2
271
+ end
272
+
273
+ def unindent
274
+ indent_by -2
275
+ end
276
+ end
277
+ end
278
+ end
@@ -1,40 +1,58 @@
1
- require 'berkshelf'
2
1
  require 'navo'
2
+ require 'parallel'
3
3
  require 'thor'
4
4
 
5
5
  module Navo
6
6
  # Command line application interface.
7
7
  class CLI < Thor
8
- desc 'create', 'create a container for test suite(s) to run within'
9
- def create(pattern = nil)
10
- exit suites_for(pattern).map(&:create).all? ? 0 : 1
8
+ def initialize(*args)
9
+ super
10
+ Navo::Logger.output = STDOUT
11
+ STDOUT.sync = true
12
+ Navo::Logger.level = config['log-level']
11
13
  end
12
14
 
13
- desc 'converge', 'run Chef for test suite(s)'
14
- def converge(pattern = nil)
15
- exit suites_for(pattern).map(&:converge).all? ? 0 : 1
16
- end
17
-
18
- desc 'verify', 'run test suite(s)'
19
- def verify(pattern = nil)
20
- exit suites_for(pattern).map(&:verify).all? ? 0 : 1
21
- end
15
+ {
16
+ create: 'create a container for test suite(s) to run within',
17
+ converge: 'run Chef for test suite(s)',
18
+ verify: 'run test suites(s)',
19
+ test: 'converge and run test suites(s)',
20
+ destroy: 'clean up test suite(s)',
21
+ }.each do |action, description|
22
+ desc "#{action} [suite|regexp]", description
23
+ option :concurrency,
24
+ aliases: '-c',
25
+ type: :numeric,
26
+ default: Parallel.processor_count,
27
+ desc: 'Execute up to the specified number of test suites concurrently'
28
+ option 'log-level',
29
+ aliases: '-l',
30
+ type: :string,
31
+ desc: 'Set the log output verbosity level'
22
32
 
23
- desc 'test', 'converge and run test suite(s)'
24
- def test(pattern = nil)
25
- exit suites_for(pattern).map(&:test).all? ? 0 : 1
26
- end
33
+ if action == :test
34
+ option 'destroy',
35
+ aliases: '-d',
36
+ type: :string,
37
+ desc: 'Destroy strategy to use after testing (passing, always, never)'
38
+ end
27
39
 
28
- desc 'destroy', 'clean up test suite(s)'
29
- def destroy(pattern = nil)
30
- exit suites_for(pattern).map(&:destroy).all? ? 0 : 1
40
+ define_method(action) do |*args|
41
+ apply_flags_to_config!
42
+ execute(action, *args)
43
+ end
31
44
  end
32
45
 
33
46
  desc 'login', "open a shell inside a suite's container"
34
47
  def login(pattern)
48
+ apply_flags_to_config!
49
+
35
50
  suites = suites_for(pattern)
36
- if suites.size > 1
37
- puts 'Pattern matched more than one test suite'
51
+ if suites.size == 0
52
+ logger.console "Pattern '#{pattern}' matched no test suites", severity: :error
53
+ exit 1
54
+ elsif suites.size > 1
55
+ logger.console "Pattern '#{pattern}' matched more than one test suite", severity: :error
38
56
  exit 1
39
57
  else
40
58
  suites.first.login
@@ -47,13 +65,52 @@ module Navo
47
65
  @config ||= Configuration.load_applicable
48
66
  end
49
67
 
68
+ def logger
69
+ @logger ||= Navo::Logger.new
70
+ end
71
+
50
72
  def suites_for(pattern)
51
73
  suite_names = config['suites'].keys
52
74
  suite_names.select! { |name| name =~ /#{pattern}/ } if pattern
53
75
 
54
76
  suite_names.map do |suite_name|
55
- Suite.new(name: suite_name, config: config)
77
+ Suite.new(name: suite_name, config: config, global_state: @global_state)
56
78
  end
57
79
  end
80
+
81
+ def apply_flags_to_config!
82
+ config['log-level'] = options['log-level'] if options['log-level']
83
+ Navo::Logger.level = config['log-level']
84
+ config['concurrency'] = options['concurrency'] if options['concurrency']
85
+ config['destroy'] = options.fetch('destroy', 'passing')
86
+
87
+ # Initialize here so config is correctly set
88
+ Berksfile.path = File.expand_path(config['chef']['berksfile'], config.repo_root)
89
+ Berksfile.config = config
90
+ @global_state = StateFile.new(file: File.join(config.repo_root, %w[.navo global-state.yaml]),
91
+ logger: logger).tap(&:load)
92
+ end
93
+
94
+ def execute(action, pattern = nil)
95
+ suites = suites_for(pattern)
96
+ results = Parallel.map(suites, in_threads: config['concurrency']) do |suite|
97
+ succeeded = suite.send(action)
98
+ [succeeded, suite]
99
+ end
100
+
101
+ failures = results.reject { |succeeded, result| succeeded }
102
+ failures.each do |_, suite|
103
+ logger.console("Failed to #{action} #{suite.name}", severity: :error)
104
+ logger.console("See #{suite.log_file} for full log output", severity: :error)
105
+ end
106
+
107
+ exit failures.any? ? 1 : 0
108
+ rescue Interrupt
109
+ # Handle Ctrl-C
110
+ logger.console('INTERRUPTED', severity: :warn)
111
+ rescue => ex
112
+ logger.console("#{ex.class}: #{ex.message}", severity: :fatal)
113
+ logger.console(ex.backtrace.join("\n"), severity: :fatal)
114
+ end
58
115
  end
59
116
  end