使用 Python 编写 KVM 脚本,第 2 部分: 添加 GUI 来使用 libvirt 和 Python 管理 KVM

由两部分组成的此系列文章将探索如何使用 Python 创建脚本,以使用 KVM 来管理虚拟机。在本期中,您将学习如何添加一个 GUI 来扩展简单的状态和显示工具。

Paul Ferrill, CTO, ATAC

Paul Ferrill 为计算机出版物撰写文章已有超过 20 年的时间了。他最初为 PC Magazine 撰写关于 LANtastic 和 Novell 的早期版本等产品的评论。Paul 拥有 BSEE 和 MSEE 学位,曾经为许多计算机平台和体系结构编写过软件。



2012 年 3 月 20 日

本系列的 使用 Python 为 KVM 编写脚本,第 1 部分:libvirt 介绍了使用 libvirt 和 Python 编写基于内核的虚拟机 (KVM) 脚本的基础知识。本期使用了上一期介绍的概念构建一些实用工具应用程序,添加一个图形用户界面 (GUI)。主要有两种既具有 Python 绑定又可跨平台的 GUI 工具包。第一个是 Qt,它现在归诺基亚所有;第二个是 wxPython。二者都具有大量支持者,许多开源项目都在使用它们。

出于个人偏好,我在本文重点介绍 wxPython。首先将简短介绍 wxPython 和正确设置的基本知识。再提供一些简短的示例程序,然后再与 libvirt 集成。此方法应该提供了足够的 wxPython 基础知识,供您构建简单的程序,以及扩展该程序来添加功能。希望您能掌握这些概念,并扩展它们以满足您的具体需要。

wxPython 基础知识

一个不错的起点是从一些基本的定义入手。wxPython 库实际上是基于 C++ 的 wxWidgets 上的一个包装器。在创建 GUI 的上下文中,一个部件 在本质上就是一个构建块。部件分层结构的最顶层包含 5 个独立的部件:

wx.Frame
wx.Dialog
wx.PopupWindow
wx.MDIParentFrame
wx.MDIChildFrame

这里的大部分示例都基于 wx.Frame,因为它在本质上实现了一个单一模态的窗口。

在 wxPython 中,您可以按原样实例化 Frame 类,或者继承它以添加或增强功能。一定要理解部件在一个框架中的显示方式,这样您在知道如何正确放置它们。布局通过绝对定位或使用调整器来确定。调整器 是一个方便的工具,在用户单击并拖动一边或一角来更改窗口大小时,它会调整部件大小。

wxPython 程序的最简单形式必须有一些代码行进行设置。一种典型的主要例程可能类似于 清单 1

清单 1. 设备 XML 定义
if __name__ == "__main__":
    app = wx.App(False)
	frame = MyFrame()
	frame.Show()
	app.MainLoop()

每个 wxPython 应用程序是 wx.App() 的一个实例,必须如清单 1 所示进行实例化。当将 False 传递到 wx.App 时,它表明 “不要将 stdout 和 stderr 重定向到一个窗口”。下一行通过实例化 MyFrame() 类来创建一个框架。接着显示框架并将控制权转交给 app.MainLoop()MyFrame() 类通常包含一个 __init__ 函数,以使用您选择的部件初始化框架。您还会在这里将任何部件事件连接到他们的正确处理函数。

现在有必要提一下 wxPython 随带的一个方便的调试工具。此工具称为部件检查工具(参见 图 1),仅需要两行代码即可使用。首先,您必须使用下述代码导入它:

import wx.lib.inspection

接着,要使用时,您只需调用 Show() 函数:

wx.lib.inspectin.InspectionTool().Show()

单击菜单工具栏上的 Events 图标会在激活事件时动态地显示事件。如果您不确定特定部件支持哪些事件,这是一种在事件发生时查看事件的真正快捷的方式。当应用程序正在运行时,它还会让您更好地了解幕后发生的情况。

图 1. wxPython 部件检查工具
该屏幕截图显示了 wxPython 部件检查工具,其中包含 PyCrust 输出中来自部件树框架和框架上并列显示的对象信息。

向命令行工具添加 GUI

