Python で KVM 用のスクリプトを作成する: 第 2 回 KVM を管理するための GUI を libvirt と Python を使用して追加する

この連載の第 1 回では、libvirt と Python を使用して KVM (Kernel-based Virtual Machine) 用のスクリプトを作成するための基本について説明しました。今回の記事では、第 1 回で説明した概念を使用してユーティリティー・アプリケーションをいくつか作成し、それらに GUI (Graphical User Interface) を追加します。クロスプラットフォームの GUI ツールキットのなかで Python バインディングを持つものとしては、主に次の 2 つがあります。1 つは現在 Nokia の一部門が開発している Qt であり、もう 1 つは wxPython です。どちらも強力なユーザー・ベースがあり、それぞれのユーザーによって数多くのオープンソース・プロジェクトが進められています。

この記事では単なる私の好みから、wxPython に焦点を絞ります。最初に、wxPython について簡単に紹介し、wxPython を使用するための適切な基本設定について説明します。次に、いくつかの簡単なサンプル・プログラムを作成し、そのなかで libvirt の機能を使用します。この手順で wxPython の基本事項を紹介することで、単純なプログラムを作成できるようにし、さらにはそのプログラムを拡張して機能を追加できるようにします。この記事を読むことで、これらの概念を理解し、それを基に自分の必要を満たすプログラムを構築できるようになってくれれば幸いです。

wxPython の基本

まず、wxPython に関する基本的な定義をいくつか説明することから始めましょう。実際には、wxPython ライブラリーは C++ ベースの wxWidgets のラッパーです。GUI を作成するという意味では、「ウィジェット」は基本的にビルディング・ブロックです。wxWidgets の階層構造の最上位レベルには、以下の 5 つの独立したウィジェットがあります。

wx.Frame,
wx.Dialog,
wx.PopupWindow,
wx.MDIParentFrame, and
wx.MDIChildFrame.

この記事で紹介するサンプルの大半は wx.Frame をベースにしています。これは wx.Frame が基本的に 1 つのモーダル・ウィンドウを実装しているからです。

wxPython での Frame クラスは、そのままの状態でインスタンス化して使用するか、機能を追加または強化するために継承して使用するクラスです。ウィジェットを適切に配置するためには、ウィジェットがフレーム内にどう表示されるかを理解することが重要です。レイアウトは、絶対位置指定によって決定するか、サイザー (sizer) を使用して決定するかのいずれかです。「サイザー」はウィジェットをリサイズするための便利なツールで、サイザーを使用すれば、ユーザーはウィンドウの枠またはコーナーをクリックしてドラッグすることで、ウィンドウのサイズを変更できるようになります。

wxPython プログラムは最も単純な形式にする場合でも、設定のために数行のコードが必要です。典型的なメイン・ルーチンはリスト 1 のようなものです。

リスト 1. wxPython プログラムのメイン・ルーチン

if __name__ == "__main__":    app = wx.App(False)        frame = MyFrame()        frame.Show()        app.MainLoop()

すべての wxPython アプリケーションは wx.App() のインスタンスであり、リスト 1 のようにインスタンス化する必要があります。wx.AppFalse を渡すと、「stdout と stderr をウィンドウにリダイレクトしない」という意味になります。その次の行は MyFrame() クラスをインスタンス化することによってフレームを作成します。次にそのフレームを表示し、app.MainLoop() に制御を渡します。MyFrame() クラスは通常、選択されたウィジェットによってフレームを初期化する __init__ 関数を含んでいます。また MyFrame() クラスは任意のウィジェット・イベントを適切なハンドラーに接続するための場所でもあります。

ここで、wxPython に付属している便利なデバッグ・ツールを紹介しておきましょう。このデバッグ・ツールは「Widget Inspection Tool (ウィジェット検査ツール)」(図 1) と呼ばれ、このツールを使用するために必要なコードは 2 行のみです。まず、このツールを以下のコードによってインポートする必要があります。

import wx.lib.inspection

次に、このツールを使用するためには単純に Show() 関数を呼び出します。

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

メニュー・ツールバーの「Events (イベント)」アイコンをクリックすると、イベントが起動されるごとに、そのイベントが動的に表示されます。ある特定のウィジェットがどのイベントをサポートしているのかわからない場合、この方法はイベントを調べる実に素晴らしい方法となります。またこの方法により、アプリケーションの実行中にどれだけのことが背後で起きているのかをよりよく理解することができます。

