Skip to content

Commit

Permalink
add monkeyble cli
Browse files Browse the repository at this point in the history
  • Loading branch information
Sispheor committed Nov 25, 2022
1 parent 494f351 commit 690e7d1
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 34 deletions.
Empty file added monkeyble/__init__.py
Empty file.
Empty file added monkeyble/cli/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions monkeyble/cli/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MONKEYBLE = "Monkeyble"
TEST_PASSED = "PASSED"
TEST_FAILED = "FAILED"
11 changes: 11 additions & 0 deletions monkeyble/cli/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

class ScenarioResult(object):
def __init__(self, scenario, result=None):
self.scenario = scenario
self.result = result


class MonkeybleResult(object):
def __init__(self, playbook, scenario_results=list):
self.playbook = playbook
self.scenario_results = scenario_results
10 changes: 10 additions & 0 deletions monkeyble/cli/monkeyble.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ansible_cmd: "ansible-playbook -v"
monkeyble_tests:
- playbook: "../../tests/test_playbook.yml"
# inventory: "inventory"
extra_vars:
- "../../tests/mocks.yml"
- "../../tests/monkeyble_scenarios.yml"
scenarios:
- validate_test_1
- validate_test_2
181 changes: 181 additions & 0 deletions monkeyble/cli/monkeyble_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import argparse
import logging
import os
import subprocess
import sys
import pathlib
import yaml
from tabulate import tabulate

from monkeyble.cli.const import *
from monkeyble.cli.models import MonkeybleResult, ScenarioResult
from monkeyble.cli.utils import Utils

logger = logging.getLogger(MONKEYBLE)

# actions available
ACTION_LIST = ["test"]


def run_ansible(ansible_cmd, playbook, inventory, extra_vars, scenario):
ansible_cmd = ansible_cmd.split()
cmd = list()
cmd.extend(ansible_cmd)
cmd.append(playbook)
if inventory is not None:
cmd.append("-i")
cmd.append(inventory)
if extra_vars is not None:
for extra_var_path in extra_vars:
cmd.append("-e")
cmd.append(f"@{extra_var_path}")
cmd.append("-e")
cmd.append(f"monkeyble_scenario={scenario}")
Utils.print_info(f"Monkeyble - exec: '{cmd}'")

pipes = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
for line in iter(pipes.stdout.readline, b''):
print(f"{line.rstrip().decode('utf-8')}")
std_out, std_err = pipes.communicate()
if pipes.returncode == 0:
return TEST_PASSED
else:
return TEST_FAILED


def run_monkeyble_test(monkeyble_config):
ansible_cmd = "ansible-playbook"
if "ansible_cmd" in monkeyble_config:
ansible_cmd = monkeyble_config["ansible_cmd"]
if "monkeyble_tests" not in monkeyble_config:
Utils.print_danger("No 'monkeyble_tests' variable defined")
sys.exit(1)
list_result = list()
for test_config in monkeyble_config["monkeyble_tests"]:
if "ansible_cmd" in test_config:
ansible_cmd = test_config["ansible_cmd"]
Utils.print_info(f"Monkeyble - ansible cmd: {ansible_cmd}")
playbook = test_config.get("playbook", None)
new_result = MonkeybleResult(playbook)
if playbook is None:
Utils.print_danger("Missing 'playbook' key in a test")
sys.exit(1)
inventory = test_config.get("inventory", None)
extra_vars = test_config.get("extra_vars", None)
scenarios = test_config.get("scenarios", None)
if scenarios is None:
Utils.print_danger("No scenario selected")
# print the current path
Utils.print_info(f"Monkeyble - current path: {pathlib.Path().resolve()}")
list_scenario_result = list()
for scenario in scenarios:
scenario_result = ScenarioResult(scenario)
scenario_result.result = run_ansible(ansible_cmd, playbook, inventory, extra_vars, scenario)
list_scenario_result.append(scenario_result)
new_result.scenario_results = list_scenario_result
list_result.append(new_result)
return list_result


def print_result_table(monkeyble_results):
headers = ["Playbook", "Scenario", "Test passed"]
table = list()

for monkeyble_result in monkeyble_results:
for scenario_result in monkeyble_result.scenario_results:
row = [monkeyble_result.playbook, scenario_result.scenario, Utils.get_icon_result(scenario_result.result)]
table.append(row)
print("")
print(tabulate(table, headers=headers, tablefmt="presto"))


def do_exit(test_results):
"""
Exit with code 1 if at least one test has failed
"""
total_test = 0
total_passed = 0
at_least_one_test_failed = False
for result in test_results:
for scenario_result in result.scenario_results:
total_test += 1
if scenario_result.result == TEST_PASSED:
total_passed += 1
else:
at_least_one_test_failed = True
print("")
test_result_message = f"Tests passed: {total_passed} of {total_test} tests"
if at_least_one_test_failed:
Utils.print_danger(f"🙊 Monkeyble test result - {test_result_message}")
sys.exit(1)
else:
Utils.print_success(f"🐵 Monkeyble test result - {test_result_message}")
sys.exit(0)


