Compare commits

..

No commits in common. "master" and "GHPS/Better-Scriptability" have entirely different histories.

6 changed files with 203 additions and 655 deletions

17
LICENSE
View File

@ -4,26 +4,27 @@ sshch is released under the DWTW license
This program is free software; you can redistribute it and/or modify it under the terms of the Do What Thou Wilt License. This program is free software; you can redistribute it and/or modify it under the terms of the Do What Thou Wilt License.
Boundless Public License DO WHAT THAU WILT
DO WHAT THOU WILT
TO PUBLIC LICENSE TO PUBLIC LICENSE
Version 2.55 Version 2.5
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it in full or in part is allowed without any restrictions. Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. Do what thou wilt shall be the whole of the Law. 0. Do what thou wilt shall be the whole of the Law.
DWTWL a license with a single requirement: DO WHAT THOU WILT Anyone is allowed to copy and distribute the copies of this license agreement in whole or in part, as well as modify it without any other limitations.
DWTW a license with a single requirement: DO WHAT THOU WILT
The license provides more freedom than any other one (such as GPL or BSD) and does not require saving the license text on copying. The license provides more freedom than any other one (such as GPL or BSD) and does not require saving the license text on copying.
DWTWL an accomplished and eligible license for free text, code and any other symbols (including the software, documentation and artwork). DWTW an accomplished and eligible license for free text (including the software, documentation and artwork).
The license does not contain a "no warranty" clause. DWTWL can be used in countries that do not legally acknowledge the transition to public domain. The license does not contain "no warranty" clause. DWTW can be used in countries that do not legally acknowledge the transition to public domain.
Summary: Summary:
An author-creator gives their source code to the world for free, without becoming distracted by worldly thinking regarding how and why the others will use it. An author-creator gives his or her source code to the world for free, without becoming distracted by worldly thinking regarding how and why the others will use it.

View File

@ -1,49 +1,31 @@
SSH connection and aliases manager with curses and command line interface SSH connection manager with curses interface
====== ======
sshch is released under DWTWL 2.55 license sshch is released under DWTWL 2.5 license
sshch compatible with pyhon2 and python3, no additional libraries are required
### Screenshot ### Screenshot
![sshch](https://dev.ussr.win/zlax/sshch/raw/branch/master/sshch_screenshot.png) ![sshch](https://raw.githubusercontent.com/zlaxy/sshch/master/sshch_screenshot.png)
### Installing ### Installing
**You can install a release version from pip:**
```bash
pip install sshch
```
**Manual installation from the package or git repository also available:**
To install for all users: To install for all users:
```bash ```
sudo python setup.py install sudo python setup.py install
``` ```
To install just for current user: To install just for current user:
```bash ```
mkdir ~/.local/bin mkdir ~/.local/bin
cp sshch/sshch ~/.local/bin/ cp sshch/sshch ~/.local/bin/
``` ```
### Using ### Using
To run curses interface: To run curses interface:
```bash ```
sshch sshch
``` ```
To run command line help: To run command line help:
```bash ```
sshch -h sshch -h
``` ```
For exit from current ssh session press `Ctrl+D`. **If you want to use unsafe 'password' feature you must install 'sshpass' first.**
**Additional Features** If you want to use bash autocompletion function with sshch, copy autocompletion script to /etc/bash_completion.d/:
- If you want to use unsafe 'password' feature you must install `sshpass` first. ```
- If you want to use bash autocompletion function with sshch, copy autocompletion script to /etc/bash_completion.d/: sudo cp sshch_bash_completion.sh /etc/bash_completion.d/sshch
```bash
sudo cp completion/sshch_bash_completion.sh /etc/bash_completion.d/sshch
``` ```
(changes will come into effect with new bash session) (changes will come into effect with new bash session)
- If you want to use zsh autocompletion:
1) Place File in a Directory where ZSH can find it
-> Search Path is Stored in $fpath
-> echo $fpath
2) Rename File to '_sshch'

View File

@ -1,27 +0,0 @@
#compdef sshch
#
# ZSH Completion for SSHCH
# Usage:
# 1) Place File in a Directory where ZSH can find it
# -> Search Path is Stored in $fpath
# -> echo $fpath
# 2) Rename File to '_sshch'
#
_arguments '::aliasname:->getAlias' \
'-e[Edit Alias]:aliasname:->getAlias' '--edit[Edit Alias]:aliasname:->getAlias' \
'-p[Set Password]:aliasname:->getAlias' '--password[Set Password]:aliasname:->getAlias' \
'-r[Remove Alias]:aliasname:->getAlias' '--remove[Remove Alias]:aliasname:->getAlias' \
'-k[Keep Connection]:aliasname:->getAlias' '--keep[Keep Connection]:aliasname:->getAlias' \
'-a[Add Alias]' '--add[Add Alias]'\
'-c[Add Command for Executing Alias]' '--command[Add Command for Executing Alias]'\
'-h[Show Help Message]' '--help[Show Help Message]'\
'-l[List Existing Alias]' '--list[List Existing Alias]'\
'-f[List Existing Alias with Connection String]' '--fulllist[List Existing Alias with Connection String]'\
'--version[Show Program Version]'
case "$state" in
getAlias)
local -a alias_list
alias_list=($(sshch -l))
_values -s ' ' 'Aliases' $alias_list
;;
esac

View File

