chef-apply 0.1.21 → 0.1.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -40,7 +40,7 @@ module ChefApply::Action
40
40
  c = target_host.run_command(cmd_str)
41
41
  target_host.run_command!("#{delete_folder} #{remote_dir_path}")
42
42
  if c.exit_status == 0
43
- ChefApply::Log.debug(c.stdout)
43
+ ChefApply::Log.info(c.stdout)
44
44
  notify(:success)
45
45
  elsif c.exit_status == 35
46
46
  notify(:reboot)
@@ -79,6 +79,16 @@ module ChefApply::Action
79
79
  exception_handlers << reporter
80
80
  EOM
81
81
 
82
+ # add the target host's log level value
83
+ # (we don't set a location because we want output to
84
+ # go in stdout for reporting back to chef-apply)
85
+ log_settings = ChefApply::Config.log
86
+ if !log_settings.target_level.nil?
87
+ workstation_rb << <<~EOM
88
+ log_level :#{log_settings.target_level}
89
+ EOM
90
+ end
91
+
82
92
  # Maybe add data collector endpoint.
83
93
  dc = ChefApply::Config.data_collector
84
94
  if !dc.url.nil? && !dc.token.nil?
@@ -35,6 +35,7 @@ require "chef_apply/target_resolver"
35
35
  require "chef_apply/telemeter"
36
36
  require "chef_apply/ui/error_printer"
37
37
  require "chef_apply/ui/terminal"
38
+ require "chef_apply/ui/terminal/job"
38
39
 
39
40
  module ChefApply
40
41
  class CLI
@@ -130,12 +131,16 @@ module ChefApply
130
131
  end
131
132
 
132
133
  def render_cookbook_setup(arguments)
133
- UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
134
+ # TODO update Job so that it doesn't require prefix and host. As a data container,
135
+ # should these attributes even be required?
136
+ job = UI::Terminal::Job.new("", nil) do |reporter|
134
137
  @temp_cookbook = generate_temp_cookbook(arguments, reporter)
135
138
  end
136
- UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
139
+ UI::Terminal.render_job("...", job)
140
+ job = UI::Terminal::Job.new("", nil) do |reporter|
137
141
  @archive_file_location = generate_local_policy(reporter)
138
142
  end
143
+ UI::Terminal.render_job("...", job)
139
144
  end
140
145
 
141
146
  def render_converge(target_hosts)
@@ -111,6 +111,8 @@ module ChefApply
111
111
  config_context :log do
112
112
  default(:level, "warn")
113
113
  default(:location, File.join(WS_BASE_PATH, "logs/default.log"))
114
+ # set the log level for the target host's chef-client run
115
+ default(:target_level, nil)
114
116
  end
115
117
 
116
118
  config_context :cache do
@@ -128,7 +130,7 @@ module ChefApply
128
130
  end
129
131
 
130
132
  config_context :dev do
131
- default(:spinner, "TTY::Spinner")
133
+ default(:spinner, true)
132
134
  end
133
135
 
134
136
  config_context :chef do
@@ -34,6 +34,10 @@ module ChefApply
34
34
  end
35
35
 
36
36
  def run
37
+ # This component is not supported in ChefDK; an exception will be raised
38
+ # if running in that context.
39
+ verify_not_in_chefdk
40
+
37
41
  # Some tasks we do only once in an installation:
38
42
  first_run_tasks
39
43
 
@@ -68,6 +72,8 @@ module ChefApply
68
72
  UI::Terminal.output(T.error.bad_config_file(e.path))
69
73
  rescue ConfigPathNotProvided
70
74
  UI::Terminal.output(T.error.missing_config_path)
75
+ rescue UnsupportedInstallation
76
+ UI::Terminal.output(T.error.unsupported_installation)
71
77
  rescue Mixlib::Config::UnknownConfigOptionError => e
72
78
  # Ideally we'd update the exception in mixlib to include
73
79
  # a field with the faulty value, line number, and nested context -
@@ -89,6 +95,15 @@ module ChefApply
89
95
  UI::Terminal.init($stdout)
90
96
  end
91
97
 
98
+ # Verify that chef-run gem is not executing out of ChefDK by checking the
99
+ # runtime path of this file.
100
+ #
101
+ # NOTE: This is imperfect - someone could theoretically
102
+ # install chefdk to a path other than the default.
103
+ def verify_not_in_chefdk
104
+ raise UnsupportedInstallation.new if script_path =~ /chefdk/
105
+ end
106
+
92
107
  def first_run_tasks
93
108
  return if Dir.exist?(Config::WS_BASE_PATH)
94
109
  create_default_config
@@ -173,6 +188,13 @@ module ChefApply
173
188
  require "chef_apply/cli"
174
189
  ChefApply::CLI.new(@argv).run