本系列的 使用 Python 为 KVM 编写脚本,第 1 部分:libvirt 提供了一个简单工具来显示所有运行的虚拟机 (VM) 的状态。使用 wxPython,可以轻松地将该工具更改为 GUI 工具。wx.ListCtrl 部件提供了您以列表形式显示信息所需的功能。要使用 wx.ListCtrl 部件,您必须使用以下语法将它添加到您的框架中:

self.list=wx.ListCtrl(frame,id,style=wx.LC_REPORT|wx.SUNKEN_BORDER)

您可以从多种不同样式中选择,包括前面使用的 wx.LC_REPORTwx.SUNKEN_BORDER 选项。第一个选项将 wx.ListCtrl 设置为报告模式,这是四种可用模式之一。其他选项包括图标、小图标和列表。要添加 wx.SUNKEN_BORDER 这样的样式,您只需使用竖杠 (|)。一些样式是相互排斥的,比如不同的边框样式,所以如果您有任何疑虑,请查阅 wxPython wiki(参见 参考资料)。

实例化 wx.ListCtrl 部件之后,您就可以开始向它添加内容了,比如列标题。InsertColumn 方法有两个强制性参数和两个可选参数。第一个是列索引,它从 0 开始,接下来是一个设置标题的字符串。第三个用于格式化,应该类似于 LIST_FORMAT_CENTER_LEFT_RIGHT。最后,您可以传入一个整数来设置固定宽度,或者使用 wx.LIST_AUTOSIZE 自动调整列。

现在您已配置了 wx.ListCtrl 部件,您可以使用 InsertStringItemSetStringItem 方法向它填充数据了。wx.ListCtrl 部件中的每一个新行都必须使用 InsertStringItem 方法添加。两个强制性参数指定在何处执行插入,包含表示在列表顶部插入的值 0 和要插入在该位置的字符串。InsertStringItem 返回一个整数,表示插入字符串的行数。您可以为列表调用 GetItemCount(),使用返回值供索引附加到底部,如 清单 2 所示。

清单 2. 命令行工具的 GUI 版本
import wx
import libvirt

conn=libvirt.open("qemu:///system")

class MyApp(wx.App):
    def OnInit(self):
        frame = wx.Frame(None, -1, "KVM Info")
        id=wx.NewId()
        self.list=wx.ListCtrl(frame,id,style=wx.LC_REPORT|wx.SUNKEN_BORDER)
        self.list.Show(True)
        self.list.InsertColumn(0,"ID")
        self.list.InsertColumn(1,"Name")
        self.list.InsertColumn(2,"State")
        self.list.InsertColumn(3,"Max Mem")
        self.list.InsertColumn(4,"# of vCPUs")
        self.list.InsertColumn(5,"CPU Time (ns)")

        for i,id in enumerate(conn.listDomainsID()):
            dom = conn.lookupByID(id)
            infos = dom.info()
            pos = self.list.InsertStringItem(i,str(id)) 
            self.list.SetStringItem(pos,1,dom.name())
            self.list.SetStringItem(pos,2,str(infos[0]))
            self.list.SetStringItem(pos,3,str(infos[1]))
            self.list.SetStringItem(pos,4,str(infos[3]))
            self.list.SetStringItem(pos,5,str(infos[2]))
            
        frame.Show(True)
        self.SetTopWindow(frame)
        return True

app = MyApp(0)
app.MainLoop()

图 2 显示了这些工作的结果。

图 2. GUI KVM 信息工具
该屏幕截图显示了 KVM 信息,包括 ID、Name、State、Max Mem、# of vCPUs 和 CPU Time 列

您可以改善这个表的外观。一种明显的改进可能是重新调整列。为此,可以将 width = 参数添加到 InsertColumn 调用中,或者使用一行代码,比如:

self.ListCtrl.SetColumnWidth(column,wx.LIST_AUTOSIZE)

您可以做的另一件事是添加一个调整器,以便控件能根据父窗口进行调整。为此,可以在几行代码中使用一个 wxBoxSizer。首先,创建调整器,然后向它添加您希望针对主窗口进行调整的部件。可能的代码如下所示:

self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.list, proportion=1,flag=wx.EXPAND | wx.ALL, border=5)
self.sizer.Add(self.button, flag=wx.EXPAND | wx.ALL, border=5)
self.panel.SetSizerAndFit(self.sizer)

