Port to Python3

Handle bytes vs strings.
Fewer logging for interactive mode.
Prepare config file: move -f to -g, use -f for config.
This commit is contained in:
Johann Dreo 2016-12-02 19:30:40 +01:00
commit 71c89f1fe7

149
ereshkigal.py Executable file → Normal file
View file

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Ereshkigal is an AutoSSH tunnel monitor # Ereshkigal is an AutoSSH tunnel monitor
@ -110,11 +110,13 @@ class AutoSSHTunnelMonitor(list):
"""Gather and parse informations from the operating system""" """Gather and parse informations from the operating system"""
# autossh processes # autossh processes
autosshs = self.get_autossh_instances() autosshs = self.get_autossh_instances()
logging.debug("Autossh processes: %s" % autosshs) if autosshs:
logging.debug("Autossh processes: %s" % autosshs)
# ssh connections related to a tunnel # ssh connections related to a tunnel
connections = self.get_connections() connections = self.get_connections()
logging.debug("SSH connections related to a tunnel: %s" % connections) if connections:
logging.debug("SSH connections related to a tunnel: %s" % connections)
# Bind existing connections to autossh processes. # Bind existing connections to autossh processes.
# Thus the instance is a list of AutoSSHinstance instances, # Thus the instance is a list of AutoSSHinstance instances,
@ -161,30 +163,33 @@ class AutoSSHTunnelMonitor(list):
# list of processes with the "autossh" string # list of processes with the "autossh" string
status_list = [ps for ps in status[1].readlines() if "autossh" in ps] status_list = [ps for ps in status[1].readlines() if b"autossh" in ps]
logging.debug("Processes containing 'autossh': %s" % status_list) if status_list:
logging.debug("Processes containing 'autossh': %s" % status_list)
# split the process line if it contains a "-L" # split the process line if it contains a "-L"
cmds = [i.split() for i in status_list if '-L' in i] cmds = [i.split() for i in status_list if '-L' in i.decode()]
autosshs = [] autosshs = []
for cmd in cmds: for cmd in cmds:
logging.debug("Parse command: %s" % cmd)
# split the command in order to obtain arguments to the -L option # split the command in order to obtain arguments to the -L option
args = [i.strip('-').strip('-').strip('L') for i in cmd if '-L' in i][0].split(':') args = [i.strip(b'L-') for i in cmd if '-L' in i.decode()][0].split(b':')
logging.debug("Split around -L: %s" % args) logging.debug("Split around -L: %s" % args)
pid = int(cmd[0]) pid = int(cmd[0])
local_port = int(args[0]) local_port = int(args[0])
target_host = args[1] target_host = args[1].decode()
foreign_port = int(args[2]) foreign_port = int(args[2])
# find the hostname where the tunnel goes # find the hostname where the tunnel goes
via_host = "unknown" via_host = "unknown"
for i in xrange( len(cmd)-1,0,-1 ): for i in range( len(cmd)-1,0,-1 ):
if cmd[i][0] != '-': if chr(cmd[i][0]) != '-':
via_host = cmd[i] via_host = cmd[i].decode()
logging.debug("Via host: %s" % via_host)
break break
@ -197,12 +202,12 @@ class AutoSSHTunnelMonitor(list):
def parse_addr_port(self, addr_port): def parse_addr_port(self, addr_port):
if len(addr_port) == 2: # ipv4 if len(addr_port) == 2: # ipv4
addr = addr_port[0] addr = addr_port[0].decode()
logging.debug("IPv4 address: %s" % addr) logging.debug("IPv4 address: %s" % addr)
port = int(addr_port[1]) port = int(addr_port[1])
logging.debug("IPv4 port: %s" % port) logging.debug("IPv4 port: %s" % port)
else: # ipv6 else: # ipv6
addr = ":".join(addr_port[:-1]) addr = b":".join(addr_port[:-1]).decode()
logging.debug("IPv6 address: %s" % addr) logging.debug("IPv6 address: %s" % addr)
port = int(addr_port[-1]) port = int(addr_port[-1])
logging.debug("IPv6 port: %s" % port) logging.debug("IPv6 port: %s" % port)
@ -220,9 +225,9 @@ class AutoSSHTunnelMonitor(list):
status = (p.stdin, p.stdout, p.stderr) status = (p.stdin, p.stdout, p.stderr)
status_list = status[1].readlines() status_list = status[1].readlines()
logging.debug("%i active connections" % len(status_list)) # logging.debug("%i active connections" % len(status_list))
cons = [i.split() for i in status_list if 'ssh' in i] cons = [i.split() for i in status_list if b'ssh' in i]
tunnels = [] tunnels = []
@ -232,18 +237,18 @@ class AutoSSHTunnelMonitor(list):
# Proto Recv-Q Send-Q Adresse locale Adresse distante Etat PID/Program name # Proto Recv-Q Send-Q Adresse locale Adresse distante Etat PID/Program name
# local infos # local infos
local = con[3].split(':') local = con[3].split(b':')
logging.debug("local infos: %s" % local) logging.debug("local infos: %s" % local)
local_addr, local_port = self.parse_addr_port(local) local_addr, local_port = self.parse_addr_port(local)
# foreign infos # foreign infos
foreign = con[4].split(':') foreign = con[4].split(b':')
foreign_addr, foreign_port = self.parse_addr_port(foreign) foreign_addr, foreign_port = self.parse_addr_port(foreign)
status = con[5] status = con[5]
logging.debug("Connection status: %s" % status) logging.debug("Connection status: %s" % status)
sshpid = int( con[6].split('/')[0] ) sshpid = int( con[6].split(b'/')[0] )
logging.debug("SSH PID: %s" % sshpid) logging.debug("SSH PID: %s" % sshpid)
# ssh cmd line, got from /proc # ssh cmd line, got from /proc
@ -266,7 +271,7 @@ class AutoSSHTunnelMonitor(list):
f = open( ppidf ) f = open( ppidf )
# filter the parent pid # filter the parent pid
lpid = [i for i in f.readlines() if 'PPid' in i] lpid = [i for i in f.readlines() if 'PPid' in str(i)]
f.close() f.close()
logging.debug("PPid: %s" % lpid) logging.debug("PPid: %s" % lpid)
@ -282,6 +287,11 @@ class AutoSSHTunnelMonitor(list):
# exclude the port # exclude the port
content = f.readlines() content = f.readlines()
logging.debug("Cmd: %s" % content[0]) logging.debug("Cmd: %s" % content[0])
if not 'autossh' in content[0]:
logging.warning("Tunnel not managed by autossh, ignore.")
# FIXME display those hanging tunnels in some way.
continue
autohost = content[0].split(':')[1] autohost = content[0].split(':')[1]
f.close() f.close()
logging.debug("Parsed cmd without port: %s" % autohost) logging.debug("Parsed cmd without port: %s" % autohost)
@ -351,6 +361,7 @@ class monitorCurses:
# first update counter # first update counter
last_update = time.clock() last_update = time.clock()
last_state = None last_state = None
log_ticks = ""
# infinite loop # infinite loop
while(1): while(1):
@ -367,11 +378,13 @@ class monitorCurses:
state = "%s" % self.tm state = "%s" % self.tm
if state != last_state: if state != last_state:
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("----- Time of screen update: %s -----" % time.time()) logging.debug("----- Time of screen update: %s -----" % time.time())
logging.debug("State of tunnels:\n%s" % self.tm) logging.debug("State of tunnels:\n%s" % self.tm)
last_state = state last_state = state
else: else:
logging.debug('.') log_ticks += "."
kc = self.scr.getch() # keycode kc = self.scr.getch() # keycode
@ -387,11 +400,15 @@ class monitorCurses:
# Quit # Quit
if ch in 'Qq': if ch in 'Qq':
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: Q") logging.debug("Key pushed: Q")
break break
# Reload related autossh tunnels # Reload related autossh tunnels
elif ch in 'rR': elif ch in 'rR':
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: R") logging.debug("Key pushed: R")
# if a pid is selected # if a pid is selected
if self.cur_pid != -1: if self.cur_pid != -1:
@ -402,23 +419,32 @@ class monitorCurses:
# Kill autossh process # Kill autossh process
elif ch in 'kK': elif ch in 'kK':
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: K") logging.debug("Key pushed: K")
if self.cur_pid != -1: if self.cur_pid != -1:
# send a SIGKILL # send a SIGKILL
# the related process is stopped # the related process is stopped
# FIXME SIGTERM or SIGKILL ? # FIXME SIGTERM or SIGKILL ?
logging.debug("SIGKILL on PID: %i" % self.cur_pid) logging.debug("SIGKILL on PID: %i" % self.cur_pid)
os.kill( self.cur_pid, signal.SIGKILL ) try:
os.kill( self.cur_pid, signal.SIGKILL )
except OSError:
logging.error("No such process: %i" % self.cur_pid)
# Switch to show ssh connections # Switch to show ssh connections
# only available for root # only available for root
elif ch in 'tT' and os.getuid() == 0: elif ch in 'tT' and os.getuid() == 0:
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: T") logging.debug("Key pushed: T")
self.show_tunnels = not self.show_tunnels self.show_tunnels = not self.show_tunnels
# key pushed # key pushed
elif kc == curses.KEY_DOWN: elif kc == curses.KEY_DOWN:
logging.debug("Key pushed: downed") logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: down")
# if not the end of the list # if not the end of the list
if self.cur_line < len(self.tm)-1: if self.cur_line < len(self.tm)-1:
self.cur_line += 1 self.cur_line += 1
@ -427,10 +453,13 @@ class monitorCurses:
# key up # key up
elif kc == curses.KEY_UP: elif kc == curses.KEY_UP:
logging.debug("Waited: %s" % log_ticks)
log_ticks = ""
logging.debug("Key pushed: up") logging.debug("Key pushed: up")
if self.cur_line > -1: if self.cur_line > -1:
self.cur_line -= 1 self.cur_line -= 1
self.cur_pid = int(self.tm[self.cur_line]['pid']) if self.cur_line > 0:
self.cur_pid = int(self.tm[self.cur_line]['pid'])
else: else:
# do nothing and wait until the next refresh # do nothing and wait until the next refresh
@ -442,6 +471,8 @@ class monitorCurses:
# force a screen refresh # force a screen refresh
self.scr.refresh() self.scr.refresh()
# end of the loop
def display(self): def display(self):
"""Generate the interface screen""" """Generate the interface screen"""
@ -458,6 +489,8 @@ class monitorCurses:
# Second line # Second line
self.scr.addstr( "Active AutoSSH instances: ", curses.color_pair(6) ) self.scr.addstr( "Active AutoSSH instances: ", curses.color_pair(6) )
self.scr.addstr( str( len(self.tm) ), curses.color_pair(1) ) self.scr.addstr( str( len(self.tm) ), curses.color_pair(1) )
self.scr.addstr( " / Active connections: ", curses.color_pair(6) )
self.scr.addstr( str( sum([len(i['tunnels']) for i in self.tm]) ), curses.color_pair(1) )
self.scr.addstr( '\n', curses.color_pair(1) ) self.scr.addstr( '\n', curses.color_pair(1) )
self.scr.clrtoeol() self.scr.clrtoeol()
@ -476,7 +509,7 @@ class monitorCurses:
self.scr.clrtoeol() self.scr.clrtoeol()
# for each autossh processes available in the monitor # for each autossh processes available in the monitor
for l in xrange(len(self.tm)): for l in range(len(self.tm)):
# add a line for the l-th autossh process # add a line for the l-th autossh process
self.add_autossh( l ) self.add_autossh( l )
@ -494,6 +527,7 @@ class monitorCurses:
# for each connections related to te line-th autossh process # for each connections related to te line-th autossh process
for t in self.tm[line]['tunnels']: for t in self.tm[line]['tunnels']:
# FIXME fail if the screen's height is too small.
self.scr.addstr( '\n\t* ' ) self.scr.addstr( '\n\t* ' )
self.scr.addstr( str( t['ssh_pid'] ), curses.color_pair(colors['ssh_pid'] ) ) self.scr.addstr( str( t['ssh_pid'] ), curses.color_pair(colors['ssh_pid'] ) )
@ -511,10 +545,10 @@ class monitorCurses:
color = self.colors_ssh['status'] color = self.colors_ssh['status']
# if the connections is established # if the connections is established
# TODO avoid hard-coded constants # TODO avoid hard-coded constants
if t['status'] != 'ESTABLISHED': if t['status'].decode() != 'ESTABLISHED':
color = self.colors_ssh['status_out'] color = self.colors_ssh['status_out']
self.scr.addstr( str( t['status'] ), curses.color_pair( color ) ) self.scr.addstr( t['status'].decode(), curses.color_pair( color ) )
self.scr.clrtoeol() self.scr.clrtoeol()
@ -534,7 +568,7 @@ class monitorCurses:
for i in self.tm[line]['tunnels']: for i in self.tm[line]['tunnels']:
# add a vertical bar | # add a vertical bar |
# the color change according to the status of the connection # the color change according to the status of the connection
if i['status'] == 'ESTABLISHED': if i['status'].decode() == 'ESTABLISHED':
self.scr.addstr( '|', curses.color_pair(self.colors_ssh['status']) ) self.scr.addstr( '|', curses.color_pair(self.colors_ssh['status']) )
else: else:
self.scr.addstr( '|', curses.color_pair(self.colors_ssh['status_out']) ) self.scr.addstr( '|', curses.color_pair(self.colors_ssh['status_out']) )
@ -569,6 +603,7 @@ class monitorCurses:
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from optparse import OptionParser from optparse import OptionParser
import configparser
usage = """%prog [options] usage = """%prog [options]
A user interface to monitor existing SSH tunnel that are managed with autossh. A user interface to monitor existing SSH tunnel that are managed with autossh.
@ -596,37 +631,53 @@ if __name__ == "__main__":
parser.add_option('-l', '--log-level', choices=list(LOG_LEVELS), default='error', metavar='LEVEL', parser.add_option('-l', '--log-level', choices=list(LOG_LEVELS), default='error', metavar='LEVEL',
help='Log level (%s), default: %s.' % (", ".join(LOG_LEVELS), 'error') ) help='Log level (%s), default: %s.' % (", ".join(LOG_LEVELS), 'error') )
parser.add_option('-f', '--log-file', default=None, metavar='FILE', parser.add_option('-g', '--log-file', default=None, metavar='FILE',
help="Log to this file, default to standard output. \ help="Log to this file, default to standard output. \
If not set, asking for the curses interface automatically set logging to the \"ereshkigal.log\" file.") If you use the curses interface, you may want to set this to actually see logs.")
(options, args) = parser.parse_args() parser.add_option('-f', '--config-file', default=None, metavar='FILE',
print(options) help="Use this configuration file (default: '~/.ereshkigal.conf')")
(asked_for, args) = parser.parse_args()
logmsg = "----- Started Ereshkigal -----" logmsg = "----- Started Ereshkigal -----"
if options.log_file:
logfile = options.log_file if asked_for.log_file:
logging.basicConfig(filename=logfile, level=LOG_LEVELS[options.log_level]) logfile = asked_for.log_file
logging.basicConfig(filename=logfile, level=LOG_LEVELS[asked_for.log_level])
logging.debug(logmsg) logging.debug(logmsg)
logging.debug("Log in %s" % logfile) logging.debug("Log in %s" % logfile)
else: else:
if options.curses: if asked_for.curses:
logging.basicConfig(filename='ereshkigal.log', level=LOG_LEVELS[options.log_level]) logging.warning("It's a bad idea to log to stdout while in the curses interface.")
logging.debug(logmsg) logging.basicConfig(level=LOG_LEVELS[asked_for.log_level])
logging.debug("Log in ereshkigal.log") logging.debug(logmsg)
else: logging.debug("Log to stdout")
logging.basicConfig(level=LOG_LEVELS[options.log_level])
logging.debug(logmsg)
logging.debug("Log to stdout")
logging.debug("Asked for: %s" % options) logging.debug("Asked for: %s" % asked_for)
# unfortunately, options class has no __len__ method in python 2.4.3 (bug?) # unfortunately, asked_for class has no __len__ method in python 2.4.3 (bug?)
#if len(options) > 1: #if len(asked_for) > 1:
# parser.error("options are mutually exclusive") # parser.error("asked_for are mutually exclusive")
config = configparser.ConfigParser()
if asked_for.config_file:
try:
config.read(asked_for.config_file)
except configparser.MissingSectionHeaderError:
logging.error("'%s' contains no known configuration" % asked_for.config_file)
else:
try:
config.read('~/.ereshkigal.conf')
except configparser.MissingSectionHeaderError:
logging.error("'%s' contains no known configuration" % asked_for.config_file)
# Load autossh instances by sections: [expected]
# if config['expected']:
if options.curses:
if asked_for.curses:
logging.debug("Entering curses mode") logging.debug("Entering curses mode")
import curses import curses
import traceback import traceback
@ -671,7 +722,7 @@ if __name__ == "__main__":
traceback.print_exc() traceback.print_exc()
elif options.connections: elif asked_for.connections:
logging.debug("Entering connections mode") logging.debug("Entering connections mode")
tm = AutoSSHTunnelMonitor() tm = AutoSSHTunnelMonitor()
# do not call update() but only get connections # do not call update() but only get connections
@ -684,7 +735,7 @@ if __name__ == "__main__":
logging.error("Only root can see SSH tunnels connections.") logging.error("Only root can see SSH tunnels connections.")
elif options.autossh: elif asked_for.autossh:
logging.debug("Entering autossh mode") logging.debug("Entering autossh mode")
tm = AutoSSHTunnelMonitor() tm = AutoSSHTunnelMonitor()
# do not call update() bu only get autossh processes # do not call update() bu only get autossh processes