Exemple #1
0
    def _eval_prompt (self, data, new_prompt=None) :
        """
        This method will match the given data against the current prompt regex,
        and expects to find an integer as match -- which is then returned, along
        with all leading data, in a tuple
        """

        with self.pty_shell.rlock :

            try :

                prompt    = self.prompt
                prompt_re = self.prompt_re

                if  new_prompt :
                    prompt    = new_prompt
                    prompt_re = re.compile ("^(.*)%s\s*$" % prompt, re.DOTALL)


                result = None
                if  not data :
                    raise se.NoSuccess ("cannot not parse prompt (%s), invalid data (%s)" \
                                     % (prompt, data))

                result = prompt_re.match (data)

                if  not result :
                    self.logger.debug  ("could not parse prompt (%s) (%s)" % (prompt, data))
                    raise se.NoSuccess ("could not parse prompt (%s) (%s)" % (prompt, data))

                txt = result.group (1)
                ret = 0

                if  len (result.groups ()) != 2 :
                    if  new_prompt :
                        self.logger.warn   ("prompt does not capture exit value (%s)" % prompt)
                      # raise se.NoSuccess ("prompt does not capture exit value (%s)" % prompt)

                else :
                    try :
                        ret = int(result.group (2))
                    except ValueError :
                        # apparently, this is not an integer. Print a warning, and
                        # assume success -- the calling entity needs to evaluate the
                        # remainder...
                        ret = 0
                        self.logger.warn  ("prompt not suitable for error checks (%s)" % prompt)
                        txt += "\n%s" % result.group (2)

                # if that worked, we can permanently set new_prompt
                if  new_prompt :
                    self.set_prompt (new_prompt)

                return (ret, txt)

            except Exception as e :
                
                raise ptye.translate_exception (e, "Could not eval prompt")
Exemple #2
0
def translate_exception(e, msg=None):
    """
    In many cases, we should be able to roughly infer the exception cause
    from the error message -- this is centrally done in this method.  If
    possible, it will return a new exception with a more concise error
    message and appropriate exception type.
    """

    if not issubclass(e.__class__, se.SagaException):
        # we do not touch non-saga exceptions
        return e

    if not issubclass(e.__class__, se.NoSuccess):
        # this seems to have a specific cause already, leave it alone
        return e

    cmsg = e._plain_message

    if msg:
        cmsg = "%s (%s)" % (cmsg, msg)

    lmsg = cmsg.lower()

    if 'could not resolve hostname' in lmsg:
        e = se.BadParameter(cmsg)

    elif 'connection timed out' in lmsg:
        e = se.BadParameter(cmsg)

    elif 'connection refused' in lmsg:
        e = se.BadParameter(cmsg)

    elif 'auth' in lmsg:
        e = se.AuthorizationFailed(cmsg)

    elif 'pass' in lmsg:
        e = se.AuthenticationFailed(cmsg)

    elif 'ssh_exchange_identification' in lmsg:
        e = se.AuthenticationFailed(
            "too frequent login attempts, or sshd misconfiguration: %s" % cmsg)

    elif 'denied' in lmsg:
        e = se.PermissionDenied(cmsg)

    elif 'shared connection' in lmsg:
        e = se.NoSuccess("Insufficient system resources: %s" % cmsg)

    elif 'pty allocation' in lmsg:
        e = se.NoSuccess("Insufficient system resources: %s" % cmsg)

    elif 'Connection to master closed' in lmsg:
        e = se.NoSuccess(
            "Connection failed (insufficient system resources?): %s" % cmsg)

    return e
Exemple #3
0
    def _eval_prompt(self, data, new_prompt=None):
        """
        This method will match the given data against the current prompt regex,
        and expects to find an integer as match -- which is then returned, along
        with all leading data, in a tuple
        """

        with self.pty_shell.rlock:

            try:

                prompt = self.prompt
                prompt_re = self.prompt_re

                if new_prompt:
                    prompt = new_prompt
                    prompt_re = re.compile("^(.*)%s\s*$" % prompt, re.DOTALL)

                result = None
                if not data:
                    raise se.NoSuccess ("cannot not parse prompt (%s), invalid data (%s)" \
                                     % (prompt, data))

                result = prompt_re.match(data)

                if not result:
                    self.logger.debug("could not parse prompt (%s) (%s)" %
                                      (prompt, data))
                    raise se.NoSuccess("could not parse prompt (%s) (%s)" %
                                       (prompt, data))

                if len(result.groups()) != 2:
                    self.logger.debug(
                        "prompt does not capture exit value (%s)" % prompt)
                    raise se.NoSuccess(
                        "prompt does not capture exit value (%s)" % prompt)

                txt = result.group(1)
                ret = int(result.group(2))

                # if that worked, we can permanently set new_prompt
                if new_prompt:
                    self.set_prompt(new_prompt)

                return (ret, txt)

            except Exception as e:

                raise ptye.translate_exception(e, "Could not eval prompt")
