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:
- 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.
Use pty as ansible connection