summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsersut <serdar@opscode.com>2013-03-01 15:14:24 -0800
committersersut <serdar@opscode.com>2013-03-01 15:14:24 -0800
commit4346211bd4f29745c0dfd467eda22b03a3746a6d (patch)
tree8e953c2de35392f7f92733f084d6b0b064162d84
parent77f911b99e68bf1ae068ce79f70f670f52943b2e (diff)
parent3a70182d28ffadfa07c8a54b8555e9a2137f53c4 (diff)
downloadchef-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-xbin/chef-service-manager8
-rw-r--r--lib/chef/application/windows_service_manager.rb104
-rw-r--r--spec/functional/win32/service_manager_spec.rb267
-rw-r--r--spec/support/lib/spec_service.rb59
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