Exemple #4
0
    def initialize(self):
        """ initialize the shell connection.  """

        with self.pty_shell.rlock:

            if self.initialized:
                self.logger.warn("initialization race")
                return

            if self.posix:
                # run a POSIX compatible shell, usually /bin/sh, in interactive mode
                # also, turn off tty echo
                command_shell = "exec /bin/sh -i"

                # use custom shell if so requested
                if 'shell' in self.options and self.options['shell']:
                    command_shell = "exec %s" % self.options['shell']
                    self.logger.info("custom  command shell: %s" %
                                     command_shell)

                self.logger.debug("running command shell:         %s" %
                                  command_shell)
                self.pty_shell.write(" stty -echo ; unset HISTFILE ; %s\n" %
                                     command_shell)

                # make sure this worked, and that we find the prompt. We use
                # a versatile prompt pattern to account for the custom shell case.
                _, out = self.find([self.prompt])

                # make sure this worked, and that we find the prompt. We use
                # a versatile prompt pattern to account for the custom shell case.
                try:
                    # set and register new prompt
                    self.run_async(" unset PROMPT_COMMAND ; " +
                                   " unset HISTFILE ; " +
                                   "PS1='PROMPT-$?->'; " + "PS2=''; " +
                                   "export PS1 PS2 2>&1 >/dev/null\n")
                    self.set_prompt(new_prompt="PROMPT-(\d+)->$")

                    self.logger.debug("got new shell prompt")

                except Exception as e:
                    raise se.NoSuccess(
                        "Shell startup on target host failed: %s" % e)

                try:
                    # got a command shell, finally!
                    # for local shells, we now change to the current working
                    # directory.  Remote shells will remain in the default pwd
                    # (usually $HOME).
                    if sumisc.host_is_local(surl.Url(self.url).host):
                        pwd = os.getcwd()
                        self.run_sync(' cd %s' % pwd)
                except Exception as e:
                    # We will ignore any errors.
                    self.logger.warning("local cd to %s failed" % pwd)

            self.pty_shell.flush()
            self.initialized = True
            self.finalized = False
Exemple #5
0
    def __init__(self, url, session=None, logger=None, opts=None, posix=True):

        if logger: self.logger = logger
        else: self.logger = rul.getLogger('saga', 'PTYShell')

        if session: self.session = session
        else: self.session = ss.Session(default=True)

        if opts: self.options = opts
        else: self.options = dict()

        self.logger.debug("PTYShell init %s" % self)

        self.url = url  # describes the shell to run
        self.posix = posix  # /bin/sh compatible?
        self.latency = 0.0  # set by factory
        self.cp_slave = None  # file copy channel

        self.initialized = False

        self.pty_id = PTYShell._pty_id
        PTYShell._pty_id += 1

        self.cfg = self.session.get_config('saga.utils.pty')

        # get prompt pattern from options, config, or use default
        if 'prompt_pattern' in self.options:
            self.prompt = self.options['prompt_pattern']
        elif 'prompt_pattern' in self.cfg:
            self.prompt = self.cfg['prompt_pattern'].get_value()
        else:
            self.prompt = DEFAULT_PROMPT

        self.prompt_re = re.compile("^(.*?)%s" % self.prompt, re.DOTALL)
        self.logger.info("PTY prompt pattern: %s" % self.prompt)

        # we need a local dir for file staging caches.  At this point we use
        # $HOME, but should make this configurable (FIXME)
        self.base = os.environ['HOME'] + '/.saga/adaptors/shell/'

        try:
            os.makedirs(self.base)

        except OSError as e:
            if e.errno == errno.EEXIST and os.path.isdir(self.base):
                pass
            else:
                raise se.NoSuccess("could not create staging dir: %s" % e)

        self.factory = supsf.PTYShellFactory()
        self.pty_info = self.factory.initialize(self.url,
                                                self.session,
                                                self.prompt,
                                                self.logger,
                                                posix=self.posix)
        self.pty_shell = self.factory.run_shell(self.pty_info)

        self._trace('init : %s' % self.pty_shell.command)

        self.initialize()
Exemple #6
0
    def re_raise(self):
        """ :todo: describe me

            :note: if job failed, that will re-raise an exception describing 
                   why, if that exists.  Otherwise, the call does nothing.
        """

        if self.state == FAILED:
            raise se.NoSuccess("job stderr: %s" % self.get_stderr_string())
        else:
            return
Exemple #7
0
    def get_exception(self, ttype=None):
        """ :todo: describe me

            :note: if job failed, that will get an exception describing 
                   why, if that exists.  Otherwise, the call returns None.
        """

        if self.state == FAILED:
            return se.NoSuccess("job stderr: %s" % self.get_stderr_string())
        else:
            return None
