chef-apply 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +423 -0
- data/LICENSE +201 -0
- data/README.md +41 -0
- data/Rakefile +32 -0
- data/bin/chef-run +23 -0
- data/chef-apply.gemspec +67 -0
- data/i18n/en.yml +513 -0
- data/lib/chef_apply.rb +20 -0
- data/lib/chef_apply/action/base.rb +158 -0
- data/lib/chef_apply/action/converge_target.rb +173 -0
- data/lib/chef_apply/action/install_chef.rb +30 -0
- data/lib/chef_apply/action/install_chef/base.rb +137 -0
- data/lib/chef_apply/action/install_chef/linux.rb +38 -0
- data/lib/chef_apply/action/install_chef/windows.rb +54 -0
- data/lib/chef_apply/action/reporter.rb +39 -0
- data/lib/chef_apply/cli.rb +470 -0
- data/lib/chef_apply/cli_options.rb +145 -0
- data/lib/chef_apply/config.rb +150 -0
- data/lib/chef_apply/error.rb +108 -0
- data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
- data/lib/chef_apply/file_fetcher.rb +70 -0
- data/lib/chef_apply/log.rb +42 -0
- data/lib/chef_apply/recipe_lookup.rb +117 -0
- data/lib/chef_apply/startup.rb +162 -0
- data/lib/chef_apply/status_reporter.rb +42 -0
- data/lib/chef_apply/target_host.rb +233 -0
- data/lib/chef_apply/target_resolver.rb +202 -0
- data/lib/chef_apply/telemeter.rb +162 -0
- data/lib/chef_apply/telemeter/patch.rb +32 -0
- data/lib/chef_apply/telemeter/sender.rb +121 -0
- data/lib/chef_apply/temp_cookbook.rb +159 -0
- data/lib/chef_apply/text.rb +77 -0
- data/lib/chef_apply/ui/error_printer.rb +261 -0
- data/lib/chef_apply/ui/plain_text_element.rb +75 -0
- data/lib/chef_apply/ui/terminal.rb +94 -0
- data/lib/chef_apply/version.rb +20 -0
- metadata +376 -0
@@ -0,0 +1,145 @@
|
|
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_apply/text"
|
19
|
+
require "chef_apply/action/install_chef"
|
20
|
+
|
21
|
+
# Moving the options into here so the cli.rb file is smaller and easier to read
|
22
|
+
# For options that need to be merged back into the global ChefApply::Config object
|
23
|
+
# we do that with a proc in the option itself. We decided to do that because it is
|
24
|
+
# an easy, straight forward way to merge those options when they do not directly
|
25
|
+
# map back to keys in the Config global. IE, we cannot just do
|
26
|
+
# `ChefApply::Config.merge!(options)` because the keys do not line up, and we do
|
27
|
+
# not want all CLI params merged back into the global config object.
|
28
|
+
# We know that the config is already loaded from the file (or program defaults)
|
29
|
+
# because the `Startup` class was invoked to start the program.
|
30
|
+
module ChefApply
|
31
|
+
module CLIOptions
|
32
|
+
|
33
|
+
T = ChefApply::Text.cli
|
34
|
+
TS = ChefApply::Text.status
|
35
|
+
|
36
|
+
def self.included(klass)
|
37
|
+
klass.banner T.description + "\n" + T.usage_full
|
38
|
+
|
39
|
+
klass.option :version,
|
40
|
+
short: "-v",
|
41
|
+
long: "--version",
|
42
|
+
description: T.version.description,
|
43
|
+
boolean: true
|
44
|
+
|
45
|
+
klass.option :help,
|
46
|
+
short: "-h",
|
47
|
+
long: "--help",
|
48
|
+
description: T.help.description,
|
49
|
+
boolean: true
|
50
|
+
|
51
|
+
# Special note:
|
52
|
+
# config_path is pre-processed in startup.rb, and is shown here only
|
53
|
+
# for purpoess of rendering help text.
|
54
|
+
klass.option :config_path,
|
55
|
+
short: "-c PATH",
|
56
|
+
long: "--config PATH",
|
57
|
+
description: T.default_config_location(ChefApply::Config.default_location),
|
58
|
+
default: ChefApply::Config.default_location,
|
59
|
+
proc: Proc.new { |path| ChefApply::Config.custom_location(path) }
|
60
|
+
|
61
|
+
klass.option :identity_file,
|
62
|
+
long: "--identity-file PATH",
|
63
|
+
short: "-i PATH",
|
64
|
+
description: T.identity_file,
|
65
|
+
proc: (Proc.new do |paths|
|
66
|
+
path = paths
|
67
|
+
unless File.exist?(path)
|
68
|
+
raise OptionValidationError.new("CHEFVAL001", self, path)
|
69
|
+
end
|
70
|
+
path
|
71
|
+
end)
|
72
|
+
|
73
|
+
klass.option :ssl,
|
74
|
+
long: "--[no-]ssl",
|
75
|
+
description: T.ssl.desc(ChefApply::Config.connection.winrm.ssl),
|
76
|
+
boolean: true,
|
77
|
+
default: ChefApply::Config.connection.winrm.ssl,
|
78
|
+
proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl(val) }
|
79
|
+
|
80
|
+
klass.option :ssl_verify,
|
81
|
+
long: "--[no-]ssl-verify",
|
82
|
+
description: T.ssl.verify_desc(ChefApply::Config.connection.winrm.ssl_verify),
|
83
|
+
boolean: true,
|
84
|
+
default: ChefApply::Config.connection.winrm.ssl_verify,
|
85
|
+
proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl_verify(val) }
|
86
|
+
|
87
|
+
klass.option :protocol,
|
88
|
+
long: "--protocol <PROTOCOL>",
|
89
|
+
short: "-p",
|
90
|
+
description: T.protocol_description(ChefApply::Config::SUPPORTED_PROTOCOLS.join(" "),
|
91
|
+
ChefApply::Config.connection.default_protocol),
|
92
|
+
default: ChefApply::Config.connection.default_protocol,
|
93
|
+
proc: Proc.new { |val| ChefApply::Config.connection.default_protocol(val) }
|
94
|
+
|
95
|
+
klass.option :user,
|
96
|
+
long: "--user <USER>",
|
97
|
+
description: T.user_description
|
98
|
+
|
99
|
+
klass.option :password,
|
100
|
+
long: "--password <PASSWORD>",
|
101
|
+
description: T.password_description
|
102
|
+
|
103
|
+
klass.option :cookbook_repo_paths,
|
104
|
+
long: "--cookbook-repo-paths PATH",
|
105
|
+
description: T.cookbook_repo_paths,
|
106
|
+
default: ChefApply::Config.chef.cookbook_repo_paths,
|
107
|
+
proc: (Proc.new do |paths|
|
108
|
+
paths = paths.split(",")
|
109
|
+
ChefApply::Config.chef.cookbook_repo_paths(paths)
|
110
|
+
paths
|
111
|
+
end)
|
112
|
+
|
113
|
+
klass.option :install,
|
114
|
+
long: "--[no-]install",
|
115
|
+
default: true,
|
116
|
+
boolean: true,
|
117
|
+
description: T.install_description(Action::InstallChef::Base::MIN_CHEF_VERSION)
|
118
|
+
|
119
|
+
klass.option :sudo,
|
120
|
+
long: "--[no-]sudo",
|
121
|
+
description: T.sudo.flag_description.sudo,
|
122
|
+
boolean: true,
|
123
|
+
default: true
|
124
|
+
|
125
|
+
klass.option :sudo_command,
|
126
|
+
long: "--sudo-command <COMMAND>",
|
127
|
+
default: "sudo",
|
128
|
+
description: T.sudo.flag_description.command
|
129
|
+
|
130
|
+
klass.option :sudo_password,
|
131
|
+
long: "--sudo-password <PASSWORD>",
|
132
|
+
description: T.sudo.flag_description.password
|
133
|
+
|
134
|
+
klass.option :sudo_options,
|
135
|
+
long: "--sudo-options 'OPTIONS...'",
|
136
|
+
description: T.sudo.flag_description.options
|
137
|
+
end
|
138
|
+
|
139
|
+
# I really don't like that mixlib-cli refers to the parsed command line flags in
|
140
|
+
# a hash accesed via the `config` method. Thats just such an overloaded word.
|
141
|
+
def parsed_options
|
142
|
+
config
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,150 @@
|
|
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_apply/log"
|
19
|
+
require "mixlib/config"
|
20
|
+
require "fileutils"
|
21
|
+
require "pathname"
|
22
|
+
require "chef-config/config"
|
23
|
+
require "chef-config/workstation_config_loader"
|
24
|
+
|
25
|
+
module ChefApply
|
26
|
+
class Config
|
27
|
+
WS_BASE_PATH = File.join(Dir.home, ".chef-workstation/")
|
28
|
+
SUPPORTED_PROTOCOLS = %w{ssh winrm}
|
29
|
+
|
30
|
+
class << self
|
31
|
+
@custom_location = nil
|
32
|
+
|
33
|
+
# Ensure when we extend Mixlib::Config that we load
|
34
|
+
# up the workstation config since we will need that
|
35
|
+
# to converge later
|
36
|
+
def initialize_mixlib_config
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def custom_location(path)
|
41
|
+
@custom_location = path
|
42
|
+
raise "No config file located at #{path}" unless exist?
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_location
|
46
|
+
File.join(WS_BASE_PATH, "config.toml")
|
47
|
+
end
|
48
|
+
|
49
|
+
def telemetry_path
|
50
|
+
File.join(WS_BASE_PATH, "telemetry")
|
51
|
+
end
|
52
|
+
|
53
|
+
def telemetry_session_file
|
54
|
+
File.join(telemetry_path, "TELEMETRY_SESSION_ID")
|
55
|
+
end
|
56
|
+
|
57
|
+
def telemetry_installation_identifier_file
|
58
|
+
File.join(WS_BASE_PATH, "installation_id")
|
59
|
+
end
|
60
|
+
|
61
|
+
def base_log_directory
|
62
|
+
File.dirname(log.location)
|
63
|
+
end
|
64
|
+
|
65
|
+
# These paths are relative to the log output path, which is user-configurable.
|
66
|
+
def error_output_path
|
67
|
+
File.join(base_log_directory, "errors.txt")
|
68
|
+
end
|
69
|
+
|
70
|
+
def stack_trace_path
|
71
|
+
File.join(base_log_directory, "stack-trace.log")
|
72
|
+
end
|
73
|
+
|
74
|
+
def using_default_location?
|
75
|
+
@custom_location.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
def location
|
79
|
+
using_default_location? ? default_location : @custom_location
|
80
|
+
end
|
81
|
+
|
82
|
+
def load
|
83
|
+
if exist?
|
84
|
+
from_file(location)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def exist?
|
89
|
+
File.exist? location
|
90
|
+
end
|
91
|
+
|
92
|
+
def reset
|
93
|
+
@custom_location = nil
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
extend Mixlib::Config
|
99
|
+
|
100
|
+
config_strict_mode true
|
101
|
+
|
102
|
+
# When working on Chef Apply itself,
|
103
|
+
# developers should set telemetry.dev to true
|
104
|
+
# in their local configuration to ensure that dev usage
|
105
|
+
# doesn't skew customer telemetry.
|
106
|
+
config_context :telemetry do
|
107
|
+
default(:dev, false)
|
108
|
+
default(:enable, true)
|
109
|
+
end
|
110
|
+
|
111
|
+
config_context :log do
|
112
|
+
default(:level, "warn")
|
113
|
+
default(:location, File.join(WS_BASE_PATH, "logs/default.log"))
|
114
|
+
end
|
115
|
+
|
116
|
+
config_context :cache do
|
117
|
+
default(:path, File.join(WS_BASE_PATH, "cache"))
|
118
|
+
end
|
119
|
+
|
120
|
+
config_context :connection do
|
121
|
+
default(:default_protocol, "ssh")
|
122
|
+
default(:default_user, nil)
|
123
|
+
|
124
|
+
config_context :winrm do
|
125
|
+
default(:ssl, false)
|
126
|
+
default(:ssl_verify, true)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
config_context :dev do
|
131
|
+
default(:spinner, "TTY::Spinner")
|
132
|
+
end
|
133
|
+
|
134
|
+
config_context :chef do
|
135
|
+
# We want to use any configured chef repo paths or trusted certs in
|
136
|
+
# ~/.chef/knife.rb on the user's workstation. But because they could have
|
137
|
+
# config that could mess up our Policyfile creation later we reset the
|
138
|
+
# ChefConfig back to default after loading that.
|
139
|
+
ChefConfig::WorkstationConfigLoader.new(nil, ChefApply::Log).load
|
140
|
+
default(:cookbook_repo_paths, [ChefConfig::Config[:cookbook_path]].flatten)
|
141
|
+
default(:trusted_certs_dir, ChefConfig::Config[:trusted_certs_dir])
|
142
|
+
ChefConfig::Config.reset
|
143
|
+
end
|
144
|
+
|
145
|
+
config_context :data_collector do
|
146
|
+
default :url, nil
|
147
|
+
default :token, nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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
|
+
class Error < StandardError
|
20
|
+
attr_reader :id, :params
|
21
|
+
attr_accessor :show_stack, :show_log, :decorate
|
22
|
+
def initialize(id, *params)
|
23
|
+
@id = id
|
24
|
+
@params = params || []
|
25
|
+
@show_log = true
|
26
|
+
@show_stack = true
|
27
|
+
@decorate = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ErrorNoLogs < Error
|
32
|
+
def initialize(id, *params)
|
33
|
+
super
|
34
|
+
@show_log = false
|
35
|
+
@show_stack = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ErrorNoStack < Error
|
40
|
+
def initialize(id, *params)
|
41
|
+
super
|
42
|
+
@show_log = true
|
43
|
+
@show_stack = false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class WrappedError < StandardError
|
48
|
+
attr_accessor :target_host, :contained_exception
|
49
|
+
def initialize(e, target_host)
|
50
|
+
super(e.message)
|
51
|
+
@contained_exception = e
|
52
|
+
@target_host = target_host
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class MultiJobFailure < ChefApply::ErrorNoLogs
|
57
|
+
attr_reader :jobs
|
58
|
+
def initialize(jobs)
|
59
|
+
super("CHEFMULTI001")
|
60
|
+
@jobs = jobs
|
61
|
+
@decorate = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Provides mappings of common errors that we don't explicitly
|
66
|
+
# handle, but can offer expanded help text around.
|
67
|
+
class StandardErrorResolver
|
68
|
+
|
69
|
+
def self.resolve_exception(exception)
|
70
|
+
deps
|
71
|
+
show_log = true
|
72
|
+
show_stack = true
|
73
|
+
case exception
|
74
|
+
when OpenSSL::SSL::SSLError
|
75
|
+
if exception.message =~ /SSL.*verify failed.*/
|
76
|
+
id = "CHEFNET002"
|
77
|
+
show_log = false
|
78
|
+
show_stack = false
|
79
|
+
end
|
80
|
+
when SocketError then id = "CHEFNET001"; show_log = false; show_stack = false
|
81
|
+
end
|
82
|
+
if id.nil?
|
83
|
+
exception
|
84
|
+
else
|
85
|
+
e = ChefApply::Error.new(id, exception.message)
|
86
|
+
e.show_log = show_log
|
87
|
+
e.show_stack = show_stack
|
88
|
+
e
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.wrap_exception(original, target_host = nil)
|
93
|
+
resolved_exception = resolve_exception(original)
|
94
|
+
WrappedError.new(resolved_exception, target_host)
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.unwrap_exception(wrapper)
|
98
|
+
resolve_exception(wrapper.contained_exception)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.deps
|
102
|
+
# Avoid loading additional includes until they're needed
|
103
|
+
require "socket"
|
104
|
+
require "openssl"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,93 @@
|
|
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 "chef_apply/error"
|
19
|
+
|
20
|
+
module ChefApply::Errors
|
21
|
+
class CCRFailureMapper
|
22
|
+
attr_reader :params
|
23
|
+
|
24
|
+
def initialize(exception, params)
|
25
|
+
@params = params
|
26
|
+
@cause_line = exception
|
27
|
+
end
|
28
|
+
|
29
|
+
def raise_mapped_exception!
|
30
|
+
if @cause_line.nil?
|
31
|
+
raise RemoteChefRunFailedToResolveError.new(params[:stdout], params[:stderr])
|
32
|
+
else
|
33
|
+
errid, *args = exception_args_from_cause()
|
34
|
+
if errid.nil?
|
35
|
+
raise RemoteChefClientRunFailedUnknownReason.new()
|
36
|
+
else
|
37
|
+
raise RemoteChefClientRunFailed.new(errid, *args)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Ideally we will write a custom handler to package up data we care
|
44
|
+
# about and present it more directly https://docs.chef.io/handlers.html
|
45
|
+
# For now, we'll just match the most common failures based on their
|
46
|
+
# messages.
|
47
|
+
def exception_args_from_cause
|
48
|
+
# Ordering is important below. Some earlier tests are more detailed
|
49
|
+
# cases of things that will match more general tests further down.
|
50
|
+
case @cause_line
|
51
|
+
when /.*had an error:(.*:)\s+(.*$)/
|
52
|
+
# Some invalid property value cases, among others.
|
53
|
+
["CHEFCCR002", $2]
|
54
|
+
when /.*Chef::Exceptions::ValidationFailed:\s+Option action must be equal to one of:\s+(.*)!\s+You passed :(.*)\./
|
55
|
+
# Invalid action - specialization of invalid property value, below
|
56
|
+
["CHEFCCR003", $2, $1]
|
57
|
+
when /.*Chef::Exceptions::ValidationFailed:\s+(.*)/
|
58
|
+
# Invalid resource property value
|
59
|
+
["CHEFCCR004", $1]
|
60
|
+
when /.*NameError: undefined local variable or method `(.+)' for cookbook.+/
|
61
|
+
# Invalid resource type in most cases
|
62
|
+
["CHEFCCR005", $1]
|
63
|
+
when /.*NoMethodError: undefined method `(.+)' for cookbook.+/
|
64
|
+
# Invalid resource type in most cases
|
65
|
+
["CHEFCCR005", $1]
|
66
|
+
when /.*undefined method `(.*)' for (.+)/
|
67
|
+
# Unknown resource property
|
68
|
+
["CHEFCCR006", $1, $2]
|
69
|
+
|
70
|
+
# Below would catch the general form of most errors, but the
|
71
|
+
# message itself in those lines is not generally aligned
|
72
|
+
# with the UX we want to provide.
|
73
|
+
# when /.*Exception|Error.*:\s+(.*)/
|
74
|
+
else
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class RemoteChefClientRunFailed < ChefApply::ErrorNoLogs
|
80
|
+
def initialize(id, *args); super(id, *args); end
|
81
|
+
end
|
82
|
+
|
83
|
+
class RemoteChefClientRunFailedUnknownReason < ChefApply::ErrorNoStack
|
84
|
+
def initialize(); super("CHEFCCR099"); end
|
85
|
+
end
|
86
|
+
|
87
|
+
class RemoteChefRunFailedToResolveError < ChefApply::ErrorNoStack
|
88
|
+
def initialize(stdout, stderr); super("CHEFCCR001", stdout, stderr); end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|