Source code for _proxy.ssh

# -*- coding: utf-8 -*-
"""
SSH Proxy
=========

Manage a remote host via SSH, using a Proxy Minion. This module doesn't have any
external dependencies, as it makes use of the native Salt internals used for
salt-ssh, therefore managing the remote machine by uploading a lightweight Salt
version on the target host, then invokes Salt functions over SSH (using the
``ssh`` binary installed on your computer or wherever this Proxy Minion runs).

.. note::

    To manage machines running Windows, you will need to install the
    ``saltwinshell`` library.

Pillar
------

The configuration is aligned to the general Proxy Minion standards: put the
connection details and credentials under the ``proxy`` key in the Proxy config
or Pillar.

.. important:

    Local (i.e., per Proxy) option override the global configuration or CLI
    options.

``host``
    The IP address or the hostname of the remove machine to manage.

``port``
    Integer, the port number to use when establishing he connection
    (defaults to 22).

``user``
    The username required for authentication.

``passwd``
    The password used for authentication.

``priv``
    Absolute path to the private SSH key used for authentication.

``priv_passwd``
    The SSH private key password.

``timeout``: 30
    The SSH timeout. Defaults to 30 seconds.

``sudo``: ``False``
    Execute commands as sudo.

``tty``: ``False``
    Connect over tty.

``sudo_user``
    The username that should execute the commands as sudo.

``remote_port_forwards``
    Enable remote port forwarding. Example: ``8888:my.company.server:443``.
    Multiple remote port forwardings are supported, using comma-separated
    values, e.g., ``8888:my.company.server:443,9999:my.company.server:80``.

``identities_only``: ``False``
    Execute SSH with ``-o IdentitiesOnly=yes``. This option is intended for
    situations where ssh-agent offers many different identities and allow ssh
    to ignore those identities and use the only one specified in options.

``ignore_host_keys``: ``False``
    By default ssh host keys are honored and connections will ask for approval.
    Use this option to disable ``StrictHostKeyChecking``.

``no_host_keys``: ``False``
    Fully ignores ssh host keys which by default are honored and connections
    would ask for approval. Useful if the host key of a remote server has
    changed and would still error with ``ignore_host_keys``.

``winrm``: ``False``
    Flag that tells Salt to connect to a Windows machine. This option requires
    the ``saltwinshell`` to be installed.

Example Pillar:

.. code-block:: yaml

  proxy:
    proxytype: ssh
    host: srv.example.com
    user: test
    passwd: test
    port: 2022
"""
from __future__ import absolute_import, print_function, unicode_literals

import json
import logging

import six
import salt.client.ssh
import salt.fileclient
import salt.exceptions
import salt.utils.path

__proxyenabled__ = ["ssh"]

log = logging.getLogger(__name__)

CONN = None
INITIALIZED = False
GRAINS_CACHE = {}


def _prep_conn(opts, fun, *args, **kwargs):
    """
    Prepare the connection.
    """
    opts["_ssh_version"] = salt.client.ssh.ssh_version()
    fsclient = salt.fileclient.FSClient(opts)
    # TODO: Have here more options to simplify the usage, through features like
    # auto-expand the path to the priv key, auto-discovery, etc.
    argv = [fun]
    argv.extend([salt.utils.json.dumps(arg) for arg in args])
    argv.extend(
        [
            "{0}={1}".format(
                salt.utils.stringutils.to_str(key), salt.utils.json.dumps(val)
            )
            for key, val in six.iteritems(kwargs)
        ]
    )
    if not opts["proxy"].get("ssh_options"):
        opts["proxy"]["ssh_options"] = []
    if opts["proxy"].get("ignore_host_keys", False):
        opts["proxy"]["ssh_options"].append("StrictHostKeyChecking=no")
    if opts["proxy"].get("no_host_keys", False):
        opts["proxy"]["ssh_options"].extend(
            ["StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null"]
        )
    for cli_opt in ("identities_only", "priv", "priv_passwd"):
        if opts.get(cli_opt) and not opts["proxy"].get(cli_opt):
            opts["proxy"][cli_opt] = opts[cli_opt]
    ext_mods = salt.client.ssh.mod_data(fsclient)
    conn = salt.client.ssh.Single(
        opts, argv, opts["id"], fsclient=fsclient, mods=ext_mods, **opts["proxy"]
    )
    conn.args = args
    conn.kwargs = kwargs
    thin_dir = conn.opts["thin_dir"]
    thin_dir = thin_dir.replace("proxy", "")
    conn.opts["thin_dir"] = thin_dir
    conn.thin_dir = thin_dir
    return conn


[docs]def init(opts): """ Init the SSH connection, and execute a simple call to ensure that the remote device is reachable, otherwise throw an error. """ global CONN, INITIALIZED if not salt.utils.path.which("ssh"): raise salt.exceptions.SaltSystemExit( code=-1, msg="No ssh binary found in path -- ssh must be installed for this Proxy module. Exiting.", ) CONN = _prep_conn(opts, "cmd.run", "echo") INITIALIZED = True
[docs]def initialized(): """ Proxy initialized properly? """ return INITIALIZED
[docs]def module_executors(): """ Return the list of executors that should invoke the Salt functions. """ return ["ssh"]
[docs]def call(fun, *args, **kwargs): """ Call an arbitrary Salt function and return the output. """ global CONN, INITIALIZED if not CONN or not INITIALIZED: return opts = CONN.opts opts["output"] = "json" ssh_conn = _prep_conn(opts, fun, *args, **kwargs) ret = ssh_conn.run() if ret[2] != 0: log.error("[%s] %s", opts["id"], ret[1]) return ret[0] thin_ret = json.loads(ret[0]) if "_error" in thin_ret["local"]: log.error(thin_ret["local"]["_error"]) if "stdout" in thin_ret["local"]: log.error(thin_ret["local"]["stdout"]) return thin_ret["local"]["return"]
[docs]def ping(): """ Execute "echo" on the remote host to ensure it's still accessible. """ global CONN, INITIALIZED if not CONN or not INITIALIZED: log.debug("Not connected, or not initialized") return False ret = CONN.run() log.debug(ret) return ret[2] == 0
[docs]def grains(): """ Invoke grains.items from the thin Salt on the remote machine, in order to return here the Grains. """ global GRAINS_CACHE if not GRAINS_CACHE: GRAINS_CACHE = call("grains.items") return GRAINS_CACHE
[docs]def shutdown(opts): """ Buh-bye... """ global CONN, INITIALIZED if CONN and INITIALIZED: del CONN INITIALIZED = False