# # 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".freeze MANUAL = "demand start".freeze DISABLED = "disabled".freeze # Win32::Service.get_current_state RUNNING = "running".freeze STOPPED = "stopped".freeze CONTINUE_PENDING = "continue pending".freeze PAUSE_PENDING = "pause pending".freeze PAUSED = "paused".freeze START_PENDING = "start pending".freeze STOP_PENDING = "stop pending".freeze TIMEOUT = 60 SERVICE_RIGHT = "SeServiceLogonRight".freeze 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) logger.trace "#{new_resource} running: #{current_resource.running}" case current_startup_type when :automatic current_resource.enabled(true) when :disabled current_resource.enabled(false) end logger.trace "#{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) configure_service_run_as_properties state = current_state if state == RUNNING logger.trace "#{@new_resource} already started - nothing to do" elsif state == START_PENDING logger.trace "#{@new_resource} already sent start signal - waiting for start" wait_for_state(RUNNING) elsif state == STOPPED if @new_resource.start_command logger.trace "#{@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 logger.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 logger.trace "#{@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 logger.trace "#{@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 logger.trace "#{@new_resource} already stopped - nothing to do" elsif state == STOP_PENDING logger.trace "#{@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 logger.trace "#{@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 logger.trace "#{@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 logger.trace "#{@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 logger.trace "#{@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 logger.trace "#{@new_resource} does not exist - nothing to do" end end action :create do if Win32::Service.exists?(new_resource.service_name) logger.trace "#{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) logger.trace "#{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) logger.warn "#{new_resource} does not exist. Maybe you need to prepend action :create" return 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 logger.info("#{@new_resource} enabled") end else logger.trace("#{@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 logger.info("#{@new_resource} disabled") end else logger.trace("#{@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 logger.trace("#{@new_resource} startup_type already #{startup_type} - nothing to do") end converge_delayed_start # Avoid changing enabled from true/false for now @new_resource.enabled(nil) end private def configure_service_run_as_properties return unless new_resource.property_is_set?(:run_as_user) 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) logger.info "#{new_resource} configured." grant_service_logon(new_resource.run_as_user) if new_resource.run_as_user.casecmp("localsystem") != 0 end # # Queries the delayed auto-start setting of the auto-start service. If # the service is not auto-start, this will return nil. # # @return [Boolean, nil] # def current_delayed_start case Win32::Service.delayed_start(new_resource.service_name) when 0 false when 1 true end end def grant_service_logon(username) return if Chef::ReservedNames::Win32::Security.get_account_right(canonicalize_username(username)).include?(SERVICE_RIGHT) begin Chef::ReservedNames::Win32::Security.add_account_right(canonicalize_username(username), SERVICE_RIGHT) rescue Chef::Exceptions::Win32APIError => err logger.fatal "Logon-as-service grant failed with output: #{err}" raise Chef::Exceptions::Service, "Logon-as-service grant failed for #{username}: #{err}" end logger.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) logger.trace "#{@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 converge_if_changed :delayed_start do config = {} config[:service_name] = new_resource.service_name config[:delayed_start] = new_resource.delayed_start ? 1 : 0 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