Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internal ansible exceptions #2

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cotea_testing_workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ jobs:
cd ./src
python cotea_ok_case.py
python cotea_ansible_error_case.py
python cotea_internal_error_case.py
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
cotea.egg-info/
__pycache__/
dist/

build/
venv/
.venv/
7 changes: 2 additions & 5 deletions contributing/contr_inv
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
[host1]
host1 ansible_host=localhost ansible_connection=ssh ansible_ssh_port=2223 ansible_user=root ansible_password=veronika

[targets]
host1
[host1]
localhost ansible_connection=ssh ansible_ssh_port=2222 ansible_user=root ansible_password=veronika ansible_ssh_common_args='-o StrictHostKeyChecking=no'
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ classifiers =
package_dir =
= src
packages = find:
python_requires = >=3.6
python_requires = >=3.8
install_requires =
ansible >=2.9.4

[options.packages.find]
where = src
Expand Down
6 changes: 6 additions & 0 deletions src/cotea/ansible_execution_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def __init__(self, logger):
self.ansible_event = threading.Event()
self.logger = logger
self.curr_breakpoint_label = None
# Used to pass exceptions from Ansible thread
self.exception = None

def status(self):
self.logger.debug("Runner event status: %s", self.runner_event.is_set())
Expand All @@ -17,6 +19,8 @@ def runner_just_wait(self):
#self.logger.debug("runner: waiting...")
self.runner_event.wait()
self.runner_event.clear()
if self.exception is not None:
raise self.exception

def ansible_just_wait(self):
#self.logger.debug("ansible: waiting...")
Expand All @@ -36,6 +40,8 @@ def continue_ansible_with_stop(self):
self.runner_event.wait()
self.runner_event.clear()
#self.logger.debug("runner: ANSIBLE WAKED ME UP")
if self.exception is not None:
raise self.exception

def continue_runner(self):
#self.logger.debug("ansible: resume runner work")
Expand Down
86 changes: 49 additions & 37 deletions src/cotea/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ def __init__(self, pb_path, arg_maker, debug_mod=None, show_progress_bar=False):
logging_lvl = logging.INFO
if debug_mod:
logging_lvl= logging.DEBUG

self.show_progress_bar = show_progress_bar

logging.basicConfig(format="%(name)s %(asctime)s %(message)s", \
datefmt="%H:%M:%S", level=logging_lvl)

self.pb_path = pb_path
self.arg_maker = arg_maker

self.logger = logging.getLogger("RUNNER")

log_sync = logging.getLogger("SYNC")
self.sync_obj = ans_sync(log_sync)

Expand All @@ -67,7 +67,7 @@ def __init__(self, pb_path, arg_maker, debug_mod=None, show_progress_bar=False):
self._set_wrappers()
start_ok = self._start_ansible()
self.logger.debug("Ansible start ok: %s", start_ok)


def _set_wrappers(self):
wrp_lgr = logging.getLogger("WRPR")
Expand Down Expand Up @@ -110,7 +110,7 @@ def _set_wrappers(self):
self.execution_tree,
self.progress_bar)
PlayIterator.add_tasks = self.iterator_add_task_wrp


def _set_wrappers_back(self):
PlaybookCLI.run = self.pbcli_run_wrp.func
Expand All @@ -121,7 +121,15 @@ def _set_wrappers_back(self):
PlayIterator.add_tasks = self.iterator_add_task_wrp.func
if self.show_progress_bar:
PlaybookExecutor.__init__ = self.playbook_executor_wrp.func


def _except_hook(self, args, /):
if (args.exc_type == SystemExit or
# NOTE: this probably should never happen
args.thread != self.ansible_thread):
return self._old_except_hook(args)

self.sync_obj.exception = args.exc_value
self.sync_obj.continue_runner()

def _start_ansible(self):
args = self.arg_maker.args
Expand All @@ -131,19 +139,22 @@ def _start_ansible(self):
self.pbCLI = PlaybookCLI(args)

self.ansible_thread = threading.Thread(target=self.pbCLI.run)
self._old_except_hook = threading.excepthook
threading.excepthook = self._except_hook

