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 `[[email protected] ~]# `
        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.

Leave a Reply

Your email address will not be published. Required fields are marked *