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

Python
Появилось на работе пара свободных часов и решил я себе сделать жизнь удобнее.
По роду деятельности(а работаю я программистом) приходится много чего делать на удалённых серверах, доступ на которые имеется только по 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:pythonqt4kde4sshfsfusegui
Hubs: Python
+103
7k 85
Comments 52

Popular right now

Преподаватель Python / Python Developer
from 100,000 to 150,000 ₽LoftschoolRemote job
Python-программист
from 80,000 to 120,000 ₽ICNXRemote job
Python-разработчик
from 140,000 to 180,000 ₽ENJOY PROСанкт-ПетербургRemote job
QA automation (Python)
from 120,000 to 150,000 ₽Почта БанкRemote job
Программист C++ / Python
from 170,000 ₽L3 TechnologiesМоскваRemote job