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

/mcollective-2.0.0/lib/mcollective/rpc/agent.rb

#
Ruby | 441 lines | 209 code | 62 blank | 170 comment | 21 complexity | 7506333e5bad04562fb91d3cc38314ce 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. #
  26. # action "foo" do
  27. # implemented_by "/some/script.sh"
  28. # end
  29. # end
  30. # end
  31. # end
  32. #
  33. # If you wish to implement the logic for an action using an external script use the
  34. # implemented_by method that will cause your script to be run with 2 arguments.
  35. #
  36. # The first argument is a file containing JSON with the request and the 2nd argument
  37. # is where the script should save its output as a JSON hash.
  38. #
  39. # We also currently have the validation code in here, this will be moved to plugins soon.
  40. class Agent
  41. attr_accessor :meta, :reply, :request
  42. attr_reader :logger, :config, :timeout, :ddl
  43. def initialize
  44. # Default meta data unset
  45. @meta = {:timeout => 10,
  46. :name => "Unknown",
  47. :description => "Unknown",
  48. :author => "Unknown",
  49. :license => "Unknown",
  50. :version => "Unknown",
  51. :url => "Unknown"}
  52. @timeout = meta[:timeout] || 10
  53. @logger = Log.instance
  54. @config = Config.instance
  55. @agent_name = self.class.to_s.split("::").last.downcase
  56. # Loads the DDL so we can later use it for validation
  57. # and help generation
  58. begin
  59. @ddl = DDL.new(@agent_name)
  60. rescue Exception => e
  61. Log.debug("Failed to load DDL for agent: #{e.class}: #{e}")
  62. @ddl = nil
  63. end
  64. # if we have a global authorization provider enable it
  65. # plugins can still override it per plugin
  66. self.class.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization
  67. startup_hook
  68. end
  69. def handlemsg(msg, connection)
  70. @request = RPC.request(msg)
  71. @reply = RPC.reply
  72. begin
  73. # Calls the authorization plugin if any is defined
  74. # if this raises an exception we wil just skip processing this
  75. # message
  76. authorization_hook(@request) if respond_to?("authorization_hook")
  77. # Audits the request, currently continues processing the message
  78. # we should make this a configurable so that an audit failure means
  79. # a message wont be processed by this node depending on config
  80. audit_request(@request, connection)
  81. before_processing_hook(msg, connection)
  82. if respond_to?("#{@request.action}_action")
  83. send("#{@request.action}_action")
  84. else
  85. raise UnknownRPCAction, "Unknown action: #{@request.action}"
  86. end
  87. rescue RPCAborted => e
  88. @reply.fail e.to_s, 1
  89. rescue UnknownRPCAction => e
  90. @reply.fail e.to_s, 2
  91. rescue MissingRPCData => e
  92. @reply.fail e.to_s, 3
  93. rescue InvalidRPCData => e
  94. @reply.fail e.to_s, 4
  95. rescue UnknownRPCError => e
  96. @reply.fail e.to_s, 5
  97. rescue Exception => e
  98. @reply.fail e.to_s, 5
  99. end
  100. after_processing_hook
  101. if @request.should_respond?
  102. return @reply.to_hash
  103. else
  104. Log.debug("Client did not request a response, surpressing reply")
  105. return nil
  106. end
  107. end
  108. # By default RPC Agents support a toggle in the configuration that
  109. # can enable and disable them based on the agent name
  110. #
  111. # Example an agent called Foo can have:
  112. #
  113. # plugin.foo.activate_agent = false
  114. #
  115. # and this will prevent the agent from loading on this particular
  116. # machine.
  117. #
  118. # Agents can use the activate_when helper to override this for example:
  119. #
  120. # activate_when do
  121. # File.exist?("/usr/bin/puppet")
  122. # end
  123. def self.activate?
  124. agent_name = self.to_s.split("::").last.downcase
  125. Log.debug("Starting default activation checks for #{agent_name}")
  126. should_activate = Config.instance.pluginconf["#{agent_name}.activate_agent"]
  127. if should_activate
  128. Log.debug("Found plugin config #{agent_name}.activate_agent with value #{should_activate}")
  129. unless should_activate =~ /^1|y|true$/
  130. return false
  131. end
  132. end
  133. return true
  134. end
  135. # Generates help using the template based on the data
  136. # created with metadata and input
  137. def self.help(template)
  138. if @ddl
  139. @ddl.help(template)
  140. else
  141. "No DDL defined"
  142. end
  143. end
  144. # to auto generate help
  145. def help
  146. self.help("#{@config[:configdir]}/rpc-help.erb")
  147. end
  148. # Returns an array of actions this agent support
  149. def self.actions
  150. public_instance_methods.sort.grep(/_action$/).map do |method|
  151. $1 if method =~ /(.+)_action$/
  152. end
  153. end
  154. private
  155. # Runs a command via the MC::Shell wrapper, options are as per MC::Shell
  156. #
  157. # The simplest use is:
  158. #
  159. # out = ""
  160. # err = ""
  161. # status = run("echo 1", :stdout => out, :stderr => err)
  162. #
  163. # reply[:out] = out
  164. # reply[:error] = err
  165. # reply[:exitstatus] = status
  166. #
  167. # This can be simplified as:
  168. #
  169. # reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
  170. #
  171. # You can set a command specific environment and cwd:
  172. #
  173. # run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
  174. #
  175. # This will run 'echo 1' from /tmp with FOO=BAR in addition to a setting forcing
  176. # LC_ALL = C. To prevent LC_ALL from being set either set it specifically or:
  177. #
  178. # run("echo 1", :cwd => "/tmp", :environment => nil)
  179. #
  180. # Exceptions here will be handled by the usual agent exception handler or any
  181. # specific one you create, if you dont it will just fall through and be sent
  182. # to the client.
  183. #
  184. # If the shell handler fails to return a Process::Status instance for exit
  185. # status this method will return -1 as the exit status
  186. def run(command, options={})
  187. shellopts = {}
  188. # force stderr and stdout to be strings as the library
  189. # will append data to them if given using the << method.
  190. #
  191. # if the data pased to :stderr or :stdin is a Symbol
  192. # add that into the reply hash with that Symbol
  193. [:stderr, :stdout].each do |k|
  194. if options.include?(k)
  195. if options[k].is_a?(Symbol)
  196. reply[ options[k] ] = ""
  197. shellopts[k] = reply[ options[k] ]
  198. else
  199. if options[k].respond_to?("<<")
  200. shellopts[k] = options[k]
  201. else
  202. reply.fail! "#{k} should support << while calling run(#{command})"
  203. end
  204. end
  205. end
  206. end
  207. [:stdin, :cwd, :environment].each do |k|
  208. if options.include?(k)
  209. shellopts[k] = options[k]
  210. end
  211. end
  212. shell = Shell.new(command, shellopts)
  213. shell.runcommand
  214. if options[:chomp]
  215. shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String)
  216. shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String)
  217. end
  218. shell.status.exitstatus rescue -1
  219. end
  220. # Registers meta data for the introspection hash
  221. def self.metadata(data)
  222. [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
  223. raise "Metadata needs a :#{arg}" unless data.include?(arg)
  224. end
  225. # Our old style agents were able to do all sorts of things to the meta
  226. # data during startup_hook etc, don't really want that but also want
  227. # backward compat.
  228. #
  229. # Here if you're using the new metadata way this replaces the getter
  230. # with one that always return the same data, setter will still work but
  231. # wont actually do anything of note.
  232. define_method("meta") {
  233. data
  234. }
  235. end
  236. # Creates the needed activate? class in a manner similar to the other
  237. # helpers like action, authorized_by etc
  238. #
  239. # activate_when do
  240. # File.exist?("/usr/bin/puppet")
  241. # end
  242. def self.activate_when(&block)
  243. (class << self; self; end).instance_eval do
  244. define_method("activate?", &block)
  245. end
  246. end
  247. # Creates a new action with the block passed and sets some defaults
  248. #
  249. # action "status" do
  250. # # logic here to restart service
  251. # end
  252. def self.action(name, &block)
  253. raise "Need to pass a body for the action" unless block_given?
  254. self.module_eval { define_method("#{name}_action", &block) }
  255. end
  256. # Helper that creates a method on the class that will call your authorization
  257. # plugin. If your plugin raises an exception that will abort the request
  258. def self.authorized_by(plugin)
  259. plugin = plugin.to_s.capitalize
  260. # turns foo_bar into FooBar
  261. plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
  262. pluginname = "MCollective::Util::#{plugin}"
  263. PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin)
  264. class_eval("
  265. def authorization_hook(request)
  266. #{pluginname}.authorize(request)
  267. end
  268. ")
  269. end
  270. # Validates a data member, if validation is a regex then it will try to match it
  271. # else it supports testing object types only:
  272. #
  273. # validate :msg, String
  274. # validate :msg, /^[\w\s]+$/
  275. #
  276. # There are also some special helper validators:
  277. #
  278. # validate :command, :shellsafe
  279. # validate :command, :ipv6address
  280. # validate :command, :ipv4address
  281. # validate :command, :boolean
  282. # validate :command, ["start", "stop"]
  283. #
  284. # It will raise appropriate exceptions that the RPC system understand
  285. #
  286. # TODO: this should be plugins, 1 per validatin method so users can add their own
  287. # at the moment i have it here just to proof the point really
  288. def validate(key, validation)
  289. raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key)
  290. if validation.is_a?(Regexp)
  291. raise InvalidRPCData, "#{key} should match #{validation}" unless @request[key].match(validation)
  292. elsif validation.is_a?(Symbol)
  293. case validation
  294. when :shellsafe
  295. raise InvalidRPCData, "#{key} should be a String" unless @request[key].is_a?(String)
  296. ['`', '$', ';', '|', '&&', '>', '<'].each do |chr|
  297. raise InvalidRPCData, "#{key} should not have #{chr} in it" if @request[key].match(Regexp.escape(chr))
  298. end
  299. when :ipv6address
  300. begin
  301. require 'ipaddr'
  302. ip = IPAddr.new(@request[key])
  303. raise InvalidRPCData, "#{key} should be an ipv6 address" unless ip.ipv6?
  304. rescue
  305. raise InvalidRPCData, "#{key} should be an ipv6 address"
  306. end
  307. when :ipv4address
  308. begin
  309. require 'ipaddr'
  310. ip = IPAddr.new(@request[key])
  311. raise InvalidRPCData, "#{key} should be an ipv4 address" unless ip.ipv4?
  312. rescue
  313. raise InvalidRPCData, "#{key} should be an ipv4 address"
  314. end
  315. when :boolean
  316. raise InvalidRPCData, "#{key} should be boolean" unless [TrueClass, FalseClass].include?(@request[key].class)
  317. end
  318. elsif validation.is_a?(Array)
  319. raise InvalidRPCData, "#{key} should be one of %s" % [ validation.join(", ") ] unless validation.include?(@request[key])
  320. else
  321. raise InvalidRPCData, "#{key} should be a #{validation}" unless @request[key].is_a?(validation)
  322. end
  323. end
  324. # convenience wrapper around Util#shellescape
  325. def shellescape(str)
  326. Util.shellescape(str)
  327. end
  328. # handles external actions
  329. def implemented_by(command, type=:json)
  330. runner = ActionRunner.new(command, request, type)
  331. res = runner.run
  332. reply.fail! "Did not receive data from #{command}" unless res.include?(:data)
  333. reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash)
  334. reply.data.merge!(res[:data])
  335. if res[:exitstatus] > 0
  336. reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus]
  337. end
  338. rescue Exception => e
  339. Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}")
  340. reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}"
  341. end
  342. # Called at the end of the RPC::Agent standard initialize method
  343. # use this to adjust meta parameters, timeouts and any setup you
  344. # need to do.
  345. #
  346. # This will not be called right when the daemon starts up, we use
  347. # lazy loading and initialization so it will only be called the first
  348. # time a request for this agent arrives.
  349. def startup_hook
  350. end
  351. # Called just after a message was received from the middleware before
  352. # it gets passed to the handlers. @request and @reply will already be
  353. # set, the msg passed is the message as received from the normal
  354. # mcollective runner and the connection is the actual connector.
  355. def before_processing_hook(msg, connection)
  356. end
  357. # Called at the end of processing just before the response gets sent
  358. # to the middleware.
  359. #
  360. # This gets run outside of the main exception handling block of the agent
  361. # so you should handle any exceptions you could raise yourself. The reason
  362. # it is outside of the block is so you'll have access to even status codes
  363. # set by the exception handlers. If you do raise an exception it will just
  364. # be passed onto the runner and processing will fail.
  365. def after_processing_hook
  366. end
  367. # Gets called right after a request was received and calls audit plugins
  368. #
  369. # Agents can disable auditing by just overriding this method with a noop one
  370. # this might be useful for agents that gets a lot of requests or simply if you
  371. # do not care for the auditing in a specific agent.
  372. def audit_request(msg, connection)
  373. PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
  374. rescue Exception => e
  375. Log.warn("Audit failed - #{e} - continuing to process message")
  376. end
  377. end
  378. end
  379. end