Class: MCollective::RPC::Agent
- Inherits:
-
Object
- Object
- MCollective::RPC::Agent
- Defined in:
- lib/mcollective/rpc/agent.rb
Overview
A wrapper around the traditional agent, it takes care of a lot of the tedious setup you would do for each agent allowing you to just create methods following a naming standard leaving the heavy lifting up to this clas.
See docs.puppetlabs.com/mcollective/simplerpc/agents.html
It only really makes sense to use this with a Simple RPC client on the other end, basic usage would be:
module MCollective
module Agent
class Helloworld<RPC::Agent
action "hello" do
reply[:msg] = "Hello #{request[:name]}"
end
action "foo" do
implemented_by "/some/script.sh"
end
end
end
end
If you wish to implement the logic for an action using an external script use the implemented_by method that will cause your script to be run with 2 arguments.
The first argument is a file containing JSON with the request and the 2nd argument is where the script should save its output as a JSON hash.
We also currently have the validation code in here, this will be moved to plugins soon.
Direct Known Subclasses
Instance Attribute Summary collapse
-
#agent_name ⇒ Object
Returns the value of attribute agent_name.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#ddl ⇒ Object
readonly
Returns the value of attribute ddl.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#meta ⇒ Object
readonly
Returns the value of attribute meta.
-
#reply ⇒ Object
Returns the value of attribute reply.
-
#request ⇒ Object
Returns the value of attribute request.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Class Method Summary collapse
-
.action(name, &block) ⇒ Object
Creates a new action with the block passed and sets some defaults.
-
.actions ⇒ Object
Returns an array of actions this agent support.
-
.activate? ⇒ Boolean
By default RPC Agents support a toggle in the configuration that can enable and disable them based on the agent name.
-
.activate_when(&block) ⇒ Object
Creates the needed activate? class in a manner similar to the other helpers like action, authorized_by etc.
-
.authorized_by(plugin) ⇒ Object
Helper that creates a method on the class that will call your authorization plugin.
-
.metadata(data) ⇒ Object
Registers meta data for the introspection hash.
Instance Method Summary collapse
-
#after_processing_hook ⇒ Object
Called at the end of processing just before the response gets sent to the middleware.
-
#audit_request(msg, connection) ⇒ Object
Gets called right after a request was received and calls audit plugins.
-
#before_processing_hook(msg, connection) ⇒ Object
Called just after a message was received from the middleware before it gets passed to the handlers.
- #handlemsg(msg, connection) ⇒ Object
-
#implemented_by(command, type = :json) ⇒ Object
handles external actions.
-
#initialize ⇒ Agent
constructor
A new instance of Agent.
- #load_ddl ⇒ Object
-
#run(command, options = {}) ⇒ Object
Runs a command via the MC::Shell wrapper, options are as per MC::Shell.
-
#shellescape(str) ⇒ Object
convenience wrapper around Util#shellescape.
-
#startup_hook ⇒ Object
Called at the end of the RPC::Agent standard initialize method use this to adjust meta parameters, timeouts and any setup you need to do.
-
#validate(key, validation) ⇒ Object
Validates a data member, if validation is a regex then it will try to match it else it supports testing object types only:.
Constructor Details
#initialize ⇒ Agent
Returns a new instance of Agent.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/mcollective/rpc/agent.rb', line 37 def initialize @agent_name = self.class.to_s.split("::").last.downcase load_ddl @logger = Log.instance @config = Config.instance # if we have a global authorization provider enable it # plugins can still override it per plugin self.class.(@config.rpcauthprovider) if @config. startup_hook end |
Instance Attribute Details
#agent_name ⇒ Object
Returns the value of attribute agent_name.
34 35 36 |
# File 'lib/mcollective/rpc/agent.rb', line 34 def agent_name @agent_name end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
35 36 37 |
# File 'lib/mcollective/rpc/agent.rb', line 35 def config @config end |
#ddl ⇒ Object (readonly)
Returns the value of attribute ddl.
35 36 37 |
# File 'lib/mcollective/rpc/agent.rb', line 35 def ddl @ddl end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
35 36 37 |
# File 'lib/mcollective/rpc/agent.rb', line 35 def logger @logger end |
#meta ⇒ Object (readonly)
Returns the value of attribute meta.
35 36 37 |
# File 'lib/mcollective/rpc/agent.rb', line 35 def @meta end |
#reply ⇒ Object
Returns the value of attribute reply.
34 35 36 |
# File 'lib/mcollective/rpc/agent.rb', line 34 def reply @reply end |
#request ⇒ Object
Returns the value of attribute request.
34 35 36 |
# File 'lib/mcollective/rpc/agent.rb', line 34 def request @request end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout.
35 36 37 |
# File 'lib/mcollective/rpc/agent.rb', line 35 def timeout @timeout end |
Class Method Details
.action(name, &block) ⇒ Object
Creates a new action with the block passed and sets some defaults
action “status” do
# logic here to restart service
end
249 250 251 252 253 |
# File 'lib/mcollective/rpc/agent.rb', line 249 def self.action(name, &block) raise "Need to pass a body for the action" unless block_given? module_eval { define_method("#{name}_action", &block) } end |
.actions ⇒ Object
Returns an array of actions this agent support
151 152 153 154 155 |
# File 'lib/mcollective/rpc/agent.rb', line 151 def self.actions public_instance_methods.sort.grep(/_action$/).map do |method| $1 if method =~ /(.+)_action$/ end end |
.activate? ⇒ Boolean
By default RPC Agents support a toggle in the configuration that can enable and disable them based on the agent name
Example an agent called Foo can have:
plugin.foo.activate_agent = false
and this will prevent the agent from loading on this particular machine.
Agents can use the activate_when helper to override this for example:
activate_when do
File.exist?("/usr/bin/puppet")
end
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/mcollective/rpc/agent.rb', line 132 def self.activate? agent_name = to_s.split("::").last.downcase config = Config.instance Log.debug("Starting default activation checks for #{agent_name}") # Check global state to determine if agent should be loaded should_activate = config.activate_agents # Check agent specific state to determine if agent should be loaded should_activate = Util.str_to_bool(config.pluginconf.fetch("#{agent_name}.activate_agent", should_activate)) Log.debug("Found plugin configuration '#{agent_name}.activate_agent' with value '#{should_activate}'") unless should_activate should_activate end |
.activate_when(&block) ⇒ Object
Creates the needed activate? class in a manner similar to the other helpers like action, authorized_by etc
activate_when do
File.exist?("/usr/bin/puppet")
end
238 239 240 241 242 |
# File 'lib/mcollective/rpc/agent.rb', line 238 def self.activate_when(&block) (class << self; self; end).instance_eval do define_method("activate?", &block) end end |
.authorized_by(plugin) ⇒ Object
Helper that creates a method on the class that will call your authorization plugin. If your plugin raises an exception that will abort the request
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/mcollective/rpc/agent.rb', line 257 def self.(plugin) plugin = plugin.to_s.capitalize # turns foo_bar into FooBar plugin = plugin.to_s.split("_").map(&:capitalize).join pluginname = "MCollective::Util::#{plugin}" PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin) # rubocop:disable Style/EvalWithLocation class_eval(" def authorization_hook(request) #{pluginname}.authorize(request) end ") # rubocop:enable Style/EvalWithLocation end |
.metadata(data) ⇒ Object
Registers meta data for the introspection hash
226 227 228 229 230 |
# File 'lib/mcollective/rpc/agent.rb', line 226 def self.(data) agent = File.basename(caller.first).split(":").first Log.warn("Setting metadata in agents has been deprecated, DDL files are now being used for this information. Please update the '#{agent}' agent") end |
Instance Method Details
#after_processing_hook ⇒ Object
Called at the end of processing just before the response gets sent to the middleware.
This gets run outside of the main exception handling block of the agent so you should handle any exceptions you could raise yourself. The reason it is outside of the block is so you’ll have access to even status codes set by the exception handlers. If you do raise an exception it will just be passed onto the runner and processing will fail.
343 |
# File 'lib/mcollective/rpc/agent.rb', line 343 def after_processing_hook; end |
#audit_request(msg, connection) ⇒ Object
Gets called right after a request was received and calls audit plugins
Agents can disable auditing by just overriding this method with a noop one this might be useful for agents that gets a lot of requests or simply if you do not care for the auditing in a specific agent.
350 351 352 353 354 |
# File 'lib/mcollective/rpc/agent.rb', line 350 def audit_request(msg, connection) PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit rescue Exception => e # rubocop:disable Lint/RescueException Log.warn("Audit failed - #{e} - continuing to process message") end |
#before_processing_hook(msg, connection) ⇒ Object
Called just after a message was received from the middleware before it gets passed to the handlers. @request and @reply will already be set, the msg passed is the message as received from the normal mcollective runner and the connection is the actual connector.
333 |
# File 'lib/mcollective/rpc/agent.rb', line 333 def before_processing_hook(msg, connection); end |
#handlemsg(msg, connection) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/mcollective/rpc/agent.rb', line 61 def handlemsg(msg, connection) @request = RPC::Request.new(msg, @ddl) @reply = RPC::Reply.new(@request.action, @ddl) begin # Incoming requests need to be validated against the DDL thus reusing # all the work users put into creating DDLs and creating a consistent # quality of input validation everywhere with the a simple once off # investment of writing a DDL @request.validate! # Calls the authorization plugin if any is defined # if this raises an exception we wil just skip processing this # message (@request) if respond_to?("authorization_hook") # Audits the request, currently continues processing the message # we should make this a configurable so that an audit failure means # a message wont be processed by this node depending on config audit_request(@request, connection) before_processing_hook(msg, connection) if respond_to?("#{@request.action}_action") send("#{@request.action}_action") else raise UnknownRPCAction, "Unknown action '#{@request.action}' for agent '#{@request.agent}'" end rescue RPCAborted => e @reply.fail e.to_s, 1 rescue UnknownRPCAction => e @reply.fail e.to_s, 2 rescue MissingRPCData => e @reply.fail e.to_s, 3 rescue InvalidRPCData, DDLValidationError => e @reply.fail e.to_s, 4 rescue UnknownRPCError => e Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s]) Log.error(e.backtrace.join("\n\t")) @reply.fail e.to_s, 5 rescue Exception => e # rubocop:disable Lint/RescueException Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s]) Log.error(e.backtrace.join("\n\t")) @reply.fail e.to_s, 5 end after_processing_hook if @request.should_respond? @reply.to_hash else Log.debug("Client did not request a response, surpressing reply") nil end end |
#implemented_by(command, type = :json) ⇒ Object
handles external actions
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/mcollective/rpc/agent.rb', line 304 def implemented_by(command, type=:json) runner = ActionRunner.new(command, request, type) res = runner.run reply.fail! "Did not receive data from #{command}" unless res.include?(:data) reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash) reply.data.merge!(res[:data]) reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus] if res[:exitstatus] > 0 rescue Exception => e # rubocop:disable Lint/RescueException Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}") reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}" end |
#load_ddl ⇒ Object
52 53 54 55 56 57 58 59 |
# File 'lib/mcollective/rpc/agent.rb', line 52 def load_ddl @ddl = DDL.new(@agent_name, :agent) @meta = @ddl. @timeout = @meta[:timeout] || 10 rescue Exception => e # rubocop:disable Lint/RescueException Log.error("Failed to load DDL for the '%s' agent, DDLs are required: %s: %s" % [@agent_name, e.class, e.to_s]) raise DDLValidationError end |
#run(command, options = {}) ⇒ Object
Runs a command via the MC::Shell wrapper, options are as per MC::Shell
The simplest use is:
out = ""
err = ""
status = run("echo 1", :stdout => out, :stderr => err)
reply[:out] = out
reply[:error] = err
reply[:exitstatus] = status
This can be simplified as:
reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
You can set a command specific environment and cwd:
run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
This will run ‘echo 1’ from /tmp with FOO=BAR in addition to a setting forcing LC_ALL = C. To prevent LC_ALL from being set either set it specifically or:
run("echo 1", :cwd => "/tmp", :environment => nil)
Exceptions here will be handled by the usual agent exception handler or any specific one you create, if you dont it will just fall through and be sent to the client.
If the shell handler fails to return a Process::Status instance for exit status this method will return -1 as the exit status
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/mcollective/rpc/agent.rb', line 188 def run(command, ={}) shellopts = {} # force stderr and stdout to be strings as the library # will append data to them if given using the << method. # # if the data pased to :stderr or :stdin is a Symbol # add that into the reply hash with that Symbol [:stderr, :stdout].each do |k| if .include?(k) if [k].is_a?(Symbol) reply[[k]] = "" shellopts[k] = reply[[k]] elsif [k].respond_to?("<<") shellopts[k] = [k] else reply.fail! "#{k} should support << while calling run(#{command})" end end end [:stdin, :cwd, :environment, :timeout].each do |k| shellopts[k] = [k] if .include?(k) end shell = Shell.new(command, shellopts) shell.runcommand if [:chomp] shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String) shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String) end shell.status.exitstatus rescue -1 end |
#shellescape(str) ⇒ Object
convenience wrapper around Util#shellescape
299 300 301 |
# File 'lib/mcollective/rpc/agent.rb', line 299 def shellescape(str) Util.shellescape(str) end |
#startup_hook ⇒ Object
Called at the end of the RPC::Agent standard initialize method use this to adjust meta parameters, timeouts and any setup you need to do.
This will not be called right when the daemon starts up, we use lazy loading and initialization so it will only be called the first time a request for this agent arrives.
327 |
# File 'lib/mcollective/rpc/agent.rb', line 327 def startup_hook; end |
#validate(key, validation) ⇒ Object
Validates a data member, if validation is a regex then it will try to match it else it supports testing object types only:
validate :msg, String validate :msg, /^[ws]+$/
There are also some special helper validators:
validate :command, :shellsafe validate :command, :ipv6address validate :command, :ipv4address validate :command, :boolean validate :command, [“start”, “stop”]
It will raise appropriate exceptions that the RPC system understand
290 291 292 293 294 295 296 |
# File 'lib/mcollective/rpc/agent.rb', line 290 def validate(key, validation) raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key) Validator.validate(@request[key], validation) rescue ValidatorError => e raise InvalidRPCData, "Input %s did not pass validation: %s" % [key, e.] end |