mirror of
https://github.com/Rudd-O/ansible-qubes.git
synced 2025-03-01 14:22:33 +01:00
initial commit
This commit is contained in:
commit
f2068b17dc
25
README.md
Normal file
25
README.md
Normal 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.
|
143
ansible/connection_plugins/qubes.py
Normal file
143
ansible/connection_plugins/qubes.py
Normal 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
45
bin/qrun
Executable 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
156
bin/qrun-bridge
Executable 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user