Создание KVM-сценариев на языке Python: Часть 2. Добавление графического интерфейса пользователя для управления KVM с применением libvirt и Python

Этот цикл из двух статей посвящен тому, как использовать язык Python для создания сценариев управления виртуальными машинами с помощью технологии Kernel-based Virtual Machine (KVM). В этой части мы покажем, как добавить графический интерфейс пользователя (GUI) для расширения простого инструмента определения и отображения состояния виртуальных машин.

Пол Феррилл, технический директор, ATAC

Пол Феррилл (Paul Ferrill) уже более 20 лет пишет статьи, посвященные различным аспектам информационных технологий. Пол начинал с написания обзоров сетевых продуктов, таких как LANtastic и ранние версии Novell Netware, для журнала PC Magazine. Пол обладает степенями магистра и бакалавра в области электротехники и имеет опыт разработки ПО для самых разнообразных платформ и архитектур.



22.06.2012

В части 1 этой серии статей мы рассмотрели основы написания сценариев Kernel-based Virtual Machine (KVM) с использованием библиотеки libvirt и языка Python. В этой статье рассмотренные идеи используются для создания нескольких полезных приложений и добавления графического интерфейса пользователя (GUI). Существует два основных варианта кроссплатформенного инструментария GUI с привязками Python. Во-первых, это Qt, который в настоящее время принадлежит компании Nokia; и во-вторых — wxPython. Оба имеют большое количество применений и используются во многих проектах по разработке ПО с открытым исходным кодом.

В этой статье я сосредоточусь на wxPython, скорее из личных предпочтений, чем по каким-то другими причинам. Я начну с краткого введения в wxPython и основ его правильной установки. Затем приведу несколько примеров коротких программ и их интеграции с libvirt. Этот подход позволит вам в достаточной мере освоить основы wxPython, чтобы создать простую программу, а затем добавлять к ней новые функции. Надеюсь, что вы освоите эти идеи и будете опираться на них в своей работе.

Основы wxPython

Начнем с нескольких базовых определений. Библиотека wxPython, по существу, представляет собой оболочку поверх wxWidget'ов на базе C++. В контексте создания GUI виджеты служат строительными блоками. Верхний уровень иерархии образуют пять независимых виджетов:

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

Большинство приведенных здесь примеров основано на wx.Frame, поскольку он реализует единое модальное окно.

Frame в wxPython представляет собой класс, экземпляр которого создается или наследуется для расширения или усиления функциональности. Чтобы правильно разместить виджеты, важно понимать, как они отображаются в фрейме. Макет определяется либо путем абсолютного позиционирования, либо с использованием сайзеров. Сайзер (sizer) - это удобный инструмент, который изменяет размер виджета, когда пользователь изменяет размер окна, потянув его за боковую сторону или за угол.

Простейший вариант программы wxPython содержит несколько строк кода. Типичная базовая процедура может выглядеть как в листинге 1.

Листинг 1. XML-определение устройства
if __name__ == "__main__":
    app = wx.App(False)
	frame = MyFrame()
	frame.Show()
	app.MainLoop()

Каждое приложение wxPython является экземпляром wx.App() и должно создаваться, как показано в листинге 1. Когда wx.App передается значение False, это означает "не перенаправлять stdout и stderr в окно". Следующая строка создает фрейм путем создания экземпляра класса MyFrame(). Затем фрейм отображается, и управление передается app.MainLoop(). Класс MyFrame() обычно содержит функцию __init__ для инициализации фрейма с нужным виджетом. Здесь же любые события виджета связываются с соответствующими обработчиками.

Тут, вероятно, следует упомянуть об удобном инструменте отладки, который поставляется с wxPython. Он называется widget inspection tool (см. рисунок 1), и для его использования требуется всего две строки кода. Во-первых, его нужно импортировать:

import wx.lib.inspection

Затем, чтобы воспользоваться инструментом, достаточно просто вызвать функцию Show():

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

Щелчок на значке Events на панели инструментов динамически отображает события по мере их наступления. Это хороший способ просмотра происходящих событий, когда вы не уверены, какие именно события поддерживает виджет. Кроме того, он позволяет лучше понять, что происходит за кулисами при выполнении приложения.

Рисунок 1. Инспектор виджетов wxPython
Инспектор виджетов wxPython

Добавление GUI к инструменту командной строки

Первая статья этого цикла содержит описание простого инструмента для отображения состояния всех работающих виртуальных машин (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 в режим Report, один из четырех доступных режимов. Другие режимы: Icon, Small Icon и List. Чтобы добавить стили, такие как wx.SUNKEN_BORDER, достаточно воспользоваться символом вертикальной черты (|). Некоторые стили являются взаимно исключающими, такие как стили различных границ, так что если у вас возникают какие-либо сомнения, обращайтесь к вики по wxPython (см. раздел Ресурсы).

После создания экземпляра виджета wx.ListCtrl можно начать дополнять его, например, добавив заголовки столбцов. Метод InsertColumn имеет два обязательных и два факультативных параметра. Первый – это индекс столбца, который начинается с нуля; за ним следует строка заголовка. Третий параметр предназначен для форматирования и должен быть чем-то вроде LIST_FORMAT_CENTER, _LEFT или _RIGHT. Наконец, можно задать фиксированную ширину, передав целое значение, или сделать размер столбца определяемым автоматически, воспользовавшись функцией wx.LIST_AUTOSIZE.

Теперь, когда виджет wx.ListCtrl настроен, можно использовать методы InsertStringItem и SetStringItem для заполнения его данными. Каждая новая строка виджета 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
GUI-инструмент с информацией KVM

Внешний вид этой таблицы можно улучшить. Заметное улучшение даст изменение размера столбцов. Это можно сделать, либо добавив параметр 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.

Листинг 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)

Также можно воспользоваться событиями для добавления возможности установить или снять все флажки, щелкнув на заголовке столбца. Это упрощает такие вещи, как запуск или останов всех виртуальных машин с помощью нескольких щелчков. Точно так же, как мы изменяли надпись на кнопке, напишем обработчики событий, чтобы реагировать на соответствующие события. Вот строка кода, необходимая для создания обработчика события щелчка на заголовке столбца:

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 есть полезная информация по распространению событий, гораздо более подробная, чем то, что я излагаю здесь.

Наконец, добавим код обработчиков для двух кнопок запуска и останова всех контролируемых 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()

Здесь надо отметить две вещи, касающиеся состояния VM в KVM: работающие VM показываются с использованием метода listDomainsID() из библиотеки libvirt. Чтобы увидеть неработающие машины, нужно использовать метод listDefinedDomains(). Они используются по отдельности и позволяют узнать, какие VM можно запустить, а какие - остановить.


Заключение

Эта статья сосредоточена главным образом на действиях, необходимых для создания с помощью wxPython оболочки графического интерфейса, который управляет KVM с помощью libvirt. Библиотека wxPython обширна и содержит широкий спектр виджетов, позволяющих создавать профессионально выглядящие приложения на основе графического интерфейса пользователя. Эта статья только приоткрывает завесу над ее возможностями, но мы надеемся, что вы заинтересовались и займетесь дальнейшим изучением. Раздел Ресурсы поможет вам в этом.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Linux
ArticleID=822396
ArticleTitle=Создание KVM-сценариев на языке Python: Часть 2. Добавление графического интерфейса пользователя для управления KVM с применением libvirt и Python
publish-date=06222012