diff options
author | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
---|---|---|
committer | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
commit | 24dc69a9a97e82a6e4207de68d6dcc664178249b (patch) | |
tree | 19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/application | |
parent | 9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff) | |
download | chef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz |
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code
used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib/chef/application')
-rw-r--r-- | lib/chef/application/agent.rb | 18 | ||||
-rw-r--r-- | lib/chef/application/client.rb | 327 | ||||
-rw-r--r-- | lib/chef/application/knife.rb | 183 | ||||
-rw-r--r-- | lib/chef/application/solo.rb | 254 | ||||
-rw-r--r-- | lib/chef/application/windows_service.rb | 237 |
5 files changed, 1019 insertions, 0 deletions
diff --git a/lib/chef/application/agent.rb b/lib/chef/application/agent.rb new file mode 100644 index 0000000000..353d45252e --- /dev/null +++ b/lib/chef/application/agent.rb @@ -0,0 +1,18 @@ +# +# Author:: AJ Christensen (<aj@opscode.comz>) +# Copyright:: Copyright (c) 2008 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 'chef/application' diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb new file mode 100644 index 0000000000..3a68a41c14 --- /dev/null +++ b/lib/chef/application/client.rb @@ -0,0 +1,327 @@ +# +# Author:: AJ Christensen (<aj@opscode.com) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Mark Mzyk (mmzyk@opscode.com) +# Copyright:: Copyright (c) 2008 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 'chef/application' +require 'chef/client' +require 'chef/config' +require 'chef/daemon' +require 'chef/log' +require 'chef/rest' +require 'chef/handler/error_report' + + +class Chef::Application::Client < Chef::Application + + # Mimic self_pipe sleep from Unicorn to capture signals safely + SELF_PIPE = [] + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => Chef::Config.platform_specific_path("/etc/chef/client.rb"), + :description => "The configuration file to use" + + option :formatter, + :short => "-F FORMATTER", + :long => "--format FORMATTER", + :description => "output format to use" + + option :color, + :long => '--[no-]color', + :boolean => true, + :default => false, + :description => "Use colored output, defaults to enabled" + + option :log_level, + :short => "-l LEVEL", + :long => "--log_level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :proc => lambda { |l| l.to_sym } + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing", + :proc => nil + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + option :user, + :short => "-u USER", + :long => "--user USER", + :description => "User to set privilege to", + :proc => nil + + option :group, + :short => "-g GROUP", + :long => "--group GROUP", + :description => "Group to set privilege to", + :proc => nil + + option :daemonize, + :short => "-d", + :long => "--daemonize", + :description => "Daemonize the process", + :proc => lambda { |p| true } + + option :pid_file, + :short => "-P PID_FILE", + :long => "--pid PIDFILE", + :description => "Set the PID file location, defaults to /tmp/chef-client.pid", + :proc => nil + + option :interval, + :short => "-i SECONDS", + :long => "--interval SECONDS", + :description => "Run chef-client periodically, in seconds", + :proc => lambda { |s| s.to_i } + + option :once, + :long => "--once", + :description => "Cancel any interval or splay options, run chef once and exit", + :boolean => true + + option :json_attribs, + :short => "-j JSON_ATTRIBS", + :long => "--json-attributes JSON_ATTRIBS", + :description => "Load attributes from a JSON file or URL", + :proc => nil + + option :node_name, + :short => "-N NODE_NAME", + :long => "--node-name NODE_NAME", + :description => "The node name for this client", + :proc => nil + + 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 :chef_server_url, + :short => "-S CHEFSERVERURL", + :long => "--server CHEFSERVERURL", + :description => "The chef server URL", + :proc => nil + + option :validation_key, + :short => "-K KEY_FILE", + :long => "--validation_key KEY_FILE", + :description => "Set the validation key file location, used for registering new clients", + :proc => nil + + option :client_key, + :short => "-k KEY_FILE", + :long => "--client_key KEY_FILE", + :description => "Set the client key file location", + :proc => nil + + option :environment, + :short => '-E ENVIRONMENT', + :long => '--environment ENVIRONMENT', + :description => 'Set the Chef Environment on the node' + + option :version, + :short => "-v", + :long => "--version", + :description => "Show chef version", + :boolean => true, + :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, + :exit => 0 + + option :override_runlist, + :short => "-o RunlistItem,RunlistItem...", + :long => "--override-runlist RunlistItem,RunlistItem...", + :description => "Replace current run list with specified items", + :proc => lambda{|items| + items = items.split(',') + items.compact.map{|item| + Chef::RunList::RunListItem.new(item) + } + } + + option :why_run, + :short => '-W', + :long => '--why-run', + :description => 'Enable whyrun mode', + :boolean => true + + option :client_fork, + :short => "-f", + :long => "--fork", + :description => "Fork client", + :boolean => true + + option :enable_reporting, + :short => "-R", + :long => "--enable-reporting", + :description => "Enable reporting data collection for chef runs", + :boolean => true + + attr_reader :chef_client_json + + def initialize + super + + @chef_client = nil + @chef_client_json = nil + end + + # Reconfigure the chef client + # Re-open the JSON attributes and load them into the node + def reconfigure + super + + Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url + unless Chef::Config[:exception_handlers].any? {|h| Chef::Handler::ErrorReport === h} + Chef::Config[:exception_handlers] << Chef::Handler::ErrorReport.new + end + + if Chef::Config[:daemonize] + Chef::Config[:interval] ||= 1800 + end + + if Chef::Config[:once] + Chef::Config[:interval] = nil + Chef::Config[:splay] = nil + end + + if Chef::Config[:json_attribs] + begin + json_io = case Chef::Config[:json_attribs] + when /^(http|https):\/\// + @rest = Chef::REST.new(Chef::Config[:json_attribs], nil, nil) + @rest.get_rest(Chef::Config[:json_attribs], true).open + else + open(Chef::Config[:json_attribs]) + end + rescue SocketError => error + Chef::Application.fatal!("I cannot connect to #{Chef::Config[:json_attribs]}", 2) + rescue Errno::ENOENT => error + Chef::Application.fatal!("I cannot find #{Chef::Config[:json_attribs]}", 2) + rescue Errno::EACCES => error + Chef::Application.fatal!("Permissions are incorrect on #{Chef::Config[:json_attribs]}. Please chmod a+r #{Chef::Config[:json_attribs]}", 2) + rescue Exception => error + Chef::Application.fatal!("Got an unexpected error reading #{Chef::Config[:json_attribs]}: #{error.message}", 2) + end + + begin + @chef_client_json = Chef::JSONCompat.from_json(json_io.read) + json_io.close unless json_io.closed? + rescue JSON::ParserError => error + Chef::Application.fatal!("Could not parse the provided JSON file (#{Chef::Config[:json_attribs]})!: " + error.message, 2) + end + end + end + + def configure_logging + super + Mixlib::Authentication::Log.use_log_devices( Chef::Log ) + Ohai::Log.use_log_devices( Chef::Log ) + end + + def setup_application + Chef::Daemon.change_privilege + end + + # Run the chef client, optionally daemonizing or looping at intervals. + def run_application + unless Chef::Platform.windows? + SELF_PIPE.replace IO.pipe + + trap("USR1") do + Chef::Log.info("SIGUSR1 received, waking up") + SELF_PIPE[1].putc('.') # wakeup master process from select + end + end + + if Chef::Config[:version] + puts "Chef version: #{::Chef::VERSION}" + end + + if Chef::Config[:daemonize] + Chef::Daemon.daemonize("chef-client") + end + + loop do + begin + if Chef::Config[:splay] + splay = rand Chef::Config[:splay] + Chef::Log.debug("Splay sleep #{splay} seconds") + sleep splay + end + @chef_client = Chef::Client.new( + @chef_client_json, + :override_runlist => config[:override_runlist] + ) + @chef_client_json = nil + + @chef_client.run + @chef_client = nil + if Chef::Config[:interval] + Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") + unless SELF_PIPE.empty? + client_sleep Chef::Config[:interval] + else + # Windows + sleep Chef::Config[:interval] + end + else + Chef::Application.exit! "Exiting", 0 + end + rescue Chef::Application::Wakeup => e + Chef::Log.debug("Received Wakeup signal. Starting run.") + next + rescue SystemExit => e + raise + rescue Exception => e + if Chef::Config[:interval] + Chef::Log.error("#{e.class}: #{e}") + Chef::Application.debug_stacktrace(e) + Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again") + unless SELF_PIPE.empty? + client_sleep Chef::Config[:interval] + else + # Windows + sleep Chef::Config[:interval] + end + retry + else + Chef::Application.debug_stacktrace(e) + Chef::Application.fatal!("#{e.class}: #{e.message}", 1) + end + end + end + end + + private + + def client_sleep(sec) + IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return + SELF_PIPE[0].getc + end +end diff --git a/lib/chef/application/knife.rb b/lib/chef/application/knife.rb new file mode 100644 index 0000000000..629cd9fc5f --- /dev/null +++ b/lib/chef/application/knife.rb @@ -0,0 +1,183 @@ +# +# Author:: Adam Jacob (<adam@opscode.com) +# Copyright:: Copyright (c) 2009 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 'chef/knife' +require 'chef/application' +require 'mixlib/log' +require 'ohai/config' + +class Chef::Application::Knife < Chef::Application + + NO_COMMAND_GIVEN = "You need to pass a sub-command (e.g., knife SUB-COMMAND)\n" + + banner "Usage: knife sub-command (options)" + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :description => "The configuration file to use", + :proc => lambda { |path| File.expand_path(path, Dir.pwd) } + + verbosity_level = 0 + option :verbosity, + :short => '-V', + :long => '--verbose', + :description => "More verbose output. Use twice for max verbosity", + :proc => Proc.new { verbosity_level += 1}, + :default => 0 + + option :color, + :long => '--[no-]color', + :boolean => true, + :default => true, + :description => "Use colored output, defaults to enabled" + + option :environment, + :short => "-E ENVIRONMENT", + :long => "--environment ENVIRONMENT", + :description => "Set the Chef environment" + + option :editor, + :short => "-e EDITOR", + :long => "--editor EDITOR", + :description => "Set the editor to use for interactive commands", + :default => ENV['EDITOR'] + + option :disable_editing, + :short => "-d", + :long => "--disable-editing", + :description => "Do not open EDITOR, just accept the data as is", + :boolean => true, + :defaut => false + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true + + option :node_name, + :short => "-u USER", + :long => "--user USER", + :description => "API Client Username" + + option :client_key, + :short => "-k KEY", + :long => "--key KEY", + :description => "API Client Key", + :proc => lambda { |path| File.expand_path(path, Dir.pwd) } + + option :chef_server_url, + :short => "-s URL", + :long => "--server-url URL", + :description => "Chef Server URL" + + option :yes, + :short => "-y", + :long => "--yes", + :description => "Say yes to all prompts for confirmation" + + option :defaults, + :long => "--defaults", + :description => "Accept default values for all questions" + + option :print_after, + :long => "--print-after", + :description => "Show the data after a destructive operation" + + option :format, + :short => "-F FORMAT", + :long => "--format FORMAT", + :description => "Which format to use for output", + :default => "summary" + + option :version, + :short => "-v", + :long => "--version", + :description => "Show chef version", + :boolean => true, + :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, + :exit => 0 + + + # Run knife + def run + Mixlib::Log::Formatter.show_time = false + validate_and_parse_options + quiet_traps + Chef::Knife.run(ARGV, options) + exit 0 + end + + private + + def quiet_traps + trap("TERM") do + exit 1 + end + + trap("INT") do + exit 2 + end + end + + def validate_and_parse_options + # Checking ARGV validity *before* parse_options because parse_options + # mangles ARGV in some situations + if no_command_given? + print_help_and_exit(1, NO_COMMAND_GIVEN) + elsif no_subcommand_given? + if (want_help? || want_version?) + print_help_and_exit + else + print_help_and_exit(2, NO_COMMAND_GIVEN) + end + end + end + + def no_subcommand_given? + ARGV[0] =~ /^-/ + end + + def no_command_given? + ARGV.empty? + end + + def want_help? + ARGV[0] =~ /^(--help|-h)$/ + end + + def want_version? + ARGV[0] =~ /^(--version|-v)$/ + end + + def print_help_and_exit(exitcode=1, fatal_message=nil) + Chef::Log.error(fatal_message) if fatal_message + + begin + self.parse_options + rescue OptionParser::InvalidOption => e + puts "#{e}\n" + end + puts self.opt_parser + puts + Chef::Knife.list_commands + exit exitcode + end + +end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb new file mode 100644 index 0000000000..7ec6c36e76 --- /dev/null +++ b/lib/chef/application/solo.rb @@ -0,0 +1,254 @@ +# +# Author:: AJ Christensen (<aj@opscode.com>) +# Author:: Mark Mzyk (mmzyk@opscode.com) +# Copyright:: Copyright (c) 2008 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 'chef' +require 'chef/application' +require 'chef/client' +require 'chef/config' +require 'chef/daemon' +require 'chef/log' +require 'chef/rest' +require 'open-uri' +require 'fileutils' + +class Chef::Application::Solo < Chef::Application + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => Chef::Config.platform_specfic_path('/etc/chef/solo.rb'), + :description => "The configuration file to use" + + option :formatter, + :short => "-F FORMATTER", + :long => "--format FORMATTER", + :description => "output format to use" + + option :color, + :long => '--[no-]color', + :boolean => true, + :default => false, + :description => "Use colored output, defaults to disabled" + + option :log_level, + :short => "-l LEVEL", + :long => "--log_level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :proc => lambda { |l| l.to_sym } + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location, defaults to STDOUT", + :proc => nil + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + option :user, + :short => "-u USER", + :long => "--user USER", + :description => "User to set privilege to", + :proc => nil + + option :group, + :short => "-g GROUP", + :long => "--group GROUP", + :description => "Group to set privilege to", + :proc => nil + + option :daemonize, + :short => "-d", + :long => "--daemonize", + :description => "Daemonize the process", + :proc => lambda { |p| true } + + option :interval, + :short => "-i SECONDS", + :long => "--interval SECONDS", + :description => "Run chef-client periodically, in seconds", + :proc => lambda { |s| s.to_i } + + option :json_attribs, + :short => "-j JSON_ATTRIBS", + :long => "--json-attributes JSON_ATTRIBS", + :description => "Load attributes from a JSON file or URL", + :proc => nil + + option :node_name, + :short => "-N NODE_NAME", + :long => "--node-name NODE_NAME", + :description => "The node name for this client", + :proc => nil + + 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 :recipe_url, + :short => "-r RECIPE_URL", + :long => "--recipe-url RECIPE_URL", + :description => "Pull down a remote gzipped tarball of recipes and untar it to the cookbook cache.", + :proc => nil + + option :version, + :short => "-v", + :long => "--version", + :description => "Show chef version", + :boolean => true, + :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, + :exit => 0 + + option :override_runlist, + :short => "-o RunlistItem,RunlistItem...", + :long => "--override-runlist RunlistItem,RunlistItem...", + :description => "Replace current run list with specified items", + :proc => lambda{|items| + items = items.split(',') + items.compact.map{|item| + Chef::RunList::RunListItem.new(item) + } + } + + option :client_fork, + :short => "-f", + :long => "--fork", + :description => "Fork client", + :boolean => true + + option :why_run, + :short => '-W', + :long => '--why-run', + :description => 'Enable whyrun mode', + :boolean => true + + attr_reader :chef_solo_json + + def initialize + super + @chef_solo = nil + @chef_solo_json = nil + end + + def reconfigure + super + + Chef::Config[:solo] = true + + if Chef::Config[:daemonize] + Chef::Config[:interval] ||= 1800 + end + + if Chef::Config[:json_attribs] + begin + json_io = case Chef::Config[:json_attribs] + when /^(http|https):\/\// + @rest = Chef::REST.new(Chef::Config[:json_attribs], nil, nil) + @rest.get_rest(Chef::Config[:json_attribs], true).open + else + open(Chef::Config[:json_attribs]) + end + rescue SocketError => error + Chef::Application.fatal!("I cannot connect to #{Chef::Config[:json_attribs]}", 2) + rescue Errno::ENOENT => error + Chef::Application.fatal!("I cannot find #{Chef::Config[:json_attribs]}", 2) + rescue Errno::EACCES => error + Chef::Application.fatal!("Permissions are incorrect on #{Chef::Config[:json_attribs]}. Please chmod a+r #{Chef::Config[:json_attribs]}", 2) + rescue Exception => error + Chef::Application.fatal!("Got an unexpected error reading #{Chef::Config[:json_attribs]}: #{error.message}", 2) + end + + begin + @chef_solo_json = Chef::JSONCompat.from_json(json_io.read) + json_io.close unless json_io.closed? + rescue JSON::ParserError => error + Chef::Application.fatal!("Could not parse the provided JSON file (#{Chef::Config[:json_attribs]})!: " + error.message, 2) + end + end + + if Chef::Config[:recipe_url] + cookbooks_path = Array(Chef::Config[:cookbook_path]).detect{|e| e =~ /\/cookbooks\/*$/ } + recipes_path = File.expand_path(File.join(cookbooks_path, '..')) + target_file = File.join(recipes_path, 'recipes.tgz') + + Chef::Log.debug "Creating path #{recipes_path} to extract recipes into" + FileUtils.mkdir_p recipes_path + path = File.join(recipes_path, 'recipes.tgz') + File.open(path, 'wb') do |f| + open(Chef::Config[:recipe_url]) do |r| + f.write(r.read) + end + end + Chef::Mixin::Command.run_command(:command => "tar zxvfC #{path} #{recipes_path}") + end + end + + def setup_application + Chef::Daemon.change_privilege + end + + def run_application + if Chef::Config[:daemonize] + Chef::Daemon.daemonize("chef-client") + end + + loop do + begin + if Chef::Config[:splay] + splay = rand Chef::Config[:splay] + Chef::Log.debug("Splay sleep #{splay} seconds") + sleep splay + end + + @chef_solo = Chef::Client.new( + @chef_solo_json, + :override_runlist => config[:override_runlist] + ) + @chef_solo.run + @chef_solo = nil + if Chef::Config[:interval] + Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") + sleep Chef::Config[:interval] + else + Chef::Application.exit! "Exiting", 0 + end + rescue SystemExit => e + raise + rescue Exception => e + if Chef::Config[:interval] + Chef::Log.error("#{e.class}: #{e}") + Chef::Log.debug("#{e.class}: #{e}\n#{e.backtrace.join("\n")}") + Chef::Log.fatal("Sleeping for #{Chef::Config[:interval]} seconds before trying again") + sleep Chef::Config[:interval] + retry + else + Chef::Application.debug_stacktrace(e) + Chef::Application.fatal!("#{e.class}: #{e.message}", 1) + end + end + end + end +end diff --git a/lib/chef/application/windows_service.rb b/lib/chef/application/windows_service.rb new file mode 100644 index 0000000000..961ec8f92f --- /dev/null +++ b/lib/chef/application/windows_service.rb @@ -0,0 +1,237 @@ +# +# Author:: Christopher Maier (<maier@lambda.local>) +# Copyright:: Copyright (c) 2011 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 'chef' +require 'chef/application' +require 'chef/client' +require 'chef/config' +require 'chef/handler/error_report' +require 'chef/log' +require 'chef/rest' +require 'mixlib/cli' +require 'socket' +require 'win32/daemon' + +class Chef + class Application + class WindowsService < ::Win32::Daemon + include Mixlib::CLI + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => "#{ENV['SYSTEMDRIVE']}/chef/client.rb", + :description => "" + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location", + :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 :override_runlist, + :short => "-o RunlistItem,RunlistItem...", + :long => "--override-runlist RunlistItem,RunlistItem...", + :description => "Replace current run list with specified items", + :proc => lambda{|items| + items = items.split(',') + items.compact.map{|item| + Chef::RunList::RunListItem.new(item) + } + } + + def service_init + reconfigure + Chef::Log.info("Chef Client Service initialized") + end + + def service_main(*startup_parameters) + + while running? + if state == RUNNING + begin + # Reconfigure each time through to pick up any changes in the client file + Chef::Log.info("Reconfiguring with startup parameters") + reconfigure(startup_parameters) + + splay = rand Chef::Config[:splay] + Chef::Log.debug("Splay sleep #{splay} seconds") + sleep splay + + # If we've stopped, then bail out now, instead of going on to run Chef + next if state != RUNNING + + @chef_client = Chef::Client.new( + @chef_client_json, + :override_runlist => config[:override_runlist] + ) + @chef_client_json = nil + + @chef_client.run + @chef_client = nil + + Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") + client_sleep Chef::Config[:interval] + rescue Chef::Application::Wakeup => e + Chef::Log.debug("Received Wakeup signal. Starting run.") + next + rescue SystemExit => e + raise + rescue Exception => e + Chef::Log.error("#{e.class}: #{e}") + Chef::Application.debug_stacktrace(e) + Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again") + client_sleep Chef::Config[:interval] + retry + end + else # PAUSED or IDLE + sleep 5 + end + end + end + + ################################################################################ + # Control Signal Callback Methods + ################################################################################ + + def service_stop + Chef::Log.info("SERVICE_CONTROL_STOP received, stopping") + end + + def service_pause + Chef::Log.info("SERVICE_CONTROL_PAUSE received, pausing") + end + + def service_resume + Chef::Log.info("SERVICE_CONTROL_CONTINUE received, resuming") + end + + def service_shutdown + Chef::Log.info("SERVICE_CONTROL_SHUTDOWN received, shutting down") + end + + ################################################################################ + # Internal Methods + ################################################################################ + + private + + def apply_config(config_file_path) + Chef::Config.from_file(config_file_path) + Chef::Config.merge!(config) + end + + # Lifted from Chef::Application, with addition of optional startup parameters + # for playing nicely with Windows Services + def reconfigure(startup_parameters=[]) + configure_chef startup_parameters + configure_logging + + Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url + unless Chef::Config[:exception_handlers].any? {|h| Chef::Handler::ErrorReport === h} + Chef::Config[:exception_handlers] << Chef::Handler::ErrorReport.new + end + + Chef::Config[:interval] ||= 1800 + end + + # Lifted from Chef::Application and Chef::Application::Client + # MUST BE RUN AFTER configuration has been parsed! + def configure_logging + # Implementation from Chef::Application + Chef::Log.init(Chef::Config[:log_location]) + Chef::Log.level = Chef::Config[:log_level] + + # Implementation from Chef::Application::Client + Mixlib::Authentication::Log.use_log_devices( Chef::Log ) + Ohai::Log.use_log_devices( Chef::Log ) + end + + def configure_chef(startup_parameters) + # Bit of a hack ahead: + # It is possible to specify a service's binary_path_name with arguments, like "foo.exe -x argX". + # It is also possible to specify startup parameters separately, either via the the Services manager + # or by using the registry (I think). + + # In order to accommodate all possible sources of parameterization, we first parse any command line + # arguments. We then parse any startup parameters. This works, because Mixlib::CLI reuses its internal + # 'config' hash; thus, anything in startup parameters will override any command line parameters that + # might be set via the service's binary_path_name + # + # All these parameters then get layered on top of those from Chef::Config + + parse_options # Operates on ARGV by default + parse_options startup_parameters + + begin + case config[:config_file] + when /^(http|https):\/\// + Chef::REST.new("", nil, nil).fetch(config[:config_file]) { |f| apply_config(f.path) } + else + ::File::open(config[:config_file]) { |f| apply_config(f.path) } + end + rescue Errno::ENOENT => error + Chef::Log.warn("*****************************************") + Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.") + Chef::Log.warn("*****************************************") + + Chef::Config.merge!(config) + rescue SocketError => error + Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}", 2) + rescue Chef::Exceptions::ConfigurationError => error + Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2) + rescue Exception => error + Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2) + end + end + + # Since we need to be able to respond to signals between Chef runs, we need to periodically + # wake up to see if we're still in the running state. The method returns when it has slept + # for +sec+ seconds (but at least +10+ seconds), or when the service + # is no client_sleep in the +RUNNING+ state, whichever comes first. + def client_sleep(sec) + chunk_length = 10 + chunks = sec / chunk_length + chunks = 1 if chunks < 1 + (1..chunks).each do + return unless state == RUNNING + sleep chunk_length + end + end + + end + 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 + Chef::Application::WindowsService.mainloop +end |