如何在ssh-agent中使用boto.manage.cmdshell?

时间:2014-09-18 00:28:38

标签: python ssh boto paramiko

我使用boto.manage.cmdshell创建与EC2实例的SSH连接。目前,每次用户必须输入密码才能加密密钥(例如~/.ssh/id_rsa)。

现在我想让用户更方便工作流程并支持ssh-agent。 到目前为止,我试过没有任何成功:

  • 在创建ssh_key_file时将None设置为FakeServer

    结果是:SSHException('Key object may not be empty')

  • 在创建ssh_pwd时将None设置为SSHClient

    结果是:paramiko.ssh_exception.PasswordRequiredException: Private key file is encrypted

有没有办法将ssh-agentboto.manage.cmdshell一起使用?我知道paramiko支持它,boto正在使用它。

2 个答案:

答案 0 :(得分:1)

(还有另一个包含相关答案的stackoverflow页面) Can't get amazon cmd shell to work through boto

但是,使用每人SSH密钥肯定会更好。但如果你有这些,他们是否在目标主机的authorized_keys文件中?如果是这样,用户只需使用ssh-add正常添加密钥(在ssh-agent会话中,通常是Linux中的默认值)。您需要先使用ssh进行测试,以便事先清楚地解决ssh-agent / -add问题。

一旦确定他们正常使用ssh,问题在于boto是否完全考虑ssh-agent。 Paramiko的SSHClient()可以,如果我没记错的话 - 我记得的paramiko代码看起来大致如下:

paramiko.SSHClient().connect(host, timeout=10, username=user,
                             key_filename=seckey, compress=True)

seckey是可选的,因此key_filename将为空,并且调用了ssh-agent。 Boto的版本似乎想要强制使用具有这样的显式调用的私钥文件,我认为每个实例都有一个指定的密钥和密码来解密它:

  self._pkey = paramiko.RSAKey.from_private_key_file(server.ssh_key_file,
                                                     password=ssh_pwd)

如果是这样,这意味着使用boto直接与使用ssh-agent和每用户登录的标准模型以及用户记录连接冲突。

paramiko.SSHClient()功能更强大,并且显式支持ssh-agent支持(来自pydoc paramiko.SSHClient):

  Authentication is attempted in the following order of priority:
      - The C{pkey} or C{key_filename} passed in (if any)
      - Any key we can find through an SSH agent
      - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/}
      - Plain username/password auth, if a password was given

基本上,你 使用paramiko代替boto。

我们遇到了一个与paramiko有关的问题:在很多情况下,连接将立即就绪,需要在发送实际命令之前发送测试命令和checkout输出。部分原因是我们在创建和EC2或VPC实例后开始启动SSH命令(使用paramiko)正确,因此无法保证它正在侦听SSH连接,而paramiko往往会失去太快发送的命令。我们使用了这样的代码来应对:

def SshCommand(**kwargs):
    '''
    Run a command on a remote host via SSH.

    Connect to the given host=<host-or-ip>, as user=<user> (defaulting to
    $USER), with optional seckey=<secret-key-file>, timeout=<seconds>
    (default 10), and execute a single command=<command> (assumed to be
    addressing a unix shell at the far end.

    Returns the exit status of the remote command (otherwise would be
    None save that an exception should be raised instead).

    Example: SshCommand(host=host, user=user, command=command, timeout=timeout,
                        seckey=seckey)
    '''
    remote_exit_status = None

    if debug:
        sys.stderr.write('SshCommand kwargs: %r\n' % (kwargs,))

    paranoid = True

    host = kwargs['host']
    user = kwargs['user'] if kwargs['user'] else os.environ['USER']
    seckey = kwargs['seckey']
    timeout = kwargs['timeout']
    command = kwargs['command']

    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    time_end = time.time() + int(timeout)

    ssh_is_up = False
    while time.time() < time_end:
        try:
            ssh.connect(host, timeout=10, username=user, key_filename=seckey,
                        compress=True)
            if paranoid:
                token_generator = 'echo xyz | tr a-z A-Z'
                token_result = 'XYZ'      # possibly buried in other text
                stdin, stdout, stderr = ssh.exec_command(token_generator)
                lines = ''.join(stdout.readlines())
                if re.search(token_result, lines):
                    ssh_is_up = True
                    if debug:
                        sys.stderr.write("[%d] command stream is UP!\n"
                                         % time.time())
                    break
            else:
                ssh_is_up = True
                break
        except paramiko.PasswordRequiredException as e:
            sys.stderr.write("usage idiom clash: %r\n" % (e,))
            return False
        except Exception as e:
            sys.stderr.write("[%d] command stream not yet available\n"
                             % time.time())
            if debug:
                sys.stderr.write("exception is %r\n" % (e,))
        time.sleep(1)

    if ssh_is_up:
        # ideally this is where Bcfg2 or Chef or such ilk get called.
#       stdin, stdout, stderr = ssh.exec_command(command)
        chan = ssh._transport.open_session()
        chan.exec_command(command)
        # note that out/err doesn't have inter-stream ordering locked down.
        stdout = chan.makefile('rb', -1)
        stderr = chan.makefile_stderr('rb', -1)
        sys.stdout.write(''.join(stdout.readlines()))
        sys.stderr.write(''.join(stderr.readlines()))
        remote_exit_status = chan.recv_exit_status()
        if debug:
            sys.stderr.write('exit status was: %d\n' % remote_exit_status)

    ssh.close()
    if None == remote_exit_status:
        raise SSHException('remote command result undefined')
    return remote_exit_status

我们还试图强制不直接登录到prod,所以这个特殊的包装器(一个ssh-send-command脚本)鼓励编写脚本,尽管亚马逊是否已经费心去及时启动实例。

答案 1 :(得分:0)

我通过创建一个继承自SSHClientAgent并覆盖boto.manage.cmdshell.SSHClient的类__init__()找到了我的问题的解决方案。在新的__init__()中,我将paramiko.RSAKey.from_private_key_file()的调用替换为None

这是我的新课程:

class SSHClientAgent(boto.manage.cmdshell.SSHClient):

    def __init__(self, server,
                 host_key_file='~/.ssh/known_hosts',
                 uname='root', timeout=None, ssh_pwd=None):
        self.server = server
        self.host_key_file = host_key_file
        self.uname = uname
        self._timeout = timeout
        # replace the call to get the private key
        self._pkey = None
        self._ssh_client = paramiko.SSHClient()
        self._ssh_client.load_system_host_keys()
        self._ssh_client.load_host_keys(os.path.expanduser(host_key_file))
        self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.connect()

在我创建ssh连接的函数中,我检查环境变量SSH_AUTH_SOCK并确定要创建的ssh客户端。