diff options
-rw-r--r-- | lib/chef/provider/service/macosx.rb | 96 | ||||
-rw-r--r-- | lib/chef/resource/macosx_service.rb | 59 | ||||
-rw-r--r-- | spec/unit/provider/service/macosx_spec.rb | 433 |
3 files changed, 355 insertions, 233 deletions
diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index df5be54fda..7cfe57a92a 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -16,8 +16,10 @@ # limitations under the License. # +require 'etc' require 'rexml/document' require 'chef/resource/service' +require 'chef/resource/macosx_service' require 'chef/provider/service/simple' require 'chef/util/path_helper' @@ -27,6 +29,7 @@ class Chef class Macosx < Chef::Provider::Service::Simple provides :service, os: "darwin" + provides :macosx_service, os: "darwin" def self.gather_plist_dirs locations = %w{/Library/LaunchAgents @@ -40,18 +43,32 @@ class Chef PLIST_DIRS = gather_plist_dirs def load_current_resource - @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource = Chef::Resource::MacosxService.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @plist_size = 0 - @plist = find_service_plist + @plist = @new_resource.plist ? @new_resource.plist : find_service_plist @service_label = find_service_label + # LauchAgents should be loaded as the console user. + @console_user = @plist ? @plist.include?('LaunchAgents') : false + @session_type = @new_resource.session_type + + if @console_user + @console_user = Etc.getlogin + Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'") + cmd = "su " + param = !node['platform_version'].include?('10.10') ? '-l ' : '' + @base_user_cmd = cmd + param + "#{@console_user} -c" + # Default LauchAgent session should be Aqua + @session_type = 'Aqua' if @session_type.nil? + end + + Chef::Log.debug("#{new_resource} Plist: '#{@plist}' service_label: '#{@service_label}'") set_service_status @current_resource end def define_resource_requirements - #super requirements.assert(:reload) do |a| a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" end @@ -61,6 +78,12 @@ class Chef a.failure_message Chef::Exceptions::Service, "Several plist files match service name. Please use full service name." end + requirements.assert(:all_actions) do |a| + a.assertion {::File.exists?(@plist.to_s) } + a.failure_message Chef::Exceptions::Service, + "Could not find plist for #{@new_resource}" + end + requirements.assert(:enable, :disable) do |a| a.assertion { !@service_label.to_s.empty? } a.failure_message Chef::Exceptions::Service, @@ -69,7 +92,7 @@ class Chef requirements.assert(:all_actions) do |a| a.assertion { @plist_size > 0 } - # No failrue here in original code - so we also will not + # No failure here in original code - so we also will not # fail. Instead warn that the service is potentially missing a.whyrun "Assuming that the service would have been previously installed and is currently disabled." do @current_resource.enabled(false) @@ -85,7 +108,7 @@ class Chef if @new_resource.start_command super else - shell_out_with_systems_locale!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + load_service end end end @@ -97,7 +120,7 @@ class Chef if @new_resource.stop_command super else - shell_out_with_systems_locale!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + unload_service end end end @@ -106,9 +129,9 @@ class Chef if @new_resource.restart_command super else - stop_service + unload_service sleep 1 - start_service + load_service end end @@ -121,10 +144,7 @@ class Chef if @current_resource.enabled Chef::Log.debug("#{@new_resource} already enabled, not enabling") else - shell_out!( - "launchctl load -w '#{@plist}'", - :user => @owner_uid, :group => @owner_gid - ) + load_service end end @@ -132,38 +152,49 @@ class Chef unless @current_resource.enabled Chef::Log.debug("#{@new_resource} not enabled, not disabling") else - shell_out!( - "launchctl unload -w '#{@plist}'", - :user => @owner_uid, :group => @owner_gid - ) + unload_service + end + end + + def load_service + session = @session_type ? "-S #{@session_type} " : '' + cmd = 'launchctl load -w ' + session + @plist + shell_out_as_user(cmd) + end + + def unload_service + cmd = 'launchctl unload -w ' + @plist + shell_out_as_user(cmd) + end + + def shell_out_as_user(cmd) + if @console_user + shell_out_with_systems_locale("#{@base_user_cmd} '#{cmd}'") + else + shell_out_with_systems_locale(cmd) + end end def set_service_status return if @plist == nil or @service_label.to_s.empty? - cmd = shell_out( - "launchctl list #{@service_label}", - :user => @owner_uid, :group => @owner_gid - ) + cmd = "launchctl list #{@service_label}" + res = shell_out_as_user(cmd) - if cmd.exitstatus == 0 + if res.exitstatus == 0 @current_resource.enabled(true) else @current_resource.enabled(false) end if @current_resource.enabled - @owner_uid = ::File.stat(@plist).uid - @owner_gid = ::File.stat(@plist).gid - - shell_out!( - "launchctl list", :user => @owner_uid, :group => @owner_gid - ).stdout.each_line do |line| - case line - when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@service_label}/ + res.stdout.each_line do |line| + case line.downcase + when /\s+\"pid\"\s+=\s+(\d+).*/ pid = $1 @current_resource.running(!pid.to_i.zero?) + Chef::Log.debug("Current PID for #{@service_label} is #{pid}") end end else @@ -178,6 +209,9 @@ class Chef # onto the node yet." return nil if @plist.nil? + # Plist must exist by this point + raise Chef::Exceptions::FileNotFound, "Cannot find #{@plist}!" unless ::File.exists?(@plist) + # Most services have the same internal label as the name of the # plist file. However, there is no rule saying that *has* to be # the case, and some core services (notably, ssh) do not follow @@ -185,7 +219,9 @@ class Chef # plist files can come in XML or Binary formats. this command # will make sure we get XML every time. - plist_xml = shell_out!("plutil -convert xml1 -o - #{@plist}").stdout + plist_xml = shell_out_with_systems_locale!( + "plutil -convert xml1 -o - #{@plist}" + ).stdout plist_doc = REXML::Document.new(plist_xml) plist_doc.elements[ diff --git a/lib/chef/resource/macosx_service.rb b/lib/chef/resource/macosx_service.rb new file mode 100644 index 0000000000..879ea99cf8 --- /dev/null +++ b/lib/chef/resource/macosx_service.rb @@ -0,0 +1,59 @@ +# +# 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/service' + +class Chef + class Resource + class MacosxService < Chef::Resource::Service + + provides :service, os: "darwin" + provides :macosx_service, os: "darwin" + + identity_attr :service_name + + state_attrs :enabled, :running + + def initialize(name, run_context=nil) + super + @resource_name = :macosx_service + @plist = nil + @session_type = nil + end + + # This will enable user to pass a plist in the case + # that the filename and label for the service dont match + def plist(arg=nil) + set_or_return( + :plist, + arg, + :kind_of => String + ) + end + + def session_type(arg=nil) + set_or_return( + :session_type, + arg, + :kind_of => String + ) + end + + end + end +end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb index 9905a6e4ae..597845a558 100644 --- a/spec/unit/provider/service/macosx_spec.rb +++ b/spec/unit/provider/service/macosx_spec.rb @@ -58,248 +58,275 @@ describe Chef::Provider::Service::Macosx do </plist> XML - ["redis-server", "io.redis.redis-server"].each do |service_name| - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - allow(provider).to receive(:shell_out!). - with("launchctl list", {:group => 1001, :user => 101}). - and_return(double("Status", :stdout => launchctl_stdout)) - allow(provider).to receive(:shell_out). - with(/launchctl list /, - {:group => nil, :user => nil}). - and_return(double("Status", - :stdout => launchctl_stdout, :exitstatus => 0)) - allow(provider).to receive(:shell_out!). - with(/plutil -convert xml1 -o/). - and_return(double("Status", :stdout => plutil_stdout)) - - allow(File).to receive(:stat).and_return(double("stat", :gid => 1001, :uid => 101)) - end - - context "#{service_name}" do - let(:new_resource) { Chef::Resource::Service.new(service_name) } - let!(:current_resource) { Chef::Resource::Service.new(service_name) } - - describe "#load_current_resource" do - - # CHEF-5223 "you can't glob for a file that hasn't been converged - # onto the node yet." - context "when the plist doesn't exist" do - - 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 - - before do - allow(Dir).to receive(:glob).and_return([]) - allow(provider).to receive(:shell_out!). - with(/plutil -convert xml1 -o/). - and_raise(Mixlib::ShellOut::ShellCommandFailed) - end - - it "works for action :nothing" do - expect { run_resource_setup_for_action(:nothing) }.not_to raise_error - end - - it "works for action :start" do - expect { run_resource_setup_for_action(:start) }.not_to raise_error - end - - it "errors if action is :enable" do - expect { run_resource_setup_for_action(:enable) }.to raise_error(Chef::Exceptions::Service) - end - - it "errors if action is :disable" do - expect { run_resource_setup_for_action(:disable) }.to raise_error(Chef::Exceptions::Service) + ["Daemon", "Agent"].each do |service_type| + ["redis-server", "io.redis.redis-server"].each do |service_name| + ["10.9", "10.10"].each do |platform_version| + let(:plist) {'/Library/LaunchDaemons/io.redis.redis-server.plist'} + let(:session) { StringIO.new } + if service_type == 'Agent' + let(:plist) {'/Library/LaunchAgents/io.redis.redis-server.plist'} + let(:session) {'-S Aqua '} + let(:su_cmd) {'su igor -c'} + if platform_version != "10.10" + let(:su_cmd) {'su -l igor -c'} end end - - context "when launchctl returns pid in service list" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - 7777 - io.redis.redis-server - - - com.lol.stopped-thing - SVC_LIST - - before do - provider.load_current_resource - end - - it "sets resource running state to true" do - expect(provider.current_resource.running).to be_truthy - end - - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy - end + let(:service_label) {'io.redis.redis-server'} + before do + allow(Dir).to receive(:glob).and_return([plist], []) + allow(Etc).to receive(:getlogin).and_return('igor') + allow(node).to receive(:[]).with("platform_version").and_return(platform_version) + cmd = "launchctl list #{service_label}" + allow(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} '#{cmd}'|#{cmd})/). + and_return(double("Status", + :stdout => launchctl_stdout, :exitstatus => 0)) + allow(File).to receive(:exists?).and_return([true], []) + allow(provider).to receive(:shell_out_with_systems_locale!). + with(/plutil -convert xml1 -o/). + and_return(double("Status", :stdout => plutil_stdout)) end - describe "running unsupported actions" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } -12761 - 0x100114220.old.machinit.thing -7777 - io.redis.redis-server -- - com.lol.stopped-thing + context "#{service_name} that is a #{service_type} running Osx #{platform_version}" do + let(:new_resource) { Chef::Resource::MacosxService.new(service_name) } + let!(:current_resource) { Chef::Resource::MacosxService.new(service_name) } + + describe "#load_current_resource" do + + # CHEF-5223 "you can't glob for a file that hasn't been converged + # onto the node yet." + context "when the plist doesn't exist" do + + 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 + + before do + allow(Dir).to receive(:glob).and_return([]) + allow(File).to receive(:exists?).and_return([true], []) + allow(provider).to receive(:shell_out!). + with(/plutil -convert xml1 -o/). + and_raise(Mixlib::ShellOut::ShellCommandFailed) + end + + it "works for action :nothing" do + expect { run_resource_setup_for_action(:nothing) }.not_to raise_error + end + + it "works for action :start" do + expect { run_resource_setup_for_action(:start) }.not_to raise_error + end + + it "errors if action is :enable" do + expect { run_resource_setup_for_action(:enable) }.to raise_error(Chef::Exceptions::Service) + end + + it "errors if action is :disable" do + expect { run_resource_setup_for_action(:disable) }.to raise_error(Chef::Exceptions::Service) + end + end + + context "when launchctl returns pid in service list" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +{ + "LimitLoadToSessionType" = "System"; + "Label" = "io.redis.redis-server"; + "TimeOut" = 30; + "OnDemand" = false; + "LastExitStatus" = 0; + "PID" = 62803; + "Program" = "do_some.sh"; + "ProgramArguments" = ( + "path/to/do_something.sh"; + "-f"; + ); +}; SVC_LIST - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - end - it "should throw an exception when reload action is attempted" do - expect {provider.run_action(:reload)}.to raise_error(Chef::Exceptions::UnsupportedAction) - end - end - context "when launchctl returns empty service pid" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - - - io.redis.redis-server - - - com.lol.stopped-thing - SVC_LIST - - before do - provider.load_current_resource - end + before do + provider.load_current_resource + end - it "sets resource running state to false" do - expect(provider.current_resource.running).to be_falsey - end + it "sets resource running state to true" do + expect(provider.current_resource.running).to be_truthy + end - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy - end - end + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end + end - context "when launchctl doesn't return service entry at all" do - let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } - 12761 - 0x100114220.old.machinit.thing - - - com.lol.stopped-thing - SVC_LIST + describe "running unsupported actions" do + before do + allow(Dir).to receive(:glob).and_return(["#{plist}"], []) + allow(File).to receive(:exists?).and_return([true], []) + end + it "should throw an exception when reload action is attempted" do + expect {provider.run_action(:reload)}.to raise_error(Chef::Exceptions::UnsupportedAction) + end + end + context "when launchctl returns empty service pid" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +{ + "LimitLoadToSessionType" = "System"; + "Label" = "io.redis.redis-server"; + "TimeOut" = 30; + "OnDemand" = false; + "LastExitStatus" = 0; + "Program" = "do_some.sh"; + "ProgramArguments" = ( + "path/to/do_something.sh"; + "-f"; + ); +}; +SVC_LIST - it "sets service running state to false" do - provider.load_current_resource - expect(provider.current_resource.running).to be_falsey - end + before do + provider.load_current_resource + end - context "and plist for service is not available" do - before do - allow(Dir).to receive(:glob).and_return([]) - provider.load_current_resource - end + it "sets resource running state to false" do + expect(provider.current_resource.running).to be_falsey + end - it "sets resouce enabled state to false" do - expect(provider.current_resource.enabled).to be_falsey + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end end - end - context "and plist for service is available" do - before do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) - provider.load_current_resource - end + context "when launchctl doesn't return service entry at all" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +Could not find service "io.redis.redis-server" in domain for system +SVC_LIST - it "sets resouce enabled state to true" do - expect(provider.current_resource.enabled).to be_truthy + it "sets service running state to false" do + provider.load_current_resource + expect(provider.current_resource.running).to be_falsey + end + + context "and plist for service is not available" do + before do + allow(Dir).to receive(:glob).and_return([]) + provider.load_current_resource + end + + it "sets resouce enabled state to false" do + expect(provider.current_resource.enabled).to be_falsey + end + end + + context "and plist for service is available" do + before do + allow(Dir).to receive(:glob).and_return(["#{plist}"], []) + provider.load_current_resource + end + + it "sets resouce enabled state to true" do + expect(provider.current_resource.enabled).to be_truthy + end + end + + describe "and several plists match service name" do + it "throws exception" do + allow(Dir).to receive(:glob).and_return(["#{plist}", + "/Users/wtf/something.plist"]) + provider.load_current_resource + provider.define_resource_requirements + expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + end + end end end - - describe "and several plists match service name" do - it "throws exception" do - allow(Dir).to receive(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist", - "/Users/wtf/something.plist"]) + describe "#start_service" do + before do + allow(Chef::Resource::MacosxService).to receive(:new).and_return(current_resource) provider.load_current_resource - provider.define_resource_requirements - expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + allow(current_resource).to receive(:running).and_return(false) end - end - end - end - describe "#start_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(false) - end - it "calls the start command if one is specified and service is not running" do - allow(new_resource).to receive(:start_command).and_return("cowsay dirty") + it "calls the start command if one is specified and service is not running" do + allow(new_resource).to receive(:start_command).and_return("cowsay dirty") - expect(provider).to receive(:shell_out_with_systems_locale!).with("cowsay dirty") - provider.start_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("cowsay dirty") + provider.start_service + end - it "shows warning message if service is already running" do - allow(current_resource).to receive(:running).and_return(true) - expect(Chef::Log).to receive(:debug).with("service[#{service_name}] already running, not starting") + it "shows warning message if service is already running" do + allow(current_resource).to receive(:running).and_return(true) + expect(Chef::Log).to receive(:debug).with("macosx_service[#{service_name}] already running, not starting") - provider.start_service - end + provider.start_service + end - it "starts service via launchctl if service found" do - expect(provider).to receive(:shell_out_with_systems_locale!). - with("launchctl load -w '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", - :group => 1001, :user => 101). - and_return(0) + it "starts service via launchctl if service found" do + cmd = 'launchctl load -w ' + session + plist + expect(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} .#{cmd}.|#{cmd})/). + and_return(0) - provider.start_service - end - end + provider.start_service + end + end - describe "#stop_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) + describe "#stop_service" do + before do + allow(Chef::Resource::MacosxService).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(true) - end + provider.load_current_resource + allow(current_resource).to receive(:running).and_return(true) + end - it "calls the stop command if one is specified and service is running" do - allow(new_resource).to receive(:stop_command).and_return("kill -9 123") + it "calls the stop command if one is specified and service is running" do + allow(new_resource).to receive(:stop_command).and_return("kill -9 123") - expect(provider).to receive(:shell_out_with_systems_locale!).with("kill -9 123") - provider.stop_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("kill -9 123") + provider.stop_service + end - it "shows warning message if service is not running" do - allow(current_resource).to receive(:running).and_return(false) - expect(Chef::Log).to receive(:debug).with("service[#{service_name}] not running, not stopping") + it "shows warning message if service is not running" do + allow(current_resource).to receive(:running).and_return(false) + expect(Chef::Log).to receive(:debug).with("macosx_service[#{service_name}] not running, not stopping") - provider.stop_service - end + provider.stop_service + end - it "stops the service via launchctl if service found" do - expect(provider).to receive(:shell_out_with_systems_locale!). - with("launchctl unload '/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist'", - :group => 1001, :user => 101). - and_return(0) + it "stops the service via launchctl if service found" do + cmd = 'launchctl unload -w '+ plist + expect(provider).to receive(:shell_out_with_systems_locale). + with(/(#{su_cmd} .#{cmd}.|#{cmd})/). + and_return(0) - provider.stop_service - end - end + provider.stop_service + end + end - describe "#restart_service" do - before do - allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) + describe "#restart_service" do + before do + allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) - provider.load_current_resource - allow(current_resource).to receive(:running).and_return(true) - allow(provider).to receive(:sleep) - end + provider.load_current_resource + allow(current_resource).to receive(:running).and_return(true) + allow(provider).to receive(:sleep) + end - it "issues a command if given" do - allow(new_resource).to receive(:restart_command).and_return("reload that thing") + it "issues a command if given" do + allow(new_resource).to receive(:restart_command).and_return("reload that thing") - expect(provider).to receive(:shell_out_with_systems_locale!).with("reload that thing") - provider.restart_service - end + expect(provider).to receive(:shell_out_with_systems_locale!).with("reload that thing") + provider.restart_service + end - it "stops and then starts service" do - expect(provider).to receive(:stop_service) - expect(provider).to receive(:start_service); + it "stops and then starts service" do + expect(provider).to receive(:unload_service) + expect(provider).to receive(:load_service); - provider.restart_service + provider.restart_service + end + end end end end |