Exemple #8
0
    def __init__(self, url, session=None, logger=None, init=None, opts={}):

        # print 'new pty shell to %s' % url

        if logger: self.logger = logger
        else: self.logger = rul.getLogger('saga', 'PTYShell')

        if session: self.session = session
        else: self.session = ss.Session(default=True)

        self.logger.debug("PTYShell init %s" % self)

        self.url = url  # describes the shell to run
        self.init = init  # call after reconnect
        self.opts = opts  # options...
        self.latency = 0.0  # set by factory
        self.cp_slave = None  # file copy channel

        self.initialized = False

        # get prompt pattern from config
        self.cfg = self.session.get_config('saga.utils.pty')

        if 'prompt_pattern' in self.cfg:
            self.prompt = self.cfg['prompt_pattern'].get_value()
            self.prompt_re = re.compile("^(.*?)%s" % self.prompt, re.DOTALL)
        else:
            self.prompt = "[\$#%>\]]\s*$"
            self.prompt_re = re.compile("^(.*?)%s" % self.prompt, re.DOTALL)

        self.logger.info("PTY prompt pattern: %s" % self.prompt)

        # we need a local dir for file staging caches.  At this point we use
        # $HOME, but should make this configurable (FIXME)
        self.base = os.environ['HOME'] + '/.saga/adaptors/shell/'

        try:
            os.makedirs(self.base)

        except OSError as e:
            if e.errno == errno.EEXIST and os.path.isdir(self.base):
                pass
            else:
                raise se.NoSuccess("could not create staging dir: %s" % e)

        self.factory = supsf.PTYShellFactory()
        self.pty_info = self.factory.initialize(self.url, self.session,
                                                self.prompt, self.logger)
        self.pty_shell = self.factory.run_shell(self.pty_info)

        self.initialize()
Exemple #9
0
    def initialize (self) :
        """ initialize the shell connection.  """

        with self.pty_shell.rlock :

            if  self.initialized :
                self.logger.warn ("initialization race")
                return


            if  self.posix :
                # run a POSIX compatible shell, usually /bin/sh, in interactive mode
                # also, turn off tty echo
                command_shell = "exec /bin/sh -i"

                # use custom shell if so requested
                if  'shell' in self.options and self.options['shell'] :
                    command_shell = "exec %s" % self.options['shell']
                    self.logger.info ("custom  command shell: %s" % command_shell)


                self.logger.debug    ("running command shell: %s" % command_shell)
                self.pty_shell.write (" stty -echo ; %s\n" % command_shell)

                # make sure this worked, and that we find the prompt. We use
                # a versatile prompt pattern to account for the custom shell case.
                _, out = self.find ([self.prompt])

                # make sure this worked, and that we find the prompt. We use
                # a versatile prompt pattern to account for the custom shell case.
                try :
                    # set and register new prompt
                    self.run_async  ( " set HISTFILE=$HOME/.saga_history;"
                                    + " PS1='PROMPT-$?->';"
                                    + " PS2='';"
                                    + " PROMPT_COMMAND='';"
                                    + " export PS1 PS2 2>&1 >/dev/null;"
                                    + " cd $HOME 2>&1 >/dev/null\n")
                    self.set_prompt (new_prompt="PROMPT-(\d+)->$")

                    self.logger.debug ("got new shell prompt")

                except Exception as e :
                    raise se.NoSuccess ("Shell startup on target host failed: %s" % e)

            # got a command shell, finally!
            self.pty_shell.flush ()
            self.initialized = True
            self.finalized   = False
Exemple #10
0
    def get_adaptor(self, adaptor_name):
        ''' Return the adaptor module's ``Adaptor`` class for the given adaptor
            name.

            This method is used if adaptor or API object implementation need to
            interact with other adaptors.
        '''

        for ctype in self._adaptor_registry.keys():
            for schema in self._adaptor_registry[ctype].keys():
                for info in self._adaptor_registry[ctype][schema]:
                    if (info['adaptor_name'] == adaptor_name):
                        return info['adaptor_instance']

        error_msg = "No adaptor named '%s' found" % adaptor_name
        self._logger.error(error_msg)
        raise se.NoSuccess(error_msg)
Exemple #11
0
    def write(self, data, nolog=False):
        """
        This method will repeatedly attempt to push the given data into the
        child's stdin pipe, until it succeeds to write all data.
        """

        with self.rlock:

            if not self.alive(recover=False):
                raise ptye.translate_exception (se.NoSuccess ("cannot write to dead process (%s)" \
                                                % self.cache[-256:]))

            try:

                log = self._hide_data(data, nolog)
                log = log.replace('\n', '\\n')
                log = log.replace('\r', '')
                if len(log) > _DEBUG_MAX:
                    self.logger.debug ("write: [%5d] [%5d] (%s ... %s)" \
                                    % (self.parent_in, len(data), log[:30], log[-30:]))
                else:
                    self.logger.debug ("write: [%5d] [%5d] (%s)" \
                                    % (self.parent_in, len(data), log))

                # attempt to write forever -- until we succeeed
                while data:

                    # check if the pty pipe is ready for data
                    _, wlist, _ = select.select([], [self.parent_in], [],
                                                _POLLDELAY)

                    for f in wlist:

                        # write will report the number of written bytes
                        size = os.write(f, data)

                        # otherwise, truncate by written data, and try again
                        data = data[size:]

                        if data:
                            self.logger.info("write: [%5d] [%5d]" % (f, size))

            except Exception as e:
                raise ptye.translate_exception(
                    e, "write to process failed (%s)" % e)
