Python

индекс
250,37

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

Появилось на работе пара свободных часов и решил я себе сделать жизнь удобнее.
По роду деятельности(а работаю я программистом) приходится много чего делать на удалённых серверах, доступ на которые имеется только по 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 == :
        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. Часть кода была взята тут


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


Спасибо всем, кто смог это всё прочитать, знаю это было непросто. =)
Всем удачи!
+103
18 февраля 2009, 13:14
79

комментарии (52)

+9
oddmanout #
Отличный пост, спасибо.
+2
si1v3r #
Отличная статья. Теперь доведите прогу до ума окончательно (тестирование и документация), и выкладывайте в репозитории или на фреш мит, что бы сообщество могло пользоваться
0
lasc #
да, да аналог Smb4k для ftp/ssh очень бы не помешал.
ввел параметры соединения, и у тебя создалась папка ~/remotesite/www.sampe.com куда все смонтировалось
НЛО прилетело и опубликовало эту надпись здесь
+17
evilbloodydemon #
какой волшебный никс, ну надо же!!!
+3
xfather #
то есть в винде на С# такое сделать за пару часиков нельзя? При том что есть куча готовых модулей для работы с чем угодно на codeproject, итп сайтах. Извините, не хочу холивар начинать, просто вырвалось…
0
khim #
В Винде пара часов уйдёт только на то, чтобы скачать Visual C# Express Edition и поставить её :-) Очень сильно удалена разработка от «простых смертных». Это, понятно, одновременно преимущество и недостаток…
+4
gaki #
Надеюсь, с вами ничего нехорошего не случится от шока, который вы испытаете, узнав, что в винде тоже можно писать на Питоне…
0
khim #
В комплект поставки он не входит и на даже на DVD его нет. В любом случае нужно где-то что-то искать.
0
proxor #
Да бросьте. Кому надо это и кто умеет — тот знает (python.org). И в Linux кому не надо не найдет.
+2
Mezomish #
А Вы пробовали? Причём не просто «на Питоне», а с использованием библиотек, не идущих в комплекте? Я как-то провёл пару незабываемых часов «нужна такая-то либа — гугль — чёрт, она только для 2.6, а у меня 2.5 — надо обновить питон — нужна ещё одна либа — гугль — блин, а эту для 2.6 ещё не упаковали в инсталлятор! — придётся руками класть куда надо — да я знать не знаю (и не желаю, если честно), где находится это „куда надо“! — блин, а эта либа вообще только для 2.4 есть, и автор, похоже, забил на неё»…

При этом для Линукса всё нужное находилось лёгким движением «yum search » без каких-либо напрягов.

