简介: 由两部分组成的此系列文章将探索如何使用 Python 创建脚本,以使用 KVM 来管理虚拟机。在本期中,您将学习如何添加一个 GUI 来扩展简单的状态和显示工具。
本系列的 使用 Python 为 KVM 编写脚本,第 1 部分:libvirt 介绍了使用 libvirt
和 Python 编写基于内核的虚拟机 (KVM) 脚本的基础知识。本期使用了上一期介绍的概念构建一些实用工具应用程序,添加一个图形用户界面 (GUI)。主要有两种既具有 Python 绑定又可跨平台的 GUI 工具包。第一个是 Qt,它现在归诺基亚所有;第二个是 wxPython。二者都具有大量支持者,许多开源项目都在使用它们。
出于个人偏好,我在本文重点介绍 wxPython。首先将简短介绍 wxPython 和正确设置的基本知识。再提供一些简短的示例程序,然后再与 libvirt
集成。此方法应该提供了足够的 wxPython 基础知识,供您构建简单的程序,以及扩展该程序来添加功能。希望您能掌握这些概念,并扩展它们以满足您的具体需要。
一个不错的起点是从一些基本的定义入手。wxPython 库实际上是基于 C++
的 wxWidgets 上的一个包装器。在创建 GUI 的上下文中,一个部件 在本质上就是一个构建块。部件分层结构的最顶层包含 5 个独立的部件:
wx.Frame
、
wx.Dialog
、
wx.PopupWindow
、
wx.MDIParentFrame
和
wx.MDIChildFrame
。
这里的大部分示例都基于 wx.Frame
,因为它在本质上实现了一个单一模态的窗口。
在 wxPython 中,您可以按原样实例化 Frame
类,或者继承它以添加或增强功能。一定要理解部件在一个框架中的显示方式,这样您在知道如何正确放置它们。布局通过绝对定位或使用调整器来确定。调整器 是一个方便的工具,在用户单击并拖动一边或一角来更改窗口大小时,它会调整部件大小。
wxPython 程序的最简单形式必须有一些代码行进行设置。一种典型的主要例程可能类似于 清单 1。
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 图标会在激活事件时动态地显示事件。如果您不确定特定部件支持哪些事件,这是一种在事件发生时查看事件的真正快捷的方式。当应用程序正在运行时,它还会让您更好地了解幕后发生的情况。
本系列的 使用 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_REPORT
和 wx.SUNKEN_BORDER
选项。第一个选项将 wx.ListCtrl
设置为报告模式,这是四种可用模式之一。其他选项包括图标、小图标和列表。要添加 wx.SUNKEN_BORDER
这样的样式,您只需使用竖杠 (|
)。一些样式是相互排斥的,比如不同的边框样式,所以如果您有任何疑虑,请查阅 wxPython wiki(参见 参考资料)。
实例化 wx.ListCtrl
部件之后,您就可以开始向它添加内容了,比如列标题。InsertColumn
方法有两个强制性参数和两个可选参数。第一个是列索引,它从 0 开始,接下来是一个设置标题的字符串。第三个用于格式化,应该类似于 LIST_FORMAT_CENTER
、_LEFT
或 _RIGHT
。最后,您可以传入一个整数来设置固定宽度,或者使用 wx.LIST_AUTOSIZE
自动调整列。
现在您已配置了 wx.ListCtrl
部件,您可以使用 InsertStringItem
和 SetStringItem
方法向它填充数据了。wx.ListCtrl
部件中的每一个新行都必须使用 InsertStringItem
方法添加。两个强制性参数指定在何处执行插入,包含表示在列表顶部插入的值 0 和要插入在该位置的字符串。InsertStringItem
返回一个整数,表示插入字符串的行数。您可以为列表调用 GetItemCount()
,使用返回值供索引附加到底部,如 清单 2 所示。
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 显示了这些工作的结果。
您可以改善这个表的外观。一种明显的改进可能是重新调整列。为此,可以将 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_DOWN
、EVT_LEFT_UP
和 EVT_LEFT_DCLICK
这样的名称,以及与其他按钮相同的命名约定。您可以通过附加到 EVT_MOUSE_EVENTS
类型来处理所有鼠标事件。难点在于在您感兴趣的应用程序或窗口上下文中捕获事件。
当控件传递到事件处理函数时,它必须执行必要的步骤来处理该操作,然后将控件返回之前所在的地方。这是一种事件驱动的编程模型,每个 GUI 都必须实现它来及时地处理用户操作。许多现代 GUI 应用程序实现了多线程来避免让用户感觉程序没有响应。本文后面将简短介绍这一主题。
计时器代表着程序可能必须处理的另一种事件类型。例如,您可能希望以用户定义的间隔执行定期监视功能。您将需要提供一个屏幕,用户可在该屏幕上指定间隔,接着启动一个计时器以在它到期时触发一个事件。计时器到期会触发一个事件,您可以使用该事件激活一段代码。再次依据用户偏好,您可能需要设置或重新开始计时。可以轻松地使用此技术开发 VM 监视工具。
清单 3 提供了一个简单的演示应用程序,它包含一个按钮和静态文本行。使用 wx.StaticText
是一种将字符串输出到窗口的轻松方式。它的理念是单击该按钮一次会启动一个计时器并记录开始时间,同时将标签更改为 Stop。再次单击该按钮会填入结束时间文本框,将按钮更改回 Start。
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 了。在拥有创建应用程序所需的一切内容之前,还需要理解 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 所示。
#!/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 的应用程序。本文仅介绍其中的一小部分功能,希望您能够进一步探索。务必查阅更多 参考资料,以有助于应用程序正常运行。
学习
libvirt
网站:查看整个网站,以获取更多相关信息。
- Reference Manual for
libvirt
:访问完整的libvirt
API 参考手册。
- Python.org:从官方网站查找您所需的 Python 资源。
- wxPython.org:获取有关 wxPython 的更多信息。
- wxPython wiki:通过这里的许多教程扩展您的知识面。
- developerWorks 播客:收听面向软件开发人员的有趣访谈和讨论。
- developerWorks 演示中心:观看免费演示,学习 IBM 与开源技术及产品功能。
- 随时关注 developerWorks 技术活动和网络广播。
- 访问 developerWorks Open source 专区获得丰富的 how-to 信息、工具和项目更新以及最受欢迎的文章和教程,帮助您用开放源码技术进行开发,并将它们与 IBM 产品结合使用。
http://www.ibm.com/developerworks/cn/opensource/os-python-kvm-scripting2/