summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Dibowitz <phil@ipom.com>2016-02-17 11:38:02 -0800
committerPhil Dibowitz <phil@ipom.com>2016-02-17 11:38:02 -0800
commit83bab84325ce969d03d6f6373497fa8b21e80b53 (patch)
tree41b65e4963294e3c9713e7324ef07173d99f44a8
parente48aee5eae2191cf5755697d3f4407827945b0e9 (diff)
parentaef381aa1cff43b684ba022d5ce1b2349ea4e4dc (diff)
downloadchef-83bab84325ce969d03d6f6373497fa8b21e80b53.tar.gz
Merge pull request #4111 from mikedodge04/launchd
launchd for osx
-rw-r--r--lib/chef/provider/launchd.rb208
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/launchd.rb104
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/provider/launchd_spec.rb189
-rw-r--r--spec/unit/resource/launchd_spec.rb31
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