Pull to refresh

Пишем полезную программу для KDE4 на питоне за два часа

Reading time11 min
Views7.9K
Появилось на работе пара свободных часов и решил я себе сделать жизнь удобнее.
По роду деятельности(а работаю я программистом) приходится много чего делать на удалённых серверах, доступ на которые имеется только по ssh. А писать и отлаживать программы удобнее всего локально, и только потом ставить на рабочую машину. Посему удобно использовать sshfs. Однако, набирать в консоли каждый раз команду на монтирование я устал, писать скрипт на баше — лень. Потому захотелось иметь графический менеджер sshfs маунтов, да ко всему прочему в KDE4.


Альтернативы


Естественно, от написания своего сопротивлялся до последнего. Гугл вскрылся выдавать мне ответы. Но ничего подходящего я не нашёл.
ksshfs заработал почему-то только в KDE3
sshfsgui тоже не захотел работать, ссылаясь на какие-то явовские ошибки. Перепробовал несколько разных версий и реализаций явамашин — не помогло.

Итак придётся самому.

Читаем


Для начала, как вообще создавать приложения для KDE4? Это знание я подчерпнул из статьи «Программируем для КДЕ4».
В остальном помогла документация.

Каркас


В плане интерфейса я ориентировался на sshfsgui. С этого и начнём.
Первым делом берём каркас для прилжения:
from PyKDE4.kdeui import KApplication, KMainWindow, KPushButton, KHBox, KVBox, KLineEdit, KListWidget
from PyKDE4.kdecore import i18n, ki18n, KAboutData, KCmdLineArgs
from PyQt4 import QtCore
from PyQt4.QtGui import Qlabel
import sys

class pyksshfsWindow(KMainWindow):

    selected_name = False

def __init__(self, parent = None): #конструктор
        KMainWindow.__init__(self, parent) #call parent constructor

appName = "pyksshfs"
catalog = ""
programName = ki18n("PyKSshfs")
version = "0.1"
description = ki18n("Gui application for using sshfs")
license = KAboutData.License_GPL
copyright = ki18n("© Akademic")
text = ki18n("none")
homePage = "сайт программы"
bugEmail = "email для общения с автором на тему ошибок"

aboutData = KAboutData(appName, catalog, programName, version, description, license, copyright, text, homePage, bugEmail)
KCmdLineArgs.init(sys.argv, aboutData)

app = KApplication()
w = pyksshfsWindow()
w.show()
app.exec_()


Скажу одно — это уже будет запускаться, и мысль эта душу мне согревает. Выглядит вот так:
Пустое приложение Qt4

Интерфейс


Поскольку программка за два часа, да и вообще простая, то всё относящееся к интерфейсу я поместил в метод __init__. Не мучаем себя Qt Designer'ом, а просто пишем код.

Выглядит это так:
def __init__(self, parent = None): #конструктор

        KMainWindow.__init__(self, parent) #call parent constructor

        hbox = KHBox( self ) # создаём горизонтальный слой
        hbox.setMargin(10) # отступы 10 пикселей
        self.setCentralWidget(hbox) #делаем его главным

        #два вертикальных слоя внутри главного горизонтального
        vbox_left = KVBox( hbox )
        vbox_right = KVBox( hbox )

        # выравниваем правый слой по верху
        hbox.layout().setAlignment( vbox_right, QtCore.Qt.AlignTop )

        # поля для ввода данных для монтирования
        entry_name_label = QLabel( 'Name:', vbox_right )
        self.entry_name = KLineEdit( vbox_right )
        server_address_label = QLabel ( 'Server address:', vbox_right )
        self.server_address = KLineEdit( vbox_right )

        server_port_label = QLabel ( 'Server port:', vbox_right )
        self.server_port = KLineEdit( vbox_right )

        user_name_label = QLabel( 'Username:', vbox_right )
        self.user_name = KLineEdit( vbox_right )

        remote_path_label = QLabel( 'Remote path:', vbox_right )
        self.remote_path = KLineEdit( vbox_right )

        local_path_label = QLabel( 'Local path:', vbox_right )
        self.local_path = KLineEdit( vbox_right )

        #кнопки монтирования и размонтирования
        #для них создаём отдельный слой
        btn_hbox_right = KHBox( vbox_right )
        connect_btn = KPushButton( btn_hbox_right )
        connect_btn.setText( i18n( 'Connect' ) )

        disconnect_btn = KPushButton( btn_hbox_right )
        disconnect_btn.setText( i18n( 'Disconnect' ) )

        #список для сохранённых профилей
        saved_list_label = QLabel( 'Stored connections:', vbox_left )
        self.saved_list = KListWidget( vbox_left )
        self.saved_list.setMaximumWidth( 150 )

        #кнопки сохранения и удаления профилей
        btn_hbox_left = KHBox( vbox_left )
        save_btn = KPushButton( btn_hbox_left )
        save_btn.setText( i18n( 'Save' ) )

        delete_btn = KPushButton( btn_hbox_left )
        delete_btn.setText( i18n( 'Delete' ) )