self.ansible_thread.start()
self.sync_obj.runner_just_wait()

if self.sync_obj.curr_breakpoint_label == self.breakpoint_labeles["before_playbook"]:
return True

return False


def has_next_play(self):
if self.sync_obj.curr_breakpoint_label == self.breakpoint_labeles["after_playbook"]:
return False

self.sync_obj.continue_ansible_with_stop()
current_bp_label = self.sync_obj.curr_breakpoint_label
self.logger.debug("has_next_play: %s", current_bp_label)
Expand Down Expand Up @@ -180,18 +191,18 @@ def run_next_task(self):

if current_bp_label != self.breakpoint_labeles["after_task"]:
self.logger.debug("run_next_task() has come not in to the 'after_task'")

for task_result_ansible_obj in self.update_conn_wrapper.current_results:
res.append(TaskResult(task_result_ansible_obj))

self.task_wrp.set_next_to_prev()

return res


def rerun_last_task(self):
self.task_wrp.rerun_last_task = True


# returns True and empty string if success
# False and error msg otherwise
Expand All @@ -202,7 +213,7 @@ def add_new_task(self, new_task_str, is_dict=False):
has_attrs, error_msg = cotea_utils.obj_has_attrs(prev_task, ["_parent"])
if not has_attrs:
return False, error_msg

curr_block = prev_task._parent
block_attrs = ["_loader", "_play", "_role", "_variable_manager", "_use_handlers"]
has_attrs, error_msg = cotea_utils.obj_has_attrs(curr_block, block_attrs)
Expand All @@ -227,16 +238,16 @@ def add_new_task(self, new_task_str, is_dict=False):
error_msg += "(from str-aka-dict to python ds): {}"
return False, error_msg.format(is_dict, str(e))
ds = [new_task_str_dict]

#print("DS:\n", ds)

has_attrs, _ = cotea_utils.obj_has_attrs(ds, ["__len__"])
if not has_attrs:
error_msg = "Python repr of the input string should have "
error_msg += "__len__ attr. Maybe something wrong with input: {}\n"
error_msg += "Python repr without __len__ attr: {}"
return False, error_msg.format(new_task_str, str(ds))

if len(ds) != 1:
error_msg = "You must add 1 new task. Instead you add: {}"
return False, error_msg.format(str(ds))
Expand All @@ -261,7 +272,7 @@ def add_new_task(self, new_task_str, is_dict=False):
error_msg = "Exception during load_list_of_tasks call "
error_msg += "(creats Ansible.Task objects): {}"
return False, error_msg.format(str(e))

has_attrs, _ = cotea_utils.obj_has_attrs(new_ansible_task, ["__len__"])
if not has_attrs:
error_msg = "Python repr of the input string should have "
Expand All @@ -274,23 +285,23 @@ def add_new_task(self, new_task_str, is_dict=False):
error_msg = "The input '{}' has been interpreted into {} tasks "
error_msg += "instead of 1. Interpretation result: {}"
return False, error_msg.format(new_task_str, new_tasks_count, str(ds))

#self.task_wrp.new_task_to_add = True
self.task_wrp.new_task = new_ansible_task[0]

adding_res, error_msg = self.task_wrp.add_tasks(new_ansible_task)

return adding_res, error_msg


def get_new_added_task(self):
return self.task_wrp.new_task


def ignore_errors_of_next_task(self):
self.task_wrp.next_task_ignore_errors = True


def dont_add_last_task_after_new(self):
self.task_wrp.dont_add_last_task_after_new()

Expand All @@ -304,33 +315,34 @@ def get_already_ignore_unrch(self):


def finish_ansible(self):
while self.sync_obj.curr_breakpoint_label != self.breakpoint_labeles["after_playbook"]:
self.sync_obj.continue_ansible_with_stop()

self.sync_obj.continue_ansible()
if self.sync_obj.exception is None:
while self.sync_obj.curr_breakpoint_label != self.breakpoint_labeles["after_playbook"]:
self.sync_obj.continue_ansible_with_stop()
self.sync_obj.continue_ansible()

self.ansible_thread.join(timeout=5)
self._set_wrappers_back()


