📜 ⬆️ ⬇️

Writing a useful program for KDE4 on python in two hours

A couple of free hours appeared at work and I decided to make life more convenient for myself.
By occupation (I work as a programmer), I have to do a lot of things on remote servers, access to which is available only via ssh. And it is most convenient to write and debug programs locally, and only then install on a working machine. Therefore, it is convenient to use sshfs. However, typing in the console every time I mount the command I am tired, writing a script on the bash is too lazy. Therefore, I wanted to have a graphic manager for sshfs mounts, and everything else in KDE4.


Alternatives


Naturally, from writing his resisted to the last. Google opened to give me answers. But I did not find anything suitable.
ksshfs earned for some reason only in KDE3
sshfsgui didn’t want to work either, referring to some Javas errors. I tried several different versions and implementations of yavamashins - it did not help.

So have yourself.
')

We read


For a start, how to create applications for KDE4 in general? I learned this knowledge from the article “Programming for KDE4” .
The rest of the documentation helped.

Frame


In terms of the interface, I was guided by sshfsgui. With this, let's start.
First of all, we take the frame for the application:
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) : # constructor
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 = "program site"
bugEmail = "email to communicate with the author on the subject of errors"

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_ ( )


I will say one thing - it will already be launched, and this thought warms my soul. Looks like this:
Empty Qt4 application

Interface


Since the program for two hours, and indeed simple, I put everything related to the interface in the __init__ method. Do not torture yourself with Qt Designer, but just write the code.

It looks like this:
def __init__ ( self , parent = None ) : # constructor

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

hbox = KHBox ( self ) # create a horizontal layer
hbox setMargin ( 10 ) # indents 10 pixels
self . setCentralWidget ( hbox ) # make it prime

# two vertical layers inside the main horizontal
vbox_left = KVBox ( hbox )
vbox_right = KVBox ( hbox )

# align the right layer to the top
hbox layout ( ) . setAlignment ( vbox_right , QtCore . Qt . AlignTop )

# data entry fields for mount
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 )

# mount and unmount buttons
# create a separate layer for them
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' ) )

# list for saved profiles
saved_list_label = QLabel ( 'Stored connections:' , vbox_left )
self . saved_list = KListWidget ( vbox_left )
self . saved_list . setMaximumWidth ( 150 )

# buttons for saving and deleting profiles
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' ) )

So, after all this we have a program that displays us a form:
Form, qt program interface

Event handling


Now we need to breathe life into the framework of our program.
Since the user (ie, I) will perform all actions by clicking on the buttons and selecting a profile in the list of saved profiles, you must install event handlers on these items. This will help us the mechanism of signals and slots.
It's simple:
# binding event handlers to buttons
# here save_btn is a variable containing the save button object
# QtCore.SIGNAL ('clicked ()') - “click on the button” signal
# self.onSave - the method called to handle the click
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 )

# the most difficult was to find in the documentation as the signal called “clicked on an element in the list”
self . connect ( self . saved_list , QtCore . SIGNAL ( 'itemClicked (QListWidgetItem *)' ) , self . onSelectServer )


Saving profile

Now it's up to you to write your own handlers. Let's start in order: saving a profile and deleting a profile.
We will store profiles in the user's home directory in ~ / .pyksshfs / hosts /.
One file per profile. The name of the file is what is in the form called “Name”.
It is logical that when starting the program should check whether there is such a directory and create it in case of absence.
To do this, add the following non-simple code after the program description:
config_path = os . getenv ( 'HOME' ) + '/.pyksshfs/hosts'
if not os . path . isdir ( config_path ) :
os . makedirs ( config_path , 0700 )


And to the top of the file with the import os program.
Thinking over how best to store the values ​​of form fields in a file, I thought that in python there must be a ready-made module for storing configs. So it happened.
Minute googling immediately gave the result: import ConfigParser
So, the onSave method:
def onSave ( self ) :
'' '
save settings
' ' '
if self . entry_name . text ( ) : # If there is a profile name
config = ConfigParser . RawConfigParser ( ) # then create and fill the config
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 ) # save the config
file . close ( )
self . selected_name = self . entry_name . text ( )
self . listServers ( ) # update list of profiles


