1module MCollective
2  module Agent
3    class Puppet<RPC::Agent
4      activate_when do
5        require 'mcollective/util/puppet_agent_mgr'
6        true
7      end
8
9      def startup_hook
10        configfile = @config.pluginconf.fetch("puppet.config", nil)
11
12        @puppet_command = @config.pluginconf.fetch("puppet.command", default_agent_command)
13        @puppet_service = @config.pluginconf.fetch("puppet.windows_service", "puppet")
14        @puppet_splaylimit = Integer(@config.pluginconf.fetch("puppet.splaylimit", 30))
15        @puppet_splay = Util.str_to_bool(@config.pluginconf.fetch("puppet.splay", "true"))
16        @puppet_agent = Util::PuppetAgentMgr.manager(configfile, @puppet_service)
17      end
18
19      # Determines the default command to run for puppet agent
20      #
21      # Return the puppet script to execute on the local platform.
22      def default_agent_command
23        if Util.windows?
24          "puppet.bat"
25        else
26          "puppet"
27        end + " agent"
28      end
29
30      def run(command, options)
31        if MCollective::Util.windows?
32          require 'win32/process'
33          # If creating the process doesn't outright fail, assume everything
34          # was okay. The caller wants to know our exit code, so we'll just use
35          # 0 or 1.
36          begin
37            ::Process.create(:command_line => command,
38                             :creation_flags => ::Process::CREATE_NEW_CONSOLE)
39            0
40          rescue Exception => e
41            Log.warn("Failed to execute #{command} - #{e}")
42            1
43          end
44        else
45          # On Unices double fork and exec to run puppet in a disowned child
46          child = fork {
47            # If relying on Puppet on PATH, ensure the default AIO path is included.
48            # On Windows, the MSI adds Puppet to the PATH.
49            if command.start_with?("puppet ")
50              ENV["PATH"] += ":/opt/puppetlabs/bin"
51            end
52
53            grandchild = fork {
54              exec command
55            }
56            if grandchild != nil
57              ::Process.detach(grandchild)
58            end
59          }
60          return 1 if child.nil?
61          ::Process.detach(child)
62          return 0
63        end
64      end
65
66      action "disable" do
67        begin
68          request_msg = request.fetch(:message, "Disabled via MCollective by " \
69                                      "%s at %s" % [request.caller,
70                                                    Time.now.strftime("%F %R")])
71          agent_msg = @puppet_agent.disable!(request_msg)
72          reply[:status] = "Succesfully locked the Puppet agent: %s" % agent_msg
73        rescue => e
74          reply.fail(reply[:status] = "Could not disable Puppet: %s" % e.to_s)
75        end
76
77        reply[:enabled] = @puppet_agent.status[:enabled]
78      end
79
80      action "enable" do
81        begin
82          @puppet_agent.enable!
83          reply[:status] = "Succesfully enabled the Puppet agent"
84        rescue => e
85          reply.fail(reply[:status] = "Could not enable Puppet: %s" % e.to_s)
86        end
87
88        reply[:enabled] = @puppet_agent.status[:enabled]
89      end
90
91      action "last_run_summary" do
92        summary = @puppet_agent.load_summary
93
94        if request[:logs]
95          reply[:logs] = @puppet_agent.last_run_logs
96        else
97          reply[:logs] = {}
98        end
99
100        reply[:type_distribution]     = @puppet_agent.managed_resource_type_distribution
101        reply[:out_of_sync_resources] = summary["resources"].fetch("out_of_sync", 0)
102        reply[:failed_resources]      = summary["resources"].fetch("failed", 0)
103        reply[:corrected_resources]   = summary["resources"].fetch("corrective_change", 0)
104        reply[:changed_resources]     = summary["resources"].fetch("changed", 0)
105        reply[:total_resources]       = summary["resources"].fetch("total", 0)
106        reply[:total_time]            = summary["time"].fetch("total", 0)
107        reply[:config_retrieval_time] = summary["time"].fetch("config_retrieval", 0)
108        reply[:lastrun]               = Integer(summary["time"].fetch("last_run", 0))
109        reply[:since_lastrun]         = Integer(Time.now.to_i - reply[:lastrun])
110        reply[:config_version]        = summary["version"].fetch("config", "unknown")
111        reply[:summary]               = summary
112      end
113
114      action "status" do
115        status = @puppet_agent.status
116
117        @reply.data.merge!(status)
118      end
119
120      action "resource" do
121        allow_managed_resources_management = Util.str_to_bool(
122          @config.pluginconf.fetch("puppet.resource_allow_managed_resources", "false"))
123        resource_types_whitelist = \
124          @config.pluginconf.fetch("puppet.resource_type_whitelist", nil)
125        resource_types_blacklist = \
126          @config.pluginconf.fetch("puppet.resource_type_blacklist", nil)
127
128        if resource_types_whitelist && resource_types_blacklist
129          reply.fail!("You cannot specify both puppet.resource_type_whitelist " \
130                      "and puppet.resource_type_blacklist in the config file")
131        end
132
133        # if 'none' is specified whitelist nothing
134        resource_types_whitelist = "" if resource_types_whitelist == "none"
135
136        # if neither is specified default to whitelisting
137        # nothing thus denying everything
138        if resource_types_blacklist.nil? && resource_types_whitelist.nil?
139          resource_types_whitelist = ""
140        end
141
142
143        params = request.data.clone
144        params.delete(:process_results)
145        type = params.delete(:type).downcase
146        resource_name = "%s[%s]" % [type.to_s.capitalize, params[:name]]
147
148        resource_names_whitelist = \
149          @config.pluginconf.fetch("puppet.resource_name_whitelist.%s" % type, nil)
150        resource_names_blacklist = \
151          @config.pluginconf.fetch("puppet.resource_name_blacklist.%s" % type, nil)
152
153        if resource_names_whitelist && resource_names_blacklist
154          reply.fail!("You cannot specify both puppet.resource_name_whitelist.%s " \
155                      "and puppet.resource_name_blacklist.%s in the config file" % [type, type])
156        end
157
158        if resource_types_blacklist
159          if resource_types_blacklist.split(",").include?(type)
160            reply.fail!("The %s type is listed in the type blacklist" % type)
161          end
162        elsif resource_types_whitelist
163          unless resource_types_whitelist.split(",").include?(type)
164            reply.fail!("The %s type is not listed in the type whitelist" % type)
165          end
166        end
167
168        if resource_names_blacklist
169          if resource_names_blacklist.split(",").include?(params[:name])
170            reply.fail!("The %s name is listed in the name blacklist" % params[:name])
171          end
172        elsif resource_names_whitelist
173          unless resource_names_whitelist.split(",").include?(params[:name])
174            reply.fail!("The %s name is not listed in the name whitelist" % params[:name])
175          end
176        end
177
178        if allow_managed_resources_management \
179           || !@puppet_agent.managing_resource?(resource_name)
180          resource = ::Puppet::Type.type(type).new(params)
181          report = ::Puppet::Transaction::Report.new(:mcollective)
182          ::Puppet::Util::Log.newdestination(report)
183          catalog = ::Puppet::Resource::Catalog.new
184          catalog.add_resource(resource)
185          catalog.apply(:report => report)
186
187          if report.logs.empty?
188            reply[:result] = "no output produced"
189          else
190            reply[:result] = report.logs.join("\n")
191          end
192
193          reply[:changed] = report.resource_statuses[resource_name].changed
194
195          if report.resource_statuses[resource_name].failed
196            reply.fail!("Failed to apply %s: %s" % [resource_name, reply[:result]])
197          end
198        else
199          reply.fail!("Puppet is managing the resource '%s', " \
200                      "refusing to create conflicting states" % resource_name)
201        end
202      end
203
204      action "runonce" do
205        args = {}
206
207        if @puppet_agent.disabled?
208          message = @puppet_agent.lock_message
209
210          if message == ""
211            reply.fail!(reply[:summary] = "Puppet is disabled")
212          else
213            reply.fail!(reply[:summary] = "Puppet is disabled: '%s'" % message)
214          end
215        end
216
217        args[:options_only] = true
218        args[:noop] = request[:noop] if request.include?(:noop)
219        args[:environment] = request[:environment] if request[:environment]
220        if request[:server]
221          if Util.str_to_bool(@config.pluginconf.fetch("puppet.allow_server_override","false"))
222            args[:server] = request[:server]
223          else
224            reply.fail!(reply[:summary] = "Passing 'server' option is not allowed in module configuration")
225          end
226        end
227        args[:tags] = request[:tags].split(",").map{|t| t.strip} if request[:tags]
228        args[:ignoreschedules] = request[:ignoreschedules] if request[:ignoreschedules]
229        args[:signal_daemon] = false if MCollective::Util.windows?
230        args[:use_cached_catalog] = request[:use_cached_catalog] if request.include?(:use_cached_catalog)
231
232        # we can only pass splay arguments if the daemon isn't in signal mode :(
233        signal_daemon = Util.str_to_bool(@config.pluginconf.fetch("puppet.signal_daemon","true"))
234        unless @puppet_agent.status[:daemon_present] && signal_daemon
235          if request[:force] == true
236            # forcing implies --no-splay
237            args[:splay] = false
238          else
239            # respect splay options
240            args[:splay] = request[:splay] if request.include?(:splay)
241            args[:splaylimit] = request[:splaylimit] if request.include?(:splaylimit)
242
243            unless args.include?(:splay)
244              args[:splay] = @puppet_splay
245            end
246
247            if !args.include?(:splaylimit) && args[:splay]
248              args[:splaylimit] = @puppet_splaylimit
249            end
250          end
251        end
252
253        begin
254          run_method, options = @puppet_agent.runonce!(args)
255        rescue => e
256          reply.fail!(reply[:summary] = e.to_s)
257        end
258
259        command = [@puppet_command].concat(options).join(" ")
260
261        case run_method
262          when :run_in_foreground
263            Log.debug("Initiating a puppet agent run using the command: %s" % command)
264
265            exitcode = run(command, {
266              :stdout => :summary,
267              :stderr => :summary,
268              :chomp => true,
269            })
270
271            unless exitcode == 0
272              reply.fail!(reply[:summary] = "Puppet command '%s' had exit " \
273                                            "code %d, expected 0" \
274                                            % [command, exitcode])
275            else
276              reply[:summary] = "Started a Puppet run using the " \
277                                "'%s' command" % command
278            end
279
280          when :signal_running_daemon
281            Log.debug("Signaling the running Puppet agent " \
282                      "to start an immediate run")
283            @puppet_agent.signal_running_daemon
284            reply[:summary] = "Signalled the running Puppet Daemon"
285
286          else
287            reply.fail!(reply[:summary] = "Do not know how to do puppet runs " \
288                                          "using method %s" % run_method)
289        end
290        reply[:initiated_at] = Time.now.to_i
291      end
292    end
293  end
294end
295