From 4b880bfe6d916b8ca844c72f7ab9f30a8b2b9f0d Mon Sep 17 00:00:00 2001 From: sersut Date: Fri, 1 Mar 2013 09:40:55 -0800 Subject: Tests, documentation and better error handling for windows service manager. --- lib/chef/application/windows_service_manager.rb | 24 ++- spec/functional/win32/service_manager_spec.rb | 264 ++++++++++++++++++++++++ spec/support/lib/spec_service.rb | 59 ++++++ 3 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 spec/functional/win32/service_manager_spec.rb create mode 100644 spec/support/lib/spec_service.rb diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index 83e89b9cfb..27e21a1acb 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -23,6 +23,12 @@ require 'mixlib/cli' class Chef class Application class WindowsServiceManager + # + # 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 + # include Mixlib::CLI option :action, @@ -57,14 +63,24 @@ class Chef # 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 - parse_options + def run(params = ARGV) + parse_options(params) case config[:action] when 'install' @@ -137,7 +153,8 @@ class Chef puts "Service '#{@service_name}' is already '#{desired_state}'." end else - puts "Cannot '#{action}' service '#{@service_name}', service does not exist." + puts "Cannot '#{action}' service '#{@service_name}'" + puts "Service #{@service_name} doesn't exist on the system." end end @@ -153,6 +170,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..439c87478b --- /dev/null +++ b/spec/functional/win32/service_manager_spec.rb @@ -0,0 +1,264 @@ +# +# Author:: Serdar Sutay () +# 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 + + # 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 + + 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 +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 () +# 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 -- cgit v1.2.1