wadebug/cli.py (376 lines of code) (raw):
# Copyright (c) Facebook, Inc. and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import absolute_import, division, print_function, unicode_literals
import json
import os
import sys
import click
import pkg_resources
from wadebug import cli_utils, results, ui, wa_actions
from wadebug.cli_param import wadebug_option
from wadebug.cli_reusable_params import json_output, logs_since, opt_out, send_logs
from wadebug.config import Config, ConfigLoadError
from wadebug.wa_actions import log_utils
# Disabling warning as using unicode_literals is considered ok
# when back-porting Python3 to Python2/3
# http://python-future.org/imports.html#should-i-import-unicode-literals
click.disable_unicode_literals_warning = True
LOGS_SINCE_PARAM_FORMAT = "%Y-%m-%d %H:%M:%S"
__VERSION__ = "unknown"
try:
__VERSION__ = pkg_resources.get_distribution("wadebug").version
except Exception:
pass # ignore when building from WhatsApp internal build system
def safe_main():
if __VERSION__ != "unknown":
prompt_upgrade(__VERSION__)
run()
def prompt_upgrade(version):
from outdated import check_outdated
try:
is_outdated, latest_version = check_outdated("wadebug", version)
if is_outdated:
click.secho(
"The current version of wadebug ({}) is out of date. "
"Run `pip3 install wadebug --upgrade` "
"to upgrade to the latest version ({})\n".format(
version, latest_version
),
fg="yellow",
)
except Exception:
if Config().development_mode:
raise
def run():
try:
main()
except Exception as e:
if Config().development_mode:
raise
print(
"An error occurred with WADebug:\n{}\n".format(e),
"Please report this via Direct Support "
"(https://business.facebook.com/direct-support) ",
"and paste this full error message.",
)
@click.group(invoke_without_command=True)
@click.pass_context
@click.version_option(__VERSION__)
@wadebug_option(opt_out)
@wadebug_option(json_output)
def main(ctx, **kwargs):
"""Investigate issues with WhatsApp Business API setup."""
# Program entry point. When no arguments, executes full_debug.
# Else execute specific command (click handles this case implicitly)
# used to pass variables between commands and sub-commands
if ctx.invoked_subcommand is None:
ctx.invoke(full_debug)
@main.command()
@click.pass_context
@wadebug_option(json_output)
def ls(ctx, **kwargs):
"""Print a list of possible debug actions."""
acts = wa_actions.get_all_actions()
if ctx.obj.get("json", False):
click.echo(json.dumps({"actions": [act.user_facing_name for act in acts]}))
return
click.secho("{:<20} {}".format("Action", "Description"), bold=True)
for act in acts:
click.secho("{:<20} {}".format(act.user_facing_name, act.short_description))
click.echo()
@main.command()
@click.pass_context
@wadebug_option(send_logs)
@wadebug_option(logs_since)
@wadebug_option(opt_out)
@wadebug_option(json_output)
def logs(ctx, **kwargs):
"""Saves multiple logfiles on current folder at ./wadebug_logs/"""
send = ctx.obj.get("send", False)
logs_since = ctx.obj.get("since", "")
opt_out = ctx.obj.get("opt_out", False)
json_output = ctx.obj.get("json", False)
logs_folder = os.path.join(os.getcwd(), log_utils.OUTPUT_FOLDER)
output = {"success": False}
if json_output:
(
prepare_logs,
handle_outputs,
handle_exceptions,
handle_upload_results,
) = get_logs_json_handlers(send, opt_out, logs_folder)
else:
(
prepare_logs,
handle_outputs,
handle_exceptions,
handle_upload_results,
) = get_logs_interactive_handlers(send, opt_out, logs_folder)
prepare_logs()
try:
zipped_logs_file_handle, log_files = log_utils.prepare_logs(
logs_since, LOGS_SINCE_PARAM_FORMAT
)
output = handle_outputs(log_files, output, zipped_logs_file_handle.name)
except Exception as e:
handle_exceptions(e, output)
handle_upload_results(output, zipped_logs_file_handle)
@main.command("full")
@click.pass_context
@wadebug_option(opt_out)
@wadebug_option(json_output)
def full_debug(ctx, **kwargs):
"""Execute all debug routines, executed by default."""
acts = wa_actions.get_all_actions()
debug_implementation(
acts,
json_output=ctx.obj.get("json", False),
opt_out=ctx.obj.get("opt_out", False),
)
@main.command("partial")
@click.pass_context
@click.argument("actions", default=None, required=True, nargs=-1)
@wadebug_option(opt_out)
@wadebug_option(json_output)
def partial_debug(ctx, actions, **kwargs):
"""Execute debug routines provided. 'wadebug ls' to actions available."""
acts, acts_not_found = process_input_actions(actions)
if acts_not_found:
if ctx.obj.get("json", False):
handle_invalid_actions(acts_not_found)
else:
handle_invalid_actions_interactive(acts_not_found)
sys.exit(-1)
debug_implementation(
acts,
json_output=ctx.obj.get("json", False),
opt_out=ctx.obj.get("opt_out", False),
)
def process_input_actions(actions):
acts = []
acts_not_found = []
for act in actions:
try:
acts.append(wa_actions.get_action_by_name(act))
except KeyError:
acts_not_found.append(act)
return acts, acts_not_found
def handle_invalid_actions(acts_not_found):
click.echo(
json.dumps(
{
"error": "Can't find action(s) requested.",
"actions_not_found": acts_not_found,
}
)
)
def handle_invalid_actions_interactive(acts_not_found):
click.echo("Can't find the following action(s) requested:\n\t", nl=False)
click.echo("\n\t".join(acts_not_found))
click.echo("Please run wadebug ls to list all available actions.")
def debug_implementation(acts, json_output, opt_out):
if json_output:
debug_json(acts, opt_out)
else:
debug_interactive(acts, opt_out)
def debug_json(acts, opt_out):
result = execute_actions(acts)
if not opt_out and not Config().disable_send_data:
cli_utils.send_results_to_fb(result)
def debug_interactive(acts, opt_out):
result = execute_actions_interactive(acts)
if not opt_out and not Config().disable_send_data:
cli_utils.send_results_to_fb(
result,
success_callback=send_usage_result_interactive_success,
failure_callback=send_result_interactive_failure,
)
def execute_actions(actions):
result = {}
config = load_config()
for act in actions:
res = act.run(config)
result[res.action.user_facing_name] = res.to_dict()
click.echo(json.dumps(result))
return result
def load_config():
return Config().values
def execute_actions_interactive(actions):
config = load_config_interactive()
# execution logic is duplicated so that we print results as they appear
# this way, if something gets stuck, users can ctrl+c or take other actions
ui.print_program_header()
if Config().development_mode:
ui.print_dev_mode_header()
else:
click.echo()
result = {}
problems = []
for act in actions:
res = act.run(config)
result[res.action.user_facing_name] = res.to_dict()
ui.print_result_header(res)
if isinstance(res, results._NotOK):
ui.print_result_details(res)
problems.append(res)
click.echo()
if problems:
click.echo("! WADebug found {} issues.".format(len(problems)))
return result
def load_config_interactive():
if Config().load_error == ConfigLoadError.CONFIG_MISSING:
handle_config_missing()
return {}
elif Config().load_error == ConfigLoadError.CONFIG_INVALID:
ui.print_invalid_config_message(Config.CONFIG_FILE, Config().load_exception)
sys.exit(-1)
else:
return Config().values
def get_logs_json_handlers(send, opt_out, logs_folder):
def prepare_logs():
if send and opt_out:
output = {"success": False}
output["error_msg"] = (
"Passing --send flag requires to send data to Facebook.\n"
"It's incompatible with --do-not-send-usage. "
"Please use only one of those flags."
)
click.echo(json.dumps(output))
sys.exit(-1)
def handle_outputs(log_files, output, _zipped_logs_file_name):
# _zipped_logs_file_name is not used in this function
# it is in the function signature to keep it consistent with
# other handle_outputs functions
output["log_files"] = log_files
output["success"] = True
return output
def handle_exceptions(exp, output):
output["error_msg"] = "wadebug could not retrieve logs:\n{}\n".format(exp)
click.echo(json.dumps(output))
sys.exit(-1)
def handle_upload_results(output, zipped_logs_file_handle):
if Config().development_mode:
return
if send:
send_logs_result = cli_utils.send_logs_to_fb(
zipped_logs_file_handle,
success_callback=send_logs_result_json_success,
failure_callback=send_result_json_failure,
)
output.update(send_logs_result)
elif not opt_out:
cli_utils.send_results_to_fb(
output,
success_callback=send_usage_result_json_success,
failure_callback=send_result_json_failure,
)
click.echo(json.dumps(output))
return prepare_logs, handle_outputs, handle_exceptions, handle_upload_results
def get_logs_interactive_handlers(send, opt_out, logs_folder):
def prepare_logs():
if send and opt_out:
click.secho(
"Passing --send flag requires to send data to Facebook.\n"
"It's incompatible with --do-not-send-usage. "
"Please use only one of those flags.",
fg="red",
)
sys.exit(-1)
click.echo("Collecting logs on {}\nPlease wait...".format(logs_folder))
def handle_outputs(log_files, _output, zipped_logs_file_name):
click.secho("Zipped logs at: {}".format(zipped_logs_file_name), bold=True)
click.secho("Log files retrieved:\n\t", nl=False, bold=True)
click.echo("\n\t".join(log_files))
support_info_file_path = os.path.join(
log_utils.OUTPUT_FOLDER, log_utils.SUPPORT_INFO_LOG_FILE
)
if support_info_file_path not in log_files:
click.secho(
"Support Info is an important piece of info that helps us understand "
"the state of your WhatsApp Business API client. We were not able to "
"retrieve it because the config file has wrong or missing values. "
"Please check and run the logs command again to capture this.",
fg="yellow",
)
# returning _output to keep it consistent with other handle_outputs
# functions
_output["success"] = True
return _output
def handle_exceptions(exp, _output):
click.echo("wadebug could not retrieve logs:\n{}\n".format(exp))
sys.exit(-1)
def handle_upload_results(output, zipped_logs_file_handle):
if Config().disable_send_data:
return
if send:
click.echo("Sending logs to Facebook\nPlease wait...")
cli_utils.send_logs_to_fb(
zipped_logs_file_handle,
success_callback=send_logs_result_interactive_success,
failure_callback=send_result_interactive_failure,
)
elif not opt_out:
click.echo("Sending report to Facebook\nPlease wait...")
cli_utils.send_results_to_fb(
output,
success_callback=send_usage_result_interactive_success,
failure_callback=send_result_interactive_failure,
)
return prepare_logs, handle_outputs, handle_exceptions, handle_upload_results
def handle_config_missing():
permission_granted = click.confirm(
click.style(
"\nWADebug requires a config file: wadebug.conf.yml "
"in the current directory in order to run full checks, "
"but none has been found. "
"Do you want to create the file now?",
fg="yellow",
)
)
if permission_granted:
if Config().create_default_config_file():
click.echo(
"The config file has been created at {}. "
"Please fill in the values and run wadebug commands again\n".format(
os.getcwd()
)
)
sys.exit(0)
else:
click.secho(
"\nUnable to create config file at {}. Error: {}\n"
"Some checks will be skipped as a result.\n".format(
os.getcwd(), Config().create_exception
),
fg="yellow",
)
else:
click.secho(
"\nYou have chosen not to create the config file. "
"Some checks will be skipped as a result.\n",
fg="yellow",
)
def send_logs_result_json_success(run_id):
return {
"success": True,
"message": "Container logs have been uploaded to Facebook. "
"You can reference run_id ({}) in Direct Support "
"(https://business.facebook.com/direct-support) "
"tickets".format(run_id),
"run_id": run_id,
}
def send_usage_result_interactive_success(result):
click.secho(
"A report of this run has been uploaded to Facebook. "
"You can reference run_id ({}) in Direct Support "
"(https://business.facebook.com/direct-support) "
"tickets".format(result["run_id"]),
fg="yellow",
)
def send_logs_result_interactive_success(run_id):
click.secho(
"Container logs of this run have been uploaded to Facebook. "
"You can reference run_id ({}) in Direct Support "
"(https://business.facebook.com/direct-support) "
"tickets".format(run_id),
fg="yellow",
)
def send_result_interactive_failure(e):
click.secho("Could not send report to Facebook:\n{}".format(e), fg="red")
def send_usage_result_json_success(output):
run_id = output["run_id"]
return {
"success": True,
"message": "Container logs have been uploaded to Facebook. "
"You can reference run_id ({}) in Direct Support "
"(https://business.facebook.com/direct-support) "
"tickets".format(run_id),
"run_id": run_id,
}
def send_result_json_failure(e):
return {
"success": False,
"message": "Could not send report to Facebook:\n{}".format(e),
}
if __name__ == "__main__":
safe_main()