Use pty as ansible connection

Well, sometimes, we can only use pty to access machines. For example, Jump Server only allowed use ssh pty to type command, get the result. But, how can I deploy application via ansible?


There are two good news:

  1. pexpect can interactive with the ssh session.
  2. ansible can use custom connection plugin.

Let’s try it

Let’s start at directory ansible-pty Firstly, we should create a connection plugin. Put those code to ansible-pty/plugins/pty.:

import base64
import re

import pexpect
from ansible.plugins.connection import ConnectionBase
from ansible.utils.display import Display

display = Display()

class Connection(ConnectionBase):
transport = ‘pty’
_mark = “=” * 10

def \_\_init\_\_(self, \*args, \*\*kwargs):
    super(Connection, self).\_\_init\_\_(\*args, \*\*kwargs)

    self.host = self.\_play\_context.remote\_addr
    self.user = self.\_play\_context.remote\_user

#
# Main public methods
#
def exec\_command(self, cmd, in\_data=None, sudoable=True):
    ''' run a command on the remote host '''

    super(Connection, self).exec\_command(cmd, in\_data=in\_data, sudoable=sudoable)

    display.vvv(u"ESTABLISH SSH CONNECTION FOR USER: {0}".format(self.\_play\_context.remote\_user),
                host=self.\_play\_context.remote\_addr)

    self.\_process.write(cmd)
    self.\_process.write(" ; echo $? && echo ")
    self.\_process.write(self.\_mark)
    self.\_process.write('\\n')
    self.\_process.readline()

    lines = b''
    while True:
        line = self.\_process.readline()
        if line.strip() == self.\_mark.encode():
            break
        lines += line
    regex\_pattern = r"((?P<output>(?:.|\\n)\*)(?:\\r|\\n)+)?(?P<retcode>\\d+)(?:\\r|\\n)+"
    matches = re.match(regex\_pattern, lines.decode(), re.MULTILINE)

    stdout = matches.group('output')
    if not stdout:
        stdout = ''
    returncode = matches.group('retcode')
    returncode = int(returncode)
    stderr = ''

    self.\_eat\_prompt(self.\_process)

    return returncode, stdout, stderr

def put\_file(self, in\_path, out\_path):
    ''' transfer a file from local to remote '''

    super(Connection, self).put\_file(in\_path, out\_path)

    display.vvv(u"PUT {0} TO {1}".format(in\_path, out\_path), host=self.host)
    self.\_process.write("base64 -d > " + out\_path)
    self.\_process.write(' && printf "' + self.\_mark + '\\n"')
    self.\_process.write('\\n')
    with open(in\_path, 'rb') as fd:
        while True:
            raw\_content = fd.read(512)
            if len(raw\_content) == 0:
                break
            encoded\_content = base64.b64encode(raw\_content)
            self.\_process.write(encoded\_content.decode())
            self.\_process.write('\\n')
    self.\_process.write('\\n')
    self.\_process.sendcontrol('d')

    self.\_process.readline()
    while True:
        line = self.\_process.readline()
        if line.strip() == self.\_mark.encode():
            break

    self.\_eat\_prompt(self.\_process)

def fetch\_file(self, in\_path, out\_path):
    ''' fetch a file from remote to local '''

    raise NotImplemented()

def \_connect(self):
    process = pexpect.spawn('ssh {user}@{host}'.format(user=self.user, host=self.host))
    process.setwinsize(40, 160)

    self.\_eat\_prompt(process)

    self.\_connected = True
    self.\_process = process

def \_eat\_prompt(self, process):
    # after logged in
    # there is some prompt like \`\[root@vultr ~\]# \`
    process.expect('\\~\\\](?:\\#|\\$) '.encode())

def close(self):
    self.\_connected = False

And make ansible can load this plugin. Put code to ansible.cfg:

[defaults]
connection_plugins = connection_plugins:./plugins/

Try pty connection with inventory files:

# inventory
vultr ansible_connection=pty ansible_user=root

Run:

ansible -i inventory -m setup all

You can get the machine information:

vultr | SUCCESS => {
“ansible_facts”: {
“ansible_all_ipv4_addresses”: [
“45.76.53.225”
],
“ansible_all_ipv6_addresses”: [
“2001:19f0:7001:1836:5400:ff:fe7c:400c”,
“fe80::5400:ff:fe7c:400c”
],
“ansible_apparmor”: {
“status”: “disabled”
},
……

Other thought

  • Not implement fetch_file
  • And, some times the command output will confilct with _mark
  • Stderr is redriect to stdout

You also can upload, download file via pty. So the jump server, and pty audit is almost useless. You can alway find way to hide what are you do.

作者

Robert Lu

发布于

2017-11-04

许可协议

评论