-
Notifications
You must be signed in to change notification settings - Fork 42
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
Add initial attempts to automatically clean up dangling hosts #337
Draft
JacobCallahan
wants to merge
1
commit into
SatelliteQE:master
Choose a base branch
from
JacobCallahan:cleanup
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+107
−6
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"""Config migrations for versions older than 0.6.1 to 0.6.1. | ||
|
||
Copy this file to a new file in the same directory and modify it to create a new migration. | ||
The new file must be named `vX_Y_Z.py` where X_Y_Z is the version you are migrating to. | ||
|
||
e.g. cp example_migration.py v0_6_1.py | ||
""" | ||
|
||
from logzero import logger | ||
|
||
TO_VERSION = "0.6.1" | ||
|
||
|
||
def example_migration(config_dict): | ||
"""Migrations should modify the config_dict in place and return it.""" | ||
config_dict["example_key"] = "example_value" | ||
return config_dict | ||
|
||
|
||
def run_migrations(config_dict): | ||
"""Run all migrations.""" | ||
logger.info(f"Running config migrations for {TO_VERSION}.") | ||
config_dict = example_migration(config_dict) | ||
config_dict["_version"] = TO_VERSION | ||
return config_dict |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""Config migrations for versions older than 0.6.3 to 0.6.3.""" | ||
|
||
from logzero import logger | ||
|
||
TO_VERSION = "0.6.3" | ||
|
||
|
||
def add_dangling_behavior(config_dict): | ||
"""Add the dangling_behavior config to AnsibleTower.""" | ||
if "AnsibleTower" in config_dict: | ||
if "dangling_behavior" not in config_dict["AnsibleTower"]: | ||
logger.debug("Adding dangling_behavior to AnsibleTower.") | ||
config_dict["AnsibleTower"]["dangling_behavior"] = "checkin" | ||
return config_dict | ||
|
||
|
||
def run_migrations(config_dict): | ||
"""Run all migrations.""" | ||
logger.info(f"Running config migrations for {TO_VERSION}.") | ||
config_dict = add_dangling_behavior(config_dict) | ||
config_dict["_version"] = TO_VERSION | ||
return config_dict |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -8,9 +8,10 @@ | |||||
import click | ||||||
from dynaconf import Validator | ||||||
from logzero import logger | ||||||
from rich.prompt import Prompt | ||||||
|
||||||
from broker import exceptions | ||||||
from broker.helpers import eval_filter, find_origin, yaml | ||||||
from broker.helpers import eval_filter, find_origin, update_inventory, yaml | ||||||
from broker.settings import settings | ||||||
|
||||||
try: | ||||||
|
@@ -29,7 +30,7 @@ | |||||
|
||||||
|
||||||
def convert_pseudonamespaces(attr_dict): | ||||||
"""Recursively convert PsuedoNamespace objects into dictionaries.""" | ||||||
"""Recursively convert PseudoNamespace objects into dictionaries.""" | ||||||
out_dict = {} | ||||||
for key, value in attr_dict.items(): | ||||||
if isinstance(value, awxkit.utils.PseudoNamespace): | ||||||
|
@@ -127,6 +128,7 @@ class AnsibleTower(Provider): | |||||
| Validator("ANSIBLETOWER.token", must_exist=True) | ||||||
), | ||||||
Validator("ANSIBLETOWER.inventory", default=None), | ||||||
Validator("ANSIBLETOWER.dangling_behavior", default="checkin"), | ||||||
] | ||||||
|
||||||
_checkout_options = [ | ||||||
|
@@ -170,11 +172,12 @@ def __init__(self, **kwargs): | |||||
self.uname = settings.ANSIBLETOWER.get("username") | ||||||
self.pword = settings.ANSIBLETOWER.get("password") | ||||||
self.token = settings.ANSIBLETOWER.get("token") | ||||||
self.dangling_behavior = settings.ANSIBLETOWER.get("dangling_behavior") | ||||||
self._inventory = kwargs.get("tower_inventory") or settings.ANSIBLETOWER.inventory | ||||||
# Init the class itself | ||||||
config = kwargs.get("config") | ||||||
root = kwargs.get("root") | ||||||
self._v2, self.username = get_awxkit_and_uname( | ||||||
self._v2, self.uname = get_awxkit_and_uname( | ||||||
config=config, | ||||||
root=root, | ||||||
url=self.url, | ||||||
|
@@ -380,6 +383,54 @@ def _get_failure_messages(self, workflow): | |||||
else: | ||||||
return failure_messages | ||||||
|
||||||
def _try_get_dangling_hosts(self, failed_workflow): | ||||||
"""Get one or more hosts that may have been left behind by a failed workflow.""" | ||||||
hosts = [] | ||||||
for node in failed_workflow.get_related("workflow_nodes").results: | ||||||
if not (job_fields := node.summary_fields.get("job", {})) or job_fields.get( | ||||||
"failed" | ||||||
): # skip jobs with no summary fields and failed jobs | ||||||
continue | ||||||
if jobs := self._v2.jobs.get(id=job_fields["id"]).results: | ||||||
if vm_name := jobs[0].artifacts.get("vm_name"): | ||||||
hosts.append(vm_name) | ||||||
return list(set(hosts)) | ||||||
|
||||||
def handle_dangling_hosts(self, job): | ||||||
"""Attempt to check in dangling hosts associated with the given job.""" | ||||||
dangling_hosts = self._try_get_dangling_hosts(job) | ||||||
if not dangling_hosts: | ||||||
logger.debug("No dangling hosts found for the failed job.") | ||||||
return | ||||||
dangling_behavior = self.dangling_behavior | ||||||
for dangling_host in dangling_hosts: | ||||||
logger.info(f"Found dangling host: {dangling_host}") | ||||||
if dangling_behavior == "prompt": | ||||||
choice = Prompt.ask( | ||||||
"What would you like to do with this host? [c/s/cA/sA]\n", | ||||||
"Checkin (c), Store (s), Checkin All (cA), Store All (sA)", | ||||||
choices=["c", "s", "cA", "sA"], | ||||||
) | ||||||
if choice == "cA": | ||||||
dangling_behavior = "checkin" | ||||||
elif choice == "sA": | ||||||
dangling_behavior = "store" | ||||||
else: | ||||||
choice = None | ||||||
# handle checkins | ||||||
if choice == "c" or dangling_behavior == "checkin": | ||||||
try: | ||||||
self.release(dangling_host) | ||||||
logger.debug(f"Successfully checked in dangling host: {dangling_host}") | ||||||
except exceptions.BrokerError: | ||||||
logger.warning(f"Failed to check in dangling host: {dangling_host}") | ||||||
elif choice == "s" or dangling_behavior == "store": | ||||||
logger.debug(f"Storing dangling host: {dangling_host}") | ||||||
host = self._v2.hosts.get(name=dangling_host).results[0] | ||||||
host = self._compile_host_info(host) | ||||||
host["failed"] = True | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I prefer to be a bit more explicit about what failed. |
||||||
update_inventory(add=host) | ||||||
|
||||||
def _compile_host_info(self, host): | ||||||
try: | ||||||
host_facts = host.related.ansible_facts.get() | ||||||
|
@@ -607,20 +658,23 @@ def execute(self, **kwargs): # noqa: PLR0912,PLR0915 - Possible TODO refactor | |||||
logger.info(f"Waiting for job: \nAPI: {job_api_url}\nUI: {job_ui_url}") | ||||||
job.wait_until_completed(timeout=settings.ANSIBLETOWER.workflow_timeout) | ||||||
if job.status != "successful": | ||||||
failure_message = self._get_failure_messages(job) | ||||||
message_data = { | ||||||
f"{subject.capitalize()} Status": job.status, | ||||||
"Reason(s)": self._get_failure_messages(job), | ||||||
"Reason(s)": failure_message, | ||||||
"URL": job_ui_url, | ||||||
} | ||||||
helpers.emit(message_data) | ||||||
if "was automatically checked-in" not in failure_message: | ||||||
self.handle_dangling_hosts(job) | ||||||
raise JobExecutionError(message_data=message_data["Reason(s)"]) | ||||||
if strategy := kwargs.pop("artifacts", None): | ||||||
return self._merge_artifacts(job, strategy=strategy) | ||||||
return job | ||||||
|
||||||
def get_inventory(self, user=None): | ||||||
"""Compile a list of hosts based on any inventory a user's name is mentioned.""" | ||||||
user = user or self.username | ||||||
user = user or self.uname | ||||||
invs = [ | ||||||
inv | ||||||
for inv in self._v2.inventory.get(page_size=200).results | ||||||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.