175
190
  end
191
+
192
+ private
193
+
194
+ def script_path
195
+ File.expand_path File.dirname(__FILE__)
196
+ end
197
+
176
198
  class ConfigPathNotProvided < StandardError; end
177
199
  class ConfigPathInvalid < StandardError
178
200
  attr_reader :path
@@ -180,5 +202,6 @@ module ChefApply
180
202
  @path = path
181
203
  end
182
204
  end
205
+ class UnsupportedInstallation < StandardError; end
183
206
  end
184
207
  end
@@ -45,6 +45,12 @@ module ChefApply
45
45
  sudo: opts_in[:sudo] === false ? false : true,
46
46
  www_form_encoded_password: true,
47
47
  key_files: opts_in[:identity_file],
48
+ non_interactive: true,
49
+ # Prevent long delays due to retries on auth failure.
50
+ # This does reduce the number of attempts we'll make for transient conditions as well, but
51
+ # train does not currently exposes these as separate controls. Ideally I'd like to see a 'retry_on_auth_failure' option.
52
+ connection_retries: 2,
53
+ connection_retry_sleep: 0.15,
48
54
  logger: ChefApply::Log }
49
55
  if opts_in.has_key? :ssl
50
56
  connection_opts[:ssl] = opts_in[:ssl]
@@ -17,11 +17,20 @@
17
17
 
18
18
  require "r18n-desktop"
19
19
  require "chef_apply/text/text_wrapper"
20
+ require "chef_apply/text/error_translation"
20
21
 
21
22
  # A very thin wrapper around R18n, the idea being that we're likely to replace r18n
22
23
  # down the road and don't want to have to change all of our commands.
23
24
  module ChefApply
24
25
  module Text
26
+ def self._error_table
27
+ # Though ther may be several translations, en.yml will be the only one with
28
+ # error metadata.
29
+ path = File.join(_translation_path, "errors", "en.yml")
30
+ raw_yaml = File.read(path)
31
+ @error_table ||= YAML.load(raw_yaml, _translation_path, symbolize_names: true)[:errors]
32
+ end
33
+
25
34
  def self._translation_path
26
35
  @translation_path ||= File.join(File.dirname(__FILE__), "..", "..", "i18n")
27
36
  end
@@ -38,8 +47,7 @@ module ChefApply
38
47
  end
39
48
  end
40
49
 
41
- # We need to do this when the class is loaded to avoid breaking a bunch of behaviors for now.
50
+ # Load on class load to ensure our text accessor methods are available from the start.
42
51
  load
43
52
  end
44
-
45
53
  end
@@ -0,0 +1,69 @@
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 ChefApply
19
+ module Text
20
+ class ErrorTranslation
21
+ ATTRIBUTES = :decorations, :header, :footer, :stack, :log
22
+ attr_reader :message, *ATTRIBUTES
23
+
24
+ def initialize(id, params: [])
25
+ # To get access to the metadata we'll go directly through the parsed yaml.
26
+ # Accessing via R18n is unnecessarily complicated
27
+ yml = Text._error_table
28
+
29
+ # We'll still use our Text mechanism for the text itself so that
30
+ # parameters, pluralization, etc will still work.
31
+ # This will raise if the key doesn't exist.
32
+ @message = Text.errors.send(id).text(*params)
33
+ options = yml[:display_defaults]
34
+
35
+ # Override any defaults if display metadata is given
36
+ display_opts = yml[id.to_sym][:display]
37
+ options = options.merge(display_opts) unless display_opts.nil?
38
+
39
+ ATTRIBUTES.each do |attribute|
40
+ instance_variable_set("@#{attribute}", options.delete(attribute))
41
+ end
42
+
43
+ if options.length > 0
44
+ # Anything not in ATTRIBUTES is not supported. This will also catch
45
+ # typos in attr names
46
+ raise InvalidDisplayAttributes.new(id, options)
47
+ end
48
+ end
49
+
50
+ def inspect
51
+ inspection = "#{self}: "
52
+ ATTRIBUTES.each do |attribute|
53
+ inspection << "#{attribute}: #{send(attribute.to_s)}; "
54
+ end
55
+ inspection << "message: #{message.gsub("\n", "\\n")}"
56
+ inspection
57
+ end
58
+
59
+ class InvalidDisplayAttributes < RuntimeError
60
+ attr_reader :invalid_attrs
61
+ def initialize(id, attrs)
62
+ @invalid_attrs = attrs
63
+ super("Invalid display attributes found for #{id}: #{attrs}")
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -25,7 +25,8 @@ require "chef_apply/errors/standard_error_resolver"
25
25
 
26
26
  module ChefApply::UI
27
27
  class ErrorPrinter
