chef-apply 0.1.21 → 0.1.27

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.
@@ -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