PageRenderTime 67ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/mcollective/rpc/agent.rb

https://github.com/sheldonh/marionette-collective
Ruby | 285 lines | 142 code | 43 blank | 100 comment | 9 complexity | a703fab53ce296d9f448c36385453fee MD5 | raw file
Possible License(s): Apache-2.0
  1. module MCollective
  2. module RPC
  3. # A wrapper around the traditional agent, it takes care of a lot of the tedious setup
  4. # you would do for each agent allowing you to just create methods following a naming
  5. # standard leaving the heavy lifting up to this clas.
  6. #
  7. # See http://marionette-collective.org/simplerpc/agents.html
  8. #
  9. # It only really makes sense to use this with a Simple RPC client on the other end, basic
  10. # usage would be:
  11. #
  12. # module MCollective
  13. # module Agent
  14. # class Helloworld<RPC::Agent
  15. # matadata :name => "Test SimpleRPC Agent",
  16. # :description => "A simple test",
  17. # :author => "You",
  18. # :license => "1.1",
  19. # :url => "http://your.com/,
  20. # :timeout => 60
  21. #
  22. # action "hello" do
  23. # reply[:msg] = "Hello #{request[:name]}"
  24. # end
  25. # end
  26. # end
  27. # end
  28. #
  29. # We also currently have the validation code in here, this will be moved to plugins soon.
  30. class Agent
  31. attr_accessor :meta, :reply, :request
  32. attr_reader :logger, :config, :timeout, :ddl
  33. def initialize
  34. # Default meta data unset
  35. @meta = {:timeout => 10,
  36. :name => "Unknown",
  37. :description => "Unknown",
  38. :author => "Unknown",
  39. :license => "Unknown",
  40. :version => "Unknown",
  41. :url => "Unknown"}
  42. @timeout = meta[:timeout] || 10
  43. @logger = Log.instance
  44. @config = Config.instance
  45. @agent_name = self.class.to_s.split("::").last.downcase
  46. # Loads the DDL so we can later use it for validation
  47. # and help generation
  48. begin
  49. @ddl = DDL.new(@agent_name)
  50. rescue
  51. @ddl = nil
  52. end
  53. # if we have a global authorization provider enable it
  54. # plugins can still override it per plugin
  55. self.class.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization
  56. startup_hook
  57. end
  58. def handlemsg(msg, connection)
  59. @request = RPC.request(msg)
  60. @reply = RPC.reply
  61. begin
  62. # Calls the authorization plugin if any is defined
  63. # if this raises an exception we wil just skip processing this
  64. # message
  65. authorization_hook(@request) if respond_to?("authorization_hook")
  66. # Audits the request, currently continues processing the message
  67. # we should make this a configurable so that an audit failure means
  68. # a message wont be processed by this node depending on config
  69. audit_request(@request, connection)
  70. before_processing_hook(msg, connection)
  71. if respond_to?("#{@request.action}_action")
  72. send("#{@request.action}_action")
  73. else
  74. raise UnknownRPCAction, "Unknown action: #{@request.action}"
  75. end
  76. rescue RPCAborted => e
  77. @reply.fail e.to_s, 1
  78. rescue UnknownRPCAction => e
  79. @reply.fail e.to_s, 2
  80. rescue MissingRPCData => e
  81. @reply.fail e.to_s, 3
  82. rescue InvalidRPCData => e
  83. @reply.fail e.to_s, 4
  84. rescue UnknownRPCError => e
  85. @reply.fail e.to_s, 5
  86. rescue Exception => e
  87. @reply.fail e.to_s, 5
  88. end
  89. after_processing_hook
  90. @reply.to_hash
  91. end
  92. # Generates help using the template based on the data
  93. # created with metadata and input
  94. def self.help(template)
  95. if @ddl
  96. @ddl.help(template)
  97. else
  98. "No DDL defined"
  99. end
  100. end
  101. # to auto generate help
  102. def help
  103. self.help("#{@config[:configdir]}/rpc-help.erb")
  104. end
  105. # Returns an array of actions this agent support
  106. def self.actions
  107. public_instance_methods.sort.grep(/_action$/).map do |method|
  108. $1 if method =~ /(.+)_action$/
  109. end
  110. end
  111. private
  112. # Registers meta data for the introspection hash
  113. def self.metadata(data)
  114. [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
  115. raise "Metadata needs a :#{arg}" unless data.include?(arg)
  116. end
  117. # Our old style agents were able to do all sorts of things to the meta
  118. # data during startup_hook etc, don't really want that but also want
  119. # backward compat.
  120. #
  121. # Here if you're using the new metadata way this replaces the getter
  122. # with one that always return the same data, setter will still work but
  123. # wont actually do anything of note.
  124. define_method("meta") {
  125. data
  126. }
  127. end
  128. # Creates a new action wit the block passed and sets some defaults
  129. #
  130. # action "status" do
  131. # # logic here to restart service
  132. # end
  133. def self.action(name, &block)
  134. raise "Need to pass a body for the action" unless block_given?
  135. self.module_eval { define_method("#{name}_action", &block) }
  136. end
  137. # Helper that creates a method on the class that will call your authorization
  138. # plugin. If your plugin raises an exception that will abort the request
  139. def self.authorized_by(plugin)
  140. plugin = plugin.to_s.capitalize
  141. # turns foo_bar into FooBar
  142. plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
  143. pluginname = "MCollective::Util::#{plugin}"
  144. PluginManager.loadclass(pluginname)
  145. class_eval("
  146. def authorization_hook(request)
  147. #{pluginname}.authorize(request)
  148. end
  149. ")
  150. end
  151. # Validates a data member, if validation is a regex then it will try to match it
  152. # else it supports testing object types only:
  153. #
  154. # validate :msg, String
  155. # validate :msg, /^[\w\s]+$/
  156. #
  157. # There are also some special helper validators:
  158. #
  159. # validate :command, :shellsafe
  160. # validate :command, :ipv6address
  161. # validate :command, :ipv4address
  162. #
  163. # It will raise appropriate exceptions that the RPC system understand
  164. #
  165. # TODO: this should be plugins, 1 per validatin method so users can add their own
  166. # at the moment i have it here just to proof the point really
  167. def validate(key, validation)
  168. raise MissingRPCData, "please supply a #{key}" unless @request.include?(key)
  169. begin
  170. if validation.is_a?(Regexp)
  171. raise InvalidRPCData, "#{key} should match #{validation}" unless @request[key].match(validation)
  172. elsif validation.is_a?(Symbol)
  173. case validation
  174. when :shellsafe
  175. raise InvalidRPCData, "#{key} should be a String" unless @request[key].is_a?(String)
  176. raise InvalidRPCData, "#{key} should not have > in it" if @request[key].match(/>/)
  177. raise InvalidRPCData, "#{key} should not have < in it" if @request[key].match(/</)
  178. raise InvalidRPCData, "#{key} should not have \` in it" if @request[key].match(/\`/)
  179. raise InvalidRPCData, "#{key} should not have | in it" if @request[key].match(/\|/)
  180. when :ipv6address
  181. begin
  182. require 'ipaddr'
  183. ip = IPAddr.new(@request[key])
  184. raise InvalidRPCData, "#{key} should be an ipv6 address" unless ip.ipv6?
  185. rescue
  186. raise InvalidRPCData, "#{key} should be an ipv6 address"
  187. end
  188. when :ipv4address
  189. begin
  190. require 'ipaddr'
  191. ip = IPAddr.new(@request[key])
  192. raise InvalidRPCData, "#{key} should be an ipv4 address" unless ip.ipv4?
  193. rescue
  194. raise InvalidRPCData, "#{key} should be an ipv4 address"
  195. end
  196. end
  197. else
  198. raise InvalidRPCData, "#{key} should be a #{validation}" unless @request.data[key].is_a?(validation)
  199. end
  200. rescue Exception => e
  201. raise UnknownRPCError, "Failed to validate #{key}: #{e}"
  202. end
  203. end
  204. # Called at the end of the RPC::Agent standard initialize method
  205. # use this to adjust meta parameters, timeouts and any setup you
  206. # need to do.
  207. #
  208. # This will not be called right when the daemon starts up, we use
  209. # lazy loading and initialization so it will only be called the first
  210. # time a request for this agent arrives.
  211. def startup_hook
  212. end
  213. # Called just after a message was received from the middleware before
  214. # it gets passed to the handlers. @request and @reply will already be
  215. # set, the msg passed is the message as received from the normal
  216. # mcollective runner and the connection is the actual connector.
  217. def before_processing_hook(msg, connection)
  218. end
  219. # Called at the end of processing just before the response gets sent
  220. # to the middleware.
  221. #
  222. # This gets run outside of the main exception handling block of the agent
  223. # so you should handle any exceptions you could raise yourself. The reason
  224. # it is outside of the block is so you'll have access to even status codes
  225. # set by the exception handlers. If you do raise an exception it will just
  226. # be passed onto the runner and processing will fail.
  227. def after_processing_hook
  228. end
  229. # Gets called right after a request was received and calls audit plugins
  230. #
  231. # Agents can disable auditing by just overriding this method with a noop one
  232. # this might be useful for agents that gets a lot of requests or simply if you
  233. # do not care for the auditing in a specific agent.
  234. def audit_request(msg, connection)
  235. PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
  236. rescue Exception => e
  237. Log.warn("Audit failed - #{e} - continuing to process message")
  238. end
  239. end
  240. end
  241. end
  242. # vi:tabstop=4:expandtab:ai