Class: MCollective::RPC::Agent

Inherits:
Object
  • Object
show all
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

Agent::Bolt_tasks

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAgent

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.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization

  startup_hook
end

Instance Attribute Details

#agent_nameObject

Returns the value of attribute agent_name.



34
35
36
# File 'lib/mcollective/rpc/agent.rb', line 34

def agent_name
  @agent_name
end

#configObject (readonly)

Returns the value of attribute config.



35
36
37
# File 'lib/mcollective/rpc/agent.rb', line 35

def config
  @config
end

#ddlObject (readonly)

Returns the value of attribute ddl.



35
36
37
# File 'lib/mcollective/rpc/agent.rb', line 35

def ddl
  @ddl
end

#loggerObject (readonly)

Returns the value of attribute logger.



35
36
37
# File 'lib/mcollective/rpc/agent.rb', line 35

def logger
  @logger
end

#metaObject (readonly)

Returns the value of attribute meta.



35
36
37
# File 'lib/mcollective/rpc/agent.rb', line 35

def meta
  @meta
end

#replyObject

Returns the value of attribute reply.



34
35
36
# File 'lib/mcollective/rpc/agent.rb', line 34

def reply
  @reply
end

#requestObject

Returns the value of attribute request.



34
35
36
# File 'lib/mcollective/rpc/agent.rb', line 34

def request
  @request
end

#timeoutObject (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

.actionsObject

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

Returns:

  • (Boolean)


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.authorized_by(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_hookObject

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
    authorization_hook(@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_ddlObject



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.meta
  @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, options={})
  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 options.include?(k)
      if options[k].is_a?(Symbol)
        reply[options[k]] = ""
        shellopts[k] = reply[options[k]]
      elsif options[k].respond_to?("<<")
        shellopts[k] = options[k]
      else
        reply.fail! "#{k} should support << while calling run(#{command})"
      end
    end
  end

  [:stdin, :cwd, :environment, :timeout].each do |k|
    shellopts[k] = options[k] if options.include?(k)
  end

  shell = Shell.new(command, shellopts)

  shell.runcommand

  if options[: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_hookObject

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.message]
end