summaryrefslogtreecommitdiff
path: root/lib/chef/resource/chef_client_cron.rb
blob: 7a3f7992fc19097d994804426aeb6ea95f9a82f2 (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
#
# Copyright:: Copyright (c) Chef Software Inc.
#
# 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.
#

require_relative "../resource"
require_relative "../dist"
require_relative "helpers/cron_validations"
require "digest/md5"

class Chef
  class Resource
    class ChefClientCron < Chef::Resource
      unified_mode true

      provides :chef_client_cron

      description "Use the **chef_client_cron** resource to setup the #{Chef::Dist::PRODUCT} to run as a cron job. This resource will also create the specified log directory if it doesn't already exist."
      introduced "16.0"
      examples <<~DOC
      **Setup #{Chef::Dist::PRODUCT} to run using the default 30 minute cadence**:

      ```ruby
      chef_client_cron "Run #{Chef::Dist::PRODUCT} as a cron job"
      ```

      **Run #{Chef::Dist::PRODUCT} twice a day**:

      ```ruby
      chef_client_cron "Run #{Chef::Dist::PRODUCT} every 12 hours" do
        minute 0
        hour "0,12"
      end
      ```

      **Run #{Chef::Dist::PRODUCT} with extra options passed to the client**:

      ```ruby
      chef_client_cron "Run an override recipe" do
        daemon_options ["--override-runlist mycorp_base::default"]
      end
      ```
      DOC

      extend Chef::ResourceHelpers::CronValidations

      property :job_name, String,
        default: Chef::Dist::CLIENT,
        description: "The name of the cron job to create."

      property :comment, String,
        description: "A comment to place in the cron.d file."

      property :user, String,
        description: "The name of the user that #{Chef::Dist::PRODUCT} runs as.",
        default: "root"

      property :minute, [Integer, String],
        description: "The minute at which #{Chef::Dist::PRODUCT} is to run (0 - 59) or a cron pattern such as '0,30'.",
        default: "0,30", callbacks: {
          "should be a valid minute spec" => method(:validate_minute),
        }

      property :hour, [Integer, String],
        description: "The hour at which #{Chef::Dist::PRODUCT} is to run (0 - 23) or a cron pattern such as '0,12'.",
        default: "*", callbacks: {
          "should be a valid hour spec" => method(:validate_hour),
        }

      property :day, [Integer, String],
        description: "The day of month at which #{Chef::Dist::PRODUCT} is to run (1 - 31) or a cron pattern such as '1,7,14,21,28'.",
        default: "*", callbacks: {
          "should be a valid day spec" => method(:validate_day),
        }

      property :month, [Integer, String],
        description: "The month in the year on which #{Chef::Dist::PRODUCT} is to run (1 - 12, jan-dec, or *).",
        default: "*", callbacks: {
          "should be a valid month spec" => method(:validate_month),
        }

      property :weekday, [Integer, String],
        description: "The day of the week on which #{Chef::Dist::PRODUCT} is to run (0-7, mon-sun, or *), where Sunday is both 0 and 7.",
        default: "*", callbacks: {
          "should be a valid weekday spec" => method(:validate_dow),
        }

      property :splay, [Integer, String],
        default: 300,
        coerce: proc { |x| Integer(x) },
        callbacks: { "should be a positive number" => proc { |v| v > 0 } },
        description: "A random number of seconds between 0 and X to add to interval so that all #{Chef::Dist::CLIENT} commands don't execute at the same time."

      property :mailto, String,
        description: "The e-mail address to e-mail any cron task failures to."

      property :accept_chef_license, [true, false],
        description: "Accept the Chef Online Master License and Services Agreement. See <https://www.chef.io/online-master-agreement/>",
        default: false

      property :config_directory, String,
        default: Chef::Dist::CONF_DIR,
        description: "The path of the config directory."

      property :log_directory, String,
        default: lazy { platform?("mac_os_x") ? "/Library/Logs/#{Chef::Dist::DIR_SUFFIX.capitalize}" : "/var/log/#{Chef::Dist::DIR_SUFFIX}" },
        default_description: "/Library/Logs/#{Chef::Dist::DIR_SUFFIX.capitalize} on macOS and /var/log/#{Chef::Dist::DIR_SUFFIX} otherwise",
        description: "The path of the directory to create the log file in."

      property :log_file_name, String,
        default: "client.log",
        description: "The name of the log file to use."

      property :append_log_file, [true, false],
        default: true,
        description: "Append to the log file instead of overwriting the log file on each run."

      property :chef_binary_path, String,
        default: "/opt/#{Chef::Dist::DIR_SUFFIX}/bin/#{Chef::Dist::CLIENT}",
        description: "The path to the #{Chef::Dist::CLIENT} binary."

      property :daemon_options, Array,
        default: lazy { [] },
        description: "An array of options to pass to the #{Chef::Dist::CLIENT} command."

      property :environment, Hash,
        default: lazy { {} },
        description: "A Hash containing additional arbitrary environment variables under which the cron job will be run in the form of `({'ENV_VARIABLE' => 'VALUE'})`."

      action :add do
        # TODO: Replace this with a :create_if_missing action on directory when that exists
        unless ::Dir.exist?(new_resource.log_directory)
          directory new_resource.log_directory do
            owner new_resource.user
            mode "0750"
            recursive true
          end
        end

        declare_resource(cron_resource_type, new_resource.job_name) do
          minute new_resource.minute
          hour        new_resource.hour
          day         new_resource.day
          weekday     new_resource.weekday
          month       new_resource.month
          environment new_resource.environment
          mailto      new_resource.mailto if new_resource.mailto
          user        new_resource.user
          comment     new_resource.comment if new_resource.comment
          command     cron_command
        end
      end

      action :remove do
        declare_resource(cron_resource_type, new_resource.job_name) do
          action :delete
        end
      end

      action_class do
        #
        # Generate a uniformly distributed unique number to sleep from 0 to the splay time
        #
        # @param [Integer] splay The number of seconds to splay
        #
        # @return [Integer]
        #
        def splay_sleep_time(splay)
          seed = node["shard_seed"] || Digest::MD5.hexdigest(node.name).to_s.hex
          random = Random.new(seed.to_i)
          random.rand(splay)
        end

        #
        # The complete cron command to run
        #
        # @return [String]
        #
        def cron_command
          cmd = ""
          cmd << "/bin/sleep #{splay_sleep_time(new_resource.splay)}; "
          cmd << "#{new_resource.chef_binary_path} "
          cmd << "#{new_resource.daemon_options.join(" ")} " unless new_resource.daemon_options.empty?
          cmd << "-c #{::File.join(new_resource.config_directory, "client.rb")} "
          cmd << "--chef-license accept " if new_resource.accept_chef_license
          cmd << log_command
          cmd << " || echo \"#{Chef::Dist::PRODUCT} execution failed\"" if new_resource.mailto
          cmd
        end

        #
        # The portion of the overall cron job that handles logging based on the append_log_file property
        #
        # @return [String]
        #
        def log_command
          if new_resource.append_log_file
            "-L #{::File.join(new_resource.log_directory, new_resource.log_file_name)}"
          else
            "> #{::File.join(new_resource.log_directory, new_resource.log_file_name)} 2>&1"
          end
        end

        #
        # The type of cron resource to run. Linux systems all support the /etc/cron.d directory
        # and can use the cron_d resource, but Solaris / AIX / FreeBSD need to use the crontab
        # via the legacy cron resource.
        #
        # @return [Symbol]
        #
        def cron_resource_type
          linux? ? :cron_d : :cron
        end
      end
    end
  end
end