From f2068b17dcc73a0ad23d0287cae553ea28e23c11 Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Mon, 15 Jun 2015 12:08:51 -0700 Subject: [PATCH] initial commit --- README.md | 25 +++++ ansible/connection_plugins/qubes.py | 143 +++++++++++++++++++++++++ bin/qrun | 45 ++++++++ bin/qrun-bridge | 156 ++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+) create mode 100644 README.md create mode 100644 ansible/connection_plugins/qubes.py create mode 100755 bin/qrun create mode 100755 bin/qrun-bridge diff --git a/README.md b/README.md new file mode 100644 index 0000000..4bd61aa --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Ansible connection plugin for Qubes +=================================== + +This is an experimental plug-in mechanism that enables Ansible to connect +to Qubes VMs, either from another Qubes VM, or from a remote host via SSH +(assuming there exists a proxy Qubes VM with SSH listening on it). + +You integrate it by (a) placing the `qubes.py` connection plugin in the Ansible +`connection_plugins` directory (b) placing the qrun and qrun-bridge +executables in one of two locations: + +1. Anywhere on your Ansible machine's `PATH`. +2. In a `../../bin` directory relative to the `qubes.py` file. + +After having done that, you can add Qubes VMs to your Ansible `hosts` file: + +``` +workvm ansible_connection=qubes +vmonremotehost ansible_connection=qubes management_proxy=1.2.3.4 +``` + +Please be *absolutely sure* you have reviewed this code before using it. + +This code is available to you under the terms of the GNU LGPL version 2 +or later. The license terms are available on the FSF's Web site. diff --git a/ansible/connection_plugins/qubes.py b/ansible/connection_plugins/qubes.py new file mode 100644 index 0000000..047ae6e --- /dev/null +++ b/ansible/connection_plugins/qubes.py @@ -0,0 +1,143 @@ +# Based on local.py (c) 2012, Anon +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import distutils.spawn +import traceback +import os +import shutil +import subprocess +import pipes +from ansible import errors +from ansible import utils +from ansible.callbacks import vvv +from ansible.inventory import Inventory + +class Connection(object): + ''' Qubes based connections ''' + + def __init__(self, runner, host, port, *args, **kwargs): + self.runner = runner + host_vars = self.runner.inventory.get_host(host).get_variables() + self.proxy = host_vars.get("management_proxy") + self.has_pipelining = False + + self.chroot_cmd = distutils.spawn.find_executable('qrun') + if not self.chroot_cmd: + self.chroot_cmd = os.path.join( + os.path.dirname(__file__), + os.path.pardir, + os.path.pardir, + "bin", + "qrun", + ) + if not os.path.exists(self.chroot_cmd): + self.chroot_cmd = None + if not self.chroot_cmd: + raise errors.AnsibleError("qrun command not found in PATH") + + self.host = host + if self.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): + ''' connect to the chroot; nothing to do here ''' + + vvv("THIS IS A QUBES VM", host=self.chroot) + + return self + + def produce_command(self, cmd, executable='/bin/sh'): + proxy = ["--proxy=%s" % self.proxy] if self.proxy else [] + chroot = "%s" % self.chroot if self.chroot else self.host + if executable: + local_cmd = [self.chroot_cmd] + proxy + [chroot, cmd] + vvv("EXEC (with executable %s) %s" % (executable, local_cmd), host=self.chroot) + else: + if proxy: + local_cmd = '%s %s "%s" %s' % (self.chroot_cmd, proxy, chroot, cmd) + else: + local_cmd = '%s "%s" %s' % (self.chroot_cmd, chroot, cmd) + vvv("EXEC (without executable) %s" % (local_cmd), host=self.chroot) + return local_cmd + + def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None): + ''' run a command on the chroot ''' + + # We enter qrun as root so sudo stuff can be ignored + local_cmd = self.produce_command(cmd, executable) + + p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring), + 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): + ''' transfer a file from local to VM ''' + + if not out_path.startswith(os.path.sep): + 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: + p = subprocess.Popen( + cmd, + stdin = subprocess.PIPE + ) + p.communicate(file(in_path).read()) + retval = p.wait() + if retval != 0: + raise subprocess.CalledProcessError(retval, cmd) + except subprocess.CalledProcessError: + traceback.print_exc() + raise errors.AnsibleError("failed to transfer file to %s" % out_path) + + def fetch_file(self, in_path, out_path): + ''' fetch a file from VM to local ''' + + if not in_path.startswith(os.path.sep): + 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: + p = subprocess.Popen( + cmd, + stdout = subprocess.PIPE + ) + out, err = p.communicate("") + retval = p.wait() + if retval == 7: + raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path) + elif retval != 0: + raise subprocess.CalledProcessError(retval, cmd) + file(out_path, "wb").write(out) + except subprocess.CalledProcessError: + traceback.print_exc() + raise errors.AnsibleError("failed to transfer file to %s" % out_path) + except IOError: + traceback.print_exc() + raise errors.AnsibleError("failed to transfer file to %s" % out_path) + + def close(self): + ''' terminate the connection; nothing to do here ''' + pass diff --git a/bin/qrun b/bin/qrun new file mode 100755 index 0000000..ed583d9 --- /dev/null +++ b/bin/qrun @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import pipes +import os +import subprocess +import sys + +argv = list(sys.argv[1:]) +if argv[0].startswith("--proxy="): + remotehost = argv[0][8:] + argv = argv[1:] +else: + remotehost = None +host, parms = argv[0], argv[1:] + +path_to_this_file = os.path.dirname(__file__) +path_to_qrun = os.path.join(path_to_this_file, "qrun-bridge") +path_to_qrun = os.path.abspath(path_to_qrun) + +if remotehost: + args = " ".join(pipes.quote(x) for x in parms) + poop = file(path_to_qrun, "rb").read().encode("hex_codec") + therest_template = ("test -x ./.qrun-bridge-stub || " + "python -c 'import os; file(\"./.qrun-bridge-stub\", \"wb\").write(\"%s\".decode(\"hex_codec\")); os.chmod(\"./.qrun-bridge-stub\", 0700)' || " + "exit 127 ;" + "export PASS_LOCAL_STDERR=1 ;" + "/usr/lib/qubes/qrexec-client-vm %s " + "qubes.VMShell ./.qrun-bridge-stub %s") + therest = therest_template % (poop, host, args) + cmd = [ + 'ssh', + '-o', 'BatchMode yes', + remotehost, + therest, + ] +else: + os.environ["PASS_LOCAL_STDERR"] = "1" + cmd = [ + '/usr/lib/qubes/qrexec-client-vm', + host, + 'qubes.VMShell', + path_to_qrun, + ] + parms + +os.execvp(cmd[0], cmd) diff --git a/bin/qrun-bridge b/bin/qrun-bridge new file mode 100755 index 0000000..d80ebd6 --- /dev/null +++ b/bin/qrun-bridge @@ -0,0 +1,156 @@ +#!/bin/sh + +echo "exec python -c ' +import sys +import os +import socket +import subprocess +import threading + + +def start_process(): + prefix = [\"su\", \"-\", \"root\"] + if socket.gethostname().startswith(\"dom0.\") or socket.gethostname() == \"dom0\": + prefix = [\"sudo\", \"-H\"] + if sys.argv[1:]: + cmd = prefix + [\"sh\", \"-c\", \" \".join(sys.argv[1:])] + else: + cmd = prefix + [\"sh\"] + p = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return p, p.stdin, p.stdout, p.stderr + +def supervise_process(p): + ret = process.wait() + sys.stdout.write(\"P\" + str(ret) + \" \") + sys.stdout.flush() + +def relay_stdin(i): + while True: + b = sys.stdin.read(1) + if b == \"i\": + data = sys.stdin.read(1) + i.write(data) + i.flush() + elif b == \"I\": + i.close() + break + elif not b: + i.close() + break + +def relay_stdout(o): + while True: + b = o.read(1) + if not b: + sys.stdout.write(\"O\") + sys.stdout.flush() + break + sys.stdout.write(\"o\" + b) + sys.stdout.flush() + +def relay_stderr(e): + while True: + b = e.read(1) + if not b: + sys.stdout.write(\"E\") + sys.stdout.flush() + break + sys.stdout.write(\"e\" + b) + sys.stdout.flush() + +process, i, o, e = start_process() + +relayer_stdin = threading.Thread(target=lambda: relay_stdin(i)) +relayer_stdout = threading.Thread(target=lambda: relay_stdout(o)) +relayer_stderr = threading.Thread(target=lambda: relay_stderr(e)) +relayer_stdin.start() +relayer_stdout.start() +relayer_stderr.start() + +process_supervisor = threading.Thread(target=lambda: supervise_process(process)) +process_supervisor.start() + +def close_stdout(): + relayer_stdout.join() + relayer_stderr.join() + relayer_stdin.join() + process_supervisor.join() + sys.stdout.close() + +stdout_closer = threading.Thread(target=close_stdout) +stdout_closer.start() + +stdout_closer.join() + +'" \"$@\" >&1 + +exec python -c ' +import sys +import os +import threading +import select +import fcntl +saved_fd_0 = int(sys.argv[1]) +local_stdin = os.fdopen(saved_fd_0, "r") +saved_fd_1 = int(sys.argv[2]) +local_stdout = os.fdopen(saved_fd_1, "w") +saved_fd_2 = int(sys.argv[3]) +local_stderr = os.fdopen(saved_fd_2, "w") + +def relay_stdin(): + while True: + b = local_stdin.read(1) + if not b: + sys.stdout.write("I") + sys.stdout.flush() + sys.stdout.close() + break + sys.stdout.write("i" + b) + sys.stdout.flush() + +retval = 0 +def display_stdouterr(): + global retval + conditions = 3 + while conditions: + b = sys.stdin.read(1) + if not b: break + elif b == "o": + b = sys.stdin.read(1) + local_stdout.write(b) + local_stdout.flush() + elif b == "e": + b = sys.stdin.read(1) + local_stderr.write(b) + local_stderr.flush() + elif b == "O": + conditions = conditions - 1 + elif b == "E": + conditions = conditions - 1 + elif b == "P": + chars = "" + while not chars.endswith(" "): + char = sys.stdin.read(1) + chars = chars + char + chars = chars.replace(" ", "") + retval = int(chars) + conditions = conditions - 1 + +stdin_relayer = threading.Thread(target=relay_stdin) +stdin_relayer.setDaemon(True) +stdin_relayer.start() + +displayer = threading.Thread(target=display_stdouterr) +displayer.start() + +displayer.join() + +sys.exit(int(retval)) + +' $SAVED_FD_0 $SAVED_FD_1 $SAVED_FD_2 +