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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/i18n/errors/en.yml +394 -0
- data/lib/chef_core/cliux/status_reporter.rb +43 -0
- data/lib/chef_core/cliux/ui/error_printer.rb +266 -0
- data/lib/chef_core/cliux/ui/plain_text_element.rb +80 -0
- data/lib/chef_core/cliux/ui/plain_text_header.rb +48 -0
- data/lib/chef_core/cliux/ui/terminal/job.rb +41 -0
- data/lib/chef_core/cliux/ui/terminal.rb +103 -0
- data/lib/chef_core/cliux.rb +7 -0
- data/lib/chef_core/error.rb +49 -0
- data/lib/chef_core/errors/standard_error_resolver.rb +38 -0
- data/lib/chef_core/file_fetcher.rb +67 -0
- data/lib/chef_core/log.rb +42 -0
- data/lib/chef_core/target_host/linux.rb +63 -0
- data/lib/chef_core/target_host/windows.rb +62 -0
- data/lib/chef_core/target_host.rb +351 -0
- data/lib/chef_core/target_resolver.rb +221 -0
- data/lib/chef_core/telemeter/patch.rb +32 -0
- data/lib/chef_core/telemeter/sender.rb +123 -0
- data/lib/chef_core/telemeter.rb +157 -0
- data/lib/chef_core/text/error_translation.rb +82 -0
- data/lib/chef_core/text/text_wrapper.rb +87 -0
- data/lib/chef_core/text.rb +79 -0
- data/lib/chef_core/version.rb +20 -0
- data/lib/chef_core.rb +3 -0
- data/resources/chef_run_reporter.rb +40 -0
- metadata +307 -0
@@ -0,0 +1,221 @@
|
|
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 "chef_core/target_host"
|
19
|
+
require "chef_core/error"
|
20
|
+
|
21
|
+
module ChefCore
|
22
|
+
|
23
|
+
# This class will resolve a provided target host name to
|
24
|
+
# one or more TargetHost instances. The target host name passed in
|
25
|
+
# to the constructor can contain up to two ranges in the form [0-9] and [a-z].
|
26
|
+
# Multiple target host names can be passed in asa comma-separated list.
|
27
|
+
#
|
28
|
+
# Use resolver_inst.targets to get the expanded list of TargetHost instances.
|
29
|
+
class TargetResolver
|
30
|
+
MAX_EXPANDED_TARGETS = 24
|
31
|
+
SUPPORTED_PROTOCOLS = %w{winrm ssh}
|
32
|
+
def initialize(target, default_protocol, conn_options, max_expanded_targets: MAX_EXPANDED_TARGETS)
|
33
|
+
@max_expanded_targets = max_expanded_targets
|
34
|
+
@default_proto = default_protocol
|
35
|
+
@unparsed_target = target
|
36
|
+
@split_targets = @unparsed_target.split(",")
|
37
|
+
@conn_options = conn_options.dup
|
38
|
+
@default_password = @conn_options.delete(:password)
|
39
|
+
@default_user = @conn_options.delete(:user)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the list of targets as an array of TargetHost instances
|
43
|
+
def targets
|
44
|
+
return @targets unless @targets.nil?
|
45
|
+
expanded_urls = []
|
46
|
+
@split_targets.each do |target|
|
47
|
+
expand_targets(target).each { |t| expanded_urls << t }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Apply max_expanded_targets to the total list of resolved
|
51
|
+
# targets, since multiple targets can be comma-separated.
|
52
|
+
# Limiting it only in the expression resolver could still
|
53
|
+
# yield more than the maximum.
|
54
|
+
if expanded_urls.length > @max_expanded_targets
|
55
|
+
raise TooManyTargets.new(expanded_urls.length, @max_expanded_targets)
|
56
|
+
end
|
57
|
+
|
58
|
+
@targets = expanded_urls.map do |url|
|
59
|
+
config = @conn_options.merge(config_for_target(url))
|
60
|
+
TargetHost.new(config.delete(:url), config)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def config_for_target(url)
|
65
|
+
prefix, target = prefix_from_target(url)
|
66
|
+
|
67
|
+
inline_password = nil
|
68
|
+
inline_user = nil
|
69
|
+
host = target
|
70
|
+
# Default greedy-scan of the regex means that
|
71
|
+
# $2 will resolve to content after the final "@"
|
72
|
+
# URL credentials will take precedence over the default :user
|
73
|
+
# in @conn_opts
|
74
|
+
if target =~ /(.*)@(.*)/
|
75
|
+
inline_credentials = $1
|
76
|
+
host = $2
|
77
|
+
# We'll use a non-greedy match to grab everthinmg up to the first ':'
|
78
|
+
# as username if there is no :, credentials is just the username
|
79
|
+
if inline_credentials =~ /(.+?):(.*)/
|
80
|
+
inline_user = $1
|
81
|
+
inline_password = $2
|
82
|
+
else
|
83
|
+
inline_user = inline_credentials
|
84
|
+
end
|
85
|
+
end
|
86
|
+
user, password = make_credentials(inline_user, inline_password)
|
87
|
+
{ url: "#{prefix}#{host}",
|
88
|
+
user: user,
|
89
|
+
password: password }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Merge the inline user/pass with the default user/pass, giving
|
93
|
+
# precedence to inline.
|
94
|
+
def make_credentials(inline_user, inline_password)
|
95
|
+
user = inline_user || @default_user
|
96
|
+
user = nil if user && user.empty?
|
97
|
+
password = (inline_password || @default_password)
|
98
|
+
password = nil if password && password.empty?
|
99
|
+
[user, password]
|
100
|
+
end
|
101
|
+
|
102
|
+
def prefix_from_target(target)
|
103
|
+
if target =~ /^(.+?):\/\/(.*)/
|
104
|
+
# We'll store the existing prefix to avoid it interfering
|
105
|
+
# with the check further below.
|
106
|
+
if SUPPORTED_PROTOCOLS.include? $1.downcase
|
107
|
+
prefix = "#{$1}://"
|
108
|
+
target = $2
|
109
|
+
else
|
110
|
+
raise UnsupportedProtocol.new($1)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
prefix = "#{@default_proto}://"
|
114
|
+
end
|
115
|
+
[prefix, target]
|
116
|
+
end
|
117
|
+
|
118
|
+
def expand_targets(target)
|
119
|
+
@current_target = target # Hold onto this for error reporting
|
120
|
+
do_parse([target.downcase])
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# A string matching PREFIX[x:y]POSTFIX:
|
126
|
+
# POSTFIX can contain further ranges itself
|
127
|
+
# This uses a greedy match (.*) to get include every character
|
128
|
+
# up to the last "[" in PREFIX
|
129
|
+
# $1 - prefix; $2 - x, $3 - y, $4 unproccessed/remaining text
|
130
|
+
TARGET_WITH_RANGE = /^(.*)\[([\p{Alnum}]+):([\p{Alnum}]+)\](.*)/.freeze
|
131
|
+
|
132
|
+
def do_parse(targets, depth = 0)
|
133
|
+
raise TooManyRanges.new(@current_target) if depth > 2
|
134
|
+
new_targets = []
|
135
|
+
done = false
|
136
|
+
targets.each do |target|
|
137
|
+
if TARGET_WITH_RANGE =~ target
|
138
|
+
# $1 - prefix; $2 - x, $3 - y, $4 unprocessed/remaining text
|
139
|
+
expand_range(new_targets, $1, $2, $3, $4)
|
140
|
+
else
|
141
|
+
# Nothing more to expand
|
142
|
+
done = true
|
143
|
+
new_targets << target
|
144
|
+
end
|
145
|
+
end
|
146
|
+
if done
|
147
|
+
new_targets
|
148
|
+
else
|
149
|
+
do_parse(new_targets, depth + 1)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def expand_range(dest, prefix, start, stop, suffix)
|
154
|
+
prefix ||= ""
|
155
|
+
suffix ||= ""
|
156
|
+
start_is_int = Integer(start) >= 0 rescue false
|
157
|
+
stop_is_int = Integer(stop) >= 0 rescue false
|
158
|
+
|
159
|
+
if (start_is_int && !stop_is_int) || (stop_is_int && !start_is_int)
|
160
|
+
raise InvalidRange.new(@current_target, "[#{start}:#{stop}]")
|
161
|
+
end
|
162
|
+
|
163
|
+
# Ensure that a numeric range doesn't get created as a string, which
|
164
|
+
# would make the created Range further below fail to iterate for some values
|
165
|
+
# because of ASCII sorting.
|
166
|
+
if start_is_int
|
167
|
+
start = Integer(start)
|
168
|
+
end
|
169
|
+
|
170
|
+
if stop_is_int
|
171
|
+
stop = Integer(stop)
|
172
|
+
end
|
173
|
+
|
174
|
+
# For range to iterate correctly, the values must
|
175
|
+
# be low,high
|
176
|
+
if start > stop
|
177
|
+
temp = stop; stop = start; start = temp
|
178
|
+
end
|
179
|
+
Range.new(start, stop).each do |value|
|
180
|
+
# Ranges will resolve only numbers and letters,
|
181
|
+
# not other ascii characters that happen to fall between.
|
182
|
+
if start_is_int || /^[a-z0-9]/ =~ value
|
183
|
+
dest << "#{prefix}#{value}#{suffix}"
|
184
|
+
end
|
185
|
+
# Stop expanding as soon as we go over limit to prevent
|
186
|
+
# making the user wait for a massive accidental expansion
|
187
|
+
if dest.length > @max_expanded_targets
|
188
|
+
raise TooManyTargets.new(@split_targets.length, @max_expanded_targets)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
# Provide an error base for all targetresolver errors, to simplify
|
193
|
+
# handling when caller is not concerned with the specific failure.
|
194
|
+
class TargetResolverError < ChefCore::Error; end
|
195
|
+
|
196
|
+
class InvalidRange < TargetResolverError
|
197
|
+
def initialize(unresolved_target, given_range)
|
198
|
+
super("CHEFRANGE001", unresolved_target, given_range)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
class TooManyRanges < TargetResolverError
|
203
|
+
def initialize(unresolved_target)
|
204
|
+
super("CHEFRANGE002", unresolved_target)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class TooManyTargets < TargetResolverError
|
209
|
+
def initialize(num_top_level_targets, max_targets)
|
210
|
+
super("CHEFRANGE003", num_top_level_targets, max_targets)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class UnsupportedProtocol < TargetResolverError
|
215
|
+
def initialize(attempted_protocol)
|
216
|
+
super("CHEFVAL011", attempted_protocol,
|
217
|
+
ChefCore::TargetResolver::SUPPORTED_PROTOCOLS.join(" "))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
+
class Telemetry
|
19
|
+
class Session
|
20
|
+
# The telemetry session data is normally kept in .chef, which we don't have.
|
21
|
+
def session_file
|
22
|
+
ChefCore::Config.telemetry_session_file.freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def deliver(data = {})
|
27
|
+
if ChefCore::Telemeter.instance.enabled?
|
28
|
+
payload = event.prepare(data)
|
29
|
+
client.await.fire(payload)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,123 @@
|
|
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 "telemetry"
|
19
|
+
require "chef_core/telemeter"
|
20
|
+
require "chef_core/telemeter/patch"
|
21
|
+
require "chef_core/log"
|
22
|
+
require "chef_core/version"
|
23
|
+
|
24
|
+
module ChefCore
|
25
|
+
class Telemeter
|
26
|
+
class Sender
|
27
|
+
attr_reader :session_files, :config
|
28
|
+
|
29
|
+
def self.start_upload_thread(config)
|
30
|
+
# Find the files before we spawn the thread - otherwise
|
31
|
+
# we may accidentally pick up the current run's session file if it
|
32
|
+
# finishes before the thread scans for new files
|
33
|
+
session_files = Sender.find_session_files(config)
|
34
|
+
sender = Sender.new(session_files, config)
|
35
|
+
Thread.new { sender.run }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_session_files(config)
|
39
|
+
ChefCore::Log.info("Looking for telemetry data to submit")
|
40
|
+
session_search = File.join(config[:payload_dir], "telemetry-payload-*.yml")
|
41
|
+
session_files = Dir.glob(session_search)
|
42
|
+
ChefCore::Log.info("Found #{session_files.length} sessions to submit")
|
43
|
+
session_files
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(session_files, config)
|
47
|
+
@session_files = session_files
|
48
|
+
@config = config
|
49
|
+
end
|
50
|
+
|
51
|
+
def run
|
52
|
+
if ChefCore::Telemeter.enabled?
|
53
|
+
ChefCore::Log.info("Telemetry enabled, beginning upload of previous session(s)")
|
54
|
+
# dev mode telemetry gets sent to a different location
|
55
|
+
|
56
|
+
if config[:dev_mode]
|
57
|
+
ENV["CHEF_TELEMETRY_ENDPOINT"] ||= "https://telemetry-acceptance.chef.io"
|
58
|
+
end
|
59
|
+
session_files.each { |path| process_session(path) }
|
60
|
+
else
|
61
|
+
# If telemetry is not enabled, just clean up and return. Even though
|
62
|
+
# the telemetry gem will not send if disabled, log output saying that we're submitting
|
63
|
+
# it when it has been disabled can be alarming.
|
64
|
+
ChefCore::Log.info("Telemetry disabled, clearing any existing session captures without sending them.")
|
65
|
+
session_files.each { |path| FileUtils.rm_rf(path) }
|
66
|
+
end
|
67
|
+
FileUtils.rm_rf(config[:session_file])
|
68
|
+
ChefCore::Log.info("Terminating, nothing more to do.")
|
69
|
+
rescue => e
|
70
|
+
ChefCore::Log.fatal "Sender thread aborted: '#{e}' failed at #{e.backtrace[0]}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_session(path)
|
74
|
+
ChefCore::Log.info("Processing telemetry entries from #{path}")
|
75
|
+
content = load_and_clear_session(path)
|
76
|
+
submit_session(content)
|
77
|
+
end
|
78
|
+
|
79
|
+
def submit_session(content)
|
80
|
+
# Each file contains the actions taken within a single run of the chef tool.
|
81
|
+
# Each run is one session, so we'll first remove remove the session file
|
82
|
+
# to force creating a new one.
|
83
|
+
FileUtils.rm_rf(config[:session_file])
|
84
|
+
# We'll use the version captured in the sesion file
|
85
|
+
entries = content["entries"]
|
86
|
+
cli_version = content["version"]
|
87
|
+
total = entries.length
|
88
|
+
telemetry = Telemetry.new(product: "chef-workstation",
|
89
|
+
origin: "command-line",
|
90
|
+
product_version: cli_version,
|
91
|
+
install_context: "omnibus")
|
92
|
+
total = entries.length
|
93
|
+
entries.each_with_index do |entry, x|
|
94
|
+
submit_entry(telemetry, entry, x + 1, total)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def submit_entry(telemetry, entry, sequence, total)
|
99
|
+
ChefCore::Log.info("Submitting telemetry entry #{sequence}/#{total}: #{entry} ")
|
100
|
+
telemetry.deliver(entry)
|
101
|
+
ChefCore::Log.info("Entry #{sequence}/#{total} submitted.")
|
102
|
+
rescue => e
|
103
|
+
# No error handling in telemetry lib, so at least track the failrue
|
104
|
+
ChefCore::Log.error("Failed to send entry #{sequence}/#{total}: #{e}")
|
105
|
+
ChefCore::Log.error("Backtrace: #{e.backtrace} ")
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def load_and_clear_session(path)
|
111
|
+
content = File.read(path)
|
112
|
+
# We'll remove it now instead of after we parse or submit it -
|
113
|
+
# if we fail to deliver, we don't want to be stuck resubmitting it if the problem
|
114
|
+
# was due to payload. This is a trade-off - if we get a transient error, the
|
115
|
+
# payload will be lost.
|
116
|
+
# TODO: Improve error handling so we can intelligently decide whether to
|
117
|
+
# retry a failed load or failed submit.
|
118
|
+
FileUtils.rm_rf(path)
|
119
|
+
YAML.load(content)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,157 @@
|
|
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 "benchmark"
|
19
|
+
require "forwardable"
|
20
|
+
require "singleton"
|
21
|
+
require "json"
|
22
|
+
require "digest/sha1"
|
23
|
+
require "securerandom"
|
24
|
+
require "yaml"
|
25
|
+
|
26
|
+
module ChefCore
|
27
|
+
|
28
|
+
# This definites the Telemeter interface. Implementation thoughts for
|
29
|
+
# when we unstub it:
|
30
|
+
# - let's track the call sequence; most of our calls will be nested inside
|
31
|
+
# a main 'timed_capture', and it would be good to see ordering within nested calls.
|
32
|
+
class Telemeter
|
33
|
+
include Singleton
|
34
|
+
DEFAULT_INSTALLATION_GUID = "00000000-0000-0000-0000-000000000000".freeze
|
35
|
+
|
36
|
+
class << self
|
37
|
+
extend Forwardable
|
38
|
+
def_delegators :instance, :setup, :timed_capture, :capture, :commit, :timed_run_capture
|
39
|
+
def_delegators :instance, :pending_event_count, :last_event, :enabled?
|
40
|
+
def_delegators :instance, :make_event_payload, :config
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :events_to_send, :run_timestamp, :config
|
44
|
+
|
45
|
+
def setup(config)
|
46
|
+
# TODO validate required & correct keys
|
47
|
+
# :payload_dir #required
|
48
|
+
# :session_file # required
|
49
|
+
# :installation_identifier_file # required
|
50
|
+
# :enabled # false, not required
|
51
|
+
# :dev_mode # false, not required
|
52
|
+
config[:dev_mode] ||= false
|
53
|
+
config[:enabled] ||= false
|
54
|
+
require "chef_core/telemeter/sender"
|
55
|
+
@config = config
|
56
|
+
Sender.start_upload_thread(config)
|
57
|
+
end
|
58
|
+
|
59
|
+
def enabled?
|
60
|
+
require "telemetry/decision"
|
61
|
+
config[:enabled] && !Telemetry::Decision.env_opt_out?
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@events_to_send = []
|
66
|
+
@run_timestamp = Time.now.utc.strftime("%FT%TZ")
|
67
|
+
end
|
68
|
+
|
69
|
+
def timed_run_capture(arguments, &block)
|
70
|
+
timed_capture(:run, arguments: arguments, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def timed_capture(name, data = {})
|
74
|
+
time = Benchmark.measure { yield }
|
75
|
+
data[:duration] = time.real
|
76
|
+
capture(name, data)
|
77
|
+
end
|
78
|
+
|
79
|
+
def capture(name, data = {})
|
80
|
+
# Adding it to the head of the list will ensure that the
|
81
|
+
# sequence of events is preserved when we send the final payload
|
82
|
+
payload = make_event_payload(name, data)
|
83
|
+
@events_to_send.unshift payload
|
84
|
+
end
|
85
|
+
|
86
|
+
def commit
|
87
|
+
if enabled?
|
88
|
+
session = convert_events_to_session
|
89
|
+
write_session(session)
|
90
|
+
end
|
91
|
+
@events_to_send = []
|
92
|
+
end
|
93
|
+
|
94
|
+
def make_event_payload(name, data)
|
95
|
+
{
|
96
|
+
event: name,
|
97
|
+
properties: {
|
98
|
+
installation_id: installation_id,
|
99
|
+
run_timestamp: run_timestamp,
|
100
|
+
host_platform: host_platform,
|
101
|
+
event_data: data,
|
102
|
+
},
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def installation_id
|
107
|
+
@installation_id ||=
|
108
|
+
begin
|
109
|
+
File.read(config[:installation_identifier_file]).chomp
|
110
|
+
rescue
|
111
|
+
ChefCore::Log.info "could not read #{config[:installation_identifier_file]} - using default id"
|
112
|
+
DEFAULT_INSTALLATION_GUID
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# For testing.
|
117
|
+
def pending_event_count
|
118
|
+
@events_to_send.length
|
119
|
+
end
|
120
|
+
|
121
|
+
def last_event
|
122
|
+
@events_to_send.last
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def host_platform
|
128
|
+
@host_platform ||= case RUBY_PLATFORM
|
129
|
+
when /mswin|mingw|windows/
|
130
|
+
"windows"
|
131
|
+
else
|
132
|
+
RUBY_PLATFORM.split("-")[1]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def convert_events_to_session
|
137
|
+
YAML.dump({ "version" => ChefCore::VERSION,
|
138
|
+
"entries" => @events_to_send })
|
139
|
+
end
|
140
|
+
|
141
|
+
def write_session(session)
|
142
|
+
File.write(next_filename, convert_events_to_session)
|
143
|
+
end
|
144
|
+
|
145
|
+
def next_filename
|
146
|
+
id = 0
|
147
|
+
filename = ""
|
148
|
+
loop do
|
149
|
+
id += 1
|
150
|
+
filename = File.join(config[:payload_dir], "telemetry-payload-#{id}.yml")
|
151
|
+
break unless File.exist?(filename)
|
152
|
+
end
|
153
|
+
filename
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,82 @@
|
|
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 Text
|
20
|
+
# Represents an error loaded from translation, with
|
21
|
+
# display attributes set.
|
22
|
+
class ErrorTranslation
|
23
|
+
ATTRIBUTES = [:decorations, :header, :footer, :stack, :log].freeze
|
24
|
+
attr_reader :message, *ATTRIBUTES
|
25
|
+
|
26
|
+
def initialize(id, params: [])
|
27
|
+
error_translation = error_translation_for_id(id)
|
28
|
+
|
29
|
+
options = YAML.load(Text.errors.display_defaults, "display_defaults",
|
30
|
+
symbolize_names: true)
|
31
|
+
|
32
|
+
# Display metadata is a string containing a YAML hash that is optionally under
|
33
|
+
# the error's 'options' attribute
|
34
|
+
# Note that we couldn't use :display, as that conflicts with a method on Object.
|
35
|
+
display_opts = if error_translation.methods.include?(:options)
|
36
|
+
YAML.load(error_translation.options, @id, symbolize_names: true)
|
37
|
+
else
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
options = options.merge(display_opts) unless display_opts.nil?
|
42
|
+
|
43
|
+
@message = error_translation.text(*params)
|
44
|
+
|
45
|
+
ATTRIBUTES.each do |attribute|
|
46
|
+
instance_variable_set("@#{attribute}", options.delete(attribute))
|
47
|
+
end
|
48
|
+
|
49
|
+
if options.length > 0
|
50
|
+
# Anything not in ATTRIBUTES is not supported. This will also catch
|
51
|
+
# typos in attr names
|
52
|
+
raise InvalidDisplayAttributes.new(id, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
inspection = "#{self}: "
|
58
|
+
ATTRIBUTES.each do |attribute|
|
59
|
+
inspection << "#{attribute}: #{send(attribute.to_s)}; "
|
60
|
+
end
|
61
|
+
inspection << "message: #{message.gsub("\n", "\\n")}"
|
62
|
+
inspection
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# This is split out for simplified unit testing of error formatting.
|
68
|
+
def error_translation_for_id(id)
|
69
|
+
Text.errors.send(id)
|
70
|
+
end
|
71
|
+
|
72
|
+
class InvalidDisplayAttributes < RuntimeError
|
73
|
+
attr_reader :invalid_attrs
|
74
|
+
def initialize(id, attrs)
|
75
|
+
@invalid_attrs = attrs
|
76
|
+
super("Invalid display attributes found for #{id}: #{attrs}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,87 @@
|
|
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 Text
|
20
|
+
# TextWrapper is a wrapper around R18n that returns all resolved values
|
21
|
+
# as Strings, and raise an error when a given i18n key is not found.
|
22
|
+
#
|
23
|
+
# This simplifies behaviors when interfacing with other libraries/components
|
24
|
+
# that don't play nicely with TranslatedString or Untranslated out of R18n.
|
25
|
+
class TextWrapper
|
26
|
+
def initialize(translation_tree)
|
27
|
+
@tree = translation_tree
|
28
|
+
@tree.translation_keys.each do |k|
|
29
|
+
# Integer keys are not translatable - they're quantity indicators in the key that
|
30
|
+
# are instead sent as arguments. If we see one here, it means it was not correctly
|
31
|
+
# labeled as plural with !!pl in the parent key
|
32
|
+
if k.class == Integer
|
33
|
+
raise MissingPlural.new(@tree.instance_variable_get(:@path), k)
|
34
|
+
end
|
35
|
+
k = k.to_sym
|
36
|
+
define_singleton_method k do |*args|
|
37
|
+
subtree = @tree.send(k, *args)
|
38
|
+
if subtree.methods.include?(:translation_keys) && !subtree.translation_keys.empty?
|
39
|
+
# If there are no more possible children, just return the translated value
|
40
|
+
TextWrapper.new(subtree)
|
41
|
+
else
|
42
|
+
subtree.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(name, *args)
|
49
|
+
raise InvalidKey.new(@tree.instance_variable_get(:@path), name)
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO - make the checks for these conditions lint steps that run during build
|
53
|
+
# instead of part of the shipped product.
|
54
|
+
class TextError < RuntimeError
|
55
|
+
attr_accessor :line
|
56
|
+
def set_call_context
|
57
|
+
# TODO - this can vary (8 isn't always right) - inspect
|
58
|
+
@line = caller(8, 1).first
|
59
|
+
if @line =~ /.*\/lib\/(.*\.rb):(\d+)/
|
60
|
+
@line = "File: #{$1} Line: #{$2}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class InvalidKey < TextError
|
66
|
+
def initialize(path, terminus)
|
67
|
+
set_call_context
|
68
|
+
# Calling back into Text here seems icky, this is an error
|
69
|
+
# that only engineering should see.
|
70
|
+
message = "i18n key #{path}.#{terminus} does not exist.\n"
|
71
|
+
message << " Referenced from #{line}"
|
72
|
+
super(message)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class MissingPlural < TextError
|
77
|
+
def initialize(path, terminus)
|
78
|
+
set_call_context
|
79
|
+
message = "i18n key #{path}.#{terminus} appears to reference a pluralization.\n"
|
80
|
+
message << " Please append the plural indicator '!!pl' to the end of #{path}.\n"
|
81
|
+
message << " Referenced from #{line}"
|
82
|
+
super(message)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|