Profile list

At the end of the writing of the method comes the idea that it would be nice if a new profile would immediately appear in the list, and when opening the program, you should also display a list of saved profiles.
So we write at once the method of getting and displaying the list and pick it up at the end of __init__ and onSave.
def listServers ( self ) :
self . saved_list . clear ( )
hosts = os . listdir ( self . config_path )
self . saved_list . insertItems ( O , hosts ) # With this call, add the file list to the list widget
if self . selected_name : # If we have already selected a profile, then it should be selected in the list
item = self . saved_list . findItems ( self . selected_name , QtCore . Qt . MatchExactly )
self . saved_list . setItemSelected ( item [ O ] , True )

(For some reason, Habr does not want to display 0 in the code, replaced with a capital letter O).

Unmounting

Let's go further. Method to unmount a remote directory. There is nothing to explain in general.

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


Mounting

Mounting is much more interesting. This part I tormented the longest. I will tell you a secret that it was because of this method that I spent much more than two hours. But in fact, the problems were of such a nature that I would have known about them before, I would have completely met the deadline given in the title.
What is the problem: the directory mount command via ssh is interactive and requires the user to enter a password. But in case key authorization is done, it does not require. Accordingly, it is necessary to form a command, execute, find out if a password is asked, then ask the user for it. And if the password is not needed, then do not touch the user.
The sshfs command has a parameter that allows you to pass a password from stdin. But then you have to ask the user in advance what is not very good when the password is not needed.
There is another subtlety. If we have never logged on to the server via ssh, they will ask us - “do we trust him?” And we will have to enter yes.

In general, we need to somehow handle these cases. To solve this kind of problem, there is a pexpect (import pexpect) module. With it, you can work with interactive programs (for example, telnet, ftp, ssh). Well, it's time to show the code.
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 = 'Do 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

I took part of the code from the linux-volume-manager-fuse-kde4 project , since At first, my code did not want to work, and after my code was working, I decided to leave this one, because It handles more options.
To get the password from the user, I used the ksshaskpass program. First, not to write, and secondly, she knows how to save / receive a password from kwalletd, which is very convenient.
The original code did not work at all due to the fact that according to the ksshaskpass documentation, it must return the password, and instead of this, in addition to the password, returns some other debugging line. She had to be filtered like this
password = pexpect.run ('ksshaskpass') .split ('\ n') [1]
By the way, if suddenly the debug lines disappear, the program will stop working.

Loading profile

Almost everything is ready. The last action is left: upload a profile when the user selects it from the list. Immediately code.
def onSelectServer ( self , item ) :
"" "
get settings from file when item selected in seved_list
" " "
name = item . text ( ) # filename
self . selected_name = name # remember selection

config = ConfigParser . RawConfigParser ( )
config . readfp ( open ( self . config_path + '/' + name ) ) # open the config

# fill the form fields from the config
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' ) )


Result


That's all. For some couple of hours, I, owning only the syntax of Python, Google and a black belt on copy-paste, made quite a working program, which I now intend to use.
Perhaps in the article I missed some part of the code.
So the best thing is to download the full pyKSshfs working version .

Finally a screenshot:
Ready program

Plans


By the middle of writing a program, I thought that it would be more convenient in the form of a plasma applet. And it should look like an applet mount flash drives. But so, as I was busy with ksshaskpass, I decided to postpone. Maybe soon I'll do it. Maybe one of you will overtake me - I will be only too happy.

Links


  1. Download pyKSshfs .
  2. “We program for KDE4”
  3. PyQt documentation
  4. Kommander script ksshfs
  5. Java program sshfsgui
  6. Part of the code was taken here.


Thanks for attention!


Thanks to everyone who could read it all, I know it was not easy. =)
Good luck to all!

Source: https://habr.com/ru/post/52217/


All Articles