update Ansible connection plugin to be compatible with Ansible 2.0

This commit is contained in:
Manuel Amador (Rudd-O) 2016-04-21 21:55:13 +00:00
parent 4ec736212b
commit 3347647e79

View File

@ -11,8 +11,16 @@ import subprocess
import pipes import pipes
from ansible import errors from ansible import errors
from ansible import utils from ansible import utils
from ansible.callbacks import vvv from ansible.utils.display import Display
display = Display()
from ansible.plugins.connection import ConnectionBase
from ansible.inventory import Inventory from ansible.inventory import Inventory
from ansible.utils.vars import combine_vars
from ansible.utils.unicode import to_bytes, to_unicode, to_str
from ansible import constants as C
BUFSIZE = 1024*1024
class QubesRPCError(subprocess.CalledProcessError): class QubesRPCError(subprocess.CalledProcessError):
@ -26,138 +34,148 @@ class QubesRPCError(subprocess.CalledProcessError):
return r return r
class Connection(object): class Connection(ConnectionBase):
''' Qubes based connections ''' ''' Qubes based connections '''
def __init__(self, runner, host, port, *args, **kwargs): transport = "qubes"
self.runner = runner has_pipelining = False
host_vars = self.runner.inventory.get_host(host).get_variables() become_from_methods = frozenset(["sudo"])
self.proxy = host_vars.get("management_proxy") _management_proxy = None
self.has_pipelining = False
self.chroot_cmd = distutils.spawn.find_executable('qrun') def set_host_overrides(self, host):
if not self.chroot_cmd: host_vars = combine_vars(host.get_group_vars(), host.get_vars())
self.chroot_cmd = os.path.join( _management_proxy = host_vars.get("_management_proxy", None)
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
self.chroot = self._play_context.remote_addr
self.qrun = distutils.spawn.find_executable('qrun')
if not self.qrun:
self.qrun = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
os.path.pardir, os.path.pardir,
os.path.pardir, os.path.pardir,
"bin", "bin",
"qrun", "qrun",
) )
if not os.path.exists(self.chroot_cmd): if not os.path.exists(self.qrun):
self.chroot_cmd = None self.qrun = None
if not self.chroot_cmd: if not self.qrun:
raise errors.AnsibleError("qrun command not found in PATH") raise errors.AnsibleError("qrun command not found in PATH")
self.host = host if self._management_proxy:
if self.proxy: assert 0, "still do not know how to deal with management proxy"
self.chroot = ".".join(self.host.split(".")[:-1])
else:
self.chroot = None
# port is unused, since this is local
self.port = port
def connect(self, port=None): def _connect(self):
"""Connect to the host we've been initialized with"""
# Check if PE is supported
if self._play_context.become:
self._become_method_supported()
def connect(self):
''' connect to the chroot; nothing to do here ''' ''' connect to the chroot; nothing to do here '''
vvv("THIS IS A QUBES VM", host=self.chroot) super(Connection, self)._connect()
display.vvv("THIS IS A QUBES VM", host=self.chroot)
return self return self
def produce_command(self, cmd, executable='/bin/sh'): def _produce_command(self, cmd):
proxy = ["--proxy=%s" % self.proxy] if self.proxy else [] # FIXME
chroot = self.chroot if self.chroot else self.host # proxy = ["--proxy=%s" % self._management_proxy] if self._management_proxy else []
chroot = self.chroot
if isinstance(cmd, basestring): if isinstance(cmd, basestring):
cmd = ["bash", "-c", cmd] assert 0, "cannot deal with basestrings like " + cmd
if proxy: #if proxy:
local_cmd = [self.chroot_cmd] + proxy + [chroot] + cmd # local_cmd = [self.qrun] + proxy + [chroot] + cmd
else: #else:
local_cmd = [self.chroot_cmd] + [chroot] + cmd local_cmd = [self.qrun, self.chroot] + cmd
if executable: local_cmd = map(to_bytes, local_cmd)
local_cmd = [executable, "-c", " ".join(pipes.quote(x) for x in local_cmd)]
vvv("EXEC (with %s) %s" % (executable, local_cmd), host=self.chroot)
else:
vvv("EXEC (without executable) %s" % (local_cmd), host=self.chroot)
return local_cmd return local_cmd
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None): def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
''' run a command on the chroot. This is only needed for implementing
put_file() get_file() so that we don't have to read the whole file
into memory.
compared to exec_command() it looses some niceties like being able to
return the process's exit code immediately.
'''
local_cmd = self._produce_command(["/bin/sh", "-c", cmd])
display.vvv("EXEC %s" % (local_cmd), host=self.chroot)
return subprocess.Popen(local_cmd, shell=False, stdin=stdin,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def exec_command(self, cmd, in_data=None, sudoable=False):
''' run a command on the chroot ''' ''' run a command on the chroot '''
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
if become_user is not None and sudoable: p = self._buffered_exec_command(cmd)
if isinstance(cmd, basestring): stdout, stderr = p.communicate(in_data)
cmd = "sudo -H -u %s bash -c %s" % (pipes.quote(become_user), pipes.quote(cmd)) return (p.returncode, stdout, stderr)
else:
assert 0, "is not basestring: %r" % cmd
local_cmd = self.produce_command(cmd, executable)
p = subprocess.Popen(local_cmd, shell=False,
cwd=self.runner.basedir,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if in_data is None:
stdout, stderr = p.communicate()
else:
stdout, stderr = p.communicate(in_data)
return (p.returncode, '', stdout, stderr)
def put_file(self, in_path, out_path): def put_file(self, in_path, out_path):
''' transfer a file from local to VM ''' ''' transfer a file from local to VM '''
super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
if not out_path.startswith(os.path.sep): out_path = pipes.quote(self._prefix_login_path(out_path))
out_path = os.path.join(os.path.sep, out_path)
normpath = os.path.normpath(out_path)
out_path = os.path.join("/", normpath[1:])
vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
if not os.path.exists(in_path):
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
cmd = self.produce_command("cat > %s" % pipes.quote(out_path))
try: try:
p = subprocess.Popen( with open(in_path, 'rb') as in_file:
cmd, try:
stdin=subprocess.PIPE, p = self._buffered_exec_command('dd of=%s bs=%s' % (out_path, BUFSIZE), stdin=in_file)
stdout=subprocess.PIPE, except OSError:
stderr=subprocess.STDOUT, raise AnsibleError("chroot connection requires dd command in the chroot")
) try:
out, _ = p.communicate(file(in_path).read()) stdout, stderr = p.communicate()
retval = p.wait() except:
if retval != 0: traceback.print_exc()
raise QubesRPCError(retval, cmd, out) raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
except subprocess.CalledProcessError: if p.returncode != 0:
traceback.print_exc() raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
raise errors.AnsibleError("failed to transfer file to %s" % out_path) except IOError:
raise AnsibleError("file or module does not exist at: %s" % in_path)
def _prefix_login_path(self, remote_path):
''' Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we aren't guaranteed that a home dir will
exist in any given chroot. So for now we're choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it's a problem
'''
if not remote_path.startswith(os.path.sep):
remote_path = os.path.join(os.path.sep, remote_path)
return os.path.normpath(remote_path)
def fetch_file(self, in_path, out_path): def fetch_file(self, in_path, out_path):
''' fetch a file from VM to local ''' ''' fetch a file from VM to local '''
super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
if not in_path.startswith(os.path.sep): in_path = pipes.quote(self._prefix_login_path(in_path))
in_path = os.path.join(os.path.sep, in_path)
normpath = os.path.normpath(in_path)
in_path = os.path.join("/", normpath[1:])
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
f = pipes.quote(in_path)
cmd = self.produce_command("test -f %s && cat %s || exit 7" % (f,f))
try: try:
p = subprocess.Popen( p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE))
cmd, except OSError:
stdout = subprocess.PIPE raise AnsibleError("Qubes connection requires dd command in the chroot")
)
out, err = p.communicate("") with open(out_path, 'wb+') as out_file:
retval = p.wait() try:
if retval == 7: chunk = p.stdout.read(BUFSIZE)
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path) while chunk:
elif retval != 0: out_file.write(chunk)
raise subprocess.CalledProcessError(retval, cmd) chunk = p.stdout.read(BUFSIZE)
file(out_path, "wb").write(out) except:
except subprocess.CalledProcessError: traceback.print_exc()
traceback.print_exc() raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
raise errors.AnsibleError("failed to transfer file to %s" % out_path) stdout, stderr = p.communicate()
except IOError: if p.returncode != 0:
traceback.print_exc() raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
def close(self): def close(self):
''' terminate the connection; nothing to do here ''' ''' terminate the connection; nothing to do here '''