commit efb24e362f4a805df15f3226e86a42f82e3a262c Author: Georg Pfuetzenreuter Date: Sat May 20 13:39:45 2023 +0200 Initial commit Signed-off-by: Georg Pfuetzenreuter diff --git a/Vagrantfile-Template b/Vagrantfile-Template new file mode 100644 index 0000000..e7d2b16 --- /dev/null +++ b/Vagrantfile-Template @@ -0,0 +1,50 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +if ! ( ENV['SCULLERY_BOX_NAME'] && ENV['SCULLERY_BOX_IMAGE'] ) || ! ( ENV['SCULLERY_MASTERS'] || ENV['SCULLERY_MINIONS'] ) + print('Missing parameters') + exit 1 +end + +salt_bootstrap = "test/bootstrap-salt-roots.sh" + +Vagrant.configure("2") do |config| + config.vm.provider "libvirt" + config.vm.box = ENV['SCULLERY_BOX_NAME'] + config.vm.box_url = ENV['SCULLERY_BOX_IMAGE'] + config.vm.provision "shell", inline: <<-SHELL + systemctl restart network + SHELL + if ENV['SCULLERY_MASTERS'] + ENV['SCULLERY_MASTERS'].split(',').each do |vmname| + config.vm.define "#{vmname}", primary: true do |master_config| + master_config.vm.hostname = "#{vmname}" + master_config.vm.provider :libvirt do |libvirt| + libvirt.memory = 768 + end + master_config.vm.provision "shell", path: salt_bootstrap + master_config.vm.provision "shell", inline: <<-SHELL + printf 'auto_accept: True\n' > /etc/salt/master.d/notsecure.conf + printf 'file_roots:\n base:\n - /srv/salt\n - /srv/formulas\n' > /etc/salt/master.d/roots.conf + systemctl enable --now salt-master + SHELL + end + end + end + if ENV['SCULLERY_MINIONS'] + ENV['SCULLERY_MINIONS'].split(',').each do |vmname| + config.vm.define "#{vmname}" do |minion_config| + minion_config.vm.hostname = "#{vmname}" + if ENV['SCULLERY_MASTERS'] + minion_config.vm.synced_folder '.', '/vagrant', disabled: true + minion_config.vm.provision "shell", inline: <<-SHELL + sed -i 's/^#master:.*/master: scullery-master0/' /etc/salt/minion + systemctl enable --now salt-minion + SHELL + else + minion_config.vm.provision "shell", path: salt_bootstrap + end + end + end + end +end diff --git a/scullery.py b/scullery.py new file mode 100644 index 0000000..41ad893 --- /dev/null +++ b/scullery.py @@ -0,0 +1,178 @@ +#!/usr/bin/python3 +""" +Copyright 2023, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. + +--- + +Scullery - a SaltStack testing tool. +""" + +from argparse import ArgumentParser +from configparser import ConfigParser +import logging +import os +import sys + +try: + import vagrant +except ImportError as myerror: + print('Could not load python-vagrant') + sys.exit(1) + +argparser = ArgumentParser() +config = ConfigParser() +env = os.environ.copy() + +arggroup = argparser.add_mutually_exclusive_group() +argparser.add_argument('--debug', help='Print extremely verbose output', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) +argparser.add_argument('--config', help='Specify the configuration file to use', default='{}/scullery.ini'.format(os.path.abspath(os.path.dirname(__file__)))) +argparser.add_argument('--suite', help='Specify the suite to run', required=True) +arggroup.add_argument('--stop', help='Stop running machines', action='store_true') +arggroup.add_argument('--test', help='Start machines and run tests', action='store_true') +arggroup.add_argument('--status', help='Get Vagrant deployment status', action='store_true') + +args = argparser.parse_args() +config.read(args.config) + +vmprefix = 'scullery' + +def _abort(msg): + log.error(msg) + sys.exit(1) + +def _config(): + configmap = {'box': {}, 'suites': {}} + if not config.options('box'): + _abort('No "box" section found in the configuration file') + for option in config.options('box'): + configmap['box'][option] = config.get('box', option) + suites = [section for section in config.sections() if section.startswith('suite.')] + if not len(suites): + _abort('No suites configured') + log.debug('Suites: {}'.format(str(suites))) + for section in suites: + suite = section.replace('suite.', '') + configmap['suites'][suite] = {} + for option in config.options(section): + if option in ['masters', 'minions']: + value = config.getint(section, option) + else: + value = config.get(section, option) + configmap['suites'][suite][option] = value + log.debug('Config map: {}'.format(str(configmap))) + return configmap + +def _vagrant(quiet=False): + return vagrant.Vagrant(quiet_stdout=False, quiet_stderr=quiet) + +def genvms(flavor, amount): + vms = [] + for i in range(amount): + vms.append('{}-{}{}'.format(vmprefix, flavor, i)) + return vms + +def vagrant_env(box_name, box_image, minions=None, masters=None, vagrantfile=None): + fh = open('.scullery_env', 'w') + if vagrantfile is not None: + env['VAGRANT_VAGRANTFILE'] = vagrantfile + fh.write('VAGRANT_VAGRANTFILE={}\n'.format(vagrantfile)) + env['SCULLERY_BOX_NAME'] = box_name + env['SCULLERY_BOX_IMAGE'] = box_image + fh.write('SCULLERY_BOX_NAME={}\nSCULLERY_BOX_IMAGE={}\n'.format(box_name, box_image)) + if masters is not None: + env_masters = ','.join(masters) + env['SCULLERY_MASTERS'] = env_masters + fh.write('SCULLERY_MASTERS={}\n'.format(env_masters)) + if minions is not None: + env_minions = ','.join(minions) + env['SCULLERY_MINIONS'] = env_minions + fh.write('SCULLERY_MINIONS={}\n'.format(env_minions)) + #log.debug('Environment is: {}'.format(str(env))) + fh.close() + + v.env = env + +def vagrant_isup(suite): + ok = 0 + nok = 0 + statuses = v.status() + total = len(statuses) + for status in statuses: + if status.state == 'running': + ok += 1 + else: + nok +=1 + if ok == total: + return True, None + elif nok == total: + return False, True + else: + return False, False + +def main_interactive(): + configmap = _config() + box = configmap['box'] + suites = configmap['suites'] + suite = args.suite + if suite not in suites: + _abort('No suite named {}'.format(suite)) + suiteconf = configmap['suites'][suite] + minions = None + masters = None + if suiteconf.get('minions', 0) > 0: + minions = genvms('minion', suiteconf['minions']) + if suiteconf.get('masters', 0) > 0: + masters = genvms('master', suiteconf['masters']) + vagrant_env(box['name'], box['image'], minions, masters, box['file']) + if args.status: + log.info('Status report: {}'.format(v.status())) + return True + status = vagrant_isup(suite) + if status[0] is True and status[1] is None: + if args.stop is True: + log.info('Destroying machines ...') + v.destroy() + if vagrant_isup(suite)[0] is False: + log.debug('OK') + else: + _abort('Destruction failed') + else: + log.info('Deployment is already running') + elif status[0] is False: + if status[1] is True: + log.debug('Deployment is not running') + elif status[1] is False: + log.warning('Deployment is in an inconsistent state, destroying ...') + try: + v.destroy() + except Exception as myerror: + log.exception(myerror) + _abort('Unhandled error') + + if args.stop is False: + log.info('Launching {} ...'.format(suite)) + v.up() + if vagrant_isup(suite)[0] is True: + log.debug('OK') + else: + _abort('Start failed') + +if __name__ == '__main__': + logging.basicConfig(format='%(asctime)s %(levelname)s - %(funcName)s: %(message)s', datefmt='%H:%M:%S') + log = logging.getLogger('scullery') + log.setLevel(args.loglevel) + log.debug(args) + if args.loglevel == logging.INFO: + log_stderr = True + else: + log_stderr = False + v = _vagrant(log_stderr) + + main_interactive() +else: + v = _vagrant()