# Copyright 2013 Mirantis, Inc. # # 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 'timeout' module MCollective module Agent # An agent to manage the Puppet Daemon # # Configuration Options: # puppetd.splaytime - Number of seconds within which to splay; no splay # by default # puppetd.statefile - Where to find the state.yaml file; defaults to # /var/lib/puppet/state/state.yaml # puppetd.lockfile - Where to find the lock file; defaults to # /var/lib/puppet/state/puppetdlock # puppetd.puppetd - Where to find the puppet binary; defaults to # /usr/bin/puppet apply # puppetd.summary - Where to find the summary file written by Puppet # 2.6.8 and newer; defaults to # /var/lib/puppet/state/last_run_summary.yaml # puppetd.pidfile - Where to find puppet agent's pid file; defaults to # /var/run/puppet/agent.pid class Puppetd1, "changed"=>0, "total"=>0, "restarted"=>0, "out_of_sync"=>0}.merge(summary["resources"]) ["time", "events", "changes", "version"].each do |dat| reply[dat.to_sym] = summary[dat] end end def set_status reply[:status] = puppet_daemon_status reply[:running] = reply[:status] == 'running' ? 1 : 0 reply[:enabled] = reply[:status] == 'disabled' ? 0 : 1 reply[:idling] = reply[:status] == 'idling' ? 1 : 0 reply[:stopped] = reply[:status] == 'stopped' ? 1 : 0 reply[:lastrun] = 0 reply[:lastrun] = File.stat(@statefile).mtime.to_i if File.exists?(@statefile) reply[:runtime] = Time.now.to_i - reply[:lastrun] reply[:output] = "Currently #{reply[:status]}; last completed run #{reply[:runtime]} seconds ago" end def rm_file file return true unless File.exists?(file) begin File.unlink file return true rescue return false end end def puppet_daemon_status err_msg = "" alive = puppet_pid locked = expected_puppet_pid == puppet_pid && !expected_puppet_pid.nil? disabled = File.exists?(@lockfile) && File::Stat.new(@lockfile).zero? if locked && !disabled && !alive err_msg << "Process not running but not empty lockfile is present. Trying to remove lockfile..." err_msg << (rm_file(@lockfile) ? "ok." : "failed.") end reply[:err_msg] = err_msg unless err_msg.empty? if disabled && !alive 'disabled' elsif disabled && alive 'running' elsif alive && locked 'running' elsif alive && !locked 'idling' elsif !alive 'stopped' end end def runonce lock_file(@lockmcofile) do set_status case (reply[:status]) when 'disabled' then # can't run reply.fail "Empty Lock file exists; puppet is disabled." when 'running' then # can't run two simultaniously reply.fail "Lock file and PID file exist; puppet is running." when 'idling' then # signal daemon kill_process set_status # recalculate state after puppet kill reply[:err_msg] = "Looks like another puppet run at the same time. Try to kill it" when 'stopped' then # just run runonce_background else reply.fail "Unknown puppet status: #{reply[:status]}" end end end def runonce_background cwd = request.fetch(:cwd, '/') cmd = [ @puppetd, "-c #{cwd}", @puppetd_agent, request.fetch(:manifest, '/etc/puppet/manifests/site.pp'), "--modulepath=#{request.fetch(:modules, '/etc/puppet/modules')}", '--logdest', 'syslog', '--trace', '--no-report' ] unless request[:forcerun] if @splaytime && @splaytime > 0 cmd << "--splaylimit" << @splaytime << "--splay" end end cmd << '--debug' if request[:puppet_debug] cmd << '--evaltrace' if request[:puppet_debug] cmd << "--logdest #{@log}" if @log cmd = cmd.join(" ") output = reply[:output] || '' run(cmd, :stdout => :output, :chomp => true, :cwd => cwd, :environment => { 'LC_ALL' => 'en_US.UTF-8' }) reply[:output] = "Called #{cmd}, " + output + (reply[:output] || '') end def stop_and_disable lock_file(@lockmcofile) do case puppet_daemon_status when 'stopped' disable when 'disabled' reply[:output] = "Puppet already stoped and disabled" return else kill_process disable end reply[:output] = "Puppet stoped and disabled" end end def enable if File.exists?(@lockfile) stat = File::Stat.new(@lockfile) if stat.zero? File.unlink(@lockfile) reply[:output] = "Lock removed" else reply[:output] = "Currently running; can't remove lock" end else reply[:output] = "Already enabled" end end def disable if File.exists?(@lockfile) stat = File::Stat.new(@lockfile) stat.zero? ? reply[:output] = "Already disabled" : reply.fail("Currently running; can't remove lock") else begin File.open(@lockfile, "w") { |file| } reply[:output] = "Lock created" rescue => e reply.fail "Could not create lock: #{e}" end end end private def kill_process return if ['stopped', 'disabled'].include? puppet_daemon_status begin Timeout.timeout(30) do Process.kill('TERM', puppet_pid) if puppet_pid while puppet_pid do sleep 1 end end rescue Timeout::Error Process.kill('KILL', puppet_pid) if puppet_pid end #FIXME: Daemonized process do not update lock file when we send signal to kill him raise "Should never happen. Some process block lock file in critical section" unless rm_file(@lockfile) rescue => e reply.fail "Failed to kill the puppet daemon (process #{puppet_pid}): #{e}" end def expected_puppet_pid File.read(@lockfile).to_i rescue Errno::ENOENT nil end def puppet_pid result = `ps -C puppet -o pid,comm --no-headers`.lines.first actual_pid = result && result.strip.split(' ')[0].to_i expected_pid = expected_puppet_pid case actual_pid when expected_pid then expected_pid when nil then nil else reply[:err_msg] = "Potencial error. Looks like expecting puppet " \ "and actual are different. Expecting pid(lockfile): " \ "#{expected_pid}, actual(ps): #{actual_pid}" actual_pid end rescue NoMethodError nil end def lock_file(file_name, &block) File.open(file_name, 'w+') do |f| begin f.flock File::LOCK_EX yield ensure f.flock File::LOCK_UN end end end end end end