Итак, после всего этого имеем программу, отображающую нам формочку:
Форма, интерфейс программы qt

Обработка событий


Теперь надо вдохнуть жизнь в каркас нашей программы.
Поскольку все действия пользователь(т.е. я) будет совершать посредством нажатия на кнопки и выбора профиля в списке сохранённых профилей, то надо установить обработчики событий на эти элементы. В этом нам поможет механизм сигналов и слотов.
Всё просто:
#привязка обработчиков событий к кнопкам
#здесь save_btn — переменная, содержащая объект кнопки сохранения
# QtCore.SIGNAL('clicked()') — сигнал «клик по кнопке»
# self.onSave — метод, вызываемый для обработки клика
self.connect( save_btn, QtCore.SIGNAL('clicked()'), self.onSave )
self.connect( delete_btn, QtCore.SIGNAL('clicked()'), self.onDelete )
self.connect( connect_btn, QtCore.SIGNAL('clicked()'), self.onConnect )
self.connect( disconnect_btn, QtCore.SIGNAL('clicked()'), self.onDisconnect )

#самым сложным было найти в документации как называется сигнал «кликнули по элементу в списке»
self.connect( self.saved_list, QtCore.SIGNAL( 'itemClicked (QListWidgetItem *)' ), self.onSelectServer )


Сохранение профиля

Теперь дело за малым — написать собственно обработчики. Начнём по порядку: сохранение профиля и удаление профиля.
Хранить профили будем в домашней директории пользователя в ~/.pyksshfs/hosts/.
Один файл на один профиль.Имя файла — то, что в форме называется «Name».
Логично, что при запуске программа должна проверять, есть ли такой каталог и создавать его в случае отсутствия.
Для этого добавим после описания программы следующий немудрёный код:
config_path = os.getenv( 'HOME' )+'/.pyksshfs/hosts'
if not os.path.isdir( config_path ):
    os.makedirs( config_path, 0700 )