図 1. wxPython の「Widget Inspection Tool (ウィジェット検査ツール)」
wxPython の「Widget Inspection Tool (ウィジェット検査ツール)」のスクリーンショットが表示されており、PyCrust 出力のフレームの上に「Widget Tree (ウィジェット・ツリー)」と「Object Info (オブジェクト情報)」のフレームが並べて表示されています。


コマンドライン・ツールに GUI を追加する

この連載の第 1 回で、実行中のドメイン (つまり仮想マシン (VM)) すべてのステータスを表示する単純なツールを紹介しました。wxPython を使用すると、そのツールを簡単に GUI ツールに変更することができます。wx.ListCtrl ウィジェットは、まさに表形式で情報を表示するために必要な機能を提供します。wx.ListCtrl ウィジェットを使用するためには、以下の構文を使用してフレームに wx.ListCtrl ウィジェットを追加する必要があります。

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

上記で使用した wx.LC_REPORTwx.SUNKEN_BORDER を含め、いくつかの異なる選択肢の中からスタイルを選択することができます。wx.LC_REPORT を選択すると、wx.ListCtrl は 4 つのモードのうちの 1 つ、「Report (レポート)」モードになります。他のモードには、「Icon (アイコン)」、「Small Icon (小アイコン)」、「List (リスト)」があります。wx.SUNKEN_BORDER のようなスタイルを追加するためには、単純にパイプ文字 (|) を使用します。一部のスタイルは同時に使用することができません (例えば、複数の異なるボーダー・スタイルを指定するなど)。よくわからない場合には wxPython のウィキを調べてください (「参考文献」を参照)。

wx.ListCtrl ウィジェットをインスタンス化すると、列見出しなど、さまざまな要素をこのウィジェットに追加することができます。InsertColumn メソッドには、必須の引数とオプションの引数が 2 つずつあります。1 つ目の引数は、列のインデックスであり、ゼロから始まります。2 つ目は、見出しとして設定されるストリングです。3 つ目は書式設定であり、LIST_FORMAT_CENTERLIST_FORMAT_LEFTLIST_FORMAT_RIGHT のような値を取ります。最後の引数は、整数を渡すとその値に列幅が固定され、wx.LIST_AUTOSIZE を渡すことで列幅を自動調整することもできます。

これで wx.ListCtrl ウィジェットを構成できたので、InsertStringItem メソッドと SetStringItem メソッドを使用して wx.ListCtrl ウィジェットにデータを追加することができます。wx.ListCtrl ウィジェットに新しい行を追加するためには InsertStringItem メソッドを使用する必要があります。2 つの必須パラメーターにより、挿入する場所 (値 0 はリストの先頭を示します) と、その場所に挿入するストリングを指定します。InsertStringItem は挿入されたストリングの行番号を示す整数を返します。これらを利用したコードをリスト 2 に示します。リストの末尾に追加する行のインデックスを得るために、GetItemCount() を呼び出し、その戻り値を使用するようにすることもできます。

リスト 2. コマンドライン・ツールの GUI 版

import wximport libvirtconn=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 Trueapp = MyApp(0)app.MainLoop()

図 2 は、これらの作業の結果を示しています。

図 2. GUI の「KVM Info (KVM 情報)」ツール
「KVM Info (KVM 情報)」ツールのスクリーンショットが表示されており、「ID」、「Name (名前)」、「State (状態)」、「Max Mem (最大メモリー)」、「# of vCPUs (仮想 CPU の番号)、「CPU Time (CPU 時間)」の列が表示されています。

この表の外観は改善することができます。わかりやすい改善は、列の幅を変更することです。そのためには、InsertColumn の呼び出しに width = パラメーターを追加するか、あるいは以下のような 1 行のコードを使用します。

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 ウィジェットの便利な機能の 1 つは、ユーザーがウィジェットの特定の部分をクリックしたことを検出し、それに基づいて何らかのアクションを実行できることです。この機能を利用すると、ユーザーが列の見出しをクリックするとアルファベットの昇順または降順に列がソートされる、といった機能を実現することができます。この機能を実現するための手法では、コールバック・メカニズムを使用します。また、ウィジェットと処理メソッドとをバインドすることで対象の各アクションを処理する関数を提供する必要があります。そのためには Bind メソッドを使用します。