@ -7,27 +7,22 @@ def main():
setup(name='sshch', setup(name='sshch',
author='zlaxy', author='zlaxy',
author_email='zlaxyi@gmail.com', url='https://github.com/zlaxy/sshch/',
url='https://gitlab.com/zlax/sshch', description='Ssh connection manager',
description='Ssh connection and aliases manager', license='DWTWL 2.5',
long_description='SSH connection and aliases manager with curses and command line interface', version='0.8',
long_description_content_type='text/x-rst',
license='DWTWL 2.55',
version='1.09.7',
py_modules=['sshch'], py_modules=['sshch'],
scripts=['sshch/sshch'], scripts=['sshch/sshch'],
keywords='sshch ssh aliases curses manager',
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4',
# http://pypi.python.org/pypi?%3Aaction=list_classifiers # http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 4 - Beta',
'Environment :: Console :: Curses', 'Environment :: Console :: Curses',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
'License :: Freeware', 'License :: Freeware',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python', 'Programming Language :: Python :: 2.7',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: System :: Networking', 'Topic :: System :: Networking',
'Topic :: System :: Systems Administration', 'Topic :: System :: Systems Administration',

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from __future__ import division
@ -7,8 +7,7 @@ try:
import configparser import configparser
except ImportError: except ImportError:
import ConfigParser as configparser # Python 2.x import import ConfigParser as configparser # Python 2.x import
import locale
locale.setlocale(locale.LC_ALL, '')
from os import path from os import path
from sys import argv from sys import argv
from math import ceil from math import ceil
@ -19,24 +18,11 @@ import base64
import time import time
import curses import curses
from curses import textpad, panel from curses import textpad, panel
from threading import Thread
# https://gitlab.com/zlax/sshch # https://github.com/zlaxy/sshch
version = "1.09.7" version = "0.9"
# expand groups by default # path to conf file, default: ~/.config/sshch.conf
expand_default = True conf_file = path.expanduser("~") + '/.config/sshch.conf'
# path to conf dir and file, default: ~/.config/sshch.conf
conf_dir = path.expanduser("~") + '/.config'
conf_file = conf_dir + '/' + 'sshch.conf'
class GroupTree(object):
"""Group object with relatives information"""
def __init__(self, group):
self.group = group
self.aliases = []
self.children = []
self.parent = []
def AddNewAlias(alias): def AddNewAlias(alias):
@ -45,7 +31,7 @@ def AddNewAlias(alias):
conf.write(open(conf_file, "w")) conf.write(open(conf_file, "w"))
return True return True
else: else:
return "error: '" + alias + "' alias or group already exists" return "error: '" + alias + "' already exists"
def SetAliasString(alias, string): def SetAliasString(alias, string):
@ -53,20 +39,10 @@ def SetAliasString(alias, string):
conf.write(open(conf_file, "w")) conf.write(open(conf_file, "w"))
def SetGroupString(alias, string):
conf.set(alias, "group", string)
conf.write(open(conf_file, "w"))
def SetPassword(alias, string): def SetPassword(alias, string):
if string == "" or string == b'': string = base64.b64encode(base64.b16encode(
conf.remove_option(alias, "password") base64.b32encode(string)))
else: conf.set(alias, "password", string)
string = string.encode()
string = base64.b64encode(base64.b16encode(
base64.b32encode(string)))
string = string.decode('utf-8')
conf.set(alias, "password", string)
conf.write(open(conf_file, "w")) conf.write(open(conf_file, "w"))
@ -76,96 +52,31 @@ def RemoveAliases(aliases):
conf.write(open(conf_file, "w")) conf.write(open(conf_file, "w"))
def ConvertPassword(password): def ConnectAlias(alias, command=False):
password_string = "'"
for char in password:
if char == "'":
password_string += "'"+'"'+"'"+'"'+"'"
elif char == '"':
password_string += "''"+'"'+"''"
elif char == ';':
password_string += "'"+r'\;'+"'"
elif char == "\\":
password_string += "'"+'"'+"\\"+"\\"+'"'+"'"
else:
password_string += char
password_string += "'"
return password_string
def Connect(aliases, command=False, threading=False, screen=False):
if screen:
curses.endwin()
groups = []
connectaliases = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
for alias in aliases:
if alias in groups:
group_aliases = GroupChildAliases(alias)
for ga in group_aliases:
connectaliases.append(ga)
else:
connectaliases.append(alias)
connectaliases = setSeq(connectaliases)
threads = {}
for alias in connectaliases:
if conf.has_section(alias):
print("Connecting to " + alias + "...")
if threading:
threads[alias] = Thread(target=ConnectAlias, args=(alias,
command, True))
threads[alias].start()
else:
ConnectAlias(alias, command)
if not threading:
print("... " + alias + " session finished.")
else:
print("error: '" + alias + "' alias does not exists")
if screen:
print("Press 'enter' to continue.")
screen.getch()
def ConnectAlias(alias, command=False, threading=False):
exec_string = "" exec_string = ""
if conf.has_option(alias, "password"): if conf.has_option(alias, "password"):
password = base64.b32decode(base64.b16decode( password = base64.b32decode(base64.b16decode(
base64.b64decode(conf.get(alias, "password")))) base64.b64decode(conf.get(alias, "password"))))
exec_string = "sshpass -p " + ConvertPassword(password.decode('utf-8')) + " " exec_string = 'sshpass -p "' + password + '" '
if conf.has_option(alias, "exec_string"): if conf.has_option(alias, "exec_string"):
exec_string = exec_string + conf.get(alias, "exec_string") exec_string = exec_string + conf.get(alias, "exec_string")
if command: if command:
exec_string = exec_string + " " + command exec_string = exec_string + " " + command
try: # Variables bellow is newer used
subprocess.Popen(exec_string, shell=True).communicate()[0] subprocess.Popen(exec_string, shell=True).communicate()[0]
except:
pass
if threading:
print ("... "+alias+" session output finished.")
def HoldConnection(alias): def HoldConnection(alias):
groups = [] print("Connecting to " + alias + ". Press CTRL+C to cancel.")
connectaliases = [] time.sleep(1)
for a in conf.sections(): while True:
if conf.has_option(a, "group"): ConnectAlias(alias)
groups.append(a) time.sleep(5)
if alias in groups:
print("Can't hold connection with group.")
else:
print("Connecting to " + alias + ". Press CTRL+C to cancel.")
time.sleep(1)
while True:
ConnectAlias(alias)
time.sleep(5)
def CMDAdd(alias): def CMDAdd(alias):
alias = alias.split()[0].strip()
result = AddNewAlias(alias) result = AddNewAlias(alias)
if result == True: if result:
prompt_add = ("".join(["Enter connection string for new alias ", prompt_add = ("".join(["Enter connection string for new alias ",
"(example: ssh user@somehost.com):\n"])) "(example: ssh user@somehost.com):\n"]))
string = "" string = ""
@ -176,179 +87,71 @@ def CMDAdd(alias):
print(result) print(result)
def CMDGroup(group):
group = group.split()[0].strip()
result = AddNewAlias(group)
if result == True:
prompt_add = ("".join(["Enter aliases for new group ",
"(example: alias1 alias2):\n"]))
string = ""
while string == "":
string = input(prompt_add)
SetGroupString(group, string)
else:
print(result)
def CMDEdit(alias): def CMDEdit(alias):
if conf.has_section(alias): if conf.has_section(alias):
if conf.has_option(alias, "exec_string"): prompt_edit = ("".join(["Enter connection string for existing alias ",
prompt_edit = ("".join(["Enter connection string for existing ", "(example: ssh user@somehost.com):\n"]))
"alias (example: ssh user@somehost.com):\n"])) string = ""
string = "" while string == "":
while string == "": string = input(prompt_edit)
string = input(prompt_edit) SetAliasString(alias, string)
SetAliasString(alias, string)
elif conf.has_option(alias, "group"):
prompt_edit = ("".join(["Enter aliases for existing group ",
"(example: alias1 alias2):\n"]))
string = ""
while string == "":
string = input(prompt_edit)
SetGroupString(alias, string)
else:
print("error: '" + alias + "' is not correct alias or group")
else: else:
print("error: '" + alias + "' alias or group does not exists") print("error: '" + alias + "' alias is not exists")
def CMDPassword(alias): def CMDPassword(alias):
groups = [] if conf.has_section(alias):
connectaliases = [] prompt_pass = ("[UNSAFE] Enter password for sshpass: ")
for a in conf.sections(): string = ""
if conf.has_option(a, "group"): string = getpass(prompt_pass)
groups.append(a) if not string == "":
if alias in groups:
print("Can't set password for group.")
else:
if conf.has_section(alias):
prompt_pass = ("[UNSAFE] Enter password for sshpass (Ctrl+C" +
" - cancel, blank - clear password):\n")
string = ""
string = getpass(prompt_pass)
SetPassword(alias, string) SetPassword(alias, string)
else: else:
print("error: '" + alias + "' alias does not exists") print("error: '" + alias + "' alias is not exists")
def CMDRemove(alias): def CMDRemove(alias):
if conf.has_section(alias): if conf.has_section(alias):
prompt_remove = ("Type 'yes' if you sure to remove '" + alias + prompt_remove = ("Type 'yes' if you sure to remove '" + alias + "' alias: ")
"' alias or group: ")
string = input(prompt_remove) string = input(prompt_remove)
if string == "yes": if string == "yes":
RemoveAliases([alias]) RemoveAliases([alias])
else: else:
print("'" + alias + "' alias or group was not deleted.") print("'" + alias + "' alias was not deleted.")
else: else:
print("error: '" + alias + "' alias or group does not exists.") print("error: '" + alias + "' alias is not exists.")
def CMDConnect(aliases, command=False):
for alias in aliases:
if conf.has_section(alias):
print("Connecting to " + alias + "...")
ConnectAlias(alias, command)
print("... " + alias + " session finished.")
else:
print("error: '" + alias + "' alias is not exists")
def CMDList(option, opt, value, parser): def CMDList(option, opt, value, parser):
print(' '.join(str(p) for p in conf.sections())) print(' '.join(str(p) for p in conf.sections()))
def setSeq(seq):
# py2&3 fastest way to remove duplicates from a list:
# https://www.rupython.com/x432-4-496.html
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
def GroupChildRecursion(group, childaliases, treelist):
if treelist[group].children:
for a in treelist[group].children:
GroupChildRecursion(a, childaliases, treelist)
if treelist[group].aliases:
for a in treelist[group].aliases:
childaliases.append(a)
return childaliases
def GroupChildAliases(group):
aliases = []
groups = []
rootgroups = []
treelist = {}
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
elif conf.has_option(a, "exec_string"):
aliases.append(a)
rootaliases = list(aliases)
for g in groups:
treelist[g] = GroupTree(g)
group_aliases = conf.get(g, "group").split()
for ga in group_aliases:
if ga in groups:
treelist[g].children.append(ga)
elif ga in aliases:
treelist[g].aliases.append(ga)
try:
rootaliases.remove(ga)
except ValueError:
pass
for g in groups:
if treelist[g].children:
for child in treelist[g].children:
treelist[child].parent.append(g)
for g in groups:
if not treelist[g].parent:
rootgroups.append(g)
if group == False:
return aliases, groups, rootaliases, rootgroups, treelist
else:
childaliases = GroupChildRecursion(group, [], treelist)
return setSeq(childaliases)
def GroupTreeRecursion(level, group, treelist, resultalias, resultstring,
expandlist, previousgroups):
if group in previousgroups:
return resultalias, resultstring
previousgroups.append(group)
resultalias.append(group)
if expandlist == True or group in expandlist:
resultstring.append(' '*(level-1)+">> "+group)
for g in treelist[group].children:
resultalias, resultstring = GroupTreeRecursion(level+1, g,
treelist, resultalias, resultstring, expandlist, previousgroups)
for ga in treelist[group].aliases:
if conf.has_option(ga, "exec_string"):
resultalias.append(ga)
result = "".join([' '*(level-1)+" ~ ", str(ga), " (",
(conf.get(ga, "exec_string") if conf.has_option(ga,
"exec_string") else ""), ")",
(" [password]" if conf.has_option(ga, "password") else "")])
resultstring.append(result)
else:
resultstring.append(' '*(level-1)+"<> "+group)
return resultalias, resultstring
def GetTreeList(strings=True, expandlist=True):
resultalias = []
resultstring = []
aliases, groups, rootaliases, rootgroups, treelist = GroupChildAliases(False)
for g in rootgroups:
resultalias, resultstring = GroupTreeRecursion(1, g, treelist,
resultalias, resultstring, expandlist, [])
for a in rootaliases:
resultalias.append(a)
result = "".join([str(a), " (", (conf.get(a, "exec_string") if
conf.has_option(a, "exec_string") else ""), ")",
(" [password]" if conf.has_option(a, "password") else "")])
resultstring.append(result)
if strings:
return resultstring
else:
return resultalias
def CMDFullList(option, opt, value, parser): def CMDFullList(option, opt, value, parser):
for p in GetTreeList(): for p in conf.sections():
print (p) to_print = "".join([str(p), " - ", (conf.get(p, "exec_string") if
conf.has_option(p, "exec_string") else ""),
(" [password]" if conf.has_option(p, "password") else "")])
print(to_print)
def CursesConnect(screen, aliases, command=False):
curses.endwin()
for alias in aliases:
print("Connecting to " + alias + "...")
ConnectAlias(alias, command)
print("... " + alias + " session finished.")
print("Press 'enter' to continue.")
screen.getch()
def CursesExit(error=False): def CursesExit(error=False):
@ -358,21 +161,9 @@ def CursesExit(error=False):
exit() exit()
class CursesTextpadEsc(Exception):
"ESC key has been pressed"
def CursesTextpadConfirm(value): def CursesTextpadConfirm(value):
if value == 10: # Enter if value == 10:
value = 7 value = 7
elif value == 27: # Esc
raise CursesTextpadEsc()
elif value == curses.KEY_DC: # Del
value = curses.ascii.EOT
elif value == curses.KEY_HOME: # Home
value = curses.ascii.SOH
elif value == curses.KEY_END: # End
value = curses.ascii.ENQ
return value return value
@ -425,7 +216,7 @@ def CursesPanel(screen, h, w, y, x, text,
position = position - 1 position = position - 1
hidden_password = hidden_password[0:-1] hidden_password = hidden_password[0:-1]
if keych > 31 and keych < 127: if keych > 31 and keych < 127:
hidden_password += curses.keyname(keych).decode('utf-8') hidden_password += curses.keyname(keych)
sub_window.addstr(1, position, "*", text_colorpair) sub_window.addstr(1, position, "*", text_colorpair)
if position < w - 4: if position < w - 4:
position += 1 position += 1
@ -445,20 +236,19 @@ def CMDOptions():
def format_epilog(self, formatter): def format_epilog(self, formatter):
return self.epilog return self.epilog
usage = "usage: %prog [options] [aliases]" usage = "usage: %prog [options] [aliases]"
progname = path.basename(__file__) progname = path.basename(__file__)
epilog = ("".join(["Examples:\n ", epilog = ("".join(["Examples:\n ",
progname, " existingalias\n ", progname, " existingalias\n ",
progname, " -a newremoteserver\n ", progname, " -a newremoteserver\n ",
progname, " --edit=newremoteserver -p newremoteserver\n ", progname, " --edit=newremoteserver -p newremoteserver\n ",
progname, ' -c "ls -l" newremoteserver\n ', progname, ' -c "ls -l" newremoteserver\n ',
progname, " -c reboot existingalias newremoteserver\n", progname, " -c reboot existingalias newremoteserver\n",
"Examples of connection string:\n ", "Examples of connection string:\n ",
"ssh user@somehost.com\n ", "ssh user@somehost.com\n ",
"ssh gates@8.8.8.8 -p 667\n ", "ssh gates@8.8.8.8 -p 667\n ",
"ssh root@somehost.com -t tmux a\n", "ssh root@somehost.com -t tmux a\n",
"Also, you can edit the config file manually: ", conf_file, "\n"])) "Also, you can edit config file manually: ", conf_file, "\n"]))
opts = FormatedParser(usage=usage, version="%prog " + version, opts = FormatedParser(usage=usage, version="%prog " + version,
epilog=epilog) epilog=epilog)
opts.add_option('-l', '--list', action="callback", opts.add_option('-l', '--list', action="callback",
@ -469,33 +259,24 @@ def CMDOptions():
opts.add_option('-a', '--add', action="store", type="string", opts.add_option('-a', '--add', action="store", type="string",
dest="add", metavar="alias", default=False, dest="add", metavar="alias", default=False,
help="add new alias for connection string") help="add new alias for connection string")
opts.add_option('-g', '--group', action="store", type="string",
dest="group", metavar="group", default=False,
help="add new group for aliases")
opts.add_option('-c', '--command', action="store", type="string", opts.add_option('-c', '--command', action="store", type="string",
dest="command", metavar="command", default=False, dest="command", metavar="command", default=False,
help="execute command for aliases and groups") help="add command for executing alias")
opts.add_option('-t', '--thread', action="store", type="string",
dest="thread", metavar="command", default=False,
help="parallel command execution for aliases and groups " +
"(ssh key authentication or set password required)" )
opts.add_option('-k', '--keep', action="store", type="string", opts.add_option('-k', '--keep', action="store", type="string",
dest="keep", metavar="alias", default=False, dest="keep", metavar="alias", default=False,
help="hold connection with specified alias") help="hold connection with specified alias")
opts.add_option('-e', '--edit', action="store", type="string", opts.add_option('-e', '--edit', action="store", type="string",
dest='edit', metavar="alias", default=False, dest='edit', metavar="alias", default=False,
help="edit existing alias or group") help="edit existing connection string")
opts.add_option('-p', '--password', action="store", type="string", opts.add_option('-p', '--password', action="store", type="string",
dest='password', metavar="alias", default=False, dest='password', metavar="alias", default=False,
help="set and store password for sshpass [UNSAFE]") help="set and store password for sshpass [UNSAFE]")
opts.add_option('-r', '--remove', action="store", type="string", opts.add_option('-r', '--remove', action="store", type="string",
dest='remove', metavar="alias", default=False, dest='remove', metavar="alias", default=False,
help="remove existing alias or group") help="remove existing alias of connection string")
options, alias = opts.parse_args() options, alias = opts.parse_args()
if options.add: if options.add:
CMDAdd(options.add) CMDAdd(options.add)
if options.group:
CMDGroup(options.group)
if options.edit: if options.edit:
CMDEdit(options.edit) CMDEdit(options.edit)
if options.password: if options.password:
@ -504,46 +285,28 @@ def CMDOptions():
CMDRemove(options.remove) CMDRemove(options.remove)
if options.keep: if options.keep:
HoldConnection(options.keep) HoldConnection(options.keep)
if options.thread: if alias:
Connect(alias, options.thread, True) CMDConnect(alias, options.command)
elif alias:
Connect(alias, options.command)
# curses template from: https://stackoverflow.com/a/30828805/6224462 # curses template from: https://stackoverflow.com/a/30828805/6224462
def CursesMain(): def CursesMain():
help_screen = ("".join([" Press:\n", help_screen = ("".join([" Press:\n",
" 'z'/'x', 'w'/'s' or arrows - navigation\n", " 'z'/'x' or arrows - navigation\n",
" 'a'/'F2' - add new alias (without spaces)\n", " 'a'/'F2' - add new alias\n",
" 'g'/'F5' - add new group (spaces will be stripped)\n", " 'e'/'F4' - edit existing alias\n",
" 'e'/'F4' - edit existing alias/group\n", " 'p'/'F6' - set alias's password for sshpass [UNSAFE]\n",
" 'p'/'F6' - set alias's password for sshpass [UNSAFE]\n", " 'space'/'insert' - select\n",
" 'space'/'insert' - select\n", " 'r'/'F8' - remove selected alias/aliases\n",
" 'r'/'F8' - remove selected alias/aliases\n", " 'c'/'F3' - execute specific command with selected alias/aliases\n",
" 'c'/'F3' - command execution for alias (group/aliases - in turn)\n", " 'k'/'F7' - hold connection with selected alias\n",
" 't'/'F11' - parallel command execution for aliases and groups\n", " 'enter'/'F9' - connect to selected alias/aliases\n",
" (ssh key authentication or set password required)\n", " 'q'/'F10' - quit\n",
" 'k'/'F7' - hold connection with selected alias\n", " Run program with '--help' option to view command line help.\n",
" 'enter'/'F9' - connect to selected alias/aliases,\n", " Also, you can edit config file manually:\n",
" expand/collapse group\n", " ", conf_file]))
" 'q'/'F10' - quit\n", strings = conf.sections()
" Run program with '--help' option to view command line help.\n",
" Also, you can edit the config file manually:\n",
" ", conf_file]))
if expand_default == True:
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
expanded = list(groups)
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
elif expand_default == False:
groups = []
expanded = []
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)] selected_strings = [" " for i in range(0, row_num + 1)]
screen = curses.initscr() screen = curses.initscr()
@ -570,7 +333,14 @@ def CursesMain():
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text) width - 6, highlight_text)
else: else:
exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]] if conf.has_option(strings[i - 1], "password"):
password = " [password]"
else:
password = ""
exec_string = ["[", selected_strings[i], "] ", str(i), " ",
strings[i - 1], " (", (conf.get(strings[i - 1],
"exec_string") if conf.has_option(strings[i - 1],
"exec_string") else ""), ")", password]
if (i == position): if (i == position):
box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text) box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text)
else: else:
@ -591,151 +361,55 @@ def CursesMain():
help_screen, normal_text, highlight_text) help_screen, normal_text, highlight_text)
if key_pressed == ord('a') or key_pressed == ord( if key_pressed == ord('a') or key_pressed == ord(
'A') or key_pressed == curses.KEY_F2: 'A') or key_pressed == curses.KEY_F2:
curses.curs_set(1)
new_alias_textpad = CursesTextpad(screen, 1, width - 8, new_alias_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter new alias:", "", (height // 2) - 1, 4, "Enter new alias:", "",
normal_text, highlight_text) normal_text, highlight_text)
try: add_alias = new_alias_textpad.edit(CursesTextpadConfirm)
add_alias = new_alias_textpad.edit(CursesTextpadConfirm) if not add_alias.rstrip() == "":
except CursesTextpadEsc: add_result = AddNewAlias(add_alias.rstrip())
add_alias = "" if not add_result:
if not add_alias == "": CursesPanel(screen, 3,
add_alias = add_alias.split()[0].strip() width - 6, (height // 2) - 1, 3, add_result,
if not add_alias == "": normal_text, highlight_text)
add_result = AddNewAlias(add_alias) else:
if add_result == True:
add_string = "" add_string = ""
try: while add_string.rstrip() == "":
while add_string == "": string_textpad = CursesTextpad(screen, 3,
string_textpad = CursesTextpad(screen, 3, width - 8, (height // 2) - 1, 4,
width - 8, (height // 2) - 1, 4, "Enter full execution string:",
"Enter full execution string:", "ssh ", normal_text, highlight_text)
"ssh ", normal_text, highlight_text) add_string = string_textpad.edit(
add_string = string_textpad.edit( CursesTextpadConfirm)
CursesTextpadConfirm) SetAliasString(add_alias.rstrip(),
SetAliasString(add_alias, add_string.replace("\n", "").rstrip())
add_string.replace("\n", "").rstrip()) strings = conf.sections()
except CursesTextpadEsc:
RemoveAliases([add_alias])
add_alias = ""
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings.append(" ") selected_strings.append(" ")
pages = int(ceil(row_num / max_row)) pages = int(ceil(row_num / max_row))
box.refresh() box.refresh()
else:
curses.curs_set(0)
CursesPanel(screen, 3,
width - 6, (height // 2) - 1, 3, add_result,
normal_text, highlight_text)
curses.curs_set(0)
if key_pressed == ord('g') or key_pressed == ord(
'G') or key_pressed == curses.KEY_F5:
curses.curs_set(1)
new_group_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter group name (without spaces):", "",
normal_text, highlight_text)
try:
add_group = new_group_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
add_group = ""
if not add_group == "":
add_group = add_group.split()[0].strip()
if not add_group == "":
add_result = AddNewAlias(add_group)
if add_result == True:
add_string = ""
try:
while add_string.rstrip() == "":
string_textpad = CursesTextpad(screen, 3,
width - 8, (height // 2) - 1, 4,
"Enter aliases for new group (example: alias1 alias2):",
"", normal_text, highlight_text)
add_string = string_textpad.edit(
CursesTextpadConfirm)
SetGroupString(add_group,
add_string.replace("\n", "").rstrip())
expanded.append(add_group)
except CursesTextpadEsc:
RemoveAliases([add_group])
add_group = ""
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)]
pages = int(ceil(row_num / max_row))
box.refresh()
else:
curses.curs_set(0)
CursesPanel(screen, 3,
width - 6, (height // 2) - 1, 3, add_result,
normal_text, highlight_text)
curses.curs_set(0)
if (key_pressed == ord('e') or key_pressed == ord( if (key_pressed == ord('e') or key_pressed == ord(
'E') or key_pressed == curses.KEY_F4) and row_num != 0: 'E') or key_pressed == curses.KEY_F4) and row_num != 0:
edit_string = "" edit_string = ""
groups = [] while edit_string.rstrip() == "":
for a in conf.sections(): string_textpad = CursesTextpad(screen, 3, width - 8,
if conf.has_option(a, "group"): (height // 2) - 1, 4, "Enter new execution string:",
groups.append(a) (conf.get(strings[position - 1],
curses.curs_set(1) "exec_string") if conf.has_option(strings[position - 1],
if strings[position - 1] in groups: "exec_string") else ""),
while edit_string.rstrip() == "": normal_text, highlight_text)
string_textpad = CursesTextpad(screen, 3, width - 8, edit_string = string_textpad.edit(CursesTextpadConfirm)
(height // 2) - 1, 4, "Enter new aliases for existing group:", SetAliasString(strings[position - 1],
(conf.get(strings[position - 1], edit_string.replace("\n", "").rstrip())
"group") if conf.has_option(strings[position - 1], strings = conf.sections()
"group") else ""),
normal_text, highlight_text)
try:
edit_string = string_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
edit_string = conf.get(strings[position - 1],
"group") if conf.has_option(strings[position - 1],
"group") else ""
SetGroupString(strings[position - 1],
edit_string.replace("\n", "").rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings.append(" ")
pages = int(ceil(row_num / max_row))
box.refresh()
else:
while edit_string.rstrip() == "":
string_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4, "Enter new execution string:",
(conf.get(strings[position - 1].split()[0].strip(),
"exec_string") if conf.has_option(strings[position - 1].split()[0].strip(),
"exec_string") else ""),
normal_text, highlight_text)
try:
edit_string = string_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
edit_string = conf.get(strings[position - 1].split()[0].strip(),
"exec_string") if conf.has_option(strings[position - 1].split()[0].strip(),
"exec_string") else ""
SetAliasString(strings[position - 1].split()[0].strip(),
edit_string.replace("\n", "").rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
curses.curs_set(0)
if (key_pressed == ord('p') or key_pressed == ord( if (key_pressed == ord('p') or key_pressed == ord(
'P') or key_pressed == curses.KEY_F6) and row_num != 0: 'P') or key_pressed == curses.KEY_F6) and row_num != 0:
groups = [] set_password = ""
for a in conf.sections(): set_password = CursesPanel(screen, 4, width - 6,
if conf.has_option(a, "group"): (height // 2) - 1, 3,
groups.append(a) " Enter user password for sshpass and press 'enter':\n>",
if not strings[position - 1].split()[0].strip() in groups: normal_text, highlight_text, "password")
set_password = "" if not set_password == "":
set_password = CursesPanel(screen, 4, width - 6, SetPassword(strings[position - 1], set_password)
(height // 2) - 1, 3,
" Enter user password for sshpass and press 'enter':\n>",
normal_text, highlight_text, "password")
SetPassword(strings[position - 1].split()[0].strip(), set_password)
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
if (key_pressed == ord('r') or key_pressed == ord( if (key_pressed == ord('r') or key_pressed == ord(
'R') or key_pressed == curses.KEY_F8 or key_pressed == ( 'R') or key_pressed == curses.KEY_F8 or key_pressed == (
curses.KEY_DC)) and row_num != 0: curses.KEY_DC)) and row_num != 0:
@ -749,15 +423,14 @@ def CursesMain():
str(len(selected)), " selected aliases? (y/N)"])) str(len(selected)), " selected aliases? (y/N)"]))
else: else:
remove_confirm = ("".join(["Are you sure to remove '", remove_confirm = ("".join(["Are you sure to remove '",
strings[position - 1].split()[0].strip(), "' alias? (y/N)"])) strings[position - 1], "' alias? (y/N)"]))
selected.append(strings[position - 1].split()[0].strip()) selected.append(strings[position - 1])
remove_result = CursesPanel(screen, 4, width - 6, remove_result = CursesPanel(screen, 4, width - 6,
(height // 2) - 1, 3, remove_confirm, normal_text, (height // 2) - 1, 3, remove_confirm, normal_text,
highlight_text, "remove") highlight_text, "remove")
if remove_result == "confirm": if remove_result == "confirm":
RemoveAliases(selected) RemoveAliases(selected)
strings = GetTreeList(False, expanded) strings = conf.sections()
stringsfull = GetTreeList(True, expanded)
row_num = len(strings) row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)] selected_strings = [" " for i in range(0, row_num + 1)]
pages = int(ceil(row_num / max_row)) pages = int(ceil(row_num / max_row))
@ -771,78 +444,29 @@ def CursesMain():
if selected_strings[i] == "*": if selected_strings[i] == "*":
selected.append(strings[i - 1]) selected.append(strings[i - 1])
if not len(selected) > 0: if not len(selected) > 0:
selected.append(strings[position - 1].split()[0].strip()) selected.append(strings[position - 1])
curses.curs_set(1)
command_textpad = CursesTextpad(screen, 3, width - 8, command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4, (height // 2) - 1, 4,
"".join([ "".join([
"Enter specific command to execute with selected ", "Enter specific command to execute with selected ",
"alias/aliases:"] "alias/aliases:"]
), "", normal_text, highlight_text) ), "", normal_text, highlight_text)
try: command_string = command_textpad.edit(CursesTextpadConfirm)
command_string = command_textpad.edit(CursesTextpadConfirm) CursesConnect(screen, selected,
Connect(selected, command_string.replace("\n", "").rstrip(), command_string.replace("\n", "").rstrip())
False, screen) if (key_pressed == ord('k') or key_pressed == ord('K') or
except CursesTextpadEsc: key_pressed == (curses.KEY_F7)) and row_num != 0:
command_string = "" curses.endwin()
curses.curs_set(0) HoldConnection(strings[position - 1])
if (key_pressed == ord('t') or key_pressed == ord( if (key_pressed == ord("\n") or key_pressed == (
'T') or key_pressed == curses.KEY_F11) and row_num != 0: curses.KEY_F9)) and row_num != 0:
selected = [] selected = []
for i in range(1, row_num + 1): for i in range(1, row_num + 1):
if selected_strings[i] == "*": if selected_strings[i] == "*":
selected.append(strings[i - 1]) selected.append(strings[i - 1])
if not len(selected) > 0: if not len(selected) > 0:
selected.append(strings[position - 1].split()[0].strip()) selected.append(strings[position - 1])
curses.curs_set(1) CursesConnect(screen, selected)
command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4,
"".join([
"Enter specific command to execute with selected ",
"alias/aliases:"]
), "", normal_text, highlight_text)
try:
command_string = command_textpad.edit(CursesTextpadConfirm)
Connect(selected, command_string.replace("\n", "").rstrip(),
True, screen)
except CursesTextpadEsc:
command_string = ""
curses.curs_set(0)
if (key_pressed == ord('k') or key_pressed == ord('K') or
key_pressed == (curses.KEY_F7)) and row_num != 0:
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
if not strings[position - 1] in groups:
curses.endwin()
HoldConnection(strings[position - 1].split()[0].strip())
if (key_pressed == ord("\n") or key_pressed == (
curses.KEY_F9)) and row_num != 0:
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
if strings[position - 1] in groups:
if strings[position - 1] in expanded:
expanded.remove(strings[position - 1])
else:
expanded.append(strings[position - 1])
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
row_num = len(strings)
selected_strings.append(" ")
pages = int(ceil(row_num / max_row))
box.refresh()
else:
selected = []
for i in range(1, row_num + 1):
if selected_strings[i] == "*":
selected.append(strings[i - 1])
if not len(selected) > 0:
selected.append(strings[position - 1].split()[0].strip())
Connect(selected, False, False, screen)
selected_strings = [" " for i in range(0, row_num + 1)]
if (key_pressed == 32 or key_pressed == ( if (key_pressed == 32 or key_pressed == (
curses.KEY_IC)) and row_num != 0: curses.KEY_IC)) and row_num != 0:
if selected_strings[position] == ' ': if selected_strings[position] == ' ':
@ -866,8 +490,7 @@ def CursesMain():
page = page + 1 page = page + 1
position = 1 + (max_row * (page - 1)) position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_DOWN or key_pressed == ord( if key_pressed == curses.KEY_DOWN or key_pressed == ord(
'x') or key_pressed == ord('X') or key_pressed == ord( 'x') or key_pressed == ord('X'):
's') or key_pressed == ord('S'):
if page == 1: if page == 1:
if position < i: if position < i:
position = position + 1 position = position + 1
@ -885,8 +508,7 @@ def CursesMain():
page = page + 1 page = page + 1
position = 1 + (max_row * (page - 1)) position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_UP or key_pressed == ord( if key_pressed == curses.KEY_UP or key_pressed == ord(
'z') or key_pressed == ord('Z') or key_pressed == ord( 'z') or key_pressed == ord('Z'):
'w') or key_pressed == ord('W'):
if page == 1: if page == 1:
if position > 1: if position > 1:
position = position - 1 position = position - 1
@ -906,12 +528,6 @@ def CursesMain():
if page < pages: if page < pages:
page = page + 1 page = page + 1
position = (1 + (max_row * (page - 1))) position = (1 + (max_row * (page - 1)))
if key_pressed == curses.KEY_HOME:
page = 1
position = 1
if key_pressed == curses.KEY_END:
page = pages
position = row_num
box.erase() box.erase()
screen.border(0) screen.border(0)
box.border(0) box.border(0)
@ -921,7 +537,14 @@ def CursesMain():
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text) width - 6, highlight_text)
else: else:
exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]] if conf.has_option(strings[i - 1], "password"):
password = " [password]"
else:
password = ""
exec_string = ["[", selected_strings[i], "] ", str(i), " ",
strings[i - 1], " (", (conf.get(strings[i - 1],
"exec_string") if conf.has_option(strings[i - 1],
"exec_string") else ""), ")", password]
if (i + (max_row * (page - 1)) == (position + (max_row * (page - 1)))): if (i + (max_row * (page - 1)) == (position + (max_row * (page - 1)))):
box.addnstr(i - (max_row * (page - 1)), 2, "".join( box.addnstr(i - (max_row * (page - 1)), 2, "".join(
exec_string), width - 6, highlight_text) exec_string), width - 6, highlight_text)
@ -942,29 +565,10 @@ if __name__ == "__main__":
except NameError: except NameError:
pass pass
if not path.exists(conf_dir):
try:
from os import makedirs
makedirs(conf_dir)
except:
print("Can't make dir " + conf_dir)
exit()
conf = configparser.RawConfigParser() conf = configparser.RawConfigParser()
if not path.exists(conf_file): if not path.exists(conf_file):
try: open(conf_file, 'w')
open(conf_file, 'w') conf.read(conf_file)
except:
print("Can't make file at " + conf_dir)
exit()
try:
conf.read(conf_file)
except:
print("Error: can't read config file " + conf_file)
exit()
if len(argv) > 1: if len(argv) > 1:
try: try:
CMDOptions() CMDOptions()
@ -973,10 +577,6 @@ if __name__ == "__main__":
except configparser.Error: except configparser.Error:
print("Error: can't parse your config file, please check it manually or make new one") print("Error: can't parse your config file, please check it manually or make new one")
exit() exit()
except IOError:
print("Error: can't use your config file, please check permissionss of " + conf_file)
exit()
else: else:
try: try:
CursesMain() CursesMain()
@ -988,6 +588,3 @@ if __name__ == "__main__":
except curses.error: except curses.error:
CursesExit("".join(["Error: can't show some curses element, maybe ", CursesExit("".join(["Error: can't show some curses element, maybe ",
"your terminal is too small"])) "your terminal is too small"]))
except IOError:
CursesExit("".join(["Error: can't use your config file, please ",
"check permissionss of ", conf_file]))