"Fossies" - the Fresh Open Source Software Archive

Member "salt-3002.2/salt/client/ssh/shell.py" (18 Nov 2020, 14732 Bytes) of package /linux/misc/salt-3002.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "shell.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3002.1_vs_3002.2.

    1 """
    2 Manage transport commands via ssh
    3 """
    4 
    5 import logging
    6 import os
    7 import re
    8 import shlex
    9 import subprocess
   10 import sys
   11 import time
   12 
   13 import salt.defaults.exitcodes
   14 import salt.utils.json
   15 import salt.utils.nb_popen
   16 import salt.utils.vt
   17 
   18 log = logging.getLogger(__name__)
   19 
   20 SSH_PASSWORD_PROMPT_RE = re.compile(r"(?:.*)[Pp]assword(?: for .*)?:", re.M)
   21 KEY_VALID_RE = re.compile(r".*\(yes\/no\).*")
   22 SSH_PRIVATE_KEY_PASSWORD_PROMPT_RE = re.compile(r"Enter passphrase for key", re.M)
   23 
   24 # Keep these in sync with ./__init__.py
   25 RSTR = "_edbc7885e4f9aac9b83b35999b68d015148caf467b78fa39c05f669c0ff89878"
   26 RSTR_RE = re.compile(r"(?:^|\r?\n)" + RSTR + r"(?:\r?\n|$)")
   27 
   28 
   29 def gen_key(path):
   30     """
   31     Generate a key for use with salt-ssh
   32     """
   33     cmd = ["ssh-keygen", "-P", "", "-f", path, "-t", "rsa", "-q"]
   34     dirname = os.path.dirname(path)
   35     if dirname and not os.path.isdir(dirname):
   36         os.makedirs(os.path.dirname(path))
   37     subprocess.call(cmd)
   38 
   39 
   40 def gen_shell(opts, **kwargs):
   41     """
   42     Return the correct shell interface for the target system
   43     """
   44     if kwargs["winrm"]:
   45         try:
   46             import saltwinshell
   47 
   48             shell = saltwinshell.Shell(opts, **kwargs)
   49         except ImportError:
   50             log.error("The saltwinshell library is not available")
   51             sys.exit(salt.defaults.exitcodes.EX_GENERIC)
   52     else:
   53         shell = Shell(opts, **kwargs)
   54     return shell
   55 
   56 
   57 class Shell:
   58     """
   59     Create a shell connection object to encapsulate ssh executions
   60     """
   61 
   62     def __init__(
   63         self,
   64         opts,
   65         host,
   66         user=None,
   67         port=None,
   68         passwd=None,
   69         priv=None,
   70         priv_passwd=None,
   71         timeout=None,
   72         sudo=False,
   73         tty=False,
   74         mods=None,
   75         identities_only=False,
   76         sudo_user=None,
   77         remote_port_forwards=None,
   78         winrm=False,
   79         ssh_options=None,
   80     ):
   81         self.opts = opts
   82         # ssh <ipv6>, but scp [<ipv6]:/path
   83         self.host = host.strip("[]")
   84         self.user = user
   85         self.port = port
   86         self.passwd = str(passwd) if passwd else passwd
   87         self.priv = priv
   88         self.priv_passwd = priv_passwd
   89         self.timeout = timeout
   90         self.sudo = sudo
   91         self.tty = tty
   92         self.mods = mods
   93         self.identities_only = identities_only
   94         self.remote_port_forwards = remote_port_forwards
   95         self.ssh_options = "" if ssh_options is None else ssh_options
   96 
   97     def get_error(self, errstr):
   98         """
   99         Parse out an error and return a targeted error string
  100         """
  101         for line in errstr.split("\n"):
  102             if line.startswith("ssh:"):
  103                 return line
  104             if line.startswith("Pseudo-terminal"):
  105                 continue
  106             if "to the list of known hosts." in line:
  107                 continue
  108             return line
  109         return errstr
  110 
  111     def _key_opts(self):
  112         """
  113         Return options for the ssh command base for Salt to call
  114         """
  115         options = [
  116             "KbdInteractiveAuthentication=no",
  117         ]
  118         if self.passwd:
  119             options.append("PasswordAuthentication=yes")
  120         else:
  121             options.append("PasswordAuthentication=no")
  122         if self.opts.get("_ssh_version", (0,)) > (4, 9):
  123             options.append("GSSAPIAuthentication=no")
  124         options.append("ConnectTimeout={}".format(self.timeout))
  125         if self.opts.get("ignore_host_keys"):
  126             options.append("StrictHostKeyChecking=no")
  127         if self.opts.get("no_host_keys"):
  128             options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null"])
  129         known_hosts = self.opts.get("known_hosts_file")
  130         if known_hosts and os.path.isfile(known_hosts):
  131             options.append("UserKnownHostsFile={}".format(known_hosts))
  132         if self.port:
  133             options.append("Port={}".format(self.port))
  134         if self.priv and self.priv != "agent-forwarding":
  135             options.append("IdentityFile={}".format(self.priv))
  136         if self.user:
  137             options.append("User={}".format(self.user))
  138         if self.identities_only:
  139             options.append("IdentitiesOnly=yes")
  140 
  141         ret = []
  142         for option in options:
  143             ret.append("-o {} ".format(option))
  144         return "".join(ret)
  145 
  146     def _passwd_opts(self):
  147         """
  148         Return options to pass to ssh
  149         """
  150         # TODO ControlMaster does not work without ControlPath
  151         # user could take advantage of it if they set ControlPath in their
  152         # ssh config.  Also, ControlPersist not widely available.
  153         options = [
  154             "ControlMaster=auto",
  155             "StrictHostKeyChecking=no",
  156         ]
  157         if self.opts["_ssh_version"] > (4, 9):
  158             options.append("GSSAPIAuthentication=no")
  159         options.append("ConnectTimeout={}".format(self.timeout))
  160         if self.opts.get("ignore_host_keys"):
  161             options.append("StrictHostKeyChecking=no")
  162         if self.opts.get("no_host_keys"):
  163             options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null"])
  164 
  165         if self.passwd:
  166             options.extend(["PasswordAuthentication=yes", "PubkeyAuthentication=yes"])
  167         else:
  168             options.extend(
  169                 [
  170                     "PasswordAuthentication=no",
  171                     "PubkeyAuthentication=yes",
  172                     "KbdInteractiveAuthentication=no",
  173                     "ChallengeResponseAuthentication=no",
  174                     "BatchMode=yes",
  175                 ]
  176             )
  177         if self.port:
  178             options.append("Port={}".format(self.port))
  179         if self.user:
  180             options.append("User={}".format(self.user))
  181         if self.identities_only:
  182             options.append("IdentitiesOnly=yes")
  183 
  184         ret = []
  185         for option in options:
  186             ret.append("-o {} ".format(option))
  187         return "".join(ret)
  188 
  189     def _ssh_opts(self):
  190         return " ".join(["-o {}".format(opt) for opt in self.ssh_options])
  191 
  192     def _copy_id_str_old(self):
  193         """
  194         Return the string to execute ssh-copy-id
  195         """
  196         if self.passwd:
  197             # Using single quotes prevents shell expansion and
  198             # passwords containing '$'
  199             return "{} {} '{} -p {} {} {}@{}'".format(
  200                 "ssh-copy-id",
  201                 "-i {}.pub".format(self.priv),
  202                 self._passwd_opts(),
  203                 self.port,
  204                 self._ssh_opts(),
  205                 self.user,
  206                 self.host,
  207             )
  208         return None
  209 
  210     def _copy_id_str_new(self):
  211         """
  212         Since newer ssh-copy-id commands ingest option differently we need to
  213         have two commands
  214         """
  215         if self.passwd:
  216             # Using single quotes prevents shell expansion and
  217             # passwords containing '$'
  218             return "{} {} {} -p {} {} {}@{}".format(
  219                 "ssh-copy-id",
  220                 "-i {}.pub".format(self.priv),
  221                 self._passwd_opts(),
  222                 self.port,
  223                 self._ssh_opts(),
  224                 self.user,
  225                 self.host,
  226             )
  227         return None
  228 
  229     def copy_id(self):
  230         """
  231         Execute ssh-copy-id to plant the id file on the target
  232         """
  233         stdout, stderr, retcode = self._run_cmd(self._copy_id_str_old())
  234         if salt.defaults.exitcodes.EX_OK != retcode and "Usage" in stderr:
  235             stdout, stderr, retcode = self._run_cmd(self._copy_id_str_new())
  236         return stdout, stderr, retcode
  237 
  238     def _cmd_str(self, cmd, ssh="ssh"):
  239         """
  240         Return the cmd string to execute
  241         """
  242 
  243         # TODO: if tty, then our SSH_SHIM cannot be supplied from STDIN Will
  244         # need to deliver the SHIM to the remote host and execute it there
  245 
  246         command = [ssh]
  247         if ssh != "scp":
  248             command.append(self.host)
  249         if self.tty and ssh == "ssh":
  250             command.append("-t -t")
  251         if self.passwd or self.priv:
  252             command.append(self.priv and self._key_opts() or self._passwd_opts())
  253         if ssh != "scp" and self.remote_port_forwards:
  254             command.append(
  255                 " ".join(
  256                     [
  257                         "-R {}".format(item)
  258                         for item in self.remote_port_forwards.split(",")
  259                     ]
  260                 )
  261             )
  262         if self.ssh_options:
  263             command.append(self._ssh_opts())
  264 
  265         command.append(cmd)
  266 
  267         return " ".join(command)
  268 
  269     def _run_nb_cmd(self, cmd):
  270         """
  271         cmd iterator
  272         """
  273         try:
  274             proc = salt.utils.nb_popen.NonBlockingPopen(
  275                 self._split_cmd(cmd), stderr=subprocess.PIPE, stdout=subprocess.PIPE,
  276             )
  277             while True:
  278                 time.sleep(0.1)
  279                 out = proc.recv()
  280                 err = proc.recv_err()
  281                 rcode = proc.returncode
  282                 if out is None and err is None:
  283                     break
  284                 if err:
  285                     err = self.get_error(err)
  286                 yield out, err, rcode
  287         except Exception:  # pylint: disable=broad-except
  288             yield ("", "Unknown Error", None)
  289 
  290     def exec_nb_cmd(self, cmd):
  291         """
  292         Yield None until cmd finished
  293         """
  294         r_out = []
  295         r_err = []
  296         rcode = None
  297         cmd = self._cmd_str(cmd)
  298 
  299         logmsg = "Executing non-blocking command: {}".format(cmd)
  300         if self.passwd:
  301             logmsg = logmsg.replace(self.passwd, ("*" * 6))
  302         log.debug(logmsg)
  303 
  304         for out, err, rcode in self._run_nb_cmd(cmd):
  305             if out is not None:
  306                 r_out.append(out)
  307             if err is not None:
  308                 r_err.append(err)
  309             yield None, None, None
  310         yield "".join(r_out), "".join(r_err), rcode
  311 
  312     def exec_cmd(self, cmd):
  313         """
  314         Execute a remote command
  315         """
  316         cmd = self._cmd_str(cmd)
  317 
  318         logmsg = "Executing command: {}".format(cmd)
  319         if self.passwd:
  320             logmsg = logmsg.replace(self.passwd, ("*" * 6))
  321         if 'decode("base64")' in logmsg or "base64.b64decode(" in logmsg:
  322             log.debug("Executed SHIM command. Command logged to TRACE")
  323             log.trace(logmsg)
  324         else:
  325             log.debug(logmsg)
  326 
  327         ret = self._run_cmd(cmd)
  328         return ret
  329 
  330     def send(self, local, remote, makedirs=False):
  331         """
  332         scp a file or files to a remote system
  333         """
  334         if makedirs:
  335             self.exec_cmd("mkdir -p {}".format(os.path.dirname(remote)))
  336 
  337         # scp needs [<ipv6}
  338         host = self.host
  339         if ":" in host:
  340             host = "[{}]".format(host)
  341 
  342         cmd = "{} {}:{}".format(local, host, remote)
  343         cmd = self._cmd_str(cmd, ssh="scp")
  344 
  345         logmsg = "Executing command: {}".format(cmd)
  346         if self.passwd:
  347             logmsg = logmsg.replace(self.passwd, ("*" * 6))
  348         log.debug(logmsg)
  349 
  350         return self._run_cmd(cmd)
  351 
  352     def _split_cmd(self, cmd):
  353         """
  354         Split a command string so that it is suitable to pass to Popen without
  355         shell=True. This prevents shell injection attacks in the options passed
  356         to ssh or some other command.
  357         """
  358         try:
  359             ssh_part, cmd_part = cmd.split("/bin/sh")
  360         except ValueError:
  361             cmd_lst = shlex.split(cmd)
  362         else:
  363             cmd_lst = shlex.split(ssh_part)
  364             cmd_lst.append("/bin/sh {}".format(cmd_part))
  365         return cmd_lst
  366 
  367     def _run_cmd(self, cmd, key_accept=False, passwd_retries=3):
  368         """
  369         Execute a shell command via VT. This is blocking and assumes that ssh
  370         is being run
  371         """
  372         if not cmd:
  373             return "", "No command or passphrase", 245
  374 
  375         term = salt.utils.vt.Terminal(
  376             self._split_cmd(cmd),
  377             log_stdout=True,
  378             log_stdout_level="trace",
  379             log_stderr=True,
  380             log_stderr_level="trace",
  381             stream_stdout=False,
  382             stream_stderr=False,
  383         )
  384         sent_passwd = 0
  385         send_password = True
  386         ret_stdout = ""
  387         ret_stderr = ""
  388         old_stdout = ""
  389 
  390         try:
  391             while term.has_unread_data:
  392                 stdout, stderr = term.recv()
  393                 if stdout:
  394                     ret_stdout += stdout
  395                     buff = old_stdout + stdout
  396                 else:
  397                     buff = stdout
  398                 if stderr:
  399                     ret_stderr += stderr
  400                 if buff and RSTR_RE.search(buff):
  401                     # We're getting results back, don't try to send passwords
  402                     send_password = False
  403                 if buff and SSH_PRIVATE_KEY_PASSWORD_PROMPT_RE.search(buff):
  404                     if not self.priv_passwd:
  405                         return "", "Private key file need passphrase", 254
  406                     term.sendline(self.priv_passwd)
  407                     continue
  408                 if buff and SSH_PASSWORD_PROMPT_RE.search(buff) and send_password:
  409                     if not self.passwd:
  410                         return (
  411                             "",
  412                             "Permission denied, no authentication information",
  413                             254,
  414                         )
  415                     if sent_passwd < passwd_retries:
  416                         term.sendline(self.passwd)
  417                         sent_passwd += 1
  418                         continue
  419                     else:
  420                         # asking for a password, and we can't seem to send it
  421                         return "", "Password authentication failed", 254
  422                 elif buff and KEY_VALID_RE.search(buff):
  423                     if key_accept:
  424                         term.sendline("yes")
  425                         continue
  426                     else:
  427                         term.sendline("no")
  428                         ret_stdout = (
  429                             "The host key needs to be accepted, to "
  430                             "auto accept run salt-ssh with the -i "
  431                             "flag:\n{}"
  432                         ).format(stdout)
  433                         return ret_stdout, "", 254
  434                 elif buff and buff.endswith("_||ext_mods||_"):
  435                     mods_raw = (
  436                         salt.utils.json.dumps(self.mods, separators=(",", ":"))
  437                         + "|_E|0|"
  438                     )
  439                     term.sendline(mods_raw)
  440                 if stdout:
  441                     old_stdout = stdout
  442                 time.sleep(0.01)
  443             return ret_stdout, ret_stderr, term.exitstatus
  444         finally:
  445             term.close(terminate=True, kill=True)