Exemple #12
0
    def initialize(self):

        with self.rlock:

            # already initialized?
            if self.child:
                self.logger.warn("initialization race: %s" %
                                 ' '.join(self.command))
                return

            self.logger.info("running: %s" % ' '.join(self.command))

            # create the child
            try:
                self.child, self.child_fd = pty.fork()
            except Exception as e:
                raise se.NoSuccess ("Could not run (%s): %s" \
                                 % (' '.join (self.command), e))

            if not self.child:
                # this is the child

                try:
                    # all I/O set up, have a pty (*fingers crossed*), lift-off!
                    os.execvpe(self.command[0], self.command, os.environ)

                except OSError as e:
                    self.logger.error ("Could not execute (%s): %s" \
                                    % (' '.join (self.command), e))
                    sys.exit(-1)

            else:
                # this is the parent
                new = termios.tcgetattr(self.child_fd)
                new[3] = new[3] & ~termios.ECHO

                termios.tcsetattr(self.child_fd, termios.TCSANOW, new)

                self.parent_in = self.child_fd
                self.parent_out = self.child_fd
Exemple #13
0
def raise_return_exception(method, spectype, result):

    if no_return_check:
        # disable this!
        return

    stack = extract_stack()
    for f in stack:
        if 'saga/utils/signatures.py' in f[0]:
            break
        frame = f

    msg = "\nSignature Mismatch\n"
    msg += "  in function   : %s\n" % (frame[2])
    msg += "  in file       : %s +%s\n" % (frame[0], frame[1])
    msg += "  on line       : %s\n" % (frame[3])
    msg += "  method        : %s\n" % (method.__name__)
    msg += "  returned type : %s\n" % (type_name(result))
    msg += "  instead  of   : %s\n" % (type_name(spectype))
    msg += "  This is an internal SAGA-Python error!"

    raise se.NoSuccess(msg)
Exemple #14
0
    def read(self, size=0, timeout=0, _force=False):
        """ 
        read some data from the child.  By default, the method reads whatever is
        available on the next read, up to _CHUNKSIZE, but other read sizes can
        be specified.  
        
        The method will return whatever data it has at timeout::
        
          timeout == 0 : return the content of the first successful read, with
                         whatever data up to 'size' have been found.
          timeout <  0 : return after first read attempt, even if no data have 
                         been available.

        If no data are found, the method returns an empty string (not None).

        This method will not fill the cache, but will just read whatever data it
        needs (FIXME).

        Note: the returned lines do *not* get '\\\\r' stripped.
        """

        with self.rlock:

            found_eof = False

            try:
                # start the timeout timer right now.  Note that even if timeout is
                # short, and child.poll is slow, we will nevertheless attempt at least
                # one read...
                start = time.time()
                ret = ""

                # read until we have enough data, or hit timeout ceiling...
                while True:

                    # first, lets see if we still have data in the cache we can return
                    if len(self.cache):

                        if not size:
                            ret = self.cache
                            self.cache = ""
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

                        # we don't even need all of the cache
                        elif size <= len(self.cache):
                            ret = self.cache[:size]
                            self.cache = self.cache[size:]
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

                    # otherwise we need to read some more data, right?
                    # idle wait 'til the next data chunk arrives, or 'til _POLLDELAY
                    rlist, _, _ = select.select([self.parent_out], [], [],
                                                _POLLDELAY)

                    # got some data?
                    for f in rlist:
                        # read whatever we still need

                        readsize = _CHUNKSIZE
                        if size:
                            readsize = size - len(ret)

                        buf = os.read(f, _CHUNKSIZE)

                        if len(buf) == 0 and sys.platform == 'darwin':
                            self.logger.debug("read : MacOS EOF")
                            self.finalize()
                            found_eof = True
                            raise se.NoSuccess("unexpected EOF (%s)" %
                                               self.tail)

                        self.cache += buf.replace('\r', '')
                        log = buf.replace('\r', '')
                        log = log.replace('\n', '\\n')
                        # print "buf: --%s--" % buf
                        # print "log: --%s--" % log
                        if len(log) > _DEBUG_MAX:
                            self.logger.debug ("read : [%5d] [%5d] (%s ... %s)" \
                                            % (f, len(log), log[:30], log[-30:]))
                        else:
                            self.logger.debug ("read : [%5d] [%5d] (%s)" \
                                            % (f, len(log), log))
                        # for c in log :
                        #     print '%s' % c

                    # lets see if we still got any data in the cache we can return
                    if len(self.cache):

                        if not size:
                            ret = self.cache
                            self.cache = ""
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

                        # we don't even need all of the cache
                        elif size <= len(self.cache):
                            ret = self.cache[:size]
                            self.cache = self.cache[size:]
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

                    # at this point, we do not have sufficient data -- only
                    # return on timeout

                    if timeout == 0:
                        # only return if we have data
                        if len(self.cache):
                            ret = self.cache
                            self.cache = ""
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

                    elif timeout < 0:
                        # return of we have data or not
                        ret = self.cache
                        self.cache = ""
                        self.tail += ret
                        self.tail = self.tail[-256:]
                        return ret

                    else:  # timeout > 0
                        # return if timeout is reached
                        now = time.time()
                        if (now - start) > timeout:
                            ret = self.cache
                            self.cache = ""
                            self.tail += ret
                            self.tail = self.tail[-256:]
                            return ret

            except Exception as e:

                if found_eof:
                    raise e

                raise se.NoSuccess ("read from process failed '%s' : (%s)" \
                                 % (e, self.tail))
