PageRenderTime 26ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/mcollective/rpc/agent.rb

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