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()