navo 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/navo.rb +19 -1
- data/lib/navo/berksfile.rb +58 -0
- data/lib/navo/chef_formatter.rb +278 -0
- data/lib/navo/cli.rb +80 -23
- data/lib/navo/configuration.rb +10 -1
- data/lib/navo/logger.rb +121 -0
- data/lib/navo/sandbox.rb +36 -24
- data/lib/navo/state_file.rb +78 -0
- data/lib/navo/suite.rb +181 -36
- data/lib/navo/utils.rb +17 -0
- data/lib/navo/version.rb +1 -1
- metadata +20 -3
- data/lib/navo/suite_state.rb +0 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e19118f65db1a7db3490eb3ce679b05d9c93dd4
|
4
|
+
data.tar.gz: 28fdb3567c3f9b883e54a88c258e3e5776f6f5b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8df3881c54fbba49a9bcabd1e1fce93728407f7302f8a115091cf89f25897fdfbd189c24212b237207cd5bc3d8499fe69d1dc48bd8f45583890990486ec4bb0c
|
7
|
+
data.tar.gz: 59849b6754981ccf3526f10ec0f893b1f8f52018fb29fa5826a163c5a051a4681ef297a49d92354933302dc8b46468e323792e0b5f6b3860d77424473095154d
|
data/lib/navo.rb
CHANGED
@@ -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/
|
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
|
data/lib/navo/cli.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
37
|
-
|
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
|