initial commit

This commit is contained in:
Manuel Amador (Rudd-O) 2015-06-15 12:08:51 -07:00
commit f2068b17dc
4 changed files with 369 additions and 0 deletions

25
README.md Normal file
View File

@ -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.

View File

@ -0,0 +1,143 @@
# Based on local.py (c) 2012, Anon <anon@anon.anon>
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
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

45
bin/qrun Executable file
View File

@ -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)

156
bin/qrun-bridge Executable file
View File

@ -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