def get_cur_play_name(self):
return str(self.play_wrp.current_play_name)


def get_next_task(self):
return self.task_wrp.get_next_task()


def get_next_task_name(self):
return str(self.task_wrp.get_next_task_name())


def get_prev_task(self):
return self.task_wrp.get_prev_task()


def get_prev_task_name(self):
return str(self.task_wrp.get_prev_task_name())


def get_last_task_result(self):
res = []
Expand All @@ -339,17 +351,17 @@ def get_last_task_result(self):
res.append(TaskResult(task_result_ansible_obj))

return res


# returns True if there was an non ignored error
def was_error(self):
return self.play_wrp.was_error


# returns list with all errors, including the ignored ones
def get_all_error_msgs(self):
return self.update_conn_wrapper.error_msgs


# returns last error msg that wasn't ignored
def get_error_msg(self):
Expand All @@ -361,9 +373,9 @@ def get_error_msg(self):

if errors_count > 0:
res = self.update_conn_wrapper.error_msgs[errors_count - 1]

return res


def get_all_vars(self):
variable_manager = self.play_wrp.variable_manager
Expand Down Expand Up @@ -419,7 +431,7 @@ def get_variable(self, var_name):
self.logger.info("There is no variable with name %s", var_name)

return None


def add_var_as_extra_var(self, new_var_name, value):
variable_manager = self.play_wrp.variable_manager
Expand Down
66 changes: 66 additions & 0 deletions src/cotea_internal_error_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import unittest


class TestCotea(unittest.TestCase):

def tearDown(self) -> None:
from cotea.utils import remove_modules_from_imported

# Remove any Ansible-related objects from memory
# to clear previous execution context
remove_modules_from_imported(module_name_like="cotea")

def test_incorrect_playbook_path_case(self):
from cotea.runner import runner
from cotea.arguments_maker import argument_maker

pb_path = "cotea_run_files/#%|&"
inv_path = "cotea_run_files/inv"

arg_maker = argument_maker()
arg_maker.add_arg("-i", inv_path)
r = runner(pb_path, arg_maker, show_progress_bar=True)

try:
while r.has_next_play():
while r.has_next_task():
r.run_next_task()
r.finish_ansible()
except Exception as e:
r.finish_ansible()
self.assertTrue(hasattr(e, "message"), msg="Exception is expected to have 'message' attribute")
self.assertTrue(e.message.startswith(f"the playbook: {pb_path} could not be found"),
msg="Unexpected exception message")
else:
self.assertFalse(True, msg="Ansible is supposed to fail due to syntax error "
"and its' exception should be passed to main thread")

def test_incorrect_syntax_case(self):
from cotea.runner import runner
from cotea.arguments_maker import argument_maker

pb_path = "cotea_run_files/incorrect.yaml"
inv_path = "cotea_run_files/inv"

arg_maker = argument_maker()
arg_maker.add_arg("-i", inv_path)
r = runner(pb_path, arg_maker, show_progress_bar=True)

try:
while r.has_next_play():
while r.has_next_task():
r.run_next_task()
r.finish_ansible()
except Exception as e:
r.finish_ansible()
# NOTE: e should be AnsibleParserError, but "isinstance" returns False for some reason
self.assertTrue(hasattr(e, "message"), msg="Exception is expected to have 'message' attribute")
self.assertTrue(e.message.startswith("couldn't resolve module/action"),
msg="Unexpected exception message")
else:
self.assertFalse(True, msg="Ansible is supposed to fail due to syntax error "
"and its' exception should be passed to main thread")


if __name__ == '__main__':
unittest.main()
6 changes: 6 additions & 0 deletions src/cotea_run_files/incorrect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: Play1
hosts: all
tasks:
- name: Syntactically incorrect command
battle_star_engine:
5 changes: 1 addition & 4 deletions src/cotea_run_files/inv
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
[host1]
host1 ansible_host=localhost ansible_connection=ssh ansible_ssh_port=2222 ansible_user=root ansible_password=amella

[targets]
host1
localhost ansible_connection=ssh ansible_ssh_port=2222 ansible_user=root ansible_password=amella ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'