すべてのウィジェットには、いくつかのイベントが関連付けられています。また、マウスなどの動作に関連付けられたイベントもあります。マウス・イベントには EVT_LEFT_DOWNEVT_LEFT_UPEVT_LEFT_DCLICK といった名前が付けられており、他のボタンにもこれと同様の命名規則が使われます。マウス・イベントを処理するには、それらのイベントを EVT_MOUSE_EVENTS タイプに追加します。そして重要なのは、関心対象のアプリケーションまたはウィンドウのコンテキストでイベントをキャッチすることです。

イベント・ハンドラーに制御が渡されると、イベント・ハンドラーはそのアクションの処理に必要なステップを実行し、実行後は制御を元に戻す必要があります。これはイベント駆動のプログラミング・モデルであり、すべての GUI はユーザー・アクションを適時に処理するために、このプログラミング・モデルを実装する必要があります。プログラムが応答していない、とユーザーに思われないように、最近の多くの GUI アプリケーションはマルチスレッドを実装しています。その点についてはこの記事の後の方で簡単に説明します。

プログラムで扱う必要のある別のタイプのイベントの候補として、タイマーがあります。例えば、ユーザーが定義した時間間隔で周期的にモニタリング関数を実行したい場合があります。その場合には、ユーザーが時間間隔を指定できる画面を表示し、ユーザーが時間を設定したら、その時間が経過したときにイベントが発生するようなタイマーを起動する必要があります。タイマーがタイムアウトしたらイベントを発生させ、それによってコードのあるセクションをアクティブにすることができます。これもユーザーの好みによりますが、タイマーを設定したり再度開始したりする必要があるかもしれません。この手法を使用すると、VM をモニタリングするツールを容易に作成することができます。

リスト 3 は 1 つのボタンと静的なテキスト行を持つ簡単なデモ・アプリケーションを示しています。wx.StaticText を使用すると、ウィンドウに容易にストリングを表示することができます。リスト 3 で行っている処理は次のようなものです。「Start (開始)」ボタンをクリックすると、タイマーが開始されて「Start Time (開始時刻)」が記録され、ボタンのラベルが「Stop (停止)」に変更されます。再度ボタンをクリックすると「Stop Time (停止時刻)」のテキスト・ボックスに値が追加され、ボタンのラベルが「Start (開始)」に戻ります。

リスト 3. ボタンと静的なテキストを持つ簡単なアプリケーション

import wxfrom time import gmtime, strftimeclass 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 について理解しておかなければならないことがもう 1 つあります。wx.ListCtrl ウィジェットの最初の列にチェック・ボックスを追加すると、そのチェック・ボックスの状態に応じて複数の行に対してアクションを実行できるようになります。そのためには、wxPython で言うところの mixin を使います。簡単に言えば、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 %dn" % event.GetColumn()        event.Skip()

他のハンドラーにイベントを伝播させる必要がある場合、event.Skip() を呼び出すことが重要です。この記事の例ではイベントを伝播させる必要性があまりよくわからないかもしれませんが、複数のハンドラーが同じイベントを処理する必要がある場合、イベントの伝播が問題になることがあります。wxPython のウィキのサイトにはイベントの伝播について詳細に説明されており、この記事ではスペースの関係で説明できないような内容も説明されています。

最後に、チェックを入れた VM すべてを起動または停止するためのコードを 2 つのボタン・ハンドラーに追加します。リスト 4 に示すように、数行のコードのみで wx.ListCtrl の複数行に繰り返し処理を行い、VM の ID を取得することができます。

リスト 4. チェックを入れた VM を起動、停止する

#!/usr/bin/env pythonimport wximport wx.lib.mixins.listctrl as listmiximport libvirtconn=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 の状態に関して重要なメソッドが 2 つあります。実行中の VM を表示するには libvirtlistDomainsID() メソッドを使用します。一方、実行されていない VM を表示するためには libvirt の listDefinedDomains() メソッドを使用する必要があります。どの VM を起動でき、どの VM を停止できるのかを判断できるように、この 2 つを区別して理解しておく必要があります。


まとめ

この記事では主に、wxPython を使用して GUI ラッパーを作成し、そのラッパーによって libvirt を利用して KVM を管理できるようにするために必要なステップに焦点を絞って説明しました。wxPython ライブラリーは非常に機能が豊富であり、プロが作成したような外観の GUI アプリケーションを作成できる多種多様なウィジェットを提供しています。この記事では表面的な機能のみを紹介しましたが、これがきっかけで wxPython ライブラリーについてさらに詳細に調べてみようという気になってくれれば幸いです。ぜひとも「参考文献」をチェックし、皆さんのアプリケーションを実行する上で役立ててください。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中