Появилось на работе пара свободных часов и решил я себе сделать жизнь удобнее.
По роду деятельности(а работаю я программистом) приходится много чего делать на удалённых серверах, доступ на которые имеется только по ssh. А писать и отлаживать программы удобнее всего локально, и только потом ставить на рабочую машину. Посему удобно использовать sshfs. Однако, набирать в консоли каждый раз команду на монтирование я устал, писать скрипт на баше — лень. Потому захотелось иметь графический менеджер sshfs маунтов, да ко всему прочему в KDE4.
Естественно, от написания своего сопротивлялся до последнего. Гугл вскрылся выдавать мне ответы. Но ничего подходящего я не нашёл.
ksshfs заработал почему-то только в KDE3
sshfsgui тоже не захотел работать, ссылаясь на какие-то явовские ошибки. Перепробовал несколько разных версий и реализаций явамашин — не помогло.
Итак придётся самому.
Для начала, как вообще создавать приложения для KDE4? Это знание я подчерпнул из статьи «Программируем для КДЕ4».
В остальном помогла документация.
В плане интерфейса я ориентировался на sshfsgui. С этого и начнём.
Первым делом берём каркас для прилжения:
Скажу одно — это уже будет запускаться, и мысль эта душу мне согревает. Выглядит вот так:
Поскольку программка за два часа, да и вообще простая, то всё относящееся к интерфейсу я поместил в метод __init__. Не мучаем себя Qt Designer'ом, а просто пишем код.
Выглядит это так:
Итак, после всего этого имеем программу, отображающую нам формочку:
Теперь надо вдохнуть жизнь в каркас нашей программы.
Поскольку все действия пользователь(т.е. я) будет совершать посредством нажатия на кнопки и выбора профиля в списке сохранённых профилей, то надо установить обработчики событий на эти элементы. В этом нам поможет механизм сигналов и слотов.
Всё просто:
Теперь дело за малым — написать собственно обработчики. Начнём по порядку: сохранение профиля и удаление профиля.
Хранить профили будем в домашней директории пользователя в ~/.pyksshfs/hosts/.
Один файл на один профиль.Имя файла — то, что в форме называется «Name».
Логично, что при запуске программа должна проверять, есть ли такой каталог и создавать его в случае отсутствия.
Для этого добавим после описания программы следующий немудрёный код:
А в начало файла с программой import os
Раздумывая над тем как лучше хранить значения полей формы в файле, я подумал, что в питоне наверняка есть уже готовый модуль для хранения конфигов. Так и вышло.
Минутное гугление тут же дало результат: import ConfigParser
Итак, метод onSave:
В конце написания метода приходит идея, что хорошо бы новый профиль сразу появлялся в списке, да и при открытии программы тоже надо отображать список сохранённых профилей.
Так что пишем сразу метод получения и вывода списка и всталяем его вызов в конец __init__ и onSave.
(Почему-то хабр не хочет отображать 0 в коде, заменил на прописную букву О).
Поехали дальше. Метод для размонтирования удалённой директории. Тут объяснять в-общем-то нечего.
Монтирование гораздо интереснее. Эту часть я мучал дольше всего. Скажу по секрету, что именно из-за этого метода я провозился гораздо больше двух часов. Но на самом деле проблемы были такого характера, что знал бы я о них раньше, то вполне уложился бы в срок, приведённый в заголовке.
В чём заключается проблема: комманда монтирования директории через ssh интерактивная и требует ввода пароля от пользователя. Но в случае, если сделана авторизация по ключам, не требует. Соответственно надо сформировать комманду, выполнить, узнать спрашивают ли пароль, затем спросить его у пользователя. А если пароль не нужен, то пользователя не трогать.
У комманды sshfs есть параметр, позволяющий передать пароль с stdin. Но тогда придётся пользователя спросить заранее, что не очень хорошо, когда пароль не нужен.
Есть ещё одна тонкость. Если мы ни разу не заходили на сервер по ssh, нас спросят — «а доверяем ли мы ему?» и надо будет ввести yes.
В-общем, нам надо как-то обработать эти случаи. Для решения такого рода задач существует модуль pexpect ( import pexpect ). С его помощью можно работать с интерактивными программами( например telnet, ftp, ssh ). Что ж, пора показать код.
Часть кода я взял из проекта linux-volume-manager-fuse-kde4, т.к. сначала мой код не хотел работать, а после того как мой код заработал, решил оставить всё же этот, т.к. он обрабатывает больше вариантов.
Для получения пароля от пользователя я использовал программу ksshaskpass. Во-первых, чтобы не писать, во-вторых, она умеет сохранять/получать пароль из kwalletd, что весьма удобно.
Первоначальный код никак не работал из-за того, что по документации ksshaskpass, должен возвращать пароль, а вместо этого в дополнение к паролю возвращает ещё какую-то отладочную строчку. Её пришлось отфильтровать вот так
password = pexpect.run( 'ksshaskpass' ).split( '\n' )[1]
Кстати, если вдруг отладочная строчки исчезнет, программа перестанет работать.
Почти всё готово. Осталось последнее действие: загрузить профиль, когда пользователь выберет его из списка. Сразу код.
Вот и всё. За каких-то пару часов я, владея только синтаксисом питона, гуглом и чёрным поясом по копипасту сделал вполне рабочую программку, которую намерен теперь использовать.
Возможно, в статье я упустил какую-то часть кода.
Так что лучше всего будет скачать полный рабочий варинат pyKSshfs.
Напоследок скриншот:
К середине написания программы я подумал, что она была бы удобнее в виде плазма-аплета. И выглядеть он должен как аплет монтирования флешек. Но так-как возился с ksshaskpass, решил отложить. Может быть скоро я займусь этим. А может быть кто-то из вас меня опередит — буду только рад.
Спасибо всем, кто смог это всё прочитать, знаю это было непросто. =)
Всем удачи!
По роду деятельности(а работаю я программистом) приходится много чего делать на удалённых серверах, доступ на которые имеется только по 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_()
Скажу одно — это уже будет запускаться, и мысль эта душу мне согревает. Выглядит вот так:
Интерфейс
Поскольку программка за два часа, да и вообще простая, то всё относящееся к интерфейсу я поместил в метод __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' ) )
Итак, после всего этого имеем программу, отображающую нам формочку:
Обработка событий
Теперь надо вдохнуть жизнь в каркас нашей программы.
Поскольку все действия пользователь(т.е. я) будет совершать посредством нажатия на кнопки и выбора профиля в списке сохранённых профилей, то надо установить обработчики событий на эти элементы. В этом нам поможет механизм сигналов и слотов.
Всё просто:
#привязка обработчиков событий к кнопкам
#здесь 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, решил отложить. Может быть скоро я займусь этим. А может быть кто-то из вас меня опередит — буду только рад.
Ссылки
- Скачать pyKSshfs.
- «Программируем для КДЕ4»
- Документация по pyQt
- Kommander-скрипт ksshfs
- Java-программа sshfsgui
- Часть кода была взята тут
Спасибо за внимание!
Спасибо всем, кто смог это всё прочитать, знаю это было непросто. =)
Всем удачи!