diff --git a/runserver.py b/runserver.py index 7ce4d40df17440f10b58001637c1eed3a080d55d..438defffcbe1058522be71648ff2480ca80df6b5 100644 --- a/runserver.py +++ b/runserver.py @@ -1,6 +1,6 @@ from tes import app import logging -#logging.basicConfig(filename="tes.log",format="%(asctime)s %(levelname)s:%(process)s: %(message)s") +logging.basicConfig(filename="tes.log",format="%(asctime)s %(levelname)s:%(process)s: %(message)s") logger=logging.getLogger() logger.setLevel(logging.DEBUG) diff --git a/tes/apiendpoints.py b/tes/apiendpoints.py index 5794cca9d33296320bdffa3fad70b48685d7deba..6ecf59f32905210095fb8a7c64e16a70e4e8fffe 100644 --- a/tes/apiendpoints.py +++ b/tes/apiendpoints.py @@ -84,6 +84,7 @@ class SSHAgent(Resource): try: sshsess = SSHSession.get_sshsession() if sshsess.socket == None: + logger.debug('trying to get the agent contents, but the agent isn\'t started yet') return [] # The agent hasn't even been started yet return sshsess.get_cert_contents() except Exception as e: @@ -129,11 +130,18 @@ def get_conn_params(): appparams = json.loads(appstr) except: appparams = {} + try: + appinstancestr = request.args.get('appinstance') + appinstanceparams = json.loads(appinstancestr) + except: + appinstanceparams = {} params = {} params['identity'] = identityparams params['interface'] = interfaceparams params['app'] = appparams + params['appinstance'] = appinstanceparams + params.update(interfaceparams) params['user'] = identityparams['username'] @@ -146,14 +154,6 @@ def get_conn_params(): return params -def get_app_params(): - """ - Return the parameters for the application retrieved from the Session - """ - keys = ['startscript', 'paramscmd', 'client','localbind'] - appstr = request.args.get('app') - returnvalue = json.loads(appstr) - return returnvalue class TunnelstatEP(Resource): """ @@ -204,6 +204,7 @@ class JobStat(Resource): """ import logging logger = logging.getLogger() + logger.debug('enteringing JobStat') try: params = get_conn_params() except: @@ -225,7 +226,10 @@ class JobStat(Resource): cmd = params['interface']['statcmd'] except (TypeError, KeyError) as e: flask_restful.abort(400, message="stat: definition of batch interface incomplete") + logger.debug('ssh sess socket is {}'.format(sshsess.socket)) + try: + logger.debug('attempting ssh execute {} {} {}'.format(host,user,cmd)) res = Ssh.execute(sshsess, host=host, user=user, cmd=cmd) except SshAgentException as e: logger.error(e) @@ -240,6 +244,7 @@ class JobStat(Resource): for j in jobs: j['identity'] = params['identity'] + logger.debug('leaving jobstat gracefully') return jobs except Exception as e: import traceback @@ -247,12 +252,46 @@ class JobStat(Resource): logger.error(traceback.format_exc()) flask_resful.abort(400, message=e) +class MkDir(Resource): + def post(self): + import logging + logger = logging.getLogger() + data = request.get_json() + logger.debug('mkdir data') + logger.debug(data) + params = get_conn_params() + sshsess = SSHSession.get_sshsession() + Ssh.sftpmkdir(sshsess, host=params['identity']['site']['host'], + user=params['identity']['username'], path=params['path'],name=data['name']) + + return + class DirList(Resource): def get(self): params = get_conn_params() sshsess = SSHSession.get_sshsession() - dirls = Ssh.sftpls(sshsess, host=params['identity']['site']['host'], - user=params['identity']['username'], path=params['path'],changepath=params['cd']) + site = params['identity']['site'] + if 'dtnport' in site: + sshport = site['dtnport'] + else: + sshport = "22" + path = params['path'] + cd = params['cd'] + if path == "": + path = "." + if cd == "": + cd = "." + print('sshport is {}'.format(sshport)) + if 'lscmd' in site and site['lscmd'] is not None and site['lscmd'] is not "": + res = Ssh.execute(sshsess, host=params['identity']['site']['host'], user=params['identity']['username'], + sshport=sshport, + cmd="{} {} {}".format(site['lscmd'],path,cd)) + dirls = json.loads(res['stdout'].decode()) + print(dirls) + else: + + dirls = Ssh.sftpls(sshsess, host=params['identity']['site']['host'], + user=params['identity']['username'], path=params['path'],changepath=params['cd'], sshport=sshport) return dirls class JobCancel(Resource): @@ -278,6 +317,7 @@ class JobSubmit(Resource): """ def post(self): """starting a job is a post, since it changes the state of the backend""" + print("entering jobsubmit") import logging logger=logging.getLogger() params = get_conn_params() @@ -288,7 +328,7 @@ class JobSubmit(Resource): try: script = data['app']['startscript'].format(**data) except: - flask_restful.abort(400,message='Incomplete job information was passed to the backend.') + flask_restful.abort(400, message='Incomplete job information was passed to the backend.') logger.debug('script formated to {}'.format(script)) res = Ssh.execute(sshsess, host=params['identity']['site']['host'], user=params['identity']['username'], @@ -311,62 +351,46 @@ def gen_authtok(): return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(16)) -class JobConnect(Resource): - """ - endpoints for connecting to an existing JobCancel - """ - - def create_tunnel(self, username, loginhost, appparams, batchhost, firewall, data): +class AppUrl(Resource): + def get(self): import logging - logger=logging.getLogger() - logger.debug('entering JobConnect.create_tunnel {} {}'.format(username,batchhost)) - connectparams = {} + logger = logging.getLogger() + appdef = json.loads(request.args.get('app')) + logger.debug('appdef {}'.format(appdef)) + inst = json.loads(request.args.get('appinst')) + logger.debug('appinst {}'.format(inst)) + url = "{}{}".format(app.config['TWSPROXY'],appdef['client']['redir'].format(**inst)) + return url + +class AppInstance(Resource): + def get(self, username, loginhost, batchhost): + """Run a command to get things like password and port number + command is passed as a query string""" sshsess = SSHSession.get_sshsession() + paramscmd = request.args.get('cmd') + import logging + logger = logging.getLogger() + logger.debug('getting appinstance {} {} {}'.format(username,loginhost,batchhost)) + logger.debug('ssh sess socket is {}'.format(sshsess.socket)) + cmd = 'ssh -o StrictHostKeyChecking=no -o CheckHostIP=no {batchhost} '.format(batchhost=batchhost) + paramscmd + res = Ssh.execute(sshsess, host=loginhost, user=username, cmd=cmd) + try: + data = json.loads(res['stdout'].decode()) + except json.decoder.JSONDecodeError as e: + raise AppParamsException(res['stderr']+res['stdout']) + if len(res['stderr']) > 0: + raise AppParamsException(res['stderr']) + if 'error' in data: + raise AppParamsException(data['error']) + if not (res['stderr'] == '' or res['stderr'] is None or res['stderr'] == b''): + flask_restful.abort(400, message=res['stderr'].decode()) + return data - if 'paramscmd' in appparams and appparams['paramscmd'] is not None: - connectparams['batchhost'] = batchhost - paramcmd = 'ssh -o StrictHostKeyChecking=no -o CheckHostIP=no {batchhost} '.format(batchhost=batchhost) + appparams['paramscmd'] - logger.debug('JobCreate.create_tunnel: using ssh to extract connection parameters') - res = Ssh.execute(sshsess, host=loginhost, user=username, cmd=paramcmd.format(data)) - try: - data = json.loads(res['stdout']) - except json.decoder.JSONDecodeError as e: - raise AppParamsException(res['stderr']+res['stdout']) - if len(res['stderr']) > 0: - raise AppParamsException(res['stderr']) - if 'error' in data: - raise AppParamsException(data['error']) - try: - connectparams.update(json.loads(res['stdout'])) - except json.decoder.JSONDecodeError as e: - logger.error(res['stdout']) - logger.error(res['stderr']) - if not (res['stderr'] == '' or res['stderr'] is None or res['stderr'] == b''): - flask_restful.abort(400, message=res['stderr'].decode()) - - if self.validate_connect_params(connectparams, username, loginhost): - authtok = gen_authtok() - logger.debug('JobCreate.create_tunnel: creating a tunnel for authtok {}'.format(authtok)) - tunnelport, pids = Ssh.tunnel(sshsess, port=connectparams['port'], - batchhost=connectparams['batchhost'], - user=username, host=loginhost, - internalfirewall=firewall, - localbind=appparams['localbind'], authtok=authtok) - - connectparams['localtunnelport'] = tunnelport - connectparams['authtok'] = authtok - logger.debug('JobCreate.create_tunnel: created a tunnel for authtok {} port {}'.format(authtok,tunnelport)) - else: - raise AppParamsException("connection parameters invalid {} {} {}".format(connectparams,username,loginhost)) - return connectparams - - def validate_connect_params(self, connectparams, username, host): - if not 'port' in connectparams: - return False - if not 'batchhost' in connectparams: - return False +class CreateTunnel(Resource): + @staticmethod + def validate_connect_params(port, username, host, batchhost): try: - intport = int(connectparams['port']) + intport = int(port) except Exception as e: return False if ' ' in username or '\n' in username: # This really needs more validation @@ -375,52 +399,45 @@ class JobConnect(Resource): return False return True - def get(self, jobid, batchhost): + def post(self,username,loginhost,batchhost): """ - Connecting to a job is a get operation (i.e. it does not make modifications) + Create a tunnel using established keys + parameters for the tunnel (host username port etc) + will be passed in the body """ import logging - logger=logging.getLogger() - logger.debug('entering JobConnect.get for jobid {} {}'.format(jobid,batchhost)) - params = get_conn_params() - appparams = get_app_params() + logger = logging.getLogger() + logger.debug("Createing tunnel") + logger.debug("recieved data {}".format(request.data)) data = request.get_json() try: - connectparams = self.create_tunnel(params['identity']['username'],params['identity']['site']['host'], - appparams, batchhost, params['interface']['internalfirewall'], - data) - except AppParamsException as e: - return make_response(render_template('appparams.html.j2',data = "{}".format(e))) - logger.debug('JobConnect.get tunnels created, moving to redirect'.format(jobid,batchhost)) - return self.connect(appparams, connectparams) - - def connect(self, appparams, connectparams): - """ - perform the connection either by forking a local client or returning a redirect - """ - import subprocess - import logging - import json - logger=logging.getLogger() - if 'cmd' in appparams['client'] and appparams['client']['cmd'] is not None: - # We need for fork a local process such as vncviewer or a terminal - # We may need a wrapper for local processes to find the correct - # process on all OS - cmdlist = [] - for cmdarg in appparams['client']['cmd']: - cmdlist.append(cmdarg.format(**connectparams)) - app_process = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - elif 'redir' in appparams['client'] and appparams['client']['redir'] is not None: - twsproxy = app.config['TWSPROXY'] - data = json.dumps({'location': twsproxy+appparams['client']['redir']. - format(**connectparams) }) - - response = make_response(data) - response.mime_type = 'application/json' - response.set_cookie('twsproxyauth', connectparams['authtok']) - logger.debug('JobConnect.connect: connecting via redirect with cookie authtok set to {}'.format(connectparams['authtok'])) - return response - return "Connecting with cmd {}".format(cmdlist) + port = data['port'] + except KeyError as missingdata: + raise AppParamsException("missing value for {}".format(missingdata)) + if not CreateTunnel.validate_connect_params(port, username, loginhost, batchhost): + raise AppParamsException("Invalid value: {} {} {} {}". + format(username, loginhost, batchhost, port)) + if 'internalfirewall' in data: + firewall = data['internalfirewall'] + else: + firewall = False + if 'localbind' in data: + localbind = data['localbind'] + else: + localbind = True + sshsess = SSHSession.get_sshsession() + authtok = gen_authtok() + # logger.debug('JobCreate.create_tunnel: creating a tunnel for authtok {}'.format(authtok)) + Ssh.tunnel(sshsess, port=port, batchhost=batchhost, + user=username, host=loginhost, + internalfirewall=firewall, + localbind=localbind, authtok=authtok) + response = make_response("") + response.mime_type = 'application/json' + response.set_cookie('twsproxyauth', authtok) + logger.debug('JobConnect.connect: connecting via redirect with cookie authtok set to {}'.format(authtok)) + return response + api.add_resource(TunnelstatEP, '/tunnelstat/<string:authtok>') @@ -428,8 +445,9 @@ api.add_resource(GetCert, '/getcert') api.add_resource(JobStat, '/stat') api.add_resource(JobCancel, '/cancel/<int:jobid>') api.add_resource(JobSubmit, '/submit') -api.add_resource(JobConnect, '/connect/<int:jobid>/<string:batchhost>') -# api.add_resource(SessionTest,'/sesstest') -#api.add_resource(StartAgent,'/startagent') +api.add_resource(CreateTunnel, '/createtunnel/<string:username>/<string:loginhost>/<string:batchhost>') +api.add_resource(AppInstance, '/appinstance/<string:username>/<string:loginhost>/<string:batchhost>') +api.add_resource(AppUrl, '/appurl') api.add_resource(SSHAgent,'/sshagent') api.add_resource(DirList,'/ls') +api.add_resource(MkDir,'/mkdir') diff --git a/tes/sshwrapper/__init__.py b/tes/sshwrapper/__init__.py index 9f43fcc8df0acc9601dce2851e91640a3a4c0558..3b9f6b9c2873078c802d4f3cbac6a9ee32b3826e 100644 --- a/tes/sshwrapper/__init__.py +++ b/tes/sshwrapper/__init__.py @@ -68,7 +68,7 @@ class Ssh: pass @staticmethod - def get_ctrl_master_socket(sess, host, user): + def get_ctrl_master_socket(sess, host, user, sshport): """ Returns the socket for the control master process Opens it if needed. @@ -87,16 +87,18 @@ class Ssh: sshcmd = ["ssh", '-o', 'StrictHostKeyChecking=no', "-S", ctrlsocket, "-M", '-o', 'ControlPersist=10m', - '-N','-l', user, host] + '-p', sshport, '-N','-l', user, host] env = os.environ.copy() if sess.socket is None: raise SshAgentException("No ssh-agent yet") env['SSH_AUTH_SOCK'] = sess.socket - + logger.debug("socket not found, starting new master") ctrl_p = subprocess.Popen(sshcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=None, env=env) - stdout = ctrl_p.communicate() + logger.debug("master spawned, attempt communicate") + stdout, stderr = ctrl_p.communicate() logger.debug('communicate on the control port complete') sess.pids.append(ctrl_p.pid) @@ -108,6 +110,7 @@ class Ssh: logger.error(ctrl_p.stderr.read()) raise SshCtrlException() if not stat.S_ISSOCK(mode): + logger.error(ctrl_p.stderr.read()) raise SshCtrlException() return ctrlsocket @@ -119,7 +122,7 @@ class Ssh: pwd = None import dateutil.parser for l in output.splitlines(): - + fields = l.split(None,8) fd = {} if len(fields) == 9: @@ -138,9 +141,51 @@ class Ssh: pwd = l[len(remotestr):].rstrip() return ({'pwd':pwd,'files':rv}) + @staticmethod + def sftpmkdir(sess, host, user, path, name, sshport): + """ + Use sftp to run mkdir. + """ + import os + import logging + logger = logging.getLogger() + env = os.environ.copy() + if path is None: + path = "." + if sess.socket is None: + raise SshAgentException("No ssh-agent yet") + env['SSH_AUTH_SOCK'] = sess.socket + Ssh.validate_username(user) + Ssh.validate_hostname(host) + ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user, sshport) + exec_p = subprocess.Popen(['sftp', '-b', '-','-o', 'Stricthostkeychecking=no', + '-P', sshport, '-o', 'ControlPath={}'.format(ctrlsocket), + '{}@{}'.format(user, host)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=subprocess.PIPE, env=env) + + sftpcmd="cd {}\n mkdir \"{}\"\n".format(path,name) + + (stdout, stderr) = exec_p.communicate(sftpcmd.encode()) + if stderr is not None: + if not (stderr == b''): + logger.error('sftp failure') + logger.error(stdout.decode()) + logger.error(stderr.decode()) + if ('Couldn\'t create directory: Failure' in stderr.decode()): + return + if ('Couldn\'t canonicalize: No such file or directory' in stderr.decode()): + logger.error('can\'t change to that directory') + return + if ('Permission denied' in stderr.decode()): + logger.error('can\'t change to that directory') + return + + raise SshCtrlException() + return @staticmethod - def sftpls(sess, host, user, path=".",changepath="."): + def sftpls(sess, host, user, sshport, path=".",changepath="."): """ Use sftp to run an ls on the given path. Return the directory listing (as a list) and the cwd @@ -154,13 +199,13 @@ class Ssh: env['SSH_AUTH_SOCK'] = sess.socket Ssh.validate_username(user) Ssh.validate_hostname(host) - ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user) + ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user, sshport) if (path is None or path == ""): path="." if (changepath is None or changepath == ""): changepath="." exec_p = subprocess.Popen(['sftp', '-b', '-','-o', 'Stricthostkeychecking=no', - '-o', 'ControlPath={}'.format(ctrlsocket), + '-P', sshport, '-o', 'ControlPath={}'.format(ctrlsocket), '{}@{}'.format(user, host)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=env) @@ -173,10 +218,10 @@ class Ssh: logger.error('sftp failure') if ('Couldn\'t canonicalize: No such file or directory' in stderr.decode()): logger.error('can\'t change to that directory') - return Ssh.sftpls(sess,host,user,path,changepath='.') + return Ssh.sftpls(sess,host,user,sshport, path,changepath='.') if ('Permission denied' in stderr.decode()): logger.error('can\'t change to that directory') - return Ssh.sftpls(sess,host,user,path,changepath='.') + return Ssh.sftpls(sess,host,user,sshport, path,changepath='.') logger.error(stderr.decode()) logger.error(('Permission denied' in stderr.decode())) @@ -188,13 +233,15 @@ class Ssh: @staticmethod - def execute(sess, host, user, cmd, stdin=None): + def execute(sess, host, user, cmd, stdin=None, sshport="22"): """ execute the command cmd on the host via ssh # assume the environment is already setup with an # SSH_AUTH_SOCK that allows login """ import os + import logging + logger = logging.getLogger() if cmd is None and stdin is None: return {'stdout': b'', 'stderr': b'No command given to execute'} env = os.environ.copy() @@ -204,10 +251,12 @@ class Ssh: Ssh.validate_username(user) Ssh.validate_hostname(host) Ssh.validate_command(cmd) - ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user) + logger.debug('getting ctrl_master') + ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user, sshport) + logger.debug('ssh.execute: got ctrlsocket {}'.format(ctrlsocket)) exec_p = subprocess.Popen(['ssh', '-A', '-o', 'Stricthostkeychecking=no', '-S', ctrlsocket, - '-l', user, host, cmd], + '-p', sshport, '-l', user, host, cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=env) if stdin is not None: @@ -217,7 +266,7 @@ class Ssh: return {'stdout':stdout, 'stderr':stderr} @staticmethod - def tunnel(sess, port, batchhost, user, host, internalfirewall = True, localbind = True, authtok = None): + def tunnel(sess, port, batchhost, user, host, internalfirewall = True, localbind = True, authtok = None, sshport="22"): """ the ProxyCommand is used if the server we run on the batch host is only addressable on localhost @@ -237,7 +286,7 @@ class Ssh: Ssh.validate_hostname(batchhost) Ssh.validate_username(user) Ssh.validate_hostname(host) - ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user) + ctrlsocket = Ssh.get_ctrl_master_socket(sess, host, user, sshport) logger.debug('Ssh.tunnel, got ctrlsocket') localport = Ssh.get_free_port() @@ -248,7 +297,7 @@ class Ssh: '-L', '{localport}:{batchhost}:{port}'. format(port=port, localport=localport, batchhost=batchhost), '-O', 'forward', '-S', ctrlsocket, - '-l', user, host] + '-p', sshport, '-l', user, host] else: # Create an ssh tunnel to the batch node using a proxycommand. # The proxy command should utilise @@ -261,7 +310,7 @@ class Ssh: '-L', '{localport}:localhost:{port}'. format(port=port, localport=localport), '-o', "ProxyCommand={}".format(proxycmd), - '-l', user, batchhost] + '-p', sshport, '-l', user, batchhost] logger.debug('Ssh.tunnel: attempting command {}'.format(sshcmd)) tunnel_p = subprocess.Popen(sshcmd, env=env) diff --git a/tes/tunnelstat/__init__.py b/tes/tunnelstat/__init__.py index c6b9b5b64083a9d1692d098ae940287390a55be0..6cb7d19e2a97a4261353ec7b6f6caf02a5876a9a 100644 --- a/tes/tunnelstat/__init__.py +++ b/tes/tunnelstat/__init__.py @@ -41,7 +41,9 @@ class SSHSession: import logging logger = logging.getLogger() if self.socket is None: + logger.debug('adding a cert, start the agent first') self.start_agent() + logger.debug('agent socket is now {}'.format(self.socket)) keyf = tempfile.NamedTemporaryFile(mode='w',delete=False) keyname = keyf.name keyf.write(key) @@ -160,6 +162,7 @@ class SSHSession: session['sshsessid'] = sshsessid if sshsessid not in sshsessions: sshsessions[sshsessid] = SSHSession() + return sshsessions[sshsessid] @staticmethod