def load_monkeyble_config(arg_config_path):
"""
Load the yaml monkeyble config file.
Precedence:
- default local monkeyble.yml
- env var MONKEYBLE_CONFIG
- cli args 'config'
"""
# set a default config
config_path = "monkeyble.yml"
# load from env if exist
env_config = os.getenv("MONKEYBLE_CONFIG", default=None)
if env_config is not None:
config_path = env_config
# load from cli args
if arg_config_path is not None:
config_path = arg_config_path
with open(config_path, "r") as stream:
try:
monkeyble_config = yaml.full_load(stream)
except yaml.YAMLError as exc:
Utils.print_danger(exc)
sys.exit(1)
Utils.print_info(f"Monkeyble - config path: {config_path}")
return monkeyble_config


def parse_args(args):
"""
Parsing function
:param args: arguments passed from the command line
:return: return parser
"""
# create arguments
parser = argparse.ArgumentParser(description=MONKEYBLE)
parser.add_argument("action", help="[test]")
parser.add_argument("-c", "--config",
help="Path to the monkeyble config")

# parse arguments from script parameters
return parser.parse_args(args)


def main():
try:
parser = parse_args(sys.argv[1:]) # script name removed from args
except SystemExit:
sys.exit(1)
logger.debug("monkeyble args: %s" % parser)

if parser.action not in ACTION_LIST:
Utils.print_warning("%s is not a recognised action\n" % parser.action)
sys.exit(1)

config = load_monkeyble_config(parser.config)

if parser.action == "test":
test_results = run_monkeyble_test(config)
print_result_table(test_results)
do_exit(test_results)


if __name__ == '__main__':
main()
34 changes: 34 additions & 0 deletions monkeyble/cli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from monkeyble.cli.const import *


class Colors:
GREEN = '\033[92m'
WARNING = '\033[93m'
RED = '\033[91m'
BLUE = '\033[94m'
PURPLE = '\033[95m'
END = '\033[0m'


class Utils(object):
@classmethod
def print_success(cls, message):
print(f"{Colors.GREEN}{message}{Colors.END}")

@classmethod
def print_danger(cls, message):
print(f"{Colors.RED}{message}{Colors.END}")

@classmethod
def print_warning(cls, message):
print(f"{Colors.WARNING}{message}{Colors.END}")

@classmethod
def print_info(cls, message):
print(f"{Colors.BLUE}{message}{Colors.END}")

@classmethod
def get_icon_result(cls, result_code):
if result_code == TEST_PASSED:
return "✅"
return "❌"
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ mkdocs == 1.4.2
mkdocs-material == 8.5.10
mike == 1.1.2
ansible == 6.6.0
tabulate == 0.9.0
PyYAML == 6.0
43 changes: 43 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from setuptools import setup, find_packages

setup(
name='monkeyble',
description='End-to-end testing framework for Ansible',
version='1.1.0b',
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
url='https://hewlettpackard.github.io/monkeyble/',
license='GNU General Public License v3 (GPLv3)',
author='Nicolas Marcq',
author_email='[email protected]',
python_requires=">=3.6",
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Information Technology',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Topic :: Utilities',
],

# required libs
install_requires=[
'tabulate>=0.9.0',
'PyYAML>=6.0'
],

# entry point script
entry_points={
'console_scripts': [
'monkeyble=monkeyble.cli.monkeyble_cli:main',
],
}
)
14 changes: 14 additions & 0 deletions tests/mocks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@


# test input
test_input_config_1:
- assert_equal:
arg_name: msg
expected: "Hello Monkeyble"

# mocks
replace_debug_mock:
monkeyble_module:
consider_changed: true
result_dict:
test_key: "changed_result"
41 changes: 8 additions & 33 deletions tests/monkeyble.yml → tests/monkeyble_scenarios.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
# playbook base extra var
my_variable_1: "my_value_1"
hello_there: "general kenobi"

# test input
test_input_config_1:
- assert_equal:
arg_name: msg
expected: "general kenobi"

# mocks
replace_debug_mock:
monkeyble_module:
consider_changed: true
result_dict:
test_key: "changed_result"

hello_there: "Hello Monkeyble"

monkeyble_scenarios:
validate_test_1:
Expand All @@ -24,24 +10,13 @@ monkeyble_scenarios:
test_input:
- assert_equal:
arg_name: msg
expected: "Hello Monkeyble"
# - task: "task1"
# mock:
# config:
# my_module:
# my_arg: "value"
# - task: "task2"
# extra_vars:
# my_var: "new value"
- task: "task1"
mock:
config:
monkeyble_module:
consider_changed: true
result_dict:
instance:
hw_eth0:
macaddress: "01:02:b1:03:04:9d"
expected: "{{ hello_there }}"
validate_test_2:
name: "Test 2"
tasks_to_test:
- task: "debug task"
test_input: "{{ test_input_config_1 }}"

# play: "play"
# role: "ddzdz"
# should_be_changed: true
Expand Down
2 changes: 1 addition & 1 deletion tests/test_playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
tasks:
- name: "debug task"
debug:
msg: "Goodbye Monkeyble"
msg: "Hello Monkeyble"

# - name: "task2"
# debug:
Expand Down

0 comments on commit 690e7d1

Please sign in to comment.