2013-03-15

编写SimpleRPC代理

SimpleRPC能够工作是因为它假设了如何来写Agents。下面通过完整的HelloWorld代理来演示。首先需要明白客户端发送数据格式,明白数据接收规则。


接收数据规则

客户端发送请求如下格式:

mc.echo(:msg => "Welcome to MCollective Simple RPC")

更复杂的例子:

exim.setsender(:msgid => "1NOTVx-00028U-7G", :sender => "foo@bar.com")

这为成员变量:msgid和:sendid创建Hash,当然也可以直接使用string:

exim.setsender("msgid" => "1NOTVx-00028U-7G", "senderid" => "foo@bar.com")

只要安全插件支持,数据类型可以保持不变,这也是默认做法。可以传递数组,哈希,哈希的哈希,openstructs但要保证传进来这些是key/value形式。

数据项不能使用:process_results,它对代理和客户端有特殊含义,这暗示代理客户端不会等待进程结果,一般用在不会选择收到回复。


示例代理

以下是一个Helloworld的代理,将输入的参数msg的值输出。


module MCollective
    module Agent
        class Helloworld<RPC::Agent
            # Basic echo server
            action "echo" do
                validate :msg, String
                reply[:msg] = request[:msg]
            end
        end
    end
end

这是一个能工作却不完全的代理。没有帮助没有Metadata.执行过程及结果

[root@master agent]# mco rpc  --agent helloworld --action echo --argument msg="Welcome to MCollective Simple RPC"
Determining the amount of hosts matching filter for 2 seconds .... 2
 * [ ============================================================> ] 2 / 2
Finished processing 2 / 2 hosts in 65.70 ms

代理名称 AgentName

代理名称由类名继承而来,示例代码类名 MCollective::Agent::Helloworld那么代理名称就是helloworld

目标数据和初始化

SimpleRPC仍需要目标数据,没有就只能是默认设置。加了目标数据的代码如下:

module MCollective
    module Agent
        class Helloworld<RPC::Agent
            metadata :name        => "SimpleRPC Sample Agent",
                     :description => "Echo service for MCollective",
                     :author      => "JunQi Lee",
                     :license     => "GPLv2",
                     :version     => "1.1",
                     :url         => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki",
                     :timeout     => 60
            # Basic echo server
            action "echo" do
                validate :msg, String
                reply[:msg] = request[:msg]
            end
        end
    end
end

增加的代码设置了创建者信息,license和版本以及timeout。timeout是在杀掉该代理前允许代理运行的时间。如果设置过短,代理执行完之前就被终结。

写动作 Write Actions

动作(actions)是代理允许执行的单个任务:

action "echo" do
    validate :msg, String

    reply[:msg] = request[:msg]
end

这里创建了一个名为'echo'的动作,它没有任何参数


激活代理

过去仅仅需要将代理拷贝到机器上就能运行,因为所有代理无论依赖都被激活。

为了使部署简单并且支持代理选择特定运行平台,默认情况下代理能够被配置是否激活。

plugin.helloworld.activate_agent = false

你可已在下面的文件中 /etc/mcollective/plugins.d/helloworld.cfg加入这句话(这个文件就是根据代理名在安装目录下的plugin.d目录下自己添加的):

activate_agent = false

这是启用和禁用代理最简单的方式。代理也可已在它的定义文件中声明何时被激活:

module MCollective
    module Agent
        class Helloworld<RPC::Agent

            activate_when do
                File.executable?("/usr/bin/puppet")
            end
        end
    end
end

如果此块返回错误或者异常,此台机器上该代理不会被激活也不会被发现。每次代理加载时都会测试/usr/bin/puppet是否存在,只有存在该代理才会被激活。


帮助和数据描述语言(DDL)

代理ruby文件之外同时还有一个单独文件用于详细描述代理,之前例子的DDL文件如下所示:(注:此处文档语法多处错误,且文档多处单词错误)

metadata :name        => "SimpleRPC Sample Agent",
         :description => "Echo service for MCollective",
         :author      => "R.I.Pienaar",
         :license     => "GPLv2",
         :version     => "1.1",
         :url         => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki",
         :timeout     => 60

action "echo", :description => "Echos back any message it receives" do
    input :msg,
          :prompt      => "Service Name",
          :description => "The service to get the status for",
          :type        => :string,
          :validation  => '^[a-zA-Z\-_\d]+$',
          :optional    => false,
          :maxlength   => 30

    output :msg,
           :description => "The message we received",
           :display_as  => "Message"
end

如上所示,DDL文件语法很简单就是一些标记值,帮助和其他重要的验证信息。DDL语言详细信息见数据描述语言DDL部分。

验证输入

如果按照规则用Hash结构接收发送的数据,然后就可以使用提供的验证器来确定接收的数据正是想要的数据。如果没有采用输入哈希结构,验证器不会起作用。今后基于DDL验证会自动执行,所以强烈建议使用哈希。

在示例的动作里验证:msg输入是String。这有些例子:

1    validate :msg, /[a-zA-Z]+/
2    validate :ipaddr, :ipv4address
3    validate :ipaddr, :ipv6address
4    validate :commmand, :shellsafe
5    validate :mode, ["all", "packages"]

下面的列表展示所有支持的验证器:

类型    说明    举例
正则表达式  将输入与提供的正则表达式匹配    validate :msg, /[a-zA-Z]+/
Type    验证输入是给定ruby数据类型  validate :msg, String
IPv4    验证IPv4地址    validate :ipaddr, :ipv4address
IPv6    验证IPv6地址    validate :ipaddr, :ipv6address
system call safety  确保输入没有><半括号,管道符号之类  validate :command, :shellsafe
Boolean 验证输入时true 或者false    validate :enable, :bool
List of valid options   验证输入是所给列表中之一    validate :mode, [“all”, “packages”]