最后对 self.panel.SetSizerAndFit() 的调用要求 wxPython 基于嵌入部件调整器的最小尺寸,设置窗格的初始大小。这有助于基于屏幕内容为您提供大小合理的初始屏幕。


基于用户操作的控制流

关于 wx.ListCtrl 部件的一个好处是,您可以检测用户何时单击了部件的具体部分,并基于该信息执行某种操作。此功能允许您基于用户对列标题的单击,按字母顺序对列进行正向或反向排列。完成此任务的技术使用了一种回调机制。您必须提供一个函数,通过将部件与处理方法绑定在一起来处理您希望处理的每个操作。为此,使用 Bind 方法。

每个部件有一定数量关联事件。还有与鼠标等实体关联的事件。鼠标事件具有 EVT_LEFT_DOWNEVT_LEFT_UPEVT_LEFT_DCLICK 这样的名称,以及与其他按钮相同的命名约定。您可以通过附加到 EVT_MOUSE_EVENTS 类型来处理所有鼠标事件。难点在于在您感兴趣的应用程序或窗口上下文中捕获事件。

当控件传递到事件处理函数时,它必须执行必要的步骤来处理该操作,然后将控件返回之前所在的地方。这是一种事件驱动的编程模型,每个 GUI 都必须实现它来及时地处理用户操作。许多现代 GUI 应用程序实现了多线程来避免让用户感觉程序没有响应。本文后面将简短介绍这一主题。

计时器代表着程序可能必须处理的另一种事件类型。例如,您可能希望以用户定义的间隔执行定期监视功能。您将需要提供一个屏幕,用户可在该屏幕上指定间隔,接着启动一个计时器以在它到期时触发一个事件。计时器到期会触发一个事件,您可以使用该事件激活一段代码。再次依据用户偏好,您可能需要设置或重新开始计时。可以轻松地使用此技术开发 VM 监视工具。

清单 3 提供了一个简单的演示应用程序,它包含一个按钮和静态文本行。使用 wx.StaticText 是一种将字符串输出到窗口的轻松方式。它的理念是单击该按钮一次会启动一个计时器并记录开始时间,同时将标签更改为 Stop。再次单击该按钮会填入结束时间文本框,将按钮更改回 Start

清单 3. 包含一个按钮和静态文本的简单应用程序
import wx
from time import gmtime, strftime

class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Buttons")
        self.panel = wx.Panel(self, wx.ID_ANY)
 
        self.button = wx.Button(self.panel, id=wx.ID_ANY, label="Start")
        self.button.Bind(wx.EVT_BUTTON, self.onButton)

    def onButton(self, event):
        if self.button.GetLabel() == "Start":
            self.button.SetLabel("Stop")
            strtime = strftime("%Y-%m-%d %H:%M:%S", gmtime())
            wx.StaticText(self, -1, 'Start Time = ' + strtime, (25, 75))
        else:
            self.button.SetLabel("Start")
            stptime = strftime("%Y-%m-%d %H:%M:%S", gmtime())
            wx.StaticText(self, -1, 'Stop Time = ' + stptime, (25, 100))
 
if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm()
    frame.Show()
    app.MainLoop()

增强的监视 GUI

现在,您可以添加前面介绍的简单监视 GUI 了。在拥有创建应用程序所需的一切内容之前,还需要理解 wxPython 的另一个方面。向 wx.ListCtrl 部件的第一行添加一个复选框,可实现基于复选框的状态在多行上执行操作。为此,您可以使用 wxPython 所称的 mixin。在本质上,mixin 是一个帮助器类,它向父部件添加某种类型的功能。要添加复选框 mixin,只需使用以下代码来实例化它:

listmix.CheckListCtrlMixin.__init__(self)

也可以利用事件来添加单击列标题来选择或清除所有复选框的功能。这样,只需几次单击即可执行启动或停止所有 VM 等操作。您需要编写一些事件处理函数,以与之前更改按钮标签相同的方式来响应合适的事件。以下是设置列单击事件处理函数所需的代码行:

self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick, self.list)

wx.EVT_LIST_COL_CLICK 在单击任何列标题时触发。要确定单击了哪一列,可以使用 event.GetColumn() 方法。以下是 OnColClick 事件的一个简单的处理函数:

def OnColClick(self, event):
    print "column clicked %d\n" % event.GetColumn()
	event.Skip()

