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:
- pexpect can interactive with the ssh session.
- 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.