几点说明:

所有这些检查会引发InvalidRPCDate异常,Simple RPC框见会在适当时候捕获处理所以不用去catch。

可以提供自己的输入验证器,自定义验证类型。

另外如果想让某个String不被传入Shell,可以使用新版本ruby的shellescape来避免:

safe = shellescape(request[:foo])

代理配置

可以在server.cfg中配置代理

plugin.helloworld.setting = foo

上面的配置等同于:

setting = config.pluginconf["helloworld.setting"] || ""

这会使任何未设置选项设置为“”

访问输入

输出可以根据repuest很容易获取,这会是之前向客户端输入的原始请求的Hash值。

request对象是MCollective::RPC::Request的实例,可以获取以下访问全权限:

属性 描述 time 消息发送时间 action 发送的要求代理执行的动作 data 数据的哈希值 sender 发送者的senderid agent 往哪个代理发送

通过这种形式:request.data[:msg]或者request[:msg]

注:根据第一种方式会给你对所有正常哈希方法的访问权限,但是第二种只能获取include中的访问权限。


执行Shell命令

存在一个帮助函数能够事运行Shell命令变得很容易,也可以获取STDOUT,STDERR。推荐所有人使用这个函数来调用shell命令,因为它强制LC_ALL为C而且等待所有的子进程并且避免僵尸进程,可以给它设置唯一的工作路径和shell环境(这个简单地使用ruby提供的system可能无法实现)

最简单地用例就是执行一个命令并且向客户端返回输出:

reply[:status] = run("echo 'hello world'", :stdout => :out, :stderr => :err)

这里可以设置基于命令输出的reply[:out], reply[:err]和 reply[:status]

也可以将输出附加到任意string:

out = []
err = ""
status = run("echo 'hello world'", :stdout => out, :stderr => err)

例子中命令的STDOUT将被存储到变量out中并且不会被送回调用者。唯一需要注意的就是变量out和err有<<方法,可以将输出每一行应用到一个数组一个成员。在例子中out将会是一个拥有多行的数组而err就是一个多行的string.

[root@master application]# mco rpc helloworld echo msg="uname" -v
Determining the amount of hosts matching filter for 2 seconds .... 2

 * [ ============================================================> ] 2 / 2

puppet.example.com                      : OK
    {:status=>0, :msg=>"uname", :out=>"Linux\n", :err=>""}

master.example.com                      : OK
    {:msg=>"uname", :out=>"Linux\n", :err=>""}

---- helloworld#echo call stats ----
           Nodes: 2 / 2
     Pass / Fail: 2 / 0
      Start Time: Wed Jul 25 13:29:02 -0400 2012
  Discovery Time: 2002.91ms
      Agent Time: 119.73ms
      Total Time: 2122.64ms

默认情况下,任何尾随的换行符将被包含在输出和错误:

reply[:status] = run("echo 'hello world'", :stdout => :out, :stderr => :err)
reply[:stdout].chomp!
reply[:stderr].chomp!

如果希望从目录/tmp处运行

reply[:status] = run("echo 'hello world'", :stdout => :out, :stderr => :err, :cwd => "/tmp")

或者希望包含环境变量:

reply[:status] = run("echo 'hello world'", :stdout => :out, :stderr => :err, :environment => {"FOO" => "BAR"})

返回状态是程序执行的返回码,如果程序完全失败比如文件不存在、资源不可用,返回码将是-1。

必须通过这些选项来设置CWD和环境变量。不要在代理中简单地调用chdir和ENV,这些在ruby多线程应用程序中不安全。

关于运行Shell命令部分,源代码主要是在actionrunner.rb中定义。ActionRunner类有如下属性:command, :agent, :action, :format, :stdout, :stderr, :request

其中run函数接收三个参数:command, request, format=:json,命令内容,请求内容,返回格式(默认json).现在只有基于json序列化数据支持今后可能会增加基于key=val形式的数据。

根据源代码,整个类主要是通过把请求对象序列化为一个输入文件(临时文件)并且创建一个空的输出文件,然后创建外部命令读取输出文件。

任何标准输出将会在info级别计入日志,而标准错误将会在error级别记录日志。

执行命令过程中调用封转的外部命令MCollective::Shell。而shell又是封装的ruby systemu执行系统命令,systemu号称在获取标准输出和标准错误和处理子进程上是万能的。Shell有属性::environment, :command, :status, :stdout, :stderr, :stdin, :cwd。程序输出经过精心封装,可根据返回码判断正确执行与否。


结构化回复

回复数据

回复数据在变量reply中,它是MCollective::RPC::Reply的实例。

reply[:msg] = request[:msg]

回复状态

状态列表见写客户端部分。

def rmmsg_action
   validate :msg, String
   validate :msg, /[a-zA-Z]+-[a-zA-Z]+-[a-zA-Z]+-[a-zA-Z]+/
   reply.fail "No such message #{request[:msg]}", 1 unless have_msg?(request[:msg])

   # check all the validation passed before doing any work
   return unless reply.statuscode == 0

   # now remove the message from the queue
end

外部脚本动作

这部分可参考自定节点部分。动作可以使用其他支持JSON的外部语言实现。

action "test" do
    implemented_by "script.py"
end

此外这部分的授权、验证、审计等内容参考本篇其他相应部分。



blog comments powered by Disqus