diff options
author | sersut <serdar@opscode.com> | 2013-03-01 15:14:24 -0800 |
---|---|---|
committer | sersut <serdar@opscode.com> | 2013-03-01 15:14:24 -0800 |
commit | 4346211bd4f29745c0dfd467eda22b03a3746a6d (patch) | |
tree | 8e953c2de35392f7f92733f084d6b0b064162d84 | |
parent | 77f911b99e68bf1ae068ce79f70f670f52943b2e (diff) | |
parent | 3a70182d28ffadfa07c8a54b8555e9a2137f53c4 (diff) | |
download | chef-4346211bd4f29745c0dfd467eda22b03a3746a6d.tar.gz |
Merge pull request #663 from opscode/serdar/service_manager_refactor
Refactor windows_service_manager slightly so that we can reuse it in dif...
-rwxr-xr-x | bin/chef-service-manager | 8 | ||||
-rw-r--r-- | lib/chef/application/windows_service_manager.rb | 104 | ||||
-rw-r--r-- | spec/functional/win32/service_manager_spec.rb | 267 | ||||
-rw-r--r-- | spec/support/lib/spec_service.rb | 59 |
4 files changed, 392 insertions, 46 deletions
diff --git a/bin/chef-service-manager b/bin/chef-service-manager index c7f2fa43a9..781fd116de 100755 --- a/bin/chef-service-manager +++ b/bin/chef-service-manager @@ -24,7 +24,13 @@ require 'chef' require 'chef/application/windows_service_manager' if Chef::Platform.windows? - Chef::Application::WindowsServiceManager.new.run + chef_client_service = { + :service_name => "chef-client", + :service_display_name => "Chef Client Service", + :service_description => "Runs Opscode Chef Client on regular, configurable intervals.", + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../lib/chef/application/windows_service.rb')) + } + Chef::Application::WindowsServiceManager.new(chef_client_service).run else puts "chef-service-manager is only available on Windows platforms." end diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index ef93e88517..13bd2c5cd6 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -22,13 +22,22 @@ require 'mixlib/cli' class Chef class Application + # + # This class is used to create and manage a windows service. + # Service should be created using Daemon class from + # win32/service gem. + # For an example see: Chef::Application::WindowsService + # + # Outside programs are expected to use this class to manage + # windows services. + # class WindowsServiceManager include Mixlib::CLI option :action, :short => "-a ACTION", :long => "--action ACTION", - :default => "start", + :default => "status", :description => "Action to carry out on chef-service (install, uninstall, status, start, stop, pause, or resume)" option :config_file, @@ -43,18 +52,6 @@ class Chef :description => "Set the log file location for chef-service", :default => "#{ENV['SYSTEMDRIVE']}/chef/client.log" - option :splay, - :short => "-s SECONDS", - :long => "--splay SECONDS", - :description => "The splay time for running at intervals, in seconds", - :proc => lambda { |s| s.to_i } - - option :interval, - :short => "-i SECONDS", - :long => "--interval SECONDS", - :description => "Set the number of seconds to wait between chef-client runs", - :proc => lambda { |s| s.to_i } - option :help, :short => "-h", :long => "--help", @@ -64,57 +61,72 @@ class Chef :show_options => true, :exit => 0 - CHEF_SERVICE_NAME = "chef-client" - CHEF_SERVICE_DISPLAY_NAME = "Chef-Client Service" - CHEF_SERVICE_DESCRIPTION = "Runs chef-client periodically" - - def run - parse_options + def initialize(service_options) + # having to call super in initialize is the most annoying + # anti-pattern :( + super() + + raise ArgumentError, "Service definition is not provided" if service_options.nil? + + required_options = [:service_name, :service_display_name, :service_name, :service_description, :service_file_path] + + required_options.each do |req_option| + if !service_options.has_key?(req_option) + raise ArgumentError, "Service definition doesn't contain required option #{req_option}" + end + end + + @service_name = service_options[:service_name] + @service_display_name = service_options[:service_display_name] + @service_description = service_options[:service_description] + @service_file_path = service_options[:service_file_path] + end + + def run(params = ARGV) + parse_options(params) case config[:action] when 'install' if service_exists? - puts "Service #{CHEF_SERVICE_NAME} already exists on the system." + puts "Service #{@service_name} already exists on the system." else ruby = File.join(RbConfig::CONFIG['bindir'], 'ruby') - path = File.expand_path(File.join(File.dirname(__FILE__), 'windows_service.rb')) - + opts = "" opts << " -c #{config[:config_file]}" if config[:config_file] opts << " -L #{config[:log_location]}" if config[:log_location] - opts << " -i #{config[:interval]}" if config[:interval] - opts << " -s #{config[:splay]}" if config[:splay] - + # Quote the full paths to deal with possible spaces in the path name. # Also ensure all forward slashes are backslashes - cmd = "\"#{ruby}\" \"#{path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR) - + cmd = "\"#{ruby}\" \"#{@service_file_path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR) + ::Win32::Service.new( - :service_name => CHEF_SERVICE_NAME, - :display_name => CHEF_SERVICE_DISPLAY_NAME, - :description => CHEF_SERVICE_DESCRIPTION, + :service_name => @service_name, + :display_name => @service_display_name, + :description => @service_description, :start_type => ::Win32::Service::SERVICE_AUTO_START, - :binary_path_name => cmd) - puts "Service '#{CHEF_SERVICE_NAME}' has successfully been installed." + :binary_path_name => cmd + ) + puts "Service '#{@service_name}' has successfully been installed." end when 'status' if !service_exists? - puts "Service #{CHEF_SERVICE_NAME} doesn't exist on the system." + puts "Service #{@service_name} doesn't exist on the system." else - puts "State of #{CHEF_SERVICE_NAME} service is: #{current_state}" + puts "State of #{@service_name} service is: #{current_state}" end when 'start' # TODO: allow override of startup parameters here? take_action('start', RUNNING) when 'stop' - take_action('stop', STOPPED) + take_action('stop', STOPPED) when 'uninstall', 'delete' take_action('stop', STOPPED) unless service_exists? - puts "Service #{CHEF_SERVICE_NAME} doesn't exist on the system." + puts "Service #{@service_name} doesn't exist on the system." else - ::Win32::Service.delete(CHEF_SERVICE_NAME) - puts "Service #{CHEF_SERVICE_NAME} deleted" + ::Win32::Service.delete(@service_name) + puts "Service #{@service_name} deleted" end when 'pause' take_action('pause', PAUSED) @@ -131,25 +143,26 @@ class Chef PAUSED = "paused" def service_exists? - return ::Win32::Service.exists?(CHEF_SERVICE_NAME) + return ::Win32::Service.exists?(@service_name) end - + def take_action(action=nil, desired_state=nil) if service_exists? if current_state != desired_state - ::Win32::Service.send(action, CHEF_SERVICE_NAME) + ::Win32::Service.send(action, @service_name) wait_for_state(desired_state) - puts "Service '#{CHEF_SERVICE_NAME}' is now '#{current_state}'." + puts "Service '#{@service_name}' is now '#{current_state}'." else - puts "Service '#{CHEF_SERVICE_NAME}' is already '#{desired_state}'." + puts "Service '#{@service_name}' is already '#{desired_state}'." end else - puts "Cannot '#{action}' service '#{CHEF_SERVICE_NAME}', service does not exist." + puts "Cannot '#{action}' service '#{@service_name}'" + puts "Service #{@service_name} doesn't exist on the system." end end def current_state - ::Win32::Service.status(CHEF_SERVICE_NAME).current_state + ::Win32::Service.status(@service_name).current_state end # Helper method that waits for a status to change its state since state @@ -160,6 +173,7 @@ class Chef sleep 1 end end + end end end diff --git a/spec/functional/win32/service_manager_spec.rb b/spec/functional/win32/service_manager_spec.rb new file mode 100644 index 0000000000..b15d1061b9 --- /dev/null +++ b/spec/functional/win32/service_manager_spec.rb @@ -0,0 +1,267 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, 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' +require 'chef/application/windows_service_manager' + +# +# ATTENTION: +# This test creates a windows service for testing purposes and runs it +# as Local System on windows boxes. +# This test will fail if you run the tests inside a Windows VM by +# sharing the code from your host since Local System account by +# default can't see the mounted partitions. +# Run this test by copying the code to a local VM directory or setup +# Local System account to see the maunted partitions for the shared +# directories. +# + +describe "Chef::Application::WindowsServiceManager", :windows_only do + + # Some helper methods. + + def test_service_exists? + ::Win32::Service.exists?("spec-service") + end + + def test_service_state + ::Win32::Service.status("spec-service").current_state + end + + def service_manager + Chef::Application::WindowsServiceManager.new(test_service) + end + + def cleanup + # Uninstall if the test service is installed. + if test_service_exists? + + # We can only uninstall when the service is stopped. + if test_service_state != "stopped" + ::Win32::Service.send("stop", "spec-service") + while test_service_state != "stopped" + sleep 1 + end + end + + ::Win32::Service.delete("spec-service") + end + + # Delete the test_service_file if it exists + if File.exists?(test_service_file) + File.delete(test_service_file) + end + + end + + + # Definition for the test-service + + let(:test_service) { + { + :service_name => "spec-service", + :service_display_name => "Spec Test Service", + :service_description => "Service for testing Chef::Application::WindowsServiceManager.", + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../support/lib/spec_service.rb')) + } + } + + # Test service creates a file for us to verify that it is running. + # Since our test service is running as Local System we should look + # for the file it creates under SYSTEM temp directory + + let(:test_service_file) { + "#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file" + } + + context "with invalid service definition" do + it "throws an error when initialized with no service definition" do + lambda { Chef::Application::WindowsServiceManager.new(nil) }.should raise_error(ArgumentError) + end + + it "throws an error with required missing options" do + test_service.each do |key,value| + service_def = test_service.dup + service_def.delete(key) + + lambda { Chef::Application::WindowsServiceManager.new(service_def) }.should raise_error(ArgumentError) + end + end + end + + context "with valid definition" do + before(:each) do + @service_manager_output = [ ] + # Uncomment below lines to debug this test + # original_puts = $stdout.method(:puts) + $stdout.stub(:puts) do |message| + @service_manager_output << message + # original_puts.call(message) + end + end + + after(:each) do + cleanup + end + + context "when service doesn't exist" do + it "default => should say service don't exist" do + service_manager.run + + @service_manager_output.grep(/doesn't exist on the system/).length.should > 0 + end + + it "install => should install the service" do + service_manager.run(["-a", "install"]) + + test_service_exists?.should be_true + end + + it "other actions => should say service doesn't exist" do + ["delete", "start", "stop", "pause", "resume", "uninstall"].each do |action| + service_manager.run(["-a", action]) + @service_manager_output.grep(/doesn't exist on the system/).length.should > 0 + @service_manager_output = [ ] + end + end + end + + context "when service exists" do + before(:each) do + service_manager.run(["-a", "install"]) + end + + it "install => should say service already exists" do + service_manager.run(["-a", "install"]) + @service_manager_output.grep(/already exists/).length.should > 0 + end + + context "and service is stopped" do + ["delete", "uninstall"].each do |action| + it "#{action} => should remove the service" do + service_manager.run(["-a", action]) + test_service_exists?.should be_false + end + end + + it "default, status => should say service is stopped" do + service_manager.run([ ]) + @service_manager_output.grep(/stopped/).length.should > 0 + @service_manager_output = [ ] + + service_manager.run(["-a", "status"]) + @service_manager_output.grep(/stopped/).length.should > 0 + end + + it "start should start the service" do + service_manager.run(["-a", "start"]) + test_service_state.should == "running" + File.exists?(test_service_file).should be_true + end + + it "stop should not affect the service" do + service_manager.run(["-a", "stop"]) + test_service_state.should == "stopped" + end + + + ["pause", "resume"].each do |action| + it "#{action} => should raise error" do + lambda {service_manager.run(["-a", action])}.should raise_error(::Win32::Service::Error) + end + end + + context "and service is started" do + before(:each) do + service_manager.run(["-a", "start"]) + end + + ["delete", "uninstall"].each do |action| + it "#{action} => should remove the service" do + service_manager.run(["-a", action]) + test_service_exists?.should be_false + end + end + + it "default, status => should say service is running" do + service_manager.run([ ]) + @service_manager_output.grep(/running/).length.should > 0 + @service_manager_output = [ ] + + service_manager.run(["-a", "status"]) + @service_manager_output.grep(/running/).length.should > 0 + end + + it "stop should stop the service" do + service_manager.run(["-a", "stop"]) + test_service_state.should == "stopped" + end + + it "pause should pause the service" do + service_manager.run(["-a", "pause"]) + test_service_state.should == "paused" + end + + it "resume should have no affect" do + service_manager.run(["-a", "resume"]) + test_service_state.should == "running" + end + end + + context "and service is paused" do + before(:each) do + service_manager.run(["-a", "start"]) + service_manager.run(["-a", "pause"]) + end + + actions = ["delete", "uninstall"] + actions.each do |action| + it "#{action} => should remove the service" do + service_manager.run(["-a", action]) + test_service_exists?.should be_false + end + end + + it "default, status => should say service is paused" do + service_manager.run([ ]) + @service_manager_output.grep(/paused/).length.should > 0 + @service_manager_output = [ ] + + service_manager.run(["-a", "status"]) + @service_manager_output.grep(/paused/).length.should > 0 + end + + it "stop should stop the service" do + service_manager.run(["-a", "stop"]) + test_service_state.should == "stopped" + end + + it "pause should not affect the service" do + service_manager.run(["-a", "pause"]) + test_service_state.should == "paused" + end + + it "start should raise an error" do + lambda {service_manager.run(["-a", "start"])}.should raise_error(::Win32::Service::Error) + end + + end + end + end + end +end diff --git a/spec/support/lib/spec_service.rb b/spec/support/lib/spec_service.rb new file mode 100644 index 0000000000..3e1f6c3638 --- /dev/null +++ b/spec/support/lib/spec_service.rb @@ -0,0 +1,59 @@ +# +# Author:: Serdar Sutay (<serdar@lambda.local>) +# Copyright:: Copyright (c) 2013 Opscode, 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 'win32/daemon' + +class SpecService < ::Win32::Daemon + def service_init + @test_service_file = "#{ENV['TMP']}/spec_service_file" + end + + def service_main(*startup_parameters) + while running? do + if !File.exists?(@test_service_file) + File.open(@test_service_file, 'wb') do |f| + f.write("This file is created by SpecService") + end + end + + sleep 1 + end + end + + ################################################################################ + # Control Signal Callback Methods + ################################################################################ + + def service_stop + end + + def service_pause + end + + def service_resume + end + + def service_shutdown + end +end + +# To run this file as a service, it must be called as a script from within +# the Windows Service framework. In that case, kick off the main loop! +if __FILE__ == $0 + SpecService.mainloop +end |