如果需要将事件传播给其他处理函数,event.Skip() 调用非常重要。尽管在此实例中可能不需要它,但当多个处理函数需要处理相同事件时,不使用它可能会出现问题。wxPython wiki 站点上对事件传播进行了很好的讨论,这比我在这里的介绍详细得多。

最后,向两个按钮处理函数添加代码来启动或停止所有选择的 VM。只需几行代码,即可迭代 wx.ListCtrl 中的各行并获取 VM ID,如 清单 4 所示。

清单 4. 启动和停止选择的 VM
#!/usr/bin/env python

import wx
import wx.lib.mixins.listctrl as listmix
import libvirt

conn=libvirt.open("qemu:///system")

class CheckListCtrl(wx.ListCtrl, listmix.CheckListCtrlMixin, 
                                 listmix.ListCtrlAutoWidthMixin):
    def __init__(self, *args, **kwargs):
        wx.ListCtrl.__init__(self, *args, **kwargs)
        listmix.CheckListCtrlMixin.__init__(self)
        listmix.ListCtrlAutoWidthMixin.__init__(self)
        self.setResizeColumn(2)

class MainWindow(wx.Frame):

    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)
        self.panel = wx.Panel(self)
        self.list = CheckListCtrl(self.panel, style=wx.LC_REPORT)
        self.list.InsertColumn(0, "Check", width = 175)
        self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClick, self.list)

        self.list.InsertColumn(1,"Max Mem", width = 100)
        self.list.InsertColumn(2,"# of vCPUs", width = 100)

        for i,id in enumerate(conn.listDefinedDomains()):
            dom = conn.lookupByName(id)
            infos = dom.info()
            pos = self.list.InsertStringItem(1,dom.name()) 
            self.list.SetStringItem(pos,1,str(infos[1]))
            self.list.SetStringItem(pos,2,str(infos[3]))

        self.StrButton = wx.Button(self.panel, label="Start")
        self.Bind(wx.EVT_BUTTON, self.onStrButton, self.StrButton)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.list, proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
        self.sizer.Add(self.StrButton, flag=wx.EXPAND | wx.ALL, border=5)
        self.panel.SetSizerAndFit(self.sizer)
        self.Show()

    def onStrButton(self, event):
        if self.StrButton.GetLabel() == "Start":
	    num = self.list.GetItemCount()
            for i in range(num):
                if self.list.IsChecked(i):
                    dom = conn.lookupByName(self.list.GetItem(i, 0).Text)
                    dom.create()
                    print "%d started" % dom.ID()
 
    def OnColClick(self, event):
         item = self.list.GetColumn(0)
         if item is not None:
             if item.GetText() == "Check":
                 item.SetText("Uncheck")
                 self.list.SetColumn(0, item)
                 num = self.list.GetItemCount()
                 for i in range(num):
                     self.list.CheckItem(i,True)
             else:
                 item.SetText("Check")
                 self.list.SetColumn(0, item)
                 num = self.list.GetItemCount()
                 for i in range(num):
                     self.list.CheckItem(i,False)

         event.Skip()
 
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()

在 KVM 中的 VM 状态方面,这里有两个亮点需要指出:当使用 libvirt 中的 listDomainsID() 方法时,正在运行的 VM 会显示出来。要查看没有运行的机器,必须使用 listDefinedDomains()。必须保持这两部分独立,才能知道可以启动哪些 VM 和可以停止哪些 VM。


结束语

本文主要介绍了使用 wxPython 构建 GUI 包装器所需的步骤,该包装器使用 libvirt 来管理 KVM。wxPython 库功能丰富,提供了许多部件来支持构建具有专业外观的基于 GUI 的应用程序。本文仅介绍其中的一小部分功能,希望您能够进一步探索。务必查阅更多 参考资料,以有助于应用程序正常运行。

参考资料

学习

获得产品和技术

  • IBM 产品评估试用版软件:从试用版下载到在云托管产品,使用特别为开发人员推荐的软件改革您的下一个开源开发项目。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Linux
ArticleID=806304
ArticleTitle=使用 Python 编写 KVM 脚本,第 2 部分: 添加 GUI 来使用 libvirt 和 Python 管理 KVM
publish-date=03202012