# # Author:: Nuo Yan # Author:: Bryan McLellan # Author:: Seth Chisamore # Copyright:: Copyright 2010-2017, Chef Software 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/service/simple" require "chef/win32_service_constants" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ require "chef/win32/error" require "win32/service" end class Chef::Provider::Service::Windows < Chef::Provider::Service provides :service, os: "windows" provides :windows_service include Chef::Mixin::ShellOut include Chef::ReservedNames::Win32::API::Error rescue LoadError include Chef::Win32ServiceConstants #Win32::Service.get_start_type AUTO_START = "auto start" MANUAL = "demand start" DISABLED = "disabled" #Win32::Service.get_current_state RUNNING = "running" STOPPED = "stopped" CONTINUE_PENDING = "continue pending" PAUSE_PENDING = "pause pending" PAUSED = "paused" START_PENDING = "start pending" STOP_PENDING = "stop pending" TIMEOUT = 60 SERVICE_RIGHT = "SeServiceLogonRight" def load_current_resource @current_resource = Chef::Resource::WindowsService.new(new_resource.name) current_resource.service_name(new_resource.service_name) if Win32::Service.exists?(current_resource.service_name) current_resource.running(current_state == RUNNING) Chef::Log.debug "#{new_resource} running: #{current_resource.running}" case current_startup_type when :automatic current_resource.enabled(true) when :disabled current_resource.enabled(false) end Chef::Log.debug "#{new_resource} enabled: #{current_resource.enabled}" config_info = Win32::Service.config_info(current_resource.service_name) current_resource.service_type(get_service_type(config_info.service_type)) if config_info.service_type current_resource.startup_type(start_type_to_sym(config_info.start_type)) if config_info.start_type current_resource.error_control(get_error_control(config_info.error_control)) if config_info.error_control current_resource.binary_path_name(config_info.binary_path_name) if config_info.binary_path_name current_resource.load_order_group(config_info.load_order_group) if config_info.load_order_group current_resource.dependencies(config_info.dependencies) if config_info.dependencies current_resource.run_as_user(config_info.service_start_name) if config_info.service_start_name current_resource.display_name(config_info.display_name) if config_info.display_name current_resource.delayed_start(current_delayed_start) if current_delayed_start end current_resource end def start_service if Win32::Service.exists?(@new_resource.service_name) # reconfiguration is idempotent, so just do it. new_config = { service_name: @new_resource.service_name, service_start_name: @new_resource.run_as_user, password: @new_resource.run_as_password, }.reject { |k, v| v.nil? || v.length == 0 } Win32::Service.configure(new_config) Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}" if new_config.has_key?(:service_start_name) unless Chef::ReservedNames::Win32::Security.get_account_right(canonicalize_username(new_config[:service_start_name])).include?(SERVICE_RIGHT) grant_service_logon(new_config[:service_start_name]) end end state = current_state if state == RUNNING Chef::Log.debug "#{@new_resource} already started - nothing to do" elsif state == START_PENDING Chef::Log.debug "#{@new_resource} already sent start signal - waiting for start" wait_for_state(RUNNING) elsif state == STOPPED if @new_resource.start_command Chef::Log.debug "#{@new_resource} starting service using the given start_command" shell_out!(@new_resource.start_command) else spawn_command_thread do begin Win32::Service.start(@new_resource.service_name) rescue SystemCallError => ex if ex.errno == ERROR_SERVICE_LOGON_FAILED Chef::Log.error ex.message raise Chef::Exceptions::Service, "Service #{@new_resource} did not start due to a logon failure (error #{ERROR_SERVICE_LOGON_FAILED}): possibly the specified user '#{@new_resource.run_as_user}' does not have the 'log on as a service' privilege, or the password is incorrect." else raise ex end end end wait_for_state(RUNNING) end @new_resource.updated_by_last_action(true) else raise Chef::Exceptions::Service, "Service #{@new_resource} can't be started from state [#{state}]" end else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end def stop_service if Win32::Service.exists?(@new_resource.service_name) state = current_state if state == RUNNING if @new_resource.stop_command Chef::Log.debug "#{@new_resource} stopping service using the given stop_command" shell_out!(@new_resource.stop_command) else spawn_command_thread do Win32::Service.stop(@new_resource.service_name) end wait_for_state(STOPPED) end @new_resource.updated_by_last_action(true) elsif state == STOPPED Chef::Log.debug "#{@new_resource} already stopped - nothing to do" elsif state == STOP_PENDING Chef::Log.debug "#{@new_resource} already sent stop signal - waiting for stop" wait_for_state(STOPPED) else raise Chef::Exceptions::Service, "Service #{@new_resource} can't be stopped from state [#{state}]" end else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end def restart_service if Win32::Service.exists?(@new_resource.service_name) if @new_resource.restart_command Chef::Log.debug "#{@new_resource} restarting service using the given restart_command" shell_out!(@new_resource.restart_command) else stop_service start_service end @new_resource.updated_by_last_action(true) else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end def enable_service if Win32::Service.exists?(@new_resource.service_name) set_startup_type(:automatic) else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end def disable_service if Win32::Service.exists?(@new_resource.service_name) set_startup_type(:disabled) else Chef::Log.debug "#{@new_resource} does not exist - nothing to do" end end action :create do if Win32::Service.exists?(new_resource.service_name) Chef::Log.debug "#{new_resource} already exists - nothing to do" return end converge_by("create service #{new_resource.service_name}") do Win32::Service.new(windows_service_config) end converge_delayed_start end action :delete do unless Win32::Service.exists?(new_resource.service_name) Chef::Log.debug "#{new_resource} does not exist - nothing to do" return end converge_by("delete service #{new_resource.service_name}") do Win32::Service.delete(new_resource.service_name) end end action :configure do unless Win32::Service.exists?(new_resource.service_name) Chef::Log.warn "#{new_resource} does not exist. Maybe you need to prepend action :create" return end # Until #6300 is solved this is required if new_resource.run_as_user == new_resource.class.properties[:run_as_user].default new_resource.run_as_user = new_resource.class.properties[:run_as_user].default end converge_if_changed :service_type, :startup_type, :error_control, :binary_path_name, :load_order_group, :dependencies, :run_as_user, :display_name, :description do Win32::Service.configure(windows_service_config(:configure)) end converge_delayed_start end def action_enable if current_startup_type != :automatic converge_by("enable service #{@new_resource}") do enable_service Chef::Log.info("#{@new_resource} enabled") end else Chef::Log.debug("#{@new_resource} already enabled - nothing to do") end load_new_resource_state @new_resource.enabled(true) end def action_disable if current_startup_type != :disabled converge_by("disable service #{@new_resource}") do disable_service Chef::Log.info("#{@new_resource} disabled") end else Chef::Log.debug("#{@new_resource} already disabled - nothing to do") end load_new_resource_state @new_resource.enabled(false) end def action_configure_startup startup_type = @new_resource.startup_type if current_startup_type != startup_type converge_by("set service #{@new_resource} startup type to #{startup_type}") do set_startup_type(startup_type) end else Chef::Log.debug("#{@new_resource} startup_type already #{startup_type} - nothing to do") end # Avoid changing enabled from true/false for now @new_resource.enabled(nil) end private def current_delayed_start if service = Win32::Service.services.find { |x| x.service_name == new_resource.service_name } service.delayed_start == 0 ? false : true else nil end end def grant_service_logon(username) begin Chef::ReservedNames::Win32::Security.add_account_right(canonicalize_username(username), SERVICE_RIGHT) rescue Chef::Exceptions::Win32APIError => err Chef::Log.fatal "Logon-as-service grant failed with output: #{err}" raise Chef::Exceptions::Service, "Logon-as-service grant failed for #{username}: #{err}" end Chef::Log.info "Grant logon-as-service to user '#{username}' successful." true end # remove characters that make for broken or wonky filenames. def clean_username_for_path(username) username.gsub(/[\/\\. ]+/, "_") end def canonicalize_username(username) username.sub(/^\.?\\+/, "") end def current_state Win32::Service.status(@new_resource.service_name).current_state end def current_startup_type start_type = Win32::Service.config_info(@new_resource.service_name).start_type start_type_to_sym(start_type) end # Helper method that waits for a status to change its state since state # changes aren't usually instantaneous. def wait_for_state(desired_state) retries = 0 loop do break if current_state == desired_state raise Timeout::Error if ( retries += 1 ) > resource_timeout sleep 1 end end def resource_timeout @resource_timeout ||= @new_resource.timeout || TIMEOUT end def spawn_command_thread worker = Thread.new do yield end Timeout.timeout(resource_timeout) do worker.join end end # @param type [Symbol] # @return [Integer] # @raise [Chef::Exceptions::ConfigurationError] if the startup type is # not supported. # @see Chef::Resource::WindowsService::ALLOWED_START_TYPES def startup_type_to_int(type) Chef::Resource::WindowsService::ALLOWED_START_TYPES.fetch(type) do raise Chef::Exceptions::ConfigurationError, "#{@new_resource.name}: Startup type '#{type}' is not supported" end end # Takes Win32::Service start_types def set_startup_type(type) startup_type = startup_type_to_int(type) Chef::Log.debug "#{@new_resource.name} setting start_type to #{type}" Win32::Service.configure( :service_name => @new_resource.service_name, :start_type => startup_type ) @new_resource.updated_by_last_action(true) end def windows_service_config(action = :create) config = {} config[:service_name] = new_resource.service_name config[:display_name] = new_resource.display_name if new_resource.display_name config[:service_type] = new_resource.service_type if new_resource.service_type config[:start_type] = startup_type_to_int(new_resource.startup_type) if new_resource.startup_type config[:error_control] = new_resource.error_control if new_resource.error_control config[:binary_path_name] = new_resource.binary_path_name if new_resource.binary_path_name config[:load_order_group] = new_resource.load_order_group if new_resource.load_order_group config[:dependencies] = new_resource.dependencies if new_resource.dependencies config[:service_start_name] = new_resource.run_as_user unless new_resource.run_as_user.empty? config[:password] = new_resource.run_as_password unless new_resource.run_as_user.empty? || new_resource.run_as_password.empty? config[:description] = new_resource.description if new_resource.description case action when :create config[:desired_access] = new_resource.desired_access if new_resource.desired_access end config end def converge_delayed_start config = {} config[:service_name] = new_resource.service_name config[:delayed_start] = new_resource.delayed_start ? 1 : 0 # Until #6300 is solved this is required if new_resource.delayed_start == new_resource.class.properties[:delayed_start].default new_resource.delayed_start = new_resource.class.properties[:delayed_start].default end converge_if_changed :delayed_start do Win32::Service.configure(config) end end # @return [Symbol] def start_type_to_sym(start_type) case start_type when "auto start" :automatic when "boot start" raise("Unsupported start type, #{start_type}. Submit bug request to fix.") when "demand start" :manual when "disabled" :disabled when "system start" raise("Unsupported start type, #{start_type}. Submit bug request to fix.") else raise("Unsupported start type, #{start_type}. Submit bug request to fix.") end end def get_service_type(service_type) case service_type when "file system driver" SERVICE_FILE_SYSTEM_DRIVER when "kernel driver" SERVICE_KERNEL_DRIVER when "own process" SERVICE_WIN32_OWN_PROCESS when "share process" SERVICE_WIN32_SHARE_PROCESS when "recognizer driver" SERVICE_RECOGNIZER_DRIVER when "driver" SERVICE_DRIVER when "win32" SERVICE_WIN32 when "all" SERVICE_TYPE_ALL when "own process, interactive" SERVICE_INTERACTIVE_PROCESS | SERVICE_WIN32_OWN_PROCESS when "share process, interactive" SERVICE_INTERACTIVE_PROCESS | SERVICE_WIN32_SHARE_PROCESS else raise("Unsupported service type, #{service_type}. Submit bug request to fix.") end end # @return [Integer] def get_start_type(start_type) case start_type when "auto start" SERVICE_AUTO_START when "boot start" SERVICE_BOOT_START when "demand start" SERVICE_DEMAND_START when "disabled" SERVICE_DISABLED when "system start" SERVICE_SYSTEM_START else raise("Unsupported start type, #{start_type}. Submit bug request to fix.") end end def get_error_control(error_control) case error_control when "critical" SERVICE_ERROR_CRITICAL when "ignore" SERVICE_ERROR_IGNORE when "normal" SERVICE_ERROR_NORMAL when "severe" SERVICE_ERROR_SEVERE else nil end end end