PageRenderTime 56ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/mcollective/rpc/agent.rb

http://mcollective.googlecode.com/
Ruby | 328 lines | 161 code | 50 blank | 117 comment | 8 complexity | fd494932152547218e7bde569900be04 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", :description => "Hello action" do
  23. # reply[:msg] = "Hello #{request[:name]}"
  24. # end
  25. #
  26. # input "name", "hello",
  27. # :prompt => "Name",
  28. # :description => "The name of the user",
  29. # :type => :string,
  30. # :validation => '/./',
  31. # :maxlength => 50
  32. # end
  33. # end
  34. # end
  35. #
  36. # The mata data, input definitions and descriptions are there to help web UI's
  37. # auto generate user interfaces for your client as well as to provide automagical
  38. # validation of inputs etc.
  39. #
  40. # We also currently have the validation code in here, this will be moved to plugins soon.
  41. class Agent
  42. attr_accessor :meta, :reply, :request
  43. attr_reader :logger, :config, :timeout
  44. # introspection variables
  45. @@actions = {}
  46. @@meta = {}
  47. def initialize
  48. @timeout = @@meta[:timeout] || 10
  49. @logger = Log.instance
  50. @config = Config.instance
  51. @meta = {}
  52. # if we're using the new meta data, use that for the timeout
  53. @meta[:timeout] = @@meta[:timeout] if @@meta.include?(:timeout)
  54. startup_hook
  55. end
  56. def handlemsg(msg, connection)
  57. @request = RPC.request(msg)
  58. @reply = RPC.reply
  59. begin
  60. # Calls the authorization plugin if any is defined
  61. # if this raises an exception we wil just skip processing this
  62. # message
  63. authorization_hook(@request) if respond_to?("authorization_hook")
  64. # Audits the request, currently continues processing the message
  65. # we should make this a configurable so that an audit failure means
  66. # a message wont be processed by this node depending on config
  67. audit_request(@request, connection)
  68. before_processing_hook(msg, connection)
  69. if respond_to?("#{@request.action}_action")
  70. send("#{@request.action}_action")
  71. else
  72. raise UnknownRPCAction, "Unknown action: #{@request.action}"
  73. end
  74. rescue RPCAborted => e
  75. @reply.fail e.to_s, 1
  76. rescue UnknownRPCAction => e
  77. @reply.fail e.to_s, 2
  78. rescue MissingRPCData => e
  79. @reply.fail e.to_s, 3
  80. rescue InvalidRPCData => e
  81. @reply.fail e.to_s, 4
  82. rescue UnknownRPCError => 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. template = IO.readlines(template).join
  92. meta = @@meta
  93. actions = @@actions
  94. erb = ERB.new(template, 0, '%')
  95. erb.result(binding)
  96. end
  97. # Compatibility layer for help as currently implimented in the
  98. # normal non SimpleRPC agent, it uses our introspection data
  99. # to auto generate help
  100. def help
  101. self.help("#{@config[:configdir]}/rpc-help.erb")
  102. end
  103. # Returns an array of actions this agent support
  104. def self.actions
  105. public_instance_methods.sort.grep(/_action$/).map do |method|
  106. $1 if method =~ /(.+)_action$/
  107. end
  108. end
  109. # Returns the interface for a specific action
  110. def self.action_interface(name)
  111. @@actions[name] || {}
  112. end
  113. # Returns the meta data for an agent
  114. def self.meta
  115. @@meta
  116. end
  117. private
  118. # Registers meta data for the introspection hash
  119. def self.metadata(meta)
  120. [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
  121. raise "Metadata needs a :#{arg}" unless meta.include?(arg)
  122. end
  123. @@meta = meta
  124. end
  125. # Creates a new action wit the block passed and sets some defaults
  126. #
  127. # action(:description => "Restarts a Service") do
  128. # # logic here to restart service
  129. # end
  130. def self.action(name, input, &block)
  131. raise "Action needs a :description" unless input.include?(:description)
  132. unless @@actions.include?(name)
  133. @@actions[name] = {}
  134. @@actions[name][:action] = name
  135. @@actions[name][:input] = {}
  136. @@actions[name][:description] = input[:description]
  137. end
  138. # If a block was passed use it to create the action
  139. # but this is optional and a user can just use
  140. # def to create the method later on still
  141. self.module_eval { define_method("#{name}_action", &block) } if block_given?
  142. end
  143. # Registers an input argument for a given action
  144. #
  145. # input "foo", "action",
  146. # :prompt => "Service Action",
  147. # :description => "The action to perform",
  148. # :type => :list,
  149. # :list => ["start", "stop", "restart", "status"]
  150. def self.input(argument, action, properties, &block)
  151. [:prompt, :description, :type].each do |arg|
  152. raise "Input needs a :#{arg}" unless properties.include?(arg)
  153. end
  154. # in case a user is making the action using a traditional
  155. # def we will just create an empty description with no block
  156. unless @@actions.include?(action)
  157. action action, :description => ""
  158. end
  159. @@actions[action][:input][argument] = {:prompt => properties[:prompt],
  160. :description => properties[:description],
  161. :type => properties[:type]}
  162. case properties[:type]
  163. when :string
  164. raise "Input type :string needs a :validation" unless properties.include?(:validation)
  165. raise "String inputs need a :maxlength" unless properties.include?(:validation)
  166. @@actions[action][:input][argument][:validation] = properties[:validation]
  167. @@actions[action][:input][argument][:maxlength] = properties[:maxlength]
  168. when :list
  169. raise "Input type :list needs a :list argument" unless properties.include?(:list)
  170. @@actions[action][:input][argument][:list] = properties[:list]
  171. end
  172. end
  173. # Helper that creates a method on the class that will call your authorization
  174. # plugin. If your plugin raises an exception that will abort the request
  175. def self.authorized_by(plugin)
  176. plugin = plugin.to_s.capitalize
  177. # turns foo_bar into FooBar
  178. plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
  179. pluginname = "MCollective::Util::#{plugin}"
  180. PluginManager.loadclass(pluginname)
  181. class_eval("
  182. def authorization_hook(request)
  183. #{pluginname}.authorize(request)
  184. end
  185. ")
  186. end
  187. # Validates a data member, if validation is a regex then it will try to match it
  188. # else it supports testing object types only:
  189. #
  190. # validate :msg, String
  191. # validate :msg, /^[\w\s]+$/
  192. #
  193. # There are also some special helper validators:
  194. #
  195. # validate :command, :shellsafe
  196. # validate :command, :ipv6address
  197. # validate :command, :ipv4address
  198. #
  199. # It will raise appropriate exceptions that the RPC system understand
  200. #
  201. # TODO: this should be plugins, 1 per validatin method so users can add their own
  202. # at the moment i have it here just to proof the point really
  203. def validate(key, validation)
  204. raise MissingRPCData, "please supply a #{key}" unless @request.include?(key)
  205. begin
  206. if validation.is_a?(Regexp)
  207. raise InvalidRPCData, "#{key} should match #{validation}" unless @request[key].match(validation)
  208. elsif validation.is_a?(Symbol)
  209. case validation
  210. when :shellsafe
  211. raise InvalidRPCData, "#{key} should be a String" unless @request[key].is_a?(String)
  212. raise InvalidRPCData, "#{key} should not have > in it" if @request[key].match(/>/)
  213. raise InvalidRPCData, "#{key} should not have < in it" if @request[key].match(/</)
  214. raise InvalidRPCData, "#{key} should not have \` in it" if @request[key].match(/\`/)
  215. raise InvalidRPCData, "#{key} should not have | in it" if @request[key].match(/\|/)
  216. when :ipv6address
  217. begin
  218. require 'ipaddr'
  219. ip = IPAddr.new(@request[key])
  220. raise InvalidRPCData, "#{key} should be an ipv6 address" unless ip.ipv6?
  221. rescue
  222. raise InvalidRPCData, "#{key} should be an ipv6 address"
  223. end
  224. when :ipv4address
  225. begin
  226. require 'ipaddr'
  227. ip = IPAddr.new(@request[key])
  228. raise InvalidRPCData, "#{key} should be an ipv4 address" unless ip.ipv4?
  229. rescue
  230. raise InvalidRPCData, "#{key} should be an ipv4 address"
  231. end
  232. end
  233. else
  234. raise InvalidRPCData, "#{key} should be a #{validation}" unless @request.data[key].is_a?(validation)
  235. end
  236. rescue Exception => e
  237. raise UnknownRPCError, "Failed to validate #{key}: #{e}"
  238. end
  239. end
  240. # Called at the end of the RPC::Agent standard initialize method
  241. # use this to adjust meta parameters, timeouts and any setup you
  242. # need to do.
  243. #
  244. # This will not be called right when the daemon starts up, we use
  245. # lazy loading and initialization so it will only be called the first
  246. # time a request for this agent arrives.
  247. def startup_hook
  248. end
  249. # Called just after a message was received from the middleware before
  250. # it gets passed to the handlers. @request and @reply will already be
  251. # set, the msg passed is the message as received from the normal
  252. # mcollective runner and the connection is the actual connector.
  253. def before_processing_hook(msg, connection)
  254. end
  255. # Called at the end of processing just before the response gets sent
  256. # to the middleware.
  257. #
  258. # This gets run outside of the main exception handling block of the agent
  259. # so you should handle any exceptions you could raise yourself. The reason
  260. # it is outside of the block is so you'll have access to even status codes
  261. # set by the exception handlers. If you do raise an exception it will just
  262. # be passed onto the runner and processing will fail.
  263. def after_processing_hook
  264. end
  265. # Gets called right after a request was received and calls audit plugins
  266. #
  267. # Agents can disable auditing by just overriding this method with a noop one
  268. # this might be useful for agents that gets a lot of requests or simply if you
  269. # do not care for the auditing in a specific agent.
  270. def audit_request(msg, connection)
  271. PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
  272. rescue Exception => e
  273. @logger.warn("Audit failed - #{e} - continuing to process message")
  274. end
  275. end
  276. end
  277. end
  278. # vi:tabstop=4:expandtab:ai