chef-core 0.0.1

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.
@@ -0,0 +1,266 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "train/errors"
19
+ require "pastel"
20
+ require "chef_core/error"
21
+ require "chef_core/text"
22
+ require "chef_core/cliux/ui/terminal"
23
+ require "chef_core/errors/standard_error_resolver"
24
+
25
+ module ChefCore
26
+ module CLIUX
27
+ module UI
28
+ class ErrorPrinter
29
+ attr_reader :id, :pastel, :translation, :exception, :target_host, :config
30
+
31
+ # 't' is a convenience method for accessing error i18n error definitions.
32
+ # It also serves as a workaround to let us verify that correct text key
33
+ # lookups happen in unit tests.
34
+ def t
35
+ ChefCore::Text.errors
36
+ end
37
+
38
+ DEFAULT_ERROR_NO = "CHEFINT001".freeze
39
+
40
+ def self.show_error(e, config)
41
+ # Name is misleading - it's unwrapping but also doing further
42
+ # error resolution for common errors:
43
+ unwrapped = ChefCore::Errors::StandardErrorResolver.unwrap_exception(e)
44
+ if unwrapped.class == ChefCore::MultiJobFailure
45
+ capture_multiple_failures(unwrapped, config)
46
+ end
47
+ formatter = ErrorPrinter.new(wrapper: e,
48
+ exception: unwrapped,
49
+ config: config)
50
+
51
+ Terminal.output(formatter.format_error)
52
+ rescue => ex
53
+ dump_unexpected_error(ex)
54
+ end
55
+
56
+ def self.capture_multiple_failures(e, config)
57
+ e.params << config[:error_output_path] # Tell the operator where to find this info
58
+ File.open(config[:error_output_path], "w") do |out|
59
+ e.jobs.each do |j|
60
+ wrapped = ChefCore::Errors::StandardErrorResolver.wrap_exception(j.exception, j.target_host)
61
+ ep = ErrorPrinter.new(wrapper: wrapped, config: config)
62
+ msg = ep.format_body().tr("\n", " ").gsub(/ {2,}/, " ").chomp.strip
63
+ out.write("Host: #{j.target_host.hostname} ")
64
+ if ep.exception.respond_to? :id
65
+ out.write("Error: #{ep.exception.id}: ")
66
+ else
67
+ out.write(": ")
68
+ end
69
+ out.write("#{msg}\n")
70
+ end
71
+ end
72
+ end
73
+
74
+ def self.write_backtrace(e, args, config)
75
+ formatter = ErrorPrinter.new(wrapper: e, config: config)
76
+ out = StringIO.new
77
+ formatter.add_backtrace_header(out, args)
78
+ formatter.add_formatted_backtrace(out)
79
+ formatter.save_backtrace(out, config)
80
+ rescue => ex
81
+ dump_unexpected_error(ex)
82
+ end
83
+
84
+ # Use this to dump an an exception to output. useful
85
+ # if an error occurs in the error handling itself.
86
+ def self.dump_unexpected_error(e)
87
+ Terminal.output "INTERNAL ERROR"
88
+ Terminal.output "-=" * 30
89
+ Terminal.output "Message:"
90
+ Terminal.output e.message if e.respond_to?(:message)
91
+ Terminal.output "Backtrace:"
92
+ Terminal.output e.backtrace if e.respond_to?(:backtrace)
93
+ Terminal.output "=-" * 30
94
+ end
95
+
96
+ def initialize(wrapper: nil, config: nil, exception: nil)
97
+ @exception = exception || wrapper.contained_exception
98
+ @target_host = wrapper.target_host || target_host
99
+ @command = @exception.respond_to?(:command) ? @exception.command : nil
100
+ @pastel = Pastel.new
101
+ @content = StringIO.new
102
+ @config = config
103
+ @id = if @exception.kind_of? ChefCore::Error
104
+ @exception.id
105
+ else
106
+ DEFAULT_ERROR_NO
107
+ end
108
+ @translation = ChefCore::Text::ErrorTranslation.new(id)
109
+ rescue => e
110
+ ErrorPrinter.dump_unexpected_error(e)
111
+ exit! 128
112
+ end
113
+
114
+ def format_error
115
+ if translation.decorations
116
+ format_decorated
117
+ else
118
+ format_undecorated
119
+ end
120
+ @content.string
121
+ end
122
+
123
+ def format_undecorated
124
+ @content << "\n"
125
+ @content << format_body()
126
+ if @command
127
+ @content << "\n"
128
+ @content << @command.usage
129
+ end
130
+ end
131
+
132
+ def format_decorated
133
+ @content << "\n"
134
+ @content << format_header()
135
+ @content << "\n\n"
136
+ @content << format_body()
137
+ @content << "\n"
138
+ @content << format_footer()
139
+ @content << "\n"
140
+ end
141
+
142
+ def format_header
143
+ pastel.decorate(@id, :bold)
144
+ end
145
+
146
+ def format_body
147
+ if exception.kind_of? ChefCore::Error
148
+ format_workstation_exception
149
+ elsif exception.kind_of? Train::Error
150
+ format_train_exception
151
+ else
152
+ format_other_exception
153
+ end
154
+ end
155
+
156
+ def format_footer
157
+ if translation.log
158
+ if translation.stack
159
+ t.footer.both(config[:log_location], config[:stack_trace_path])
160
+ else
161
+ t.footer.log_only(config[:log_location])
162
+ end
163
+ else
164
+ if translation.stack
165
+ t.footer.stack_only
166
+ else
167
+ t.footer.neither
168
+ end
169
+ end
170
+ end
171
+
172
+ def add_backtrace_header(out, args)
173
+ out.write("\n#{"-" * 80}\n")
174
+ out.print("#{Time.now}: Error encountered while running the following:\n")
175
+ out.print(" #{args.join(' ')}\n")
176
+ out.print("Backtrace:\n")
177
+ end
178
+
179
+ def save_backtrace(output, config)
180
+ File.open(config[:stack_trace_path], "ab+") do |f|
181
+ f.write(output.string)
182
+ end
183
+ end
184
+
185
+ def self.error_summary(e)
186
+ if e.kind_of? ChefCore::Error
187
+ # By convention, all of our defined messages have a short summary on the first line.
188
+ ChefCore::Text.errors.send(e.id).text(*e.params).split("\n").first
189
+ elsif e.kind_of? String
190
+ e
191
+ else
192
+ if e.respond_to? :message
193
+ e.message
194
+ else
195
+ ChefCore::Text.errors.UNKNOWN
196
+ end
197
+ end
198
+ end
199
+
200
+ def format_workstation_exception
201
+ params = exception.params
202
+ t.send(@id).text(*params)
203
+ end
204
+
205
+ # TODO this gets moved to trainerrormapper or simply removed since
206
+ # many of these issues are now handled in the RemoteTarget::ConnectionFailure
207
+ def format_train_exception
208
+ backend, host = formatted_host()
209
+ if host.nil?
210
+ t.CHEFTRN002.text(exception.message)
211
+ else
212
+ t.CHEFTRN001.text(backend, host, exception.message)
213
+ end
214
+ end
215
+
216
+ def format_other_exception
217
+ t.send(DEFAULT_ERROR_NO).text(exception.message)
218
+ end
219
+
220
+ def formatted_host
221
+ return nil if target_host.nil?
222
+ cfg = target_host.config
223
+ port = cfg[:port].nil? ? "" : ":#{cfg[:port]}"
224
+ user = cfg[:user].nil? ? "" : "#{cfg[:user]}@"
225
+ "#{user}#{target_host.hostname}#{port}"
226
+ end
227
+
228
+ # mostly copied from
229
+ # https://gist.github.com/stanio/13d74294ca1868fed7fb
230
+ def add_formatted_backtrace(out)
231
+ _format_single(out, exception)
232
+ current_backtrace = exception.backtrace
233
+ cause = exception.cause
234
+ until cause.nil?
235
+ cause_trace = _unique_trace(cause.backtrace.to_a, current_backtrace)
236
+ out.print "Caused by: "
237
+ _format_single(out, cause, cause_trace)
238
+ backtrace_length = cause.backtrace.length
239
+ if backtrace_length > cause_trace.length
240
+ out.print "\t... #{backtrace_length - cause_trace.length} more"
241
+ end
242
+ out.print "\n"
243
+ current_backtrace = cause.backtrace
244
+ cause = cause.cause
245
+ end
246
+ end
247
+
248
+ def _format_single(out, exception, backtrace = nil)
249
+ out.puts "#{exception.class}: #{exception.message}"
250
+ backtrace ||= exception.backtrace.to_a
251
+ backtrace.each { |trace| out.puts "\t#{trace}" }
252
+ end
253
+
254
+ def _unique_trace(backtrace1, backtrace2)
255
+ i = 1
256
+ while i <= backtrace1.size && i <= backtrace2.size
257
+ break if backtrace1[-i] != backtrace2[-i]
258
+ i += 1
259
+ end
260
+ backtrace1[0..-i]
261
+ end
262
+ end
263
+
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,80 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefCore
19
+ module CLIUX
20
+ module UI
21
+ class PlainTextElement
22
+ def initialize(format, opts)
23
+ @orig_format = format
24
+ @format = format
25
+ @output = opts[:output]
26
+ end
27
+
28
+ def run(&block)
29
+ yield
30
+ end
31
+
32
+ def update(params)
33
+ # Some of this is particular to our usage -
34
+ # prefix does not cause a text update, but does
35
+ # change the prefix for future messages.
36
+ if params.key?(:prefix)
37
+ @format = @orig_format.gsub(":prefix", params[:prefix])
38
+ return
39
+ end
40
+
41
+ if @succ
42
+ ind = "OK"
43
+ @succ = false
44
+ log_method = :info
45
+ elsif @err
46
+ ind = "ERR"
47
+ @err = false
48
+ log_method = :error
49
+ else
50
+ log_method = :debug
51
+ ind = " - "
52
+ end
53
+
54
+ # Since this is a generic type, we can replace any component
55
+ # name in this regex - but for now :spinner is the only component
56
+ # we're standing in for.
57
+ msg = @format.gsub(/:spinner/, ind)
58
+ params.each_pair do |k, v|
59
+ msg.gsub!(/:#{k}/, v)
60
+ end
61
+ ChefCore::Log.send(log_method, msg)
62
+ @output.puts(msg)
63
+ end
64
+
65
+ def error
66
+ @err = true
67
+ @succ = false
68
+ end
69
+
70
+ def success
71
+ @succ = true
72
+ @err = false
73
+ end
74
+
75
+ def auto_spin
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,48 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ require "thread"
18
+ require "chef_core/cliux/ui/plain_text_element"
19
+
20
+ module ChefCore
21
+ module CLIUX
22
+ module UI
23
+ class PlainTextHeader
24
+ def initialize(format, opts)
25
+ @format = format
26
+ @output = opts[:output]
27
+ @children = {}
28
+ @threads = []
29
+ end
30
+
31
+ def register(child_format, child_opts, &block)
32
+ child_opts[:output] = @output
33
+ child = PlainTextElement.new(child_format, child_opts)
34
+ @children[child] = block
35
+ end
36
+
37
+ def auto_spin
38
+ msg = @format.gsub(/:spinner/, " HEADER ")
39
+ @output.puts(msg)
40
+ @children.each do |child, block|
41
+ @threads << Thread.new { block.call(child) }
42
+ end
43
+ @threads.each { |thr| thr.join }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefCore
19
+ module CLIUX
20
+ module UI
21
+ class Terminal
22
+ class Job
23
+ attr_reader :proc, :prefix, :target_host, :exception
24
+ def initialize(prefix, target_host, &block)
25
+ @proc = block
26
+ @prefix = prefix
27
+ @target_host = target_host
28
+ @error = nil
29
+ end
30
+
31
+ def run(reporter)
32
+ @proc.call(reporter)
33
+ rescue => e
34
+ reporter.error(e.to_s)
35
+ @exception = e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,103 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "tty-spinner"
19
+ require "tty-cursor"
20
+ require "chef_core/log"
21
+ require "chef_core/cliux/status_reporter"
22
+ require "chef_core/cliux/ui/plain_text_element"
23
+ require "chef_core/cliux/ui/plain_text_header"
24
+ require "chef_core/cliux/ui/terminal/job"
25
+
26
+ module ChefCore
27
+ module CLIUX
28
+ module UI
29
+ class Terminal
30
+ class << self
31
+ # To support matching in test
32
+ attr_accessor :location, :enable_spinners
33
+
34
+ def init(location, enable_spinners: false)
35
+ @enable_spinners = enable_spinners
36
+ @location = location
37
+ end
38
+
39
+ def write(msg)
40
+ @location.write(msg)
41
+ end
42
+
43
+ def output(msg)
44
+ @location.puts msg
45
+ end
46
+
47
+ def render_parallel_jobs(header, jobs)
48
+ # Do not indent the topmost 'parent' spinner, but do indent child spinners
49
+ indent_style = { top: "",
50
+ middle: TTY::Spinner::Multi::DEFAULT_INSET[:middle],
51
+ bottom: TTY::Spinner::Multi::DEFAULT_INSET[:bottom] }
52
+ # @option options [Hash] :style
53
+ # keys :top :middle and :bottom can contain Strings that are used to
54
+ # indent the spinners. Ignored if message is blank
55
+ multispinner = get_multispinner.new("[:spinner] #{header}",
56
+ output: @location,
57
+ hide_cursor: true,
58
+ style: indent_style)
59
+ jobs.each do |job|
60
+ multispinner.register(spinner_prefix(job.prefix), hide_cursor: true) do |spinner|
61
+ reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
62
+ job.run(reporter)
63
+ end
64
+ end
65
+ multispinner.auto_spin
66
+ ensure
67
+ # Spinners hide the cursor for better appearance, so we need to make sure
68
+ # we always bring it back
69
+ show_cursor
70
+ end
71
+
72
+ def render_job(initial_msg, job)
73
+ # TODO why do we have to pass prefix to both the spinner and the reporter?
74
+ spinner = get_spinner.new(spinner_prefix(job.prefix), output: @location, hide_cursor: true)
75
+ reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
76
+ reporter.update(initial_msg)
77
+ spinner.auto_spin
78
+ job.run(reporter)
79
+ end
80
+
81
+ def spinner_prefix(prefix)
82
+ spinner_msg = "[:spinner] "
83
+ spinner_msg += ":prefix " unless prefix.empty?
84
+ spinner_msg + ":status"
85
+ end
86
+
87
+ def get_multispinner
88
+ enable_spinners ? TTY::Spinner::Multi : PlainTextHeader
89
+ end
90
+
91
+ def get_spinner
92
+ # TODO bootstrap - these was as below, which seems backwards:
93
+ enable_spinners ? TTY::Spinner : PlainTextElement
94
+ end
95
+
96
+ def show_cursor
97
+ TTY::Cursor.show()
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,7 @@
1
+
2
+ require "chef_core/text"
3
+
4
+ module ChefCore
5
+ module CLIUX
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefCore
19
+ class Error < StandardError
20
+ attr_reader :id, :params
21
+ def initialize(id, *params)
22
+ @id = id
23
+ @params = params || []
24
+ end
25
+ end
26
+
27
+
28
+ class WrappedError < StandardError
29
+ attr_accessor :target_host, :contained_exception
30
+ def initialize(e, target_host)
31
+ super(e.message)
32
+ @contained_exception = e
33
+ @target_host = target_host
34
+ end
35
+ end
36
+
37
+ class MultiJobFailure < Error
38
+ attr_reader :jobs
39
+ def initialize(jobs)
40
+ super("CHEFMULTI001")
41
+ @jobs = jobs
42
+ end
43
+ end
44
+
45
+ # Provide a base type for internal usage errors that should not leak out
46
+ # but may anyway.
47
+ class APIError < Error
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ module ChefCore
2
+ module Errors
3
+ # Provides mappings of common errors that we don't explicitly
4
+ # handle, but can offer expanded help text around.
5
+ class StandardErrorResolver
6
+ def self.resolve_exception(exception)
7
+ deps
8
+ case exception
9
+ when OpenSSL::SSL::SSLError
10
+ if exception.message =~ /SSL.*verify failed.*/
11
+ id = "CHEFNET002"
12
+ end
13
+ when SocketError then id = "CHEFNET001"
14
+ end
15
+ if id.nil?
16
+ exception
17
+ else
18
+ ChefCore::Error.new(id, exception.message)
19
+ end
20
+ end
21
+
22
+ def self.wrap_exception(original, target_host = nil)
23
+ resolved_exception = resolve_exception(original)
24
+ WrappedError.new(resolved_exception, target_host)
25
+ end
26
+
27
+ def self.unwrap_exception(wrapper)
28
+ resolve_exception(wrapper.contained_exception)
29
+ end
30
+
31
+ def self.deps
32
+ # Avoid loading additional includes until they're needed
33
+ require "socket"
34
+ require "openssl"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,67 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "net/http"
19
+ require "uri"
20
+
21
+ module ChefCore
22
+ class FileFetcher
23
+ class << self
24
+ # Simple fetcher of an http(s) url. Returns the local path
25
+ # of the downloaded file.
26
+ def fetch(cache_path, path)
27
+ FileUtils.mkdir_p(cache_path)
28
+ url = URI.parse(path)
29
+ name = File.basename(url.path)
30
+ local_path = File.join(cache_path, name)
31
+
32
+ # TODO header check for size or checksum?
33
+ return local_path if File.exist?(local_path)
34
+
35
+ download_file(url, local_path)
36
+ local_path
37
+ end
38
+
39
+ def download_file(url, local_path)
40
+ temp_path = "#{local_path}.downloading"
41
+ file = open(temp_path, "wb")
42
+ ChefCore::Log.debug "Downloading: #{temp_path}"
43
+ Net::HTTP.start(url.host) do |http|
44
+ begin
45
+ http.request_get(url.path) do |resp|
46
+ resp.read_body do |segment|
47
+ file.write(segment)
48
+ end
49
+ end
50
+ rescue e
51
+ @error = true
52
+ raise
53
+ ensure
54
+ file.close()
55
+ # If any failures occurred, don't risk keeping
56
+ # an incomplete download that we'll see as 'cached'
57
+ if @error
58
+ FileUtils.rm_f(temp_path)
59
+ else
60
+ FileUtils.mv(temp_path, local_path)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end