diff options
-rw-r--r-- | lib/chef/provider/launchd.rb | 208 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/launchd.rb | 104 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/launchd_spec.rb | 189 | ||||
-rw-r--r-- | spec/unit/resource/launchd_spec.rb | 31 |
6 files changed, 534 insertions, 0 deletions
diff --git a/lib/chef/provider/launchd.rb b/lib/chef/provider/launchd.rb new file mode 100644 index 0000000000..41c61eafde --- /dev/null +++ b/lib/chef/provider/launchd.rb @@ -0,0 +1,208 @@ +# +# Author:: Mike Dodge (<mikedodge04@gmail.com>) +# Copyright:: Copyright (c) 2015 Facebook, 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. +# + +require "chef/provider" +require "chef/resource/launchd" +require "chef/resource/file" +require "chef/resource/cookbook_file" +require "chef/resource/macosx_service" +require "plist" +require "forwardable" + +class Chef + class Provider + class Launchd < Chef::Provider + extend Forwardable + provides :launchd, os: "darwin" + + def_delegators :@new_resource, *[ + :backup, + :cookbook, + :group, + :label, + :mode, + :owner, + :path, + :source, + :session_type, + :type + ] + + def load_current_resource + current_resource = Chef::Resource::Launchd.new(new_resource.name) + @path = path ? path : gen_path_from_type + end + + def gen_path_from_type + types = { + "daemon" => "/Library/LaunchDaemons/#{label}.plist", + "agent" => "/Library/LaunchAgents/#{label}.plist" + } + types[type] + end + + def action_create + manage_plist(:create) + end + + def action_create_if_missing + manage_plist(:create_if_missing) + end + + def action_delete + # If you delete a service you want to make sure its not loaded or + # the service will be in memory and you wont be able to stop it. + if ::File.exists?(@path) + manage_service(:disable) + end + manage_plist(:delete) + end + + def action_enable + if manage_plist(:create) + manage_service(:restart) + else + manage_service(:enable) + end + end + + def action_disable + manage_service(:disable) + end + + def manage_plist(action) + if source + res = cookbook_file_resource + else + res = file_resource + end + res.run_action(action) + new_resource.updated_by_last_action(true) if res.updated? + res.updated + end + + def manage_service(action) + res = service_resource + res.run_action(action) + new_resource.updated_by_last_action(true) if res.updated? + end + + def service_resource + res = Chef::Resource::MacosxService.new(label, run_context) + res.name(label) if label + res.service_name(label) if label + res.plist(@path) if @path + res.session_type(session_type) if session_type + res + end + + def file_resource + res = Chef::Resource::File.new(@path, run_context) + res.name(@path) if @path + res.backup(backup) if backup + res.content(content) if content + res.group(group) if group + res.mode(mode) if mode + res.owner(owner) if owner + res + end + + def cookbook_file_resource + res = Chef::Resource::CookbookFile.new(@path, run_context) + res.cookbook_name = cookbook if cookbook + res.name(@path) if @path + res.backup(backup) if backup + res.group(group) if group + res.mode(mode) if mode + res.owner(owner) if owner + res.source(source) if source + res + end + + def define_resource_requirements + requirements.assert( + :create, :create_if_missing, :delete, :enable, :disable + ) do |a| + type = new_resource.type + a.assertion { %w{daemon agent}.include?(type.to_s) } + error_msg = "type must be daemon or agent." + a.failure_message Chef::Exceptions::ValidationFailed, error_msg + end + end + + def content? + !!content + end + + def content + plist_hash = new_resource.hash || gen_hash + Plist::Emit.dump(plist_hash) unless plist_hash.nil? + end + + def gen_hash + return nil unless new_resource.program || new_resource.program_arguments + { + "label" => "Label", + "program" => "Program", + "program_arguments" => "ProgramArguments", + "abandon_process_group" => "AbandonProcessGroup", + "debug" => "Debug", + "disabled" => "Disabled", + "enable_globbing" => "EnableGlobbing", + "enable_transactions" => "EnableTransactions", + "environment_variables" => "EnvironmentVariables", + "exit_timeout" => "ExitTimeout", + "ld_group" => "GroupName", + "hard_resource_limits" => "HardreSourceLimits", + "inetd_compatibility" => "inetdCompatibility", + "init_groups" => "InitGroups", + "keep_alive" => "KeepAlive", + "launch_only_once" => "LaunchOnlyOnce", + "limit_load_from_hosts" => "LimitLoadFromHosts", + "limit_load_to_hosts" => "LimitLoadToHosts", + "limit_load_to_session_type" => "LimitLoadToSessionType", + "low_priority_io" => "LowPriorityIO", + "mach_services" => "MachServices", + "nice" => "Nice", + "on_demand" => "OnDemand", + "process_type" => "ProcessType", + "queue_directories" => "QueueDirectories", + "root_directory" => "RootDirectory", + "run_at_load" => "RunAtLoad", + "sockets" => "Sockets", + "soft_resource_limits" => "SoftResourceLimits", + "standard_error_path" => "StandardErrorPath", + "standard_in_path" => "StandardInPath", + "standard_out_path" => "StandardOutPath", + "start_calendar_interval" => "StartCalendarInterval", + "start_interval" => "StartInterval", + "start_on_mount" => "StartOnMount", + "throttle_interval" => "ThrottleInterval", + "time_out" => "TimeOut", + "umask" => "Umask", + "username" => "UserName", + "wait_for_debugger" => "WaitForDebugger", + "watch_paths" => "WatchPaths", + "working_directory" => "WorkingDirectory", + }.each_with_object({}) do |(key, val), memo| + memo[val] = new_resource.send(key) if new_resource.send(key) + end + end + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 8af25b4c40..6fd67e0068 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -35,6 +35,7 @@ require "chef/provider/git" require "chef/provider/group" require "chef/provider/http_request" require "chef/provider/ifconfig" +require "chef/provider/launchd" require "chef/provider/link" require "chef/provider/log" require "chef/provider/ohai" diff --git a/lib/chef/resource/launchd.rb b/lib/chef/resource/launchd.rb new file mode 100644 index 0000000000..f3c378a6a8 --- /dev/null +++ b/lib/chef/resource/launchd.rb @@ -0,0 +1,104 @@ +# +# Author:: Mike Dodge (<mikedodge04@gmail.com>) +# Copyright:: Copyright (c) 2015 Facebook, 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. +# + +require "chef/resource" +require "chef/provider/launchd" + +class Chef + class Resource + class Launchd < Chef::Resource + provides :launchd, os: "darwin" + + identity_attr :label + + default_action :create + allowed_actions :create, :create_if_missing, :delete, :enable, :disable + + def initialize(name, run_context = nil) + super + provider = Chef::Provider::Launchd + resource_name = :launchd + end + + property :label, String, default: lazy { name }, identity: true + property :backup, [Integer, FalseClass] + property :cookbook, String + property :group, [String, Integer] + property :hash, Hash + property :mode, [String, Integer] + property :owner, [String, Integer] + property :path, String + property :source, String + property :session_type, String + + property :type, String, default: "daemon", coerce: proc { |type| + type = type ? type.downcase : "daemon" + types = %w{daemon agent} + + unless types.include?(type) + error_msg = "type must be daemon or agent" + raise Chef::Exceptions::ValidationFailed, error_msg + end + type + } + + # Apple LaunchD Keys + property :abandon_process_group, [ TrueClass, FalseClass ] + property :debug, [ TrueClass, FalseClass ] + property :disabled, [ TrueClass, FalseClass ], default: false + property :enable_globbing, [ TrueClass, FalseClass ] + property :enable_transactions, [ TrueClass, FalseClass ] + property :environment_variables, Hash + property :exit_timeout, Integer + property :hard_resource_limits, Hash + property :inetd_compatibility, Hash + property :init_groups, [ TrueClass, FalseClass ] + property :keep_alive, [ TrueClass, FalseClass ] + property :launch_only_once, [ TrueClass, FalseClass ] + property :ld_group, String + property :limit_load_from_hosts, Array + property :limit_load_to_hosts, Array + property :limit_load_to_session_type, String + property :low_priority_io, [ TrueClass, FalseClass ] + property :mach_services, Hash + property :nice, Integer + property :on_demand, [ TrueClass, FalseClass ] + property :process_type, String + property :program, String + property :program_arguments, Array + property :queue_directories, Array + property :root_directory, String + property :run_at_load, [ TrueClass, FalseClass ] + property :sockets, Hash + property :soft_resource_limits, Array + property :standard_error_path, String + property :standard_in_path, String + property :standard_out_path, String + property :start_calendar_interval, Hash + property :start_interval, Integer + property :start_on_mount, [ TrueClass, FalseClass ] + property :throttle_interval, Integer + property :time_out, Integer + property :umask, Integer + property :username, String + property :wait_for_debugger, [ TrueClass, FalseClass ] + property :watch_paths, Array + property :working_directory, String + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 71fe281e24..797f7b8b7e 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -46,6 +46,7 @@ require "chef/resource/http_request" require "chef/resource/homebrew_package" require "chef/resource/ifconfig" require "chef/resource/ksh" +require "chef/resource/launchd" require "chef/resource/link" require "chef/resource/log" require "chef/resource/macports_package" diff --git a/spec/unit/provider/launchd_spec.rb b/spec/unit/provider/launchd_spec.rb new file mode 100644 index 0000000000..d88783b1f6 --- /dev/null +++ b/spec/unit/provider/launchd_spec.rb @@ -0,0 +1,189 @@ +# +# Author:: Mike Dodge (<mikedodge04@gmail.com>) +# Copyright:: Copyright (c) 2015 Facebook, 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. +# + +require "spec_helper" + +describe Chef::Provider::Launchd do + + context "When launchd manages call.mom.weekly" do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:provider) { Chef::Provider::Launchd.new(new_resource, run_context) } + + let(:label) { "call.mom.weekly" } + let(:new_resource) { Chef::Resource::Launchd.new(label) } + let!(:current_resource) { Chef::Resource::Launchd.new(label) } + let(:test_plist) { String.new <<-XML } +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> +\t<key>Label</key> +\t<string>call.mom.weekly</string> +\t<key>Program</key> +\t<string>/Library/scripts/call_mom.sh</string> +\t<key>StartCalendarInterval</key> +\t<dict> +\t\t<key>Hourly</key> +\t\t<integer>10</integer> +\t\t<key>Weekday</key> +\t\t<integer>7</integer> +\t</dict> +\t<key>TimeOut</key> +\t<integer>300</integer> +</dict> +</plist> +XML + + let(:test_hash) do { + "Label" => "call.mom.weekly", + "Program" => "/Library/scripts/call_mom.sh", + "StartCalendarInterval" => { + "Hourly" => 10, + "Weekday" => 7, + }, + "TimeOut" => 300 + } end + + before(:each) do + provider.load_current_resource + end + + it "resource name and label should be call.mom.weekly" do + expect(new_resource.name).to eql(label) + expect(new_resource.label).to eql(label) + end + + def run_resource_setup_for_action(action) + new_resource.action(action) + provider.action = action + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements + end + + describe "with type is set to" do + describe "agent" do + it "path should be /Library/LaunchAgents/call.mom.weekly.plist" do + new_resource.type "agent" + expect(provider.gen_path_from_type). + to eq("/Library/LaunchAgents/call.mom.weekly.plist") + end + end + describe "daemon" do + it "path should be /Library/LaunchDaemons/call.mom.weekly.plist" do + expect(provider.gen_path_from_type). + to eq("/Library/LaunchDaemons/call.mom.weekly.plist") + end + end + end + + describe "with a :create action and" do + describe "program is passed" do + it "should produce the test_plist from properties" do + new_resource.program "/Library/scripts/call_mom.sh" + new_resource.time_out 300 + new_resource.start_calendar_interval "Hourly" => 10, "Weekday" => 7 + expect(provider.content?).to be_truthy + expect(provider.content).to eql(test_plist) + end + end + + describe "hash is passed" do + it "should produce the test_plist from the hash" do + new_resource.hash test_hash + expect(provider.content?).to be_truthy + expect(provider.content).to eql(test_plist) + end + end + end + + describe "with an :enable action" do + describe "and the file has been updated" do + before(:each) do + allow(provider).to receive( + :manage_plist).with(:create).and_return(true) + allow(provider).to receive( + :manage_service).with(:restart).and_return(true) + end + + it "should call manage_service with a :restart action" do + expect(provider.manage_service(:restart)).to be_truthy + end + + it "works with action enable" do + expect(run_resource_setup_for_action(:enable)).to be_truthy + provider.action_enable + end + end + + describe "and the file has not been updated" do + before(:each) do + allow(provider).to receive( + :manage_plist).with(:create).and_return(nil) + allow(provider).to receive( + :manage_service).with(:enable).and_return(true) + end + + it "should call manage_service with a :enable action" do + expect(provider.manage_service(:enable)).to be_truthy + end + + it "works with action enable" do + expect(run_resource_setup_for_action(:enable)).to be_truthy + provider.action_enable + end + end + end + + describe "with an :delete action" do + describe "and the ld file is present" do + before(:each) do + allow(File).to receive(:exists?).and_return(true) + allow(provider).to receive( + :manage_service).with(:disable).and_return(true) + allow(provider).to receive( + :manage_plist).with(:delete).and_return(true) + end + + it "should call manage_service with a :disable action" do + expect(provider.manage_service(:disable)).to be_truthy + end + + it "works with action :delete" do + expect(run_resource_setup_for_action(:delete)).to be_truthy + provider.action_delete + end + end + + describe "and the ld file is not present" do + before(:each) do + allow(File).to receive(:exists?).and_return(false) + allow(provider).to receive( + :manage_plist).with(:delete).and_return(true) + end + + it "works with action :delete" do + expect(run_resource_setup_for_action(:delete)).to be_truthy + provider.action_delete + end + end + end + end +end diff --git a/spec/unit/resource/launchd_spec.rb b/spec/unit/resource/launchd_spec.rb new file mode 100644 index 0000000000..95febc47cf --- /dev/null +++ b/spec/unit/resource/launchd_spec.rb @@ -0,0 +1,31 @@ +# + +require "spec_helper" + +describe Chef::Resource::Launchd do + @launchd = Chef::Resource::Launchd.new("io.chef.chef-client") + let(:resource) { Chef::Resource::Launchd.new( + "io.chef.chef-client", + run_context + )} + + it "should create a new Chef::Resource::Launchd" do + expect(resource).to be_a_kind_of(Chef::Resource) + expect(resource).to be_a_kind_of(Chef::Resource::Launchd) + end + + it "should have a resource name of Launchd" do + expect(resource.resource_name).to eql(:launchd) + end + + it "should have a default action of create" do + expect(resource.action).to eql([:create]) + end + + it "should accept enable, disable, create, and delete as actions" do + expect { resource.action :enable }.not_to raise_error + expect { resource.action :disable }.not_to raise_error + expect { resource.action :create }.not_to raise_error + expect { resource.action :delete }.not_to raise_error + end +end |