From 0e35693165498066c587157c6651b2cb212ce40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 30 Apr 2016 16:42:41 +0200 Subject: [PATCH] Extend qubesctl to configure also VMs This commit uses salt-ssh (running over qrexec) called from "DispVM" to configure VMs. For that to work, DispVM gets copy of Salt configuration (mostly full configuration - the only exception is pillars, filtered at dom0 side). Fixes QubesOS/qubes-issues#1541 --- Makefile | 13 ++ qubes.SaltLinuxVM | 42 ++++++ qubesctl | 90 ++++++++++- qubessalt/__init__.py | 246 +++++++++++++++++++++++++++++++ qubessalt/__init__.pyc | Bin 0 -> 7536 bytes rpm_spec/qubes-mgmt-salt-vm.spec | 15 ++ rpm_spec/qubes-mgmt-salt.spec | 16 ++ setup.py | 14 ++ ssh-wrapper | 77 ++++++++++ 9 files changed, 505 insertions(+), 8 deletions(-) create mode 100755 qubes.SaltLinuxVM create mode 100644 qubessalt/__init__.py create mode 100644 qubessalt/__init__.pyc create mode 100644 setup.py create mode 100755 ssh-wrapper diff --git a/Makefile b/Makefile index 49b963a..06d6b65 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,19 @@ install-custom:: cp -Tr etc $(DESTDIR)/etc cp -Tr srv $(DESTDIR)/srv +.PHONY: install-vm +install-vm: + install -d $(DESTDIR)/usr/lib/qubes-vm-connector/ssh-wrapper + install ssh-wrapper $(DESTDIR)/usr/lib/qubes-vm-connector/ssh-wrapper/ssh + ln -s ssh $(DESTDIR)/usr/lib/qubes-vm-connector/ssh-wrapper/scp + ln -s /bin/true $(DESTDIR)/usr/lib/qubes-vm-connector/ssh-wrapper/ssh-keygen + install -d $(DESTDIR)/etc/qubes-rpc + install qubes.SaltLinuxVM $(DESTDIR)/etc/qubes-rpc + +.PHONY: install-dom0 +install-dom0: + python setup.py install -O1 --root $(DESTDIR) + .PHONY: get-sources get-sources: GIT_REPOS := $(addprefix $(SRC_DIR)/,$(MGMT_SALT_COMPONENTS) mgmt-salt-app-saltstack) get-sources: diff --git a/qubes.SaltLinuxVM b/qubes.SaltLinuxVM new file mode 100755 index 0000000..34280d1 --- /dev/null +++ b/qubes.SaltLinuxVM @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marek Marczykowski-Górecki +# +# +# q program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +set -e + +if [ "`id -u`" -ne 0 ]; then + exec sudo "$0" "$@" +fi + +read target_vm +read salt_command + +rm -rf /srv +mv /home/user/QubesIncoming/dom0/srv /srv +if [ -f /srv/master ]; then + mv /srv/master /etc/salt/ +fi +(cd /srv/salt; find _tops -type f -o -type l| sed -e 's/^/- /' > tops.yaml) +cat > /etc/salt/roster <>sys.stderr, "DOM0 configuration failed, not continuing" + return 1 + + # Load VM list only after dom0 salt call - some new VMs might be created + qc = qubes.qubes.QubesVmCollection() + qc.lock_db_for_reading() + qc.load() + qc.unlock_db() + + targets = [] + if args.templates: + targets = [vm for vm in qc.values() if vm.is_template()] + elif args.app: + targets = [vm for vm in qc.values() if vm.is_appvm()] + elif args.targets: + names = args.targets.split(',') + targets = [vm for vm in qc.values() if vm.name in names] + elif args.all: + # all but DispVMs + targets = [vm for vm in qc.values() if not vm.is_disposablevm()] + + # remove dom0 - already handled + if qc[0] in targets: + targets.remove(qc[0]) + + if args.show_output and args.force_color: + args.command.insert(0, '--force-color') -from salt.scripts import salt_call + # templates first + vms_to_go = [vm for vm in targets if vm.is_template()] + runner = qubessalt.ManageVMRunner(qc, vms_to_go, args.command, + show_output=args.show_output, force_color=args.force_color) + runner.run() + # then non-templates (AppVMs) + vms_to_go = [vm for vm in targets if not vm.is_template()] + runner = qubessalt.ManageVMRunner(qc, vms_to_go, args.command, + show_output=args.show_output, force_color=args.force_color) + runner.run() + # TODO: return meaningful exit code if __name__ == '__main__': - salt_call() + # --dom0-only is a passthrough to salt-call + if len(sys.argv) > 1 and sys.argv[1] == '--dom0-only': + try: + import qubes.mgmt.patches + except ImportError: + pass + from salt.scripts import salt_call + sys.argv[1] = '--local' + salt_call() + else: + main() \ No newline at end of file diff --git a/qubessalt/__init__.py b/qubessalt/__init__.py new file mode 100644 index 0000000..ac4c96e --- /dev/null +++ b/qubessalt/__init__.py @@ -0,0 +1,246 @@ +#!/usr/bin/python2 +# coding=utf-8 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2016 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +# +import argparse +import multiprocessing +import os +import pipes +import tempfile +import shutil +import subprocess +import sys +import time +import yaml +import qubes.qubes + + +class ManageVM(object): + def __init__(self, qc, vm, mgmt_template=None): + super(ManageVM, self).__init__() + self.vm = vm + self.qc = qc + if mgmt_template is not None: + self.mgmt_template = mgmt_template + else: + self.mgmt_template = self.qc.get_default_template() + + def prepare_salt_config_for_vm(self): + tmpdir = tempfile.mkdtemp() + output_dir = os.path.join(tmpdir, 'srv') + shutil.copytree('/srv', output_dir) + # make sure only pillars for given host are send + + p = subprocess.Popen( + ['qubesctl', '--dom0-only', + '--id={}'.format(self.vm.name), '--output=yaml', + 'pillar.items'], stdout=subprocess.PIPE) + (pillar_items_output, _) = p.communicate() + pillar_data = yaml.safe_load(pillar_items_output) + pillar_data = pillar_data['local'] + # remove source pillar files + # TODO: remove also pillar modules + for env, roots in pillar_data['master']['pillar_roots'].iteritems(): + for root in roots: + # do not use os.path.join on purpose - root is absolute path + pillar_path = tmpdir + root + if os.path.exists(pillar_path): + shutil.rmtree(tmpdir + root) + + # pass selected configuration options + master_conf = {} + for opt in ['file_roots']: + if opt in pillar_data['master'].keys(): + master_conf[opt] = pillar_data['master'][opt] + with open(os.path.join(output_dir, 'master'), 'w') as f: + f.write(yaml.dump(master_conf)) + + # remove unneded pillar entries + for entry in ['master', 'salt']: + if entry in pillar_data.keys(): + del pillar_data[entry] + + # save rendered pillar data + pillar_dir = os.path.join(output_dir, 'pillar') + os.mkdir(pillar_dir) + with open(os.path.join(pillar_dir, 'combined.sls'), 'w') as f: + f.write(yaml.dump(pillar_data)) + # TODO only selected environments? + with open(os.path.join(pillar_dir, 'top.sls'), 'w') as f: + f.write(yaml.dump( + {'base': + {self.vm.name: ['combined']}})) + return output_dir + + def salt_call(self, command='state.highstate', return_output=False): + self.qc.lock_db_for_writing() + self.qc.load() + tpl = self.mgmt_template + + name = 'disp-mgmt-{}'.format(self.vm.name) + # name is limited to 31 chars + if len(name) > 31: + name = name[:31] + dispvm = self.qc.get_vm_by_name(name) + if dispvm is None: + # FIXME: this should be DisposableVm type in core3 + dispvm = self.qc.add_new_vm('QubesAppVm', + name=name, + label=qubes.qubes.QubesVmLabels['red'], + template=tpl, + netvm=None, + uses_default_netvm=False, + internal=True) + create = True + self.qc.save() + else: + create = False + qrexec_policy(dispvm.name, self.vm.name, True) + self.qc.unlock_db() + return_data = "NO RESULT" + try: + initially_running = self.vm.is_running() + if create: + dispvm.create_on_disk(verbose=False) + if not dispvm.is_running(): + dispvm.start(start_guid=False) + # Copy whole Salt configuration + salt_config = self.prepare_salt_config_for_vm() + retcode = dispvm.run_service( + 'qubes.Filecopy', + localcmd='/usr/lib/qubes/qfile-dom0-agent {}'.format( + salt_config), + gui=False) + shutil.rmtree(salt_config) + if retcode != 0: + raise qubes.QubesException( + "Failed to copy Salt configuration to {}". + format(dispvm.name)) + p = dispvm.run_service('qubes.SaltLinuxVM', passio_popen=True, + gui=False) + (stdout, _) = p.communicate(self.vm.name + '\n' + command + '\n') + if return_output and stdout: + lines = stdout.splitlines() + if lines[0].count(self.vm.name + ':') == 1: + lines = lines[1:] + return_data = lines + else: + # TODO consider saving output to some log file + return_data = "OK" if p.returncode == 0 else "ERROR" + if self.vm.is_running() and not initially_running: + self.vm.shutdown() + # FIXME: convert to self.vm.shutdown(wait=True) in core3 + while self.vm.is_running(): + time.sleep(1) + finally: + qrexec_policy(dispvm.name, self.vm.name, False) + try: + dispvm.force_shutdown() + except qubes.qubes.QubesException: + pass + dispvm.remove_from_disk() + self.qc.lock_db_for_writing() + self.qc.load() + self.qc.pop(dispvm.qid) + self.qc.save() + self.qc.unlock_db() + return return_data + + +def run_one(vmname, command, show_output): + qc = qubes.qubes.QubesVmCollection() + qc.lock_db_for_reading() + qc.load() + qc.unlock_db() + vm = qc.get_vm_by_name(vmname) + if vm is None: + return vmname, "ERROR (vm not found)" + runner = ManageVM(qc, vm) + result = runner.salt_call( + ' '.join([pipes.quote(word) for word in command]), + return_output=show_output) + return vm.name, result + + +class ManageVMRunner(object): + """Call salt in multiple VMs at the same time""" + + def __init__(self, qc, vms, command, max_concurrency=4, show_output=False, + force_color=False): + super(ManageVMRunner, self).__init__() + self.vms = vms + self.qc = qc + self.command = command + self.max_concurrency = max_concurrency + self.show_output = show_output + self.force_color = force_color + + def collect_result(self, result_tuple): + name, result = result_tuple + if self.show_output and isinstance(result, list): + sys.stdout.write(name + ":\n") + # removing control characters, unless colors are enabled + if not self.force_color: + result = [ + ''.join([c for c in line if ord(c) >= 0x20]) for line in + result] + sys.stdout.write('\n'.join([' ' + line for line in result])) + sys.stdout.write('\n') + else: + print name + ": " + result + + def has_config(self, vm): + p = subprocess.Popen(['qubesctl', '--dom0-only', '--out=yaml', + '-l', 'quiet', + '--id={}'.format(vm.name), 'state.show_top'], + stdout=subprocess.PIPE + ) + (stdout, _) = p.communicate() + return stdout != 'local: {}\n' + + def run(self): + pool = multiprocessing.Pool(self.max_concurrency) + for vm in self.vms: + # TODO: add some override for this check + if self.has_config(vm): + pool.apply_async(run_one, + (vm.name, self.command, self.show_output), + callback=self.collect_result + ) + else: + self.collect_result((vm.name, "SKIP (nothing to do)")) + pool.close() + pool.join() + + +def qrexec_policy(src, dst, allow): + for service in ('qubes.Filecopy', 'qubes.VMShell'): + with open('/etc/qubes-rpc/policy/{}'.format(service), 'r+') as policy: + policy_rules = policy.readlines() + line = "{} {} allow,user=root\n".format(src, dst) + if allow: + policy_rules.insert(0, line) + else: + policy_rules.remove(line) + policy.truncate(0) + policy.seek(0) + policy.write(''.join(policy_rules)) diff --git a/qubessalt/__init__.pyc b/qubessalt/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06c73729ee3af3a54996bae11394386455f25f4c GIT binary patch literal 7536 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 literal 0 HcmV?d00001 diff --git a/rpm_spec/qubes-mgmt-salt-vm.spec b/rpm_spec/qubes-mgmt-salt-vm.spec index 2346952..caa79a9 100644 --- a/rpm_spec/qubes-mgmt-salt-vm.spec +++ b/rpm_spec/qubes-mgmt-salt-vm.spec @@ -32,6 +32,15 @@ Requires(post): /usr/bin/qubesctl %description formulas Qubes+Salt Management VM formulas. +%package connector +Summary: Interface for managing VM from dom0 +Group: System administration tools +BuildArch: noarch +Requires: salt-ssh + +%description connector +Interface for managing VM from dom0 + %prep # we operate on the current directory, so no need to unpack anything # symlink is to generate useful debuginfo packages @@ -42,6 +51,7 @@ ln -sf . %{name}-%{version} %build %install +make install-vm DESTDIR=%{buildroot} %post qubesctl saltutil.clear_cache -l quiet --out quiet > /dev/null || true @@ -57,4 +67,9 @@ qubesctl saltutil.sync_all refresh=true -l quiet --out quiet > /dev/null || true %files formulas %defattr(-,root,root) +%files connector +%defattr(-,root,root) +/etc/qubes-rpc/qubes.SaltLinuxVM +/usr/lib/qubes-vm-connector/ssh-wrapper + %changelog diff --git a/rpm_spec/qubes-mgmt-salt.spec b/rpm_spec/qubes-mgmt-salt.spec index 87ef874..f9d74de 100644 --- a/rpm_spec/qubes-mgmt-salt.spec +++ b/rpm_spec/qubes-mgmt-salt.spec @@ -60,6 +60,15 @@ Requires(post): /usr/bin/qubesctl %description shared-formulas Qubes+Salt Management shared (qubes-mgmt-all-*) formulas. +%package admin-tools +Summary: Management tools integrating dom0 and VM management +Group: System administration tools +BuildArch: noarch +Requires: qubes-mgmt-salt + +%description admin-tools +Tools to integrate dom0 and VM management into a single qubesctl tool. + %prep # we operate on the current directory, so no need to unpack anything # symlink is to generate useful debuginfo packages @@ -71,6 +80,7 @@ ln -sf . %{name}-%{version} %install make install DESTDIR=%{buildroot} LIBDIR=%{_libdir} BINDIR=%{_bindir} SBINDIR=%{_sbindir} SYSCONFDIR=%{_sysconfdir} +make install-dom0 DESTDIR=%{buildroot} %post qubesctl saltutil.clear_cache -l quiet --out quiet > /dev/null || true @@ -115,7 +125,13 @@ qubesctl saltutil.sync_all refresh=true -l quiet --out quiet > /dev/null || true /srv/salt/top.jinja /srv/salt/top.sls.old +%files admin-tools /usr/bin/qubesctl +%dir %{python_sitelib}/qubessalt-*egg-info +%{python_sitelib}/qubessalt-*egg-info/* +%dir %{python_sitelib}/qubessalt +%{python_sitelib}/qubessalt/__init__.py* + %files shared-formulas %defattr(-,root,root) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3143175 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +# coding=utf-8 +import setuptools + +if __name__ == '__main__': + setuptools.setup( + name='qubessalt', + version='1.0', + packages=['qubessalt'], + url='https://www.qubes-os.org', + license='GPLv2', + author='Marek Marczykowski-Górecki', + author_email='marmarek@invisiblethingslab.com', + description='VM configuration salt connector for Qubes', + ) diff --git a/ssh-wrapper b/ssh-wrapper new file mode 100755 index 0000000..334e119 --- /dev/null +++ b/ssh-wrapper @@ -0,0 +1,77 @@ +#!/usr/bin/python +# vim: fileencoding=utf-8 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +import argparse +import os +import socket +import subprocess +import sys + + +def ssh(args): + assert args[1] == '/bin/sh' + os.execl('/usr/bin/qrexec-client-vm', + 'qrexec-client-vm', args[0], 'qubes.VMShell') + return 1 + + +def scp(args): + assert len(args) == 2 and args[1].count(':') == 1 + src_path = args[0] + (dst_host, dst_path) = args[1].split(':') + subprocess.check_call(['qvm-copy-to-vm', dst_host, src_path]) + p = subprocess.Popen(['qrexec-client-vm', dst_host, 'qubes.VMShell'], + stdin=subprocess.PIPE + ) + p.communicate('sudo mv "/home/user/QubesIncoming/{}/{}" "{}"\n'.format( + socket.gethostname(), + os.path.basename(src_path), + dst_path + )) + if p.returncode != 0: + raise RuntimeError('Failed to write target file {}'.format(dst_path)) + return 0 + + +def parse_opts(args): + parser = argparse.ArgumentParser() + parser.add_argument('-o', action='append', nargs=1) + parser.add_argument('--version', '-V', action='store_true') + (opts, args) = parser.parse_known_args(args) + if opts.version: + print 'OpenSSH_6.6.1p1 qubes.VMShell wrapper, ' \ + 'OpenSSL 1.0.1k-fips 8 Jan 2015' + sys.exit(0) + return args + + +def main(args=None): + args = parse_opts(args) + if sys.argv[0].endswith('ssh'): + return ssh(args) + elif sys.argv[0].endswith('scp'): + return scp(args) + else: + raise RuntimeError('Unsupported program {} called'.format(sys.argv[0])) + +if __name__ == '__main__': + sys.exit(main())