# # Author:: Bryan McLellan # Copyright:: Copyright 2010-2016, Bryan McLellan # 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" require "chef/provider/service/simple" require "chef/mixin/command" require "chef/util/file_edit" class Chef class Provider class Service class Upstart < Chef::Provider::Service::Simple # to maintain a local state of service across restart's internal calls attr_accessor :upstart_service_running provides :service, platform_family: "debian", override: true do |node| Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end UPSTART_STATE_FORMAT = /\S+ \(?(start|stop)?\)? ?[\/ ](\w+)/ def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:upstart) end # Upstart does more than start or stop a service, creating multiple 'states' [1] that a service can be in. # In chef, when we ask a service to start, we expect it to have started before performing the next step # since we have top down dependencies. Which is to say we may follow witha resource next that requires # that service to be running. According to [2] we can trust that sending a 'goal' such as start will not # return until that 'goal' is reached, or some error has occurred. # # [1] http://upstart.ubuntu.com/wiki/JobStates # [2] http://www.netsplit.com/2008/04/27/upstart-05-events/ def initialize(new_resource, run_context) # TODO: re-evaluate if this is needed after integrating cookbook fix raise ArgumentError, "run_context cannot be nil" unless run_context super run_context.node # dup so we can mutate @job @job = @new_resource.service_name.dup if @new_resource.parameters @new_resource.parameters.each do |key, value| @job << " #{key}=#{value}" end end platform, version = Chef::Platform.find_platform_and_version(run_context.node) if platform == "ubuntu" && (8.04..9.04).cover?(version.to_f) @upstart_job_dir = "/etc/event.d" @upstart_conf_suffix = "" else @upstart_job_dir = "/etc/init" @upstart_conf_suffix = ".conf" end @command_success = true # new_resource.status_command= false, means upstart used @config_file_found = true @upstart_command_success = true end def define_resource_requirements # Do not call super, only call shared requirements shared_resource_requirements requirements.assert(:all_actions) do |a| if !@command_success whyrun_msg = if @new_resource.status_command "Provided status command #{@new_resource.status_command} failed." else "Could not determine upstart state for service" end end a.assertion { @command_success } # no failure here, just document the assumptions made. a.whyrun "#{whyrun_msg} Assuming service installed and not running." end requirements.assert(:all_actions) do |a| a.assertion { @config_file_found } # no failure here, just document the assumptions made. a.whyrun "Could not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}. Assuming service is disabled." end end def load_current_resource @current_resource = Chef::Resource::Service.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) # Get running/stopped state # We do not support searching for a service via ps when using upstart since status is a native # upstart function. We will however support status_command in case someone wants to do something special. if @new_resource.status_command Chef::Log.debug("#{@new_resource} you have specified a status command, running..") begin if shell_out!(@new_resource.status_command).exitstatus == 0 @upstart_service_running = true end rescue @command_success = false @upstart_service_running = false nil end else begin if upstart_goal_state == "start" @upstart_service_running = true else @upstart_service_running = false end rescue Chef::Exceptions::Exec @command_success = false @upstart_service_running = false nil end end # Get enabled/disabled state by reading job configuration file if ::File.exists?("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") Chef::Log.debug("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}", "r") do |file| while line = file.gets case line when /^start on/ Chef::Log.debug("#{@new_resource} enabled: #{line.chomp}") @current_resource.enabled true break when /^#start on/ Chef::Log.debug("#{@new_resource} disabled: #{line.chomp}") @current_resource.enabled false break end end end else @config_file_found = false Chef::Log.debug("#{@new_resource} did not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") @current_resource.enabled false end @current_resource.running @upstart_service_running @current_resource end def start_service # Calling start on a service that is already started will return 1 # Our 'goal' when we call start is to ensure the service is started if @upstart_service_running Chef::Log.debug("#{@new_resource} already running, not starting") else if @new_resource.start_command super else shell_out_with_systems_locale!("/sbin/start #{@job}") end end @upstart_service_running = true end def stop_service # Calling stop on a service that is already stopped will return 1 # Our 'goal' when we call stop is to ensure the service is stopped unless @upstart_service_running Chef::Log.debug("#{@new_resource} not running, not stopping") else if @new_resource.stop_command super else shell_out_with_systems_locale!("/sbin/stop #{@job}") end end @upstart_service_running = false end def restart_service if @new_resource.restart_command super # Upstart always provides restart functionality so we don't need to mimic it with stop/sleep/start. # Older versions of upstart would fail on restart if the service was currently stopped, check for that. LP:430883 # But for safe working of latest upstart job config being loaded, 'restart' can't be used as per link # http://upstart.ubuntu.com/cookbook/#restart (it doesn't uses latest jon config from disk but retains old) else if @upstart_service_running stop_service sleep 1 start_service else start_service end end @upstart_service_running = true end def reload_service if @new_resource.reload_command super else # upstart >= 0.6.3-4 supports reload (HUP) shell_out_with_systems_locale!("/sbin/reload #{@job}") end @upstart_service_running = true end # https://bugs.launchpad.net/upstart/+bug/94065 def enable_service Chef::Log.debug("#{@new_resource} upstart lacks inherent support for enabling services, editing job config file") conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") conf.search_file_replace(/^#start on/, "start on") conf.write_file end def disable_service Chef::Log.debug("#{@new_resource} upstart lacks inherent support for disabling services, editing job config file") conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") conf.search_file_replace(/^start on/, "#start on") conf.write_file end def upstart_goal_state command = "/sbin/status #{@job}" status = popen4(command) do |pid, stdin, stdout, stderr| stdout.each_line do |line| # service goal/state # OR # service (instance) goal/state # OR # service (goal) state line =~ UPSTART_STATE_FORMAT data = Regexp.last_match return data[1] end end end end end end end