Exemple #15
0
    def wait(self):
        """ 
        blocks forever until the child finishes on its own, or is getting
        killed.  

        Actully, we might just as well try to figure out what is going on on the
        remote end of things -- so we read the pipe until the child dies...
        """

        output = ""
        # yes, for ever and ever...
        while True:
            try:
                output += self.read()
            except:
                break

        # yes, for ever and ever...
        while True:

            if not self.child:
                # this was quick ;-)
                return output

            # we need to lock, as the SIGCHLD will only arrive once
            with self.rlock:
                # hey, kiddo, whats up?
                try:
                    wpid, wstat = os.waitpid(self.child, 0)

                except OSError as e:

                    if e.errno == errno.ECHILD:

                        # child disappeared
                        self.exit_code = None
                        self.exit_signal = None
                        self.finalize()
                        return output

                    # no idea what happened -- it is likely bad
                    raise se.NoSuccess("waitpid failed on wait")

                # did we get a note about child termination?
                if 0 == wpid:

                    # nope, all is well - carry on
                    continue

                # Yes, we got a note.
                # Well, maybe the child fooled us and is just playing dead?
                if os.WIFSTOPPED   (wstat) or \
                   os.WIFCONTINUED (wstat)    :
                    # we don't care if someone stopped/resumed the child -- that is up
                    # to higher powers.  For our purposes, the child is alive.  Ha!
                    continue

                # not stopped, poor thing... - soooo, what happened??  But hey,
                # either way, its dead -- make sure it stays dead, to avoid
                # zombie apocalypse...
                self.child = None
                self.finalize(wstat=wstat)

                return output
