summaryrefslogtreecommitdiff
path: root/lib/chef/application/exit_code.rb
blob: 2c1298cf91d7cdcc99fb74f945cee20ea458da3c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#
# Author:: Steven Murawski (<smurawski@chef.io>)
# Copyright:: Copyright 2016, Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

class Chef
  class Application

    # These are the exit codes defined in Chef RFC 062
    # https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md
    class ExitCode

      # -1 is defined as DEPRECATED_FAILURE in RFC 062, so it is
      # not enumerated in an active constant.
      #
      VALID_RFC_062_EXIT_CODES = {
        SUCCESS: 0,
        GENERIC_FAILURE: 1,
        SIGINT_RECEIVED: 2,
        SIGTERM_RECEIVED: 3,
        REBOOT_SCHEDULED: 35,
        REBOOT_NEEDED: 37,
        REBOOT_FAILED: 41,
        AUDIT_MODE_FAILURE: 42,
        CLIENT_UPGRADED: 213,
      }

      DEPRECATED_RFC_062_EXIT_CODES = {
        DEPRECATED_FAILURE: -1,
      }

      class << self

        def normalize_exit_code(exit_code = nil)
          if normalization_not_configured?
            normalize_legacy_exit_code_with_warning(exit_code)
          elsif normalization_disabled?
            normalize_legacy_exit_code(exit_code)
          else
            normalize_exit_code_to_rfc(exit_code)
          end
        end

        def enforce_rfc_062_exit_codes?
          !normalization_disabled? && !normalization_not_configured?
        end

        def notify_reboot_exit_code_deprecation
          return if normalization_disabled?
          notify_on_deprecation(reboot_deprecation_warning)
        end

        def notify_deprecated_exit_code
          return if normalization_disabled?
          notify_on_deprecation(deprecation_warning)
        end

        private

        def normalization_disabled?
          Chef::Config[:exit_status] == :disabled
        end

        def normalization_not_configured?
          Chef::Config[:exit_status].nil?
        end

        def normalize_legacy_exit_code_with_warning(exit_code)
          normalized_exit_code = normalize_legacy_exit_code(exit_code)
          unless valid_exit_codes.include? normalized_exit_code
            notify_on_deprecation(deprecation_warning)
          end
          normalized_exit_code
        end

        def normalize_legacy_exit_code(exit_code)
          case exit_code
          when Integer
            exit_code
          when Exception
            lookup_exit_code_by_exception(exit_code)
          else
            default_exit_code
          end
        end

        def normalize_exit_code_to_rfc(exit_code)
          normalized_exit_code = normalize_legacy_exit_code_with_warning(exit_code)
          if valid_exit_codes.include? normalized_exit_code
            normalized_exit_code
          else
            VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
          end
        end

        def lookup_exit_code_by_exception(exception)
          if sigint_received?(exception)
            VALID_RFC_062_EXIT_CODES[:SIGINT_RECEIVED]
          elsif sigterm_received?(exception)
            VALID_RFC_062_EXIT_CODES[:SIGTERM_RECEIVED]
          elsif normalization_disabled? || normalization_not_configured?
            if legacy_exit_code?(exception)
              # We have lots of "Chef::Application.fatal!('', 2)
              # This maintains that behavior at initial introduction
              # and when the RFC exit_status compliance is disabled.
              VALID_RFC_062_EXIT_CODES[:SIGINT_RECEIVED]
            else
              VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
            end
          elsif reboot_scheduled?(exception)
            VALID_RFC_062_EXIT_CODES[:REBOOT_SCHEDULED]
          elsif reboot_needed?(exception)
            VALID_RFC_062_EXIT_CODES[:REBOOT_NEEDED]
          elsif reboot_failed?(exception)
            VALID_RFC_062_EXIT_CODES[:REBOOT_FAILED]
          elsif audit_failure?(exception)
            VALID_RFC_062_EXIT_CODES[:AUDIT_MODE_FAILURE]
          elsif client_upgraded?(exception)
            VALID_RFC_062_EXIT_CODES[:CLIENT_UPGRADED]
          else
            VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
          end
        end

        def legacy_exit_code?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::DeprecatedExitCode
          end
        end

        def reboot_scheduled?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::Reboot
          end
        end

        def reboot_needed?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::RebootPending
          end
        end

        def reboot_failed?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::RebootFailed
          end
        end

        def audit_failure?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::AuditError
          end
        end

        def client_upgraded?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::ClientUpgraded
          end
        end

        def sigint_received?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::SigInt
          end
        end

        def sigterm_received?(exception)
          resolve_exception_array(exception).any? do |e|
            e.is_a? Chef::Exceptions::SigTerm
          end
        end

        def resolve_exception_array(exception)
          exception_array = [exception]
          if exception.respond_to?(:wrapped_errors)
            exception.wrapped_errors.each do |e|
              exception_array.push e
            end
          end
          exception_array
        end

        def valid_exit_codes
          VALID_RFC_062_EXIT_CODES.values
        end

        def notify_on_deprecation(message)
          begin
            Chef.deprecated(:exit_code, message)
          rescue Chef::Exceptions::DeprecatedFeatureError
            # Have to rescue this, otherwise this unhandled error preempts
            # the current exit code assignment.
          end
        end

        def deprecation_warning
          "Chef RFC 062 (https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md) defines the" \
          " exit codes that should be used with Chef.  Chef::Application::ExitCode defines valid exit codes"  \
          " In a future release, non-standard exit codes will be redefined as" \
          " GENERIC_FAILURE unless `exit_status` is set to `:disabled` in your client.rb."
        end

        def reboot_deprecation_warning
          "Per RFC 062 (https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md)" \
          ", when a reboot is requested Chef Client will exit with an exit code of 35, REBOOT_SCHEDULED." \
          " To maintain the current behavior (an exit code of 0), you will need to set `exit_status` to" \
          " `:disabled` in your client.rb"
        end

        def default_exit_code
          if normalization_disabled? || normalization_not_configured?
            return DEPRECATED_RFC_062_EXIT_CODES[:DEPRECATED_FAILURE]
          else
            VALID_RFC_062_EXIT_CODES[:GENERIC_FAILURE]
          end
        end

      end
    end

  end
end