Compare commits
No commits in common. "main" and "v0.1" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
.vagrant/
|
|
||||||
__pycache__/
|
|
||||||
.scullery_*
|
|
79
README.md
79
README.md
@ -2,83 +2,4 @@
|
|||||||
|
|
||||||
This is intended to be a replacement for [KitchenCI](https://test-kitchen.chef.io/) and [kitchen-salt](https://github.com/saltstack/kitchen-salt), focussing on testing SaltStack states using Vagrant and Pytest/Testinfra.
|
This is intended to be a replacement for [KitchenCI](https://test-kitchen.chef.io/) and [kitchen-salt](https://github.com/saltstack/kitchen-salt), focussing on testing SaltStack states using Vagrant and Pytest/Testinfra.
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
So far, a packaged installation is possible on openSUSE Tumbleweed:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ zypper ar -f https://download.opensuse.org/repositories/home:/crameleon:/Scullery/openSUSE_Tumbleweed/home:crameleon:Scullery.repo
|
|
||||||
$ zypper in scullery
|
|
||||||
```
|
|
||||||
|
|
||||||
This will pull in `vagrant`, `vagrant-libvirt`, `python3-vagrant`, `python3-pytest` and `python3-pytest-testinfra` as dependencies.
|
|
||||||
|
|
||||||
Installation on Leap 15.4 is possible, however will currently cause dependency issues with the needed `python-vagrant` library.
|
|
||||||
|
|
||||||
It is possible to run `scullery.py` from a Git checkout of this repository if the dependencies are resolved manually.
|
|
||||||
|
|
||||||
Currently the script behaves purely as a console application and imports as a Python module are not supported.
|
|
||||||
|
|
||||||
## Basic usage
|
|
||||||
|
|
||||||
```
|
|
||||||
scullery -h
|
|
||||||
usage: scullery [-h] [--debug] [--config CONFIG] [--env] --suite SUITE [--stop | --test | --status | --refresh] [--force-stop]
|
|
||||||
|
|
||||||
options:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
--debug Print extremely verbose output
|
|
||||||
--config CONFIG Specify the configuration file to use
|
|
||||||
--env Write environment file for direct use of Vagrant
|
|
||||||
--suite SUITE Specify the suite to run
|
|
||||||
--stop Stop running machines
|
|
||||||
--test Start machines and run tests
|
|
||||||
--status Get Vagrant deployment status
|
|
||||||
--refresh Re-sync files and re-run bootstrap scripts
|
|
||||||
--force-stop Invoke Vagrant destruction without having detected any running VM's
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command line examples
|
|
||||||
|
|
||||||
* To run a complete test suite:
|
|
||||||
`$ scullery --suite <name of suite> --test`
|
|
||||||
|
|
||||||
* To create Vagrant VM's without invoking the tests:
|
|
||||||
`$ scullery --suite <name of suite>`
|
|
||||||
|
|
||||||
* To refresh the VM's Salt files:
|
|
||||||
`$ scullery --suite <name of suite> --refresh`
|
|
||||||
|
|
||||||
* To stop the VM's and tidy up:
|
|
||||||
`$ scullery --suite <name of suite> --stop`
|
|
||||||
|
|
||||||
## Interacting with Vagrant
|
|
||||||
|
|
||||||
It is possible to directly interact with the Vagrant environment templated by Scullery. To do so, generate an environment file by adding `--env` to any command line call - for example:
|
|
||||||
|
|
||||||
`$ scullery --suite <name of suite> --env`
|
|
||||||
|
|
||||||
This will write a `.scullery_env` file to the current working directory. Export the environment variables in the file using your Shell, afterwards use `vagrant` like you normally would. Example for `bash`/`sh`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ set -a; . .scullery_env; set +a
|
|
||||||
$ vagrant ssh # or any other Vagrant command
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not forget to unset the exported variables before you call Scullery with a different `--suite`!
|
|
||||||
|
|
||||||
## Hacking
|
|
||||||
|
|
||||||
To test Scullery itself, call Pytest with your favourite arguments from the repository root:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ pytest -v -rx -x tests/*.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to not have _any_ other Scullery based Vagrant VM's running in the same Libvirt session and no `SCULLERY_*` or `VAGRANT_*` variables exported in your shell.
|
|
||||||
|
|
||||||
The complete test suite takes about 30 minutes to execute on an Intel i7-6820HQ CPU with SSHD storage. If you want to test only certain functionality, simply add `-k <name of test function>`.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the European Union Public License found in the file `LICENSE`.
|
|
||||||
|
7
TODO.md
7
TODO.md
@ -1,7 +0,0 @@
|
|||||||
# To-Do's
|
|
||||||
|
|
||||||
- Introduce `--apply` argument to execute `state.apply` on VM's without chaining to test executions
|
|
||||||
- Do not restart the network stack on `--refresh` (the `systemctl restart network` call is needed on the first boot to get Avahi DNS resolution to work - there is currently no logic to skip the command on subsequent executions of the bootstrap script)
|
|
||||||
- Add example configurations
|
|
||||||
- Use `pyproject.toml` style packaging to allow for installation as a Python module
|
|
||||||
- Resolve Pytest warnings
|
|
@ -14,11 +14,7 @@ Vagrant.configure("2") do |config|
|
|||||||
config.vm.box = ENV['SCULLERY_BOX_NAME']
|
config.vm.box = ENV['SCULLERY_BOX_NAME']
|
||||||
config.vm.box_url = ENV['SCULLERY_BOX_IMAGE']
|
config.vm.box_url = ENV['SCULLERY_BOX_IMAGE']
|
||||||
config.vm.provision "shell", inline: <<-SHELL
|
config.vm.provision "shell", inline: <<-SHELL
|
||||||
if [ ! -f '/var/adm/scullery.configured' ]
|
|
||||||
then
|
|
||||||
systemctl restart network
|
systemctl restart network
|
||||||
touch /var/adm/scullery.configured
|
|
||||||
fi
|
|
||||||
SHELL
|
SHELL
|
||||||
if ENV['SCULLERY_MASTERS']
|
if ENV['SCULLERY_MASTERS']
|
||||||
ENV['SCULLERY_MASTERS'].split(',').each do |vmname|
|
ENV['SCULLERY_MASTERS'].split(',').each do |vmname|
|
||||||
@ -27,14 +23,14 @@ Vagrant.configure("2") do |config|
|
|||||||
master_config.vm.provider :libvirt do |libvirt|
|
master_config.vm.provider :libvirt do |libvirt|
|
||||||
libvirt.memory = 768
|
libvirt.memory = 768
|
||||||
end
|
end
|
||||||
|
if salt_bootstrap
|
||||||
|
master_config.vm.provision "shell", path: salt_bootstrap
|
||||||
|
end
|
||||||
master_config.vm.provision "shell", env: {'SALT_FILE_ROOTS': salt_file_roots}, inline: <<-SHELL
|
master_config.vm.provision "shell", env: {'SALT_FILE_ROOTS': salt_file_roots}, inline: <<-SHELL
|
||||||
printf 'auto_accept: True\n' > /etc/salt/master.d/notsecure.conf
|
printf 'auto_accept: True\n' > /etc/salt/master.d/notsecure.conf
|
||||||
printf "$SALT_FILE_ROOTS" > /etc/salt/master.d/roots.conf
|
printf "$SALT_FILE_ROOTS" > /etc/salt/master.d/roots.conf
|
||||||
systemctl enable --now salt-master
|
systemctl enable --now salt-master
|
||||||
SHELL
|
SHELL
|
||||||
if salt_bootstrap
|
|
||||||
master_config.vm.provision "shell", path: salt_bootstrap
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
56
scullery.py
56
scullery.py
@ -17,7 +17,6 @@ from configparser import ConfigParser
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pytest import ExitCode
|
|
||||||
|
|
||||||
argparser = ArgumentParser()
|
argparser = ArgumentParser()
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
@ -48,11 +47,7 @@ is_packaged = False
|
|||||||
if is_packaged:
|
if is_packaged:
|
||||||
vagfile='/usr/share/scullery/{}'.format(vagfile)
|
vagfile='/usr/share/scullery/{}'.format(vagfile)
|
||||||
else:
|
else:
|
||||||
try:
|
vagfile='{}/{}'.format(os.path.abspath(os.path.dirname(__file__)), vagfile)
|
||||||
me = __file__
|
|
||||||
except NameError:
|
|
||||||
me = sys.argv[0]
|
|
||||||
vagfile='{}/{}'.format(os.path.abspath(os.path.dirname(me)), vagfile)
|
|
||||||
|
|
||||||
def _abort(msg):
|
def _abort(msg):
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
@ -60,7 +55,7 @@ def _abort(msg):
|
|||||||
|
|
||||||
def _config():
|
def _config():
|
||||||
configmap = {'boxes': {}, 'suites': {}, 'tests': {}}
|
configmap = {'boxes': {}, 'suites': {}, 'tests': {}}
|
||||||
if not 'box' in config.sections():
|
if not config.options('box'):
|
||||||
_abort('No "box" section found in the configuration file')
|
_abort('No "box" section found in the configuration file')
|
||||||
multis = {'boxes': {'prefix': 'box.', 'singular': 'box'}, 'suites': {'prefix': 'suite.', 'singular': 'suite'}, 'tests': {'prefix': 'test.', 'singular': 'test'}}
|
multis = {'boxes': {'prefix': 'box.', 'singular': 'box'}, 'suites': {'prefix': 'suite.', 'singular': 'suite'}, 'tests': {'prefix': 'test.', 'singular': 'test'}}
|
||||||
for multi, multiconf in multis.items():
|
for multi, multiconf in multis.items():
|
||||||
@ -78,7 +73,6 @@ def _config():
|
|||||||
if onemulti in config.sections():
|
if onemulti in config.sections():
|
||||||
for option in config.options(onemulti):
|
for option in config.options(onemulti):
|
||||||
for collection in configmap[multi]:
|
for collection in configmap[multi]:
|
||||||
if not option in configmap[multi][collection]:
|
|
||||||
configmap[multi][collection][option] = config.get(onemulti, option)
|
configmap[multi][collection][option] = config.get(onemulti, option)
|
||||||
if multi in ['boxes', 'suites']:
|
if multi in ['boxes', 'suites']:
|
||||||
if not len(lowconf):
|
if not len(lowconf):
|
||||||
@ -106,8 +100,6 @@ def _setenv(envmap, dump=False):
|
|||||||
env[variable] = value
|
env[variable] = value
|
||||||
if dump:
|
if dump:
|
||||||
fh.write(f'{variable}={value}\n')
|
fh.write(f'{variable}={value}\n')
|
||||||
elif variable in env:
|
|
||||||
del env[variable]
|
|
||||||
if dump:
|
if dump:
|
||||||
fh.close()
|
fh.close()
|
||||||
|
|
||||||
@ -144,42 +136,24 @@ def vagrant_sshconfig(outfile):
|
|||||||
with open(outfile, 'w') as fh:
|
with open(outfile, 'w') as fh:
|
||||||
fh.write(ssh_config)
|
fh.write(ssh_config)
|
||||||
|
|
||||||
def _saltcmd(target):
|
def runapply(state, target):
|
||||||
if target == 'local':
|
if target == 'local':
|
||||||
saltcmd = 'salt-call --local'
|
saltcmd = 'salt-call --local'
|
||||||
else:
|
else:
|
||||||
saltcmd = 'salt -t10 {}'.format(target)
|
saltcmd = 'salt {}'.format(target)
|
||||||
return saltcmd
|
|
||||||
|
|
||||||
def runping(target):
|
|
||||||
saltcmd = _saltcmd(target)
|
|
||||||
sshout = v.ssh(command='sudo {} test.ping'.format(saltcmd))
|
|
||||||
log.info('\n{}\n'.format(str(sshout)))
|
|
||||||
return sshout
|
|
||||||
|
|
||||||
def runapply(state, target):
|
|
||||||
saltcmd = _saltcmd(target)
|
|
||||||
sshout = v.ssh(command='sudo {} state.apply {}'.format(saltcmd, state))
|
sshout = v.ssh(command='sudo {} state.apply {}'.format(saltcmd, state))
|
||||||
log.info('\n{}\n'.format(str(sshout)))
|
log.info('\n{}\n'.format(str(sshout)))
|
||||||
|
|
||||||
def runtests(payload, hosts):
|
def runtests(payload, hosts):
|
||||||
|
if not os.path.isfile(sshfile):
|
||||||
vagrant_sshconfig(sshfile)
|
vagrant_sshconfig(sshfile)
|
||||||
testresult = pytest.main(['--verbose', '--hosts={}'.format(','.join(hosts)), '--ssh-config={}'.format(sshfile), payload])
|
testresult = pytest.main(['--verbose', '--hosts={}'.format(','.join(hosts)), '--ssh-config={}'.format(sshfile), payload])
|
||||||
log.debug('Test result is {}'.format(str(testresult.value)))
|
log.debug('Test result is {}'.format(str(testresult)))
|
||||||
if testresult == ExitCode.OK:
|
if not testresult:
|
||||||
log.debug('Test succeeded')
|
|
||||||
else:
|
|
||||||
log.warning('Tests failed')
|
log.warning('Tests failed')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cleanup():
|
|
||||||
for file in [envfile, sshfile]:
|
|
||||||
if os.path.isfile(file):
|
|
||||||
log.debug('Removing {}'.format(file))
|
|
||||||
os.remove(file)
|
|
||||||
|
|
||||||
|
|
||||||
def main_interactive():
|
def main_interactive():
|
||||||
configmap = _config()
|
configmap = _config()
|
||||||
boxes = configmap['boxes']
|
boxes = configmap['boxes']
|
||||||
@ -194,7 +168,7 @@ def main_interactive():
|
|||||||
_abort('Specified suite does not reference a box')
|
_abort('Specified suite does not reference a box')
|
||||||
boxconf = configmap['boxes'].get(box, None)
|
boxconf = configmap['boxes'].get(box, None)
|
||||||
if boxconf is None:
|
if boxconf is None:
|
||||||
_abort('Suite references an undefined box')
|
_abort('Suite referencs an undefined box')
|
||||||
box_name = boxconf.get('name', None)
|
box_name = boxconf.get('name', None)
|
||||||
box_image = boxconf.get('image', None)
|
box_image = boxconf.get('image', None)
|
||||||
box_file = boxconf.get('file', vagfile)
|
box_file = boxconf.get('file', vagfile)
|
||||||
@ -216,6 +190,10 @@ def main_interactive():
|
|||||||
if True in [args.stop, args.force_stop]:
|
if True in [args.stop, args.force_stop]:
|
||||||
log.info('Destroying machines ...')
|
log.info('Destroying machines ...')
|
||||||
v.destroy()
|
v.destroy()
|
||||||
|
for file in [envfile, sshfile]:
|
||||||
|
if os.path.isfile(file):
|
||||||
|
log.debug('Removing {}'.format(file))
|
||||||
|
os.remove(file)
|
||||||
if vagrant_isup(suite)[0] is False:
|
if vagrant_isup(suite)[0] is False:
|
||||||
log.debug('OK')
|
log.debug('OK')
|
||||||
else:
|
else:
|
||||||
@ -224,9 +202,7 @@ def main_interactive():
|
|||||||
log.info('Deployment is already running')
|
log.info('Deployment is already running')
|
||||||
elif args.refresh:
|
elif args.refresh:
|
||||||
log.info('Deployment is running, initiating refresh ...')
|
log.info('Deployment is running, initiating refresh ...')
|
||||||
_cleanup()
|
|
||||||
v.provision()
|
v.provision()
|
||||||
vagrant_sshconfig(sshfile)
|
|
||||||
elif status[0] is False:
|
elif status[0] is False:
|
||||||
if status[1] is True:
|
if status[1] is True:
|
||||||
log.debug('Deployment is not running')
|
log.debug('Deployment is not running')
|
||||||
@ -246,9 +222,6 @@ def main_interactive():
|
|||||||
else:
|
else:
|
||||||
_abort('Start failed')
|
_abort('Start failed')
|
||||||
|
|
||||||
if args.stop:
|
|
||||||
_cleanup()
|
|
||||||
|
|
||||||
if args.test:
|
if args.test:
|
||||||
test = suiteconf.get('test', None)
|
test = suiteconf.get('test', None)
|
||||||
if test is None:
|
if test is None:
|
||||||
@ -263,11 +236,6 @@ def main_interactive():
|
|||||||
log.debug('state.apply requested')
|
log.debug('state.apply requested')
|
||||||
if masters is not None:
|
if masters is not None:
|
||||||
target = 'scullery-*'
|
target = 'scullery-*'
|
||||||
count = 0
|
|
||||||
while not runping(target):
|
|
||||||
if count == 5:
|
|
||||||
_abort('Unable to reach minions')
|
|
||||||
count += 1
|
|
||||||
else:
|
else:
|
||||||
target = 'local'
|
target = 'local'
|
||||||
runapply(testconf['apply'], target)
|
runapply(testconf['apply'], target)
|
||||||
|
@ -1,182 +0,0 @@
|
|||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Testing functions for Scullery - a SaltStack testing tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
import dotenv
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_arguments(script_runner, script):
|
|
||||||
result = script_runner.run(script)
|
|
||||||
assert result.success is False
|
|
||||||
assert 'the following arguments are required: --suite' in result.stderr
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_config(script_runner, script):
|
|
||||||
result = script_runner.run(script, '--suite', 'foo', print_result=True)
|
|
||||||
assert result.success is False
|
|
||||||
assert 'Unable to locate configuration file' in result.stderr
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('section,message', [
|
|
||||||
('box', 'No "box" section found in the configuration file'),
|
|
||||||
('suites', 'No suites configured'),
|
|
||||||
('boxes', 'No boxes configured'),
|
|
||||||
('suite_box', 'Specified suite does not reference a box'),
|
|
||||||
('box_name', 'Box configuration is incomplete'),
|
|
||||||
('box_image', 'Box configuration is incomplete'),
|
|
||||||
])
|
|
||||||
def test_config_incomplete(script_runner, script, testbase, section, message):
|
|
||||||
configfile = '{}/configs/missing_{}.ini'.format(testbase, section)
|
|
||||||
result = script_runner.run(script, '--config', configfile, '--suite', 'one_minion')
|
|
||||||
assert result.success is False
|
|
||||||
assert message in result.stderr
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('section,message', [
|
|
||||||
('suite_box', 'Suite references an undefined box')
|
|
||||||
])
|
|
||||||
def test_config_undefined(script_runner, script, testbase, section, message):
|
|
||||||
configfile = '{}/configs/undefined_{}.ini'.format(testbase, section)
|
|
||||||
result = script_runner.run(script, '--config', configfile, '--suite', 'one_minion')
|
|
||||||
assert result.success is False
|
|
||||||
assert message in result.stderr
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite,report', [
|
|
||||||
('one_minion', "[Status(name='scullery-minion0', state='not_created', provider='libvirt')]"),
|
|
||||||
('two_minions', "[Status(name='scullery-minion0', state='not_created', provider='libvirt'), Status(name='scullery-minion1', state='not_created', provider='libvirt')]"),
|
|
||||||
('one_master', "[Status(name='scullery-master0', state='not_created', provider='libvirt')]"),
|
|
||||||
('one_minion_one_master', "[Status(name='scullery-master0', state='not_created', provider='libvirt'), Status(name='scullery-minion0', state='not_created', provider='libvirt')]"),
|
|
||||||
('two_minions_one_master', "[Status(name='scullery-master0', state='not_created', provider='libvirt'), Status(name='scullery-minion0', state='not_created', provider='libvirt'), Status(name='scullery-minion1', state='not_created', provider='libvirt')]")
|
|
||||||
])
|
|
||||||
def test_status_report_not_running(script_runner, script, config, suite, report):
|
|
||||||
result = script_runner.run(script, '--config', config, '--suite', suite, '--status', '--debug', '--env')
|
|
||||||
assert result.success
|
|
||||||
assert result.stderr.endswith("INFO - main_interactive: Status report: {}\n".format(report))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite', [
|
|
||||||
'one_minion',
|
|
||||||
'two_minions',
|
|
||||||
'one_master',
|
|
||||||
'one_minion_one_master',
|
|
||||||
'two_minions_one_master'
|
|
||||||
])
|
|
||||||
def test_launch_stop(script_runner, script, virt, config, suite):
|
|
||||||
cmd = (script, '--config', config, '--suite', suite)
|
|
||||||
result = script_runner.run(*cmd)
|
|
||||||
assert result.success
|
|
||||||
assert 'Launching {} ...\n'.format(suite) in result.stderr
|
|
||||||
domains = []
|
|
||||||
print(virt.getURI())
|
|
||||||
print(virt.listDomainsID())
|
|
||||||
for domain in virt.listDomainsID():
|
|
||||||
print(domain)
|
|
||||||
domains.append(virt.lookupByID(domain).name())
|
|
||||||
print(str(domains))
|
|
||||||
# consider refining this
|
|
||||||
assert any('scullery' in domain for domain in domains)
|
|
||||||
result = script_runner.run(*cmd, '--stop')
|
|
||||||
assert result.success
|
|
||||||
assert 'Destroying machines ...\n' in result.stderr
|
|
||||||
domains = []
|
|
||||||
for domain in virt.listDomainsID():
|
|
||||||
domains.append(virt.lookupByID(domain).name())
|
|
||||||
assert not any('scullery' in domain for domain in domains)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite,masters,minions', [
|
|
||||||
('one_minion', None, 'scullery-minion0'),
|
|
||||||
('two_minions', None, 'scullery-minion0,scullery-minion1'),
|
|
||||||
('one_master', 'scullery-master0', None),
|
|
||||||
('one_minion_one_master', 'scullery-master0', 'scullery-minion0'),
|
|
||||||
('two_minions_one_master', 'scullery-master0', 'scullery-minion0,scullery-minion1')
|
|
||||||
])
|
|
||||||
def test_envfile(script_runner, script, config, suite, masters, minions):
|
|
||||||
cmd = (script, '--config', config, '--suite', suite)
|
|
||||||
envfile = '.scullery_env'
|
|
||||||
result = script_runner.run(*cmd, '--env')
|
|
||||||
assert result.success
|
|
||||||
assert os.path.isfile(envfile)
|
|
||||||
envmap = dotenv.dotenv_values(envfile)
|
|
||||||
if masters is not None:
|
|
||||||
assert envmap['SCULLERY_MASTERS'] == masters
|
|
||||||
if minions is not None:
|
|
||||||
assert envmap['SCULLERY_MINIONS'] == minions
|
|
||||||
script_runner.run(*cmd, '--stop')
|
|
||||||
assert os.path.isfile(envfile) is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
def test_test_undeclared(script_runner, script, config):
|
|
||||||
result = script_runner.run(script, '--config', config, '--suite', 'one_minion', '--test')
|
|
||||||
assert not result.success
|
|
||||||
assert result.stderr.endswith('Tests requested but not declared in suite configuration\n')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
def test_test_undefined(script_runner, script, config):
|
|
||||||
result = script_runner.run(script, '--config', config, '--suite', 'one_minion_bogus_test', '--test')
|
|
||||||
assert not result.success
|
|
||||||
assert result.stderr.endswith('Specified test is not defined\n')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
def test_test_incomplete(script_runner, script, config):
|
|
||||||
result = script_runner.run(script, '--config', config, '--suite', 'one_minion_bogus_test_2', '--test')
|
|
||||||
assert not result.success
|
|
||||||
assert result.stderr.endswith('Incomplete test configuration\n')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite', [
|
|
||||||
'one_minion',
|
|
||||||
'two_minions',
|
|
||||||
'one_master',
|
|
||||||
'one_minion_one_master',
|
|
||||||
'two_minions_one_master',
|
|
||||||
'one_minion_bootstrap',
|
|
||||||
'one_minion_one_master_bootstrap',
|
|
||||||
'one_minion_salt',
|
|
||||||
'two_minions_one_master_salt'
|
|
||||||
])
|
|
||||||
def test_stop(script_runner, script, config, suite):
|
|
||||||
cmd = (script, '--config', config, '--suite', suite, '--stop', '--force')
|
|
||||||
result = script_runner.run(*cmd)
|
|
||||||
assert result.success
|
|
||||||
|
|
||||||
|
|
||||||
def test_stop_success(virt):
|
|
||||||
domains = []
|
|
||||||
for domain in virt.listDomainsID():
|
|
||||||
domains.append(virt.lookupByID(domain).name())
|
|
||||||
assert not any('scullery' in domain for domain in domains)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite', [
|
|
||||||
'one_minion',
|
|
||||||
'two_minions',
|
|
||||||
'one_master',
|
|
||||||
'one_minion_one_master',
|
|
||||||
'two_minions_one_master'
|
|
||||||
])
|
|
||||||
def test_already_stopped(script_runner, script, virt, config, suite):
|
|
||||||
cmd = (script, '--config', config, '--suite', suite, '--stop', '--debug')
|
|
||||||
result = script_runner.run(*cmd)
|
|
||||||
assert result.success
|
|
||||||
assert result.stderr.endswith('DEBUG - main_interactive: Deployment is not running\n')
|
|
@ -1,43 +0,0 @@
|
|||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Testing functions for Scullery - a SaltStack testing tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
from conftest import loadenv
|
|
||||||
import sys
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite', [
|
|
||||||
'one_minion_bootstrap',
|
|
||||||
'one_minion_one_master_bootstrap',
|
|
||||||
])
|
|
||||||
def test_bootstrap(script_runner, script, config, suite, vag, testbase):
|
|
||||||
bootstrap_script = '{}/scripts/bootstrap_hello_world.txt'.format(testbase)
|
|
||||||
do_digest = False
|
|
||||||
if sys.version_info[1] > 10:
|
|
||||||
do_digest = True
|
|
||||||
import hashlib
|
|
||||||
with open(bootstrap_script, 'rb') as fh:
|
|
||||||
digest_local = hashlib.file_digest(fh, 'md5')
|
|
||||||
cmd = (script, '--config', config, '--suite', suite)
|
|
||||||
result = script_runner.run(*cmd, '--env')
|
|
||||||
assert result.success
|
|
||||||
assert result.stderr.endswith('main_interactive: Launching {} ...\n'.format(suite))
|
|
||||||
v = vag
|
|
||||||
v.env = loadenv()
|
|
||||||
assert v.ssh(command='cat /srv/hello_world.txt') == 'Hello world!\n'
|
|
||||||
if do_digest:
|
|
||||||
digest_remote = v.ssh(command='md5sum /srv/hello_world.txt | awk "{ print $1 }"')
|
|
||||||
assert digest_local == digest_remote
|
|
||||||
assert script_runner.run(*cmd, '--stop')
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
|||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Testing functions for Scullery - a SaltStack testing tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('config', ['complete'], indirect=True)
|
|
||||||
@pytest.mark.parametrize('suite,count', [
|
|
||||||
('one_minion_salt', 0),
|
|
||||||
('two_minions_one_master_salt', 1)
|
|
||||||
])
|
|
||||||
def test_salt_test(script_runner, script, config, suite, count):
|
|
||||||
cmd = (script, '--config', config, '--suite', suite)
|
|
||||||
result = script_runner.run(*cmd, '--debug', '--env', '--test')
|
|
||||||
assert result.success
|
|
||||||
for message in [
|
|
||||||
'File /srv/hello_world.txt',
|
|
||||||
'Succeeded: 1',
|
|
||||||
'DEBUG - main_interactive: state.apply requested',
|
|
||||||
'INFO - main_interactive: Initiating tests ...',
|
|
||||||
'DEBUG - runtests: Test result is 0',
|
|
||||||
'DEBUG - runtests: Test succeeded'
|
|
||||||
]:
|
|
||||||
assert message in result.stderr
|
|
||||||
assert not any(term in result.stderr for term in [
|
|
||||||
'FAILED', 'WARNING - runtests: Tests failed',
|
|
||||||
'DEBUG - runtests: Test result is 1', 'AssertionError'
|
|
||||||
])
|
|
||||||
if count > 0:
|
|
||||||
minions = [0, 1]
|
|
||||||
else:
|
|
||||||
minions = [0]
|
|
||||||
for m in minions:
|
|
||||||
assert 'test_hello_world_content[paramiko://scullery-minion%i] PASSED' % m in result.stdout
|
|
||||||
assert script_runner.run(*cmd, '--stop')
|
|
@ -1,71 +0,0 @@
|
|||||||
[box]
|
|
||||||
bootstrap=tests/scripts/bootstrap.sh
|
|
||||||
name=Tumbleweed.x86_64
|
|
||||||
image=https://download.opensuse.org/repositories/home:/crameleon:/appliances/openSUSE_Tumbleweed/boxes/Tumbleweed.x86_64.json
|
|
||||||
|
|
||||||
[box.tumbleweed]
|
|
||||||
|
|
||||||
[box.tumbleweed_bootstrap]
|
|
||||||
bootstrap=tests/scripts/bootstrap_hello_world.sh
|
|
||||||
|
|
||||||
[box.tumbleweed_salt]
|
|
||||||
bootstrap=tests/scripts/bootstrap_salt.sh
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.one_minion_bootstrap]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed_bootstrap
|
|
||||||
|
|
||||||
[suite.one_minion_one_master_bootstrap]
|
|
||||||
minions=1
|
|
||||||
masters=1
|
|
||||||
box=tumbleweed_bootstrap
|
|
||||||
|
|
||||||
[suite.two_minions]
|
|
||||||
minions=2
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.one_minion_one_master]
|
|
||||||
minions=1
|
|
||||||
masters=1
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.one_master]
|
|
||||||
masters=1
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.two_minions_one_master]
|
|
||||||
minions=2
|
|
||||||
masters=1
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.one_minion_bogus_test]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
||||||
test=cashew
|
|
||||||
|
|
||||||
[suite.one_minion_bogus_test_2]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
||||||
test=bogus
|
|
||||||
|
|
||||||
[test.hello]
|
|
||||||
apply=hello_world
|
|
||||||
test=tests/salt/hello_world/tests/test_hello.py
|
|
||||||
|
|
||||||
[test.bogus]
|
|
||||||
apply=cashew
|
|
||||||
|
|
||||||
[suite.one_minion_salt]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed_salt
|
|
||||||
test=hello
|
|
||||||
|
|
||||||
[suite.two_minions_one_master_salt]
|
|
||||||
masters=1
|
|
||||||
minions=2
|
|
||||||
box=tumbleweed_salt
|
|
||||||
test=hello
|
|
@ -1,5 +0,0 @@
|
|||||||
[suite]
|
|
||||||
foo=bar
|
|
||||||
|
|
||||||
[test.foo]
|
|
||||||
foo=bar
|
|
@ -1,9 +0,0 @@
|
|||||||
[box]
|
|
||||||
bootstrap=scripts/bootstrap.sh
|
|
||||||
|
|
||||||
[box.tumbleweed]
|
|
||||||
name=mybox
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
@ -1,9 +0,0 @@
|
|||||||
[box]
|
|
||||||
bootstrap=scripts/bootstrap.sh
|
|
||||||
|
|
||||||
[box.tumbleweed]
|
|
||||||
image=https://download.opensuse.org/repositories/home:/crameleon:/appliances/openSUSE_Tumbleweed/boxes/Tumbleweed.x86_64.json
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
@ -1,8 +0,0 @@
|
|||||||
[box]
|
|
||||||
image=https://example.com/example.box
|
|
||||||
|
|
||||||
[suite.example_suite]
|
|
||||||
minions=1
|
|
||||||
|
|
||||||
[test.example]
|
|
||||||
test=example.py
|
|
@ -1,10 +0,0 @@
|
|||||||
[box]
|
|
||||||
bootstrap=scripts/bootstrap.sh
|
|
||||||
|
|
||||||
[box.tumbleweed]
|
|
||||||
name=Tumbleweed.x86_64
|
|
||||||
image=https://download.opensuse.org/repositories/home:/crameleon:/appliances/openSUSE_Tumbleweed/boxes/Tumbleweed.x86_64.json
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
[box]
|
|
||||||
image=https://example.com/example.box
|
|
||||||
|
|
||||||
[box.example]
|
|
||||||
name=ExampleBox
|
|
||||||
file=ExampleVagrantfile
|
|
||||||
|
|
||||||
[test.example]
|
|
||||||
test=example.py
|
|
@ -1,14 +0,0 @@
|
|||||||
[box]
|
|
||||||
[box.tumbleweed]
|
|
||||||
bootstrap=tests/scripts/bootstrap_salt.sh
|
|
||||||
name=Tumbleweed.x86_64
|
|
||||||
image=https://download.opensuse.org/repositories/home:/crameleon:/appliances/openSUSE_Tumbleweed/boxes/Tumbleweed.x86_64.json
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
box=tumbleweed
|
|
||||||
|
|
||||||
[suite.two_minions_one_master_bootstrap]
|
|
||||||
minions=2
|
|
||||||
masters=1
|
|
||||||
box=tumbleweed
|
|
@ -1,10 +0,0 @@
|
|||||||
[box]
|
|
||||||
bootstrap=scripts/bootstrap.sh
|
|
||||||
|
|
||||||
[box.tumbleweed]
|
|
||||||
name=Tumbleweed.x86_64
|
|
||||||
image=https://download.opensuse.org/repositories/home:/crameleon:/appliances/openSUSE_Tumbleweed/boxes/Tumbleweed.x86_64.json
|
|
||||||
|
|
||||||
[suite.one_minion]
|
|
||||||
minions=1
|
|
||||||
box=leap
|
|
@ -1,55 +0,0 @@
|
|||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Testing functions for Scullery - a SaltStack testing tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
import libvirt
|
|
||||||
import vagrant
|
|
||||||
import dotenv
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def script():
|
|
||||||
is_packaged = False
|
|
||||||
script = 'scullery'
|
|
||||||
if not is_packaged:
|
|
||||||
script = f'{script}.py'
|
|
||||||
return script
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def testbase():
|
|
||||||
return os.path.abspath(os.path.dirname(os.getenv('PYTEST_CURRENT_TEST')))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config(testbase, request):
|
|
||||||
return '{}/configs/{}.ini'.format(testbase, request.param)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def virt():
|
|
||||||
return libvirt.openReadOnly('qemu:///system')
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def vag():
|
|
||||||
return vagrant.Vagrant(quiet_stderr=False, quiet_stdout=False)
|
|
||||||
|
|
||||||
def loadenv():
|
|
||||||
env = os.environ.copy()
|
|
||||||
envmap = dotenv.dotenv_values('.scullery_env')
|
|
||||||
for variable, value in envmap.items():
|
|
||||||
if value is not None:
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = ','.join(value)
|
|
||||||
env[variable] = value
|
|
||||||
return env
|
|
@ -1 +0,0 @@
|
|||||||
greeting: 'Hello salted world!'
|
|
@ -1,3 +0,0 @@
|
|||||||
/srv/hello_world.txt:
|
|
||||||
file.managed:
|
|
||||||
- contents_pillar: greeting
|
|
@ -1,2 +0,0 @@
|
|||||||
def test_hello_world_content(host):
|
|
||||||
assert host.file('/srv/hello_world.txt').content.decode('UTF-8') == 'Hello salted world!\n'
|
|
@ -1 +0,0 @@
|
|||||||
ln -s /vagrant /tmp/mysources
|
|
@ -1 +0,0 @@
|
|||||||
echo 'Hello world!' > /srv/hello_world.txt
|
|
@ -1,27 +0,0 @@
|
|||||||
testbase='/vagrant/tests/salt/hello_world'
|
|
||||||
sls='hello_world.sls'
|
|
||||||
|
|
||||||
ln -s "$testbase/states/$sls" /srv/salt/
|
|
||||||
ln -s "$testbase/pillar/$sls" /srv/pillar/
|
|
||||||
|
|
||||||
tee /srv/pillar/top.sls >/dev/null <<EOF
|
|
||||||
base:
|
|
||||||
'*':
|
|
||||||
- hello_world
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if systemctl is-enabled salt-master
|
|
||||||
then
|
|
||||||
count=0
|
|
||||||
until salt -t10 scullery-\* test.ping
|
|
||||||
do
|
|
||||||
if [ "$count" = 3 ]
|
|
||||||
then
|
|
||||||
logger -s 'No minions connected'
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo 'Waiting for minions to connect ...'
|
|
||||||
sleep 5
|
|
||||||
count=$((count+1))
|
|
||||||
done
|
|
||||||
fi
|
|
Loading…
x
Reference in New Issue
Block a user