shell.py (salt-3002.1) | : | shell.py (salt-3002.2) | ||
---|---|---|---|---|
# -*- coding: utf-8 -*- | ||||
""" | """ | |||
Manage transport commands via ssh | Manage transport commands via ssh | |||
""" | """ | |||
from __future__ import absolute_import, print_function, unicode_literals | ||||
import logging | import logging | |||
import os | import os | |||
# Import python libs | ||||
import re | import re | |||
import shlex | import shlex | |||
import subprocess | import subprocess | |||
import sys | import sys | |||
import time | import time | |||
# Import salt libs | ||||
import salt.defaults.exitcodes | import salt.defaults.exitcodes | |||
import salt.utils.json | import salt.utils.json | |||
import salt.utils.nb_popen | import salt.utils.nb_popen | |||
import salt.utils.vt | import salt.utils.vt | |||
from salt.ext import six | ||||
log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | |||
SSH_PASSWORD_PROMPT_RE = re.compile(r"(?:.*)[Pp]assword(?: for .*)?:", re.M) | SSH_PASSWORD_PROMPT_RE = re.compile(r"(?:.*)[Pp]assword(?: for .*)?:", re.M) | |||
KEY_VALID_RE = re.compile(r".*\(yes\/no\).*") | KEY_VALID_RE = re.compile(r".*\(yes\/no\).*") | |||
SSH_PRIVATE_KEY_PASSWORD_PROMPT_RE = re.compile(r"Enter passphrase for key", re. M) | SSH_PRIVATE_KEY_PASSWORD_PROMPT_RE = re.compile(r"Enter passphrase for key", re. M) | |||
# Keep these in sync with ./__init__.py | # Keep these in sync with ./__init__.py | |||
RSTR = "_edbc7885e4f9aac9b83b35999b68d015148caf467b78fa39c05f669c0ff89878" | RSTR = "_edbc7885e4f9aac9b83b35999b68d015148caf467b78fa39c05f669c0ff89878" | |||
RSTR_RE = re.compile(r"(?:^|\r?\n)" + RSTR + r"(?:\r?\n|$)") | RSTR_RE = re.compile(r"(?:^|\r?\n)" + RSTR + r"(?:\r?\n|$)") | |||
def gen_key(path): | def gen_key(path): | |||
""" | """ | |||
Generate a key for use with salt-ssh | Generate a key for use with salt-ssh | |||
""" | """ | |||
cmd = ["ssh-keygen", "-P", '""', "-f", path, "-t", "rsa", "-q"] | cmd = ["ssh-keygen", "-P", "", "-f", path, "-t", "rsa", "-q"] | |||
dirname = os.path.dirname(path) | dirname = os.path.dirname(path) | |||
if dirname and not os.path.isdir(dirname): | if dirname and not os.path.isdir(dirname): | |||
os.makedirs(os.path.dirname(path)) | os.makedirs(os.path.dirname(path)) | |||
subprocess.call(cmd) | subprocess.call(cmd) | |||
def gen_shell(opts, **kwargs): | def gen_shell(opts, **kwargs): | |||
""" | """ | |||
Return the correct shell interface for the target system | Return the correct shell interface for the target system | |||
""" | """ | |||
if kwargs["winrm"]: | if kwargs["winrm"]: | |||
skipping to change at line 60 | skipping to change at line 54 | |||
import saltwinshell | import saltwinshell | |||
shell = saltwinshell.Shell(opts, **kwargs) | shell = saltwinshell.Shell(opts, **kwargs) | |||
except ImportError: | except ImportError: | |||
log.error("The saltwinshell library is not available") | log.error("The saltwinshell library is not available") | |||
sys.exit(salt.defaults.exitcodes.EX_GENERIC) | sys.exit(salt.defaults.exitcodes.EX_GENERIC) | |||
else: | else: | |||
shell = Shell(opts, **kwargs) | shell = Shell(opts, **kwargs) | |||
return shell | return shell | |||
class Shell(object): | class Shell: | |||
""" | """ | |||
Create a shell connection object to encapsulate ssh executions | Create a shell connection object to encapsulate ssh executions | |||
""" | """ | |||
def __init__( | def __init__( | |||
self, | self, | |||
opts, | opts, | |||
host, | host, | |||
user=None, | user=None, | |||
port=None, | port=None, | |||
skipping to change at line 89 | skipping to change at line 83 | |||
sudo_user=None, | sudo_user=None, | |||
remote_port_forwards=None, | remote_port_forwards=None, | |||
winrm=False, | winrm=False, | |||
ssh_options=None, | ssh_options=None, | |||
): | ): | |||
self.opts = opts | self.opts = opts | |||
# ssh <ipv6>, but scp [<ipv6]:/path | # ssh <ipv6>, but scp [<ipv6]:/path | |||
self.host = host.strip("[]") | self.host = host.strip("[]") | |||
self.user = user | self.user = user | |||
self.port = port | self.port = port | |||
self.passwd = six.text_type(passwd) if passwd else passwd | self.passwd = str(passwd) if passwd else passwd | |||
self.priv = priv | self.priv = priv | |||
self.priv_passwd = priv_passwd | self.priv_passwd = priv_passwd | |||
self.timeout = timeout | self.timeout = timeout | |||
self.sudo = sudo | self.sudo = sudo | |||
self.tty = tty | self.tty = tty | |||
self.mods = mods | self.mods = mods | |||
self.identities_only = identities_only | self.identities_only = identities_only | |||
self.remote_port_forwards = remote_port_forwards | self.remote_port_forwards = remote_port_forwards | |||
self.ssh_options = "" if ssh_options is None else ssh_options | self.ssh_options = "" if ssh_options is None else ssh_options | |||
skipping to change at line 127 | skipping to change at line 121 | |||
""" | """ | |||
options = [ | options = [ | |||
"KbdInteractiveAuthentication=no", | "KbdInteractiveAuthentication=no", | |||
] | ] | |||
if self.passwd: | if self.passwd: | |||
options.append("PasswordAuthentication=yes") | options.append("PasswordAuthentication=yes") | |||
else: | else: | |||
options.append("PasswordAuthentication=no") | options.append("PasswordAuthentication=no") | |||
if self.opts.get("_ssh_version", (0,)) > (4, 9): | if self.opts.get("_ssh_version", (0,)) > (4, 9): | |||
options.append("GSSAPIAuthentication=no") | options.append("GSSAPIAuthentication=no") | |||
options.append("ConnectTimeout={0}".format(self.timeout)) | options.append("ConnectTimeout={}".format(self.timeout)) | |||
if self.opts.get("ignore_host_keys"): | if self.opts.get("ignore_host_keys"): | |||
options.append("StrictHostKeyChecking=no") | options.append("StrictHostKeyChecking=no") | |||
if self.opts.get("no_host_keys"): | if self.opts.get("no_host_keys"): | |||
options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev /null"]) | options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev /null"]) | |||
known_hosts = self.opts.get("known_hosts_file") | known_hosts = self.opts.get("known_hosts_file") | |||
if known_hosts and os.path.isfile(known_hosts): | if known_hosts and os.path.isfile(known_hosts): | |||
options.append("UserKnownHostsFile={0}".format(known_hosts)) | options.append("UserKnownHostsFile={}".format(known_hosts)) | |||
if self.port: | if self.port: | |||
options.append("Port={0}".format(self.port)) | options.append("Port={}".format(self.port)) | |||
if self.priv and self.priv != "agent-forwarding": | if self.priv and self.priv != "agent-forwarding": | |||
options.append("IdentityFile={0}".format(self.priv)) | options.append("IdentityFile={}".format(self.priv)) | |||
if self.user: | if self.user: | |||
options.append("User={0}".format(self.user)) | options.append("User={}".format(self.user)) | |||
if self.identities_only: | if self.identities_only: | |||
options.append("IdentitiesOnly=yes") | options.append("IdentitiesOnly=yes") | |||
ret = [] | ret = [] | |||
for option in options: | for option in options: | |||
ret.append("-o {0} ".format(option)) | ret.append("-o {} ".format(option)) | |||
return "".join(ret) | return "".join(ret) | |||
def _passwd_opts(self): | def _passwd_opts(self): | |||
""" | """ | |||
Return options to pass to ssh | Return options to pass to ssh | |||
""" | """ | |||
# TODO ControlMaster does not work without ControlPath | # TODO ControlMaster does not work without ControlPath | |||
# user could take advantage of it if they set ControlPath in their | # user could take advantage of it if they set ControlPath in their | |||
# ssh config. Also, ControlPersist not widely available. | # ssh config. Also, ControlPersist not widely available. | |||
options = [ | options = [ | |||
"ControlMaster=auto", | "ControlMaster=auto", | |||
"StrictHostKeyChecking=no", | "StrictHostKeyChecking=no", | |||
] | ] | |||
if self.opts["_ssh_version"] > (4, 9): | if self.opts["_ssh_version"] > (4, 9): | |||
options.append("GSSAPIAuthentication=no") | options.append("GSSAPIAuthentication=no") | |||
options.append("ConnectTimeout={0}".format(self.timeout)) | options.append("ConnectTimeout={}".format(self.timeout)) | |||
if self.opts.get("ignore_host_keys"): | if self.opts.get("ignore_host_keys"): | |||
options.append("StrictHostKeyChecking=no") | options.append("StrictHostKeyChecking=no") | |||
if self.opts.get("no_host_keys"): | if self.opts.get("no_host_keys"): | |||
options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev /null"]) | options.extend(["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev /null"]) | |||
if self.passwd: | if self.passwd: | |||
options.extend(["PasswordAuthentication=yes", "PubkeyAuthentication= yes"]) | options.extend(["PasswordAuthentication=yes", "PubkeyAuthentication= yes"]) | |||
else: | else: | |||
options.extend( | options.extend( | |||
[ | [ | |||
"PasswordAuthentication=no", | "PasswordAuthentication=no", | |||
"PubkeyAuthentication=yes", | "PubkeyAuthentication=yes", | |||
"KbdInteractiveAuthentication=no", | "KbdInteractiveAuthentication=no", | |||
"ChallengeResponseAuthentication=no", | "ChallengeResponseAuthentication=no", | |||
"BatchMode=yes", | "BatchMode=yes", | |||
] | ] | |||
) | ) | |||
if self.port: | if self.port: | |||
options.append("Port={0}".format(self.port)) | options.append("Port={}".format(self.port)) | |||
if self.user: | if self.user: | |||
options.append("User={0}".format(self.user)) | options.append("User={}".format(self.user)) | |||
if self.identities_only: | if self.identities_only: | |||
options.append("IdentitiesOnly=yes") | options.append("IdentitiesOnly=yes") | |||
ret = [] | ret = [] | |||
for option in options: | for option in options: | |||
ret.append("-o {0} ".format(option)) | ret.append("-o {} ".format(option)) | |||
return "".join(ret) | return "".join(ret) | |||
def _ssh_opts(self): | def _ssh_opts(self): | |||
return " ".join(["-o {0}".format(opt) for opt in self.ssh_options]) | return " ".join(["-o {}".format(opt) for opt in self.ssh_options]) | |||
def _copy_id_str_old(self): | def _copy_id_str_old(self): | |||
""" | """ | |||
Return the string to execute ssh-copy-id | Return the string to execute ssh-copy-id | |||
""" | """ | |||
if self.passwd: | if self.passwd: | |||
# Using single quotes prevents shell expansion and | # Using single quotes prevents shell expansion and | |||
# passwords containing '$' | # passwords containing '$' | |||
return "{0} {1} '{2} -p {3} {4} {5}@{6}'".format( | return "{} {} '{} -p {} {} {}@{}'".format( | |||
"ssh-copy-id", | "ssh-copy-id", | |||
"-i {0}.pub".format(self.priv), | "-i {}.pub".format(self.priv), | |||
self._passwd_opts(), | self._passwd_opts(), | |||
self.port, | self.port, | |||
self._ssh_opts(), | self._ssh_opts(), | |||
self.user, | self.user, | |||
self.host, | self.host, | |||
) | ) | |||
return None | return None | |||
def _copy_id_str_new(self): | def _copy_id_str_new(self): | |||
""" | """ | |||
Since newer ssh-copy-id commands ingest option differently we need to | Since newer ssh-copy-id commands ingest option differently we need to | |||
have two commands | have two commands | |||
""" | """ | |||
if self.passwd: | if self.passwd: | |||
# Using single quotes prevents shell expansion and | # Using single quotes prevents shell expansion and | |||
# passwords containing '$' | # passwords containing '$' | |||
return "{0} {1} {2} -p {3} {4} {5}@{6}".format( | return "{} {} {} -p {} {} {}@{}".format( | |||
"ssh-copy-id", | "ssh-copy-id", | |||
"-i {0}.pub".format(self.priv), | "-i {}.pub".format(self.priv), | |||
self._passwd_opts(), | self._passwd_opts(), | |||
self.port, | self.port, | |||
self._ssh_opts(), | self._ssh_opts(), | |||
self.user, | self.user, | |||
self.host, | self.host, | |||
) | ) | |||
return None | return None | |||
def copy_id(self): | def copy_id(self): | |||
""" | """ | |||
skipping to change at line 260 | skipping to change at line 254 | |||
if ssh != "scp": | if ssh != "scp": | |||
command.append(self.host) | command.append(self.host) | |||
if self.tty and ssh == "ssh": | if self.tty and ssh == "ssh": | |||
command.append("-t -t") | command.append("-t -t") | |||
if self.passwd or self.priv: | if self.passwd or self.priv: | |||
command.append(self.priv and self._key_opts() or self._passwd_opts() ) | command.append(self.priv and self._key_opts() or self._passwd_opts() ) | |||
if ssh != "scp" and self.remote_port_forwards: | if ssh != "scp" and self.remote_port_forwards: | |||
command.append( | command.append( | |||
" ".join( | " ".join( | |||
[ | [ | |||
"-R {0}".format(item) | "-R {}".format(item) | |||
for item in self.remote_port_forwards.split(",") | for item in self.remote_port_forwards.split(",") | |||
] | ] | |||
) | ) | |||
) | ) | |||
if self.ssh_options: | if self.ssh_options: | |||
command.append(self._ssh_opts()) | command.append(self._ssh_opts()) | |||
command.append(cmd) | command.append(cmd) | |||
return " ".join(command) | return " ".join(command) | |||
skipping to change at line 302 | skipping to change at line 296 | |||
def exec_nb_cmd(self, cmd): | def exec_nb_cmd(self, cmd): | |||
""" | """ | |||
Yield None until cmd finished | Yield None until cmd finished | |||
""" | """ | |||
r_out = [] | r_out = [] | |||
r_err = [] | r_err = [] | |||
rcode = None | rcode = None | |||
cmd = self._cmd_str(cmd) | cmd = self._cmd_str(cmd) | |||
logmsg = "Executing non-blocking command: {0}".format(cmd) | logmsg = "Executing non-blocking command: {}".format(cmd) | |||
if self.passwd: | if self.passwd: | |||
logmsg = logmsg.replace(self.passwd, ("*" * 6)) | logmsg = logmsg.replace(self.passwd, ("*" * 6)) | |||
log.debug(logmsg) | log.debug(logmsg) | |||
for out, err, rcode in self._run_nb_cmd(cmd): | for out, err, rcode in self._run_nb_cmd(cmd): | |||
if out is not None: | if out is not None: | |||
r_out.append(out) | r_out.append(out) | |||
if err is not None: | if err is not None: | |||
r_err.append(err) | r_err.append(err) | |||
yield None, None, None | yield None, None, None | |||
yield "".join(r_out), "".join(r_err), rcode | yield "".join(r_out), "".join(r_err), rcode | |||
def exec_cmd(self, cmd): | def exec_cmd(self, cmd): | |||
""" | """ | |||
Execute a remote command | Execute a remote command | |||
""" | """ | |||
cmd = self._cmd_str(cmd) | cmd = self._cmd_str(cmd) | |||
logmsg = "Executing command: {0}".format(cmd) | logmsg = "Executing command: {}".format(cmd) | |||
if self.passwd: | if self.passwd: | |||
logmsg = logmsg.replace(self.passwd, ("*" * 6)) | logmsg = logmsg.replace(self.passwd, ("*" * 6)) | |||
if 'decode("base64")' in logmsg or "base64.b64decode(" in logmsg: | if 'decode("base64")' in logmsg or "base64.b64decode(" in logmsg: | |||
log.debug("Executed SHIM command. Command logged to TRACE") | log.debug("Executed SHIM command. Command logged to TRACE") | |||
log.trace(logmsg) | log.trace(logmsg) | |||
else: | else: | |||
log.debug(logmsg) | log.debug(logmsg) | |||
ret = self._run_cmd(cmd) | ret = self._run_cmd(cmd) | |||
return ret | return ret | |||
def send(self, local, remote, makedirs=False): | def send(self, local, remote, makedirs=False): | |||
""" | """ | |||
scp a file or files to a remote system | scp a file or files to a remote system | |||
""" | """ | |||
if makedirs: | if makedirs: | |||
self.exec_cmd("mkdir -p {0}".format(os.path.dirname(remote))) | self.exec_cmd("mkdir -p {}".format(os.path.dirname(remote))) | |||
# scp needs [<ipv6} | # scp needs [<ipv6} | |||
host = self.host | host = self.host | |||
if ":" in host: | if ":" in host: | |||
host = "[{0}]".format(host) | host = "[{}]".format(host) | |||
cmd = "{0} {1}:{2}".format(local, host, remote) | cmd = "{} {}:{}".format(local, host, remote) | |||
cmd = self._cmd_str(cmd, ssh="scp") | cmd = self._cmd_str(cmd, ssh="scp") | |||
logmsg = "Executing command: {0}".format(cmd) | logmsg = "Executing command: {}".format(cmd) | |||
if self.passwd: | if self.passwd: | |||
logmsg = logmsg.replace(self.passwd, ("*" * 6)) | logmsg = logmsg.replace(self.passwd, ("*" * 6)) | |||
log.debug(logmsg) | log.debug(logmsg) | |||
return self._run_cmd(cmd) | return self._run_cmd(cmd) | |||
def _split_cmd(self, cmd): | def _split_cmd(self, cmd): | |||
""" | """ | |||
Split a command string so that it is suitable to pass to Popen without | Split a command string so that it is suitable to pass to Popen without | |||
shell=True. This prevents shell injection attacks in the options passed | shell=True. This prevents shell injection attacks in the options passed | |||
skipping to change at line 434 | skipping to change at line 428 | |||
return "", "Password authentication failed", 254 | return "", "Password authentication failed", 254 | |||
elif buff and KEY_VALID_RE.search(buff): | elif buff and KEY_VALID_RE.search(buff): | |||
if key_accept: | if key_accept: | |||
term.sendline("yes") | term.sendline("yes") | |||
continue | continue | |||
else: | else: | |||
term.sendline("no") | term.sendline("no") | |||
ret_stdout = ( | ret_stdout = ( | |||
"The host key needs to be accepted, to " | "The host key needs to be accepted, to " | |||
"auto accept run salt-ssh with the -i " | "auto accept run salt-ssh with the -i " | |||
"flag:\n{0}" | "flag:\n{}" | |||
).format(stdout) | ).format(stdout) | |||
return ret_stdout, "", 254 | return ret_stdout, "", 254 | |||
elif buff and buff.endswith("_||ext_mods||_"): | elif buff and buff.endswith("_||ext_mods||_"): | |||
mods_raw = ( | mods_raw = ( | |||
salt.utils.json.dumps(self.mods, separators=(",", ":")) | salt.utils.json.dumps(self.mods, separators=(",", ":")) | |||
+ "|_E|0|" | + "|_E|0|" | |||
) | ) | |||
term.sendline(mods_raw) | term.sendline(mods_raw) | |||
if stdout: | if stdout: | |||
old_stdout = stdout | old_stdout = stdout | |||
End of changes. 31 change blocks. | ||||
32 lines changed or deleted | 26 lines changed or added |