Exemple #16
0
    def _initialize_pty(self, pty_shell, info, posix=None):

        # posix: only for posix shells we use prompt triggers.  sftp for example
        # does not deal well with triggers (no printf).

        with self.rlock:

            # import pprint
            # pprint.pprint (info)

            shell_pass = info['pass']
            key_pass = info['key_pass']
            prompt = info['prompt']
            logger = info['logger']
            latency = info['latency']
            timeout = info['ssh_timeout']

            pty_shell.latency = latency

            if posix == None:
                posix = info['posix']

            # if we did not see a decent prompt within 'delay' time, something
            # went wrong.  Try to prompt a prompt (duh!)  Delay should be
            # minimum 0.1 second (to avoid flooding of local shells), and at
            # maximum 1 second (to keep startup time reasonable)
            # most one second.  We try to get within that range with 10*latency.
            delay = min(1.0, max(0.1, 10 * latency))

            try:
                prompt_patterns = [
                    "[Pp]assword:\s*$",  # password   prompt
                    "Enter passphrase for .*:\s*$",  # passphrase prompt
                    "Token_Response.*:\s*$",  # passtoken  prompt
                    "Enter PASSCODE:$",  # RSA SecureID
                    "want to continue connecting",  # hostkey confirmation
                    ".*HELLO_\\d+_SAGA$",  # prompt detection helper
                    prompt
                ]  # greedy native shell prompt

                # use a very aggressive, but portable prompt setting scheme.
                # Error messages may appear for tcsh and others.  Excuse
                # non-posix shells
                if posix:
                    pty_shell.write(
                        " export PROMPT_COMMAND='' PS1='$' ; set prompt='$'\n")

                # find a prompt
                n, match = pty_shell.find(prompt_patterns, delay)

                # this loop will run until we finally find the shell prompt, or
                # if we think we have tried enough and give up.  On success
                # we'll try to set a different prompt, and when we found that,
                # too, we exit the loop and are be ready to running shell

                # commands.
                retries = 0
                retry_trigger = True
                used_trigger = False
                found_trigger = ""
                time_start = time.time()

                while True:

                    # --------------------------------------------------------------
                    if n == None:

                        # we found none of the prompts, yet, and need to try
                        # again.  But to avoid hanging on invalid prompts, we
                        # print 'HELLO_x_SAGA', and search for that one, too.
                        # We actually do 'printf HELLO_%d_SAGA x' so that the
                        # pattern only appears in the result, not in the
                        # command...

                        if time.time() - time_start > timeout:
                            raise se.NoSuccess(
                                "Could not detect shell prompt (timeout)")

                        # make sure we retry a finite time...
                        retries += 1

                        if not retry_trigger:
                            # just waiting for the *right* trigger or prompt,
                            # don't need new ones...
                            continue

                        if posix:
                            # use a very aggressive, but portable prompt setting scheme
                            pty_shell.write(
                                " export PROMPT_COMMAND='' PS1='$' > /dev/null 2>&1 || set prompt='$'\n"
                            )
                            pty_shell.write(
                                " printf 'HELLO_%%d_SAGA\\n' %d\n" % retries)
                            used_trigger = True

                        # FIXME:  consider better timeout
                        n, match = pty_shell.find(prompt_patterns, delay)

                    # --------------------------------------------------------------
                    elif n == 0:
                        logger.info("got password prompt")
                        if not shell_pass:
                            raise se.AuthenticationFailed ("prompted for unknown password (%s)" \
                                                          % match)

                        pty_shell.write("%s\n" % shell_pass, nolog=True)
                        n, match = pty_shell.find(prompt_patterns, delay)

                    # --------------------------------------------------------------
                    elif n == 1:
                        logger.info("got passphrase prompt : %s" % match)

                        start = string.find(match, "'", 0)
                        end = string.find(match, "'", start + 1)

                        if start == -1 or end == -1:
                            raise se.AuthenticationFailed(
                                "could not extract key name (%s)" % match)

                        key = match[start + 1:end]

                        if not key in key_pass:
                            raise se.AuthenticationFailed ("prompted for unknown key password (%s)" \
                                                          % key)

                        pty_shell.write("%s\n" % key_pass[key], nolog=True)
                        n, match = pty_shell.find(prompt_patterns, delay)

                    # --------------------------------------------------------------
                    elif n == 2 or n == 3:
                        logger.info("got token prompt")
                        import getpass
                        token = getpass.getpass("enter token: ")
                        pty_shell.write("%s\n" % token.strip(), nolog=True)
                        n, match = pty_shell.find(prompt_patterns, delay)

                    # --------------------------------------------------------------
                    elif n == 4:
                        logger.info("got hostkey prompt")
                        pty_shell.write("yes\n")
                        n, match = pty_shell.find(prompt_patterns, delay)

                    # --------------------------------------------------------------
                    elif n == 5:

                        # one of the trigger commands got through -- we can now
                        # hope to find the prompt (or the next trigger...)
                        logger.debug("got shell prompt trigger (%s) (%s)" %
                                     (n, match))

                        found_trigger = match
                        retry_trigger = False
                        n, match = pty_shell.find(prompt_patterns, delay)
                        continue

                    # --------------------------------------------------------------
                    elif n == 6:

                        logger.debug("got initial shell prompt (%s) (%s)" %
                                     (n, match))

                        if retries:
                            if used_trigger:
                                # we already sent triggers -- so this match is only
                                # useful if saw the *correct* shell prompt trigger
                                # first
                                trigger = "HELLO_%d_SAGA" % retries

                                if not trigger in found_trigger:

                                    logger.debug ("waiting for prompt trigger %s: (%s) (%s)" \
                                               % (trigger, n, match))
                                    # but more retries won't help...
                                    retry_trigger = False
                                    attempts = 0
                                    n = None

                                    while not n:

                                        attempts += 1
                                        n, match = pty_shell.find(
                                            prompt_patterns, delay)

                                        if not n:
                                            if attempts == 1:
                                                if posix:
                                                    pty_shell.write(
                                                        " printf 'HELLO_%%d_SAGA\\n' %d\n"
                                                        % retries)

                                            if attempts > 100:
                                                raise se.NoSuccess(
                                                    "Could not detect shell prompt (timeout)"
                                                )

                                    continue

                        logger.debug("Got initial shell prompt (%s) (%s)" %
                                     (n, match))
                        # we are done waiting for a prompt
                        break

            except Exception as e:
                logger.exception(e)
                raise ptye.translate_exception(e)