28
- attr_reader :id, :pastel, :show_log, :show_stack, :exception, :target_host
28
+ attr_reader :id, :pastel, :translation, :exception, :target_host
29
+
29
30
  # TODO define 't' as a method is a temporary workaround
30
31
  # to ensure that text key lookups are testable.
31
32
  def t
@@ -91,27 +92,22 @@ module ChefApply::UI
91
92
  def initialize(wrapper, unwrapped = nil, target_host = nil)
92
93
  @exception = unwrapped || wrapper.contained_exception
93
94
  @target_host = wrapper.target_host || target_host
95
+ @command = exception.respond_to?(:command) ? exception.command : nil
94
96
  @pastel = Pastel.new
95
- @show_log = exception.respond_to?(:show_log) ? exception.show_log : true
96
- @show_stack = exception.respond_to?(:show_stack) ? exception.show_stack : true
97
97
  @content = StringIO.new
98
- @command = exception.respond_to?(:command) ? exception.command : nil
99
- @id = DEFAULT_ERROR_NO
100
- if exception.respond_to?(:id) && exception.id =~ /CHEF.*/
101
- @id = exception.id
102
- end
103
- if exception.respond_to?(:decorate)
104
- @decorate = exception.decorate
105
- else
106
- @decorate = true
107
- end
98
+ @id = if exception.kind_of? ChefApply::Error
99
+ exception.id
100
+ else
101
+ DEFAULT_ERROR_NO
102
+ end
103
+ @translation = ChefApply::Text::ErrorTranslation.new(id)
108
104
  rescue => e
109
105
  ErrorPrinter.dump_unexpected_error(e)
110
106
  exit! 128
111
107
  end
112
108
 
113
109
  def format_error
114
- if @decorate
110
+ if translation.decorations
115
111
  format_decorated
116
112
  else
117
113
  format_undecorated
@@ -153,15 +149,15 @@ module ChefApply::UI
153
149
  end
154
150
 
155
151
  def format_footer
156
- if show_log
157
- if show_stack
152
+ if translation.log
153
+ if translation.stack
158
154
  t.footer.both(ChefApply::Config.log.location,
159
155
  ChefApply::Config.stack_trace_path)
160
156
  else
161
157
  t.footer.log_only(ChefApply::Config.log.location)
162
158
  end
163
159
  else
164
- if show_stack
160
+ if translation.stack
165
161
  t.footer.stack_only
166
162
  else
167
163
  t.footer.neither
@@ -185,7 +181,7 @@ module ChefApply::UI
185
181
  def self.error_summary(e)
186
182
  if e.kind_of? ChefApply::Error
187
183
  # By convention, all of our defined messages have a short summary on the first line.
188
- ChefApply::Text.errors.send(e.id, *e.params).split("\n").first
184
+ ChefApply::Text.errors.send(e.id).text(*e.params).split("\n").first
189
185
  elsif e.kind_of? String
190
186
  e
191
187
  else
@@ -199,20 +195,22 @@ module ChefApply::UI
199
195
 
200
196
  def format_workstation_exception
201
197
  params = exception.params
202
- t.send(@id, *params)
198
+ t.send(@id).text(*params)
203
199
  end
204
200
 
201
+ # TODO this gets moved to trainerrormapper or simply removed since
202
+ # many of these issues are now handled in the RemoteTarget::ConnectionFailure
205
203
  def format_train_exception
206
204
  backend, host = formatted_host()
207
205
  if host.nil?
208
- t.CHEFTRN002(exception.message)
206
+ t.CHEFTRN002.text(exception.message)
209
207
  else
210
- t.CHEFTRN001(backend, host, exception.message)
208
+ t.CHEFTRN001.text(backend, host, exception.message)
211
209
  end
212
210
  end
213
211
 
214
212
  def format_other_exception
215
- t.send(DEFAULT_ERROR_NO, exception.message)
213
+ t.send(DEFAULT_ERROR_NO).text(exception.message)
216
214
  end
217
215
 
218
216
  def formatted_host
@@ -29,7 +29,7 @@ module ChefApply
29
29
  end
30
30
 
31
31
  def update(params)
32
- # SOme of this is particular to our usage -
32
+ # Some of this is particular to our usage -
33
33
  # prefix does not cause a text update, but does
34
34
  # change the prefix for future messages.
35
35
  if params.has_key?(:prefix)
@@ -70,6 +70,9 @@ module ChefApply
70
70
  @succ = true
71
71
  @err = false
72
72
  end
73
+
74
+ def auto_spin
75
+ end
73
76
  end
74
77
  end
75
78
  end
