When writing deployment scripts, remote command execution or file transfer is unavoidable.

Sh library has been used before, call sh.ssh to remotely execute some commands, sh.scp file transfer, but the actual use is still more troublesome, just simulate the user login point, also need to define a separate method to simulate input. Feel it:

from sh import ssh

PASS = 'xxxx'

def ssh_interact(line, stdin):
    line = line.strip()
    print(line)
    if line.endswith('password:'):
        stdin.put(PASS)

ssh('x.x.x.x', _out=ssh_interact)Copy the code

From official documentation

The Paramiko library was found to be more elegant and convenient, so I decided to replace the SH with Pramiko.

Previously, I learned from my colleague that when Paramiko executes a Python script remotely, the output of the script may be output through the stderr pipe, so the exec_command method in paramiko’s SSHClient class is used directly. It is not possible to determine the successful execution of a command by reading whether there is output in the stderr pipe. It is better to use the recv_exit_status method of the lower level Channel class to determine the execution exit code.

The installation

This can be done by using PIP Install Paramiko, which is not detailed here.

encapsulation

Start by defining a few exceptions

# coding: utf-8
import os.path

from paramiko import SSHClient, AutoAddPolicy, AuthenticationException


class ConnectError(Exception):
    """Exception thrown when connection error occurs"""
    pass

class RemoteExecError(Exception):
    """An exception thrown when a command fails to execute remotely."""
    pass

class SCPError(Exception):
    """An exception thrown when sending a file remotely"""
    passCopy the code
. class Remote(object): def __init__(self, host, username, password=None, port=22, key_filename=None): self.host = host self.username = username self.password = password self.port = port self.key_filename = key_filename self._ssh = None def _connect(self): self._ssh = SSHClient() self._ssh.set_missing_host_key_policy(AutoAddPolicy()) try:if self.key_filename:
                self._ssh.connect(self.host, username=self.username, port=self.port, key_filename=self.key_filename)
            else:
                self._ssh.connect(self.host, username=self.username, password=self.password, port=self.port)
        except AuthenticationException: 
            self._ssh = None
            raise ConnectionError('Connection failed, please confirm username, password, port or key file is valid')
        except Exception as e:
            self._ssh = None
            raise ConnectionError('Unexpected error while connecting: %s' % e)

    def get_ssh(self):
        if not self._ssh:
            self._connect()
        return self._sshCopy the code

Instantiate the SSHClient class to get an SSH connection through its connect() method.

The set_missing_host_key_policy() method is used to set a policy. AutoAddPolicy() is used here.

Here, _connect supports two login methods, one is to provide a host username and password, and the other is through a key file. Check at connection time: If a key file is specified, log in using this method; otherwise, log in using the user name and password.

_connect() is the actual method for establishing a connection, but the actual external interface is get_ssh(). If an SSH connection has been established, it is returned directly to avoid repeated connection establishment.

class Remote(object):
    ...


    def ssh(self, cmd, root_password=None, get_pty=False, super=False):
        cmd = self._prepare_cmd(cmd, root_password, super)
        stdout = self._exec(cmd, get_pty)
        return stdout

    def _prepare_cmd(self, cmd, root_password=None, super=False):
        ifself.username ! ='root' and super:
            if root_password:
                cmd = "echo '{}'|su - root -c '{}'".format(root_password, cmd)
            else:
                cmd = "echo '{}'|sudo -p '' -S su - root -c '{}'".format(self.password, cmd)
        return cmd

    def _exec(self, cmd, gty_pty=False):
        channel = self.get_ssh().get_transport().open_session()
        if get_pty:
            channel.get_pty()
        channel.exec_command(cmd)
        stdout = channel.makefile('r', -1).readlines()
        stderr = channel.makefile_stderr('r', -1).readlines()
        ret_code = channel.recv_exit_status()
        if ret_code:
            msg = ' '.join(stderr) if stderr else ' '.join(stdout)
            raise RemoteExecError(msg)
        return stdoutCopy the code

When you run some commands remotely, administrator rights may be required. In this case, you need to check whether the user name provided for login is not root and modify the command. The common user has sudo permission and only needs to run the command after sudo permission is added. The common user does not have sudo permission and needs to su to switch to root and then run the command. In this case, the root password is required.

Setting get_pty to True simulates tty. However, there is a problem with remote execution of a long-running process. For example, when the nginx service is started, all programs run by the remote command are terminated after SSH exit. Therefore, when some services or programs need to be run by remote command, you cannot specify the get_pty parameter. However, if a common user logs in remotely, he/she does not have the permission to run the service command. One suggested approach is to modify the /etc/sudoers configuration file to comment out the Defaults requiretty line.

class Remote(object):
    ...

    def scp(self, local_file, remote_path):
        if not os.path.exists(local_file):
            raise SCPError("Local %s isn't exists" % local_file)
        if not os.path.isfile(local_file):
            raise SCPError("%s is not a File" % local_file)
        sftp = self.get_ssh().open_sftp()
        try:
            sftp.put(local_file, remote_path)
        except Exception as e:
            raise SCPError(e)Copy the code

Ensure that the file to be delivered exists and is not a directory. If the file is not a directory, an exception is thrown. In addition, remote_path must be the absolute directory of files on the remote host, for example, / TMP /xxx.xxx, not/TMP.

use

# coding: utf-8
from remote_client import RemoteClient

rc = RemoteClient('10.1.100.1'.'test'.'test_pass')
rc.ssh('whoami')   # [u'test\n']
rc.scp('/tmp/test.out'.'/tmp/test.out')Copy the code

conclusion

Paramiko is much more useful than SH. This is just a simple package, but there are many other uses of Paramiko that you are welcome to discuss.

The above is just my understanding, if there is any mistake, welcome to correct.