А в начало файла с программой import os
Раздумывая над тем как лучше хранить значения полей формы в файле, я подумал, что в питоне наверняка есть уже готовый модуль для хранения конфигов. Так и вышло.
Минутное гугление тут же дало результат: import ConfigParser
Итак, метод onSave:
def onSave( self ):
    '''
    save settings
    '
''
    if self.entry_name.text(): #Если есть ли имя профиля
        config = ConfigParser.RawConfigParser() # то создадим и заполним конфиг
        config.add_section( 'Connection' )
        config.set( 'Connection', 'host', self.server_address.text() )
        config.set( 'Connection', 'port', self.server_port.text() )
        config.set( 'Connection', 'user_name', self.user_name.text() )
        config.set( 'Connection', 'remote_path', self.remote_path.text() )
        config.set( 'Connection', 'local_path', self.local_path.text() )

        if self.selected_name:
            os.unlink( self.config_path+'/'+self.selected_name )

        path = self.config_path+'/'+self.entry_name.text()
        file = open( path, 'w' )
        config.write( file )    #сохраним конфиг
        file.close()
        self.selected_name = self.entry_name.text()
        self.listServers() # обновим список профилей


Список профилей

В конце написания метода приходит идея, что хорошо бы новый профиль сразу появлялся в списке, да и при открытии программы тоже надо отображать список сохранённых профилей.
Так что пишем сразу метод получения и вывода списка и всталяем его вызов в конец __init__ и onSave.
def listServers( self ):
    self.saved_list.clear()
    hosts = os.listdir( self.config_path )
    self.saved_list.insertItems( O, hosts ) #этим вызовом добавляем список файлов в виджет «список»
    if self.selected_name: #Если мы уже выбирали какой-то профиль, то его надо выделить в списке
        item = self.saved_list.findItems( self.selected_name, QtCore.Qt.MatchExactly )
        self.saved_list.setItemSelected( item[O], True )

(Почему-то хабр не хочет отображать 0 в коде, заменил на прописную букву О).

Размонтирование

Поехали дальше. Метод для размонтирования удалённой директории. Тут объяснять в-общем-то нечего.

def onDisconnect( self ):
    if( self.local_path.text() ):
        os.system( 'fusermount -u ' + str( self.local_path.text() ) )


Монтирование

Монтирование гораздо интереснее. Эту часть я мучал дольше всего. Скажу по секрету, что именно из-за этого метода я провозился гораздо больше двух часов. Но на самом деле проблемы были такого характера, что знал бы я о них раньше, то вполне уложился бы в срок, приведённый в заголовке.
В чём заключается проблема: комманда монтирования директории через ssh интерактивная и требует ввода пароля от пользователя. Но в случае, если сделана авторизация по ключам, не требует. Соответственно надо сформировать комманду, выполнить, узнать спрашивают ли пароль, затем спросить его у пользователя. А если пароль не нужен, то пользователя не трогать.
У комманды sshfs есть параметр, позволяющий передать пароль с stdin. Но тогда придётся пользователя спросить заранее, что не очень хорошо, когда пароль не нужен.
Есть ещё одна тонкость. Если мы ни разу не заходили на сервер по ssh, нас спросят — «а доверяем ли мы ему?» и надо будет ввести yes.

В-общем, нам надо как-то обработать эти случаи. Для решения такого рода задач существует модуль pexpect ( import pexpect ). С его помощью можно работать с интерактивными программами( например telnet, ftp, ssh ). Что ж, пора показать код.
def onConnect( self ):
    command = 'sshfs '
    if self.user_name.text():
        command += self.user_name.text() + '@'
    command += self.server_address.text()
    if self.remote_path.text():
        command += ':' + self.remote_path.text()
    else:
        command += ':/'

    if self.server_port.text():
        command += ' -p ' + self.server_port.text()

    command += ' ' + self.local_path.text()

    sshfs = pexpect.spawn( str( command ), env = {'SSH_ASKPASS':'/dev/null'} )
    ssh_newkey = 'Are you sure you want to continue connecting'
    i = sshfs.expect( [ssh_newkey, 'assword:', pexpect.EOF, pexpect.TIMEOUT] )

    if i == 0:
        sshfs.sendline('yes')
        i = sshfs.expect([ssh_newkey,'assword:',pexpect.EOF])
    if i == 1:
        #If no password ask for it
        askpasscmd = 'ksshaskpass %s'%self.entry_name.text()
        password = pexpect.run( askpasscmd ).split( '\n' )[1]
        sshfs.sendline( password )
        j = sshfs.expect( [pexpect.EOF, 'assword:'] )
        if j == 1:
            #Password incorrect, force the connection close
            print "Password incorrect"
            sshfs.close(True)
    #p.terminate(True)
    elif i == 2:
        #Any problem
        print "Error found: %s" % sshfs.before
    elif i == 3:
        #Timeout
        print "Timeout: %s" % sshfs.before
    print sshfs.before

Часть кода я взял из проекта linux-volume-manager-fuse-kde4, т.к. сначала мой код не хотел работать, а после того как мой код заработал, решил оставить всё же этот, т.к. он обрабатывает больше вариантов.
Для получения пароля от пользователя я использовал программу ksshaskpass. Во-первых, чтобы не писать, во-вторых, она умеет сохранять/получать пароль из kwalletd, что весьма удобно.
Первоначальный код никак не работал из-за того, что по документации ksshaskpass, должен возвращать пароль, а вместо этого в дополнение к паролю возвращает ещё какую-то отладочную строчку. Её пришлось отфильтровать вот так
password = pexpect.run( 'ksshaskpass' ).split( '\n' )[1]
Кстати, если вдруг отладочная строчки исчезнет, программа перестанет работать.

Загрузка профиля

Почти всё готово. Осталось последнее действие: загрузить профиль, когда пользователь выберет его из списка. Сразу код.
def onSelectServer( self, item ):
    """
    get settings from file, when item selected in seved_list
    "
""
    name = item.text() # имя файла
    self.selected_name = name #запоминаем выбор

    config = ConfigParser.RawConfigParser()
    config.readfp( open( self.config_path+'/'+name ) ) #открываем конфиг

    # заполняем поля формы из конфига
    self.entry_name.setText( name )
    self.server_address.setText( config.get( 'Connection', 'host' ) )
    self.server_port.setText( config.get( 'Connection', 'port' ) )
    self.user_name.setText( config.get( 'Connection', 'user_name' ) )
    self.remote_path.setText( config.get( 'Connection', 'remote_path' ) )
    self.local_path.setText( config.get( 'Connection', 'local_path' ) )


Результат


Вот и всё. За каких-то пару часов я, владея только синтаксисом питона, гуглом и чёрным поясом по копипасту сделал вполне рабочую программку, которую намерен теперь использовать.
Возможно, в статье я упустил какую-то часть кода.
Так что лучше всего будет скачать полный рабочий варинат pyKSshfs.

Напоследок скриншот:
Готовая программа

Планы


К середине написания программы я подумал, что она была бы удобнее в виде плазма-аплета. И выглядеть он должен как аплет монтирования флешек. Но так-как возился с ksshaskpass, решил отложить. Может быть скоро я займусь этим. А может быть кто-то из вас меня опередит — буду только рад.

Ссылки


  1. Скачать pyKSshfs.
  2. «Программируем для КДЕ4»
  3. Документация по pyQt
  4. Kommander-скрипт ksshfs
  5. Java-программа sshfsgui
  6. Часть кода была взята тут


Спасибо за внимание!


Спасибо всем, кто смог это всё прочитать, знаю это было непросто. =)
Всем удачи!
Tags:
Hubs:
+103
Comments52

Articles