定制qga(qemu-guest-agent)命令

  • libvirt-qemu-kvm平台是什么

    简单来说,是一套虚拟机平台,是目前很多云平台的底层软件支撑,如云平台openstack底层就使用了这套软件,qemu-guest-agent是里面的一个工具。

    严格来说,这套平台是由3部分组成的,正如名称所示,是由libvirt,qemu,kvm组成。

    1. libvirt是什么?

      按官网的介绍来说,libvirt是

      • 管理虚拟化主机的工具集
      • 可以通过多种编程语言访问(也就是调用),如C、Python、Perl、Java,从而使用这些编程语言就可以管理物理主机与虚拟机
      • 以开源许可协议方式授权
      • 致力于支持多平台:Linux、FreeBSD、Windows、OS-X
      • 被许多应用软件使用

      上面这些没有说的是,libvirt提供API供其它程序调用,支持管理多种虚拟机hypervisor,例如Xen,qemu-kvm

    2. qemu是什么?

      • 模拟器:

        运行为其它平台编译的程序或OS,如在X86 PC上运行arm平台的程序

      • 虚拟机:

        运行与本地平台相同类型的虚拟机。可以在xen环境下运行,也可以基于KVM运行。当基于KVM时,qemu可以虚拟化X86、基于PowerPC的服务器和嵌入式系统、S390、ARM等

    3. KVM是什么?

      • 英文全称为:Kernel-Based Virtual Machine,翻译为中文为“基于内核的虚拟机”
      • 是Linux内核的一个可加载模块,通过调用Linux本身内核功能,实现对CPU的虚拟化和内存的虚拟化,使Linux内核成为虚拟化层。KVM需要硬件支持虚拟化,比如vmx(intel),svm(amd)
    4. 分工

      • libvirt提供管理工具
      • qemu-kvm实现虚拟机,这其中,qemu实现除CPU和内存外的其它设备,如磁盘,网卡的虚拟化;KVM实现内存和CPU的虚拟化
  • qga介绍

    • qemu-guest-agent,简称qga,是虚拟机内部的守护进程,用来接收物理主机发出的命令,对虚拟机进行注入操作或获取虚拟机信息
    • qga有很多命令,比如查看虚拟机CPU信息的guest-get-vcpus子命令。使用命令guest-info可以查看qga中可以调用的子命令列表
    • 在qga执行子命令guest-info后,可以发现有很多命令。但这些命令,都是功能固定的,即一条子命令只能实现一种特定的功能,如子命令guest-file-write只能实现将数据写入虚拟机中文件的功能。这样一来,导致qga虽然命令很多,而我们却无法对虚拟机进行更多的控制
    • 好在qga是开源的,我们能够获取到源码。有了源码后,我们可以直接修改源码,定制出一款满足需求的qga
  • 定制前的准备工作

    在进行定制前,先进行一些准备工作,例如测试的方法,如何制作软件包。

    1. 测试新命令的方法

      先说明如何测试新的qga命令。接下来的示例中,都可以用这一方法来测试

      测试环境(X86):

      组件 版本
      vm CentOS Linux release 7.2.1511
      qga qemu-guest-agent-2.3.0-4.el7
      host CentOS Linux release 7.3.1611
      virsh 3.2.0
      qemu-kvm qemu-kvm-1.5.3-141.el7_4.6
      1. 首先,虚拟机XML配置文件中要存在如下内容:

        <channel type='unix'>
          <target type='virtio' name='org.qemu.guest_agent.0'/>
        </channel>

      2. 使用virsh命令启动虚拟机,假设虚拟机名字为live-centos7.2

        virsh start live-centos7.2

      3. 在主机上执行命令的方法:

        virsh qemu-agent-command live-dwcentos7.2 --cmd '{"execute":"guest-info"}'

        输出内容如下:

        qga guest-info cmd output


        证明qga命令可以正常使用。

    2. 如何制作rpm包

      使用的是centos的系统,在安装定制化的qga时,使用rpm方式安装比较好。下面介绍一下创建qga安装包的方法(以qemu-guest-agent-2.3.0-4.el7为例)。

      1. 安装源码包

        1. 先下载源码包qemu-guest-agent-2.3.0-4.el7.src.rpm
        2. 使用命令安装:

          rpm -i qemu-guest-agent-2.3.0-4.el7.src.rpm

      2. 修改相关文件

        1. rpmbuild -bp ~/rpmbuild/SPECS/qemu-guest-agent.spec
        2. mkdir -p ~/tmp
        3. cp -r ~/rpmbuild/BUILD/qemu-2.3.0/ ~/tmp/
        4. cp -r ~/rpmbuild/BUILD/qemu-2.3.0/ ~/tmp/qemu-2.3.0.new
        5. 修改文件 ~/tmp/qemu-2.3.0.new/qga/qapi-schema.json
        6. 修改文件 ~/tmp/qemu-2.3.0.new/qga/commands-posix.c
      3. 生成补丁文件

        1. diff -rNu ~/tmp/qemu-2.3.0 ~/tmp/qemu-2.3.0.new/ >~/qemu-ga-new-cmd.patch
        2. cp ~/qemu-ga-new-cmd.patch ~/rpmbuild/SOURCES/
      4. 修改spec文件

        编辑文件~/rpmbuild/SPECS/qemu-guest-agent.spec

        1. 找到Patchxxx这样的字符串,xxx表示数字,在其中xxx最大一行下面添加


          Patchxxy: patch_file
          假设xxx是最大的数字,则xxy=xxx+1

          在当前例子中是添加

          Patch1253: qemu-ga-new-cmd.patch

        2. 找到%patchxxx -pn这样的字符串,xxx,n表示数字


          在其中xxx最大的一行下面添加%patchxxy -pm
          xxy,m为数字。假设xxx是最大的数字,则xxy=xxx+1

          在当前例子中是添加

          %patch1253 -p5
          -p后面跟随的数字的含义,可以网上搜一下patch的使用方法

      5. 执行命令生成安装包

        rpmbuild -bb ~/rpmbuild/SPECS/qemu-guest-agent.spec

        命令执行完后,生成的安装包位于:

        ~/rpmbuild/RPMS/x86_64/qemu-guest-agent-2.3.0-4.el7.centos.x86_64.rpm

  • 如何定制qga命令

    定制qga命令本质是基于QAPI框架实现QMP命令。

    • 步骤1 : 在qapi schema(qapi模式)文件中添加命令的声明
    • 步骤2 : 在相应C源码文件中用C实现命令
    • 步骤3 : 测试命令是否正常

    下面,一步一步的说明如何做,由简及繁

    1. 创建无返回值的命令

      新命令是“hello-world”,没有参数,也没有返回值。

      1. 在文件qapi-schema.json中添加:


        { 'command': 'hello-world' }

        说明:

        • command”关键字定义了一个新的QGA命令,新命令是JSON对象格式的;
        • “hello-world”是命令的名称。
      2. 用C语言实现命令“hello-world”


        C代码位置在commands-posix.c中:

        #include <stdlib.h>
        void qmp_hello_world(Error **errp)
        {
            int ret;
            ret = system("touch /Hello-world");
            ret=ret;

            return;
        }

        C语言代码说明:

        • 以“qmp_”作为C语言函数的前缀;
        • qmp_hello_world()无返回值,与json代码对应;
        • 有形参“Error **”。这个参数必须有,后面我们会介绍如何返回错误以及使用更多的参数。如果命令不返回错误的话,就不要操作这个参数;
        • 不需要在头文件中对函数进行声明,QAPI会自动实现;
        • C函数的功能是在系统根目录/下创建文件Hello-world;
        • 函数内与ret变量相关的部分是为了让代码编译通过。
      3. 编译测试

        按照前述制作rpm包的部分,将上述修改添加到qga源码相关文件中,编译制作新的qga rpm包。制作完成后,将旧的qga卸载,安装新的。

        将虚拟机重新启动后,在物理机中按前述测试方法执行命令:
        virsh qemu-agent-command live-centos7.2 --cmd '{"execute":"hello-world"}'

        执行完后,物理主机内返回:

        no-params-ret

         

        同时,在虚拟机内根目录下可看到文件Hello-world

        no-params-result

         

    2. 创建使用参数的命令

      现在,我们给命令hello-world添加参数“message”,参数类型是可选参数,会包含字符串。

      1. 在qapi-schema.json文件中修改命令声明

        { 'command': 'hello-world', 'data': { '*message': 'str' } }

        在命令声明中,新增加了成员“data”,对应的“:”符号后面的元素,是新增加的参数。message前面的星号,表示参数“message”是可选的。“str”表示“message”的类型,表示常量字符串。

        qga命令中支持的参数类型如下所示(针对qemu 2.3.0版本json文件):

        • 整数:int(与int64等价)、uint64、uint32、uint16、uint8、int64、int32、int16、int8
        • 布尔
        • 枚举
        • str(修饰参数时对应于C语言中的const char *,在其它地方对应于C语言中的char *)
        • size(与uint64等价)
        • number(对应于C语言中的double)
        • 自定义的类型。
      2. 更新文件commands-posix.c中的C语言代码


        #include <stdlib.h>

        void qmp_hello_world(bool has_message, const char *message, Error **errp)
        {
            int ret;
            char msg[1024] = {'\0'};

            ret = system("touch /Hello-world");
            if (has_message)
            {
                sprintf(msg,"echo \"%s\" > /Hello-world", message);
            }
            else
            {
                sprintf(msg,"echo \"%s\" > /Hello-world", "hello world");
            }
            ret = system(msg);
            ret = ret;

            return;
        }

      3. 编译测试

        编译安装后,分别执行下述命令

        virsh qemu-agent-command live-centos7.2 --cmd '{"execute":"hello-world"}'

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world","arguments":{"message":"We love KVM"}}'

        两条命令都会返回

        “{"return":{}}”

        has-params-ret1has-params-ret2

        在虚拟机内文件/Hello-world中,分别会看到内容“hello world”和“We love KVM”

        has-params-result

         

    3. 创建处理错误的命令

      在执行命令过程中遇到错误时,qga命令应该使用[qemu-src-path]/include/qapi/error.h头文件中定义的相关接口来处理。

      1. 一般的错误,使用接口error_setg()就可以

        假设前一个示例中字符串message不能包含单词“We”。如果包含了,就返回错误。

        C语言代码如下:

        #include <stdlib.h>

        void qmp_hello_world(bool has_message, const char *message, Error **errp)
        {
            int ret;
            char msg[1024] = {'\0'};

            ret = system("touch /Hello-world");
            if (has_message)

            {
                if (strstr(message, "We"))
                {
                    error_setg(errp, "the word 'We' is not allowed:%d",-1);
                    return;
                }

                sprintf(msg,"echo \"%s\" > /Hello-world", message);
            }
            else
            {
                sprintf(msg,"echo \"%s\" > /Hello-world", "hello world");
            }
            ret = system(msg);
            ret=ret;

            return;
        }

        • error_setg的第一个参数是函数qmp_hello_world的2级指针形参errp
        • error_setg的第二个参数是字符串,使用方法类似于printf函数的参数

        下面,我们输入命令测试一下:

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world","arguments":{"message":"We love KVM"}}'

        反馈结果如下:

        error: internal error: unable to execute QEMU agentcommand 'hello-world': the word 'We' is not allowed:-1

        qga deal error 1

         

      2. 希望返回C库函数错误值的具体涵义时,可以使用error_setg_errno()

        对应的json文件声明:

        { 'command': 'hello-world',
        'data': { '*message': 'str' } }

        对应的C函数代码:

        void qmp_hello_world(bool has_message, const char *message, Error **errp)
        {
            if (has_message)
            {
                error_setg_errno(errp, 2, "test error");
            }

            return;
        }

        注:error_setg_errno参数中的2,表示的是要返回linux中错误码2对应的涵义: No such file or directory

        执行命令:virsh qemu-agent-command live-centos7.2 --cmd '{"execute":"hello-world","arguments":{"message":"We love KVM"}}'

        结果:

        qga deal error 2

         

    4. 代码中添加注释

      到目前为止,离完整的声明命令“hello-world”还差一步,那就是注释。在qapi-schema.json文件中,有很多例子可以参考,下面是hello-world命令的完整声明。

      ##
      # @hello-world
      #
      # Print a client provided string to the standard output stream.
      #
      # @message: #optional string to be printed
      #
      # Returns: Nothing on success.
      #
      # Notes: if @message is not provided, the "Hello, world" string will
      # be printed instead
      #
      # Since: <next qemu stable release, eg. 1.0> 
      ##
      { 'command': 'hello-world', 'data': { '*message': 'str' } }

       

      注意:“Returns”这一行在无返回值或不处理错误的情况下可以省略。

    5. 创建有返回值的命令

      以hello-world为例,qapi-schema.json中命令声明格式为:

      { 'command': 'hello-world',
      'data': { '*message': 'str' },
      'returns':'int' }

      commands-posix.c中对应的C代码为:

      long qmp_hello_world(bool has_message, const char *message, Error **errp)
      {
          if (has_message)
          {
              return 1;
          }

          return 0;
      }

      注:qemu 2.3.0版本中json形式的int对应的是64位C语言long型

      不同形式命令对应的结果如下:

      • 命令1:

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world","arguments":{"message":"We love KVM"}}'

        结果:

        {"return":1}

        return-result-1

         

      • 命令2:

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world"}'

        结果:

        {"return":0}

        return-result-2

         

    6. 创建自定义类型

      自定义类型对应于C语言中的结构体,格式如下:

      { 'type': 'MyType',
      'data':{'member1':'mem1-type','member2':'mem2-type','*member3':'mem3-type'} }

      注:
      type :表示这是类型声明
      MyType :是类型名
      data :表示后面跟新数据结构的成员
      member1,member2,member3这些是成员名称,成员数量可根据需要增减,mem1-type,mem2-type,mem3-type分别表示各成员的类型。member3前的星号表示这个参数是可选的,可选参数需要放在末尾。

      新类型示例:

      { 'type': 'HelloType',
      'data': { 'm1': 'int', '*m2': 'str' } }

      { 'command': 'hello-world',
      'data': { 'newtype': 'HelloType' },
      'returns':'int' }

      commands-posix.c中对应的C代码为:

      long qmp_hello_world(HelloType *newtype, Error **errp)
      {
          if (newtype->has_m2)
          {
              return newtype->m1 + 1;
          }

          return newtype->m1;
      }

      不同形式命令对应的结果如下:

      • 命令1:

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world","arguments":{"newtype":{"m1":1,"m2":"We love KVM"}}}'

        结果:

        {"return":2}

        self-define-type-1

         

      • 命令2:

        virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world","arguments":{"newtype":{"m1":1}}}'

        结果2:

        {"return":1}

        self-define-type-2

         

    7. 创建返回列表的命令

      1. qga命令支持返回列表形式的数据

        在json文件中声明格式类似如下:

        { 'type': 'IntStruct',
        'data': { 'm1': 'int'} }

        { 'command': 'hello-world',
        'returns':['IntStruct'] }

        注:IntStruct是自定义的数据类型。以列表形式返回数据的关键是中括号[]。如果在返回的数据类型外部加了中括号[],就表示返回列表。

        对应的C代码:

        IntStructList *qmp_hello_world(Error **errp)
        {
            IntStructList *int_list = NULL;
            int i = 0;

            for(i = 0;i < 5;i++)
            {
                IntStructList *list_entry = g_malloc0(sizeof(*list_entry));
                list_entry->value = g_malloc0(sizeof(*list_entry->value));
                list_entry->value->m1 = (long)i;

                list_entry->next = int_list;
                int_list = list_entry;

            }

            return int_list;
        }

        注:g_malloc0()函数是glib库里面的。
        C函数返回类型名称是列表元素类型名后缀List,如列表元素类型为IntStruct时,返回的类型为IntStructList。

        • 测试命令:

          virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world"}'

          结果:

          {"return":[{"m1":4},{"m1":3},{"m1":2},{"m1":1},{"m1":0}]}

          ret-list-1

           

      2. 返回整数列表

        在json文件中声明格式如下:

        { 'command': 'hello-world',
        'returns':['int'] }

        对应C代码为

        intList *qmp_hello_world(Error **errp)
        {
            intList *int_list = NULL;
            int i = 0;

            for(i = 0;i < 5;i++)
            {
                intList *list_entry = g_malloc0(sizeof(*list_entry));
                list_entry->value = (long)i;

                list_entry->next = int_list;
                int_list = list_entry;
            }

            return int_list;
        }

        • 测试命令:

          virsh qemu-agent-command live-centos7.2 --cmd '{ "execute":"hello-world"}'

          结果:

          ret-list-2

           

      3. 返回字符串列表

        在json文件中声明如下:

        { 'command': 'hello-world',
        'returns':['str'] }

        对应C代码为:

        strList *qmp_hello_world(Error **errp)
        {
            strList *str_list = NULL;
            int i = 0;

            for(i = 0;i < 5;i++)
            {
                strList *list_entry = g_malloc0(sizeof(*list_entry));
                list_entry->value = g_malloc0(10);
                memset(list_entry->value, 0, 10);
                *(list_entry->value) = 'a' + i;
                list_entry->next = str_list;
                str_list = list_entry;
            }

            return str_list;
        }

        • 测试命令:

          virsh qemu-agent-command live-centos7.2 --cmd' { "execute":"hello-world"}'

          结果:

          ret-list-3

           

关于定制qga命令的方法就介绍完了

如果觉得本文不错,欢迎打赏