summaryrefslogtreecommitdiff
path: root/lib/chef/resource/chef_client_launchd.rb
blob: c44b80d480401013ebe16cf98c3ee0d134aada7b (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
#
# 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 "chef-utils/dist" unless defined?(ChefUtils::Dist)
class Chef
  class Resource
    class ChefClientLaunchd < Chef::Resource
      unified_mode true

      provides :chef_client_launchd

      description "Use the **chef_client_launchd** resource to configure the #{ChefUtils::Dist::Infra::PRODUCT} to run on a schedule on macOS systems."
      introduced "16.5"
      examples <<~DOC
        **Set the #{ChefUtils::Dist::Infra::PRODUCT} to run on a schedule**:

        ```ruby
        chef_client_launchd 'Setup the #{ChefUtils::Dist::Infra::PRODUCT} to run every 30 minutes' do
          interval 30
          action :enable
        end
        ```

        **Disable the #{ChefUtils::Dist::Infra::PRODUCT} running on a schedule**:

        ```ruby
        chef_client_launchd 'Prevent the #{ChefUtils::Dist::Infra::PRODUCT} from running on a schedule' do
          action :disable
        end
        ```
      DOC

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

      property :working_directory, String,
        description: "The working directory to run the #{ChefUtils::Dist::Infra::PRODUCT} from.",
        default: "/var/root"

      property :interval, [Integer, String],
        description: "Time in minutes between #{ChefUtils::Dist::Infra::PRODUCT} executions.",
        coerce: proc { |x| Integer(x) },
        callbacks: { "should be a positive number" => proc { |v| v > 0 } },
        default: 30

      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 #{ChefUtils::Dist::Infra::CLIENT} commands don't execute at the same time."

      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,
        description: "The path of the config directory.",
        default: ChefConfig::Config.etc_chef_dir

      property :log_directory, String,
        description: "The path of the directory to create the log file in.",
        default: "/Library/Logs/Chef"

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

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

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

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

      property :nice, [Integer, String],
        description: "The process priority to run the #{ChefUtils::Dist::Infra::CLIENT} process at. A value of -20 is the highest priority and 19 is the lowest priority.",
        coerce: proc { |x| Integer(x) },
        callbacks: { "should be an Integer between -20 and 19" => proc { |v| v >= -20 && v <= 19 } }

      property :low_priority_io, [true, false],
        description: "Run the #{ChefUtils::Dist::Infra::CLIENT} process with low priority disk IO",
        default: true

      action :enable do
        unless ::Dir.exist?(new_resource.log_directory)
          directory new_resource.log_directory do
            owner new_resource.user
            mode "0750"
            recursive true
          end
        end

        launchd "com.#{ChefUtils::Dist::Infra::SHORT}.#{ChefUtils::Dist::Infra::CLIENT}" do
          username new_resource.user
          working_directory new_resource.working_directory
          start_interval new_resource.interval * 60
          program_arguments ["/bin/bash", "-c", client_command]
          environment_variables new_resource.environment unless new_resource.environment.empty?
          nice new_resource.nice
          low_priority_io true
          notifies :sleep, "chef_sleep[Sleep before client restart]", :immediately
          action :create # create only creates the file. No service restart triggering
        end

        # Launchd doesn't have the concept of a reload aka restart. Instead to update a daemon config you have
        # to unload it and then reload the new plist. That's usually fine, but not if chef-client is trying
        # to restart itself. If the chef-client process uses launchd or macosx_service resources to restart itself
        # we'll end up with a stopped service that will never get started back up. Instead we use this daemon
        # that triggers when the chef-client plist file is updated, and handles the restart outside the run.
        launchd "com.#{ChefUtils::Dist::Infra::SHORT}.restarter" do
          username "root"
          watch_paths ["/Library/LaunchDaemons/com.#{ChefUtils::Dist::Infra::SHORT}.#{ChefUtils::Dist::Infra::CLIENT}.plist"]
          standard_out_path ::File.join(new_resource.log_directory, new_resource.log_file_name)
          standard_error_path ::File.join(new_resource.log_directory, new_resource.log_file_name)
          program_arguments ["/bin/bash",
                             "-c",
                             "echo; echo #{ChefUtils::Dist::Infra::PRODUCT} launchd daemon config has been updated. Manually unloading and reloading the daemon; echo Now unloading the daemon; launchctl unload /Library/LaunchDaemons/com.#{ChefUtils::Dist::Infra::SHORT}.#{ChefUtils::Dist::Infra::CLIENT}.plist; sleep 2; echo Now loading the daemon; launchctl load /Library/LaunchDaemons/com.#{ChefUtils::Dist::Infra::SHORT}.#{ChefUtils::Dist::Infra::CLIENT}.plist"]
          action :enable # enable creates the plist & triggers service restarts on change
        end

        # We want to make sure that after we update the chef-client launchd config that we don't move on to another recipe
        # before the restarter daemon can do its thing. This sleep avoids killing the client while it's doing something like
        # installing a package, which could be problematic. It also makes it a bit more clear in the log that the killed process
        # was intentional.
        chef_sleep "Sleep before client restart" do
          seconds 10
          action :nothing
        end
      end

      action :disable do
        service ChefUtils::Dist::Infra::PRODUCT do
          service_name "com.#{ChefUtils::Dist::Infra::SHORT}.#{ChefUtils::Dist::Infra::CLIENT}"
          action :disable
        end

        service "com.#{ChefUtils::Dist::Infra::SHORT}.restarter" do
          action :disable
        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

        #
        # random sleep time + chef-client + daemon option properties + license acceptance
        #
        # @return [String]
        #
        def client_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 << " -L #{::File.join(new_resource.log_directory, new_resource.log_file_name)}"
          cmd << " --chef-license accept" if new_resource.accept_chef_license
          cmd
        end
      end
    end
  end
end