@@ -0,0 +1,46 @@
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_apply/ui/plain_text_element"
19
+
20
+ module ChefApply
21
+ module UI
22
+ class PlainTextHeader
23
+ def initialize(format, opts)
24
+ @format = format
25
+ @output = opts[:output]
26
+ @children = {}
27
+ @threads = []
28
+ end
29
+
30
+ def register(child_format, child_opts, &block)
31
+ child_opts[:output] = @output
32
+ child = PlainTextElement.new(child_format, child_opts)
33
+ @children[child] = block
34
+ end
35
+
36
+ def auto_spin
37
+ msg = @format.gsub(/:spinner/, " HEADER ")
38
+ @output.puts(msg)
39
+ @children.each do |child, block|
40
+ @threads << Thread.new { block.call(child) }
41
+ end
42
+ @threads.each { |thr| thr.join }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -20,27 +20,11 @@ require "chef_apply/status_reporter"
20
20
  require "chef_apply/config"
21
21
  require "chef_apply/log"
22
22
  require "chef_apply/ui/plain_text_element"
23
+ require "chef_apply/ui/plain_text_header"
23
24
 
24
25
  module ChefApply
25
26
  module UI
26
27
  class Terminal
27
- class Job
28
- attr_reader :proc, :prefix, :target_host, :exception
29
- def initialize(prefix, target_host, &block)
30
- @proc = block
31
- @prefix = prefix
32
- @target_host = target_host
33
- @error = nil
34
- end
35
-
36
- def run(reporter)
37
- @proc.call(reporter)
38
- rescue => e
39
- reporter.error(e.to_s)
40
- @exception = e
41
- end
42
- end
43
-
44
28
  class << self
45
29
  # To support matching in test
46
30
  attr_accessor :location
@@ -58,31 +42,30 @@ module ChefApply
58
42
  end
59
43
 
60
44
  def render_parallel_jobs(header, jobs)
61
- # Do not indent the topmost 'parent' spinner, but do indent child spinners
45
+ # Do not indent the topmost 'parent' spinner, but do indent child spinners
62
46
  indent_style = { top: "",
63
47
  middle: TTY::Spinner::Multi::DEFAULT_INSET[:middle],
64
48
  bottom: TTY::Spinner::Multi::DEFAULT_INSET[:bottom] }
65
- # @option options [Hash] :style
66
- # keys :top :middle and :bottom can contain Strings that are used to
67
- # indent the spinners. Ignored if message is blank
68
- multispinner = TTY::Spinner::Multi.new("[:spinner] #{header}", output: @location, hide_cursor: true, style: indent_style)
69
- jobs.each do |a|
70
- multispinner.register(spinner_prefix(a.prefix), hide_cursor: true) do |spinner|
71
- reporter = StatusReporter.new(spinner, prefix: a.prefix, key: :status)
72
- a.run(reporter)
49
+ # @option options [Hash] :style
50
+ # keys :top :middle and :bottom can contain Strings that are used to
51
+ # indent the spinners. Ignored if message is blank
52
+ multispinner = get_multispinner.new("[:spinner] #{header}", output: @location, hide_cursor: true, style: indent_style)
53
+ jobs.each do |job|
54
+ multispinner.register(spinner_prefix(job.prefix), hide_cursor: true) do |spinner|
55
+ reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
56
+ job.run(reporter)
73
57
  end
74
58
  end
75
59
  multispinner.auto_spin
76
60
  end
77
61
 
78
- # TODO update this to accept a job instead of a block, for consistency of usage
79
- # between render_job and render_parallel
80
- def render_job(msg, prefix: "", &block)
81
- klass = ChefApply::UI.const_get(ChefApply::Config.dev.spinner)
82
- spinner = klass.new(spinner_prefix(prefix), output: @location, hide_cursor: true)
83
- reporter = StatusReporter.new(spinner, prefix: prefix, key: :status)
84
- reporter.update(msg)
85
- spinner.run { yield(reporter) }
62
+ def render_job(initial_msg, job)
63
+ # TODO why do we have to pass prefix to both the spinner and the reporter?
64
+ spinner = get_spinner.new(spinner_prefix(job.prefix), output: @location, hide_cursor: true)
65
+ reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
66
+ reporter.update(initial_msg)
67
+ spinner.auto_spin
68
+ job.run(reporter)
86
69
  end
87
70
 
88
71
  def spinner_prefix(prefix)
@@ -90,6 +73,14 @@ module ChefApply
90
73
  spinner_msg += ":prefix " unless prefix.empty?
91
74
  spinner_msg + ":status"
92
75
  end
76
+
77
+ def get_multispinner
78
+ ChefApply::Config.dev.spinner ? TTY::Spinner::Multi : PlainTextHeader
79
+ end
80
+
81
+ def get_spinner
82
+ ChefApply::Config.dev.spinner ? TTY::Spinner : PlainTextElement
83
+ end
93
84
  end
94
85
  end
95
86
  end