Exemple #17
0
    def run_copy_to (self, src, tgt, cp_flags="") :
        """ 
        This initiates a slave copy connection.   Src is interpreted as local
        path, tgt as path on the remote host.

        Now, this is ugly when over sftp: sftp supports recursive copy, and
        wildcards, all right -- but for recursive copies, it wants the target
        dir to exist -- so, we have to check if the local src is a  dir, and if
        so, we first create the target before the copy.  Worse, for wildcards we
        have to do a local expansion, and the to do the same for each entry...
        """

        with self.pty_shell.rlock :

            self._trace ("copy  to  : %s -> %s" % (src, tgt))
            self.pty_shell.flush ()

            info = self.pty_info
            repl = dict ({'src'      : src, 
                          'tgt'      : tgt,
                          'cp_flags' : '' # cp_flags # TODO: needs to be "translated" for specific backend
                          }.items () + info.items ())

            # at this point, we do have a valid, living master
            s_cmd = info['scripts'][info['copy_mode']]['copy_to']    % repl
            s_in  = info['scripts'][info['copy_mode']]['copy_to_in'] % repl
            posix = info['scripts'][info['copy_mode']]['copy_is_posix']

            if  not s_in :
                # this code path does not use an interactive shell for copy --
                # so the above s_cmd is all we want to run, really.  We get
                # do not use the chached cp_slave in this case, but just run the
                # command.  We do not have a list of transferred files though,
                # yet -- that should be parsed from the proc output.

                cp_proc = supp.PTYProcess (s_cmd)
                out = cp_proc.wait ()
                if  cp_proc.exit_code :
                    raise ptye.translate_exception (se.NoSuccess ("file copy failed: %s" % out))

                return list()


            # this code path uses an interactive shell to transfer files, of
            # some form, such as sftp.  Get the shell cp_slave from cache, and
            # run the actual copy command.
            if  not self.cp_slave :
                self._trace ("get cp slave")
                self.cp_slave = self.factory.get_cp_slave (s_cmd, info, posix)

            self.cp_slave.flush ()
            if  'sftp' in s_cmd :
                # prepare target dirs for recursive copy, if needed
                import glob
                src_list = glob.glob (src)
                for s in src_list :
                    if  os.path.isdir (s) :
                        prep = "mkdir %s/%s\n" % (tgt, os.path.basename (s))
                        # TODO: this doesn't deal with multiple levels of creation

                        self.cp_slave.flush()
                        self.cp_slave.write("%s\n" % prep)
                        self.cp_slave.find(['[\$\>\]]\s*$'], -1)
                        # TODO: check return values

                if cp_flags == sfs.CREATE_PARENTS and os.path.split(tgt)[0]:
                    # TODO: this needs to be numeric and checking the flag
                    prep = "mkdir %s\n" % os.path.dirname(tgt)
                    # TODO: this doesn't deal with multiple levels of creation

                    self.cp_slave.flush()
                    self.cp_slave.write("%s\n" % prep)
                    self.cp_slave.find(['[\$\>\]]\s*$'], -1)
                    # TODO: check return values

            self.cp_slave.flush()
            _ = self.cp_slave.write("%s\n" % s_in)
            _, out = self.cp_slave.find(['[\$\>\]]\s*$'], -1)

            # FIXME: we don't really get exit codes from copy
            # if  self.cp_slave.exit_code != 0 :
            #     raise se.NoSuccess._log (info['logger'], "file copy failed: %s" % str(out))

            if 'Invalid flag' in out :
                raise se.NoSuccess._log (info['logger'], "sftp version not supported (%s)" % str(out))

            if 'No such file or directory' in out :
                raise se.DoesNotExist._log (info['logger'], "file copy failed: %s" % str(out))

            if 'is not a directory' in out :
                raise se.BadParameter._log (info['logger'], "File copy failed: %s" % str(out))

            if  'sftp' in s_cmd :
                if 'not found' in out :
                    raise se.BadParameter._log (info['logger'], "file copy failed: %s" % out)


            # we interpret the first word on the line as name of src file -- we
            # will return a list of those
            lines = out.split ('\n')
            files = []

            for line in lines :

                elems = line.split (' ', 2)

                if  elems :

                    f = elems[0]

                    # remove quotes
                    if  f :

                        if  f[ 0] in ["'", '"', '`'] : f = f[1:  ]
                        if  f[-1] in ["'", '"', '`'] : f = f[ :-1]

                    # ignore empty lines
                    if  f :

                        files.append (f)

            info['logger'].debug ("copy done: %s" % files)

            return files
Exemple #18
0
    def set_prompt (self, new_prompt) :
        """
        :type  new_prompt:  string 
        :param new_prompt:  a regular expression matching the shell prompt

        The new_prompt regex is expected to be a regular expression with one set
        of catching brackets, which MUST return the previous command's exit
        status.  This method will send a newline to the client, and expects to
        find the prompt with the exit value '0'.

        As a side effect, this method will discard all previous data on the pty,
        thus effectively flushing the pty output.  

        By encoding the exit value in the command prompt, we safe one roundtrip.
        The prompt on Posix compliant shells can be set, for example, via::

          PS1='PROMPT-$?->'; export PS1

        The newline in the example above allows to nicely anchor the regular
        expression, which would look like::

          PROMPT-(\d+)->$

        The regex is compiled with 're.DOTALL', so the dot character matches
        all characters, including line breaks.  Be careful not to match more
        than the exact prompt -- otherwise, a prompt search will swallow stdout
        data.  For example, the following regex::

          PROMPT-(.+)->$

        would capture arbitrary strings, and would thus match *all* of::

          PROMPT-0->ls
          data/ info
          PROMPT-0->

        and thus swallow the ls output...

        Note that the string match *before* the prompt regex is non-gready -- if
        the output contains multiple occurrences of the prompt, only the match
        up to the first occurence is returned.
        """

        def escape (txt) :
            pat = re.compile(r'\x1b[^m]*m')
            return pat.sub ('', txt)


        with self.pty_shell.rlock :

            old_prompt     = self.prompt
            self.prompt    = new_prompt
            self.prompt_re = re.compile ("^(.*?)%s\s*$" % self.prompt, re.DOTALL)

            retries  = 0
            triggers = 0

            while True :

                try :
                    # make sure we have a non-zero waiting delay (default to
                    # 1 second)
                    delay = 10 * self.latency
                    if  not delay :
                        delay = 1.0

                    # FIXME: how do we know that _PTY_TIMOUT suffices?  In particular if
                    # we actually need to flush...
                    fret, match = self.pty_shell.find ([self.prompt], delay)

                    if  fret == None :
                    
                        retries += 1
                        if  retries > 10 :
                            self.prompt = old_prompt
                            raise se.BadParameter ("Cannot use new prompt, parsing failed (10 retries)")

                        self.pty_shell.write ("\n")
                        self.logger.debug  ("sent prompt trigger again (%d)" % retries)
                        triggers += 1
                        continue


                    # found a match -- lets see if this is working now...
                    ret, _ = self._eval_prompt (match)

                    if  ret != 0 :
                        self.prompt = old_prompt
                        raise se.BadParameter ("could not parse exit value (%s)" \
                                            % match)

                    # prompt looks valid...
                    break

                except Exception as e :
                    self.prompt = old_prompt
                    raise ptye.translate_exception (e, "Could not set shell prompt")


            # got a valid prompt -- but we have to sync the output again in
            # those cases where we had to use triggers to actually get the
            # prompt
            if triggers > 0 :
                self.run_async (' printf "SYNCHRONIZE_PROMPT\n"')

                # FIXME: better timout value?
                fret, match = self.pty_shell.find (["SYNCHRONIZE_PROMPT"], timeout=10.0)  

                if  fret == None :
                    # not find prompt after blocking?  BAD!  Restart the shell
                    self.finalize (kill_pty=True)
                    raise se.NoSuccess ("Could not synchronize prompt detection")

                self.find_prompt ()