P.S.: не холивара ради, а справедливости для.
+2
gaki #
Не только пробовали, а в основном так и пишем. А описываемый вами негативный экспериенс был как раз под линуксом, а не под виндами, в которых почему-то всё находилось и ставилось без проблем. А в линуксе, пока требуемое есть в репозитории, тоже легко, но уж если нет — циркулярные депенденси всю душу вынут, пока чего-то достигнешь…
0
proxor #
easy_install решает часть проблем. Правда не все. Оставшуюся часть решает перекомпиляция питона под свой компилятор с тем, чтобы ставить нескомпиленные либы через python install, благо сделать это нетрудно.
НЛО прилетело и опубликовало эту надпись здесь
+3
ara #
Такое можно сделать даже на винде. Но это не отменяет тот факт что Linux более открытая и дружелюбная _программисту_ система. Это и не удивительно, Linux писали как бы «для своих».
НЛО прилетело и опубликовало эту надпись здесь
0
evilbloodydemon #
а что у него с пеп8? по-моему нормально всё
НЛО прилетело и опубликовало эту надпись здесь
+1
Akademic #
Это же не просто прочитать надо, но усвоить, запомнить и не забывать применять.
В-общем, требует времени.
Почитаю. «Ну надо так надо» =).
НЛО прилетело и опубликовало эту надпись здесь
+1
GeorP #
Все больше и больше порываюсь разобраться с Питоном. Хорошая статья, спасибо.
0
zzeus #
тыкнуть в дельфине правой кнопкой по панели быстрого доступа к папкам и добавить туда sshfs не канает?
0
Akademic #
Эээ… Как?
+1
darkstyler #
Мне кажется что использование PyKDE4 в даном случае не очень оправдано.
0
darkstyler #
Разумеется если только задача не стоит принудительно использовать PyKDE4.
А для такой программы лучше б подошла «чистая» PyQt4.
+1
Akademic #
Когда я начинал писать мне очень хотелось как можно быстрее получить результат.
Я особо не задумывался что делаю, и как делаю.
Теперь вижу, что «это надо переписать» :-)
Дело в том, что если бы я сразу начал думать на pyKDE или pyQt делать, то не собрался бы оочень долго.
0
Zert #
Хорошая статья, спасибо!
А вот Вы со связкой PyQt4 и WebKit не работали, случайно?
0
Akademic #
Только читал вот это:
deepwalker.blogspot.com/2008/10/blog-post.html
0
Zert #
Превелико благодарен!
0
Erraen #
Чего только не выдумают, чтоб нормальным редактором (ну, например emacs :) ) не пользоваться…
0
Akademic #
Код написан в vim :-)
0
Erraen #
В vim нету аналогa емаксовского tramp?
0
Akademic #
Не знаю. Сходу ничего не нашёл. Да, вроде и не надо…
+1
Erraen #
Ну, вобщем нагуглил довольно легко — netrw
0
Akademic #
Интересно, попробую. Однако, я больше привык пользоваться внешними файлменеджерами, а редактор использую только для редактирования. М.б. этот плагин будет удобен, а может и нет.
0
zzeus #
Пишем велосипед!
Теперь главный вопрос — а зачем нам оно и чем оно лучше встроенного средства в кде?:D
0
Akademic #
А это уже где-то обсуждалось. Попробуйте воспроизвести фильм так.
fish — это далеко не то же самое, что sshfs.
–1
zzeus #
это монтирование в /media/…
0
Akademic #
А я вижу, что это fish и в /media/ как-то ничего нового не наблюдается.
На всякий случай: debian lenny, KDE4.2 из experimental
0
mocksoul #
ну вообще почти одно и тоже, только sshfs виртуализирует протокол для ядра и всех userspace-программ. Принцип же работы у обоих прост, только fish куда более древняя реализация, и куда менее приятная нежели чем sshfs.

Фильмы смотреть через sshfs — это уже вообще — зачем? sftp и вперёд.
0
non7top #
а вот бы еще подобное почитать про меню и панели инструментов из pykde4. очень было бы интересно.
0
eschava #
я для такого сделал просто набор скриптов и положил их в папку с быстрым доступом
для меня это удобнее. например — можно очень быстро поменять логин и пароль для доступа к какой-нибудь сети
НЛО прилетело и опубликовало эту надпись здесь
0
odonacer #
Ну наконец-то новая статья о Питоне.

На Хабре я не нашел упоминания об этом замечательном сайте showmedo.com — там очень много всего о Python и не только. Раньше не мог опубликовать, потому как кармы мало.
Пользуйтесь :)
0
odonacer #
Извените, какие-то проблемы — немогу вставить ссылку :(
+2
eLLoco #
Похоже, что написание поста заняло больше времени, чем программа :) Спасибо.
+1
Akademic #
Так и есть. :-)
0
DnAp #
Написал что-то подобное на mono с gtk. Если кому интересно могу выложить сырцы.
Теперь по теме.
Я столкнулся с проблемой, что если примонтированная директория длительное время не посещается она может зависнуть из за обрыва SSH соединения.
Решил проблему регулярным, раз в 5 минут, чтением списка файлов из подключенных директорий.
0
Akademic #
У sshfs есть опция reconnect.
Если соединение оборвалось, то при обращении к каталогу куда смонтировали, sshfs попытается восстановить соединение. Правда при этом обычно спрашивает пароль. Просто пароль, непонятно от чего. Каждый раз меня это вводит в задумчивость — что оно от меня хочет.

В случае KDE (и 3 и 4), должен выскакивать(если разрешён доступ в kwallet, то сразу введёт пароль сам, ничего не показывая пользователю) ksshaskpass(если он установлен), он может просто взять пароль из kwallet. Но на одном компьютере у меня так работает, а на втором нет.

Поэтому лучше всего настроить авторизацию по ключам.
–1
zencd #
А чем не подошли системы контроля версий?
По-моему задача как раз для них.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.