1require File.expand_path(File.join(File.dirname(__FILE__), 2 'puppet_server_address_validation.rb')) 3require File.expand_path(File.join(File.dirname(__FILE__), 4 'puppet_agent_mgr', 'mgr_v2.rb')) 5require File.expand_path(File.join(File.dirname(__FILE__), 6 'puppet_agent_mgr', 'mgr_v3.rb')) 7require File.expand_path(File.join(File.dirname(__FILE__), 8 'puppet_agent_mgr', 'mgr_windows.rb')) 9 10 11module MCollective 12 module Util 13 14 # 15 ### Manager parent class 16 # 17 18 class PuppetAgentMgr 19 20 class NotImplementedError < StandardError 21 end 22 23 # manager cache 24 @@the_manager = nil 25 26 # returns the manager instance 27 def self.manager(configfile = nil, 28 service_name = 'puppet', 29 do_init = false, 30 testing = false) 31 # we want a new instance for each spec 32 if testing || do_init || !@@the_manager 33 # TODO: get rid of the conditional require 'puppet' 34 # we should get puppet if not testing 35 require 'puppet' if not testing 36 Log.debug("Creating a new instance of puppet agent manager: " \ 37 "config file = %s, service name = %s, testing = %s" \ 38 % [configfile, service_name, testing]) unless testing 39 @@the_manager = from_version(configfile, service_name, testing) 40 end 41 @@the_manager 42 end 43 44 ### manager factory (class method) 45 46 class << self 47 # NB: initilize() must be implemented by subclasses 48 def from_version(configfile, service_name, testing) 49 if Puppet.version =~ /^(\d+)/ 50 case $1 51 when "2" 52 raise "Window is not supported yet" if MCollective::Util.windows? 53 return MgrV2.new(configfile, service_name, testing) 54 when "3", "4", "5", "6" 55 if MCollective::Util.windows? 56 return MgrWindows.new(configfile, service_name, testing) 57 else 58 return MgrV3.new(configfile, service_name, testing) 59 end 60 else 61 raise "Cannot manage Puppet version %s" % $1 62 end 63 else 64 raise "Cannot determine the Puppet major version" 65 end 66 end 67 end 68 69 ### utility methods 70 71 # all the managed resources 72 def managed_resources 73 # need to add some caching here based on mtime of the resources file 74 return [] unless File.exist?(Puppet[:resourcefile]) 75 File.readlines(Puppet[:resourcefile]).map do |resource| 76 resource.chomp 77 end 78 end 79 80 # loads the summary file and ensures that some keys are always present 81 def load_summary 82 summary = {"changes" => {}, 83 "time" => {}, 84 "resources" => {}, 85 "version" => {}, 86 "events" => {}} 87 if File.exist?(Puppet[:lastrunfile]) 88 summary.merge!(YAML.load_file(Puppet[:lastrunfile])) 89 end 90 summary["resources"] = \ 91 {"failed" => 0, 92 "changed" => 0, 93 "corrective_change" => 0, 94 "total" => 0, 95 "restarted" => 0, 96 "out_of_sync" => 0}.merge!(summary["resources"]) 97 summary 98 end 99 100 # epoch time when the last catalog was applied 101 def lastrun 102 summary = load_summary 103 summary_time = summary["time"].fetch("last_run", 0) 104 begin 105 return Integer(summary_time) 106 rescue => e 107 Log.warn("Couldn't parse %s (time from summary file); " \ 108 "returning 0" % summary_time) 109 0 110 end 111 end 112 113 # how mnay of each type of resource is the node 114 def managed_resource_type_distribution 115 type_distribution = {} 116 if File.exist?(Puppet[:resourcefile]) 117 File.readlines(Puppet[:resourcefile]).each do |line| 118 type = line.split("[").first.split("::").map { 119 |i| i.capitalize}.join("::") 120 type_distribution[type] ||= 0 121 type_distribution[type] += 1 122 end 123 end 124 type_distribution 125 end 126 127 # Reads the last run report and extracts the log lines 128 # 129 # @return [Array<Hash>] 130 def last_run_logs 131 return [] unless File.exists?(Puppet[:lastrunreport]) 132 133 report = YAML.load_file(Puppet[:lastrunreport]) 134 135 report.logs.map do |line| 136 { 137 "time_utc" => line.time.utc.to_i, 138 "time" => line.time.to_i, 139 "level" => line.level.to_s, 140 "source" => line.source, 141 "msg" => line.message.chomp 142 } 143 end 144 end 145 146 # covert seconds to human readable string 147 def seconds_to_human(seconds) 148 days = seconds / 86400 149 seconds -= 86400 * days 150 151 hours = seconds / 3600 152 seconds -= 3600 * hours 153 154 minutes = seconds / 60 155 seconds -= 60 * minutes 156 157 if days > 1 158 return "%d days %d hours %d minutes %02d seconds" % [ 159 days, hours, minutes, seconds] 160 elsif days == 1 161 return "%d day %d hours %d minutes %02d seconds" % [ 162 days, hours, minutes, seconds] 163 elsif hours > 0 164 return "%d hours %d minutes %02d seconds" % [hours, minutes, seconds] 165 elsif minutes > 0 166 return "%d minutes %02d seconds" % [minutes, seconds] 167 else 168 return "%02d seconds" % seconds 169 end 170 end 171 172 # simple utility to return a hash with lots of useful information 173 # about the state of the agent 174 def status 175 the_last_run = lastrun 176 status = {:applying => applying?, 177 :enabled => enabled?, 178 :daemon_present => daemon_present?, 179 :lastrun => the_last_run, 180 :idling => idling?, 181 :disable_message => lock_message, 182 :since_lastrun => (Time.now.to_i - the_last_run)} 183 184 if !status[:enabled] 185 status[:status] = "disabled" 186 elsif status[:applying] 187 status[:status] = "applying a catalog" 188 elsif status[:idling] 189 status[:status] = "idling" 190 elsif !status[:applying] 191 status[:status] = "stopped" 192 end 193 194 status[:message] = "Currently %s; last completed run %s ago" \ 195 % [status[:status], seconds_to_human(status[:since_lastrun])] 196 status 197 end 198 199 # returns true if name is a single letter or an alphanumeric string 200 def valid_name?(name) 201 if name.length == 1 202 return false unless name =~ /\A[a-zA-Z]\Z/ 203 else 204 return false unless name =~ /\A[a-zA-Z0-9_]+\Z/ 205 end 206 true 207 end 208 209 # validates arguments and returns the CL options to execute puppet 210 def create_common_puppet_cli(noop=nil, tags=[], environment=nil, 211 server=nil, splay=nil, splaylimit=nil, 212 ignoreschedules=nil, use_cached_catalog=nil) 213 opts = [] 214 tags = [tags].flatten.compact 215 216 MCollective::Util::PuppetServerAddressValidation.validate_server(server) 217 hostname, port = \ 218 MCollective::Util::PuppetServerAddressValidation.parse_name_and_port_of(server) 219 220 if environment && !valid_name?(environment) 221 raise("Invalid environment '%s' specified" % environment) 222 end 223 224 if splaylimit && !splaylimit.is_a?(Integer) 225 raise("Invalid splaylimit '%s' specified" % splaylimit) 226 end 227 228 unless tags.empty? 229 [tags].flatten.each do |tag| 230 tag.split("::").each do |part| 231 raise("Invalid tag '%s' specified" % tag) unless valid_name?(part) 232 end 233 end 234 opts << "--tags %s" % tags.join(",") 235 end 236 237 opts << "--splay" if splay == true 238 opts << "--no-splay" if splay == false 239 opts << "--splaylimit %s" % splaylimit if splaylimit 240 opts << "--noop" if noop == true 241 opts << "--no-noop" if noop == false 242 opts << "--environment %s" % environment if environment 243 opts << "--server %s" % hostname if hostname 244 opts << "--masterport %s" % port if port 245 opts << "--ignoreschedules" if ignoreschedules 246 opts << "--use_cached_catalog" if use_cached_catalog == true 247 opts << "--no-use_cached_catalog" if use_cached_catalog == false 248 opts 249 end 250 251 def run_in_foreground(clioptions, execute=true) 252 options = ["--onetime", "--no-daemonize", "--color=false", 253 "--show_diff", "--verbose"].concat(clioptions) 254 return options unless execute 255 %x[puppet agent #{options.join(' ')}] 256 end 257 258 def run_in_background(clioptions, execute=true) 259 options =["--onetime", 260 "--daemonize", "--color=false"].concat(clioptions) 261 return options unless execute 262 %x[puppet agent #{options.join(' ')}] 263 end 264 265 # do a run based on the following options: 266 # 267 # :foreground_run - runs in the foreground a --test run 268 # :foreground_run - runs in the foreground a --onetime --no-daemonize 269 # --show_diff --verbose run 270 # :signal_daemon - if the daemon is running, sends it USR1 to wake it up 271 # :noop - enables or disabled noop mode based on true/false 272 # :tags - an array of tags to limit the run to 273 # :environment - the environment to run 274 # :server - puppet master to use, can be some.host or some.host:port 275 # :splay - enables or disables splay based on true/false 276 # :splaylimit - set the maximum splay time 277 # :ignoreschedules - instructs puppet to ignore any defined schedules 278 # 279 # else a single background run will be attempted but this will fail if 280 # an idling daemon is present and :signal_daemon was false 281 def runonce!(options={}) 282 valid_options = [:noop, :signal_daemon, :foreground_run, :tags, 283 :environment, :server, :splay, :splaylimit, 284 :options_only, :ignoreschedules, :use_cached_catalog] 285 286 options.keys.each do |opt| 287 unless valid_options.include?(opt) 288 raise("Unknown option %s specified" % opt) 289 end 290 end 291 292 if applying? 293 raise "Puppet is currently applying a catalog, cannot run now" 294 end 295 296 if disabled? 297 raise "Puppet is disabled, cannot run now" 298 end 299 300 splay = options.fetch(:splay, nil) 301 splaylimit = options.fetch(:splaylimit, nil) 302 noop = options.fetch(:noop, nil) 303 signal_daemon = options.fetch(:signal_daemon, 304 Util.str_to_bool(Config.instance.pluginconf.fetch("puppet.signal_daemon", "true"))) 305 foreground_run = options.fetch(:foreground_run, false) 306 environment = options.fetch(:environment, nil) 307 server = options.fetch(:server, nil) 308 ignoreschedules = options.fetch(:ignoreschedules, nil) 309 use_cached_catalog = options.fetch(:use_cached_catalog, nil) 310 tags = [ options[:tags] ].flatten.compact 311 312 clioptions = create_common_puppet_cli(noop, tags, environment, 313 server, splay, splaylimit, 314 ignoreschedules, use_cached_catalog) 315 316 if idling? && signal_daemon && !clioptions.empty? 317 raise "Cannot specify any custom puppet options " \ 318 "when the daemon is running" 319 end 320 321 if foreground_run 322 if options[:options_only] 323 return :foreground_run, run_in_foreground(clioptions, false) 324 end 325 return run_in_foreground(clioptions) 326 elsif idling? && signal_daemon 327 return :signal_running_daemon, clioptions if options[:options_only] 328 return signal_running_daemon 329 else 330 raise "Cannot run when the agent is running" if applying? 331 return :run_in_foreground, run_in_foreground(clioptions, false) 332 end 333 end 334 335 def atomic_file(file) 336 tempfile = Tempfile.new(File.basename(file), File.dirname(file)) 337 yield(tempfile) 338 tempfile.close 339 File.rename(tempfile.path, file) 340 end 341 342 ### Unix methods (Windows manager subclass must override) 343 344 # is the agent daemon currently in the unix process list? 345 def daemon_present? 346 if File.exist?(Puppet[:pidfile]) 347 return has_process_for_pid?(File.read(Puppet[:pidfile])) 348 end 349 false 350 end 351 352 # is the agent currently applying a catalog 353 def applying? 354 begin 355 platform_applying? 356 rescue NotImplementedError 357 raise 358 rescue => e 359 Log.warn("Could not determine if Puppet is applying a catalog: " \ 360 "%s: %s: %s" % [e.backtrace.first, e.class, e.to_s]) 361 false 362 end 363 end 364 365 def signal_running_daemon 366 pid = File.read(Puppet[:pidfile]) 367 368 if has_process_for_pid?(pid) 369 begin 370 ::Process.kill("USR1", Integer(pid)) 371 rescue Exception => e 372 raise "Failed to signal the puppet agent at pid " \ 373 "%s: %s" % [pid, e.to_s] 374 end 375 else 376 run_in_background 377 end 378 end 379 380 def has_process_for_pid?(pid) 381 !!::Process.kill(0, Integer(pid)) rescue false 382 end 383 384 ### state of the world 385 386 # is a catalog being applied rigt now? 387 def stopped? 388 !applying? 389 end 390 391 # is the daemon running but not applying a catalog 392 def idling? 393 daemon_present? && !applying? 394 end 395 396 # is the agent enabled 397 def enabled? 398 !disabled? 399 end 400 401 # is a background run allowed? by default it's only allowed if the 402 # daemon isn't present but can be overridden 403 def background_run_allowed? 404 !daemon_present? 405 end 406 407 408 # seconds since the last catalog was applied 409 def since_lastrun 410 (Time.now - lastrun).to_i 411 end 412 413 # if a resource is being managed, resource in the syntax File[/x] etc 414 def managing_resource?(resource) 415 if resource =~ /^(.+?)\[(.+)$/ 416 managed_resources.include?([$1.downcase, $2].join("[")) 417 else 418 raise "Invalid resource name %s" % resource 419 end 420 end 421 422 # how many resources are managed 423 def managed_resources_count 424 managed_resources.size 425 end 426 427 ### methods that must be implemented by subclasses 428 429 def initialize(*args) 430 raise NotImplementedError, "Must use PuppetAgentMgr.manager" 431 end 432 433 # enables the puppet agent, it can now start applying catalogs again 434 def enable! 435 raise NotImplementedError, "Not implemented by subclass" 436 end 437 438 # disable the puppet agent, on version 2.x the message is ignored 439 def disable!(msg=nil) 440 raise NotImplementedError, "Not implemented by subclass" 441 end 442 443 # the current lock message, always "" on 2.0 444 def lock_message 445 raise NotImplementedError, "Not implemented by subclass" 446 end 447 448 # is the agent disabled 449 def disabled? 450 raise NotImplementedError, "Not implemented by subclass" 451 end 452 453 ### private methods 454 455 private 456 457 def platform_applying? 458 raise NotImplementedError, "Not implemented by subclass" 459 end 460 461 end 462 end 463end 464