Exemple #19
0
    def run_copy_from (self, src, tgt, cp_flags="") :
        """ 
        This initiates a slave copy connection.   Src is interpreted as path on
        the remote host, tgt as local path.

        We have to do the same mkdir trick as for the run_copy_to, but here we
        need to expand wildcards on the *remote* side :/
        """

        with self.pty_shell.rlock :

            self._trace ("copy  from: %s -> %s" % (src, tgt))
            self.pty_shell.flush ()

            info = self.pty_info
            repl = dict ({'src'      : src, 
                          'tgt'      : tgt, 
                          'cp_flags' : cp_flags}.items() + info.items ())

            # at this point, we do have a valid, living master
            s_cmd = info['scripts'][info['copy_mode']]['copy_from']    % repl
            s_in  = info['scripts'][info['copy_mode']]['copy_from_in'] % repl
            posix = info['scripts'][info['copy_mode']]['copy_is_posix']

            if  not s_in :
                # this code path does not use an interactive shell for copy --
                # so the above s_cmd is all we want to run, really.  We get
                # do not use the chached cp_slave in this case, but just run the
                # command.  We do not have a list of transferred files though,
                # yet -- that should be parsed from the proc output.
                cp_proc = supp.PTYProcess (s_cmd)
                cp_proc.wait ()
                if  cp_proc.exit_code :
                    raise ptye.translate_exception (se.NoSuccess ("file copy failed: exit code %s" % cp_proc.exit_code))

                return list()

            if  not self.cp_slave :
                self._trace ("get cp slave")
                self.cp_slave = self.factory.get_cp_slave (s_cmd, info, posix)

            self.cp_slave.flush ()
            prep = ""

            if  'sftp' in s_cmd :
                # prepare target dirs for recursive copy, if needed
                self.cp_slave.write (" ls %s\n" % src)
                _, out = self.cp_slave.find (["^sftp> "], -1)

                src_list = out[1].split('\n')

                for s in src_list :
                    if  os.path.isdir (s) :
                        prep += "lmkdir %s/%s\n" % (tgt, os.path.basename (s))


            self.cp_slave.flush ()
            _      = self.cp_slave.write    ("%s%s\n" % (prep, s_in))
            _, out = self.cp_slave.find     (['[\$\>\]] *$'], -1)

            # FIXME: we don't really get exit codes from copy
          # if  self.cp_slave.exit_code != 0 :
          #     raise se.NoSuccess._log (info['logger'], "file copy failed: %s" % out)

            if 'Invalid flag' in out :
                raise se.NoSuccess._log (info['logger'], "sftp version not supported (%s)" % out)

            if 'No such file or directory' in out :
                raise se.DoesNotExist._log (info['logger'], "file copy failed: %s" % out)

            if 'is not a directory' in out :
                raise se.BadParameter._log (info['logger'], "file copy failed: %s" % out)

            if  'sftp' in s_cmd :
                if 'not found' in out :
                    raise se.BadParameter._log (info['logger'], "file copy failed: %s" % out)


            # we run copy with -v, so get a list of files which have been copied
            # -- we parse that list and return it.  we interpret the *second*
            # word on the line as name of src file.
            lines = out.split ('\n')
            files = []

            for line in lines :

                elems = line.split (' ', 3)
                
                if  elems and len(elems) > 1 and elems[0] == 'Fetching' :

                    f = elems[1]

                    # remove quotes
                    if  f :

                        if  f[ 0] in ["'", '"', '`']  :  f = f[1:  ]
                        if  f[-1] in ["'", '"', '`']  :  f = f[ :-1]

                    # ignore empty lines
                    if  f :
                        files.append (f)

            info['logger'].debug ("copy done: %s" % files)

            return files