diff --git a/asadm-deps/install.sh b/asadm-deps/install.sh index bd0b312c..7519a9a3 100755 --- a/asadm-deps/install.sh +++ b/asadm-deps/install.sh @@ -35,6 +35,8 @@ elif [ "$OS" == "linux" ]; then DISTRO="rpm" elif [ -f /etc/debian_version ] ; then DISTRO="deb" + elif [ -f /etc/system-release ] ; then + DISTRO="rpm" fi else echo "No support to OS {$OS}" diff --git a/asadm.py b/asadm.py index 859b194a..2431362d 100755 --- a/asadm.py +++ b/asadm.py @@ -23,6 +23,16 @@ import sys import logging +if '-e' not in sys.argv and '--asinfo' not in sys.argv: + # asinfo mode or non-interactive mode does not need readline + # if we import readline then it adds escape character, which breaks some of asinfo use-cases. + import readline + if 'libedit' in readline.__doc__: + # BSD libedit style tab completion for OS X + readline.parse_and_bind("bind ^I rl_complete") + else: + readline.parse_and_bind("tab: complete") + # Setup logger before anything @@ -60,11 +70,13 @@ def critical(self, msg, *args, **kwargs): from lib.basiccontroller import BasicRootController from lib.client import info +from lib.client.assocket import ASSocket from lib.client.ssl_context import SSLContext from lib.collectinfocontroller import CollectinfoRootController from lib.logcontroller import LogRootController +from lib.utils import util from lib.utils.constants import ADMIN_HOME -from lib.view import terminal +from lib.view import terminal, view __version__ = '$$__version__$$' CMD_FILE_SINGLE_LINE_COMMENT_START = "//" @@ -81,6 +93,8 @@ def __init__(self, seed, user=None, password=None, use_services_alumni=False, us log_path="", log_analyser=False, collectinfo=False, ssl_context=None, only_connect_seed=False, execute_only_mode=False, timeout=5): + self.execute_only_mode = execute_only_mode + if log_analyser: self.name = 'Aerospike Log Analyzer Shell' elif collectinfo: @@ -159,7 +173,6 @@ def __init__(self, seed, user=None, password=None, use_services_alumni=False, us exit(1) if not execute_only_mode: - import readline try: readline.read_history_file(ADMINHIST) except Exception: @@ -224,7 +237,7 @@ def precmd(self, line, max_commands_to_print_header=1, return " ".join(line) if len(lines) > max_commands_to_print_header: - if any(cmd.startswith(line[0]) for cmd in MULTILEVEL_COMMANDS): + if len(line) > 1 and any(cmd.startswith(line[0]) for cmd in MULTILEVEL_COMMANDS): index = command_index_to_print_from else: # If single level command then print from first index. For example: health, features, grep etc. @@ -341,7 +354,6 @@ def complete(self, text, state): """ try: if state >= 0: - import readline origline = readline.get_line_buffer() line = origline.lstrip() stripped = len(origline) - len(line) @@ -370,8 +382,9 @@ def close(self): # Other def do_exit(self, line): self.close() - import readline - readline.write_history_file(ADMINHIST) + if not self.execute_only_mode and readline.get_current_history_length() > 0: + readline.write_history_file(ADMINHIST) + return True def do_EOF(self, line): @@ -440,6 +453,42 @@ def parse_tls_input(cli_args): exit(1) +def execute_asinfo_commands(commands_arg, seed, user=None, password=None, ssl_context=None, line_separator=False): + cmds = [None] + + if commands_arg: + asinfo_command_pattern = re.compile(r'''((?:[^;"']|"[^"]*"|'[^']*')+)''') + + cmds = asinfo_command_pattern.split(commands_arg)[1::2] + if not cmds: + return + + if user != None: + if password == "prompt": + if sys.stdin.isatty(): + password = getpass.getpass("Enter Password:") + else: + password = sys.stdin.readline().strip() + password = info.hashpassword(password) + + assock = ASSocket(seed[0], seed[1], seed[2], user, password, ssl_context) + if not assock.connect(): + raise Exception("Could not connect to node") + + node_name = "%s:%s"%(seed[0],seed[1]) + + for cmd in cmds: + if cmd: + cmd = util.strip_string(cmd) + result = assock.execute(cmd) + if result == -1 or result == None: + result = IOError("Error: Invalid command '%s'" % cmd) + + view.CliView.asinfo({node_name:result}, line_separator, False, None) + + return + + def main(): try: import argparse @@ -452,38 +501,52 @@ def main(): parser.add_argument("-t", "--tls_name", dest="tls_name", help="TLS name of host to verify for TLS connection. It is required if tls_enable is set.") parser.add_argument("-U", "--user", dest="user", help="user name") - parser.add_argument( - "-P", "--password", dest="password", nargs="?", const="prompt", help="password") + parser.add_argument("-P", "--password", dest="password", nargs="?", const="prompt", help="password") + + parser.add_argument("-c", "--collectinfo", dest="collectinfo", action="store_true", + help="Start asadm to run against offline collectinfo files.") + parser.add_argument("-l", "--log_analyser", dest="log_analyser", action="store_true", + help="Start asadm in log-analyser mode and analyse data from log files") + parser.add_argument("--asinfo", dest="asinfo_mode", action="store_true", + #help="Enable asinfo mode to connect directly to seed node without cluster creation. By default asadm connects to all nodes and creates cluster.", + help=argparse.SUPPRESS) + parser.add_argument("-e", "--execute", dest="execute", help="Execute a single or multiple asadm commands and exit. The input value is either string of ';' separated asadm commands or path of file which has asadm commands (ends with ';')") parser.add_argument("-o", "--out_file", dest="out_file", help="Path of file to write output of -e command/s") + parser.add_argument("--no-color", dest="no_color", action="store_true", help="Disable colored output") parser.add_argument("--profile", dest="profile", action="store_true" # , help="Profile Aerospike Admin for performance issues" , help=argparse.SUPPRESS) - parser.add_argument( - "-u", "--help", dest="help", action="store_true", help="show program usage") + + parser.add_argument("-u", "--help", dest="help", action="store_true", help="show program usage") parser.add_argument("--version", dest="show_version", action="store_true", help="Show the version of asadm and exit") + parser.add_argument("-s", "--services_alumni", dest="use_services_alumni", action="store_true", help="Enable use of services-alumni-list instead of services-list") parser.add_argument("-a", "--services_alternate", dest="use_services_alternate", action="store_true", help="Enable use of services-alternate instead of services in info request during cluster tending") - parser.add_argument("-l", "--log_analyser", dest="log_analyser", action="store_true", - help="Start asadm in log-analyser mode and analyse data from log files") - parser.add_argument("-c", "--collectinfo", dest="collectinfo", action="store_true", - help="Start asadm to run against offline collectinfo files.") - parser.add_argument("-f", "--file-path", dest="log_path", - help="Path of cluster collectinfo file or directory containing collectinfo and system info files.") parser.add_argument("--single_node_cluster", dest="only_connect_seed", action="store_true", help="Enable asadm mode to connect only seed node. By default asadm connects to all nodes in cluster.") + parser.add_argument("--timeout", dest="timeout", type=float, default=5, + help="Set timeout value in seconds to node level operations. TLS connection does not support timeout. Default: 5 seconds") + + parser.add_argument("--lineseperator", dest="line_separator", action="store_true", + # help="Print output in separate lines. This works only for asinfo mode." + help=argparse.SUPPRESS) + + parser.add_argument("-f", "--file-path", dest="log_path", + help="Path of cluster collectinfo file or directory containing collectinfo and system info files." + "This works only for collectinfo-analyser or log-analyser mode.") + parser.add_argument("--tls_enable", dest="enable_tls", action="store_true", help="Enable TLS on connections. By default TLS is disabled.") parser.add_argument("--tls_encrypt_only", dest="encrypt_only", action="store_true", help="Enable mode to do only encryption, so connections won't verify certificates.") - parser.add_argument( - "--tls_cafile", dest="cafile", help="Path to a trusted CA certificate file.") + parser.add_argument("--tls_cafile", dest="cafile", help="Path to a trusted CA certificate file.") parser.add_argument("--tls_capath", dest="capath", help="Path to a directory of trusted CA certificates.") parser.add_argument("--tls_protocols", dest="protocols", help="Set the TLS protocol selection criteria. This format is the same as Apache's SSLProtocol documented " @@ -507,8 +570,6 @@ def main(): help="Enable CRL checking for leaf certificate. An error occurs if a valid CRL files cannot be found in tls_capath.") parser.add_argument("--tls_crl_check_all", dest="crl_check_all", action="store_true", help="Enable CRL checking for entire certificate chain. An error occurs if a valid CRL files cannot be found in tls_capath.") - parser.add_argument("--timeout", dest="timeout", type=float, default=5, - help="Set timeout value in seconds to node level operations. TLS connection does not support timeout. Default: 5 seconds") cli_args = parser.parse_args() except Exception: @@ -524,36 +585,50 @@ def main(): parser.add_option("-U", "--user", dest="user", help="user name") parser.add_option("-P", "--password", dest="password", action="store_const" # , nargs="?" , const="prompt", help="password") + + parser.add_option("-c", "--collectinfo", dest="collectinfo", action="store_true", + help="Start asadm to run against offline collectinfo files.") + parser.add_option("-l", "--log_analyser", dest="log_analyser", action="store_true", + help="Start asadm in log-analyser mode and analyse data from log files") + parser.add_option("--asinfo_mode", dest="asinfo_mode", action="store_true", + # help="Enable asinfo mode to connect directly to seed node without cluster creation. By default asadm connects to all nodes and creates cluster." + help=optparse.SUPPRESS_USAGE) + parser.add_option("-e", "--execute", dest="execute", help="Execute a single or multiple asadm commands and exit. The input value is either string of ';' separated asadm commands or path of file which has asadm commands (ends with ';')") parser.add_option("-o", "--out_file", dest="out_file", help="Path of file to write output of -e command/s") + parser.add_option("--no-color", dest="no_color", action="store_true", help="Disable colored output") parser.add_option("--profile", dest="profile", action="store_true" # , help="Profile Aerospike Admin for performance issues" , help=optparse.SUPPRESS_USAGE) - parser.add_option( - "-u", "--help", dest="help", action="store_true", help="show program usage") + + parser.add_option("-u", "--help", dest="help", action="store_true", help="show program usage") parser.add_option("--version", dest="show_version", action="store_true", help="Show the version of asadm and exit") + parser.add_option("-s", "--services_alumni", dest="use_services_alumni", action="store_true", help="Enable use of services-alumni-list instead of services-list") parser.add_option("-a", "--services_alternate", dest="use_services_alternate", action="store_true", help="Enable use of services-alternate instead of services in info request during cluster tending") - parser.add_option("-l", "--log_analyser", dest="log_analyser", action="store_true", - help="Start asadm in log-analyser mode and analyse data from log files") - parser.add_option("-c", "--collectinfo", dest="collectinfo", action="store_true", - help="Start asadm to run against offline collectinfo files.") - parser.add_option("-f", "--file-path", dest="log_path", - help="Path of cluster collectinfo file or directory containing collectinfo and system info files.") parser.add_option("--single_node_cluster", dest="only_connect_seed", action="store_true", help="Enable asadm mode to connect only seed node. By default asadm connects to all nodes in cluster.") + parser.add_option("--timeout", dest="timeout",type=float, default=5, + help="Set timeout value in seconds to node level operations. TLS connection does not support timeout. Default: 5 seconds") + + parser.add_option("--lineseperator", dest="line_separator", action="store_true", + # help="Print output in separate lines. This works only for asinfo mode." + help=optparse.SUPPRESS_USAGE) + + parser.add_option("-f", "--file-path", dest="log_path", + help="Path of cluster collectinfo file or directory containing collectinfo and system info files.") + parser.add_option("--tls_enable", dest="enable_tls", action="store_true", help="Enable TLS on connections. By default TLS is disabled.") parser.add_option("--tls_encrypt_only", dest="encrypt_only", action="store_true", help="Enable mode to do only encryption, so connections won't verify certificates.") - parser.add_option( - "--tls_cafile", dest="cafile", help="Path to a trusted CA certificate file.") + parser.add_option("--tls_cafile", dest="cafile", help="Path to a trusted CA certificate file.") parser.add_option("--tls_capath", dest="capath", help="Path to a directory of trusted CA certificates.") parser.add_option("--tls_protocols", dest="protocols", help="Set the TLS protocol selection criteria. This format is the same as Apache's SSLProtocol documented " @@ -577,8 +652,6 @@ def main(): help="Enable CRL checking for leaf certificate. An error occurs if a valid CRL files cannot be found in tls_capath.") parser.add_option("--tls_crl_check_all", dest="crl_check_all", action="store_true", help="Enable CRL checking for entire certificate chain. An error occurs if a valid CRL files cannot be found in tls_capath.") - parser.add_option("--timeout", dest="timeout",type=float, default=5, - help="Set timeout value in seconds to node level operations. TLS connection does not support timeout. Default: 5 seconds") (cli_args, args) = parser.parse_args() @@ -608,9 +681,27 @@ def main(): ssl_context = parse_tls_input(cli_args) + if cli_args.asinfo_mode: + + if cli_args.collectinfo or cli_args.log_analyser: + print "asinfo mode can not work with Collectinfo-analyser or Log-analyser mode." + exit(1) + + commands_arg = cli_args.execute + if commands_arg and os.path.isfile(commands_arg): + commands_arg = parse_commands(commands_arg) + + try: + execute_asinfo_commands(commands_arg, seed, user=cli_args.user, + password=cli_args.password, ssl_context=ssl_context, line_separator=cli_args.line_separator) + exit(0) + except Exception as e: + logger.error(str(e)) + exit(1) + if not execute_only_mode: - import readline readline.set_completer_delims(' \t\n;') + shell = AerospikeShell(seed, user=cli_args.user, password=cli_args.password, use_services_alumni=cli_args.use_services_alumni, diff --git a/lib/basiccontroller.py b/lib/basiccontroller.py index ea81a228..93122495 100644 --- a/lib/basiccontroller.py +++ b/lib/basiccontroller.py @@ -15,7 +15,6 @@ import copy from distutils.version import LooseVersion import json -import logging import time import os import sys @@ -26,8 +25,10 @@ import zipfile from lib.client.cluster import Cluster +from lib.collectinfocontroller import CollectinfoRootController from lib.controllerlib import BaseController, CommandController, CommandHelp, ShellException -from lib.getcontroller import GetConfigController, GetStatisticsController, GetDistributionController, get_sindex_stats +from lib.getcontroller import GetConfigController, GetStatisticsController, GetDistributionController, get_sindex_stats, \ + GetPmapController from lib.health.util import create_health_input_dict, h_eval, create_snapshot_key from lib.utils import util from lib.utils.data import lsof_file_type_desc @@ -166,6 +167,12 @@ def do_namespace(self, line): return util.Future(self.view.info_namespace, stats, self.cluster, **self.mods) + @CommandHelp('Displays summary information for objects of each namespace.') + def do_object(self, line): + stats = self.cluster.info_all_namespace_statistics(nodes=self.nodes) + return util.Future(self.view.info_object, stats, self.cluster, + **self.mods) + @CommandHelp('Displays summary information for Cross Datacenter', 'Replication (XDR).') def do_xdr(self, line): @@ -339,7 +346,7 @@ def do_object_size(self, line): arg="-b", default=False, modifiers=self.modifiers, mods=self.mods) - show_bucket_count = util.get_arg_and_delete_from_mods(line=line, + bucket_count = util.get_arg_and_delete_from_mods(line=line, arg="-k", return_type=int, default=5, modifiers=self.modifiers, mods=self.mods) @@ -351,7 +358,7 @@ def do_object_size(self, line): 'objsz', self.cluster, like=self.mods['for']) - histogram = self.getter.do_object_size(byte_distribution = True, show_bucket_count=show_bucket_count, nodes=self.nodes) + histogram = self.getter.do_object_size(byte_distribution = True, bucket_count=bucket_count, nodes=self.nodes) histogram_name = 'objsz' title = 'Object Size Distribution' @@ -359,7 +366,7 @@ def do_object_size(self, line): set_bucket_count = True return util.Future(self.view.show_object_distribution, title, - histogram, unit, histogram_name, show_bucket_count, + histogram, unit, histogram_name, bucket_count, set_bucket_count, self.cluster, like=self.mods['for']) class ShowLatencyController(BasicCommandController): @@ -437,11 +444,12 @@ def __init__(self): @CommandHelp('Displays service, network, and namespace configuration', ' Options:', ' -r - Repeating output table title and row header after every r columns.', - ' default: 0, no repetition.') + ' default: 0, no repetition.', + ' -flip - Flip output table to show Nodes on Y axis and config on X axis.') def _do_default(self, line): - actions = (util.Future(self.do_service, line).start(), - util.Future(self.do_network, line).start(), - util.Future(self.do_namespace, line).start()) + actions = (util.Future(self.do_service, line[:]).start(), + util.Future(self.do_network, line[:]).start(), + util.Future(self.do_namespace, line[:]).start()) return [action.result() for action in actions] @@ -455,11 +463,15 @@ def do_service(self, line): modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + service_configs = self.getter.get_service(nodes=self.nodes) return util.Future(self.view.show_config, "Service Configuration", service_configs, self.cluster, - title_every_nth=title_every_nth, + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays network configuration') @@ -469,10 +481,14 @@ def do_network(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + network_configs = self.getter.get_network(nodes=self.nodes) return util.Future(self.view.show_config, "Network Configuration", - network_configs, self.cluster, title_every_nth=title_every_nth, + network_configs, self.cluster, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays namespace configuration') @@ -482,11 +498,15 @@ def do_namespace(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + ns_configs = self.getter.get_namespace(nodes=self.nodes) return [util.Future(self.view.show_config, "%s Namespace Configuration" % (ns), configs, self.cluster, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for ns, configs in ns_configs.iteritems()] @CommandHelp('Displays XDR configuration') @@ -496,10 +516,14 @@ def do_xdr(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + xdr_configs = self.getter.get_xdr(nodes=self.nodes) return util.Future(self.view.show_config, "XDR Configuration", - xdr_configs, self.cluster, title_every_nth=title_every_nth, + xdr_configs, self.cluster, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays datacenter configuration') @@ -509,11 +533,15 @@ def do_dc(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + dc_configs = self.getter.get_dc(nodes=self.nodes) return [util.Future(self.view.show_config, "%s DC Configuration" % (dc), configs, self.cluster, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for dc, configs in dc_configs.iteritems()] @CommandHelp('Displays Cluster configuration') @@ -523,10 +551,14 @@ def do_cluster(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + cl_configs = self.getter.get_cluster(nodes=self.nodes) return util.Future(self.view.show_config, "Cluster Configuration", - cl_configs, self.cluster, title_every_nth=title_every_nth, + cl_configs, self.cluster, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @@ -562,317 +594,180 @@ def __init__(self): self.modifiers = set(['with', 'like', 'for']) self.getter = GetStatisticsController(self.cluster) - @CommandHelp('Displays service statistics') - def do_service(self, line, show_total=False): + @CommandHelp('Displays bin, set, service, and namespace statistics', + ' Options:', + ' -t - Set to show total column at the end. It contains node wise sum for statistics.', + ' -r - Repeating output table title and row header after every r columns.', + ' default: 0, no repetition.', + ' -flip - Flip output table to show Nodes on Y axis and stats on X axis.') + def _do_default(self, line): - service_stats = self.getter.get_service(nodes=self.nodes) + actions = (util.Future(self.do_bins, line[:]).start(), + util.Future(self.do_sets, line[:]).start(), + util.Future(self.do_service, line[:]).start(), + util.Future(self.do_namespace, line[:]).start()) - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + return [action.result() for action in actions] + + @CommandHelp('Displays service statistics') + def do_service(self, line): + + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + service_stats = self.getter.get_service(nodes=self.nodes) + return util.Future(self.view.show_stats, "Service Statistics", service_stats, self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays namespace statistics') - def do_namespace(self, line, show_total=False): + def do_namespace(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + ns_stats = self.getter.get_namespace(nodes=self.nodes, for_mods=self.mods['for']) - return [util.Future(self.view.show_stats, + return [util.Future(self.view.show_stats, "%s Namespace Statistics" % (namespace), ns_stats[namespace], self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for namespace in sorted(ns_stats.keys())] @CommandHelp('Displays sindex statistics') - def do_sindex(self, line, show_total=False): + def do_sindex(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + sindex_stats = self.getter.get_sindex(nodes=self.nodes, for_mods=self.mods['for']) return [util.Future(self.view.show_stats, "%s Sindex Statistics" % (ns_set_sindex), sindex_stats[ns_set_sindex], self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for ns_set_sindex in sorted(sindex_stats.keys())] @CommandHelp('Displays set statistics') - def do_sets(self, line, show_total=False): + def do_sets(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + set_stats = self.getter.get_sets(nodes=self.nodes, for_mods=self.mods['for']) return [util.Future(self.view.show_stats, "%s %s Set Statistics" % (namespace, set_name), stats, self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for (namespace, set_name), stats in set_stats.iteritems()] @CommandHelp('Displays bin statistics') - def do_bins(self, line, show_total=False): + def do_bins(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + new_bin_stats = self.getter.get_bins(nodes=self.nodes, for_mods=self.mods['for']) return [util.Future(self.view.show_stats, "%s Bin Statistics" % (namespace), new_bin_stat, self.cluster, - show_total=show_total, title_every_nth=title_every_nth, **self.mods) + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for namespace, new_bin_stat in new_bin_stats.iteritems()] @CommandHelp('Displays XDR statistics') - def do_xdr(self, line, show_total=False): + def do_xdr(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + xdr_stats = self.getter.get_xdr(nodes=self.nodes) return util.Future(self.view.show_stats, "XDR Statistics", xdr_stats, self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays datacenter statistics') - def do_dc(self, line, show_total=False): + def do_dc(self, line): - show_total = show_total or util.check_arg_and_delete_from_mods(line=line, arg="-t", + show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", default=False, modifiers=self.modifiers, mods=self.mods) title_every_nth = util.get_arg_and_delete_from_mods(line=line, arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + dc_stats = self.getter.get_dc(nodes=self.nodes) return [util.Future(self.view.show_config, "%s DC Statistics" % (dc), stats, self.cluster, show_total=show_total, - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) for dc, stats in dc_stats.iteritems()] - @CommandHelp('Displays bin, set, service, and namespace statistics', - ' Options:', - ' -t - Set to show total column at the end. It contains node wise sum for statistics.', - ' -r - Repeating output table title and row header after every r columns.', - ' default: 0, no repetition.') - def _do_default(self, line): - - show_total = util.check_arg_and_delete_from_mods(line=line, arg="-t", - default=False, modifiers=self.modifiers, mods=self.mods) - - actions = (util.Future(self.do_bins, line, show_total).start(), - util.Future(self.do_sets, line, show_total).start(), - util.Future(self.do_service, line, show_total).start(), - util.Future(self.do_namespace, line, show_total).start()) - - return [action.result() for action in actions] - @CommandHelp('Displays partition map analysis of Aerospike cluster.') class ShowPmapController(BasicCommandController): def __init__(self): self.modifiers = set() + self.getter = GetPmapController(self.cluster) def _do_default(self, line): - versions = util.Future(self.cluster.info, 'version', nodes=self.nodes).start() - pmap_info = util.Future(self.cluster.info, 'partition-info', nodes=self.nodes).start() - namespaces = util.Future(self.cluster.info_namespaces, nodes=self.nodes).start() - versions = versions.result() - pmap_info = pmap_info.result() - namespaces = namespaces.result() - namespaces = namespaces.values() - namespace_set = set() - namespace_stats = dict() - - for namespace in namespaces: - if isinstance(namespace, Exception): - continue - - namespace_set.update(namespace) - - for namespace in sorted(namespace_set): - ns_stats = self.cluster.info_namespace_statistics(namespace, nodes=self.nodes) - namespace_stats[namespace] = ns_stats - - pmap_data = self._get_pmap_data(pmap_info, self._get_namespace_data(namespace_stats), versions) + pmap_data = self.getter.get_pmap(nodes=self.nodes) return util.Future(self.view.show_pmap, pmap_data, self.cluster) - def _format_missing_part(self, part_data): - missing_part = '' - get_part = lambda pid, pindex: str(pid) + ':S:' + str(pindex) + ',' - - for pid, part in enumerate(part_data): - if part: - for pindex in part: - missing_part += get_part(pid, pindex) - - return missing_part[:-1] - - def _get_namespace_data(self, namespace_stats): - disc_pct_allowed = 1 # Considering Negative & Positive both discrepancy - ns_info = {} - - for ns, nodes in namespace_stats.items(): - ns_info[ns] = {} - master_objs = 0 - replica_objs = 0 - repl_factor = 0 - - for params in nodes.values(): - if isinstance(params, Exception): - continue - - master_objs += util.get_value_from_dict(params,('master-objects','master_objects'),0,int) - replica_objs += util.get_value_from_dict(params,('prole-objects','prole_objects'),0,int) - repl_factor = max(repl_factor, int(params['repl-factor'])) - - ns_info[ns]['avg_master_objs'] = master_objs / 4096 - ns_info[ns]['avg_replica_objs'] = replica_objs / 4096 - ns_info[ns]['repl_factor'] = repl_factor - diff_master = ns_info[ns]['avg_master_objs'] * disc_pct_allowed / 100 - - if diff_master < 1024: - diff_master = 1024 - - diff_replica = ns_info[ns]['avg_replica_objs'] * disc_pct_allowed / 100 - - if diff_replica < 1024: - diff_replica = 1024 - - ns_info[ns]['diff_master'] = diff_master - ns_info[ns]['diff_replica'] = diff_replica - - return ns_info - - def _get_pmap_data(self, pmap_info, ns_info, versions): - pid_range = 4096 # each namespace is divided into 4096 partition - is_dist_delta_exeeds = lambda exp, act, diff: abs(exp - act) > diff - pmap_data = {} - ns_missing_part = {} - visited_ns = set() - - # required fields - # format : (index_ptr, field_name, default_index) - required_fields = [("ns_index","namespace",0),("pid_index","partition",1),("state_index","state",2), - ("pindex_index","replica",3),("objects_index","records",9)] - - for _node, partitions in pmap_info.items(): - node_pmap = dict() - - if isinstance(partitions, Exception): - continue - - f_indices = {} - - # default index in partition fields for server < 3.6.1 - for t in required_fields: - f_indices[t[0]] = t[2] - - index_set = False - - for item in partitions.split(';'): - fields = item.split(':') - - if not index_set: - index_set = True - - if all(i[1] in fields for i in required_fields): - # pmap format contains headers from server 3.9 onwards - for t in required_fields: - f_indices[t[0]] = fields.index(t[1]) - - continue - - elif LooseVersion(versions[_node]) >= LooseVersion("3.6.1"): - # pmap format is changed(1 field is removed) in aerospike 3.6.1 - # In 3.7.5, one new field got added but at the end of the fields. So it doesn't affect required indices - f_indices["objects_index"]=8 - - ns, pid, state, pindex, objects = fields[f_indices["ns_index"]], int(fields[f_indices["pid_index"]]),\ - fields[f_indices["state_index"]], int(fields[f_indices["pindex_index"]]),\ - int(fields[f_indices["objects_index"]]) - - if ns not in node_pmap: - node_pmap[ns] = { 'pri_index' : 0, - 'sec_index' : 0, - 'master_disc_part': [], - 'replica_disc_part':[] - } - - if ns not in visited_ns: - ns_missing_part[ns] = {} - ns_missing_part[ns]['missing_part'] = [range(ns_info[ns]['repl_factor']) for i in range(pid_range)] - visited_ns.add(ns) - - if state == 'S': - # partition state is SYNC - try: - if pindex == 0: - node_pmap[ns]['pri_index'] += 1 - exp_master_objs = ns_info[ns]['avg_master_objs'] - - if exp_master_objs == 0 and objects == 0: - pass - elif is_dist_delta_exeeds(exp_master_objs, objects, ns_info[ns]['diff_master']): - node_pmap[ns]['master_disc_part'].append(pid) - - if pindex in range(1, ns_info[ns]['repl_factor']): - node_pmap[ns]['sec_index'] += 1 - exp_replica_objs = ns_info[ns]['avg_replica_objs'] - - if exp_replica_objs == 0 and objects == 0: - pass - - elif is_dist_delta_exeeds(exp_replica_objs, objects, ns_info[ns]['diff_replica']): - node_pmap[ns]['replica_disc_part'].append(pid) - - ns_missing_part[ns]['missing_part'][pid].remove(pindex) - - except Exception: - pass - if pid not in range(pid_range): - print "For {0} found partition-ID {1} which is beyond legal partitions(0...4096)".format(ns, pid) - - pmap_data[_node] = node_pmap - - for _node, _ns in pmap_data.items(): - for ns_name, params in _ns.items(): - params['missing_part'] = self._format_missing_part(ns_missing_part[ns_name]['missing_part']) - - return pmap_data - @CommandHelp('"collectinfo" is used to collect cluster info, aerospike conf file and system stats.') class CollectinfoController(BasicCommandController): @@ -880,7 +775,7 @@ def __init__(self): self.modifiers = set(['with']) def _collect_local_file(self, src, dest_dir): - print "[INFO] Copying file %s to %s" % (src, dest_dir) + self.logger.info("Copying file %s to %s" % (src, dest_dir)) try: shutil.copy2(src, dest_dir) except Exception, e: @@ -895,9 +790,9 @@ def _collectinfo_content(self, func, parm='', alt_parm=''): name = func.func_name except Exception: pass - info_line = "[INFO] Data collection for " + name + \ + info_line = "Data collection for " + name + \ "%s" % (" %s" % (str(parm)) if parm else "") + " in progress.." - print info_line + self.logger.info(info_line) if parm: sep += str(parm) + "\n" @@ -905,22 +800,25 @@ def _collectinfo_content(self, func, parm='', alt_parm=''): o, e = util.shell_command(parm) if e: if e: - info_line = "[ERROR] " + str(e) - print info_line + self.logger.error(str(e)) + if alt_parm and alt_parm[0]: - info_line = "[INFO] Data collection for alternative command " + \ - name + str(alt_parm) + " in progress.." - print info_line + info_line = "Data collection for alternative command " + \ + name + str(alt_parm) + " in progress.." + self.logger.info(info_line) sep += str(alt_parm) + "\n" o_alt, e_alt = util.shell_command(alt_parm) + if e_alt: self.cmds_error.add(parm[0]) self.cmds_error.add(alt_parm[0]) + if e_alt: - info_line = "[ERROR] " + str(e_alt) - print info_line + self.logger.error(str(e_alt)) + if o_alt: o = o_alt + else: self.cmds_error.add(parm[0]) @@ -941,14 +839,16 @@ def _write_log(self, collectedinfo): def _write_version(self, line): print "asadm version " + str(self.asadm_version) - def _get_metadata(self, response_str, prefix=''): + def _get_metadata(self, response_str, prefix='', old_response=''): aws_c = '' aws_metadata_base_url = 'http://169.254.169.254/latest/meta-data' + # set of values which will give same old_response, so no need to go further + last_values = [] for rsp in response_str.split("\n"): if rsp[-1:] == '/': rsp_p = rsp.strip('/') - aws_c += self._get_metadata(rsp_p, prefix) + aws_c += self._get_metadata(rsp_p, prefix, old_response=old_response) else: meta_url = aws_metadata_base_url + prefix + rsp @@ -956,14 +856,17 @@ def _get_metadata(self, response_str, prefix=''): r = urllib2.urlopen(req) # r = requests.get(meta_url,timeout=aws_timeout) if r.code != 404: - response = r.read() - if response.strip().endswith("/") or "\n" in response.strip() or (rsp.strip() == "placement" and response.strip() == "availability-zone"): - try: - aws_c += self._get_metadata(response.strip(), prefix + rsp + "/") - except Exception: + response = r.read().strip() + if response == old_response: + last_values.append(rsp.strip()) + continue + try: + aws_c += self._get_metadata(response, prefix + rsp + "/", old_response=response) + except Exception: aws_c += (prefix + rsp).strip('/') + '\n' + response + "\n\n" - else: - aws_c += (prefix + rsp).strip('/') + '\n' + response + "\n\n" + + if last_values: + aws_c += prefix.strip('/') + '\n' + '\n'.join(last_values) + "\n\n" return aws_c @@ -1120,8 +1023,9 @@ def _archive_log(self, logdir): self._zip_files(logdir) util.shell_command(["tar -czvf " + logdir + ".tgz " + aslogdir]) sys.stderr.write("\x1b[2J\x1b[H") - print "\n\n\nFiles in " + logdir + " and " + logdir + ".tgz saved. " - print "END OF ASCOLLECTINFO" + print "\n\n\n" + self.logger.info("Files in " + logdir + " and " + logdir + ".tgz saved. ") + self.logger.info("END OF ASCOLLECTINFO") def _parse_namespace(self, namespace_data): """ @@ -1284,18 +1188,45 @@ def _get_meta_for_sec(self, metasec, sec_name, nodeid, metamap): def _get_as_metadata(self): metamap = {} builds = util.Future(self.cluster.info, 'build', nodes=self.nodes).start().result() + editions = util.Future(self.cluster.info, 'version', nodes=self.nodes).start().result() xdr_builds = util.Future(self.cluster.info_XDR_build_version, nodes=self.nodes).start().result() + node_ids = util.Future(self.cluster.info_node, nodes=self.nodes).start().result() + ips = util.Future(self.cluster.info_ip_port, nodes=self.nodes).start().result() udf_data = util.Future(self.cluster.info_udf_list, nodes=self.nodes).start().result() for nodeid in builds: metamap[nodeid] = {} self._get_meta_for_sec(builds, 'asd_build', nodeid, metamap) + self._get_meta_for_sec(editions, 'edition', nodeid, metamap) self._get_meta_for_sec(xdr_builds, 'xdr_build', nodeid, metamap) + self._get_meta_for_sec(node_ids, 'node_id', nodeid, metamap) + self._get_meta_for_sec(ips, 'ip', nodeid, metamap) self._get_meta_for_sec(udf_data, 'udf', nodeid, metamap) return metamap - def _dump_in_file(self, timestamp, as_logfile_prefix, dump): + def _get_as_histograms(self): + histogram_map = {} + hist_list = ['ttl', 'objsz'] + + for hist in hist_list: + hist_dump = util.Future(self.cluster.info_histogram, hist, raw_output=True, nodes=self.nodes).start().result() + for node in hist_dump: + if node not in histogram_map: + histogram_map[node] = {} + + if not hist_dump[node] or isinstance(hist_dump[node], Exception): + continue + + histogram_map[node][hist] = hist_dump[node] + + return histogram_map + + def _get_as_pmap(self): + getter = GetPmapController(self.cluster) + return getter.get_pmap(nodes=self.nodes) + + def _dump_in_json_file(self, as_logfile_prefix, dump): self.logger.info("Dumping collectinfo in JSON format.") aslogfile = as_logfile_prefix + 'ascinfo.json' with open(aslogfile, "w") as f: @@ -1308,6 +1239,10 @@ def _get_collectinfo_data_json(self, default_user, default_pwd, meta_map = self._get_as_metadata() + histogram_map = self._get_as_histograms() + + pmap_map = self._get_as_pmap() + sys_map = self.cluster.info_system_statistics(default_user=default_user, default_pwd=default_pwd, default_ssh_key=default_ssh_key, default_ssh_port=default_ssh_port, credential_file=credential_file, nodes=self.nodes) @@ -1321,6 +1256,12 @@ def _get_collectinfo_data_json(self, default_user, default_pwd, if node in meta_map: dump_map[node]['as_stat']['meta_data'] = meta_map[node] + if node in histogram_map: + dump_map[node]['as_stat']['histogram'] = histogram_map[node] + + if node in pmap_map: + dump_map[node]['as_stat']['pmap'] = pmap_map[node] + # Get the cluster name and add one more level in map cluster_name = 'null' cluster_names = util.Future( @@ -1336,39 +1277,20 @@ def _get_collectinfo_data_json(self, default_user, default_pwd, snp_map[cluster_name] = dump_map return snp_map - def _main_collectinfo(self, default_user, default_pwd, default_ssh_port, default_ssh_key, - credential_file, snp_count, wait_time, show_all=False, - verbose=False): - - global aslogdir, output_time - timestamp = time.gmtime() - output_time = time.strftime("%Y%m%d_%H%M%S", timestamp) - aslogdir = '/tmp/collect_info_' + output_time - as_logfile_prefix = aslogdir + '/' + output_time + '_' - - os.makedirs(aslogdir) - - # Pretty print collectinfo - self._dump_collectinfo_pretty_print( - timestamp, as_logfile_prefix, show_all=show_all, verbose=verbose) - - # JSON collectinfo - if snp_count < 1: - self._archive_log(aslogdir) - return - + def _dump_collectinfo_json(self, timestamp, as_logfile_prefix, default_user, default_pwd, default_ssh_port, default_ssh_key, credential_file, + snp_count, wait_time): snpshots = {} + for i in range(snp_count): - snp_timestamp = time.strftime( - "%Y-%m-%d %H:%M:%S UTC", time.gmtime()) - print("[INFO] Data collection for Snapshot: " + str(i + 1) + " in progress..") + + snp_timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()) + self.logger.info("Data collection for Snapshot: " + str(i + 1) + " in progress..") snpshots[snp_timestamp] = self._get_collectinfo_data_json( default_user, default_pwd, default_ssh_port, default_ssh_key, credential_file) + time.sleep(wait_time) - self._dump_in_file(timestamp, as_logfile_prefix, snpshots) - - self._archive_log(aslogdir) + self._dump_in_json_file(as_logfile_prefix, snpshots) def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, show_all=False, verbose=False): @@ -1401,6 +1323,7 @@ def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, 'ls /etc|grep release|xargs -I f cat /etc/f'], ['cat /proc/meminfo', 'vmstat -s'], ['cat /proc/interrupts', ''], + ['cat /proc/partitions', 'fdisk -l'], [ 'ls /sys/block/{sd*,xvd*}/queue/rotational |xargs -I f sh -c "echo f; cat f;"', ''], [ @@ -1451,6 +1374,10 @@ def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, 'dump-smd:' ] + summary_params = ['summary'] + summary_info_params = ['network', 'namespace', 'object', 'set', 'xdr', 'dc', 'sindex'] + health_params = ['health -v'] + hist_list = ['ttl', 'objsz'] hist_dump_info_str = "hist-dump:ns=%s;hist=%s" @@ -1508,8 +1435,6 @@ def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, else: cmd_dmesg = 'cat /var/log/messages' - terminal.enable_color(False) - ####### Dignostic info ######## aslogfile = as_logfile_prefix + 'ascollectinfo.log' @@ -1552,6 +1477,44 @@ def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, self._write_log(str(e)) sys.stdout = sys.__stdout__ + ####### Summary ######## + collectinfo_root_controller = CollectinfoRootController(asadm_version=self.asadm_version, clinfo_path=as_logfile_prefix + "ascinfo.json") + + aslogfile = as_logfile_prefix + 'summary.log' + self._write_log(collect_output) + try: + self._collectinfo_content(self._write_version) + except Exception as e: + self._write_log(str(e)) + sys.stdout = sys.__stdout__ + + try: + for summary_param in summary_params: + self._collectinfo_content(collectinfo_root_controller.execute, [summary_param]) + except Exception as e: + self._write_log(str(e)) + sys.stdout = sys.__stdout__ + + try: + info_controller = InfoController() + for info_param in summary_info_params: + self._collectinfo_content(info_controller, [info_param]) + except Exception as e: + self._write_log(str(e)) + sys.stdout = sys.__stdout__ + + ####### Health ######## + + aslogfile = as_logfile_prefix + 'health.log' + self._write_log(collect_output) + + try: + for health_param in health_params: + self._collectinfo_content(collectinfo_root_controller.execute, health_param.split()) + except Exception as e: + self._write_log(str(e)) + sys.stdout = sys.__stdout__ + ####### System info ######## aslogfile = as_logfile_prefix + 'sysinfo.log' @@ -1687,6 +1650,38 @@ def _dump_collectinfo_pretty_print(self, timestamp, as_logfile_prefix, self._write_log(str(e)) sys.stdout = sys.__stdout__ + def _main_collectinfo(self, default_user, default_pwd, default_ssh_port, default_ssh_key, + credential_file, snp_count, wait_time, show_all=False, + verbose=False): + global aslogdir, output_time + timestamp = time.gmtime() + output_time = time.strftime("%Y%m%d_%H%M%S", timestamp) + aslogdir = '/tmp/collect_info_' + output_time + as_logfile_prefix = aslogdir + '/' + output_time + '_' + + os.makedirs(aslogdir) + + # Coloring might writes extra characters to file, to avoid it we need to disable terminal coloring + terminal.enable_color(False) + + # JSON collectinfo + if snp_count < 1: + self._archive_log(aslogdir) + return + + self._dump_collectinfo_json(timestamp, as_logfile_prefix, default_user, default_pwd, default_ssh_port, default_ssh_key, + credential_file, snp_count, wait_time,) + + # Pretty print collectinfo + self._dump_collectinfo_pretty_print(timestamp, as_logfile_prefix, show_all=show_all, verbose=verbose) + + # Archive collectinfo directory + self._archive_log(aslogdir) + + # If multiple commands are given in execute_only mode then we might need coloring for next commands + terminal.enable_color(True) + + @CommandHelp('Collects cluster info, aerospike conf file for local node and system stats from all nodes if remote server credentials provided.', 'If credentials are not available then it will collect system stats from local node only.', ' Options:', @@ -1738,7 +1733,7 @@ def _do_default(self, line): self.cmds_error = set() self._main_collectinfo(default_user, default_pwd, default_ssh_port, default_ssh_key, - credential_file, snp_count, wait_time, False, False) + credential_file, snp_count, wait_time, show_all=False, verbose=False) if self.cmds_error: self.logger.error( @@ -1750,19 +1745,15 @@ def _do_default(self, line): ' verbose - Enable to collect additional stats with detailed output of "info dump-*" commands' ) def do_all(self, line): - credential_file = util.get_arg_and_delete_from_mods(line=line, - arg="-cf", return_type=str, default=None, - modifiers=self.modifiers, mods=self.mods) - default_user = util.get_arg_and_delete_from_mods(line=line, - arg="-user", return_type=str, default=None, + arg="-U", return_type=str, default=None, modifiers=self.modifiers, mods=self.mods) - default_pwd = util.get_arg_and_delete_from_mods(line=line, arg="-pwd", + default_pwd = util.get_arg_and_delete_from_mods(line=line, arg="-P", return_type=str, default=None, modifiers=self.modifiers, mods=self.mods) - snp_count = util.get_arg_and_delete_from_mods(line=line, arg="-sc", + snp_count = util.get_arg_and_delete_from_mods(line=line, arg="-n", return_type=int, default=1, modifiers=self.modifiers, mods=self.mods) @@ -1774,12 +1765,21 @@ def do_all(self, line): arg="-sp", return_type=int, default=None, modifiers=self.modifiers, mods=self.mods) + default_ssh_key = util.get_arg_and_delete_from_mods(line=line, + arg="-sk", return_type=str, default=None, + modifiers=self.modifiers, mods=self.mods) + + credential_file = util.get_arg_and_delete_from_mods(line=line, + arg="-cf", return_type=str, default=None, + modifiers=self.modifiers, mods=self.mods) + verbose = False if 'verbose' in line: verbose = True + self.cmds_error = set() - self._main_collectinfo(default_user, default_pwd, default_ssh_port, - credential_file, snp_count, wait_time, True, verbose) + self._main_collectinfo(default_user, default_pwd, default_ssh_port, default_ssh_key, + credential_file, snp_count, wait_time, show_all=True, verbose=verbose) if self.cmds_error: self.logger.error( @@ -1851,6 +1851,10 @@ def _get_asstat_data(self, stanza): return get_sindex_stats(cluster=self.cluster, nodes=self.nodes) elif stanza == "udf": return self.cluster.info_udf_list(nodes=self.nodes) + elif stanza == "endpoints": + return self.cluster.info_service(nodes=self.nodes) + elif stanza == "services": + return self.cluster.info_services(nodes=self.nodes) def _get_asconfig_data(self, stanza): if stanza == "xdr": @@ -1999,6 +2003,14 @@ def _do_default(self, line): ("build", "METADATA", False, False, [ ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "version")]), ]), + "endpoints": (self._get_asstat_data, [ + ("endpoints", "METADATA", False, False, [ + ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "endpoints")]), + ]), + "services": (self._get_asstat_data, [ + ("services", "METADATA", False, False, [ + ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "services")]), + ]), "metadata": (self._get_asstat_data, [ ("udf", "UDF", False, False, [ ("CLUSTER", cluster_name), ("NODE", None), (None, None), ("FILENAME", None)]), @@ -2097,6 +2109,9 @@ def _do_default(self, line): except Exception: continue + if cmd_section == "free-m": + d = util.mbytes_to_bytes(d) + try: new_tuple_keys = copy.deepcopy(cmd_item[4]) except: diff --git a/lib/client/assocket.py b/lib/client/assocket.py index fd2b6130..34af1382 100644 --- a/lib/client/assocket.py +++ b/lib/client/assocket.py @@ -28,11 +28,15 @@ class ASSocket: - def __init__(self, node, ip, port, pool_size=3, timeout=5): + def __init__(self, ip, port, tls_name, user, password, ssl_context, pool_size=3, timeout=5): self.sock = None - self.node = node + self.ip = ip self.port = port + self.tls_name = tls_name + self.user = user + self.password = password + self.ssl_context = ssl_context self.pool_size = pool_size self._timeout = timeout @@ -99,9 +103,9 @@ def _create_socket(self, host, port, tls_name=None, user=None, def connect(self): try: self.sock = self._create_socket(self.ip, self.port, - tls_name=self.node.tls_name, user=self.node.user, - password=self.node.password, - ssl_context=self.node.ssl_context) + tls_name=self.tls_name, user=self.user, + password=self.password, + ssl_context=self.ssl_context) if not self.sock: return False diff --git a/lib/client/node.py b/lib/client/node.py index c5f96fb0..0ea436f6 100644 --- a/lib/client/node.py +++ b/lib/client/node.py @@ -40,8 +40,6 @@ except ImportError: PEXPECT_VERSION = NO_MODULE -COMMAND_PROMPT = '[#$] ' - def getfqdn(address, timeout=0.5): # note: cannot use timeout lib because signal must be run from the # main thread @@ -92,6 +90,7 @@ def __init__(self, address, port=3000, tls_name=None, timeout=5, user=None, ALSO NOTE: May be better to just use telnet instead? """ self.logger = logging.getLogger('asadm') + self.remote_system_command_prompt = '[#$] ' self._update_IP(address, port) self.port = port self.xdr_port = 3004 # TODO: Find the xdr port @@ -308,7 +307,7 @@ def _get_connection(self, ip, port): pass if sock: return sock - sock = ASSocket(self, ip, port, timeout=self._timeout) + sock = ASSocket(ip, port, self.tls_name, self.user, self.password, self.ssl_context, timeout=self._timeout) if sock.connect(): return sock return None @@ -402,6 +401,17 @@ def info_node(self): return self.info("node") + @return_exceptions + def info_ip_port(self): + """ + Get this nodes ip:port. + + Returns: + string -- this node's ip:port. + """ + + return self.create_key(self.ip, self.port) + @return_exceptions def _info_peers_list_helper(self, peers): """ @@ -546,15 +556,22 @@ def info_services_alt(self): return self._info_services_helper(self.info("services-alternate")) @return_exceptions - def info_service(self, address, return_None=False): + def info_service(self, address="", return_None=False): try: service = self.info("service") s = map(util.info_to_tuple, util.info_to_list(service)) + return map(lambda v: (v[0], int(v[1]), self.tls_name), s) + except Exception: pass + if return_None: return None + + if not address: + address = self.ip + return [(address, self.port, self.tls_name)] @return_exceptions @@ -959,7 +976,7 @@ def info_XDR_get_config(self): return xdr_configs @return_exceptions - def info_histogram(self, histogram): + def info_histogram(self, histogram, raw_output=False): namespaces = self.info_namespaces() data = {} @@ -967,14 +984,18 @@ def info_histogram(self, histogram): try: datum = self.info("hist-dump:ns=%s;hist=%s" % (namespace, histogram)) - datum = datum.split(',') - datum.pop(0) # don't care about ns, hist_name, or length - width = int(datum.pop(0)) - datum[-1] = datum[-1].split(';')[0] - datum = map(int, datum) - - data[namespace] = { - 'histogram': histogram, 'width': width, 'data': datum} + if raw_output: + data[namespace] = datum + + else: + datum = datum.split(',') + datum.pop(0) # don't care about ns, hist_name, or length + width = int(datum.pop(0)) + datum[-1] = datum[-1].split(';')[0] + datum = map(int, datum) + + data[namespace] = {'histogram': histogram, 'width': width, 'data': datum} + except Exception: pass return data @@ -1164,11 +1185,14 @@ def _login_remote_system(self, ip, user, pwd, ssh_key=None, port=None): @return_exceptions def _spawn_remote_system(self, ip, user, pwd, ssh_key=None, port=None): - global COMMAND_PROMPT - terminal_prompt = '(?i)terminal type\?' + terminal_prompt_msg = '(?i)terminal type' + ssh_newkey_msg = '(?i)are you sure you want to continue connecting' + connection_closed_msg = "(?i)connection closed by remote host" + permission_denied_msg = "(?i)permission denied" + pwd_passphrase_msg = "(?i)(?:password)|(?:passphrase for key)" + terminal_type = 'vt100' - ssh_newkey = '(?i)are you sure you want to continue connecting' ssh_options = "-o 'NumberOfPasswordPrompts=1' " if port: @@ -1183,43 +1207,67 @@ def _spawn_remote_system(self, ip, user, pwd, ssh_key=None, port=None): ssh_options += ' -i %s' % (ssh_key) s = pexpect.spawn('ssh %s -l %s %s'%(ssh_options, str(user), str(ip))) - - i = s.expect([pexpect.TIMEOUT, ssh_newkey, COMMAND_PROMPT, '(?i)(?:password)|(?:passphrase for key)']) + i = s.expect([ssh_newkey_msg, self.remote_system_command_prompt, pwd_passphrase_msg, permission_denied_msg, terminal_prompt_msg, pexpect.TIMEOUT, connection_closed_msg, pexpect.EOF], timeout=10) if i == 0: - # Timeout - return None - - enter_pwd = False - - if i == 1: # In this case SSH does not have the public key cached. - s.sendline ('yes') - s.expect ('(?i)(?:password)|(?:passphrase for key)') - enter_pwd = True + s.sendline("yes") + i = s.expect([ssh_newkey_msg, self.remote_system_command_prompt, pwd_passphrase_msg, permission_denied_msg, terminal_prompt_msg, pexpect.TIMEOUT]) + if i == 2: + # password or passphrase + s.sendline(pwd) + i = s.expect([ssh_newkey_msg, self.remote_system_command_prompt, pwd_passphrase_msg, permission_denied_msg, terminal_prompt_msg, pexpect.TIMEOUT]) + if i == 4: + s.sendline(terminal_type) + i = s.expect([ssh_newkey_msg, self.remote_system_command_prompt, pwd_passphrase_msg, permission_denied_msg, terminal_prompt_msg, pexpect.TIMEOUT]) + if i == 7: + s.close() + return None - elif i == 2: + if i == 0: + # twice not expected + s.close() + return None + elif i == 1: pass - + elif i == 2: + # password prompt again means input password is wrong + s.close() + return None elif i == 3: - enter_pwd = True + # permission denied means input password is wrong + s.close() + return None + elif i == 4: + # twice not expected + s.close() + return None + elif i == 5: + # timeout + # Two possibilities + # 1. couldn't login + # 2. couldn't match shell prompt + # safe option is to pass + pass + elif i == 6: + # connection closed by remote host + s.close() + return None + else: + # unexpected + s.close() + return None - if enter_pwd: - s.sendline(pwd) - i = s.expect ([COMMAND_PROMPT, terminal_prompt]) - if i == 1: - s.sendline (terminal_type) - s.expect (COMMAND_PROMPT) + self.remote_system_command_prompt = "\[PEXPECT\][\$\#] " + s.sendline("unset PROMPT_COMMAND") - COMMAND_PROMPT = "\[PEXPECT\][\$\#] " # sh style s.sendline ("PS1='[PEXPECT]\$ '") - i = s.expect ([pexpect.TIMEOUT, COMMAND_PROMPT], timeout=10) - + i = s.expect ([pexpect.TIMEOUT, self.remote_system_command_prompt], timeout=10) if i == 0: # csh-style. s.sendline ("set prompt='[PEXPECT]\$ '") - i = s.expect ([pexpect.TIMEOUT, COMMAND_PROMPT], timeout=10) + i = s.expect ([pexpect.TIMEOUT, self.remote_system_command_prompt], timeout=10) if i == 0: return None @@ -1245,7 +1293,7 @@ def _execute_remote_system_command(self, conn, cmd): if PEXPECT_VERSION == NEW_MODULE: conn.prompt() elif PEXPECT_VERSION == OLD_MODULE: - conn.expect (COMMAND_PROMPT) + conn.expect (self.remote_system_command_prompt) else: return None return conn.before @@ -1281,6 +1329,8 @@ def _stop_ssh_connection(self, conn): if conn: conn.close() + self.remote_system_command_prompt = '[#$] ' + @return_exceptions def _get_remote_host_system_statistics(self, commands): sys_stats = {} diff --git a/lib/collectinfo/cinfolog.py b/lib/collectinfo/cinfolog.py index 375ab1fe..49cbfa84 100644 --- a/lib/collectinfo/cinfolog.py +++ b/lib/collectinfo/cinfolog.py @@ -13,9 +13,7 @@ # limitations under the License. import copy -from lib.utils import logutil -from lib.utils.constants import * -from lib.collectinfo.reader import CollectinfoReader +from lib.collectinfo_parser.full_parser import parse_info_all class CollectinfoNode(object): @@ -54,30 +52,42 @@ def set_cluster_name(self, cluster_name): self.cluster_name = cluster_name def set_asd_version(self, asd_version): - if asd_version.lower() in ['enterprise', 'true']: + if asd_version.lower() in ['enterprise', 'true', 'ee'] or 'enterprise' in asd_version.lower(): self.asd_version = "Enterprise" - elif asd_version.lower() in ['community', 'false']: + elif asd_version.lower() in ['community', 'false', 'ce'] or 'community' in asd_version.lower(): self.asd_version = "Community" else: self.asd_version = "N/E" -class CollectinfoLog(object): +class CollectinfoSnapshot(object): - def __init__(self, timestamp, cinfo_file, reader): + def __init__(self, cluster_name, timestamp, cinfo_data, cinfo_file): + self.cluster_name = cluster_name self.timestamp = timestamp - self.cinfo_file = cinfo_file - self.reader = reader self.nodes = {} self.node_names = {} - self.cinfo_data = {} + self.cinfo_data = cinfo_data + self.cinfo_file = cinfo_file + self._initialize_nodes() + + def _initialize_nodes(self): + try: + self._set_nodes(self.get_node_names()) + self._set_node_id() + self._set_ip() + self._set_xdr_build() + self._set_asd_build() + self._set_asd_version() + self._set_cluster_name() + except Exception: + pass def destroy(self): try: del self.timestamp - del self.cinfo_file - del self.reader del self.cinfo_data + del self.cinfo_file del self.nodes del self.node_names except Exception: @@ -85,47 +95,101 @@ def destroy(self): def get_node_names(self): if not self.node_names: - if (self.cinfo_data - and "config" in self.cinfo_data - and CONFIG_SERVICE in self.cinfo_data["config"]): - node_names = self.cinfo_data["config"][CONFIG_SERVICE].keys() + if self.cinfo_data: + node_names = self.cinfo_data.keys() else: - node_names = self.reader.get_node_names(self.cinfo_file) + return {} for node_name in node_names: self.node_names[node_name] = node_name return copy.deepcopy(self.node_names) def get_data(self, type="", stanza=""): - if not type or not stanza: - return {} + data = {} + + if not type or not self.cinfo_data: + return data + try: - if not self.cinfo_data: - self.cinfo_data = self.reader.read(self.cinfo_file) - if (self.cinfo_data - and "config" in self.cinfo_data - and CONFIG_SERVICE in self.cinfo_data["config"]): - self._set_nodes( - self.cinfo_data["config"][CONFIG_SERVICE].keys()) - elif (self.cinfo_data - and "statistics" in self.cinfo_data - and STAT_SERVICE in self.cinfo_data["statistics"]): - self._set_nodes( - self.cinfo_data["statistics"][STAT_SERVICE].keys()) - self._set_node_id() - self._set_ip() - self._set_xdr_build() - self._set_asd_build() - self._set_asd_version() - self._set_cluster_name() - - elif type not in self.cinfo_data: - self.cinfo_data.update(self.reader.read(self.cinfo_file)) - - return copy.deepcopy(self.cinfo_data[type][stanza]) + # return copy.deepcopy(self.cinfo_data[type][stanza]) + for node, node_data in self.cinfo_data.iteritems(): + try: + if not node or not node_data: + continue + + if not 'as_stat' in node_data or type not in node_data['as_stat']: + continue + + if node not in data: + data[node] = {} + + d = node_data['as_stat'][type] + + if not stanza: + data[node] = copy.deepcopy(d) + continue + + if stanza in ['namespace', 'bin', 'bins', 'set', 'sindex']: + d = d["namespace"] + + for ns_name in d.keys(): + try: + if stanza == "namespace": + data[node][ns_name] = copy.deepcopy(d[ns_name]["service"]) + + elif stanza == "bin" or stanza == "bins": + data[node][ns_name] = copy.deepcopy(d[ns_name][stanza]) + + elif stanza in ["set", "sindex"]: + + for _name in d[ns_name][stanza]: + _key = "%s %s" % (ns_name, _name) + data[node][_key] = copy.deepcopy(d[ns_name][stanza][_name]) + + except Exception: + pass + + elif type == "meta_data" and stanza in ["endpoints", "services"]: + try: + data[node] = copy.deepcopy(d[stanza]).split(';') + except Exception: + data[node] = copy.deepcopy(d[stanza]) + + else: + data[node] = copy.deepcopy(d[stanza]) + + except Exception: + data[node] = {} + except Exception: pass - return {} + + return data + + def get_sys_data(self, stanza=""): + data = {} + + if not type or not stanza or not self.cinfo_data: + return data + + try: + for node, node_data in self.cinfo_data.iteritems(): + try: + if not node or not node_data: + continue + + if not 'sys_stat' in node_data or stanza not in node_data['sys_stat']: + continue + + data[node] = node_data['sys_stat'][stanza] + + except Exception: + data[node] = {} + + except Exception: + pass + + return data def get_node(self, node_key): if node_key in self.nodes: @@ -140,14 +204,12 @@ def get_statistics(self, stanza=""): return self.get_data(type="statistics", stanza=stanza) def get_histograms(self, stanza=""): - return self.get_data(type="distribution", stanza=stanza) + return self.get_data(type="histogram", stanza=stanza) def get_summary(self, stanza=""): return self.get_data(type="summary", stanza=stanza) def get_expected_principal(self): - if not self.cinfo_data: - self._set_node_id() try: principal="0" for n in self.nodes.itervalues(): @@ -164,9 +226,6 @@ def get_expected_principal(self): def get_xdr_build(self): xdr_build={} try: - if not self.cinfo_data: - self._set_xdr_build() - for node in self.nodes: xdr_build[node]=self.nodes[node].xdr_build except Exception: @@ -176,9 +235,6 @@ def get_xdr_build(self): def get_asd_build(self): asd_build={} try: - if not self.cinfo_data: - self._set_asd_build() - for node in self.nodes: asd_build[node]=self.nodes[node].asd_build except Exception: @@ -188,9 +244,6 @@ def get_asd_build(self): def get_asd_version(self): asd_version={} try: - if not self.cinfo_data: - self._set_asd_version() - for node in self.nodes: asd_version[node]=self.nodes[node].asd_version except Exception: @@ -200,9 +253,6 @@ def get_asd_version(self): def get_cluster_name(self): cluster_name={} try: - if not self.cinfo_data: - self._set_cluster_name() - for node in self.nodes: cluster_name[node]=self.nodes[node].cluster_name except Exception: @@ -218,159 +268,76 @@ def _get_node_count(self): return len(self.nodes.keys()) def _set_node_id(self): - - node_ids=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_SERVICE, header_columns=['Node'], - column_to_find=['Node', 'Id'], symbol_to_neglct='.') - - if not node_ids: - node_ids=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_NETWORK, header_columns=['Node'], - column_to_find=['Node', 'Id'], symbol_to_neglct='.') - - if not node_ids and self._get_node_count() == 1: - service_stats=self.get_data(type="statistics", - stanza=STAT_SERVICE) - - for node in self.nodes: - self.nodes[node].set_node_id( - logutil.fetch_value_from_dic(service_stats, - [node, 'paxos_principal'])) - - elif node_ids: - for node in node_ids: - if node in self.nodes: - self.nodes[node].set_node_id(node_ids[node]) + for node in self.nodes: + try: + self.nodes[node].set_node_id(self.cinfo_data[node]['as_stat']['meta_data']['node_id']) + except Exception: + pass def _set_ip(self): - ips=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_SERVICE, header_columns=['Node', 'Ip'], - column_to_find=['Ip'], symbol_to_neglct='.') - - if not ips: - ips=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_NETWORK, header_columns=['Node', 'Ip'], - column_to_find=['Ip'], symbol_to_neglct='.') - - if ips: - for node in ips: - if node in self.nodes: - self.nodes[node].set_ip(ips[node]) + for node in self.nodes: + try: + self.nodes[node].set_ip(self.cinfo_data[node]['as_stat']['meta_data']['ip']) + except Exception: + pass def _set_xdr_build(self): - xdr_builds=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_XDR, header_columns=['Node', 'Build'], - column_to_find=['Build'], symbol_to_neglct='.') - - if xdr_builds: - for node in xdr_builds: - if node in self.nodes: - self.nodes[node].set_xdr_build(xdr_builds[node]) + for node in self.nodes: + try: + self.nodes[node].set_xdr_build(self.cinfo_data[node]['as_stat']['meta_data']['xdr_build']) + except Exception: + pass def _set_asd_build(self): - asd_builds=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_SERVICE, header_columns=['Node', 'Build'], - column_to_find=['Build'], symbol_to_neglct='.') - - if not asd_builds: - asd_builds=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_NETWORK, header_columns=['Node', 'Build'], - column_to_find=['Build'], symbol_to_neglct='.') - - if asd_builds: - for node in asd_builds: - if node in self.nodes and asd_builds[node]: - if asd_builds[node].startswith("E-"): - self.nodes[node].set_asd_build(asd_builds[node][2:]) - self.nodes[node].set_asd_version("Enterprise") - elif asd_builds[node].startswith("C-"): - self.nodes[node].set_asd_build(asd_builds[node][2:]) - self.nodes[node].set_asd_version("Community") - else: - self.nodes[node].set_asd_build(asd_builds[node]) + for node in self.nodes: + try: + self.nodes[node].set_asd_build(self.cinfo_data[node]['as_stat']['meta_data']['asd_build']) + except Exception: + pass def _set_asd_version(self): - asd_versions=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_NETWORK, header_columns=['Node', 'Enterprise'], - column_to_find=['Enterprise'], symbol_to_neglct='.') - - if asd_versions: - for node in asd_versions: - if node in self.nodes and asd_versions[node]: - self.nodes[node].set_asd_version(asd_versions[node]) + for node in self.nodes: + try: + self.nodes[node].set_asd_version(self.cinfo_data[node]['as_stat']['meta_data']['edition']) + except Exception: + pass def _set_cluster_name(self): - cluster_names=self._fetch_columns_for_nodes(type="summary", - stanza=SUMMARY_NETWORK, header_columns=['Cluster', 'Node'], - column_to_find=['Cluster', 'Name'], symbol_to_neglct='.') - - if cluster_names: - for node in cluster_names: - if node in self.nodes: - self.nodes[node].set_cluster_name(cluster_names[node]) - - def _find_column_num(self, lines, column_to_find, header_columns): - - if not lines: - return lines, None - column_found=False - column_to_find_index=0 - header_search_incomplete=False - indices=[] - - while not column_found and lines: - line=lines.pop(0) - - if (all(column in line for column in header_columns) - or header_search_incomplete): - line_list=line.split() - - temp_indices=[i for i, x in enumerate( - line_list) if x == column_to_find[column_to_find_index]] - - if not indices: - indices=temp_indices - else: - indices=logutil.intersect_list(indices, temp_indices) - - column_to_find_index += 1 - - if column_to_find_index == len(column_to_find): - column_found=True - header_search_incomplete=False - else: - header_search_incomplete=True - return lines, indices - - def _fetch_columns_for_nodes(self, type, stanza, header_columns, - column_to_find, symbol_to_neglct): - - summary=self.get_data(type=type, stanza=stanza) - node_value={} - if summary and isinstance(summary, str): - lines=summary.split('\n') - lines, node_col=self._find_column_num( - lines, ['Node', '.'], header_columns) - - lines=summary.split('\n') - lines, indices=self._find_column_num( - lines, column_to_find, header_columns) - for line in lines: - try: + for node in self.nodes: + try: + self.nodes[node].set_cluster_name(self.cluster_name) + except Exception: + pass - if(line.split()[node_col[0]].strip() == symbol_to_neglct): - continue - else: - line_list=line.split() - node=line_list[node_col[0]].strip() - col_val=line_list[indices[0]].strip() - if node in self.nodes: - node_value[node]=col_val - except Exception: - pass - return node_value + +class CollectinfoLog(object): + def __init__(self, cinfo_path, files, reader): + self.files = files + self.reader = reader + self.snapshots = {} + self.data = {} + parse_info_all(files, self.data, True) + if self.data: + for ts in self.data: + if self.data[ts]: + for cl in self.data[ts]: + self.snapshots[ts] = CollectinfoSnapshot(cl, ts, self.data[ts][cl], cinfo_path) + + def destroy(self): + try: + del self.files + del self.reader + for sn in self.snapshots: + self.snapshots[sn].destroy() + del self.snapshots + del self.data + except Exception: + pass + + def get_snapshots(self): + return self.snapshots \ No newline at end of file diff --git a/lib/collectinfo/loghdlr.py b/lib/collectinfo/loghdlr.py index 75363cd4..012fa155 100644 --- a/lib/collectinfo/loghdlr.py +++ b/lib/collectinfo/loghdlr.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy import ntpath import os import logging @@ -22,12 +21,10 @@ import zipfile -from lib.collectinfo_parser.full_parser import parse_info_all from lib.collectinfo.reader import CollectinfoReader from lib.collectinfo.cinfolog import CollectinfoLog from lib.utils.constants import ADMIN_HOME, CLUSTER_FILE, JSON_FILE, SYSTEM_FILE -from lib.utils.util import restructure_sys_data -from lib.utils import logutil +from lib.utils import logutil, util ###### Constants ###### DATE_SEG = 0 @@ -47,13 +44,6 @@ class CollectinfoLoghdlr(object): all_cinfo_logs = {} selected_cinfo_logs = {} - # for healthchecker - # TODO: This is temporary extra dict, all commands should fetch data from one dict. - # TODO: Remove parsing code from asadm - parsed_data = {} - parsed_as_data_logs = [] - parsed_system_data_logs = [] - # for zipped files COLLECTINFO_DIR = ADMIN_HOME + 'collectinfo/' COLLECTINFO_INTERNAL_DIR = "collectinfo_analyser_extracted_files" @@ -65,19 +55,12 @@ def __init__(self, cinfo_path): self.logger = logging.getLogger('asadm') self.reader = CollectinfoReader() - cinfo_added, err_cinfo = self._add_cinfo_log_files(cinfo_path) - if not cinfo_added: - cinfo_added, _ = self._add_cinfo_log_files(self.COLLECTINFO_DIR) + snapshot_added, err_cinfo = self._add_cinfo_log_files(cinfo_path) - if cinfo_added == 0: + if snapshot_added == 0: self.logger.error(err_cinfo) sys.exit(1) - health_data_updated = self._add_data_to_health_dict(cinfo_path) - - if not health_data_updated: - self.logger.info("No data added for healthcheck.") - def __str__(self): status_str = "" if not self.all_cinfo_logs: @@ -114,12 +97,6 @@ def close(self): if os.path.exists(self.COLLECTINFO_DIR): shutil.rmtree(self.COLLECTINFO_DIR) - def get_cinfo_path(self): - return self.cinfo_path - - def get_cinfo_timestamp(self): - return self.cinfo_timestamp - def get_cinfo_log_at(self, timestamp=""): if not timestamp or timestamp not in self.all_cinfo_logs: @@ -127,63 +104,61 @@ def get_cinfo_log_at(self, timestamp=""): return self.all_cinfo_logs[timestamp] - def info_getconfig(self, stanza=""): - return self._fetch_from_cinfo_log(type="config", stanza=stanza) - - def info_statistics(self, stanza=""): - return self._fetch_from_cinfo_log(type="statistics", stanza=stanza) - - def info_histogram(self, stanza=""): - return self._fetch_from_cinfo_log(type="distribution", stanza=stanza) + def info_getconfig(self, stanza="", flip=False): + return self._fetch_from_cinfo_log(type="config", stanza=stanza, flip=flip) - def info_summary(self, stanza=""): - return self._fetch_from_cinfo_log(type="summary", stanza=stanza) + def info_statistics(self, stanza="", flip=False): + return self._fetch_from_cinfo_log(type="statistics", stanza=stanza, flip=flip) - def get_asstat_data(self, stanza=""): - return self._fetch_from_parsed_as_data(info_type="statistics", - stanza=stanza) + def info_histogram(self, stanza="", flip=False): + hist_dict = self._fetch_from_cinfo_log(type="histogram", stanza=stanza, flip=flip) + res_dict = {} - def get_asconfig_data(self, stanza=""): - return self._fetch_from_parsed_as_data(info_type="config", - stanza=stanza) + for timestamp, hist_snapshot in hist_dict.items(): + res_dict[timestamp] = {} + if not hist_snapshot: + continue - def get_asmeta_data(self, stanza=""): - return self._fetch_from_parsed_as_data(info_type="meta_data", - stanza=stanza) + for node, node_snapshot in hist_snapshot.items(): + res_dict[timestamp][node] = {} + if not node_snapshot: + continue - def get_sys_data(self, stanza=""): - res_dict = {} + for namespace, namespace_snapshot in node_snapshot.items(): + if not namespace_snapshot: + continue - for sys_ts in sorted(self.parsed_data.keys()): - res_dict[sys_ts] = {} - - for cl in self.parsed_data[sys_ts]: - d = self.parsed_data[sys_ts][cl] - for node in d: try: - res_dict[sys_ts][node] = copy.deepcopy( - d[node]['sys_stat'][stanza]) + datum = namespace_snapshot.split(',') + datum.pop(0) # don't care about ns, hist_name, or length + width = int(datum.pop(0)) + datum[-1] = datum[-1].split(';')[0] + datum = map(int, datum) + + res_dict[timestamp][node][namespace] = {'histogram': stanza, 'width': width, 'data': datum} except Exception: pass + return res_dict - try: - res_dict[sys_ts] = restructure_sys_data( - res_dict[sys_ts], stanza) - except Exception: - pass + def info_meta_data(self, stanza=""): + return self._fetch_from_cinfo_log(type="meta_data", stanza=stanza) - return res_dict + def info_pmap(self): + return self._fetch_from_cinfo_log(type="pmap") + + def get_sys_data(self, stanza=""): + res_dict = {} + if not stanza: + return res_dict - def get_asd_build(self): - res_dic = {} for timestamp in sorted(self.selected_cinfo_logs.keys()): try: - res_dic[timestamp] = self.selected_cinfo_logs[ - timestamp].get_asd_build() + out = self.selected_cinfo_logs[timestamp].get_sys_data(stanza=stanza) + res_dict[timestamp] = util.restructure_sys_data(out, stanza) except Exception: continue - return res_dic + return res_dict def _get_files_by_type(self, file_type, cinfo_path=""): try: @@ -230,36 +205,7 @@ def _get_files_by_type(self, file_type, cinfo_path=""): except Exception: return [] - def _update_parsed_log_list(self, stanza, old_log_list): - logs = [] - if not stanza or not self.parsed_data: - return logs - found_new = False - for sn in self.parsed_data.keys(): - for cluster in self.parsed_data[sn].keys(): - for node in self.parsed_data[sn][cluster].keys(): - try: - if (self.parsed_data[sn][cluster][node][stanza] - and sn not in old_log_list): - found_new = True - old_log_list.append(sn) - except Exception: - pass - return found_new - - def _is_parsed_data_changed(self): - as_logs_updated = self._update_parsed_log_list( - stanza="as_stat", old_log_list=self.parsed_as_data_logs) - sys_logs_updated = self._update_parsed_log_list( - stanza="sys_stat", old_log_list=self.parsed_system_data_logs) - if as_logs_updated or sys_logs_updated: - return True - return False - - def _add_data_to_health_dict(self, cinfo_path): - if not cinfo_path or not os.path.exists(cinfo_path): - return False - + def _get_all_file_paths(self, cinfo_path): files = [] @@ -290,119 +236,45 @@ def _add_data_to_health_dict(self, cinfo_path): for sysinfo_file in self._get_files_by_type(SYSTEM_FILE, self.COLLECTINFO_DIR): files.append(sysinfo_file) - if files: - parse_info_all(files, self.parsed_data, True) - if self._is_parsed_data_changed(): - return True - - return False + return files def _add_cinfo_log_files(self, cinfo_path=""): - logs_added = 0 + snapshots_added = 0 if not cinfo_path: - return logs_added, "Collectinfo path not specified." + return snapshots_added, "Collectinfo path not specified." if not os.path.exists(cinfo_path): - return logs_added, "Wrong Collectinfo path." - - error = "" - if os.path.isdir(cinfo_path): - for log_file in self._get_files_by_type(CLUSTER_FILE, cinfo_path): - timestamp = self.reader.get_timestamp(log_file) - - if timestamp: - cinfo_log = CollectinfoLog( - timestamp, log_file, self.reader) - self.selected_cinfo_logs[timestamp] = cinfo_log - self.all_cinfo_logs[timestamp] = cinfo_log - logs_added += 1 - if not self.cinfo_timestamp: - self.cinfo_timestamp = timestamp - else: - return logs_added, "Missing timestamp, cannot add specified collectinfo file " + str(log_file) + ". Only supports collectinfo generated by asadm (>=0.0.13)." - - if logs_added == 0: - return 0, "No aerospike collectinfo file found at " + str(cinfo_path) - - elif (os.path.isfile(cinfo_path) - and self.reader.is_cinfo_log_file(cinfo_path)): - timestamp = self.reader.get_timestamp(cinfo_path) - - if timestamp: - cinfo_log = CollectinfoLog(timestamp, cinfo_path, self.reader) - self.selected_cinfo_logs[timestamp] = cinfo_log - self.all_cinfo_logs[timestamp] = cinfo_log - logs_added += 1 - if not self.cinfo_timestamp: - self.cinfo_timestamp = timestamp - else: - return 0, "Missing timestamp, cannot add specified collectinfo file " + str(cinfo_path) + ". Only supports collectinfo generated by asadm (>=0.0.13)." - - elif (os.path.isfile(cinfo_path) - and self.reader.is_system_log_file(cinfo_path)): - return logs_added, "Only sysinfo file path is not sufficient for collectinfo-analyzer. Please provide collectinfo directory path." + return snapshots_added, "Wrong Collectinfo path." + files = self._get_all_file_paths(cinfo_path) + if files: + cinfo_log = CollectinfoLog(cinfo_path, files, self.reader) + self.selected_cinfo_logs = cinfo_log.snapshots + self.all_cinfo_logs = cinfo_log.snapshots + snapshots_added = len(self.all_cinfo_logs) + return snapshots_added, "" else: - return logs_added, "Incorrect collectinfo path " + str(cinfo_path) + " specified. Please provide correct collectinfo directory path." + return snapshots_added, "Incorrect collectinfo path " + str(cinfo_path) + " specified. Please provide correct collectinfo directory path." - return logs_added, "" + return snapshots_added, "" - def _fetch_from_cinfo_log(self, type="", stanza=""): - res_dic = {} - if not stanza or not type: - return res_dic - - for timestamp in sorted(self.selected_cinfo_logs.keys()): - try: - res_dic[timestamp] = self.selected_cinfo_logs[ - timestamp].get_data(type=type, stanza=stanza) - except Exception: - continue - - return res_dic - - def _fetch_from_parsed_as_data(self, info_type="", stanza=""): + def _fetch_from_cinfo_log(self, type="", stanza="", flip=False): res_dict = {} - if not info_type or not stanza: - return res_dict - for ts in sorted(self.parsed_data.keys()): - res_dict[ts] = {} - for cl in self.parsed_data[ts]: - data = self.parsed_data[ts][cl] + if not type: + return res_dict - for node in data: + for timestamp in sorted(self.selected_cinfo_logs.keys()): + try: + out = self.selected_cinfo_logs[timestamp].get_data(type=type, stanza=stanza) + if flip: + out = util.flip_keys(out) - try: - d = copy.deepcopy(data[node]['as_stat'][info_type]) - if not d or isinstance(d, Exception): - continue - - if node not in res_dict[ts]: - res_dict[ts][node] = {} - - if stanza in ['namespace', 'bin', 'set', 'sindex']: - d = d["namespace"] - - for ns_name in d.keys(): - if stanza == "namespace": - res_dict[ts][node][ns_name] = d[ - ns_name]["service"] - elif stanza == "bin": - res_dict[ts][node][ns_name] = d[ - ns_name][stanza] - elif stanza in ["set", "sindex"]: - - for _name in d[ns_name][stanza]: - _key = "%s %s" % (ns_name, _name) - res_dict[ts][node][_key] = d[ - ns_name][stanza][_name] - else: - res_dict[ts][node] = d[stanza] + res_dict[timestamp] = out - except Exception: - pass + except Exception: + continue return res_dict diff --git a/lib/collectinfo/reader.py b/lib/collectinfo/reader.py index 7614baff..d65c2c54 100644 --- a/lib/collectinfo/reader.py +++ b/lib/collectinfo/reader.py @@ -12,68 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime -import re -import time - from lib.utils.util import shell_command -from lib.utils.constants import * - -INDEX_DT_LEN = 4 -STEP = 1000 class CollectinfoReader(object): - ascollectinfo_ext1 = "/ascollectinfo.log" - ascollectinfo_ext2 = "/*.log" - summary_pattern = '~([^~]+) Information(~+)' - network_start_pattern = 'Network Information' - service_start_pattern = 'Service Configuration' - network_end_pattern = 'Number of rows' - section_separator = "(=+)ASCOLLECTINFO(=+)" - section_separator_with_date = "(=+)ASCOLLECTINFO\(([\d_]*)\)(=+)" - stats_pattern = "\[\'statistics\'" - config_pattern = "\[\'config\'" - config_diff_pattern = "\[\'config\',[\s]*\'diff\'" - distribution_pattern = "\[\'distribution\'" cinfo_log_file_identifier_key = "=ASCOLLECTINFO" cinfo_log_file_identifiers = ["Configuration~~~", "Statistics~"] system_log_file_identifier_key = "=ASCOLLECTINFO" system_log_file_identifiers = ["hostname -I", "uname -a", "ip addr", "Data collection for get_awsdata in progress", "top -n", "cat /var/log/syslog"] - def get_node_names(self, path): - node_names = [] - lines = open(path, 'r').readlines() - line = lines.pop(0) - while(line): - if re.search(self.service_start_pattern, line): - line = lines.pop(0) - nodes = line.split() - node_names = nodes[2:len(nodes)] - break - line = lines.pop(0) - return node_names - - def get_timestamp(self, log_file): - file_id = open(log_file, "r") - file_id.seek(0, 0) - timestamp = "" - while not timestamp: - line = file_id.readline() - timestamp = line.strip().strip("\n").strip() - if timestamp.endswith("UTC"): - return timestamp - elif "===ASCOLLECTINFO===" in timestamp: - return self._extract_timestamp_from_path(log_file) - elif re.search(self.section_separator_with_date, timestamp): - dt_tm_str = re.search( - self.section_separator_with_date, timestamp).group(2) - date_object = datetime.datetime.strptime( - dt_tm_str, '%Y%m%d_%H%M%S') - return date_object.strftime('%Y-%m-%d %H:%M:%S UTC') - return "" - def is_cinfo_log_file(self, log_file=""): if not log_file: return False @@ -136,369 +84,3 @@ def is_system_log_file(self, log_file=""): return True return False - def read(self, path): - loginfo = {} - loginfo["statistics"] = {} - loginfo["config"] = {} - loginfo["distribution"] = {} - loginfo["summary"] = {} - file_id = open(path, "r") - line = file_id.readline() - - while(line): - config_pattern_matched = re.search(self.config_pattern, line) - distribution_pattern_matched = re.search(self.distribution_pattern, line) - stats_pattern_matched = re.search(self.stats_pattern, line) - summary_pattern_matched = re.search(self.summary_pattern, line) - - if config_pattern_matched: - try: - if not re.search(self.config_diff_pattern, line): - loginfo["config"].update(self._read_config(file_id)) - except Exception: - pass - - elif distribution_pattern_matched: - try: - loginfo["distribution"].update( - self._read_distribution(file_id)) - except Exception: - pass - - elif stats_pattern_matched: - try: - loginfo["statistics"].update(self._read_stats(file_id)) - except Exception: - pass - - elif summary_pattern_matched: - try: - loginfo["summary"].update( - self._read_summary(file_id, line)) - except Exception: - pass - - try: - line = file_id.readline() - except IndexError: - break - - return loginfo - - def _extract_timestamp_from_path(self, path): - try: - filename = re.split("/", path)[-2] - except Exception: - filename = path - try: - return time.strftime( - '%Y-%m-%d %H:%M:%S', - time.localtime( - float( - re.split( - '_', - filename)[2]))) - except Exception: - return filename - - def _htable_to_dict(self, file_id): - current_line = 0 - nodes = [] - res_dir = {} - line = file_id.readline() - while(line.strip().__len__() != 0 and not line.startswith('~')): - if current_line == 0: - temp_nodes = line.split() - nodes = temp_nodes[2:len(temp_nodes)] - for node in nodes: - res_dir[node] = {} - else: - temp_list = line.split() - current_node = 0 - beg = 2 - if len(temp_list) > 1 and temp_list[1] != ":": - beg = 1 - temp_list[0] = temp_list[0][0:len(temp_list[0]) - 1] - for temp_val in temp_list[beg:len(temp_list)]: - temp_val = temp_val.strip() - # need to make same scenario as cluster mode, in cluster - # mode we do not get any value with 'N/E' - if temp_val.strip() == 'N/E': - current_node += 1 - continue - temp_dir = {} - if res_dir: - if nodes[current_node] not in res_dir: - res_dir[nodes[current_node]] = {} - temp_dir = res_dir[nodes[current_node]] - temp_dir[temp_list[0]] = temp_val - res_dir[nodes[current_node]] = temp_dir - current_node += 1 - - current_line += 1 - line = file_id.readline() - return res_dir - - def _vtable_to_dict(self, file_id): - res_dic = {} - line = file_id.readline() - while (line.strip().__len__() != 0 - and (line.split()[0].strip() != "Node" - and not line.strip().startswith("Number of rows"))): - line = file_id.readline() - - if line.strip().__len__() == 0 or line.strip().startswith("Number of rows"): - return res_dic - columns = [] - - while (line.strip().__len__() != 0 - and not line.strip().startswith('~') - and not line.strip().startswith("Number of rows")): - - if line.strip().startswith('.') or line.strip().startswith('Node'): - temp_columns = line.split()[1:] - if not columns: - columns = temp_columns - else: - _columns = ["%s %s" % (c1.strip(), c2.strip()) - for c1, c2 in zip(columns, temp_columns)] - columns = _columns - else: - temp_list = line.split() - current_column = 0 - temp_dic = {} - for temp_val in temp_list[1:len(temp_list)]: - temp_val = temp_val.strip() - try: - # bytewise distribution values are in K,M format... to - # fix this issue we need to differentiate between float - # and string - float(temp_val) - except Exception: - current_column -= 1 - column = columns[current_column] - if column in temp_dic: - temp_dic[column] += " %s" % (temp_val) - else: - temp_dic[column] = temp_val - current_column += 1 - res_dic[temp_list[0]] = {} - res_dic[temp_list[0]]['values'] = temp_dic - line = file_id.readline() - - return columns, res_dic - - def _dist_table_to_dict(self, file_id): - result = {} - line = file_id.readline() - while (line.strip().__len__() != 0 - and (line.split()[0].strip() != "Node" - and not line.strip().startswith("Number of rows"))): - line = file_id.readline() - - if (line.strip().__len__() == 0 - or line.strip().startswith("Number of rows")): - return result - - line = file_id.readline() - while not line.strip().startswith("Number of rows"): - vals = line.split() - data = {} - data['percentiles'] = vals[1:len(vals)] - result[vals[0]] = data - line = file_id.readline() - - #file_id.seek(1, 1) - return result - - def _read_stats(self, file_id): - stat_dic = {} - - bin_pattern = '~([^~]+) Bin Statistics' - set_pattern = '~([^~]+) Set Statistics' - service_pattern = 'Service Statistics' - ns_pattern = '~([^~]+) Namespace Statistics' - xdr_pattern = 'XDR Statistics' - dc_pattern = '~([^~]+) DC Statistics' - sindex_pattern = '~([^~]+) Sindex Statistics' - - line = file_id.readline() - while (line - and not re.search(self.section_separator, line) - and not re.search(self.section_separator_with_date, line)): - if line.strip().__len__() != 0: - dic = {} - key = "key" - if re.search(bin_pattern, line): - if STAT_BINS not in stat_dic: - stat_dic[STAT_BINS] = {} - dic = stat_dic[STAT_BINS] - key = re.search(bin_pattern, line).group(1) - elif re.search(set_pattern, line): - if STAT_SETS not in stat_dic: - stat_dic[STAT_SETS] = {} - dic = stat_dic[STAT_SETS] - key = re.search(set_pattern, line).group(1) - elif re.search(service_pattern, line): - dic = stat_dic - key = STAT_SERVICE - elif re.search(ns_pattern, line): - if STAT_NAMESPACE not in stat_dic: - stat_dic[STAT_NAMESPACE] = {} - dic = stat_dic[STAT_NAMESPACE] - key = re.search(ns_pattern, line).group(1) - elif re.search(xdr_pattern, line): - dic = stat_dic - key = STAT_XDR - elif re.search(dc_pattern, line): - if STAT_DC not in stat_dic: - stat_dic[STAT_DC] = {} - dic = stat_dic[STAT_DC] - key = re.search(dc_pattern, line).group(1) - elif re.search(sindex_pattern, line): - if STAT_SINDEX not in stat_dic: - stat_dic[STAT_SINDEX] = {} - dic = stat_dic[STAT_SINDEX] - key = re.search(sindex_pattern, line).group(1) - - dic[key] = self._htable_to_dict(file_id) - - try: - line = file_id.readline() - except Exception: - break - - return stat_dic - - def _read_config(self, file_id): - config_dic = {} - service_pattern = '(~+)Service Configuration(~+)' - net_pattern = '(~+)Network Configuration(~+)' - ns_pattern = '~([^~]+)Namespace Configuration(~+)' - xdr_pattern = '(~+)XDR Configuration(~+)' - dc_pattern = '~([^~]+)DC Configuration(~+)' - cluster_pattern = '(~+)Cluster Configuration(~+)' - - line = file_id.readline() - - while (line - and not re.search(self.section_separator, line) - and not re.search(self.section_separator_with_date, line)): - if line.strip().__len__() != 0: - dic = {} - key = "key" - if re.search(service_pattern, line): - dic = config_dic - key = CONFIG_SERVICE - elif re.search(net_pattern, line): - dic = config_dic - key = CONFIG_NETWORK - elif re.search(ns_pattern, line): - if CONFIG_NAMESPACE not in config_dic: - config_dic[CONFIG_NAMESPACE] = {} - dic = config_dic[CONFIG_NAMESPACE] - key = re.search(ns_pattern, line).group(1).strip() - elif re.search(xdr_pattern, line): - dic = config_dic - key = CONFIG_XDR - elif re.search(dc_pattern, line): - if CONFIG_DC not in config_dic: - config_dic[CONFIG_DC] = {} - dic = config_dic[CONFIG_DC] - key = re.search(dc_pattern, line).group(1).strip() - elif re.search(cluster_pattern, line): - dic = config_dic - key = CONFIG_CLUSTER - - dic[key] = self._htable_to_dict(file_id) - try: - line = file_id.readline() - except IndexError: - break - return config_dic - - def _read_summary(self, file_id, header): - summary_info = {} - summary_pattern_matched = re.search(self.summary_pattern, header) - if not summary_pattern_matched: - return summary_info - - stanza = summary_pattern_matched.group(1) - stanza = stanza.lower() - if stanza: - if stanza == "secondary index": - stanza = SUMMARY_SINDEX - elif stanza == "set": - stanza = SUMMARY_SETS - - summary_info[stanza] = header + self._read_summary_str(file_id) - - return summary_info - - def _read_summary_str(self, file_id): - line = file_id.readline() - summary_str = "" - while (line - and not re.search(self.section_separator, line) - and not re.search(self.section_separator_with_date, line)): - if line.strip().__len__() != 0: - summary_str += line - try: - line = file_id.readline() - except IndexError: - break - - return summary_str - - def _read_distribution(self, file_id): - config_dic = {} - - ttl_pattern = '~([^~]+) - TTL Distribution in Seconds(~+)' - evict_pattern = '~([^~]+) - Eviction Distribution in Seconds(~+)' - objsz_pattern = '~([^~]+) - Object Size Distribution in Record Blocks(~+)' - objsz_bytes_pattern = '([^~]+) - Object Size Distribution in Bytes' - - line = file_id.readline() - bytewise_distribution = False - while (line - and not re.search(self.section_separator, line) - and not re.search(self.section_separator_with_date, line)): - if line.strip().__len__() != 0: - m1 = re.search(ttl_pattern, line) - m2 = re.search(evict_pattern, line) - m3 = re.search(objsz_pattern, line) - m4 = re.search(objsz_bytes_pattern, line) - dic = {} - key = "key" - if m1: - if "ttl" not in config_dic: - config_dic["ttl"] = {} - dic = config_dic["ttl"] - key = m1.group(1).strip() - elif m2: - if "evict" not in config_dic: - config_dic["evict"] = {} - dic = config_dic["evict"] - key = m2.group(1).strip() - elif m3: - if "objsz" not in config_dic: - config_dic["objsz"] = {} - dic = config_dic["objsz"] - key = m3.group(1).strip() - elif m4: - if "objsz-b" not in config_dic: - config_dic["objsz-b"] = {} - dic = config_dic["objsz-b"] - key = m4.group(1).strip() - bytewise_distribution = True - - if bytewise_distribution: - columns, dic[key] = self._vtable_to_dict(file_id) - dic[key]['columns'] = columns - else: - dic[key] = self._dist_table_to_dict(file_id) - try: - line = file_id.readline() - except IndexError: - break - return config_dic diff --git a/lib/collectinfo_parser/as_section_parser.py b/lib/collectinfo_parser/as_section_parser.py index d7153d14..a46cf70c 100644 --- a/lib/collectinfo_parser/as_section_parser.py +++ b/lib/collectinfo_parser/as_section_parser.py @@ -16,7 +16,7 @@ import copy import logging import section_filter_list -from utils import is_valid_section, get_section_name_from_id, is_bool +from utils import is_valid_section, get_section_name_from_id, is_bool, is_collision_allowed_for_section logger = logging.getLogger(__name__) @@ -52,6 +52,9 @@ def parse_as_section(section_list, imap, parsed_map): elif section == 'config.xdr': _parse_xdr_config_section(nodes, imap, parsed_map) + elif section == 'config.cluster': + _parse_cluster_config_section(nodes, imap, parsed_map) + elif section == 'latency': _parse_latency_section(nodes, imap, parsed_map) @@ -61,6 +64,18 @@ def parse_as_section(section_list, imap, parsed_map): elif section == 'features': _parse_features(nodes, imap, parsed_map) + elif section == 'histogram.ttl': + _parse_hist_dump_ttl(nodes, imap, parsed_map) + + elif section == 'histogram.objsz': + _parse_hist_dump_objsz(nodes, imap, parsed_map) + + elif section == 'endpoints': + _parse_endpoints(nodes, imap, parsed_map) + + elif section == 'services': + _parse_services(nodes, imap, parsed_map) + else: logger.warning( "Section unknown, can not be parsed. Check AS_SECTION_NAME_LIST. Section: " + section) @@ -80,6 +95,7 @@ def parse_as_section(section_list, imap, parsed_map): # type_check_basic_values(param_map) parsed_map[node][section] = copy.deepcopy(param_map[section]) + # output: {in_aws: AAA, instance_type: AAA} def get_cluster_name(parsed_map): for node in parsed_map: @@ -99,6 +115,7 @@ def get_meta_info(imap, meta_map): asd_meta = _get_meta_from_network_info(imap, nodes) xdr_meta = _get_xdr_build(imap, nodes) ip_meta = _get_ip_from_network_info(imap, nodes) + for node in nodes: meta_map[node] = {} if node in asd_meta: @@ -684,6 +701,7 @@ def _parse_multi_column_sub_section(raw_section, parsed_map, final_section_name, sub_section_name = sec_line xdr_section = 'xdr' dc_section = 'dc' + cluster_section = 'cluster' cur_sec = '' dc_name = '' @@ -697,6 +715,10 @@ def _parse_multi_column_sub_section(raw_section, parsed_map, final_section_name, dc_name = tok[0] cur_sec = dc_section + elif 'Cluster ' in sub_section_name: + # ~~~~~Cluster Config~~~~ + cur_sec = cluster_section + else: logger.info("Unknown header line: " + sub_section_name) return @@ -710,8 +732,8 @@ def _parse_multi_column_sub_section(raw_section, parsed_map, final_section_name, logger.warning("Nodeid is not in info_network or latency: " + node) continue - # Update XDR and DC information. - if cur_sec == xdr_section: + # Update XDR/DC/Cluster information. + if cur_sec == xdr_section or cur_sec == cluster_section: parsed_sec[final_section_name] = section_obj[node] elif cur_sec == dc_section: @@ -720,7 +742,7 @@ def _parse_multi_column_sub_section(raw_section, parsed_map, final_section_name, parsed_sec[final_section_name][dc_name] = section_obj[node] -def _parse_multi_column_dc_xdr_section(info_section, parsed_map, final_section_name, parent_section_name): +def _parse_multi_column_section(info_section, parsed_map, final_section_name, parent_section_name): section_list = _get_section_array_from_multicolumn_section(info_section) for raw_section in section_list: _parse_multi_column_sub_section(raw_section, parsed_map, @@ -819,7 +841,7 @@ def _get_node_id_from_latency_line(data_string): return None -def _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map): +def _parse_nondefault_section(sec_id, nodes, imap, parsed_map): raw_section_name, final_section_name, parent_section_name = get_section_name_from_id(sec_id) logger.info("Parsing section: " + final_section_name) @@ -837,7 +859,7 @@ def _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map): # initialize only if parent section is not present, do not overwrite. _init_nodes_for_parsed_json(nodes, parsed_map, parent_section_name) - _parse_multi_column_dc_xdr_section( + _parse_multi_column_section( info_section, parsed_map, final_section_name, parent_section_name) @@ -866,13 +888,16 @@ def _parse_config_section(nodes, imap, parsed_map): def _parse_dc_config_section(nodes, imap, parsed_map): sec_id = 'ID_7' - _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map) + _parse_nondefault_section(sec_id, nodes, imap, parsed_map) def _parse_xdr_config_section(nodes, imap, parsed_map): sec_id = 'ID_6' - _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map) + _parse_nondefault_section(sec_id, nodes, imap, parsed_map) +def _parse_cluster_config_section(nodes, imap, parsed_map): + sec_id = 'ID_102' + _parse_nondefault_section(sec_id, nodes, imap, parsed_map) def _get_stat_sindex_section(imap): raw_section_name, final_section_name, _ = get_section_name_from_id('ID_14') @@ -925,12 +950,12 @@ def _parse_stat_section(nodes, imap, parsed_map): def _parse_dc_stat_section(nodes, imap, parsed_map): sec_id = 'ID_13' - _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map) + _parse_nondefault_section(sec_id, nodes, imap, parsed_map) def _parse_xdr_stat_section(nodes, imap, parsed_map): sec_id = 'ID_12' - _parse_dc_xdr_section(sec_id, nodes, imap, parsed_map) + _parse_nondefault_section(sec_id, nodes, imap, parsed_map) def _parse_latency_section(nodes, imap, parsed_map): @@ -1093,6 +1118,96 @@ def _parse_features(nodes, imap, parsed_map): continue parsed_map[node][final_section_name] = featureobj[node] +def _parse_hist_dump(section): + namespace = None + parsed_section = {} + if not section or len(section) <= 0: + return namespace, parsed_section + + parsed_section = eval(section[0]) + + if not parsed_section: + return namespace, parsed_section + + for node, hist_dump in parsed_section.items(): + if not node or not hist_dump or isinstance(hist_dump, Exception) or ":" not in hist_dump: + continue + + namespace = hist_dump.split(':')[0].strip() + break + + return namespace, parsed_section + +def _parse_hist_dump_section(sec_id, nodes, imap, parsed_map): + raw_section_name, final_section_name, parent_section_name = get_section_name_from_id(sec_id) + logger.info("Parsing section: " + final_section_name) + + if not is_valid_section(imap, raw_section_name, final_section_name, collision_allowed=is_collision_allowed_for_section(sec_id)): + return + + hist_dump_sections = imap[raw_section_name] + + for hist_dump_section in hist_dump_sections: + namespace, hist_dump_section = _parse_hist_dump(hist_dump_section) + if not namespace: + continue + + for node, hist_dump in hist_dump_section.items(): + map_ptr = None + if node not in parsed_map: + parsed_map[node] = {} + map_ptr = parsed_map[node] + + if parent_section_name: + if parent_section_name not in map_ptr: + map_ptr[parent_section_name] = {} + map_ptr = map_ptr[parent_section_name] + + if final_section_name not in map_ptr: + map_ptr[final_section_name] = {} + map_ptr = map_ptr[final_section_name] + + map_ptr[namespace] = copy.deepcopy(hist_dump) + +def _parse_hist_dump_ttl(nodes, imap, parsed_map): + sec_id = 'ID_98' + _parse_hist_dump_section(sec_id, nodes, imap, parsed_map) + +def _parse_hist_dump_objsz(nodes, imap, parsed_map): + sec_id = 'ID_99' + _parse_hist_dump_section(sec_id, nodes, imap, parsed_map) + +def _parse_asinfo_node_value_section(sec_id, imap, parsed_map): + raw_section_name, final_section_name, parent_section_name = get_section_name_from_id(sec_id) + + if not is_valid_section(imap, raw_section_name, final_section_name): + return + + for raw_dump in imap[raw_section_name]: + try: + for node, val in eval(raw_dump[0]).items(): + map_ptr = None + if node not in parsed_map: + parsed_map[node] = {} + map_ptr = parsed_map[node] + + if parent_section_name: + if parent_section_name not in map_ptr: + map_ptr[parent_section_name] = {} + map_ptr = map_ptr[parent_section_name] + + map_ptr[final_section_name] = val + + except Exception: + pass + +def _parse_endpoints(nodes, imap, parsed_map): + sec_id = 'ID_55' + _parse_asinfo_node_value_section(sec_id, imap, parsed_map) + +def _parse_services(nodes, imap, parsed_map): + sec_id = 'ID_56' + _parse_asinfo_node_value_section(sec_id, imap, parsed_map) def _stat_exist_in_statistics(statmap, statlist): if not statmap: @@ -1237,7 +1352,6 @@ def _identify_features_from_stats(nodes, imap, parsed_map, section_name): break parsed_map[node][section_name] = featureobj - def _get_xdr_build(imap, nodes): raw_section_name, final_section_name, _ = get_section_name_from_id('ID_3') @@ -1282,27 +1396,52 @@ def _get_meta_from_network_info(imap, nodes): info_section_lines = imap[raw_section_name][0] meta_map = {} del_found = False + node_found = False skip_lines = 2 build_found = False - build_index = 0 + build_index = -1 + node_id_found = False + node_id_index = -1 + node_indices = [] for i in range(len(info_section_lines)): if "~~~~~~~" in info_section_lines[i] or "====" in info_section_lines[i]: del_found = True continue + # Get index for node_ids + if node_found and 'Id' in info_section_lines[i]: + tags = info_section_lines[i].split() + for j in node_indices: + if 'Id' in tags[j]: + node_id_found = True + node_id_index = j + + if node_id_index == -1: + node_id_index = len(tags) + 1 + # Get index for build - if 'Node' in info_section_lines[i]: + if not node_found and 'Node' in info_section_lines[i]: + index = 0 tags = info_section_lines[i].split() for tag in tags: - if 'Build' in tag: + if not build_found and 'Build' in tag: build_found = True - break - build_index = build_index + 1 + build_index = index + # break + if 'Node' in tag: + node_indices.append(index) + index += 1 + + if build_index == -1: + build_index = index + + node_found = True if del_found: if skip_lines == 0: break skip_lines = skip_lines - 1 + for node in nodes: for i in range(len(info_section_lines)): if node not in info_section_lines[i]: @@ -1312,6 +1451,7 @@ def _get_meta_from_network_info(imap, nodes): node_line = info_section_lines[i].split() build = '' edition = '' + node_id = '' if build_found: if 'C-' in node_line[build_index]: build = node_line[build_index][2:] @@ -1322,8 +1462,15 @@ def _get_meta_from_network_info(imap, nodes): else: build = node_line[build_index] + if node_id_found: + node_id = node_line[node_id_index] + if node_id: + node_id = node_id.strip() + meta_map[node]['asd_build'] = build meta_map[node]['edition'] = edition + meta_map[node]['node_id'] = node_id + return meta_map def _get_ip_from_network_info(imap, nodes): diff --git a/lib/collectinfo_parser/cinfo_parser.py b/lib/collectinfo_parser/cinfo_parser.py index 860c9fb8..29d9d17b 100644 --- a/lib/collectinfo_parser/cinfo_parser.py +++ b/lib/collectinfo_parser/cinfo_parser.py @@ -125,7 +125,7 @@ def _imap_verify_section_count(imap, imap_old_keys, n_section, ignore_exception) logger.debug("imap_sec: " + str(imap_n_section) + "n_section: " + str(n_section)) - if imap_n_section != n_section: + if imap_n_section != n_section and not ignore_exception: logger.error( "Something wrong, no of section in file and no of extracted are not matching") logger.error("imap_sec: " + str(imap_n_section) + diff --git a/lib/collectinfo_parser/full_parser.py b/lib/collectinfo_parser/full_parser.py index d9d70a07..685ede04 100644 --- a/lib/collectinfo_parser/full_parser.py +++ b/lib/collectinfo_parser/full_parser.py @@ -12,23 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -from as_section_parser import parse_as_section, get_meta_info, get_cluster_name -from sys_section_parser import parse_sys_section - -import section_filter_list -import logging -import cinfo_parser -import json import copy from datetime import datetime +import json +import logging +import os + +from as_section_parser import parse_as_section, get_meta_info, get_cluster_name +import cinfo_parser +import section_filter_list +from sys_section_parser import parse_sys_section logger = logging.getLogger(__name__) AS_SECTION_NAME_LIST = section_filter_list.AS_SECTION_NAME_LIST +HISTOGRAM_SECTION_NAME_LIST = section_filter_list.HISTOGRAM_SECTION_NAME_LIST SYS_SECTION_NAME_LIST = section_filter_list.SYS_SECTION_NAME_LIST SECTION_FILTER_LIST = section_filter_list.FILTER_LIST DERIVED_SECTION_LIST = section_filter_list.DERIVED_SECTION_LIST + def parse_info_all(cinfo_paths, parsed_map, ignore_exception=False): UNKNOWN_NODE = 'UNKNOWN_NODE' @@ -36,14 +39,15 @@ def parse_info_all(cinfo_paths, parsed_map, ignore_exception=False): imap = {} timestamp = '' + json_parsed_timestamps = [] # IF a valid cinfo json is present in cinfo_paths then append # its data in parsed_map. for cinfo_path_name in cinfo_paths: - if 'json' in cinfo_path_name: + if os.path.splitext(cinfo_path_name)[1] == ".json": cinfo_map = {} try: with open(cinfo_path_name) as cinfo_json: - cinfo_map = json.load(cinfo_json) + cinfo_map = json.load(cinfo_json, object_hook=_stringify) except IOError as e: if not ignore_exception: logger.error(str(e)) @@ -54,48 +58,40 @@ def parse_info_all(cinfo_paths, parsed_map, ignore_exception=False): else: logger.info("File is already pasred_json: " + cinfo_path_name) parsed_map.update(cinfo_map) - return + if _is_complete_json_data(cinfo_map): + return + json_parsed_timestamps = cinfo_map.keys() + break for cinfo_path in cinfo_paths: + if os.path.splitext(cinfo_path)[1] == ".json": + continue + if timestamp == '': timestamp = cinfo_parser.get_timestamp_from_file(cinfo_path) try: - cinfo_parser.extract_validate_filter_section_from_file( - cinfo_path, imap, ignore_exception) + cinfo_parser.extract_validate_filter_section_from_file(cinfo_path, imap, ignore_exception) except Exception as e: if not ignore_exception: logger.error("Cinfo parser can not create intermediate json. Err: " + str(e)) raise + if json_parsed_timestamps: + return _add_missing_data(imap, parsed_map, json_parsed_timestamps, ignore_exception) + # get as_map using imap - as_map = {} - as_section_list = _get_section_list_for_parsing(imap, AS_SECTION_NAME_LIST) - try: - parse_as_section(as_section_list, imap, as_map) - except Exception as e: - if not ignore_exception: - logger.error("as_section_parser can not parse intermediate json. Err: " + str(e)) - raise + as_map = _get_as_map(imap, AS_SECTION_NAME_LIST, ignore_exception) + + # get histogram_map using imap + histogram_map = _get_as_map(imap, HISTOGRAM_SECTION_NAME_LIST, ignore_exception) # get sys_map using imap - sys_map = {} - sys_section_list = _get_section_list_for_parsing( - imap, SYS_SECTION_NAME_LIST) - try: - parse_sys_section(sys_section_list, imap, sys_map) - except Exception as e: - if not ignore_exception: - logger.error("sys_section_parser can not parse intermediate json. Err: " + str(e)) - raise + sys_map = _get_sys_map(imap, ignore_exception) # get meta_map using imap - meta_map = {} - try: - get_meta_info(imap, meta_map) - except Exception as e: - if not ignore_exception: - logger.error("as_section_parser can not parse intermediate json to get meta info. Err: " + str(e)) - raise + meta_map = _get_meta_map(imap, ignore_exception) + # ip_to_node mapping required for correct arrangement of histogram map + ip_to_node_map = _create_ip_to_node_map(meta_map) # Get valid cluster name # Valid Cluster name could be stored in parsed_map, check that too. @@ -113,21 +109,19 @@ def parse_info_all(cinfo_paths, parsed_map, ignore_exception=False): elif 'null' not in parsed_map[timestamp] and cluster_name == 'null': cluster_name = parsed_map[timestamp].keys()[0] - nodemap = parsed_map[timestamp][cluster_name] # Insert as_stat - for nodeid in as_map: - if nodeid not in nodemap: - nodemap[nodeid] = {} - # TODO can we get better name? - _update_map(nodemap[nodeid], 'as_stat', as_map[nodeid]) + _merge_nodelevel_map_to_mainmap(parsed_map, as_map, [timestamp], keys_after_node_id=["as_stat"], create_new_node=True) + + # Insert histogram stat + _merge_nodelevel_map_to_mainmap(parsed_map, histogram_map, [timestamp], keys_after_node_id=["as_stat"], node_ip_mapping=ip_to_node_map) # insert meta_stat - for nodeid in meta_map: - if nodeid not in nodemap: - nodemap[nodeid] = {} - if 'as_stat' not in nodemap[nodeid]: - nodemap[nodeid]['as_stat'] = {} - _update_map(nodemap[nodeid]['as_stat'], 'meta_data', meta_map[nodeid]) + _merge_nodelevel_map_to_mainmap(parsed_map, meta_map, [timestamp], keys_after_node_id=["as_stat", "meta_data"], node_ip_mapping=ip_to_node_map) + + # insert endpoints + _add_missing_endpoints_data(imap, parsed_map, [timestamp], ip_to_node_map, ignore_exception) + + nodemap = parsed_map[timestamp][cluster_name] node_ip_map = _create_node_ip_map(meta_map) # Insert sys_stat @@ -258,7 +252,6 @@ def _is_valid_collectinfo_json(cinfo_map): for timestamp in cinfo_map: try: datetime.strptime(timestamp, timestamp_format) - return True except ValueError: return False @@ -278,3 +271,250 @@ def _create_node_ip_map(nodemap): pass return node_ip_map + +def _is_complete_json_data(cinfo_map): + """ + Check cinfo_map parsed from json file is having all necessary data or not. + Old json file does not have some data sections Ex. node_id, histogram, cluster config etc. + + """ + + if not cinfo_map: + return True + + for timestamp in cinfo_map: + if cinfo_map[timestamp]: + + for cl in cinfo_map[timestamp]: + if cinfo_map[timestamp][cl]: + + for node in cinfo_map[timestamp][cl]: + try: + if "node_id" in cinfo_map[timestamp][cl][node]["as_stat"]["meta_data"]: + return True + + except Exception: + return False + + return False + +def _create_ip_to_node_map(meta_map): + """ + Create IP to NodeId mapping from meta_map + + """ + + ip_to_node = {} + if not meta_map or not isinstance(meta_map, dict): + return ip_to_node + + for node in meta_map: + if not meta_map[node] or not 'ip' in meta_map[node]: + continue + + ip_to_node[meta_map[node]['ip']] = node + + return ip_to_node + +def _stringify(input): + """ + Convert unicode to string. + + """ + + if isinstance(input, dict): + data = {} + for _k,v in input.iteritems(): + data[_stringify(_k)] = _stringify(v) + + return data + + elif isinstance(input, list): + return [_stringify(element) for element in input] + + elif isinstance(input, unicode): + return str(input) + + else: + return input + +def _merge_samelevel_maps(main_map, from_map): + ''' + :param main_map: main dictionary to update + :param from_map: dictionary to merge into main_map + :return: updated main_map + ''' + + if not main_map: + return copy.deepcopy(from_map) + + if not isinstance(from_map, dict): + return main_map + + for _k in from_map: + + if _k not in main_map: + main_map[_k] = copy.deepcopy(from_map[_k]) + + elif _k in main_map and isinstance(main_map[_k], dict) and isinstance(from_map[_k], dict): + main_map[_k] = _merge_samelevel_maps(main_map[_k], from_map[_k]) + + return main_map + +def _merge_nodelevel_map_to_mainmap(main_map, nodes_data_map, timestamps, node_ip_mapping={}, keys_after_node_id=[], create_new_node=False): + ''' + :param main_map: main dictionary which is output of this function. Format should be {timestamp: { cluster: {nodeid: {....}}}} + :param nodes_data_map: dictionary to merge into main_map. Format should be {nodeid: {...}} + :param timestamps: list of timestamps to consider while merging + :param node_ip_mapping: NodeId to IP or IP to NodeId mapping + :param keys_after_node_id: List of extra keys to add after nodeid + :param create_new_node: True if want to force to create new nodeid which is available in nodes_data_map but not available in main_map + :return: updated main_map + ''' + + if not nodes_data_map: + return + + for timestamp in timestamps: + if timestamp not in main_map: + main_map[timestamp] = {} + main_map[timestamp]["null"] = {} + + for cl in main_map[timestamp]: + for node in nodes_data_map: + node_key = None + + if node in main_map[timestamp][cl]: + node_key = node + + elif node in node_ip_mapping and node_ip_mapping[node] in main_map[timestamp][cl]: + node_key = node_ip_mapping[node] + + elif create_new_node: + node_key = node + main_map[timestamp][cl][node_key] = {} + + if not node_key: + continue + + if not keys_after_node_id or len(keys_after_node_id) == 0: + _dict_ptr = main_map[timestamp][cl] + _key = node_key + + elif len(keys_after_node_id) == 1: + _dict_ptr = main_map[timestamp][cl][node_key] + _key = keys_after_node_id[0] + if _key not in _dict_ptr: + _dict_ptr[_key] = {} + + else: + _dict_ptr = main_map[timestamp][cl][node_key] + for _k in keys_after_node_id[:-1]: + if _k not in _dict_ptr: + _dict_ptr[_k] = {} + _dict_ptr = _dict_ptr[_k] + _key = keys_after_node_id[-1] + if _key not in _dict_ptr: + _dict_ptr[_key] = {} + + _dict_ptr[_key] = _merge_samelevel_maps(_dict_ptr[_key], nodes_data_map[node]) + + +def _get_meta_map(imap, ignore_exception): + """ + Extract Metadata information from imap + + """ + + meta_map = {} + + try: + get_meta_info(imap, meta_map) + + except Exception as e: + + if not ignore_exception: + logger.error("as_section_parser can not parse intermediate json to get meta info. Err: " + str(e)) + raise + + return meta_map + +def _get_as_map(imap, as_section_name_list, ignore_exception): + """ + Extract Aerospike information (config, stats, histogram dump) from imap + + """ + + as_map = {} + as_section_list = _get_section_list_for_parsing(imap, as_section_name_list) + + try: + parse_as_section(as_section_list, imap, as_map) + except Exception as e: + + if not ignore_exception: + logger.error("as_section_parser can not parse intermediate json. Err: " + str(e)) + raise + + return as_map + +def _get_sys_map(imap, ignore_exception): + """ + Extract System information from imap + + """ + + sys_map = {} + sys_section_list = _get_section_list_for_parsing(imap, SYS_SECTION_NAME_LIST) + + try: + parse_sys_section(sys_section_list, imap, sys_map) + + except Exception as e: + + if not ignore_exception: + logger.error("sys_section_parser can not parse intermediate json. Err: " + str(e)) + raise + + return sys_map + +def _add_missing_as_data(imap, parsed_map, timestamps, node_ip_mapping, ignore_exception): + """ + Add missing Aerospike data (config and stats) into parsed_map which is loaded from old format json file + + """ + + as_section_name_list = ["config.cluster"] + as_map = _get_as_map(imap, as_section_name_list, ignore_exception) + _merge_nodelevel_map_to_mainmap(parsed_map, as_map, timestamps, node_ip_mapping, ["as_stat"]) + +def _add_missing_endpoints_data(imap, parsed_map, timestamps, node_ip_mapping, ignore_exception): + """ + Add missing Aerospike data (config and stats) into parsed_map which is loaded from old format json file + + """ + as_section_name_list = ["endpoints", "services"] + as_map = _get_as_map(imap, as_section_name_list, ignore_exception) + _merge_nodelevel_map_to_mainmap(parsed_map, as_map, timestamps, node_ip_mapping, ["as_stat", "meta_data"]) + +def _add_missing_histogram_data(imap, parsed_map, timestamps, node_ip_mapping, ignore_exception): + """ + Add missing Aerospike histogram data into parsed_map which is loaded from old format json file + + """ + + histogram_map = _get_as_map(imap, HISTOGRAM_SECTION_NAME_LIST, ignore_exception) + _merge_nodelevel_map_to_mainmap(parsed_map, histogram_map, timestamps, node_ip_mapping, ["as_stat"]) + +def _add_missing_data(imap, parsed_map, timestamps, ignore_exception): + """ + Add missing data (Aerospike stats, config, metadata and histogram dump) into parsed_map which is loaded from old format json file + + """ + + meta_map = _get_meta_map(imap, ignore_exception) + node_to_ip_mapping = _create_node_ip_map(meta_map) + _merge_nodelevel_map_to_mainmap(parsed_map, meta_map, timestamps, node_to_ip_mapping, ["as_stat", "meta_data"]) + _add_missing_as_data(imap, parsed_map, timestamps, node_to_ip_mapping, ignore_exception) + _add_missing_histogram_data(imap, parsed_map, timestamps, node_to_ip_mapping, ignore_exception) + _add_missing_endpoints_data(imap, parsed_map, timestamps, node_to_ip_mapping, ignore_exception) diff --git a/lib/collectinfo_parser/section_filter_list.py b/lib/collectinfo_parser/section_filter_list.py index 333064c8..c1d10499 100644 --- a/lib/collectinfo_parser/section_filter_list.py +++ b/lib/collectinfo_parser/section_filter_list.py @@ -23,6 +23,7 @@ # } # Param 'regex_new': regex for collectinfos having delimiter. # Param 'regex_old': regex for collectinfos, not having delimiter. +# Param 'collision_allowed': True if multiple sections allowed for same final_section_name FILTER_LIST = { 'ID_1': { 'enable': True, @@ -430,15 +431,17 @@ # TODO:---------------- 'enable': True, 'raw_section_name': 'info_service list', + 'final_section_name': 'endpoints', #'regex_new': '[INFO] Data collection for service in progress..', #'regex_new': "service\n|(?=.*service)(?!.*(services|'service'))" - 'regex_new': "service\n|for service in" + 'regex_new': "service\n|for service in", # 'parser_func': }, 'ID_56': { 'enable': True, 'raw_section_name': 'info_services', - 'regex_new': 'services\n| for services in' + 'final_section_name': 'services', + 'regex_new': 'services\n| for services in', # 'parser_func': }, 'ID_57': { @@ -697,13 +700,19 @@ 'ID_98': { 'enable': True, 'raw_section_name': 'hist-dump:ttl', - 'regex_new': "hist-dump:ns=.*;hist=ttl" + 'final_section_name': 'ttl', + 'regex_new': "hist-dump:ns=.*;hist=ttl", + 'parent_section_name': 'histogram', + 'collision_allowed': True # 'parser_func' }, 'ID_99': { 'enable': True, 'raw_section_name': 'hist-dump:objsz', - 'regex_new': "hist-dump:ns=.*;hist=objsz" + 'final_section_name': 'objsz', + 'regex_new': "hist-dump:ns=.*;hist=objsz", + 'parent_section_name': 'histogram', + 'collision_allowed': True # 'parser_func' }, # SUD: need to be added in dev code @@ -721,6 +730,14 @@ 'regex_new': "\['config', 'cluster'\]" # 'parser_func' }, + 'ID_102': { + 'enable': True, + 'raw_section_name': 'config_cluster', + 'final_section_name': 'cluster', + 'parent_section_name': 'config', + 'regex_new': "\['config', 'cluster'\]" + # 'parser_func' + }, #{ # 'enable': True, # 'raw_section_name': 'set', @@ -730,15 +747,18 @@ } -SKIP_LIST = ['hist-dump', 'dump-wb-summary'] +SKIP_LIST = [ 'dump-wb-summary', 'hist-dump'] # xdr, dc are different component, so pass them separately to parse. # Namespace, sindex, set, bin are basic part so they will be parsed # automatically. AS_SECTION_NAME_LIST = ['statistics', 'statistics.dc', - 'statistics.xdr', 'config', 'config.dc', 'config.xdr'] + 'statistics.xdr', 'config', 'config.dc', 'config.xdr', 'config.cluster'] # Other Available sections ['latency', 'sindex_info', 'features'] SYS_SECTION_NAME_LIST = ['top', 'lsb', 'uname', 'meminfo', 'hostname', 'df', 'free-m', 'iostat', 'interrupts', 'ip_addr'] # Meta data have all meta info (asd_build, xdr_build, cluster_name) DERIVED_SECTION_LIST = ['features'] + +# List of histogram dumps (raw) +HISTOGRAM_SECTION_NAME_LIST = ['histogram.ttl', 'histogram.objsz'] diff --git a/lib/collectinfo_parser/sys_section_parser.py b/lib/collectinfo_parser/sys_section_parser.py index 4bafeca7..f1d22754 100644 --- a/lib/collectinfo_parser/sys_section_parser.py +++ b/lib/collectinfo_parser/sys_section_parser.py @@ -102,7 +102,10 @@ def _get_mem_in_byte_from_str(memstr, mem_unit_len): def _get_bytes_from_float(memstr, shift, mem_unit_len): try: - memnum = float(memstr[:-mem_unit_len]) + if mem_unit_len == 0: + memnum = float(memstr) + else: + memnum = float(memstr[:-mem_unit_len]) except ValueError: return memstr if memstr == '0': @@ -170,12 +173,9 @@ def _parse_top_section(imap, parsed_map): if re.search('top -n3 -b', line): continue - if 'Ki_b' in line: + if 'Ki_b' in line or 'KiB' in line: kib_format = True - if 'KiB' in line: - kib_format = True - # Match object to get uptime in days. # "top - 18:56:45 up 103 days, 13:00, 2 users, load average: 1.29, 1.34, 1.35\n" matchobj_1 = re.match(r'.*up (.*?) days.*', line) @@ -274,9 +274,15 @@ def _parse_top_section(imap, parsed_map): if kib_format: for key in topdata['ram']: - topdata['ram'][key] = topdata['ram'][key] * 1024 + try: + topdata['ram'][key] = _get_bytes_from_float(topdata['ram'][key], 10, 0) + except Exception: + pass for key in topdata['swap']: - topdata['swap'][key] = topdata['swap'][key] * 1024 + try: + topdata['swap'][key] = _get_bytes_from_float(topdata['swap'][key], 10, 0) + except Exception: + pass _replace_comma_from_map_value_field(topdata) diff --git a/lib/collectinfo_parser/utils.py b/lib/collectinfo_parser/utils.py index b17dbdec..db7f225d 100644 --- a/lib/collectinfo_parser/utils.py +++ b/lib/collectinfo_parser/utils.py @@ -92,8 +92,16 @@ def get_section_name_from_id(sec_id): parent_section_name = FILTER_LIST[sec_id]['parent_section_name'] if 'parent_section_name' in FILTER_LIST[sec_id] else '' return raw_section_name, final_section_name, parent_section_name +def is_collision_allowed_for_section(sec_id): + if "collision_allowed" not in FILTER_LIST[sec_id]: + return False + + if FILTER_LIST[sec_id]["collision_allowed"] == True: + return True -def is_valid_section(imap, raw_section_name, final_section_name): + return False + +def is_valid_section(imap, raw_section_name, final_section_name, collision_allowed=False): if not imap: logger.warning("Null section json") return False @@ -102,7 +110,7 @@ def is_valid_section(imap, raw_section_name, final_section_name): logger.warning(raw_section_name + " section not present.") return False - if len(imap[raw_section_name]) > 1: + if len(imap[raw_section_name]) > 1 and not collision_allowed: logger.warning( "More than one entries detected, There is a collision for this section: " + final_section_name) return False @@ -195,4 +203,3 @@ def _type_check_field_and_raw_values(section): for key in keys: section.pop(key, None) - diff --git a/lib/collectinfocontroller.py b/lib/collectinfocontroller.py index ddb401bc..f74e07c5 100644 --- a/lib/collectinfocontroller.py +++ b/lib/collectinfocontroller.py @@ -13,7 +13,6 @@ # limitations under the License. import copy -import os from lib.controllerlib import BaseController, CommandHelp, CommandController from lib.collectinfo.loghdlr import CollectinfoLoghdlr @@ -94,31 +93,13 @@ def _do_default(self, line): @CommandHelp( 'Displays network summary information.') def do_network(self, line): - network_infos = self.loghdlr.info_summary(stanza=SUMMARY_NETWORK) - service_infos = self.loghdlr.info_summary(stanza=SUMMARY_SERVICE) - network_stats = self.loghdlr.info_statistics(stanza=STAT_SERVICE) + service_stats = self.loghdlr.info_statistics(stanza=STAT_SERVICE) cluster_configs = self.loghdlr.info_getconfig(stanza=CONFIG_CLUSTER) - for timestamp in sorted(network_stats.keys()): - if not network_stats[timestamp]: - try: - if service_infos[timestamp]: - self.view.info_string( - timestamp, service_infos[timestamp]) - except Exception: - pass - try: - if network_infos[timestamp]: - self.view.info_string( - timestamp, network_infos[timestamp]) - except Exception: - pass - continue - for node in network_stats[timestamp]: + for timestamp in sorted(service_stats.keys()): + for node in service_stats[timestamp]: try: - if not isinstance(cluster_configs[timestamp][node]["mode"], - Exception): - network_stats[timestamp][node][ - "rackaware_mode"] = cluster_configs[timestamp][node]["mode"] + if not isinstance(cluster_configs[timestamp][node]["mode"], Exception): + service_stats[timestamp][node]["rackaware_mode"] = cluster_configs[timestamp][node]["mode"] except Exception: pass cinfo_log = self.loghdlr.get_cinfo_log_at(timestamp=timestamp) @@ -126,30 +107,37 @@ def do_network(self, line): versions = cinfo_log.get_asd_version() cluster_names = cinfo_log.get_cluster_name() - # Note how clinfo_log mapped to cluster. Both implement interfaces + # Note how cinfo_log mapped to cluster. Both implement interfaces # required by view object - self.view.info_network(network_stats[timestamp], cluster_names, + self.view.info_network(service_stats[timestamp], cluster_names, versions, builds, cluster=cinfo_log, title_suffix=" (%s)" % (timestamp), **self.mods) @CommandHelp( 'Displays namespace summary information.') def do_namespace(self, line): - ns_infos = self.loghdlr.info_summary(stanza=SUMMARY_NAMESPACE) - ns_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE) + ns_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE, flip=True) + for timestamp in sorted(ns_stats.keys()): if not ns_stats[timestamp]: - try: - if ns_infos[timestamp]: - self.view.info_string(timestamp, ns_infos[timestamp]) - except Exception: - pass continue + self.view.info_namespace(util.flip_keys(ns_stats[timestamp]), - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), title_suffix=" (%s)" % (timestamp), **self.mods) + @CommandHelp('Displays summary information for objects of each namespace.') + def do_object(self, line): + ns_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE, flip=True) + + for timestamp in sorted(ns_stats.keys()): + if not ns_stats[timestamp]: + continue + + self.view.info_object(util.flip_keys(ns_stats[timestamp]), + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + title_suffix=" (%s)" % (timestamp), **self.mods) + def convert_key_to_tuple(self, stats): for key in stats.keys(): key_tuple = tuple(key.split()) @@ -159,105 +147,82 @@ def convert_key_to_tuple(self, stats): @CommandHelp( 'Displays set summary information.') def do_set(self, line): - set_infos = self.loghdlr.info_summary(stanza=SUMMARY_SETS) - set_stats = self.loghdlr.info_statistics(stanza=STAT_SETS) + set_stats = self.loghdlr.info_statistics(stanza=STAT_SETS, flip=True) + for timestamp in sorted(set_stats.keys()): if not set_stats[timestamp]: - try: - if set_infos[timestamp]: - self.view.info_string(timestamp, set_infos[timestamp]) - except Exception: - pass continue + self.convert_key_to_tuple(set_stats[timestamp]) self.view.info_set(util.flip_keys(set_stats[timestamp]), - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), title_suffix=" (%s)" % (timestamp), **self.mods) @CommandHelp( 'Displays Cross Datacenter Replication (XDR) summary information.') def do_xdr(self, line): - xdr_infos = self.loghdlr.info_summary(stanza=SUMMARY_XDR) xdr_stats = self.loghdlr.info_statistics(stanza=STAT_XDR) for timestamp in sorted(xdr_stats.keys()): if not xdr_stats[timestamp]: - try: - if xdr_infos[timestamp]: - self.view.info_string(timestamp, xdr_infos[timestamp]) - except Exception: - pass continue xdr_enable = {} - cinfo_log = self.loghdlr.get_cinfo_log_at( - timestamp=timestamp) + cinfo_log = self.loghdlr.get_cinfo_log_at(timestamp=timestamp) builds = cinfo_log.get_xdr_build() for xdr_node in xdr_stats[timestamp].keys(): xdr_enable[xdr_node] = True self.view.info_XDR(xdr_stats[timestamp], builds, xdr_enable, - cluster=cinfo_log, title_suffix=" (%s)" % ( - timestamp), + cluster=cinfo_log, title_suffix=" (%s)" % (timestamp), **self.mods) @CommandHelp( 'Displays datacenter summary information.') def do_dc(self, line): - dc_infos = self.loghdlr.info_summary(stanza=SUMMARY_DC) - dc_stats = self.loghdlr.info_statistics(stanza=STAT_DC) - dc_config = self.loghdlr.info_getconfig(stanza=CONFIG_DC) + dc_stats = self.loghdlr.info_statistics(stanza=STAT_DC, flip=True) + dc_config = self.loghdlr.info_getconfig(stanza=CONFIG_DC, flip=True) for timestamp in sorted(dc_stats.keys()): if not dc_stats[timestamp]: - try: - if dc_infos[timestamp]: - self.view.info_string(timestamp, dc_infos[timestamp]) - except Exception: - pass continue + for dc in dc_stats[timestamp].keys(): - if (dc_stats[timestamp][dc] - and not isinstance(dc_stats[timestamp][dc], Exception) - and dc_config[timestamp] - and dc_config[timestamp][dc] - and not isinstance(dc_config[timestamp][dc], Exception)): + try: + if (dc_stats[timestamp][dc] + and not isinstance(dc_stats[timestamp][dc], Exception) + and dc_config[timestamp] + and dc_config[timestamp][dc] + and not isinstance(dc_config[timestamp][dc], Exception)): + + for node in dc_stats[timestamp][dc].keys(): + if node in dc_config[timestamp][dc]: + dc_stats[timestamp][dc][node].update(dc_config[timestamp][dc][node]) - for node in dc_stats[timestamp][dc].keys(): - if node in dc_config[timestamp][dc]: - dc_stats[timestamp][dc][node].update( - dc_config[timestamp][dc][node]) + elif ((not dc_stats[timestamp][dc] + or isinstance(dc_stats[timestamp][dc], Exception)) + and dc_config[timestamp] + and dc_config[timestamp][dc] + and not isinstance(dc_config[timestamp][dc], Exception)): - elif ((not dc_stats[timestamp][dc] - or isinstance(dc_stats[timestamp][dc], Exception)) - and dc_config[timestamp] - and dc_config[timestamp][dc] - and not isinstance(dc_config[timestamp][dc], Exception)): + dc_stats[timestamp][dc] = dc_config[timestamp][dc] - dc_stats[timestamp][dc] = dc_config[timestamp][dc] + except Exception: + pass self.view.info_dc(util.flip_keys(dc_stats[timestamp]), - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), title_suffix=" (%s)" % (timestamp), **self.mods) @CommandHelp( 'Displays secondary index (SIndex) summary information).') def do_sindex(self, line): - sindex_infos = self.loghdlr.info_summary(stanza=SUMMARY_SINDEX) - sindex_stats = self.loghdlr.info_statistics(stanza=STAT_SINDEX) + sindex_stats = self.loghdlr.info_statistics(stanza=STAT_SINDEX, flip=True) for timestamp in sorted(sindex_stats.keys()): if not sindex_stats[timestamp]: - try: - if sindex_infos[timestamp]: - self.view.info_string( - timestamp, sindex_infos[timestamp]) - except Exception: - pass continue + self.view.info_sindex(sindex_stats[timestamp], - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), title_suffix=" (%s)" % (timestamp), **self.mods) @@ -270,7 +235,9 @@ def __init__(self): self.controller_map = { 'config': ShowConfigController, 'statistics': ShowStatisticsController, - 'distribution': ShowDistributionController} + 'distribution': ShowDistributionController, + 'pmap': ShowPmapController + } self.modifiers = set() def _do_default(self, line): @@ -285,12 +252,14 @@ def __init__(self): self.modifiers = set(['like', 'diff']) @CommandHelp('Displays service, network, and namespace configuration', - ' Options:', ' -r - Repeating output table title and row header after every r columns.', - ' default: 0, no repetition.') + ' Options:', + ' -r - Repeating output table title and row header after every r columns.', + ' default: 0, no repetition.', + ' -flip - Flip output table to show Nodes on Y axis and config on X axis.') def _do_default(self, line): - self.do_service(line) - self.do_network(line) - self.do_namespace(line) + self.do_service(line[:]) + self.do_network(line[:]) + self.do_namespace(line[:]) @CommandHelp('Displays service configuration') def do_service(self, line): @@ -298,6 +267,10 @@ def do_service(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + service_configs = self.loghdlr.info_getconfig(stanza=CONFIG_SERVICE) for timestamp in sorted(service_configs.keys()): @@ -305,7 +278,7 @@ def do_service(self, line): service_configs[timestamp], self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays network configuration') def do_network(self, line): @@ -313,14 +286,18 @@ def do_network(self, line): arg="-r", return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - service_configs = self.loghdlr.info_getconfig(stanza=CONFIG_NETWORK) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) - for timestamp in sorted(service_configs.keys()): + network_configs = self.loghdlr.info_getconfig(stanza=CONFIG_NETWORK) + + for timestamp in sorted(network_configs.keys()): self.view.show_config("Network Configuration (%s)" % (timestamp), - service_configs[timestamp], + network_configs[timestamp], self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays namespace configuration') def do_namespace(self, line): @@ -329,7 +306,11 @@ def do_namespace(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - ns_configs = self.loghdlr.info_getconfig(stanza=CONFIG_NAMESPACE) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + ns_configs = self.loghdlr.info_getconfig(stanza=CONFIG_NAMESPACE, flip=True) for timestamp in sorted(ns_configs.keys()): for ns, configs in ns_configs[timestamp].iteritems(): @@ -337,7 +318,7 @@ def do_namespace(self, line): (ns, timestamp), configs, self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays XDR configuration') def do_xdr(self, line): @@ -346,6 +327,10 @@ def do_xdr(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + xdr_configs = self.loghdlr.info_getconfig(stanza=CONFIG_XDR) for timestamp in sorted(xdr_configs.keys()): @@ -353,7 +338,7 @@ def do_xdr(self, line): xdr_configs[timestamp], self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays datacenter configuration') def do_dc(self, line): @@ -362,7 +347,11 @@ def do_dc(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - dc_configs = self.loghdlr.info_getconfig(stanza=CONFIG_DC) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + dc_configs = self.loghdlr.info_getconfig(stanza=CONFIG_DC, flip=True) for timestamp in sorted(dc_configs.keys()): for dc, configs in dc_configs[timestamp].iteritems(): @@ -370,7 +359,7 @@ def do_dc(self, line): (dc, timestamp), configs, self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays cluster configuration') def do_cluster(self, line): @@ -379,6 +368,10 @@ def do_cluster(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + cl_configs = self.loghdlr.info_getconfig(stanza=CONFIG_CLUSTER) for timestamp in sorted(cl_configs.keys()): @@ -386,8 +379,7 @@ def do_cluster(self, line): cl_configs[timestamp], self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - title_every_nth=title_every_nth, **self.mods) - + title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp( '"distribution" is used to show the distribution of object sizes', @@ -405,7 +397,9 @@ def _do_default(self, line): def _do_distribution(self, histogram_name, title, unit): histogram = self.loghdlr.info_histogram(histogram_name) for timestamp in sorted(histogram.keys()): - self.view.show_distribution(title, histogram[timestamp], unit, + if not histogram[timestamp]: + continue + self.view.show_distribution(title, util.create_histogram_output(histogram_name, histogram[timestamp]), unit, histogram_name, self.loghdlr.get_cinfo_log_at( timestamp=timestamp), @@ -423,21 +417,24 @@ def do_object_size(self, line): byte_distribution = util.check_arg_and_delete_from_mods(line=line, arg="-b", default=False, modifiers=self.modifiers, mods=self.mods) + bucket_count = util.get_arg_and_delete_from_mods(line=line, + arg="-k", return_type=int, default=5, modifiers=self.modifiers, + mods=self.mods) - if byte_distribution: - histogram = self.loghdlr.info_histogram("objsz-b") - for timestamp in histogram: - self.view.show_object_distribution('Object Size Distribution', - histogram[ - timestamp], 'Bytes', 'objsz', 10, False, - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), - title_suffix=" (%s)" % ( - timestamp), - loganalyser_mode=True, like=self.mods['for']) - else: - return self._do_distribution('objsz', 'Object Size Distribution', - 'Record Blocks') + histogram_name = "objsz" + if not byte_distribution: + return self._do_distribution(histogram_name, 'Object Size Distribution', 'Record Blocks') + + histogram = self.loghdlr.info_histogram(histogram_name) + builds = self.loghdlr.info_meta_data(stanza="asd_build") + + for timestamp in histogram: + self.view.show_object_distribution('Object Size Distribution', + util.create_histogram_output(histogram_name, histogram[timestamp], byte_distribution=True, bucket_count=bucket_count, builds=builds), + 'Bytes', 'objsz', bucket_count, True, + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + title_suffix=" (%s)" % (timestamp), + loganalyser_mode=True, like=self.mods['for']) @CommandHelp('Shows the distribution of Eviction TTLs for namespaces') def do_eviction(self, line): @@ -454,12 +451,13 @@ def __init__(self): ' Options:', ' -t - Set to show total column at the end. It contains node wise sum for statistics.', ' -r - Repeating output table title and row header after every r columns.', - ' default: 0, no repetition.') + ' default: 0, no repetition.', + ' -flip - Flip output table to show Nodes on Y axis and stats on X axis.') def _do_default(self, line): - self.do_bins(line) - self.do_sets(line) - self.do_service(line) - self.do_namespace(line) + self.do_bins(line[:]) + self.do_sets(line[:]) + self.do_service(line[:]) + self.do_namespace(line[:]) @CommandHelp('Displays service statistics') def do_service(self, line): @@ -471,6 +469,10 @@ def do_service(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + service_stats = self.loghdlr.info_statistics(stanza=STAT_SERVICE) for timestamp in sorted(service_stats.keys()): @@ -478,7 +480,7 @@ def do_service(self, line): service_stats[timestamp], self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays namespace statistics') @@ -491,7 +493,11 @@ def do_namespace(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - ns_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + ns_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE, flip=True) for timestamp in sorted(ns_stats.keys()): namespace_list = util.filter_list( @@ -502,7 +508,7 @@ def do_namespace(self, line): (ns, timestamp), stats, self.loghdlr.get_cinfo_log_at( timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays set statistics') @@ -515,7 +521,11 @@ def do_sets(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - set_stats = self.loghdlr.info_statistics(stanza=STAT_SETS) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + set_stats = self.loghdlr.info_statistics(stanza=STAT_SETS, flip=True) for timestamp in sorted(set_stats.keys()): if not set_stats[timestamp]: @@ -528,9 +538,8 @@ def do_sets(self, line): continue self.view.show_stats("%s Set Statistics (%s)" % (ns_set, timestamp), stats, - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays bin statistics') @@ -543,23 +552,28 @@ def do_bins(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - new_bin_stats = self.loghdlr.info_statistics(stanza=STAT_BINS) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + new_bin_stats = self.loghdlr.info_statistics(stanza=STAT_BINS, flip=True) for timestamp in sorted(new_bin_stats.keys()): if (not new_bin_stats[timestamp] or isinstance(new_bin_stats[timestamp], Exception)): continue + namespace_list = util.filter_list(new_bin_stats[timestamp].keys(), self.mods['for']) for ns, stats in new_bin_stats[timestamp].iteritems(): if ns not in namespace_list: continue + self.view.show_stats("%s Bin Statistics (%s)" % (ns, timestamp), stats, - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays XDR statistics') @@ -572,13 +586,17 @@ def do_xdr(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + xdr_stats = self.loghdlr.info_statistics(stanza=STAT_XDR) for timestamp in sorted(xdr_stats.keys()): self.view.show_config( "XDR Statistics (%s)" % (timestamp), xdr_stats[timestamp], self.loghdlr.get_cinfo_log_at(timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays datacenter statistics') @@ -591,14 +609,18 @@ def do_dc(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - dc_stats = self.loghdlr.info_statistics(stanza=STAT_DC) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + dc_stats = self.loghdlr.info_statistics(stanza=STAT_DC, flip=True) for timestamp in sorted(dc_stats.keys()): for dc, stats in dc_stats[timestamp].iteritems(): self.view.show_stats( "%s DC Statistics (%s)" % (dc, timestamp), stats, self.loghdlr.get_cinfo_log_at(timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) @CommandHelp('Displays sindex statistics') @@ -611,7 +633,11 @@ def do_sindex(self, line): return_type=int, default=0, modifiers=self.modifiers, mods=self.mods) - sindex_stats = self.loghdlr.info_statistics(stanza=STAT_SINDEX) + flip_output = util.check_arg_and_delete_from_mods(line=line, + arg="-flip", default=False, modifiers=self.modifiers, + mods=self.mods) + + sindex_stats = self.loghdlr.info_statistics(stanza=STAT_SINDEX, flip=True) for timestamp in sorted(sindex_stats.keys()): if not sindex_stats[timestamp] or isinstance(sindex_stats[timestamp], Exception): @@ -624,12 +650,26 @@ def do_sindex(self, line): continue self.view.show_stats("%s Sindex Statistics (%s)" % (sindex, timestamp), stats, - self.loghdlr.get_cinfo_log_at( - timestamp=timestamp), - show_total=show_total, title_every_nth=title_every_nth, + self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + show_total=show_total, title_every_nth=title_every_nth, flip_output=flip_output, **self.mods) +@CommandHelp('Displays partition map analysis of Aerospike cluster.') +class ShowPmapController(CollectinfoCommandController): + def __init__(self): + self.modifiers = set() + + def _do_default(self, line): + pmap_data = self.loghdlr.info_pmap() + + for timestamp in sorted(pmap_data.keys()): + if not pmap_data[timestamp]: + continue + + self.view.show_pmap(pmap_data[timestamp], self.loghdlr.get_cinfo_log_at(timestamp=timestamp), + title_suffix=" (%s)" % (timestamp)) + @CommandHelp('Displays features used in Aerospike cluster.') class FeaturesController(CollectinfoCommandController): @@ -646,7 +686,7 @@ def _do_default(self, line): if timestamp in namespace_stats: ns_stats = namespace_stats[timestamp] - ns_stats = util.flip_keys(ns_stats) + # ns_stats = util.flip_keys(ns_stats) for feature, keys in util.FEATURE_KEYS.iteritems(): for node, s_stats in service_stats[timestamp].iteritems(): @@ -741,7 +781,7 @@ def _do_default(self, line): # cluster-name. cluster_name = "C1" stanza_dict = { - "statistics": (self.loghdlr.get_asstat_data, [ + "statistics": (self.loghdlr.info_statistics, [ ("service", "SERVICE", "STATISTICS", True, [("CLUSTER", cluster_name), ("NODE", None)]), ("namespace", "NAMESPACE", "STATISTICS", True, [ @@ -757,7 +797,7 @@ def _do_default(self, line): ("sindex", "SINDEX", "STATISTICS", True, [("CLUSTER", cluster_name), ("NODE", None), ( None, None), ("NAMESPACE", ("ns",)), ("SET", ("set",)), ("SINDEX", ("indexname",))]) ]), - "config": (self.loghdlr.get_asconfig_data, [ + "config": (self.loghdlr.info_getconfig, [ ("service", "SERVICE", "CONFIG", True, [("CLUSTER", cluster_name), ("NODE", None)]), ("xdr", "XDR", "CONFIG", True, [ @@ -769,11 +809,19 @@ def _do_default(self, line): ("namespace", "NAMESPACE", "CONFIG", True, [ ("CLUSTER", cluster_name), ("NODE", None), (None, None), ("NAMESPACE", None)]) ]), - "cluster": (self.loghdlr.get_asmeta_data, [ + "cluster": (self.loghdlr.info_meta_data, [ ("asd_build", "METADATA", "CLUSTER", True, [ ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "version")]), ]), - "udf": (self.loghdlr.get_asmeta_data, [ + "endpoints": (self.loghdlr.info_meta_data, [ + ("endpoints", "METADATA", "ENDPOINTS", True, [ + ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "endpoints")]), + ]), + "services": (self.loghdlr.info_meta_data, [ + ("services", "METADATA", "SERVICES", True, [ + ("CLUSTER", cluster_name), ("NODE", None), ("KEY", "services")]), + ]), + "udf": (self.loghdlr.info_meta_data, [ ("udf", "UDF", "METADATA", True, [ ("CLUSTER", cluster_name), ("NODE", None), (None, None), ("FILENAME", None)]), ]), @@ -808,6 +856,9 @@ def _do_default(self, line): if not d: continue + if stanza == "free-m": + d = util.mbytes_to_bytes(d) + sn_ct = 0 new_tuple_keys = [] @@ -854,8 +905,9 @@ def _do_default(self, line): @CommandHelp('Displays list of all added collectinfos files.') def do_all(self, line): - timestamp = self.loghdlr.get_cinfo_timestamp() - print terminal.bold() + str(timestamp) + terminal.unbold() + ": " + str(self.loghdlr.get_cinfo_path()) + cinfo_logs = self.loghdlr.all_cinfo_logs + for timestamp, snapshot in cinfo_logs.items(): + print terminal.bold() + str(timestamp) + terminal.unbold() + ": " + str(snapshot.cinfo_file) @CommandHelp("Set pager for output") @@ -889,12 +941,12 @@ def __init__(self): self.modifiers = set([]) def _do_default(self, line): - service_stats = self.loghdlr.get_asstat_data(stanza=STAT_SERVICE) - namespace_stats = self.loghdlr.get_asstat_data(stanza=STAT_NAMESPACE) - set_stats = self.loghdlr.get_asstat_data(stanza="set") + service_stats = self.loghdlr.info_statistics(stanza=STAT_SERVICE) + namespace_stats = self.loghdlr.info_statistics(stanza=STAT_NAMESPACE) + set_stats = self.loghdlr.info_statistics(stanza=STAT_SETS) os_version = self.loghdlr.get_sys_data(stanza="lsb") - server_version = self.loghdlr.get_asmeta_data(stanza="asd_build") + server_version = self.loghdlr.info_meta_data(stanza="asd_build") last_timestamp = sorted(service_stats.keys())[-1] diff --git a/lib/getcontroller.py b/lib/getcontroller.py index 7d103007..f02cb224 100644 --- a/lib/getcontroller.py +++ b/lib/getcontroller.py @@ -12,10 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from distutils.version import LooseVersion - from lib.utils import util -from lib.utils import filesize def get_sindex_stats(cluster, nodes='all', for_mods=[]): @@ -60,175 +57,21 @@ def __init__(self, cluster): def do_distribution(self, histogram_name, nodes='all'): histogram = self.cluster.info_histogram(histogram_name, nodes=nodes) - histogram = util.flip_keys(histogram) - - for namespace, host_data in histogram.iteritems(): - for host_id, data in host_data.iteritems(): - hist = data['data'] - width = data['width'] - - cum_total = 0 - total = sum(hist) - percentile = 0.1 - result = [] - - for i, v in enumerate(hist): - cum_total += float(v) - if total > 0: - portion = cum_total / total - else: - portion = 0.0 - - while portion >= percentile: - percentile += 0.1 - result.append(i + 1) - - if percentile > 1.0: - break - - if result == []: - result = [0] * 10 - - if histogram_name is "objsz": - data['percentiles'] = [ - (r * width) - 1 if r > 0 else r for r in result] - else: - data['percentiles'] = [r * width for r in result] - - return histogram + return util.create_histogram_output(histogram_name, histogram) - def do_object_size(self, byte_distribution=False, show_bucket_count=5, nodes='all'): + def do_object_size(self, byte_distribution=False, bucket_count=5, nodes='all'): histogram_name = 'objsz' if not byte_distribution: return self.do_distribution(histogram_name) - histogram = util.Future( - self.cluster.info_histogram, histogram_name, nodes=nodes).start() - builds = util.Future( - self.cluster.info, 'build', nodes=nodes).start() - histogram = util.flip_keys(histogram.result()) + histogram = util.Future(self.cluster.info_histogram, histogram_name, nodes=nodes).start() + builds = util.Future(self.cluster.info, 'build', nodes=nodes).start() + histogram = histogram.result() builds = builds.result() - for namespace, host_data in histogram.iteritems(): - result = [] - rblock_size_bytes = 128 - width = 1 - for host_id, data in host_data.iteritems(): - try: - as_version = builds[host_id] - if (LooseVersion(as_version) < LooseVersion("2.7.0") - or (LooseVersion(as_version) >= LooseVersion("3.0.0") - and LooseVersion(as_version) < LooseVersion("3.1.3"))): - rblock_size_bytes = 512 - except Exception: - pass - - hist = data['data'] - width = data['width'] - - for i, v in enumerate(hist): - if v and v > 0: - result.append(i) - - result = list(set(result)) - result.sort() - start_buckets = [] - if len(result) <= show_bucket_count: - # if asinfo buckets with values>0 are less than - # show_bucket_count then we can show all single buckets as it - # is, no need to merge to show big range - for res in result: - start_buckets.append(res) - start_buckets.append(res + 1) - else: - # dividing volume buckets (from min possible bucket with - # value>0 to max possible bucket with value>0) into same range - start_bucket = result[0] - size = result[len(result) - 1] - result[0] + 1 - - bucket_width = size / show_bucket_count - additional_bucket_index = show_bucket_count - \ - (size % show_bucket_count) - - bucket_index = 0 - - while bucket_index < show_bucket_count: - start_buckets.append(start_bucket) - if bucket_index == additional_bucket_index: - bucket_width += 1 - start_bucket += bucket_width - bucket_index += 1 - start_buckets.append(start_bucket) - - columns = [] - need_to_show = {} - for i, bucket in enumerate(start_buckets): - if i == len(start_buckets) - 1: - break - key = self.get_bucket_range( - bucket, start_buckets[i + 1], width, rblock_size_bytes) - need_to_show[key] = False - columns.append(key) - for host_id, data in host_data.iteritems(): - rblock_size_bytes = 128 - try: - as_version = builds[host_id] - - if (LooseVersion(as_version) < LooseVersion("2.7.0") - or (LooseVersion(as_version) >= LooseVersion("3.0.0") - and LooseVersion(as_version) < LooseVersion("3.1.3"))): - rblock_size_bytes = 512 - except Exception: - pass - hist = data['data'] - width = data['width'] - data['values'] = {} - for i, s in enumerate(start_buckets): - if i == len(start_buckets) - 1: - break - b_index = s - key = self.get_bucket_range( - s, start_buckets[i + 1], width, rblock_size_bytes) - if key not in columns: - columns.append(key) - if key not in data["values"]: - data["values"][key] = 0 - while b_index < start_buckets[i + 1]: - data["values"][key] += hist[b_index] - b_index += 1 - - if data["values"][key] > 0: - need_to_show[key] = True - else: - if key not in need_to_show: - need_to_show[key] = False - host_data["columns"] = [] - for column in columns: - if need_to_show[column]: - host_data["columns"].append(column) - - return histogram - - def get_bucket_range(self, current_bucket, next_bucket, width, rblock_size_bytes): - s_b = "0 B" - if current_bucket > 0: - last_bucket_last_rblock_end = ( - (current_bucket * width) - 1) * rblock_size_bytes - if last_bucket_last_rblock_end < 1: - last_bucket_last_rblock_end = 0 - else: - last_bucket_last_rblock_end += 1 - s_b = filesize.size(last_bucket_last_rblock_end, filesize.byte) - if current_bucket == 99 or next_bucket > 99: - return ">%s" % (s_b.replace(" ", "")) - - bucket_last_rblock_end = ( - (next_bucket * width) - 1) * rblock_size_bytes - e_b = filesize.size(bucket_last_rblock_end, filesize.byte) - return "%s to %s" % (s_b.replace(" ", ""), e_b.replace(" ", "")) - + return util.create_histogram_output(histogram_name, histogram, byte_distribution=True, bucket_count=bucket_count, builds=builds) class GetLatencyController(): @@ -265,7 +108,8 @@ def get_all(self, nodes='all'): 'namespace': (util.Future(self.get_namespace, nodes=nodes).start()).result(), 'network': (util.Future(self.get_network, nodes=nodes).start()).result(), 'xdr': (util.Future(self.get_xdr, nodes=nodes).start()).result(), - 'dc': (util.Future(self.get_dc, nodes=nodes).start()).result() + 'dc': (util.Future(self.get_dc, nodes=nodes).start()).result(), + 'cluster': (util.Future(self.get_cluster, nodes=nodes).start()).result() } return config_map @@ -526,3 +370,139 @@ def get_features(self, nodes='all'): return features +class GetPmapController(): + + def __init__(self, cluster): + self.cluster = cluster + + def _get_namespace_data(self, namespace_stats, cluster_keys): + ns_info = {} + + for ns, nodes in namespace_stats.items(): + repl_factor = {} + + for node, params in nodes.items(): + if isinstance(params, Exception): + continue + if cluster_keys[node] not in repl_factor: + repl_factor[cluster_keys[node]] = 0 + + repl_factor[cluster_keys[node]] = max(repl_factor[cluster_keys[node]], int(params['repl-factor'])) + + for ck in repl_factor: + if ck not in ns_info: + ns_info[ck] = {} + if ns not in ns_info[ck]: + ns_info[ck][ns] = {} + + ns_info[ck][ns]['repl_factor'] = repl_factor[ck] + + return ns_info + + def _get_pmap_data(self, pmap_info, ns_info, versions, cluster_keys): + pid_range = 4096 # each namespace is divided into 4096 partition + pmap_data = {} + ns_available_part = {} + + # required fields + # format : (index_ptr, field_name, default_index) + required_fields = [("namespace_index","namespace",0),("partition_index","partition",1),("state_index","state",2), + ("replica_index","replica",3),("origin_index","origin",4),("target_index","target",5)] + + for _node, partitions in pmap_info.items(): + node_pmap = dict() + ck = cluster_keys[_node] + + if isinstance(partitions, Exception): + continue + + f_indices = {} + + # default index in partition fields for server < 3.6.1 + for t in required_fields: + f_indices[t[0]] = t[2] + + index_set = False + + for item in partitions.split(';'): + fields = item.split(':') + + if not index_set: + index_set = True + + if all(i[1] in fields for i in required_fields): + # pmap format contains headers from server 3.9 onwards + for t in required_fields: + f_indices[t[0]] = fields.index(t[1]) + + continue + + ns, pid, state, replica, origin, target = fields[f_indices["namespace_index"]], int(fields[f_indices["partition_index"]]),\ + fields[f_indices["state_index"]], int(fields[f_indices["replica_index"]]),\ + fields[f_indices["origin_index"]], fields[f_indices["target_index"]] + + if pid not in range(pid_range): + print "For {0} found partition-ID {1} which is beyond legal partitions(0...4096)".format(ns, pid) + continue + + if ns not in node_pmap: + node_pmap[ns] = { 'master_partition_count' : 0, + 'prole_partition_count' : 0, + } + + if ck not in ns_available_part: + ns_available_part[ck] = {} + + if ns not in ns_available_part[ck]: + ns_available_part[ck][ns] = {} + ns_available_part[ck][ns]['available_partition_count'] = 0 + + if replica == 0: + if origin == '0': + node_pmap[ns]['master_partition_count'] += 1 + else: + node_pmap[ns]['prole_partition_count'] += 1 + else: + if target == '0': + if state == 'S' or state == 'D': + node_pmap[ns]['prole_partition_count'] += 1 + else: + node_pmap[ns]['master_partition_count'] += 1 + + if state == 'S' or state == 'D': + ns_available_part[ck][ns]['available_partition_count'] += 1 + + + + pmap_data[_node] = node_pmap + + for _node, _ns_data in pmap_data.items(): + ck = cluster_keys[_node] + for ns, params in _ns_data.items(): + params['missing_partition_count'] = (pid_range * ns_info[ck][ns]['repl_factor']) - ns_available_part[ck][ns]['available_partition_count'] + params['cluster_key'] = ck + + return pmap_data + + def get_pmap(self, nodes='all'): + getter = GetStatisticsController(self.cluster) + versions = util.Future(self.cluster.info, 'version', nodes=nodes).start() + pmap_info = util.Future(self.cluster.info, 'partition-info', nodes=nodes).start() + service_stats = getter.get_service(nodes=nodes) + namespace_stats = getter.get_namespace(nodes=nodes) + + versions = versions.result() + pmap_info = pmap_info.result() + + cluster_keys = {} + for node in service_stats.keys(): + if not service_stats[node] or isinstance(service_stats[node], Exception): + cluster_keys[node] = "N/E" + else: + cluster_keys[node] = util.get_value_from_dict(service_stats[node], ('cluster_key'), default_value="N/E") + + ns_info = self._get_namespace_data(namespace_stats, cluster_keys) + + pmap_data = self._get_pmap_data(pmap_info, ns_info, versions, cluster_keys) + + return pmap_data \ No newline at end of file diff --git a/lib/health/operation.py b/lib/health/operation.py index 327fd08b..96280a82 100644 --- a/lib/health/operation.py +++ b/lib/health/operation.py @@ -116,9 +116,14 @@ def vector_to_scalar_equal_operation(op, v): i0 = v[0] k1, v1 = get_kv(i0) + if v1 and isinstance(v1, list): + v1 = sorted(v1) for i in v[1:]: k2, v2 = get_kv(i) + if v2 and isinstance(v2, list): + v2 = sorted(v2) + if not op(v1, v2): return False @@ -194,25 +199,32 @@ def vector_to_vector_sd_anomaly_operation(kv, op, a): try: n = len(kv) if n < 3: - no_analogy = True + no_anomaly = True range_start = 0 range_end = 0 else: values = [get_kv(m)[1] for m in kv] - no_analogy = False - s = sum(values) - mean = float(s) / float(n) - variance = 0 - for v in values: - variance += pow((v - mean), 2) - variance = float(variance) / float(n) - sd = sqrt(variance) - range_start = mean - (a * sd) - range_end = mean + (a * sd) + no_anomaly = False + + try: + # We should consider int and floats only + s = sum(values) + except Exception: + no_anomaly = True + + if not no_anomaly: + mean = float(s) / float(n) + variance = 0 + for v in values: + variance += pow((v - mean), 2) + variance = float(variance) / float(n) + sd = sqrt(variance) + range_start = mean - (a * sd) + range_end = mean + (a * sd) for x in kv: k, v = get_kv(x) - if (no_analogy or (float(v) >= float(range_start) + if (no_anomaly or (float(v) >= float(range_start) and float(v) <= float(range_end))): res[make_key(k)] = False else: diff --git a/lib/health/parser.py b/lib/health/parser.py index e0af3a76..808a1861 100644 --- a/lib/health/parser.py +++ b/lib/health/parser.py @@ -49,6 +49,7 @@ class HealthLexer(object): 'DEVICE_INTERRUPTS': 'DEVICE_INTERRUPTS', 'DEVICE_STAT': 'DEVICE_STAT', 'DF': 'DF', + 'ENDPOINTS': 'ENDPOINTS', 'FREE': 'FREE', 'INTERRUPTS': 'INTERRUPTS', 'IOSTAT': 'IOSTAT', @@ -61,6 +62,7 @@ class HealthLexer(object): 'SYSTEM': 'SYSTEM', 'SECURITY': 'SECURITY', 'SERVICE': 'SERVICE', + 'SERVICES': 'SERVICES', 'STATISTICS': 'STATISTICS', 'SWAP': 'SWAP', 'TASKS': 'TASKS', diff --git a/lib/health/query.py b/lib/health/query.py index ad5a07e9..4ec1212f 100644 --- a/lib/health/query.py +++ b/lib/health/query.py @@ -139,7 +139,7 @@ */ s = select "memory-size" from NAMESPACE; n = group by NODE do SUM(s); -s = select "total" from SYSTEM.RAM; +s = select "total" from SYSTEM.FREE.MEM; m = group by NODE do SUM(s); r = do n <= m on common; ASSERT(r, True, "Namespace memory misconfiguration.", "LIMITS", WARNING, @@ -150,7 +150,7 @@ r = do r >= 5368709120; ASSERT(r, True, "Aerospike runtime memory configured < 5G.", "LIMITS", INFO, "Listed node[s] have less than 5G free memory available for Aerospike runtime. Please run 'show statistics namespace like memory-size' to check configured memory and check output of 'free' for system memory. Possible misconfiguration.", - "Namespace memory configuration check."); + "Runtime memory configuration check."); /* @@ -602,6 +602,19 @@ "Listed nodes[s] have non-zero LDT statistics. This feature is deprecated. Please visit Aerospike Homepage for details.", "LDT statistics check."); + +/* ENDPOINTS */ + +service = select "endpoints" as "e" from METADATA.ENDPOINTS; +services = select "services" as "e" from METADATA.SERVICES; +all_endpoints = do service + services; + +r = group by CLUSTER do EQUAL(all_endpoints); +ASSERT(r, True, "Services list discrepancy.", "OPERATIONS", WARNING, + "Listed Cluster[s] shows different services list for different nodes. Please run 'asinfo -v services' to get all services.", + "Services list discrepancy test."); + + /* Different queries for different versions. All version constraint sections should be at the bottom of file, it will avoid extra version reset at the end. */ @@ -742,6 +755,15 @@ "Node read errors count check"); +SET CONSTRAINT VERSION >= 3.3.17; + +defslp= select "defrag-sleep", "storage-engine.defrag-sleep" from NAMESPACE.CONFIG; +defslp = group by CLUSTER, NAMESPACE defslp; +r = do defslp == 1000; +ASSERT(r, True, "Non-default namespace defrag-sleep configuration.", "OPERATIONS",INFO, + "Listed namespace[s] have non-default defrag-sleep configuration. Please run 'show config namespace like defrag' to check value. It may be a non-issue in case namespaces are configured for aggressive defrag. Ignore those.", + "Non-default namespace defrag-sleep check."); + SET CONSTRAINT VERSION ALL; ''' diff --git a/lib/health/query/health.hql b/lib/health/query/health.hql index 23c2cda3..fb5ef234 100644 --- a/lib/health/query/health.hql +++ b/lib/health/query/health.hql @@ -124,7 +124,7 @@ group by for system stats helps to remove key, this is requirement for proper ma */ s = select "memory-size" from NAMESPACE; n = group by NODE do SUM(s); -s = select "total" from SYSTEM.RAM; +s = select "total" from SYSTEM.FREE.MEM; m = group by NODE do SUM(s); r = do n <= m on common; ASSERT(r, True, "Namespace memory misconfiguration.", "LIMITS", WARNING, @@ -135,7 +135,7 @@ r = do m - n on common; r = do r >= 5368709120; ASSERT(r, True, "Aerospike runtime memory configured < 5G.", "LIMITS", INFO, "Listed node[s] have less than 5G free memory available for Aerospike runtime. Please run 'show statistics namespace like memory-size' to check configured memory and check output of 'free' for system memory. Possible misconfiguration.", - "Namespace memory configuration check."); + "Runtime memory configuration check."); /* @@ -587,6 +587,19 @@ ASSERT(r, False, "Deprecated feature LDT in use.", "OPERATIONS", WARNING, "Listed nodes[s] have non-zero LDT statistics. This feature is deprecated. Please visit Aerospike Homepage for details.", "LDT statistics check."); + +/* ENDPOINTS */ + +service = select "endpoints" as "e" from METADATA.ENDPOINTS; +services = select "services" as "e" from METADATA.SERVICES; +all_endpoints = do service + services; + +r = group by CLUSTER do EQUAL(all_endpoints); +ASSERT(r, True, "Services list discrepancy.", "OPERATIONS", WARNING, + "Listed Cluster[s] shows different services list for different nodes. Please run 'asinfo -v services' to get all services.", + "Services list discrepancy test."); + + /* Different queries for different versions. All version constraint sections should be at the bottom of file, it will avoid extra version reset at the end. */ @@ -727,4 +740,13 @@ ASSERT(r, True, "Non-zero node read errors count", "OPERATIONS", INFO, "Node read errors count check"); +SET CONSTRAINT VERSION >= 3.3.17; + +defslp= select "defrag-sleep", "storage-engine.defrag-sleep" from NAMESPACE.CONFIG; +defslp = group by CLUSTER, NAMESPACE defslp; +r = do defslp == 1000; +ASSERT(r, True, "Non-default namespace defrag-sleep configuration.", "OPERATIONS",INFO, + "Listed namespace[s] have non-default defrag-sleep configuration. Please run 'show config namespace like defrag' to check value. It may be a non-issue in case namespaces are configured for aggressive defrag. Ignore those.", + "Non-default namespace defrag-sleep check."); + SET CONSTRAINT VERSION ALL; diff --git a/lib/log/latency.py b/lib/log/latency.py index fc56d317..59197675 100644 --- a/lib/log/latency.py +++ b/lib/log/latency.py @@ -33,8 +33,8 @@ DT_TO_MINUTE_FMT = "%b %d %Y %H:%M" DT_TIME_FMT = "%H:%M:%S" HIST_TAG_PREFIX = "histogram dump: " -HIST_TAG_PATTERNS = [ - HIST_TAG_PREFIX + "%s ", HIST_TAG_PREFIX + "{[a-zA-Z0-9_-]+}-%s "] +HIST_WITH_NS_PATTERN = "{.+}-[a-zA-Z0-9_-]+" +HIST_TAG_PATTERNS = [HIST_TAG_PREFIX + "%s ", HIST_TAG_PREFIX + "{[a-zA-Z0-9_-]+}-%s "] NS_HIST_TAG_PATTERNS = [HIST_TAG_PREFIX + "{%s}-%s "] NS_SLICE_SECONDS = 5 SCAN_SIZE = 1024 * 1024 @@ -187,7 +187,7 @@ def ceil_time(self, dt): # def _read_hist(self, hist_tags, after_dt, file_itr, line=0, end_dt=None, - before_dt=None): + before_dt=None, read_all_dumps=False): if not line: line = self._read_line(file_itr) while True: @@ -208,13 +208,16 @@ def _read_hist(self, hist_tags, after_dt, file_itr, line=0, end_dt=None, total, values, line = self._read_bucket_values(line, file_itr) if not line: return 0, 0, 0, 0 - if not before_dt: - before_dt = dt + datetime.timedelta(seconds=NS_SLICE_SECONDS) - r_total, r_values, r_dt, line = self._read_hist( - hist_tags, after_dt, file_itr, line, end_dt, before_dt) - total += r_total - if r_values: - values = self._add_buckets(values, r_values) + + if read_all_dumps: + if not before_dt: + before_dt = dt + datetime.timedelta(seconds=NS_SLICE_SECONDS) + r_total, r_values, r_dt, line = self._read_hist( + hist_tags, after_dt, file_itr, line, end_dt, before_dt, read_all_dumps=read_all_dumps) + total += r_total + if r_values: + values = self._add_buckets(values, r_values) + return total, values, dt, line #------------------------------------------------ @@ -261,17 +264,31 @@ def compute_latency(self, arg_log_itr, arg_hist, arg_slice, arg_from, arg_num_buckets = arg_num_buckets - 1 file_itr = arg_log_itr + # By default reading one bucket dump for 10 second slice, + # In case of multiple namespaces, it will read all bucket dumps for all namepspaces for same slice + read_all_dumps = False + # Set histogram tag: if arg_ns: - hist_tags = [s % (arg_ns, arg_hist) - for s in NS_HIST_TAG_PATTERNS] + # Analysing latency for histogram arg_hist for specific namespace arg_ns + # It needs to read single bucket dump for a slice + hist_tags = [s % (arg_ns, arg_hist) for s in NS_HIST_TAG_PATTERNS] + + elif re.match(HIST_WITH_NS_PATTERN, arg_hist): + # Analysing latency for specific histogram for specific namespace ({namespace}-histogram) + # It needs to read single bucket dump for a slice + hist_tags = [HIST_TAG_PREFIX+"%s "%(arg_hist)] + else: + # Analysing latency for histogram arg_hist + # It needs to read all bucket dumps for a slice hist_tags = [s % (arg_hist) for s in HIST_TAG_PATTERNS] + read_all_dumps = True init_dt = arg_from # Find first histogram: old_total, old_values, old_dt, line = self._read_hist( - hist_tags, init_dt, file_itr, end_dt=arg_end_date) + hist_tags, init_dt, file_itr, end_dt=arg_end_date, read_all_dumps=read_all_dumps) if line: end_dt = arg_end_date @@ -293,7 +310,7 @@ def compute_latency(self, arg_log_itr, arg_hist, arg_slice, arg_from, # Process all the time slices: while end_dt > old_dt: new_total, new_values, new_dt, line = self._read_hist( - hist_tags, after_dt, file_itr, line, end_dt=arg_end_date) + hist_tags, after_dt, file_itr, line, end_dt=arg_end_date, read_all_dumps=read_all_dumps) if not new_values: # This can happen in either eof or end of input time # range diff --git a/lib/utils/constants.py b/lib/utils/constants.py index af09987f..ec596ac7 100644 --- a/lib/utils/constants.py +++ b/lib/utils/constants.py @@ -23,11 +23,11 @@ CONFIG_CLUSTER = "cluster" STAT_SERVICE = "service" -STAT_SETS = "sets" +STAT_SETS = "set" STAT_NAMESPACE = "namespace" STAT_XDR = "xdr" STAT_DC = "dc" -STAT_BINS = "bins" +STAT_BINS = "bin" STAT_SINDEX = "sindex" SUMMARY_SERVICE = "service" diff --git a/lib/utils/util.py b/lib/utils/util.py index b6af8611..cd865870 100644 --- a/lib/utils/util.py +++ b/lib/utils/util.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import copy +from distutils.version import LooseVersion import re import threading @@ -22,6 +23,7 @@ # Dictionary to contain feature and related stats to identify state of that feature # Format : { feature1: ((service_stat1, service_stat2, ....), (namespace_stat1, namespace_stat2, ...), ...} +from lib.utils import filesize FEATURE_KEYS = { "KVS": (('stat_read_reqs', 'stat_write_reqs'), ('client_read_error', 'client_read_success', 'client_write_error', 'client_write_success')), @@ -702,8 +704,10 @@ def create_summary(service_stats, namespace_stats, set_stats, metadata): if len(set(cl_nodewise_device_counts.values())) > 1: summary_dict["CLUSTER"]["device"]["count_same_across_nodes"] = False - summary_dict["CLUSTER"]["memory"]["total"] = sum(cl_nodewise_mem_size.values()) - summary_dict["CLUSTER"]["memory"]["aval_pct"] = (float(sum(cl_nodewise_mem_aval.values()))/float(sum(cl_nodewise_mem_size.values())))*100.0 + cl_memory_size_total = sum(cl_nodewise_mem_size.values()) + if cl_memory_size_total > 0: + summary_dict["CLUSTER"]["memory"]["total"] = cl_memory_size_total + summary_dict["CLUSTER"]["memory"]["aval_pct"] = (float(sum(cl_nodewise_mem_aval.values()))/float(cl_memory_size_total))*100.0 cl_device_size_total = sum(cl_nodewise_device_size.values()) if cl_device_size_total > 0: @@ -712,3 +716,211 @@ def create_summary(service_stats, namespace_stats, set_stats, metadata): summary_dict["CLUSTER"]["device"]["aval_pct"] = (float(sum(cl_nodewise_device_aval.values()))/float(cl_device_size_total))*100.0 return summary_dict + +def mbytes_to_bytes(data): + if not data: + return data + + if isinstance(data, int) or isinstance(data, float): + return data * 1048576 + + if isinstance(data, dict): + for _k in data.keys(): + data[_k] = copy.deepcopy(mbytes_to_bytes(data[_k])) + return data + + return data + + +def _create_histogram_percentiles_output(histogram_name, histogram_data): + histogram_data = flip_keys(histogram_data) + + for namespace, host_data in histogram_data.iteritems(): + for host_id, data in host_data.iteritems(): + hist = data['data'] + width = data['width'] + + cum_total = 0 + total = sum(hist) + percentile = 0.1 + result = [] + + for i, v in enumerate(hist): + cum_total += float(v) + if total > 0: + portion = cum_total / total + else: + portion = 0.0 + + while portion >= percentile: + percentile += 0.1 + result.append(i + 1) + + if percentile > 1.0: + break + + if result == []: + result = [0] * 10 + + if histogram_name is "objsz": + data['percentiles'] = [(r * width) - 1 if r > 0 else r for r in result] + else: + data['percentiles'] = [r * width for r in result] + + return histogram_data + +def _create_bytewise_histogram_percentiles_output(histogram_data, bucket_count, builds): + histogram_data = flip_keys(histogram_data) + + for namespace, host_data in histogram_data.iteritems(): + result = [] + rblock_size_bytes = 128 + width = 1 + + for host_id, data in host_data.iteritems(): + + try: + as_version = builds[host_id] + if (LooseVersion(as_version) < LooseVersion("2.7.0") + or (LooseVersion(as_version) >= LooseVersion("3.0.0") + and LooseVersion(as_version) < LooseVersion("3.1.3"))): + rblock_size_bytes = 512 + + except Exception: + pass + + hist = data['data'] + width = data['width'] + + for i, v in enumerate(hist): + if v and v > 0: + result.append(i) + + result = list(set(result)) + result.sort() + start_buckets = [] + + if len(result) <= bucket_count: + # if asinfo buckets with values>0 are less than + # show_bucket_count then we can show all single buckets as it + # is, no need to merge to show big range + for res in result: + start_buckets.append(res) + start_buckets.append(res + 1) + + else: + # dividing volume buckets (from min possible bucket with + # value>0 to max possible bucket with value>0) into same range + start_bucket = result[0] + size = result[len(result) - 1] - result[0] + 1 + + bucket_width = size / bucket_count + additional_bucket_index = bucket_count - (size % bucket_count) + + bucket_index = 0 + + while bucket_index < bucket_count: + start_buckets.append(start_bucket) + + if bucket_index == additional_bucket_index: + bucket_width += 1 + + start_bucket += bucket_width + bucket_index += 1 + + start_buckets.append(start_bucket) + + columns = [] + need_to_show = {} + + for i, bucket in enumerate(start_buckets): + + if i == len(start_buckets) - 1: + break + + key = _get_bucket_range(bucket, start_buckets[i + 1], width, rblock_size_bytes) + need_to_show[key] = False + columns.append(key) + + for host_id, data in host_data.iteritems(): + + rblock_size_bytes = 128 + + try: + as_version = builds[host_id] + + if (LooseVersion(as_version) < LooseVersion("2.7.0") + or (LooseVersion(as_version) >= LooseVersion("3.0.0") + and LooseVersion(as_version) < LooseVersion("3.1.3"))): + rblock_size_bytes = 512 + + except Exception: + pass + + hist = data['data'] + width = data['width'] + data['values'] = {} + + for i, s in enumerate(start_buckets): + + if i == len(start_buckets) - 1: + break + + b_index = s + + key = _get_bucket_range(s, start_buckets[i + 1], width, rblock_size_bytes) + + if key not in columns: + columns.append(key) + + if key not in data["values"]: + data["values"][key] = 0 + + while b_index < start_buckets[i + 1]: + data["values"][key] += hist[b_index] + b_index += 1 + + if data["values"][key] > 0: + need_to_show[key] = True + + else: + if key not in need_to_show: + need_to_show[key] = False + + host_data["columns"] = [] + + for column in columns: + if need_to_show[column]: + host_data["columns"].append(column) + + return histogram_data + +def _get_bucket_range(current_bucket, next_bucket, width, rblock_size_bytes): + s_b = "0 B" + if current_bucket > 0: + last_bucket_last_rblock_end = ((current_bucket * width) - 1) * rblock_size_bytes + + if last_bucket_last_rblock_end < 1: + last_bucket_last_rblock_end = 0 + + else: + last_bucket_last_rblock_end += 1 + + s_b = filesize.size(last_bucket_last_rblock_end, filesize.byte) + + if current_bucket == 99 or next_bucket > 99: + return ">%s" % (s_b.replace(" ", "")) + + bucket_last_rblock_end = ((next_bucket * width) - 1) * rblock_size_bytes + e_b = filesize.size(bucket_last_rblock_end, filesize.byte) + return "%s to %s" % (s_b.replace(" ", ""), e_b.replace(" ", "")) + +def create_histogram_output(histogram_name, histogram_data, **params): + if "byte_distribution" not in params or not params["byte_distribution"]: + return _create_histogram_percentiles_output(histogram_name, histogram_data) + + if "bucket_count" not in params or "builds" not in params: + return {} + + return _create_bytewise_histogram_percentiles_output(histogram_data, params["bucket_count"], params["builds"]) + diff --git a/lib/view/table.py b/lib/view/table.py index 904c0029..7dd8c942 100644 --- a/lib/view/table.py +++ b/lib/view/table.py @@ -106,7 +106,7 @@ class Table(object): def __init__(self, title, column_names, sort_by=0, group_by=None, style=Styles.HORIZONTAL, title_format=TitleFormats.var_to_title, - description=''): + description='', n_last_columns_ignore_sort=0): self._data = [] self._need_sort = False @@ -120,6 +120,7 @@ def __init__(self, title, column_names, sort_by=0, group_by=None, self._group_by = group_by self._style = style self._description = description + self._n_last_columns_ignore_sort = n_last_columns_ignore_sort self._column_names = [] self._column_display_names = [] @@ -179,6 +180,9 @@ def _update_column_metadata(self, row, header=False): def add_data_source(self, column, function): self._data_source[column] = function + def ignore_sort(self, ignore=True): + self._need_sort = not ignore + def add_data_source_tuple(self, column, *functions, **kwargs): def tuple_extractor(data): args = [] @@ -265,24 +269,41 @@ def _do_group(self, data): return data def _do_sort(self): - if self._need_sort == True: + if self._need_sort == True and self._n_last_columns_ignore_sort < len(self._data): + if self._n_last_columns_ignore_sort > 0: + data_len = len(self._data) + data_to_process = self._data[0:data_len-self._n_last_columns_ignore_sort] + fixed_data = self._data[data_len-self._n_last_columns_ignore_sort:data_len] + else: + data_to_process = self._data + fixed_data = [] + if self._style == Styles.HORIZONTAL: sorted_data = sorted( - self._data, key=lambda d: d[self._sort_by][1]) + data_to_process, key=lambda d: d[self._sort_by][1]) self._data = self._do_group(sorted_data) self._need_sort = False else: # style is Vertical # Need to sort but messes with column widths transform = sorted( - range(len(self._data)), key=lambda d: self._data[d][self._sort_by][1]) + range(len(data_to_process)), key=lambda d: data_to_process[d][self._sort_by][1]) - self._data = map(lambda i: self._data[i], transform) + self._data = map(lambda i: data_to_process[i], transform) first = self._column_widths[0] + + if self._n_last_columns_ignore_sort > 0: + fixed_column_widths = self._column_widths[len(self._column_widths)-self._n_last_columns_ignore_sort:len(self._column_widths)] + else: + fixed_column_widths = [] + self._column_widths = map(lambda i: self._column_widths[i + 1], transform) self._column_widths.insert(0, first) + self._column_widths = self._column_widths + fixed_column_widths self._need_sort = False + self._data = self._data + fixed_data + def _gen_render_data(self, horizontal=True): self._render_column_display_names = [] self._render_column_names = [] @@ -328,10 +349,15 @@ def gen_description(self, line_width, desc_width): return [description, ] - def _get_horizontal_header(self): + def _get_horizontal_header(self, title_every_nth=0): width = sum(self._render_column_widths) + total_repeat_titles = 0 + if title_every_nth: + total_columns = len(self._render_column_display_names) - 1 # Ignoring first columns of Row Header + total_repeat_titles = (total_columns-1)/title_every_nth + width += total_repeat_titles * self._render_column_widths[0] # Width is same as first column width += len(self._column_padding) * \ - (len(self._render_column_widths) - 1) + (len(self._render_column_widths) + total_repeat_titles - 1) column_name_lines = map( lambda h: h.split(" "), self._render_column_display_names) max_deep = max(map(len, column_name_lines)) @@ -345,6 +371,13 @@ def _get_horizontal_header(self): for r in range(max_deep): row = [] for i, c in enumerate(column_name_lines): + if title_every_nth and (i-1) >0 and (i-1)%title_every_nth == 0: + try: + row.append(column_name_lines[0][r].rjust(self._render_column_widths[0])) + except IndexError: + row.append(".".rjust(self._render_column_widths[0])) + row.append(self._column_padding) + try: row.append(c[r].rjust(self._render_column_widths[i])) except IndexError: @@ -363,18 +396,31 @@ def __str__(self, horizontal_title_every_nth=0): self._gen_render_data() if self._style == Styles.HORIZONTAL: - return self._str_horizontal() + return self._str_horizontal(title_every_nth=horizontal_title_every_nth) elif self._style == Styles.VERTICAL: return self._str_vertical(title_every_nth=horizontal_title_every_nth) else: raise ValueError("Invalid style, must be either " + "table.HORIZONTAL or table.VERTICAL") - def _str_horizontal(self): + def _format_cell(self, cell, index): + if self._render_column_types[index] == "number": + cell = cell.rjust(self._render_column_widths[index]) + elif self._render_column_types[index] == "string": + cell = cell.ljust(self._render_column_widths[index]) + else: + raise ValueError( + "Unknown column type: '%s'" % self._render_column_types[index]) + return cell + + def _str_horizontal(self, title_every_nth=0): output = [] - output.append(self._get_horizontal_header()) + output.append(self._get_horizontal_header(title_every_nth=title_every_nth)) for drow in self._data: row = [] + title_cell_format = drow[0][0] + title_cell = self._format_cell(drow[0][1], 0) + for i, (cell_format, cell) in enumerate(drow): row.append(terminal.style( terminal.bg_clear, terminal.fg_clear)) @@ -382,15 +428,11 @@ def _str_horizontal(self): continue i = self._render_remap[i] + if title_every_nth and i-1 > 0 and (i-1)%title_every_nth == 0: + row.append("%s%s" % (title_cell_format(), title_cell)) + row.append(self._column_padding) - if self._render_column_types[i] == "number": - cell = cell.rjust(self._render_column_widths[i]) - elif self._render_column_types[i] == "string": - cell = cell.ljust(self._render_column_widths[i]) - else: - raise ValueError( - "Unknown column type: '%s'" % self._render_column_types[i]) - + cell = self._format_cell(cell, i) row.append("%s%s" % (cell_format(), cell)) row.append(self._column_padding) output.append(''.join(row)) @@ -440,6 +482,7 @@ def _str_vertical(self, title_every_nth=0): self.gen_description(sum(title_width), sum(title_width) - 10)) for i, column_name in enumerate(self._render_column_names): + row = [] row.append(terminal.style( terminal.bg_clear, terminal.fg_clear)) @@ -451,8 +494,10 @@ def _str_vertical(self, title_every_nth=0): row.append(":") row.append(self._column_padding) added_columns = 0 + for j, (cell_format, cell) in enumerate((raw_data[i] for raw_data in self._data), 1): + if (title_every_nth and added_columns > 0 and added_columns % title_every_nth == 0): row.append( diff --git a/lib/view/view.py b/lib/view/view.py index f2073487..b8537e2c 100644 --- a/lib/view/view.py +++ b/lib/view/view.py @@ -152,38 +152,32 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): principal = cluster.get_expected_principal() title = "Namespace Information%s" % (title_suffix) - column_names = ('namespace', 'node', ('available_pct', 'Avail%'), ('_evicted_objects', 'Evictions'), ('_master_objects', 'Master (Objects,Tombstones)'), ('_prole_objects', 'Replica (Objects,Tombstones)'), 'repl-factor', 'stop_writes', ('_migrates', 'Pending Migrates (tx,rx)'), - ('_used_bytes_disk', 'Disk Used'), ('_used_disk_pct', 'Disk Used%'), ('high-water-disk-pct', 'HWM Disk%'), ('_used_bytes_memory', 'Mem Used'), ('_used_mem_pct', 'Mem Used%'), ('high-water-memory-pct', 'HWM Mem%'), ('stop-writes-pct', 'Stop Writes%')) + column_names = ('namespace', 'node', ('available_pct', 'Avail%'), ('_evicted_objects', 'Evictions'), ('_total_objects', 'Total Objects'), ('_expired_objects', 'Expirations'), 'repl-factor', 'stop_writes', ('_migrates', 'Pending Migrates (tx,rx)'), + ('_used_bytes_disk', 'Disk Used'), ('_used_disk_pct', 'Disk Used%'), ('high-water-disk-pct', 'HWM Disk%'), ('_used_bytes_memory', 'Mem Used'), ('_used_mem_pct', 'Mem Used%'), ('high-water-memory-pct', 'HWM Mem%'), ('stop-writes-pct', 'Stop Writes%'), + ('rack-id', 'Rack ID') + ) t = Table(title, column_names, sort_by=0) + t.add_data_source('_total_objects', Extractors.sif_extractor( + ('total_objects'))) t.add_data_source('_evicted_objects', Extractors.sif_extractor( ('evicted-objects', 'evicted_objects'))) + t.add_data_source('_expired_objects', Extractors.sif_extractor( + ('expired-objects', 'expired_objects'))) t.add_data_source('_used_bytes_disk', Extractors.byte_extractor( ('used-bytes-disk', 'device_used_bytes'))) t.add_data_source('_used_bytes_memory', Extractors.byte_extractor( ('used-bytes-memory', 'memory_used_bytes'))) - t.add_data_source_tuple( - '_master_objects', - Extractors.sif_extractor(('master-objects', 'master_objects')), - Extractors.sif_extractor(('master_tombstones'))) - - t.add_data_source_tuple( - '_prole_objects', - Extractors.sif_extractor(('prole-objects', 'prole_objects')), - Extractors.sif_extractor(('prole_tombstones'))) - t.add_data_source('_used_disk_pct', lambda data: 100 - int(data['free_pct_disk']) if data['free_pct_disk'] is not " " else " ") t.add_data_source('_used_mem_pct', lambda data: 100 - int( data['free_pct_memory']) if data['free_pct_memory'] is not " " else " ") - t.add_cell_alert('available_pct', lambda data: int( - data['available_pct']) <= 10 if data['available_pct'] is not " " else " ") + t.add_cell_alert('available_pct', lambda data: data['available_pct'] != " " and int(data['available_pct']) <= 10) - t.add_cell_alert( - 'stop_writes', lambda data: data['stop_writes'] != 'false') + t.add_cell_alert('stop_writes', lambda data: data['stop_writes'] != " " and data['stop_writes'] != 'false') t.add_data_source_tuple( '_migrates', @@ -192,11 +186,9 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): Extractors.sif_extractor(('migrate_rx_partitions_remaining', 'migrate-rx-partitions-remaining'))) - t.add_cell_alert('_used_mem_pct', lambda data: (100 - int(data['free_pct_memory'])) >= int( - data['high-water-memory-pct']) if data['free_pct_memory'] is not " " else " ") + t.add_cell_alert('_used_mem_pct', lambda data: data['free_pct_memory'] != " " and (100 - int(data['free_pct_memory'])) >= int(data['high-water-memory-pct'])) - t.add_cell_alert('_used_disk_pct', lambda data: (100 - int(data['free_pct_disk'])) >= int( - data['high-water-disk-pct']) if data['free_pct_disk'] is not " " else " ") + t.add_cell_alert('_used_disk_pct', lambda data: data['free_pct_disk'] != " " and (100 - int(data['free_pct_disk'])) >= int(data['high-water-disk-pct'])) t.add_cell_alert( 'node', lambda data: data['real_node_id'] == principal, color=terminal.fg_green) @@ -204,19 +196,15 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): t.add_cell_alert( 'namespace', lambda data: data['node'] is " ", color=terminal.fg_blue) t.add_cell_alert( - '_master_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) - t.add_cell_alert( - '_master_tombstones', lambda data: data['node'] is " ", color=terminal.fg_blue) - t.add_cell_alert( - '_prole_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) - t.add_cell_alert( - '_prole_tombstones', lambda data: data['node'] is " ", color=terminal.fg_blue) + '_total_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) t.add_cell_alert( '_used_bytes_memory', lambda data: data['node'] is " ", color=terminal.fg_blue) t.add_cell_alert( '_used_bytes_disk', lambda data: data['node'] is " ", color=terminal.fg_blue) t.add_cell_alert( '_evicted_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_expired_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) t.add_cell_alert( '_migrates', lambda data: data['node'] is " ", color=terminal.fg_blue) @@ -230,6 +218,8 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): sorted_node_list = [x for (y, x) in sorted( zip(node_column_list, node_key_list), key=lambda pair: pair[0])] + rack_id_available = False + for node_key in sorted_node_list: n_stats = stats[node_key] node = cluster.get_node(node_key)[0] @@ -245,35 +235,51 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): else: row = ns_stats + if "rack-id" in row: + rack_id_available = True + + total_objects = 0 + if ns not in total_res: total_res[ns] = {} - total_res[ns]["master_objects"] = 0 - total_res[ns]["master_tombstones"] = 0 - total_res[ns]["prole_objects"] = 0 - total_res[ns]["prole_tombstones"] = 0 + total_res[ns]["total_objects"] = 0 total_res[ns]["used-bytes-memory"] = 0 total_res[ns]["used-bytes-disk"] = 0 total_res[ns]["evicted_objects"] = 0 + total_res[ns]["expired_objects"] = 0 total_res[ns]["migrate_tx_partitions_remaining"] = 0 total_res[ns]["migrate_rx_partitions_remaining"] = 0 + try: - total_res[ns]["master_objects"] += get_value_from_dict( - ns_stats, ('master-objects', 'master_objects'), return_type=int) + total_objects += get_value_from_dict( + ns_stats, ('master-objects', 'master_objects'), default_value=0, return_type=int) except Exception: pass try: - total_res[ns][ - "master_tombstones"] += get_value_from_dict(ns_stats, ('master_tombstones'), return_type=int) + total_objects += get_value_from_dict( + ns_stats, ('master_tombstones'), default_value=0, return_type=int) except Exception: pass + try: - total_res[ns]["prole_objects"] += get_value_from_dict( - ns_stats, ('prole-objects', 'prole_objects'), return_type=int) + total_objects += get_value_from_dict( + ns_stats, ('prole-objects', 'prole_objects'), default_value=0, return_type=int) except Exception: pass try: - total_res[ns][ - "prole_tombstones"] += get_value_from_dict(ns_stats, ('prole_tombstones'), return_type=int) + total_objects += get_value_from_dict( + ns_stats, ('prole_tombstones'), default_value=0, return_type=int) + except Exception: + pass + + try: + total_objects += get_value_from_dict( + ns_stats, ('non-replica-objects', 'non_replica_objects'), default_value=0, return_type=int) + except Exception: + pass + try: + total_objects += get_value_from_dict( + ns_stats, ('non_replica_tombstones'), default_value=0, return_type=int) except Exception: pass @@ -294,6 +300,12 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): except Exception: pass + try: + total_res[ns]["expired_objects"] += get_value_from_dict( + ns_stats, ('expired-objects', 'expired_objects'), return_type=int) + except Exception: + pass + try: total_res[ns]["migrate_tx_partitions_remaining"] += get_value_from_dict( ns_stats, ('migrate-tx-partitions-remaining', 'migrate_tx_partitions_remaining'), return_type=int) @@ -317,6 +329,9 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): row, ('free-pct-memory', 'memory_free_pct'))) set_value_in_dict( row, "stop_writes", get_value_from_dict(row, ('stop-writes', 'stop_writes'))) + set_value_in_dict( + row, "total_objects", total_objects) + total_res[ns]["total_objects"] += total_objects t.insert_row(row) @@ -331,15 +346,15 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): row["free_pct_memory"] = " " row["high-water-memory-pct"] = " " row["stop-writes-pct"] = " " + if rack_id_available: + row["rack-id"] = " " row['namespace'] = ns - row["master_objects"] = str(total_res[ns]["master_objects"]) - row["master_tombstones"] = str(total_res[ns]["master_tombstones"]) - row["prole_objects"] = str(total_res[ns]["prole_objects"]) - row["prole_tombstones"] = str(total_res[ns]["prole_tombstones"]) + row["total_objects"] = str(total_res[ns]["total_objects"]) row["used-bytes-memory"] = str(total_res[ns]["used-bytes-memory"]) row["used-bytes-disk"] = str(total_res[ns]["used-bytes-disk"]) row["evicted_objects"] = str(total_res[ns]["evicted_objects"]) + row["expired_objects"] = str(total_res[ns]["expired_objects"]) row["migrate_tx_partitions_remaining"] = str(total_res[ns]["migrate_tx_partitions_remaining"]) row["migrate_rx_partitions_remaining"] = str(total_res[ns]["migrate_rx_partitions_remaining"]) @@ -347,6 +362,158 @@ def info_namespace(stats, cluster, title_suffix="", **ignore): CliView.print_result(t) + @staticmethod + def info_object(stats, cluster, title_suffix="", **ignore): + prefixes = cluster.get_node_names() + principal = cluster.get_expected_principal() + + title = "Object Information%s" % (title_suffix) + column_names = ('namespace', 'node', ('_master_objects', 'Master (Objects,Tombstones)'), ('_prole_objects', 'Replica (Objects,Tombstones)'), + ('_non_replica_objects', 'Non-Replica (Objects,Tombstones)'), ('_migration', 'Migration') + ) + + t = Table(title, column_names, sort_by=0) + + t.add_data_source_tuple( + '_master_objects', + Extractors.sif_extractor(('master-objects', 'master_objects')), + Extractors.sif_extractor(('master_tombstones'))) + + t.add_data_source_tuple( + '_prole_objects', + Extractors.sif_extractor(('prole-objects', 'prole_objects')), + Extractors.sif_extractor(('prole_tombstones'))) + + t.add_data_source_tuple( + '_non_replica_objects', + Extractors.sif_extractor(('non_replica_objects')), + Extractors.sif_extractor(('non_replica_tombstones'))) + + t.add_cell_alert( + 'node', lambda data: data['real_node_id'] == principal, color=terminal.fg_green) + + t.add_cell_alert( + 'namespace', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_master_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_master_tombstones', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_prole_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_prole_tombstones', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_non_replica_objects', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_non_replica_tombstones', lambda data: data['node'] is " ", color=terminal.fg_blue) + t.add_cell_alert( + '_migration', lambda data: data['node'] is " ", color=terminal.fg_blue) + + total_res = {} + + # Need to maintain Node column ascending order per namespace. If set sort_by in table, it will affect total rows. + # So we need to add rows as Nodes ascending order. So need to sort + # stats.keys as per respective Node value (prefixes[node_key]). + node_key_list = stats.keys() + node_column_list = [prefixes[key] for key in node_key_list] + sorted_node_list = [x for (y, x) in sorted( + zip(node_column_list, node_key_list), key=lambda pair: pair[0])] + + for node_key in sorted_node_list: + n_stats = stats[node_key] + node = cluster.get_node(node_key)[0] + if isinstance(n_stats, Exception): + t.insert_row( + {'real_node_id': node.node_id, 'node': prefixes[node_key]}) + continue + + for ns, ns_stats in n_stats.iteritems(): + + if isinstance(ns_stats, Exception): + row = {} + else: + row = ns_stats + + pending_migration = False + + if ns not in total_res: + total_res[ns] = {} + total_res[ns]["master_objects"] = 0 + total_res[ns]["master_tombstones"] = 0 + total_res[ns]["prole_objects"] = 0 + total_res[ns]["prole_tombstones"] = 0 + total_res[ns]["non_replica_objects"] = 0 + total_res[ns]["non_replica_tombstones"] = 0 + total_res[ns]["migration"] = False + + try: + total_res[ns]["master_objects"] += get_value_from_dict( + ns_stats, ('master-objects', 'master_objects'), return_type=int) + except Exception: + pass + try: + total_res[ns][ + "master_tombstones"] += get_value_from_dict(ns_stats, ('master_tombstones'), return_type=int) + except Exception: + pass + try: + total_res[ns]["prole_objects"] += get_value_from_dict( + ns_stats, ('prole-objects', 'prole_objects'), return_type=int) + except Exception: + pass + try: + total_res[ns][ + "prole_tombstones"] += get_value_from_dict(ns_stats, ('prole_tombstones'), return_type=int) + except Exception: + pass + try: + total_res[ns]["non_replica_objects"] += get_value_from_dict( + ns_stats, ('non_replica_objects'), return_type=int) + except Exception: + pass + try: + total_res[ns][ + "non_replica_tombstones"] += get_value_from_dict(ns_stats, ('non_replica_tombstones'), return_type=int) + except Exception: + pass + + try: + if get_value_from_dict(ns_stats, ('migrate-tx-partitions-remaining', 'migrate_tx_partitions_remaining'), default_value=0, return_type=int): + pending_migration = True + total_res[ns]["migration"] = True + except Exception: + pass + + try: + if get_value_from_dict(ns_stats, ('migrate-rx-partitions-remaining', 'migrate_rx_partitions_remaining'), default_value=0, return_type=int): + pending_migration = True + total_res[ns]["migration"] = True + except Exception: + pass + + row['namespace'] = ns + row['real_node_id'] = node.node_id + row['node'] = prefixes[node_key] + row['_migration'] = str(pending_migration).lower() + t.insert_row(row) + + for ns in total_res: + row = {} + row['node'] = " " + + row['namespace'] = ns + row["master_objects"] = str(total_res[ns]["master_objects"]) + row["master_tombstones"] = str(total_res[ns]["master_tombstones"]) + row["prole_objects"] = str(total_res[ns]["prole_objects"]) + row["prole_tombstones"] = str(total_res[ns]["prole_tombstones"]) + row["non_replica_objects"] = str(total_res[ns]["non_replica_objects"]) + row["non_replica_tombstones"] = str(total_res[ns]["non_replica_tombstones"]) + row['_migration'] = str(total_res[ns]["migration"]).lower() + + t.insert_row(row) + + CliView.print_result(t) + @staticmethod def info_set(stats, cluster, title_suffix="", **ignore): prefixes = cluster.get_node_names() @@ -645,7 +812,7 @@ def show_distribution(title, histogram, unit, hist, cluster, like=None, title_su CliView.print_result(t) @staticmethod - def show_object_distribution(title, histogram, unit, hist, show_bucket_count, set_bucket_count, cluster, like=None, title_suffix="", loganalyser_mode=False, **ignore): + def show_object_distribution(title, histogram, unit, hist, bucket_count, set_bucket_count, cluster, like=None, title_suffix="", loganalyser_mode=False, **ignore): prefixes = cluster.get_node_names() likes = CliView.compile_likes(like) @@ -682,7 +849,7 @@ def show_object_distribution(title, histogram, unit, hist, show_bucket_count, se t.insert_row(row) CliView.print_result(t) - if set_bucket_count and (len(columns) - 1) < show_bucket_count: + if set_bucket_count and (len(columns) - 1) < bucket_count: print "%sShowing only %s bucket%s as remaining buckets have zero objects%s\n" % (terminal.fg_green(), (len(columns) - 1), "s" if (len(columns) - 1) > 1 else "", terminal.fg_clear()) @staticmethod @@ -758,7 +925,7 @@ def show_latency(latency, cluster, machine_wise_display=False, show_ns_details=F CliView.print_result(t) @staticmethod - def show_config(title, service_configs, cluster, like=None, diff=None, show_total=False, title_every_nth=0, **ignore): + def show_config(title, service_configs, cluster, like=None, diff=None, show_total=False, title_every_nth=0, flip_output=False, **ignore): prefixes = cluster.get_node_names() column_names = set() @@ -788,8 +955,17 @@ def show_config(title, service_configs, cluster, like=None, diff=None, show_tota column_names.insert(0, "NODE") + table_style = Styles.VERTICAL + if flip_output: + table_style = Styles.HORIZONTAL + + if show_total: + n_last_columns_ignore_sort = 1 + else: + n_last_columns_ignore_sort = 0 + t = Table(title, column_names, - title_format=TitleFormats.no_change, style=Styles.VERTICAL) + title_format=TitleFormats.no_change, style=table_style, n_last_columns_ignore_sort=n_last_columns_ignore_sort) row = None if show_total: @@ -847,7 +1023,9 @@ def show_grep_count(title, grep_result, title_every_nth=0, like=None, diff=None, t.insert_row(row1) t.insert_row(row2) - t._need_sort = False + + t.ignore_sort() + CliView.print_result( t.__str__(horizontal_title_every_nth=2 * title_every_nth)) @@ -902,7 +1080,9 @@ def show_grep_diff(title, grep_result, title_every_nth=0, like=None, diff=None, t.insert_row(row1) t.insert_row(row2) t.insert_row(row3) - t._need_sort = False + + t.ignore_sort() + CliView.print_result( t.__str__(horizontal_title_every_nth=title_every_nth * 3)) if different_writer_info: @@ -981,7 +1161,8 @@ def show_log_latency(title, grep_result, title_every_nth=0, like=None, diff=None row['NODE'] = "|" row['.'] = "|" t.insert_row(row) - t._need_sort = False + + t.ignore_sort() # print t CliView.print_result(t.__str__( horizontal_title_every_nth=title_every_nth * (sub_columns_per_column + 1))) @@ -1022,12 +1203,15 @@ def show_health(*args, **kwargs): @staticmethod def asinfo(results, line_sep, show_node_name, cluster, **kwargs): - like = set(kwargs['like']) + like = set([]) + if 'like' in kwargs: + like = set(kwargs['like']) + for node_id, value in results.iteritems(): - prefix = cluster.get_node_names()[node_id] - node = cluster.get_node(node_id)[0] if show_node_name: + prefix = cluster.get_node_names()[node_id] + node = cluster.get_node(node_id)[0] print "%s%s (%s) returned%s:" % (terminal.bold(), prefix, node.ip, terminal.reset()) if isinstance(value, Exception): @@ -1564,29 +1748,28 @@ def print_summary(summary): print @staticmethod - def show_pmap(pmap_data, cluster, **ignore): + def show_pmap(pmap_data, cluster, title_suffix="", **ignore): prefixes = cluster.get_node_names() - title = "Partition Map Analysis" - column_names = ('Node', - 'Namespace', - 'Primary Partitions', - 'Secondary Partitions', - 'Missing Partitions', - 'Master Discrepancy Partitions', - 'Replica Discrepancy Partitions') - t = Table(title, column_names) + title = "Partition Map Analysis%s"%(title_suffix) + column_names = (('_cluster_key', 'Cluster Key'), + 'namespace', + 'node', + ('_primary_partitions', 'Primary Partitions'), + ('_secondary_partitions', 'Secondary Partitions'), + ('_missing_partitions', 'Missing Partitions'), + ) + t = Table(title, column_names, group_by=0, sort_by=1) for node_key, n_stats in pmap_data.iteritems(): row = {} - row['Node'] = prefixes[node_key] + row['node'] = prefixes[node_key] for ns, ns_stats in n_stats.iteritems(): - row['Namespace'] = ns - row['Primary Partitions'] = ns_stats['pri_index'] - row['Secondary Partitions'] = ns_stats['sec_index'] - row['Missing Partitions'] = ns_stats['missing_part'] - row['Master Discrepancy Partitions'] = ns_stats['master_disc_part'] - row['Replica Discrepancy Partitions'] = ns_stats['replica_disc_part'] + row['namespace'] = ns + row['_primary_partitions'] = ns_stats['master_partition_count'] + row['_secondary_partitions'] = ns_stats['prole_partition_count'] + row['_missing_partitions'] = ns_stats['missing_partition_count'] + row['_cluster_key'] = ns_stats['cluster_key'] t.insert_row(row) CliView.print_result(t) diff --git a/test/e2e/test_info.py b/test/e2e/test_info.py index 85b655b0..159219a7 100644 --- a/test/e2e/test_info.py +++ b/test/e2e/test_info.py @@ -26,6 +26,7 @@ class TestInfo(unittest.TestCase): service_info = '' network_info = '' namespace_info = '' + object_info = '' sindex_info = '' xdr_info = '' @@ -34,7 +35,8 @@ def setUpClass(cls): TestInfo.rc = controller.BasicRootController() actual_out = util.capture_stdout(TestInfo.rc.execute, ['info']) TestInfo.output_list = test_util.get_separate_output(actual_out, 'Information') - # TestInfo.output_list.append(util.capture_stdout(TestInfo.rc.execute, ['info', 'sindex'])) + # TestInfo.output_list.append(util.capture_stdout(TestInfo.rc.execute, ['info', 'sindex'])) + TestInfo.output_list.append(util.capture_stdout(TestInfo.rc.execute, ['info', 'object'])) for item in TestInfo.output_list: if "~~Network Information~~" in item: TestInfo.network_info = item @@ -44,6 +46,8 @@ def setUpClass(cls): TestInfo.sindex_info = item elif "~~XDR Information~~" in item: TestInfo.xdr_info = item + elif "~~Object Information~~" in item: + TestInfo.object_info = item @classmethod def tearDownClass(self): @@ -101,29 +105,51 @@ def test_sindex(self): def test_namespace(self): """ This test will assert info Namespace output for heading, headerline1, headerline2 - and no of row displayed in output + displayed in output TODO: test for values as well """ exp_heading = "~~Namespace Information~~" exp_header = [ 'Node', 'Namespace', 'Evictions', + 'Expirations', 'Repl Factor', 'Stop Writes', + 'Disk Used', + 'Disk Used%', 'HWM Disk%', 'Mem Used', 'Mem Used%', 'HWM Mem%', 'Stop Writes%', - ('Master Objects', 'Master (Objects,Tombstones)'), - ('Replica Objects', 'Replica (Objects,Tombstones)') + 'Total Objects', + 'Pending Migrates', + 'Rack ID' ] - exp_no_of_rows = len(TestInfo.rc.cluster.nodes) - + actual_heading, actual_header, actual_no_of_rows = test_util.parse_output(TestInfo.namespace_info, horizontal = True) self.assertTrue(test_util.check_for_subset(actual_header, exp_header)) self.assertTrue(exp_heading in actual_heading) + def test_object(self): + """ + This test will assert info Object output for heading, headerline1, headerline2 + displayed in output + TODO: test for values as well + """ + exp_heading = "~~Object Information~~" + exp_header = [ 'Namespace', + 'Node', + 'Master (Objects,Tombstones)', + 'Replica (Objects,Tombstones)', + 'Non-Replica (Objects,Tombstones)', + 'Migration' + ] + + actual_heading, actual_header, actual_no_of_rows = test_util.parse_output(TestInfo.object_info, horizontal = True) + self.assertTrue(test_util.check_for_subset(actual_header, exp_header)) + self.assertTrue(exp_heading in actual_heading) + @unittest.skip("Will enable only when xdr is configuired") def test_xdr(self): """ diff --git a/test/test_asinfo.sh b/test/test_asinfo.sh index 77e0b035..c861d897 100755 --- a/test/test_asinfo.sh +++ b/test/test_asinfo.sh @@ -16,19 +16,23 @@ run_test(){ unknown_option_error="Do not understand" - asinfo_cmd_str="asinfo --no_node_name -v \"$1\" " - cmd_out=`./asadm.py --single_node_cluster -e "${asinfo_cmd_str}"` + asinfo_cmd_str="'$1' " + cmd_out=`./asadm.py --asinfo -e "${asinfo_cmd_str}"` cmd_status="$?" # echo ${cmd_out} if [ "$cmd_status" -ne 0 ]; then + echo return 1 fi if [[ $cmd_out == *"${unknown_option_error}"* ]]; then + echo return 1 fi if [[ $cmd_out != *"$2"* ]];then + echo return 1 fi + echo -n "." } asinfo_cmd="bins" @@ -104,9 +108,9 @@ fi # Test for invisible escape characters which can get added due to any python library like readline -asinfo_cmd_str="asinfo --no_node_name -v \"STATUS\" " +asinfo_cmd_str="\"STATUS\" " -cmd_out=`./asadm.py --single_node_cluster -e "${asinfo_cmd_str}" | tr -dc '[:alnum:]\n\r'` +cmd_out=`./asadm.py --asinfo -e "${asinfo_cmd_str}" | tr -dc '[:alnum:]\n\r'` cmd_status="$?" if [ "$cmd_status" -ne 0 ]; then @@ -118,7 +122,7 @@ if [[ $cmd_out != "OK" ]];then exit 1 fi -cmd_out=`./asadm.py --single_node_cluster -e "${asinfo_cmd_str}" | hexdump` +cmd_out=`./asadm.py --asinfo -e "${asinfo_cmd_str}" | hexdump` cmd_status="$?" expected_output=`echo "OK" | hexdump` @@ -131,5 +135,6 @@ if [[ $cmd_out != $expected_output ]];then exit 1 fi +echo echo "OK" exit 0 diff --git a/test/unit/test_controller.py b/test/unit/test_controller.py index 61467f2e..c700fb03 100644 --- a/test/unit/test_controller.py +++ b/test/unit/test_controller.py @@ -68,7 +68,7 @@ def test_info_controller(self): def test_show_distribution_controller(self): sdc = ShowDistributionController() - + sdc.pre_command([""]) sdc.do_time_to_live(["time_to_live"]) sdc.do_eviction(["evict"]) @@ -89,7 +89,7 @@ def test_show_latency_controller(self): slc.pre_command([""]) slc._do_default(["latency"]) - + def test_ShowStatisticsController(self): ssc = ShowStatisticsController() @@ -102,33 +102,55 @@ def test_ShowStatisticsController(self): class ShowPmapControllerTest(unittest.TestCase): + def mock_info_call(self, cmd, nodes="all"): + if cmd == "version": + return {'10.71.71.169:3000':'3.6.0'} + + if cmd == "partition-info": + return self.partition_info + + return {} + def setUp(self): - self.controller = ShowPmapController() - - def test_get_pmap_data_pos(self): - input_config = {'10.71.71.169:3000': 'test:0:A:2:0:0:0:0:0:0:0:0;test:1:A:2:0:0:0:0:0:0:0:0;test:2:A:2:0:0:0:0:0:0:0:0;test:3:S:1:0:0:0:0:207069:3001:0:0;test:4:S:0:0:0:0:0:0:0:0:0;test:4094:S:0:0:0:0:0:206724:2996:0:0;test:4095:S:0:0:0:0:0:213900:3100:0:0'} - expected_output = {'10.71.71.169:3000': {'test': {'missing_part': '0:S:0,0:S:1,1:S:0,1:S:1,2:S:0,2:S:1,3:S:0,4:S:1,5:S:0,5:S:1,6:S:0,6:S:1,7:S:0,7:S:1,8:S:0,8:S:1,9:S:0,9:S:1,10:S:0,10:S:1,11:S:0,11:S:1,12:S:0,12:S:1,13:S:0,13:S:1,14:S:0,14:S:1,15:S:0,15:S:1,16:S:0,16:S:1,17:S:0,17:S:1,18:S:0,18:S:1,19:S:0,19:S:1,20:S:0,20:S:1,21:S:0,21:S:1,22:S:0,22:S:1,23:S:0,23:S:1,24:S:0,24:S:1,25:S:0,25:S:1,26:S:0,26:S:1,27:S:0,27:S:1,28:S:0,28:S:1,29:S:0,29:S:1,30:S:0,30:S:1,31:S:0,31:S:1,32:S:0,32:S:1,33:S:0,33:S:1,34:S:0,34:S:1,35:S:0,35:S:1,36:S:0,36:S:1,37:S:0,37:S:1,38:S:0,38:S:1,39:S:0,39:S:1,40:S:0,40:S:1,41:S:0,41:S:1,42:S:0,42:S:1,43:S:0,43:S:1,44:S:0,44:S:1,45:S:0,45:S:1,46:S:0,46:S:1,47:S:0,47:S:1,48:S:0,48:S:1,49:S:0,49:S:1,50:S:0,50:S:1,51:S:0,51:S:1,52:S:0,52:S:1,53:S:0,53:S:1,54:S:0,54:S:1,55:S:0,55:S:1,56:S:0,56:S:1,57:S:0,57:S:1,58:S:0,58:S:1,59:S:0,59:S:1,60:S:0,60:S:1,61:S:0,61:S:1,62:S:0,62:S:1,63:S:0,63:S:1,64:S:0,64:S:1,65:S:0,65:S:1,66:S:0,66:S:1,67:S:0,67:S:1,68:S:0,68:S:1,69:S:0,69:S:1,70:S:0,70:S:1,71:S:0,71:S:1,72:S:0,72:S:1,73:S:0,73:S:1,74:S:0,74:S:1,75:S:0,75:S:1,76:S:0,76:S:1,77:S:0,77:S:1,78:S:0,78:S:1,79:S:0,79:S:1,80:S:0,80:S:1,81:S:0,81:S:1,82:S:0,82:S:1,83:S:0,83:S:1,84:S:0,84:S:1,85:S:0,85:S:1,86:S:0,86:S:1,87:S:0,87:S:1,88:S:0,88:S:1,89:S:0,89:S:1,90:S:0,90:S:1,91:S:0,91:S:1,92:S:0,92:S:1,93:S:0,93:S:1,94:S:0,94:S:1,95:S:0,95:S:1,96:S:0,96:S:1,97:S:0,97:S:1,98:S:0,98:S:1,99:S:0,99:S:1,100:S:0,100:S:1,101:S:0,101:S:1,102:S:0,102:S:1,103:S:0,103:S:1,104:S:0,104:S:1,105:S:0,105:S:1,106:S:0,106:S:1,107:S:0,107:S:1,108:S:0,108:S:1,109:S:0,109:S:1,110:S:0,110:S:1,111:S:0,111:S:1,112:S:0,112:S:1,113:S:0,113:S:1,114:S:0,114:S:1,115:S:0,115:S:1,116:S:0,116:S:1,117:S:0,117:S:1,118:S:0,118:S:1,119:S:0,119:S:1,120:S:0,120:S:1,121:S:0,121:S:1,122:S:0,122:S:1,123:S:0,123:S:1,124:S:0,124:S:1,125:S:0,125:S:1,126:S:0,126:S:1,127:S:0,127:S:1,128:S:0,128:S:1,129:S:0,129:S:1,130:S:0,130:S:1,131:S:0,131:S:1,132:S:0,132:S:1,133:S:0,133:S:1,134:S:0,134:S:1,135:S:0,135:S:1,136:S:0,136:S:1,137:S:0,137:S:1,138:S:0,138:S:1,139:S:0,139:S:1,140:S:0,140:S:1,141:S:0,141:S:1,142:S:0,142:S:1,143:S:0,143:S:1,144:S:0,144:S:1,145:S:0,145:S:1,146:S:0,146:S:1,147:S:0,147:S:1,148:S:0,148:S:1,149:S:0,149:S:1,150:S:0,150:S:1,151:S:0,151:S:1,152:S:0,152:S:1,153:S:0,153:S:1,154:S:0,154:S:1,155:S:0,155:S:1,156:S:0,156:S:1,157:S:0,157:S:1,158:S:0,158:S:1,159:S:0,159:S:1,160:S:0,160:S:1,161:S:0,161:S:1,162:S:0,162:S:1,163:S:0,163:S:1,164:S:0,164:S:1,165:S:0,165:S:1,166:S:0,166:S:1,167:S:0,167:S:1,168:S:0,168:S:1,169:S:0,169:S:1,170:S:0,170:S:1,171:S:0,171:S:1,172:S:0,172:S:1,173:S:0,173:S:1,174:S:0,174:S:1,175:S:0,175:S:1,176:S:0,176:S:1,177:S:0,177:S:1,178:S:0,178:S:1,179:S:0,179:S:1,180:S:0,180:S:1,181:S:0,181:S:1,182:S:0,182:S:1,183:S:0,183:S:1,184:S:0,184:S:1,185:S:0,185:S:1,186:S:0,186:S:1,187:S:0,187:S:1,188:S:0,188:S:1,189:S:0,189:S:1,190:S:0,190:S:1,191:S:0,191:S:1,192:S:0,192:S:1,193:S:0,193:S:1,194:S:0,194:S:1,195:S:0,195:S:1,196:S:0,196:S:1,197:S:0,197:S:1,198:S:0,198:S:1,199:S:0,199:S:1,200:S:0,200:S:1,201:S:0,201:S:1,202:S:0,202:S:1,203:S:0,203:S:1,204:S:0,204:S:1,205:S:0,205:S:1,206:S:0,206:S:1,207:S:0,207:S:1,208:S:0,208:S:1,209:S:0,209:S:1,210:S:0,210:S:1,211:S:0,211:S:1,212:S:0,212:S:1,213:S:0,213:S:1,214:S:0,214:S:1,215:S:0,215:S:1,216:S:0,216:S:1,217:S:0,217:S:1,218:S:0,218:S:1,219:S:0,219:S:1,220:S:0,220:S:1,221:S:0,221:S:1,222:S:0,222:S:1,223:S:0,223:S:1,224:S:0,224:S:1,225:S:0,225:S:1,226:S:0,226:S:1,227:S:0,227:S:1,228:S:0,228:S:1,229:S:0,229:S:1,230:S:0,230:S:1,231:S:0,231:S:1,232:S:0,232:S:1,233:S:0,233:S:1,234:S:0,234:S:1,235:S:0,235:S:1,236:S:0,236:S:1,237:S:0,237:S:1,238:S:0,238:S:1,239:S:0,239:S:1,240:S:0,240:S:1,241:S:0,241:S:1,242:S:0,242:S:1,243:S:0,243:S:1,244:S:0,244:S:1,245:S:0,245:S:1,246:S:0,246:S:1,247:S:0,247:S:1,248:S:0,248:S:1,249:S:0,249:S:1,250:S:0,250:S:1,251:S:0,251:S:1,252:S:0,252:S:1,253:S:0,253:S:1,254:S:0,254:S:1,255:S:0,255:S:1,256:S:0,256:S:1,257:S:0,257:S:1,258:S:0,258:S:1,259:S:0,259:S:1,260:S:0,260:S:1,261:S:0,261:S:1,262:S:0,262:S:1,263:S:0,263:S:1,264:S:0,264:S:1,265:S:0,265:S:1,266:S:0,266:S:1,267:S:0,267:S:1,268:S:0,268:S:1,269:S:0,269:S:1,270:S:0,270:S:1,271:S:0,271:S:1,272:S:0,272:S:1,273:S:0,273:S:1,274:S:0,274:S:1,275:S:0,275:S:1,276:S:0,276:S:1,277:S:0,277:S:1,278:S:0,278:S:1,279:S:0,279:S:1,280:S:0,280:S:1,281:S:0,281:S:1,282:S:0,282:S:1,283:S:0,283:S:1,284:S:0,284:S:1,285:S:0,285:S:1,286:S:0,286:S:1,287:S:0,287:S:1,288:S:0,288:S:1,289:S:0,289:S:1,290:S:0,290:S:1,291:S:0,291:S:1,292:S:0,292:S:1,293:S:0,293:S:1,294:S:0,294:S:1,295:S:0,295:S:1,296:S:0,296:S:1,297:S:0,297:S:1,298:S:0,298:S:1,299:S:0,299:S:1,300:S:0,300:S:1,301:S:0,301:S:1,302:S:0,302:S:1,303:S:0,303:S:1,304:S:0,304:S:1,305:S:0,305:S:1,306:S:0,306:S:1,307:S:0,307:S:1,308:S:0,308:S:1,309:S:0,309:S:1,310:S:0,310:S:1,311:S:0,311:S:1,312:S:0,312:S:1,313:S:0,313:S:1,314:S:0,314:S:1,315:S:0,315:S:1,316:S:0,316:S:1,317:S:0,317:S:1,318:S:0,318:S:1,319:S:0,319:S:1,320:S:0,320:S:1,321:S:0,321:S:1,322:S:0,322:S:1,323:S:0,323:S:1,324:S:0,324:S:1,325:S:0,325:S:1,326:S:0,326:S:1,327:S:0,327:S:1,328:S:0,328:S:1,329:S:0,329:S:1,330:S:0,330:S:1,331:S:0,331:S:1,332:S:0,332:S:1,333:S:0,333:S:1,334:S:0,334:S:1,335:S:0,335:S:1,336:S:0,336:S:1,337:S:0,337:S:1,338:S:0,338:S:1,339:S:0,339:S:1,340:S:0,340:S:1,341:S:0,341:S:1,342:S:0,342:S:1,343:S:0,343:S:1,344:S:0,344:S:1,345:S:0,345:S:1,346:S:0,346:S:1,347:S:0,347:S:1,348:S:0,348:S:1,349:S:0,349:S:1,350:S:0,350:S:1,351:S:0,351:S:1,352:S:0,352:S:1,353:S:0,353:S:1,354:S:0,354:S:1,355:S:0,355:S:1,356:S:0,356:S:1,357:S:0,357:S:1,358:S:0,358:S:1,359:S:0,359:S:1,360:S:0,360:S:1,361:S:0,361:S:1,362:S:0,362:S:1,363:S:0,363:S:1,364:S:0,364:S:1,365:S:0,365:S:1,366:S:0,366:S:1,367:S:0,367:S:1,368:S:0,368:S:1,369:S:0,369:S:1,370:S:0,370:S:1,371:S:0,371:S:1,372:S:0,372:S:1,373:S:0,373:S:1,374:S:0,374:S:1,375:S:0,375:S:1,376:S:0,376:S:1,377:S:0,377:S:1,378:S:0,378:S:1,379:S:0,379:S:1,380:S:0,380:S:1,381:S:0,381:S:1,382:S:0,382:S:1,383:S:0,383:S:1,384:S:0,384:S:1,385:S:0,385:S:1,386:S:0,386:S:1,387:S:0,387:S:1,388:S:0,388:S:1,389:S:0,389:S:1,390:S:0,390:S:1,391:S:0,391:S:1,392:S:0,392:S:1,393:S:0,393:S:1,394:S:0,394:S:1,395:S:0,395:S:1,396:S:0,396:S:1,397:S:0,397:S:1,398:S:0,398:S:1,399:S:0,399:S:1,400:S:0,400:S:1,401:S:0,401:S:1,402:S:0,402:S:1,403:S:0,403:S:1,404:S:0,404:S:1,405:S:0,405:S:1,406:S:0,406:S:1,407:S:0,407:S:1,408:S:0,408:S:1,409:S:0,409:S:1,410:S:0,410:S:1,411:S:0,411:S:1,412:S:0,412:S:1,413:S:0,413:S:1,414:S:0,414:S:1,415:S:0,415:S:1,416:S:0,416:S:1,417:S:0,417:S:1,418:S:0,418:S:1,419:S:0,419:S:1,420:S:0,420:S:1,421:S:0,421:S:1,422:S:0,422:S:1,423:S:0,423:S:1,424:S:0,424:S:1,425:S:0,425:S:1,426:S:0,426:S:1,427:S:0,427:S:1,428:S:0,428:S:1,429:S:0,429:S:1,430:S:0,430:S:1,431:S:0,431:S:1,432:S:0,432:S:1,433:S:0,433:S:1,434:S:0,434:S:1,435:S:0,435:S:1,436:S:0,436:S:1,437:S:0,437:S:1,438:S:0,438:S:1,439:S:0,439:S:1,440:S:0,440:S:1,441:S:0,441:S:1,442:S:0,442:S:1,443:S:0,443:S:1,444:S:0,444:S:1,445:S:0,445:S:1,446:S:0,446:S:1,447:S:0,447:S:1,448:S:0,448:S:1,449:S:0,449:S:1,450:S:0,450:S:1,451:S:0,451:S:1,452:S:0,452:S:1,453:S:0,453:S:1,454:S:0,454:S:1,455:S:0,455:S:1,456:S:0,456:S:1,457:S:0,457:S:1,458:S:0,458:S:1,459:S:0,459:S:1,460:S:0,460:S:1,461:S:0,461:S:1,462:S:0,462:S:1,463:S:0,463:S:1,464:S:0,464:S:1,465:S:0,465:S:1,466:S:0,466:S:1,467:S:0,467:S:1,468:S:0,468:S:1,469:S:0,469:S:1,470:S:0,470:S:1,471:S:0,471:S:1,472:S:0,472:S:1,473:S:0,473:S:1,474:S:0,474:S:1,475:S:0,475:S:1,476:S:0,476:S:1,477:S:0,477:S:1,478:S:0,478:S:1,479:S:0,479:S:1,480:S:0,480:S:1,481:S:0,481:S:1,482:S:0,482:S:1,483:S:0,483:S:1,484:S:0,484:S:1,485:S:0,485:S:1,486:S:0,486:S:1,487:S:0,487:S:1,488:S:0,488:S:1,489:S:0,489:S:1,490:S:0,490:S:1,491:S:0,491:S:1,492:S:0,492:S:1,493:S:0,493:S:1,494:S:0,494:S:1,495:S:0,495:S:1,496:S:0,496:S:1,497:S:0,497:S:1,498:S:0,498:S:1,499:S:0,499:S:1,500:S:0,500:S:1,501:S:0,501:S:1,502:S:0,502:S:1,503:S:0,503:S:1,504:S:0,504:S:1,505:S:0,505:S:1,506:S:0,506:S:1,507:S:0,507:S:1,508:S:0,508:S:1,509:S:0,509:S:1,510:S:0,510:S:1,511:S:0,511:S:1,512:S:0,512:S:1,513:S:0,513:S:1,514:S:0,514:S:1,515:S:0,515:S:1,516:S:0,516:S:1,517:S:0,517:S:1,518:S:0,518:S:1,519:S:0,519:S:1,520:S:0,520:S:1,521:S:0,521:S:1,522:S:0,522:S:1,523:S:0,523:S:1,524:S:0,524:S:1,525:S:0,525:S:1,526:S:0,526:S:1,527:S:0,527:S:1,528:S:0,528:S:1,529:S:0,529:S:1,530:S:0,530:S:1,531:S:0,531:S:1,532:S:0,532:S:1,533:S:0,533:S:1,534:S:0,534:S:1,535:S:0,535:S:1,536:S:0,536:S:1,537:S:0,537:S:1,538:S:0,538:S:1,539:S:0,539:S:1,540:S:0,540:S:1,541:S:0,541:S:1,542:S:0,542:S:1,543:S:0,543:S:1,544:S:0,544:S:1,545:S:0,545:S:1,546:S:0,546:S:1,547:S:0,547:S:1,548:S:0,548:S:1,549:S:0,549:S:1,550:S:0,550:S:1,551:S:0,551:S:1,552:S:0,552:S:1,553:S:0,553:S:1,554:S:0,554:S:1,555:S:0,555:S:1,556:S:0,556:S:1,557:S:0,557:S:1,558:S:0,558:S:1,559:S:0,559:S:1,560:S:0,560:S:1,561:S:0,561:S:1,562:S:0,562:S:1,563:S:0,563:S:1,564:S:0,564:S:1,565:S:0,565:S:1,566:S:0,566:S:1,567:S:0,567:S:1,568:S:0,568:S:1,569:S:0,569:S:1,570:S:0,570:S:1,571:S:0,571:S:1,572:S:0,572:S:1,573:S:0,573:S:1,574:S:0,574:S:1,575:S:0,575:S:1,576:S:0,576:S:1,577:S:0,577:S:1,578:S:0,578:S:1,579:S:0,579:S:1,580:S:0,580:S:1,581:S:0,581:S:1,582:S:0,582:S:1,583:S:0,583:S:1,584:S:0,584:S:1,585:S:0,585:S:1,586:S:0,586:S:1,587:S:0,587:S:1,588:S:0,588:S:1,589:S:0,589:S:1,590:S:0,590:S:1,591:S:0,591:S:1,592:S:0,592:S:1,593:S:0,593:S:1,594:S:0,594:S:1,595:S:0,595:S:1,596:S:0,596:S:1,597:S:0,597:S:1,598:S:0,598:S:1,599:S:0,599:S:1,600:S:0,600:S:1,601:S:0,601:S:1,602:S:0,602:S:1,603:S:0,603:S:1,604:S:0,604:S:1,605:S:0,605:S:1,606:S:0,606:S:1,607:S:0,607:S:1,608:S:0,608:S:1,609:S:0,609:S:1,610:S:0,610:S:1,611:S:0,611:S:1,612:S:0,612:S:1,613:S:0,613:S:1,614:S:0,614:S:1,615:S:0,615:S:1,616:S:0,616:S:1,617:S:0,617:S:1,618:S:0,618:S:1,619:S:0,619:S:1,620:S:0,620:S:1,621:S:0,621:S:1,622:S:0,622:S:1,623:S:0,623:S:1,624:S:0,624:S:1,625:S:0,625:S:1,626:S:0,626:S:1,627:S:0,627:S:1,628:S:0,628:S:1,629:S:0,629:S:1,630:S:0,630:S:1,631:S:0,631:S:1,632:S:0,632:S:1,633:S:0,633:S:1,634:S:0,634:S:1,635:S:0,635:S:1,636:S:0,636:S:1,637:S:0,637:S:1,638:S:0,638:S:1,639:S:0,639:S:1,640:S:0,640:S:1,641:S:0,641:S:1,642:S:0,642:S:1,643:S:0,643:S:1,644:S:0,644:S:1,645:S:0,645:S:1,646:S:0,646:S:1,647:S:0,647:S:1,648:S:0,648:S:1,649:S:0,649:S:1,650:S:0,650:S:1,651:S:0,651:S:1,652:S:0,652:S:1,653:S:0,653:S:1,654:S:0,654:S:1,655:S:0,655:S:1,656:S:0,656:S:1,657:S:0,657:S:1,658:S:0,658:S:1,659:S:0,659:S:1,660:S:0,660:S:1,661:S:0,661:S:1,662:S:0,662:S:1,663:S:0,663:S:1,664:S:0,664:S:1,665:S:0,665:S:1,666:S:0,666:S:1,667:S:0,667:S:1,668:S:0,668:S:1,669:S:0,669:S:1,670:S:0,670:S:1,671:S:0,671:S:1,672:S:0,672:S:1,673:S:0,673:S:1,674:S:0,674:S:1,675:S:0,675:S:1,676:S:0,676:S:1,677:S:0,677:S:1,678:S:0,678:S:1,679:S:0,679:S:1,680:S:0,680:S:1,681:S:0,681:S:1,682:S:0,682:S:1,683:S:0,683:S:1,684:S:0,684:S:1,685:S:0,685:S:1,686:S:0,686:S:1,687:S:0,687:S:1,688:S:0,688:S:1,689:S:0,689:S:1,690:S:0,690:S:1,691:S:0,691:S:1,692:S:0,692:S:1,693:S:0,693:S:1,694:S:0,694:S:1,695:S:0,695:S:1,696:S:0,696:S:1,697:S:0,697:S:1,698:S:0,698:S:1,699:S:0,699:S:1,700:S:0,700:S:1,701:S:0,701:S:1,702:S:0,702:S:1,703:S:0,703:S:1,704:S:0,704:S:1,705:S:0,705:S:1,706:S:0,706:S:1,707:S:0,707:S:1,708:S:0,708:S:1,709:S:0,709:S:1,710:S:0,710:S:1,711:S:0,711:S:1,712:S:0,712:S:1,713:S:0,713:S:1,714:S:0,714:S:1,715:S:0,715:S:1,716:S:0,716:S:1,717:S:0,717:S:1,718:S:0,718:S:1,719:S:0,719:S:1,720:S:0,720:S:1,721:S:0,721:S:1,722:S:0,722:S:1,723:S:0,723:S:1,724:S:0,724:S:1,725:S:0,725:S:1,726:S:0,726:S:1,727:S:0,727:S:1,728:S:0,728:S:1,729:S:0,729:S:1,730:S:0,730:S:1,731:S:0,731:S:1,732:S:0,732:S:1,733:S:0,733:S:1,734:S:0,734:S:1,735:S:0,735:S:1,736:S:0,736:S:1,737:S:0,737:S:1,738:S:0,738:S:1,739:S:0,739:S:1,740:S:0,740:S:1,741:S:0,741:S:1,742:S:0,742:S:1,743:S:0,743:S:1,744:S:0,744:S:1,745:S:0,745:S:1,746:S:0,746:S:1,747:S:0,747:S:1,748:S:0,748:S:1,749:S:0,749:S:1,750:S:0,750:S:1,751:S:0,751:S:1,752:S:0,752:S:1,753:S:0,753:S:1,754:S:0,754:S:1,755:S:0,755:S:1,756:S:0,756:S:1,757:S:0,757:S:1,758:S:0,758:S:1,759:S:0,759:S:1,760:S:0,760:S:1,761:S:0,761:S:1,762:S:0,762:S:1,763:S:0,763:S:1,764:S:0,764:S:1,765:S:0,765:S:1,766:S:0,766:S:1,767:S:0,767:S:1,768:S:0,768:S:1,769:S:0,769:S:1,770:S:0,770:S:1,771:S:0,771:S:1,772:S:0,772:S:1,773:S:0,773:S:1,774:S:0,774:S:1,775:S:0,775:S:1,776:S:0,776:S:1,777:S:0,777:S:1,778:S:0,778:S:1,779:S:0,779:S:1,780:S:0,780:S:1,781:S:0,781:S:1,782:S:0,782:S:1,783:S:0,783:S:1,784:S:0,784:S:1,785:S:0,785:S:1,786:S:0,786:S:1,787:S:0,787:S:1,788:S:0,788:S:1,789:S:0,789:S:1,790:S:0,790:S:1,791:S:0,791:S:1,792:S:0,792:S:1,793:S:0,793:S:1,794:S:0,794:S:1,795:S:0,795:S:1,796:S:0,796:S:1,797:S:0,797:S:1,798:S:0,798:S:1,799:S:0,799:S:1,800:S:0,800:S:1,801:S:0,801:S:1,802:S:0,802:S:1,803:S:0,803:S:1,804:S:0,804:S:1,805:S:0,805:S:1,806:S:0,806:S:1,807:S:0,807:S:1,808:S:0,808:S:1,809:S:0,809:S:1,810:S:0,810:S:1,811:S:0,811:S:1,812:S:0,812:S:1,813:S:0,813:S:1,814:S:0,814:S:1,815:S:0,815:S:1,816:S:0,816:S:1,817:S:0,817:S:1,818:S:0,818:S:1,819:S:0,819:S:1,820:S:0,820:S:1,821:S:0,821:S:1,822:S:0,822:S:1,823:S:0,823:S:1,824:S:0,824:S:1,825:S:0,825:S:1,826:S:0,826:S:1,827:S:0,827:S:1,828:S:0,828:S:1,829:S:0,829:S:1,830:S:0,830:S:1,831:S:0,831:S:1,832:S:0,832:S:1,833:S:0,833:S:1,834:S:0,834:S:1,835:S:0,835:S:1,836:S:0,836:S:1,837:S:0,837:S:1,838:S:0,838:S:1,839:S:0,839:S:1,840:S:0,840:S:1,841:S:0,841:S:1,842:S:0,842:S:1,843:S:0,843:S:1,844:S:0,844:S:1,845:S:0,845:S:1,846:S:0,846:S:1,847:S:0,847:S:1,848:S:0,848:S:1,849:S:0,849:S:1,850:S:0,850:S:1,851:S:0,851:S:1,852:S:0,852:S:1,853:S:0,853:S:1,854:S:0,854:S:1,855:S:0,855:S:1,856:S:0,856:S:1,857:S:0,857:S:1,858:S:0,858:S:1,859:S:0,859:S:1,860:S:0,860:S:1,861:S:0,861:S:1,862:S:0,862:S:1,863:S:0,863:S:1,864:S:0,864:S:1,865:S:0,865:S:1,866:S:0,866:S:1,867:S:0,867:S:1,868:S:0,868:S:1,869:S:0,869:S:1,870:S:0,870:S:1,871:S:0,871:S:1,872:S:0,872:S:1,873:S:0,873:S:1,874:S:0,874:S:1,875:S:0,875:S:1,876:S:0,876:S:1,877:S:0,877:S:1,878:S:0,878:S:1,879:S:0,879:S:1,880:S:0,880:S:1,881:S:0,881:S:1,882:S:0,882:S:1,883:S:0,883:S:1,884:S:0,884:S:1,885:S:0,885:S:1,886:S:0,886:S:1,887:S:0,887:S:1,888:S:0,888:S:1,889:S:0,889:S:1,890:S:0,890:S:1,891:S:0,891:S:1,892:S:0,892:S:1,893:S:0,893:S:1,894:S:0,894:S:1,895:S:0,895:S:1,896:S:0,896:S:1,897:S:0,897:S:1,898:S:0,898:S:1,899:S:0,899:S:1,900:S:0,900:S:1,901:S:0,901:S:1,902:S:0,902:S:1,903:S:0,903:S:1,904:S:0,904:S:1,905:S:0,905:S:1,906:S:0,906:S:1,907:S:0,907:S:1,908:S:0,908:S:1,909:S:0,909:S:1,910:S:0,910:S:1,911:S:0,911:S:1,912:S:0,912:S:1,913:S:0,913:S:1,914:S:0,914:S:1,915:S:0,915:S:1,916:S:0,916:S:1,917:S:0,917:S:1,918:S:0,918:S:1,919:S:0,919:S:1,920:S:0,920:S:1,921:S:0,921:S:1,922:S:0,922:S:1,923:S:0,923:S:1,924:S:0,924:S:1,925:S:0,925:S:1,926:S:0,926:S:1,927:S:0,927:S:1,928:S:0,928:S:1,929:S:0,929:S:1,930:S:0,930:S:1,931:S:0,931:S:1,932:S:0,932:S:1,933:S:0,933:S:1,934:S:0,934:S:1,935:S:0,935:S:1,936:S:0,936:S:1,937:S:0,937:S:1,938:S:0,938:S:1,939:S:0,939:S:1,940:S:0,940:S:1,941:S:0,941:S:1,942:S:0,942:S:1,943:S:0,943:S:1,944:S:0,944:S:1,945:S:0,945:S:1,946:S:0,946:S:1,947:S:0,947:S:1,948:S:0,948:S:1,949:S:0,949:S:1,950:S:0,950:S:1,951:S:0,951:S:1,952:S:0,952:S:1,953:S:0,953:S:1,954:S:0,954:S:1,955:S:0,955:S:1,956:S:0,956:S:1,957:S:0,957:S:1,958:S:0,958:S:1,959:S:0,959:S:1,960:S:0,960:S:1,961:S:0,961:S:1,962:S:0,962:S:1,963:S:0,963:S:1,964:S:0,964:S:1,965:S:0,965:S:1,966:S:0,966:S:1,967:S:0,967:S:1,968:S:0,968:S:1,969:S:0,969:S:1,970:S:0,970:S:1,971:S:0,971:S:1,972:S:0,972:S:1,973:S:0,973:S:1,974:S:0,974:S:1,975:S:0,975:S:1,976:S:0,976:S:1,977:S:0,977:S:1,978:S:0,978:S:1,979:S:0,979:S:1,980:S:0,980:S:1,981:S:0,981:S:1,982:S:0,982:S:1,983:S:0,983:S:1,984:S:0,984:S:1,985:S:0,985:S:1,986:S:0,986:S:1,987:S:0,987:S:1,988:S:0,988:S:1,989:S:0,989:S:1,990:S:0,990:S:1,991:S:0,991:S:1,992:S:0,992:S:1,993:S:0,993:S:1,994:S:0,994:S:1,995:S:0,995:S:1,996:S:0,996:S:1,997:S:0,997:S:1,998:S:0,998:S:1,999:S:0,999:S:1,1000:S:0,1000:S:1,1001:S:0,1001:S:1,1002:S:0,1002:S:1,1003:S:0,1003:S:1,1004:S:0,1004:S:1,1005:S:0,1005:S:1,1006:S:0,1006:S:1,1007:S:0,1007:S:1,1008:S:0,1008:S:1,1009:S:0,1009:S:1,1010:S:0,1010:S:1,1011:S:0,1011:S:1,1012:S:0,1012:S:1,1013:S:0,1013:S:1,1014:S:0,1014:S:1,1015:S:0,1015:S:1,1016:S:0,1016:S:1,1017:S:0,1017:S:1,1018:S:0,1018:S:1,1019:S:0,1019:S:1,1020:S:0,1020:S:1,1021:S:0,1021:S:1,1022:S:0,1022:S:1,1023:S:0,1023:S:1,1024:S:0,1024:S:1,1025:S:0,1025:S:1,1026:S:0,1026:S:1,1027:S:0,1027:S:1,1028:S:0,1028:S:1,1029:S:0,1029:S:1,1030:S:0,1030:S:1,1031:S:0,1031:S:1,1032:S:0,1032:S:1,1033:S:0,1033:S:1,1034:S:0,1034:S:1,1035:S:0,1035:S:1,1036:S:0,1036:S:1,1037:S:0,1037:S:1,1038:S:0,1038:S:1,1039:S:0,1039:S:1,1040:S:0,1040:S:1,1041:S:0,1041:S:1,1042:S:0,1042:S:1,1043:S:0,1043:S:1,1044:S:0,1044:S:1,1045:S:0,1045:S:1,1046:S:0,1046:S:1,1047:S:0,1047:S:1,1048:S:0,1048:S:1,1049:S:0,1049:S:1,1050:S:0,1050:S:1,1051:S:0,1051:S:1,1052:S:0,1052:S:1,1053:S:0,1053:S:1,1054:S:0,1054:S:1,1055:S:0,1055:S:1,1056:S:0,1056:S:1,1057:S:0,1057:S:1,1058:S:0,1058:S:1,1059:S:0,1059:S:1,1060:S:0,1060:S:1,1061:S:0,1061:S:1,1062:S:0,1062:S:1,1063:S:0,1063:S:1,1064:S:0,1064:S:1,1065:S:0,1065:S:1,1066:S:0,1066:S:1,1067:S:0,1067:S:1,1068:S:0,1068:S:1,1069:S:0,1069:S:1,1070:S:0,1070:S:1,1071:S:0,1071:S:1,1072:S:0,1072:S:1,1073:S:0,1073:S:1,1074:S:0,1074:S:1,1075:S:0,1075:S:1,1076:S:0,1076:S:1,1077:S:0,1077:S:1,1078:S:0,1078:S:1,1079:S:0,1079:S:1,1080:S:0,1080:S:1,1081:S:0,1081:S:1,1082:S:0,1082:S:1,1083:S:0,1083:S:1,1084:S:0,1084:S:1,1085:S:0,1085:S:1,1086:S:0,1086:S:1,1087:S:0,1087:S:1,1088:S:0,1088:S:1,1089:S:0,1089:S:1,1090:S:0,1090:S:1,1091:S:0,1091:S:1,1092:S:0,1092:S:1,1093:S:0,1093:S:1,1094:S:0,1094:S:1,1095:S:0,1095:S:1,1096:S:0,1096:S:1,1097:S:0,1097:S:1,1098:S:0,1098:S:1,1099:S:0,1099:S:1,1100:S:0,1100:S:1,1101:S:0,1101:S:1,1102:S:0,1102:S:1,1103:S:0,1103:S:1,1104:S:0,1104:S:1,1105:S:0,1105:S:1,1106:S:0,1106:S:1,1107:S:0,1107:S:1,1108:S:0,1108:S:1,1109:S:0,1109:S:1,1110:S:0,1110:S:1,1111:S:0,1111:S:1,1112:S:0,1112:S:1,1113:S:0,1113:S:1,1114:S:0,1114:S:1,1115:S:0,1115:S:1,1116:S:0,1116:S:1,1117:S:0,1117:S:1,1118:S:0,1118:S:1,1119:S:0,1119:S:1,1120:S:0,1120:S:1,1121:S:0,1121:S:1,1122:S:0,1122:S:1,1123:S:0,1123:S:1,1124:S:0,1124:S:1,1125:S:0,1125:S:1,1126:S:0,1126:S:1,1127:S:0,1127:S:1,1128:S:0,1128:S:1,1129:S:0,1129:S:1,1130:S:0,1130:S:1,1131:S:0,1131:S:1,1132:S:0,1132:S:1,1133:S:0,1133:S:1,1134:S:0,1134:S:1,1135:S:0,1135:S:1,1136:S:0,1136:S:1,1137:S:0,1137:S:1,1138:S:0,1138:S:1,1139:S:0,1139:S:1,1140:S:0,1140:S:1,1141:S:0,1141:S:1,1142:S:0,1142:S:1,1143:S:0,1143:S:1,1144:S:0,1144:S:1,1145:S:0,1145:S:1,1146:S:0,1146:S:1,1147:S:0,1147:S:1,1148:S:0,1148:S:1,1149:S:0,1149:S:1,1150:S:0,1150:S:1,1151:S:0,1151:S:1,1152:S:0,1152:S:1,1153:S:0,1153:S:1,1154:S:0,1154:S:1,1155:S:0,1155:S:1,1156:S:0,1156:S:1,1157:S:0,1157:S:1,1158:S:0,1158:S:1,1159:S:0,1159:S:1,1160:S:0,1160:S:1,1161:S:0,1161:S:1,1162:S:0,1162:S:1,1163:S:0,1163:S:1,1164:S:0,1164:S:1,1165:S:0,1165:S:1,1166:S:0,1166:S:1,1167:S:0,1167:S:1,1168:S:0,1168:S:1,1169:S:0,1169:S:1,1170:S:0,1170:S:1,1171:S:0,1171:S:1,1172:S:0,1172:S:1,1173:S:0,1173:S:1,1174:S:0,1174:S:1,1175:S:0,1175:S:1,1176:S:0,1176:S:1,1177:S:0,1177:S:1,1178:S:0,1178:S:1,1179:S:0,1179:S:1,1180:S:0,1180:S:1,1181:S:0,1181:S:1,1182:S:0,1182:S:1,1183:S:0,1183:S:1,1184:S:0,1184:S:1,1185:S:0,1185:S:1,1186:S:0,1186:S:1,1187:S:0,1187:S:1,1188:S:0,1188:S:1,1189:S:0,1189:S:1,1190:S:0,1190:S:1,1191:S:0,1191:S:1,1192:S:0,1192:S:1,1193:S:0,1193:S:1,1194:S:0,1194:S:1,1195:S:0,1195:S:1,1196:S:0,1196:S:1,1197:S:0,1197:S:1,1198:S:0,1198:S:1,1199:S:0,1199:S:1,1200:S:0,1200:S:1,1201:S:0,1201:S:1,1202:S:0,1202:S:1,1203:S:0,1203:S:1,1204:S:0,1204:S:1,1205:S:0,1205:S:1,1206:S:0,1206:S:1,1207:S:0,1207:S:1,1208:S:0,1208:S:1,1209:S:0,1209:S:1,1210:S:0,1210:S:1,1211:S:0,1211:S:1,1212:S:0,1212:S:1,1213:S:0,1213:S:1,1214:S:0,1214:S:1,1215:S:0,1215:S:1,1216:S:0,1216:S:1,1217:S:0,1217:S:1,1218:S:0,1218:S:1,1219:S:0,1219:S:1,1220:S:0,1220:S:1,1221:S:0,1221:S:1,1222:S:0,1222:S:1,1223:S:0,1223:S:1,1224:S:0,1224:S:1,1225:S:0,1225:S:1,1226:S:0,1226:S:1,1227:S:0,1227:S:1,1228:S:0,1228:S:1,1229:S:0,1229:S:1,1230:S:0,1230:S:1,1231:S:0,1231:S:1,1232:S:0,1232:S:1,1233:S:0,1233:S:1,1234:S:0,1234:S:1,1235:S:0,1235:S:1,1236:S:0,1236:S:1,1237:S:0,1237:S:1,1238:S:0,1238:S:1,1239:S:0,1239:S:1,1240:S:0,1240:S:1,1241:S:0,1241:S:1,1242:S:0,1242:S:1,1243:S:0,1243:S:1,1244:S:0,1244:S:1,1245:S:0,1245:S:1,1246:S:0,1246:S:1,1247:S:0,1247:S:1,1248:S:0,1248:S:1,1249:S:0,1249:S:1,1250:S:0,1250:S:1,1251:S:0,1251:S:1,1252:S:0,1252:S:1,1253:S:0,1253:S:1,1254:S:0,1254:S:1,1255:S:0,1255:S:1,1256:S:0,1256:S:1,1257:S:0,1257:S:1,1258:S:0,1258:S:1,1259:S:0,1259:S:1,1260:S:0,1260:S:1,1261:S:0,1261:S:1,1262:S:0,1262:S:1,1263:S:0,1263:S:1,1264:S:0,1264:S:1,1265:S:0,1265:S:1,1266:S:0,1266:S:1,1267:S:0,1267:S:1,1268:S:0,1268:S:1,1269:S:0,1269:S:1,1270:S:0,1270:S:1,1271:S:0,1271:S:1,1272:S:0,1272:S:1,1273:S:0,1273:S:1,1274:S:0,1274:S:1,1275:S:0,1275:S:1,1276:S:0,1276:S:1,1277:S:0,1277:S:1,1278:S:0,1278:S:1,1279:S:0,1279:S:1,1280:S:0,1280:S:1,1281:S:0,1281:S:1,1282:S:0,1282:S:1,1283:S:0,1283:S:1,1284:S:0,1284:S:1,1285:S:0,1285:S:1,1286:S:0,1286:S:1,1287:S:0,1287:S:1,1288:S:0,1288:S:1,1289:S:0,1289:S:1,1290:S:0,1290:S:1,1291:S:0,1291:S:1,1292:S:0,1292:S:1,1293:S:0,1293:S:1,1294:S:0,1294:S:1,1295:S:0,1295:S:1,1296:S:0,1296:S:1,1297:S:0,1297:S:1,1298:S:0,1298:S:1,1299:S:0,1299:S:1,1300:S:0,1300:S:1,1301:S:0,1301:S:1,1302:S:0,1302:S:1,1303:S:0,1303:S:1,1304:S:0,1304:S:1,1305:S:0,1305:S:1,1306:S:0,1306:S:1,1307:S:0,1307:S:1,1308:S:0,1308:S:1,1309:S:0,1309:S:1,1310:S:0,1310:S:1,1311:S:0,1311:S:1,1312:S:0,1312:S:1,1313:S:0,1313:S:1,1314:S:0,1314:S:1,1315:S:0,1315:S:1,1316:S:0,1316:S:1,1317:S:0,1317:S:1,1318:S:0,1318:S:1,1319:S:0,1319:S:1,1320:S:0,1320:S:1,1321:S:0,1321:S:1,1322:S:0,1322:S:1,1323:S:0,1323:S:1,1324:S:0,1324:S:1,1325:S:0,1325:S:1,1326:S:0,1326:S:1,1327:S:0,1327:S:1,1328:S:0,1328:S:1,1329:S:0,1329:S:1,1330:S:0,1330:S:1,1331:S:0,1331:S:1,1332:S:0,1332:S:1,1333:S:0,1333:S:1,1334:S:0,1334:S:1,1335:S:0,1335:S:1,1336:S:0,1336:S:1,1337:S:0,1337:S:1,1338:S:0,1338:S:1,1339:S:0,1339:S:1,1340:S:0,1340:S:1,1341:S:0,1341:S:1,1342:S:0,1342:S:1,1343:S:0,1343:S:1,1344:S:0,1344:S:1,1345:S:0,1345:S:1,1346:S:0,1346:S:1,1347:S:0,1347:S:1,1348:S:0,1348:S:1,1349:S:0,1349:S:1,1350:S:0,1350:S:1,1351:S:0,1351:S:1,1352:S:0,1352:S:1,1353:S:0,1353:S:1,1354:S:0,1354:S:1,1355:S:0,1355:S:1,1356:S:0,1356:S:1,1357:S:0,1357:S:1,1358:S:0,1358:S:1,1359:S:0,1359:S:1,1360:S:0,1360:S:1,1361:S:0,1361:S:1,1362:S:0,1362:S:1,1363:S:0,1363:S:1,1364:S:0,1364:S:1,1365:S:0,1365:S:1,1366:S:0,1366:S:1,1367:S:0,1367:S:1,1368:S:0,1368:S:1,1369:S:0,1369:S:1,1370:S:0,1370:S:1,1371:S:0,1371:S:1,1372:S:0,1372:S:1,1373:S:0,1373:S:1,1374:S:0,1374:S:1,1375:S:0,1375:S:1,1376:S:0,1376:S:1,1377:S:0,1377:S:1,1378:S:0,1378:S:1,1379:S:0,1379:S:1,1380:S:0,1380:S:1,1381:S:0,1381:S:1,1382:S:0,1382:S:1,1383:S:0,1383:S:1,1384:S:0,1384:S:1,1385:S:0,1385:S:1,1386:S:0,1386:S:1,1387:S:0,1387:S:1,1388:S:0,1388:S:1,1389:S:0,1389:S:1,1390:S:0,1390:S:1,1391:S:0,1391:S:1,1392:S:0,1392:S:1,1393:S:0,1393:S:1,1394:S:0,1394:S:1,1395:S:0,1395:S:1,1396:S:0,1396:S:1,1397:S:0,1397:S:1,1398:S:0,1398:S:1,1399:S:0,1399:S:1,1400:S:0,1400:S:1,1401:S:0,1401:S:1,1402:S:0,1402:S:1,1403:S:0,1403:S:1,1404:S:0,1404:S:1,1405:S:0,1405:S:1,1406:S:0,1406:S:1,1407:S:0,1407:S:1,1408:S:0,1408:S:1,1409:S:0,1409:S:1,1410:S:0,1410:S:1,1411:S:0,1411:S:1,1412:S:0,1412:S:1,1413:S:0,1413:S:1,1414:S:0,1414:S:1,1415:S:0,1415:S:1,1416:S:0,1416:S:1,1417:S:0,1417:S:1,1418:S:0,1418:S:1,1419:S:0,1419:S:1,1420:S:0,1420:S:1,1421:S:0,1421:S:1,1422:S:0,1422:S:1,1423:S:0,1423:S:1,1424:S:0,1424:S:1,1425:S:0,1425:S:1,1426:S:0,1426:S:1,1427:S:0,1427:S:1,1428:S:0,1428:S:1,1429:S:0,1429:S:1,1430:S:0,1430:S:1,1431:S:0,1431:S:1,1432:S:0,1432:S:1,1433:S:0,1433:S:1,1434:S:0,1434:S:1,1435:S:0,1435:S:1,1436:S:0,1436:S:1,1437:S:0,1437:S:1,1438:S:0,1438:S:1,1439:S:0,1439:S:1,1440:S:0,1440:S:1,1441:S:0,1441:S:1,1442:S:0,1442:S:1,1443:S:0,1443:S:1,1444:S:0,1444:S:1,1445:S:0,1445:S:1,1446:S:0,1446:S:1,1447:S:0,1447:S:1,1448:S:0,1448:S:1,1449:S:0,1449:S:1,1450:S:0,1450:S:1,1451:S:0,1451:S:1,1452:S:0,1452:S:1,1453:S:0,1453:S:1,1454:S:0,1454:S:1,1455:S:0,1455:S:1,1456:S:0,1456:S:1,1457:S:0,1457:S:1,1458:S:0,1458:S:1,1459:S:0,1459:S:1,1460:S:0,1460:S:1,1461:S:0,1461:S:1,1462:S:0,1462:S:1,1463:S:0,1463:S:1,1464:S:0,1464:S:1,1465:S:0,1465:S:1,1466:S:0,1466:S:1,1467:S:0,1467:S:1,1468:S:0,1468:S:1,1469:S:0,1469:S:1,1470:S:0,1470:S:1,1471:S:0,1471:S:1,1472:S:0,1472:S:1,1473:S:0,1473:S:1,1474:S:0,1474:S:1,1475:S:0,1475:S:1,1476:S:0,1476:S:1,1477:S:0,1477:S:1,1478:S:0,1478:S:1,1479:S:0,1479:S:1,1480:S:0,1480:S:1,1481:S:0,1481:S:1,1482:S:0,1482:S:1,1483:S:0,1483:S:1,1484:S:0,1484:S:1,1485:S:0,1485:S:1,1486:S:0,1486:S:1,1487:S:0,1487:S:1,1488:S:0,1488:S:1,1489:S:0,1489:S:1,1490:S:0,1490:S:1,1491:S:0,1491:S:1,1492:S:0,1492:S:1,1493:S:0,1493:S:1,1494:S:0,1494:S:1,1495:S:0,1495:S:1,1496:S:0,1496:S:1,1497:S:0,1497:S:1,1498:S:0,1498:S:1,1499:S:0,1499:S:1,1500:S:0,1500:S:1,1501:S:0,1501:S:1,1502:S:0,1502:S:1,1503:S:0,1503:S:1,1504:S:0,1504:S:1,1505:S:0,1505:S:1,1506:S:0,1506:S:1,1507:S:0,1507:S:1,1508:S:0,1508:S:1,1509:S:0,1509:S:1,1510:S:0,1510:S:1,1511:S:0,1511:S:1,1512:S:0,1512:S:1,1513:S:0,1513:S:1,1514:S:0,1514:S:1,1515:S:0,1515:S:1,1516:S:0,1516:S:1,1517:S:0,1517:S:1,1518:S:0,1518:S:1,1519:S:0,1519:S:1,1520:S:0,1520:S:1,1521:S:0,1521:S:1,1522:S:0,1522:S:1,1523:S:0,1523:S:1,1524:S:0,1524:S:1,1525:S:0,1525:S:1,1526:S:0,1526:S:1,1527:S:0,1527:S:1,1528:S:0,1528:S:1,1529:S:0,1529:S:1,1530:S:0,1530:S:1,1531:S:0,1531:S:1,1532:S:0,1532:S:1,1533:S:0,1533:S:1,1534:S:0,1534:S:1,1535:S:0,1535:S:1,1536:S:0,1536:S:1,1537:S:0,1537:S:1,1538:S:0,1538:S:1,1539:S:0,1539:S:1,1540:S:0,1540:S:1,1541:S:0,1541:S:1,1542:S:0,1542:S:1,1543:S:0,1543:S:1,1544:S:0,1544:S:1,1545:S:0,1545:S:1,1546:S:0,1546:S:1,1547:S:0,1547:S:1,1548:S:0,1548:S:1,1549:S:0,1549:S:1,1550:S:0,1550:S:1,1551:S:0,1551:S:1,1552:S:0,1552:S:1,1553:S:0,1553:S:1,1554:S:0,1554:S:1,1555:S:0,1555:S:1,1556:S:0,1556:S:1,1557:S:0,1557:S:1,1558:S:0,1558:S:1,1559:S:0,1559:S:1,1560:S:0,1560:S:1,1561:S:0,1561:S:1,1562:S:0,1562:S:1,1563:S:0,1563:S:1,1564:S:0,1564:S:1,1565:S:0,1565:S:1,1566:S:0,1566:S:1,1567:S:0,1567:S:1,1568:S:0,1568:S:1,1569:S:0,1569:S:1,1570:S:0,1570:S:1,1571:S:0,1571:S:1,1572:S:0,1572:S:1,1573:S:0,1573:S:1,1574:S:0,1574:S:1,1575:S:0,1575:S:1,1576:S:0,1576:S:1,1577:S:0,1577:S:1,1578:S:0,1578:S:1,1579:S:0,1579:S:1,1580:S:0,1580:S:1,1581:S:0,1581:S:1,1582:S:0,1582:S:1,1583:S:0,1583:S:1,1584:S:0,1584:S:1,1585:S:0,1585:S:1,1586:S:0,1586:S:1,1587:S:0,1587:S:1,1588:S:0,1588:S:1,1589:S:0,1589:S:1,1590:S:0,1590:S:1,1591:S:0,1591:S:1,1592:S:0,1592:S:1,1593:S:0,1593:S:1,1594:S:0,1594:S:1,1595:S:0,1595:S:1,1596:S:0,1596:S:1,1597:S:0,1597:S:1,1598:S:0,1598:S:1,1599:S:0,1599:S:1,1600:S:0,1600:S:1,1601:S:0,1601:S:1,1602:S:0,1602:S:1,1603:S:0,1603:S:1,1604:S:0,1604:S:1,1605:S:0,1605:S:1,1606:S:0,1606:S:1,1607:S:0,1607:S:1,1608:S:0,1608:S:1,1609:S:0,1609:S:1,1610:S:0,1610:S:1,1611:S:0,1611:S:1,1612:S:0,1612:S:1,1613:S:0,1613:S:1,1614:S:0,1614:S:1,1615:S:0,1615:S:1,1616:S:0,1616:S:1,1617:S:0,1617:S:1,1618:S:0,1618:S:1,1619:S:0,1619:S:1,1620:S:0,1620:S:1,1621:S:0,1621:S:1,1622:S:0,1622:S:1,1623:S:0,1623:S:1,1624:S:0,1624:S:1,1625:S:0,1625:S:1,1626:S:0,1626:S:1,1627:S:0,1627:S:1,1628:S:0,1628:S:1,1629:S:0,1629:S:1,1630:S:0,1630:S:1,1631:S:0,1631:S:1,1632:S:0,1632:S:1,1633:S:0,1633:S:1,1634:S:0,1634:S:1,1635:S:0,1635:S:1,1636:S:0,1636:S:1,1637:S:0,1637:S:1,1638:S:0,1638:S:1,1639:S:0,1639:S:1,1640:S:0,1640:S:1,1641:S:0,1641:S:1,1642:S:0,1642:S:1,1643:S:0,1643:S:1,1644:S:0,1644:S:1,1645:S:0,1645:S:1,1646:S:0,1646:S:1,1647:S:0,1647:S:1,1648:S:0,1648:S:1,1649:S:0,1649:S:1,1650:S:0,1650:S:1,1651:S:0,1651:S:1,1652:S:0,1652:S:1,1653:S:0,1653:S:1,1654:S:0,1654:S:1,1655:S:0,1655:S:1,1656:S:0,1656:S:1,1657:S:0,1657:S:1,1658:S:0,1658:S:1,1659:S:0,1659:S:1,1660:S:0,1660:S:1,1661:S:0,1661:S:1,1662:S:0,1662:S:1,1663:S:0,1663:S:1,1664:S:0,1664:S:1,1665:S:0,1665:S:1,1666:S:0,1666:S:1,1667:S:0,1667:S:1,1668:S:0,1668:S:1,1669:S:0,1669:S:1,1670:S:0,1670:S:1,1671:S:0,1671:S:1,1672:S:0,1672:S:1,1673:S:0,1673:S:1,1674:S:0,1674:S:1,1675:S:0,1675:S:1,1676:S:0,1676:S:1,1677:S:0,1677:S:1,1678:S:0,1678:S:1,1679:S:0,1679:S:1,1680:S:0,1680:S:1,1681:S:0,1681:S:1,1682:S:0,1682:S:1,1683:S:0,1683:S:1,1684:S:0,1684:S:1,1685:S:0,1685:S:1,1686:S:0,1686:S:1,1687:S:0,1687:S:1,1688:S:0,1688:S:1,1689:S:0,1689:S:1,1690:S:0,1690:S:1,1691:S:0,1691:S:1,1692:S:0,1692:S:1,1693:S:0,1693:S:1,1694:S:0,1694:S:1,1695:S:0,1695:S:1,1696:S:0,1696:S:1,1697:S:0,1697:S:1,1698:S:0,1698:S:1,1699:S:0,1699:S:1,1700:S:0,1700:S:1,1701:S:0,1701:S:1,1702:S:0,1702:S:1,1703:S:0,1703:S:1,1704:S:0,1704:S:1,1705:S:0,1705:S:1,1706:S:0,1706:S:1,1707:S:0,1707:S:1,1708:S:0,1708:S:1,1709:S:0,1709:S:1,1710:S:0,1710:S:1,1711:S:0,1711:S:1,1712:S:0,1712:S:1,1713:S:0,1713:S:1,1714:S:0,1714:S:1,1715:S:0,1715:S:1,1716:S:0,1716:S:1,1717:S:0,1717:S:1,1718:S:0,1718:S:1,1719:S:0,1719:S:1,1720:S:0,1720:S:1,1721:S:0,1721:S:1,1722:S:0,1722:S:1,1723:S:0,1723:S:1,1724:S:0,1724:S:1,1725:S:0,1725:S:1,1726:S:0,1726:S:1,1727:S:0,1727:S:1,1728:S:0,1728:S:1,1729:S:0,1729:S:1,1730:S:0,1730:S:1,1731:S:0,1731:S:1,1732:S:0,1732:S:1,1733:S:0,1733:S:1,1734:S:0,1734:S:1,1735:S:0,1735:S:1,1736:S:0,1736:S:1,1737:S:0,1737:S:1,1738:S:0,1738:S:1,1739:S:0,1739:S:1,1740:S:0,1740:S:1,1741:S:0,1741:S:1,1742:S:0,1742:S:1,1743:S:0,1743:S:1,1744:S:0,1744:S:1,1745:S:0,1745:S:1,1746:S:0,1746:S:1,1747:S:0,1747:S:1,1748:S:0,1748:S:1,1749:S:0,1749:S:1,1750:S:0,1750:S:1,1751:S:0,1751:S:1,1752:S:0,1752:S:1,1753:S:0,1753:S:1,1754:S:0,1754:S:1,1755:S:0,1755:S:1,1756:S:0,1756:S:1,1757:S:0,1757:S:1,1758:S:0,1758:S:1,1759:S:0,1759:S:1,1760:S:0,1760:S:1,1761:S:0,1761:S:1,1762:S:0,1762:S:1,1763:S:0,1763:S:1,1764:S:0,1764:S:1,1765:S:0,1765:S:1,1766:S:0,1766:S:1,1767:S:0,1767:S:1,1768:S:0,1768:S:1,1769:S:0,1769:S:1,1770:S:0,1770:S:1,1771:S:0,1771:S:1,1772:S:0,1772:S:1,1773:S:0,1773:S:1,1774:S:0,1774:S:1,1775:S:0,1775:S:1,1776:S:0,1776:S:1,1777:S:0,1777:S:1,1778:S:0,1778:S:1,1779:S:0,1779:S:1,1780:S:0,1780:S:1,1781:S:0,1781:S:1,1782:S:0,1782:S:1,1783:S:0,1783:S:1,1784:S:0,1784:S:1,1785:S:0,1785:S:1,1786:S:0,1786:S:1,1787:S:0,1787:S:1,1788:S:0,1788:S:1,1789:S:0,1789:S:1,1790:S:0,1790:S:1,1791:S:0,1791:S:1,1792:S:0,1792:S:1,1793:S:0,1793:S:1,1794:S:0,1794:S:1,1795:S:0,1795:S:1,1796:S:0,1796:S:1,1797:S:0,1797:S:1,1798:S:0,1798:S:1,1799:S:0,1799:S:1,1800:S:0,1800:S:1,1801:S:0,1801:S:1,1802:S:0,1802:S:1,1803:S:0,1803:S:1,1804:S:0,1804:S:1,1805:S:0,1805:S:1,1806:S:0,1806:S:1,1807:S:0,1807:S:1,1808:S:0,1808:S:1,1809:S:0,1809:S:1,1810:S:0,1810:S:1,1811:S:0,1811:S:1,1812:S:0,1812:S:1,1813:S:0,1813:S:1,1814:S:0,1814:S:1,1815:S:0,1815:S:1,1816:S:0,1816:S:1,1817:S:0,1817:S:1,1818:S:0,1818:S:1,1819:S:0,1819:S:1,1820:S:0,1820:S:1,1821:S:0,1821:S:1,1822:S:0,1822:S:1,1823:S:0,1823:S:1,1824:S:0,1824:S:1,1825:S:0,1825:S:1,1826:S:0,1826:S:1,1827:S:0,1827:S:1,1828:S:0,1828:S:1,1829:S:0,1829:S:1,1830:S:0,1830:S:1,1831:S:0,1831:S:1,1832:S:0,1832:S:1,1833:S:0,1833:S:1,1834:S:0,1834:S:1,1835:S:0,1835:S:1,1836:S:0,1836:S:1,1837:S:0,1837:S:1,1838:S:0,1838:S:1,1839:S:0,1839:S:1,1840:S:0,1840:S:1,1841:S:0,1841:S:1,1842:S:0,1842:S:1,1843:S:0,1843:S:1,1844:S:0,1844:S:1,1845:S:0,1845:S:1,1846:S:0,1846:S:1,1847:S:0,1847:S:1,1848:S:0,1848:S:1,1849:S:0,1849:S:1,1850:S:0,1850:S:1,1851:S:0,1851:S:1,1852:S:0,1852:S:1,1853:S:0,1853:S:1,1854:S:0,1854:S:1,1855:S:0,1855:S:1,1856:S:0,1856:S:1,1857:S:0,1857:S:1,1858:S:0,1858:S:1,1859:S:0,1859:S:1,1860:S:0,1860:S:1,1861:S:0,1861:S:1,1862:S:0,1862:S:1,1863:S:0,1863:S:1,1864:S:0,1864:S:1,1865:S:0,1865:S:1,1866:S:0,1866:S:1,1867:S:0,1867:S:1,1868:S:0,1868:S:1,1869:S:0,1869:S:1,1870:S:0,1870:S:1,1871:S:0,1871:S:1,1872:S:0,1872:S:1,1873:S:0,1873:S:1,1874:S:0,1874:S:1,1875:S:0,1875:S:1,1876:S:0,1876:S:1,1877:S:0,1877:S:1,1878:S:0,1878:S:1,1879:S:0,1879:S:1,1880:S:0,1880:S:1,1881:S:0,1881:S:1,1882:S:0,1882:S:1,1883:S:0,1883:S:1,1884:S:0,1884:S:1,1885:S:0,1885:S:1,1886:S:0,1886:S:1,1887:S:0,1887:S:1,1888:S:0,1888:S:1,1889:S:0,1889:S:1,1890:S:0,1890:S:1,1891:S:0,1891:S:1,1892:S:0,1892:S:1,1893:S:0,1893:S:1,1894:S:0,1894:S:1,1895:S:0,1895:S:1,1896:S:0,1896:S:1,1897:S:0,1897:S:1,1898:S:0,1898:S:1,1899:S:0,1899:S:1,1900:S:0,1900:S:1,1901:S:0,1901:S:1,1902:S:0,1902:S:1,1903:S:0,1903:S:1,1904:S:0,1904:S:1,1905:S:0,1905:S:1,1906:S:0,1906:S:1,1907:S:0,1907:S:1,1908:S:0,1908:S:1,1909:S:0,1909:S:1,1910:S:0,1910:S:1,1911:S:0,1911:S:1,1912:S:0,1912:S:1,1913:S:0,1913:S:1,1914:S:0,1914:S:1,1915:S:0,1915:S:1,1916:S:0,1916:S:1,1917:S:0,1917:S:1,1918:S:0,1918:S:1,1919:S:0,1919:S:1,1920:S:0,1920:S:1,1921:S:0,1921:S:1,1922:S:0,1922:S:1,1923:S:0,1923:S:1,1924:S:0,1924:S:1,1925:S:0,1925:S:1,1926:S:0,1926:S:1,1927:S:0,1927:S:1,1928:S:0,1928:S:1,1929:S:0,1929:S:1,1930:S:0,1930:S:1,1931:S:0,1931:S:1,1932:S:0,1932:S:1,1933:S:0,1933:S:1,1934:S:0,1934:S:1,1935:S:0,1935:S:1,1936:S:0,1936:S:1,1937:S:0,1937:S:1,1938:S:0,1938:S:1,1939:S:0,1939:S:1,1940:S:0,1940:S:1,1941:S:0,1941:S:1,1942:S:0,1942:S:1,1943:S:0,1943:S:1,1944:S:0,1944:S:1,1945:S:0,1945:S:1,1946:S:0,1946:S:1,1947:S:0,1947:S:1,1948:S:0,1948:S:1,1949:S:0,1949:S:1,1950:S:0,1950:S:1,1951:S:0,1951:S:1,1952:S:0,1952:S:1,1953:S:0,1953:S:1,1954:S:0,1954:S:1,1955:S:0,1955:S:1,1956:S:0,1956:S:1,1957:S:0,1957:S:1,1958:S:0,1958:S:1,1959:S:0,1959:S:1,1960:S:0,1960:S:1,1961:S:0,1961:S:1,1962:S:0,1962:S:1,1963:S:0,1963:S:1,1964:S:0,1964:S:1,1965:S:0,1965:S:1,1966:S:0,1966:S:1,1967:S:0,1967:S:1,1968:S:0,1968:S:1,1969:S:0,1969:S:1,1970:S:0,1970:S:1,1971:S:0,1971:S:1,1972:S:0,1972:S:1,1973:S:0,1973:S:1,1974:S:0,1974:S:1,1975:S:0,1975:S:1,1976:S:0,1976:S:1,1977:S:0,1977:S:1,1978:S:0,1978:S:1,1979:S:0,1979:S:1,1980:S:0,1980:S:1,1981:S:0,1981:S:1,1982:S:0,1982:S:1,1983:S:0,1983:S:1,1984:S:0,1984:S:1,1985:S:0,1985:S:1,1986:S:0,1986:S:1,1987:S:0,1987:S:1,1988:S:0,1988:S:1,1989:S:0,1989:S:1,1990:S:0,1990:S:1,1991:S:0,1991:S:1,1992:S:0,1992:S:1,1993:S:0,1993:S:1,1994:S:0,1994:S:1,1995:S:0,1995:S:1,1996:S:0,1996:S:1,1997:S:0,1997:S:1,1998:S:0,1998:S:1,1999:S:0,1999:S:1,2000:S:0,2000:S:1,2001:S:0,2001:S:1,2002:S:0,2002:S:1,2003:S:0,2003:S:1,2004:S:0,2004:S:1,2005:S:0,2005:S:1,2006:S:0,2006:S:1,2007:S:0,2007:S:1,2008:S:0,2008:S:1,2009:S:0,2009:S:1,2010:S:0,2010:S:1,2011:S:0,2011:S:1,2012:S:0,2012:S:1,2013:S:0,2013:S:1,2014:S:0,2014:S:1,2015:S:0,2015:S:1,2016:S:0,2016:S:1,2017:S:0,2017:S:1,2018:S:0,2018:S:1,2019:S:0,2019:S:1,2020:S:0,2020:S:1,2021:S:0,2021:S:1,2022:S:0,2022:S:1,2023:S:0,2023:S:1,2024:S:0,2024:S:1,2025:S:0,2025:S:1,2026:S:0,2026:S:1,2027:S:0,2027:S:1,2028:S:0,2028:S:1,2029:S:0,2029:S:1,2030:S:0,2030:S:1,2031:S:0,2031:S:1,2032:S:0,2032:S:1,2033:S:0,2033:S:1,2034:S:0,2034:S:1,2035:S:0,2035:S:1,2036:S:0,2036:S:1,2037:S:0,2037:S:1,2038:S:0,2038:S:1,2039:S:0,2039:S:1,2040:S:0,2040:S:1,2041:S:0,2041:S:1,2042:S:0,2042:S:1,2043:S:0,2043:S:1,2044:S:0,2044:S:1,2045:S:0,2045:S:1,2046:S:0,2046:S:1,2047:S:0,2047:S:1,2048:S:0,2048:S:1,2049:S:0,2049:S:1,2050:S:0,2050:S:1,2051:S:0,2051:S:1,2052:S:0,2052:S:1,2053:S:0,2053:S:1,2054:S:0,2054:S:1,2055:S:0,2055:S:1,2056:S:0,2056:S:1,2057:S:0,2057:S:1,2058:S:0,2058:S:1,2059:S:0,2059:S:1,2060:S:0,2060:S:1,2061:S:0,2061:S:1,2062:S:0,2062:S:1,2063:S:0,2063:S:1,2064:S:0,2064:S:1,2065:S:0,2065:S:1,2066:S:0,2066:S:1,2067:S:0,2067:S:1,2068:S:0,2068:S:1,2069:S:0,2069:S:1,2070:S:0,2070:S:1,2071:S:0,2071:S:1,2072:S:0,2072:S:1,2073:S:0,2073:S:1,2074:S:0,2074:S:1,2075:S:0,2075:S:1,2076:S:0,2076:S:1,2077:S:0,2077:S:1,2078:S:0,2078:S:1,2079:S:0,2079:S:1,2080:S:0,2080:S:1,2081:S:0,2081:S:1,2082:S:0,2082:S:1,2083:S:0,2083:S:1,2084:S:0,2084:S:1,2085:S:0,2085:S:1,2086:S:0,2086:S:1,2087:S:0,2087:S:1,2088:S:0,2088:S:1,2089:S:0,2089:S:1,2090:S:0,2090:S:1,2091:S:0,2091:S:1,2092:S:0,2092:S:1,2093:S:0,2093:S:1,2094:S:0,2094:S:1,2095:S:0,2095:S:1,2096:S:0,2096:S:1,2097:S:0,2097:S:1,2098:S:0,2098:S:1,2099:S:0,2099:S:1,2100:S:0,2100:S:1,2101:S:0,2101:S:1,2102:S:0,2102:S:1,2103:S:0,2103:S:1,2104:S:0,2104:S:1,2105:S:0,2105:S:1,2106:S:0,2106:S:1,2107:S:0,2107:S:1,2108:S:0,2108:S:1,2109:S:0,2109:S:1,2110:S:0,2110:S:1,2111:S:0,2111:S:1,2112:S:0,2112:S:1,2113:S:0,2113:S:1,2114:S:0,2114:S:1,2115:S:0,2115:S:1,2116:S:0,2116:S:1,2117:S:0,2117:S:1,2118:S:0,2118:S:1,2119:S:0,2119:S:1,2120:S:0,2120:S:1,2121:S:0,2121:S:1,2122:S:0,2122:S:1,2123:S:0,2123:S:1,2124:S:0,2124:S:1,2125:S:0,2125:S:1,2126:S:0,2126:S:1,2127:S:0,2127:S:1,2128:S:0,2128:S:1,2129:S:0,2129:S:1,2130:S:0,2130:S:1,2131:S:0,2131:S:1,2132:S:0,2132:S:1,2133:S:0,2133:S:1,2134:S:0,2134:S:1,2135:S:0,2135:S:1,2136:S:0,2136:S:1,2137:S:0,2137:S:1,2138:S:0,2138:S:1,2139:S:0,2139:S:1,2140:S:0,2140:S:1,2141:S:0,2141:S:1,2142:S:0,2142:S:1,2143:S:0,2143:S:1,2144:S:0,2144:S:1,2145:S:0,2145:S:1,2146:S:0,2146:S:1,2147:S:0,2147:S:1,2148:S:0,2148:S:1,2149:S:0,2149:S:1,2150:S:0,2150:S:1,2151:S:0,2151:S:1,2152:S:0,2152:S:1,2153:S:0,2153:S:1,2154:S:0,2154:S:1,2155:S:0,2155:S:1,2156:S:0,2156:S:1,2157:S:0,2157:S:1,2158:S:0,2158:S:1,2159:S:0,2159:S:1,2160:S:0,2160:S:1,2161:S:0,2161:S:1,2162:S:0,2162:S:1,2163:S:0,2163:S:1,2164:S:0,2164:S:1,2165:S:0,2165:S:1,2166:S:0,2166:S:1,2167:S:0,2167:S:1,2168:S:0,2168:S:1,2169:S:0,2169:S:1,2170:S:0,2170:S:1,2171:S:0,2171:S:1,2172:S:0,2172:S:1,2173:S:0,2173:S:1,2174:S:0,2174:S:1,2175:S:0,2175:S:1,2176:S:0,2176:S:1,2177:S:0,2177:S:1,2178:S:0,2178:S:1,2179:S:0,2179:S:1,2180:S:0,2180:S:1,2181:S:0,2181:S:1,2182:S:0,2182:S:1,2183:S:0,2183:S:1,2184:S:0,2184:S:1,2185:S:0,2185:S:1,2186:S:0,2186:S:1,2187:S:0,2187:S:1,2188:S:0,2188:S:1,2189:S:0,2189:S:1,2190:S:0,2190:S:1,2191:S:0,2191:S:1,2192:S:0,2192:S:1,2193:S:0,2193:S:1,2194:S:0,2194:S:1,2195:S:0,2195:S:1,2196:S:0,2196:S:1,2197:S:0,2197:S:1,2198:S:0,2198:S:1,2199:S:0,2199:S:1,2200:S:0,2200:S:1,2201:S:0,2201:S:1,2202:S:0,2202:S:1,2203:S:0,2203:S:1,2204:S:0,2204:S:1,2205:S:0,2205:S:1,2206:S:0,2206:S:1,2207:S:0,2207:S:1,2208:S:0,2208:S:1,2209:S:0,2209:S:1,2210:S:0,2210:S:1,2211:S:0,2211:S:1,2212:S:0,2212:S:1,2213:S:0,2213:S:1,2214:S:0,2214:S:1,2215:S:0,2215:S:1,2216:S:0,2216:S:1,2217:S:0,2217:S:1,2218:S:0,2218:S:1,2219:S:0,2219:S:1,2220:S:0,2220:S:1,2221:S:0,2221:S:1,2222:S:0,2222:S:1,2223:S:0,2223:S:1,2224:S:0,2224:S:1,2225:S:0,2225:S:1,2226:S:0,2226:S:1,2227:S:0,2227:S:1,2228:S:0,2228:S:1,2229:S:0,2229:S:1,2230:S:0,2230:S:1,2231:S:0,2231:S:1,2232:S:0,2232:S:1,2233:S:0,2233:S:1,2234:S:0,2234:S:1,2235:S:0,2235:S:1,2236:S:0,2236:S:1,2237:S:0,2237:S:1,2238:S:0,2238:S:1,2239:S:0,2239:S:1,2240:S:0,2240:S:1,2241:S:0,2241:S:1,2242:S:0,2242:S:1,2243:S:0,2243:S:1,2244:S:0,2244:S:1,2245:S:0,2245:S:1,2246:S:0,2246:S:1,2247:S:0,2247:S:1,2248:S:0,2248:S:1,2249:S:0,2249:S:1,2250:S:0,2250:S:1,2251:S:0,2251:S:1,2252:S:0,2252:S:1,2253:S:0,2253:S:1,2254:S:0,2254:S:1,2255:S:0,2255:S:1,2256:S:0,2256:S:1,2257:S:0,2257:S:1,2258:S:0,2258:S:1,2259:S:0,2259:S:1,2260:S:0,2260:S:1,2261:S:0,2261:S:1,2262:S:0,2262:S:1,2263:S:0,2263:S:1,2264:S:0,2264:S:1,2265:S:0,2265:S:1,2266:S:0,2266:S:1,2267:S:0,2267:S:1,2268:S:0,2268:S:1,2269:S:0,2269:S:1,2270:S:0,2270:S:1,2271:S:0,2271:S:1,2272:S:0,2272:S:1,2273:S:0,2273:S:1,2274:S:0,2274:S:1,2275:S:0,2275:S:1,2276:S:0,2276:S:1,2277:S:0,2277:S:1,2278:S:0,2278:S:1,2279:S:0,2279:S:1,2280:S:0,2280:S:1,2281:S:0,2281:S:1,2282:S:0,2282:S:1,2283:S:0,2283:S:1,2284:S:0,2284:S:1,2285:S:0,2285:S:1,2286:S:0,2286:S:1,2287:S:0,2287:S:1,2288:S:0,2288:S:1,2289:S:0,2289:S:1,2290:S:0,2290:S:1,2291:S:0,2291:S:1,2292:S:0,2292:S:1,2293:S:0,2293:S:1,2294:S:0,2294:S:1,2295:S:0,2295:S:1,2296:S:0,2296:S:1,2297:S:0,2297:S:1,2298:S:0,2298:S:1,2299:S:0,2299:S:1,2300:S:0,2300:S:1,2301:S:0,2301:S:1,2302:S:0,2302:S:1,2303:S:0,2303:S:1,2304:S:0,2304:S:1,2305:S:0,2305:S:1,2306:S:0,2306:S:1,2307:S:0,2307:S:1,2308:S:0,2308:S:1,2309:S:0,2309:S:1,2310:S:0,2310:S:1,2311:S:0,2311:S:1,2312:S:0,2312:S:1,2313:S:0,2313:S:1,2314:S:0,2314:S:1,2315:S:0,2315:S:1,2316:S:0,2316:S:1,2317:S:0,2317:S:1,2318:S:0,2318:S:1,2319:S:0,2319:S:1,2320:S:0,2320:S:1,2321:S:0,2321:S:1,2322:S:0,2322:S:1,2323:S:0,2323:S:1,2324:S:0,2324:S:1,2325:S:0,2325:S:1,2326:S:0,2326:S:1,2327:S:0,2327:S:1,2328:S:0,2328:S:1,2329:S:0,2329:S:1,2330:S:0,2330:S:1,2331:S:0,2331:S:1,2332:S:0,2332:S:1,2333:S:0,2333:S:1,2334:S:0,2334:S:1,2335:S:0,2335:S:1,2336:S:0,2336:S:1,2337:S:0,2337:S:1,2338:S:0,2338:S:1,2339:S:0,2339:S:1,2340:S:0,2340:S:1,2341:S:0,2341:S:1,2342:S:0,2342:S:1,2343:S:0,2343:S:1,2344:S:0,2344:S:1,2345:S:0,2345:S:1,2346:S:0,2346:S:1,2347:S:0,2347:S:1,2348:S:0,2348:S:1,2349:S:0,2349:S:1,2350:S:0,2350:S:1,2351:S:0,2351:S:1,2352:S:0,2352:S:1,2353:S:0,2353:S:1,2354:S:0,2354:S:1,2355:S:0,2355:S:1,2356:S:0,2356:S:1,2357:S:0,2357:S:1,2358:S:0,2358:S:1,2359:S:0,2359:S:1,2360:S:0,2360:S:1,2361:S:0,2361:S:1,2362:S:0,2362:S:1,2363:S:0,2363:S:1,2364:S:0,2364:S:1,2365:S:0,2365:S:1,2366:S:0,2366:S:1,2367:S:0,2367:S:1,2368:S:0,2368:S:1,2369:S:0,2369:S:1,2370:S:0,2370:S:1,2371:S:0,2371:S:1,2372:S:0,2372:S:1,2373:S:0,2373:S:1,2374:S:0,2374:S:1,2375:S:0,2375:S:1,2376:S:0,2376:S:1,2377:S:0,2377:S:1,2378:S:0,2378:S:1,2379:S:0,2379:S:1,2380:S:0,2380:S:1,2381:S:0,2381:S:1,2382:S:0,2382:S:1,2383:S:0,2383:S:1,2384:S:0,2384:S:1,2385:S:0,2385:S:1,2386:S:0,2386:S:1,2387:S:0,2387:S:1,2388:S:0,2388:S:1,2389:S:0,2389:S:1,2390:S:0,2390:S:1,2391:S:0,2391:S:1,2392:S:0,2392:S:1,2393:S:0,2393:S:1,2394:S:0,2394:S:1,2395:S:0,2395:S:1,2396:S:0,2396:S:1,2397:S:0,2397:S:1,2398:S:0,2398:S:1,2399:S:0,2399:S:1,2400:S:0,2400:S:1,2401:S:0,2401:S:1,2402:S:0,2402:S:1,2403:S:0,2403:S:1,2404:S:0,2404:S:1,2405:S:0,2405:S:1,2406:S:0,2406:S:1,2407:S:0,2407:S:1,2408:S:0,2408:S:1,2409:S:0,2409:S:1,2410:S:0,2410:S:1,2411:S:0,2411:S:1,2412:S:0,2412:S:1,2413:S:0,2413:S:1,2414:S:0,2414:S:1,2415:S:0,2415:S:1,2416:S:0,2416:S:1,2417:S:0,2417:S:1,2418:S:0,2418:S:1,2419:S:0,2419:S:1,2420:S:0,2420:S:1,2421:S:0,2421:S:1,2422:S:0,2422:S:1,2423:S:0,2423:S:1,2424:S:0,2424:S:1,2425:S:0,2425:S:1,2426:S:0,2426:S:1,2427:S:0,2427:S:1,2428:S:0,2428:S:1,2429:S:0,2429:S:1,2430:S:0,2430:S:1,2431:S:0,2431:S:1,2432:S:0,2432:S:1,2433:S:0,2433:S:1,2434:S:0,2434:S:1,2435:S:0,2435:S:1,2436:S:0,2436:S:1,2437:S:0,2437:S:1,2438:S:0,2438:S:1,2439:S:0,2439:S:1,2440:S:0,2440:S:1,2441:S:0,2441:S:1,2442:S:0,2442:S:1,2443:S:0,2443:S:1,2444:S:0,2444:S:1,2445:S:0,2445:S:1,2446:S:0,2446:S:1,2447:S:0,2447:S:1,2448:S:0,2448:S:1,2449:S:0,2449:S:1,2450:S:0,2450:S:1,2451:S:0,2451:S:1,2452:S:0,2452:S:1,2453:S:0,2453:S:1,2454:S:0,2454:S:1,2455:S:0,2455:S:1,2456:S:0,2456:S:1,2457:S:0,2457:S:1,2458:S:0,2458:S:1,2459:S:0,2459:S:1,2460:S:0,2460:S:1,2461:S:0,2461:S:1,2462:S:0,2462:S:1,2463:S:0,2463:S:1,2464:S:0,2464:S:1,2465:S:0,2465:S:1,2466:S:0,2466:S:1,2467:S:0,2467:S:1,2468:S:0,2468:S:1,2469:S:0,2469:S:1,2470:S:0,2470:S:1,2471:S:0,2471:S:1,2472:S:0,2472:S:1,2473:S:0,2473:S:1,2474:S:0,2474:S:1,2475:S:0,2475:S:1,2476:S:0,2476:S:1,2477:S:0,2477:S:1,2478:S:0,2478:S:1,2479:S:0,2479:S:1,2480:S:0,2480:S:1,2481:S:0,2481:S:1,2482:S:0,2482:S:1,2483:S:0,2483:S:1,2484:S:0,2484:S:1,2485:S:0,2485:S:1,2486:S:0,2486:S:1,2487:S:0,2487:S:1,2488:S:0,2488:S:1,2489:S:0,2489:S:1,2490:S:0,2490:S:1,2491:S:0,2491:S:1,2492:S:0,2492:S:1,2493:S:0,2493:S:1,2494:S:0,2494:S:1,2495:S:0,2495:S:1,2496:S:0,2496:S:1,2497:S:0,2497:S:1,2498:S:0,2498:S:1,2499:S:0,2499:S:1,2500:S:0,2500:S:1,2501:S:0,2501:S:1,2502:S:0,2502:S:1,2503:S:0,2503:S:1,2504:S:0,2504:S:1,2505:S:0,2505:S:1,2506:S:0,2506:S:1,2507:S:0,2507:S:1,2508:S:0,2508:S:1,2509:S:0,2509:S:1,2510:S:0,2510:S:1,2511:S:0,2511:S:1,2512:S:0,2512:S:1,2513:S:0,2513:S:1,2514:S:0,2514:S:1,2515:S:0,2515:S:1,2516:S:0,2516:S:1,2517:S:0,2517:S:1,2518:S:0,2518:S:1,2519:S:0,2519:S:1,2520:S:0,2520:S:1,2521:S:0,2521:S:1,2522:S:0,2522:S:1,2523:S:0,2523:S:1,2524:S:0,2524:S:1,2525:S:0,2525:S:1,2526:S:0,2526:S:1,2527:S:0,2527:S:1,2528:S:0,2528:S:1,2529:S:0,2529:S:1,2530:S:0,2530:S:1,2531:S:0,2531:S:1,2532:S:0,2532:S:1,2533:S:0,2533:S:1,2534:S:0,2534:S:1,2535:S:0,2535:S:1,2536:S:0,2536:S:1,2537:S:0,2537:S:1,2538:S:0,2538:S:1,2539:S:0,2539:S:1,2540:S:0,2540:S:1,2541:S:0,2541:S:1,2542:S:0,2542:S:1,2543:S:0,2543:S:1,2544:S:0,2544:S:1,2545:S:0,2545:S:1,2546:S:0,2546:S:1,2547:S:0,2547:S:1,2548:S:0,2548:S:1,2549:S:0,2549:S:1,2550:S:0,2550:S:1,2551:S:0,2551:S:1,2552:S:0,2552:S:1,2553:S:0,2553:S:1,2554:S:0,2554:S:1,2555:S:0,2555:S:1,2556:S:0,2556:S:1,2557:S:0,2557:S:1,2558:S:0,2558:S:1,2559:S:0,2559:S:1,2560:S:0,2560:S:1,2561:S:0,2561:S:1,2562:S:0,2562:S:1,2563:S:0,2563:S:1,2564:S:0,2564:S:1,2565:S:0,2565:S:1,2566:S:0,2566:S:1,2567:S:0,2567:S:1,2568:S:0,2568:S:1,2569:S:0,2569:S:1,2570:S:0,2570:S:1,2571:S:0,2571:S:1,2572:S:0,2572:S:1,2573:S:0,2573:S:1,2574:S:0,2574:S:1,2575:S:0,2575:S:1,2576:S:0,2576:S:1,2577:S:0,2577:S:1,2578:S:0,2578:S:1,2579:S:0,2579:S:1,2580:S:0,2580:S:1,2581:S:0,2581:S:1,2582:S:0,2582:S:1,2583:S:0,2583:S:1,2584:S:0,2584:S:1,2585:S:0,2585:S:1,2586:S:0,2586:S:1,2587:S:0,2587:S:1,2588:S:0,2588:S:1,2589:S:0,2589:S:1,2590:S:0,2590:S:1,2591:S:0,2591:S:1,2592:S:0,2592:S:1,2593:S:0,2593:S:1,2594:S:0,2594:S:1,2595:S:0,2595:S:1,2596:S:0,2596:S:1,2597:S:0,2597:S:1,2598:S:0,2598:S:1,2599:S:0,2599:S:1,2600:S:0,2600:S:1,2601:S:0,2601:S:1,2602:S:0,2602:S:1,2603:S:0,2603:S:1,2604:S:0,2604:S:1,2605:S:0,2605:S:1,2606:S:0,2606:S:1,2607:S:0,2607:S:1,2608:S:0,2608:S:1,2609:S:0,2609:S:1,2610:S:0,2610:S:1,2611:S:0,2611:S:1,2612:S:0,2612:S:1,2613:S:0,2613:S:1,2614:S:0,2614:S:1,2615:S:0,2615:S:1,2616:S:0,2616:S:1,2617:S:0,2617:S:1,2618:S:0,2618:S:1,2619:S:0,2619:S:1,2620:S:0,2620:S:1,2621:S:0,2621:S:1,2622:S:0,2622:S:1,2623:S:0,2623:S:1,2624:S:0,2624:S:1,2625:S:0,2625:S:1,2626:S:0,2626:S:1,2627:S:0,2627:S:1,2628:S:0,2628:S:1,2629:S:0,2629:S:1,2630:S:0,2630:S:1,2631:S:0,2631:S:1,2632:S:0,2632:S:1,2633:S:0,2633:S:1,2634:S:0,2634:S:1,2635:S:0,2635:S:1,2636:S:0,2636:S:1,2637:S:0,2637:S:1,2638:S:0,2638:S:1,2639:S:0,2639:S:1,2640:S:0,2640:S:1,2641:S:0,2641:S:1,2642:S:0,2642:S:1,2643:S:0,2643:S:1,2644:S:0,2644:S:1,2645:S:0,2645:S:1,2646:S:0,2646:S:1,2647:S:0,2647:S:1,2648:S:0,2648:S:1,2649:S:0,2649:S:1,2650:S:0,2650:S:1,2651:S:0,2651:S:1,2652:S:0,2652:S:1,2653:S:0,2653:S:1,2654:S:0,2654:S:1,2655:S:0,2655:S:1,2656:S:0,2656:S:1,2657:S:0,2657:S:1,2658:S:0,2658:S:1,2659:S:0,2659:S:1,2660:S:0,2660:S:1,2661:S:0,2661:S:1,2662:S:0,2662:S:1,2663:S:0,2663:S:1,2664:S:0,2664:S:1,2665:S:0,2665:S:1,2666:S:0,2666:S:1,2667:S:0,2667:S:1,2668:S:0,2668:S:1,2669:S:0,2669:S:1,2670:S:0,2670:S:1,2671:S:0,2671:S:1,2672:S:0,2672:S:1,2673:S:0,2673:S:1,2674:S:0,2674:S:1,2675:S:0,2675:S:1,2676:S:0,2676:S:1,2677:S:0,2677:S:1,2678:S:0,2678:S:1,2679:S:0,2679:S:1,2680:S:0,2680:S:1,2681:S:0,2681:S:1,2682:S:0,2682:S:1,2683:S:0,2683:S:1,2684:S:0,2684:S:1,2685:S:0,2685:S:1,2686:S:0,2686:S:1,2687:S:0,2687:S:1,2688:S:0,2688:S:1,2689:S:0,2689:S:1,2690:S:0,2690:S:1,2691:S:0,2691:S:1,2692:S:0,2692:S:1,2693:S:0,2693:S:1,2694:S:0,2694:S:1,2695:S:0,2695:S:1,2696:S:0,2696:S:1,2697:S:0,2697:S:1,2698:S:0,2698:S:1,2699:S:0,2699:S:1,2700:S:0,2700:S:1,2701:S:0,2701:S:1,2702:S:0,2702:S:1,2703:S:0,2703:S:1,2704:S:0,2704:S:1,2705:S:0,2705:S:1,2706:S:0,2706:S:1,2707:S:0,2707:S:1,2708:S:0,2708:S:1,2709:S:0,2709:S:1,2710:S:0,2710:S:1,2711:S:0,2711:S:1,2712:S:0,2712:S:1,2713:S:0,2713:S:1,2714:S:0,2714:S:1,2715:S:0,2715:S:1,2716:S:0,2716:S:1,2717:S:0,2717:S:1,2718:S:0,2718:S:1,2719:S:0,2719:S:1,2720:S:0,2720:S:1,2721:S:0,2721:S:1,2722:S:0,2722:S:1,2723:S:0,2723:S:1,2724:S:0,2724:S:1,2725:S:0,2725:S:1,2726:S:0,2726:S:1,2727:S:0,2727:S:1,2728:S:0,2728:S:1,2729:S:0,2729:S:1,2730:S:0,2730:S:1,2731:S:0,2731:S:1,2732:S:0,2732:S:1,2733:S:0,2733:S:1,2734:S:0,2734:S:1,2735:S:0,2735:S:1,2736:S:0,2736:S:1,2737:S:0,2737:S:1,2738:S:0,2738:S:1,2739:S:0,2739:S:1,2740:S:0,2740:S:1,2741:S:0,2741:S:1,2742:S:0,2742:S:1,2743:S:0,2743:S:1,2744:S:0,2744:S:1,2745:S:0,2745:S:1,2746:S:0,2746:S:1,2747:S:0,2747:S:1,2748:S:0,2748:S:1,2749:S:0,2749:S:1,2750:S:0,2750:S:1,2751:S:0,2751:S:1,2752:S:0,2752:S:1,2753:S:0,2753:S:1,2754:S:0,2754:S:1,2755:S:0,2755:S:1,2756:S:0,2756:S:1,2757:S:0,2757:S:1,2758:S:0,2758:S:1,2759:S:0,2759:S:1,2760:S:0,2760:S:1,2761:S:0,2761:S:1,2762:S:0,2762:S:1,2763:S:0,2763:S:1,2764:S:0,2764:S:1,2765:S:0,2765:S:1,2766:S:0,2766:S:1,2767:S:0,2767:S:1,2768:S:0,2768:S:1,2769:S:0,2769:S:1,2770:S:0,2770:S:1,2771:S:0,2771:S:1,2772:S:0,2772:S:1,2773:S:0,2773:S:1,2774:S:0,2774:S:1,2775:S:0,2775:S:1,2776:S:0,2776:S:1,2777:S:0,2777:S:1,2778:S:0,2778:S:1,2779:S:0,2779:S:1,2780:S:0,2780:S:1,2781:S:0,2781:S:1,2782:S:0,2782:S:1,2783:S:0,2783:S:1,2784:S:0,2784:S:1,2785:S:0,2785:S:1,2786:S:0,2786:S:1,2787:S:0,2787:S:1,2788:S:0,2788:S:1,2789:S:0,2789:S:1,2790:S:0,2790:S:1,2791:S:0,2791:S:1,2792:S:0,2792:S:1,2793:S:0,2793:S:1,2794:S:0,2794:S:1,2795:S:0,2795:S:1,2796:S:0,2796:S:1,2797:S:0,2797:S:1,2798:S:0,2798:S:1,2799:S:0,2799:S:1,2800:S:0,2800:S:1,2801:S:0,2801:S:1,2802:S:0,2802:S:1,2803:S:0,2803:S:1,2804:S:0,2804:S:1,2805:S:0,2805:S:1,2806:S:0,2806:S:1,2807:S:0,2807:S:1,2808:S:0,2808:S:1,2809:S:0,2809:S:1,2810:S:0,2810:S:1,2811:S:0,2811:S:1,2812:S:0,2812:S:1,2813:S:0,2813:S:1,2814:S:0,2814:S:1,2815:S:0,2815:S:1,2816:S:0,2816:S:1,2817:S:0,2817:S:1,2818:S:0,2818:S:1,2819:S:0,2819:S:1,2820:S:0,2820:S:1,2821:S:0,2821:S:1,2822:S:0,2822:S:1,2823:S:0,2823:S:1,2824:S:0,2824:S:1,2825:S:0,2825:S:1,2826:S:0,2826:S:1,2827:S:0,2827:S:1,2828:S:0,2828:S:1,2829:S:0,2829:S:1,2830:S:0,2830:S:1,2831:S:0,2831:S:1,2832:S:0,2832:S:1,2833:S:0,2833:S:1,2834:S:0,2834:S:1,2835:S:0,2835:S:1,2836:S:0,2836:S:1,2837:S:0,2837:S:1,2838:S:0,2838:S:1,2839:S:0,2839:S:1,2840:S:0,2840:S:1,2841:S:0,2841:S:1,2842:S:0,2842:S:1,2843:S:0,2843:S:1,2844:S:0,2844:S:1,2845:S:0,2845:S:1,2846:S:0,2846:S:1,2847:S:0,2847:S:1,2848:S:0,2848:S:1,2849:S:0,2849:S:1,2850:S:0,2850:S:1,2851:S:0,2851:S:1,2852:S:0,2852:S:1,2853:S:0,2853:S:1,2854:S:0,2854:S:1,2855:S:0,2855:S:1,2856:S:0,2856:S:1,2857:S:0,2857:S:1,2858:S:0,2858:S:1,2859:S:0,2859:S:1,2860:S:0,2860:S:1,2861:S:0,2861:S:1,2862:S:0,2862:S:1,2863:S:0,2863:S:1,2864:S:0,2864:S:1,2865:S:0,2865:S:1,2866:S:0,2866:S:1,2867:S:0,2867:S:1,2868:S:0,2868:S:1,2869:S:0,2869:S:1,2870:S:0,2870:S:1,2871:S:0,2871:S:1,2872:S:0,2872:S:1,2873:S:0,2873:S:1,2874:S:0,2874:S:1,2875:S:0,2875:S:1,2876:S:0,2876:S:1,2877:S:0,2877:S:1,2878:S:0,2878:S:1,2879:S:0,2879:S:1,2880:S:0,2880:S:1,2881:S:0,2881:S:1,2882:S:0,2882:S:1,2883:S:0,2883:S:1,2884:S:0,2884:S:1,2885:S:0,2885:S:1,2886:S:0,2886:S:1,2887:S:0,2887:S:1,2888:S:0,2888:S:1,2889:S:0,2889:S:1,2890:S:0,2890:S:1,2891:S:0,2891:S:1,2892:S:0,2892:S:1,2893:S:0,2893:S:1,2894:S:0,2894:S:1,2895:S:0,2895:S:1,2896:S:0,2896:S:1,2897:S:0,2897:S:1,2898:S:0,2898:S:1,2899:S:0,2899:S:1,2900:S:0,2900:S:1,2901:S:0,2901:S:1,2902:S:0,2902:S:1,2903:S:0,2903:S:1,2904:S:0,2904:S:1,2905:S:0,2905:S:1,2906:S:0,2906:S:1,2907:S:0,2907:S:1,2908:S:0,2908:S:1,2909:S:0,2909:S:1,2910:S:0,2910:S:1,2911:S:0,2911:S:1,2912:S:0,2912:S:1,2913:S:0,2913:S:1,2914:S:0,2914:S:1,2915:S:0,2915:S:1,2916:S:0,2916:S:1,2917:S:0,2917:S:1,2918:S:0,2918:S:1,2919:S:0,2919:S:1,2920:S:0,2920:S:1,2921:S:0,2921:S:1,2922:S:0,2922:S:1,2923:S:0,2923:S:1,2924:S:0,2924:S:1,2925:S:0,2925:S:1,2926:S:0,2926:S:1,2927:S:0,2927:S:1,2928:S:0,2928:S:1,2929:S:0,2929:S:1,2930:S:0,2930:S:1,2931:S:0,2931:S:1,2932:S:0,2932:S:1,2933:S:0,2933:S:1,2934:S:0,2934:S:1,2935:S:0,2935:S:1,2936:S:0,2936:S:1,2937:S:0,2937:S:1,2938:S:0,2938:S:1,2939:S:0,2939:S:1,2940:S:0,2940:S:1,2941:S:0,2941:S:1,2942:S:0,2942:S:1,2943:S:0,2943:S:1,2944:S:0,2944:S:1,2945:S:0,2945:S:1,2946:S:0,2946:S:1,2947:S:0,2947:S:1,2948:S:0,2948:S:1,2949:S:0,2949:S:1,2950:S:0,2950:S:1,2951:S:0,2951:S:1,2952:S:0,2952:S:1,2953:S:0,2953:S:1,2954:S:0,2954:S:1,2955:S:0,2955:S:1,2956:S:0,2956:S:1,2957:S:0,2957:S:1,2958:S:0,2958:S:1,2959:S:0,2959:S:1,2960:S:0,2960:S:1,2961:S:0,2961:S:1,2962:S:0,2962:S:1,2963:S:0,2963:S:1,2964:S:0,2964:S:1,2965:S:0,2965:S:1,2966:S:0,2966:S:1,2967:S:0,2967:S:1,2968:S:0,2968:S:1,2969:S:0,2969:S:1,2970:S:0,2970:S:1,2971:S:0,2971:S:1,2972:S:0,2972:S:1,2973:S:0,2973:S:1,2974:S:0,2974:S:1,2975:S:0,2975:S:1,2976:S:0,2976:S:1,2977:S:0,2977:S:1,2978:S:0,2978:S:1,2979:S:0,2979:S:1,2980:S:0,2980:S:1,2981:S:0,2981:S:1,2982:S:0,2982:S:1,2983:S:0,2983:S:1,2984:S:0,2984:S:1,2985:S:0,2985:S:1,2986:S:0,2986:S:1,2987:S:0,2987:S:1,2988:S:0,2988:S:1,2989:S:0,2989:S:1,2990:S:0,2990:S:1,2991:S:0,2991:S:1,2992:S:0,2992:S:1,2993:S:0,2993:S:1,2994:S:0,2994:S:1,2995:S:0,2995:S:1,2996:S:0,2996:S:1,2997:S:0,2997:S:1,2998:S:0,2998:S:1,2999:S:0,2999:S:1,3000:S:0,3000:S:1,3001:S:0,3001:S:1,3002:S:0,3002:S:1,3003:S:0,3003:S:1,3004:S:0,3004:S:1,3005:S:0,3005:S:1,3006:S:0,3006:S:1,3007:S:0,3007:S:1,3008:S:0,3008:S:1,3009:S:0,3009:S:1,3010:S:0,3010:S:1,3011:S:0,3011:S:1,3012:S:0,3012:S:1,3013:S:0,3013:S:1,3014:S:0,3014:S:1,3015:S:0,3015:S:1,3016:S:0,3016:S:1,3017:S:0,3017:S:1,3018:S:0,3018:S:1,3019:S:0,3019:S:1,3020:S:0,3020:S:1,3021:S:0,3021:S:1,3022:S:0,3022:S:1,3023:S:0,3023:S:1,3024:S:0,3024:S:1,3025:S:0,3025:S:1,3026:S:0,3026:S:1,3027:S:0,3027:S:1,3028:S:0,3028:S:1,3029:S:0,3029:S:1,3030:S:0,3030:S:1,3031:S:0,3031:S:1,3032:S:0,3032:S:1,3033:S:0,3033:S:1,3034:S:0,3034:S:1,3035:S:0,3035:S:1,3036:S:0,3036:S:1,3037:S:0,3037:S:1,3038:S:0,3038:S:1,3039:S:0,3039:S:1,3040:S:0,3040:S:1,3041:S:0,3041:S:1,3042:S:0,3042:S:1,3043:S:0,3043:S:1,3044:S:0,3044:S:1,3045:S:0,3045:S:1,3046:S:0,3046:S:1,3047:S:0,3047:S:1,3048:S:0,3048:S:1,3049:S:0,3049:S:1,3050:S:0,3050:S:1,3051:S:0,3051:S:1,3052:S:0,3052:S:1,3053:S:0,3053:S:1,3054:S:0,3054:S:1,3055:S:0,3055:S:1,3056:S:0,3056:S:1,3057:S:0,3057:S:1,3058:S:0,3058:S:1,3059:S:0,3059:S:1,3060:S:0,3060:S:1,3061:S:0,3061:S:1,3062:S:0,3062:S:1,3063:S:0,3063:S:1,3064:S:0,3064:S:1,3065:S:0,3065:S:1,3066:S:0,3066:S:1,3067:S:0,3067:S:1,3068:S:0,3068:S:1,3069:S:0,3069:S:1,3070:S:0,3070:S:1,3071:S:0,3071:S:1,3072:S:0,3072:S:1,3073:S:0,3073:S:1,3074:S:0,3074:S:1,3075:S:0,3075:S:1,3076:S:0,3076:S:1,3077:S:0,3077:S:1,3078:S:0,3078:S:1,3079:S:0,3079:S:1,3080:S:0,3080:S:1,3081:S:0,3081:S:1,3082:S:0,3082:S:1,3083:S:0,3083:S:1,3084:S:0,3084:S:1,3085:S:0,3085:S:1,3086:S:0,3086:S:1,3087:S:0,3087:S:1,3088:S:0,3088:S:1,3089:S:0,3089:S:1,3090:S:0,3090:S:1,3091:S:0,3091:S:1,3092:S:0,3092:S:1,3093:S:0,3093:S:1,3094:S:0,3094:S:1,3095:S:0,3095:S:1,3096:S:0,3096:S:1,3097:S:0,3097:S:1,3098:S:0,3098:S:1,3099:S:0,3099:S:1,3100:S:0,3100:S:1,3101:S:0,3101:S:1,3102:S:0,3102:S:1,3103:S:0,3103:S:1,3104:S:0,3104:S:1,3105:S:0,3105:S:1,3106:S:0,3106:S:1,3107:S:0,3107:S:1,3108:S:0,3108:S:1,3109:S:0,3109:S:1,3110:S:0,3110:S:1,3111:S:0,3111:S:1,3112:S:0,3112:S:1,3113:S:0,3113:S:1,3114:S:0,3114:S:1,3115:S:0,3115:S:1,3116:S:0,3116:S:1,3117:S:0,3117:S:1,3118:S:0,3118:S:1,3119:S:0,3119:S:1,3120:S:0,3120:S:1,3121:S:0,3121:S:1,3122:S:0,3122:S:1,3123:S:0,3123:S:1,3124:S:0,3124:S:1,3125:S:0,3125:S:1,3126:S:0,3126:S:1,3127:S:0,3127:S:1,3128:S:0,3128:S:1,3129:S:0,3129:S:1,3130:S:0,3130:S:1,3131:S:0,3131:S:1,3132:S:0,3132:S:1,3133:S:0,3133:S:1,3134:S:0,3134:S:1,3135:S:0,3135:S:1,3136:S:0,3136:S:1,3137:S:0,3137:S:1,3138:S:0,3138:S:1,3139:S:0,3139:S:1,3140:S:0,3140:S:1,3141:S:0,3141:S:1,3142:S:0,3142:S:1,3143:S:0,3143:S:1,3144:S:0,3144:S:1,3145:S:0,3145:S:1,3146:S:0,3146:S:1,3147:S:0,3147:S:1,3148:S:0,3148:S:1,3149:S:0,3149:S:1,3150:S:0,3150:S:1,3151:S:0,3151:S:1,3152:S:0,3152:S:1,3153:S:0,3153:S:1,3154:S:0,3154:S:1,3155:S:0,3155:S:1,3156:S:0,3156:S:1,3157:S:0,3157:S:1,3158:S:0,3158:S:1,3159:S:0,3159:S:1,3160:S:0,3160:S:1,3161:S:0,3161:S:1,3162:S:0,3162:S:1,3163:S:0,3163:S:1,3164:S:0,3164:S:1,3165:S:0,3165:S:1,3166:S:0,3166:S:1,3167:S:0,3167:S:1,3168:S:0,3168:S:1,3169:S:0,3169:S:1,3170:S:0,3170:S:1,3171:S:0,3171:S:1,3172:S:0,3172:S:1,3173:S:0,3173:S:1,3174:S:0,3174:S:1,3175:S:0,3175:S:1,3176:S:0,3176:S:1,3177:S:0,3177:S:1,3178:S:0,3178:S:1,3179:S:0,3179:S:1,3180:S:0,3180:S:1,3181:S:0,3181:S:1,3182:S:0,3182:S:1,3183:S:0,3183:S:1,3184:S:0,3184:S:1,3185:S:0,3185:S:1,3186:S:0,3186:S:1,3187:S:0,3187:S:1,3188:S:0,3188:S:1,3189:S:0,3189:S:1,3190:S:0,3190:S:1,3191:S:0,3191:S:1,3192:S:0,3192:S:1,3193:S:0,3193:S:1,3194:S:0,3194:S:1,3195:S:0,3195:S:1,3196:S:0,3196:S:1,3197:S:0,3197:S:1,3198:S:0,3198:S:1,3199:S:0,3199:S:1,3200:S:0,3200:S:1,3201:S:0,3201:S:1,3202:S:0,3202:S:1,3203:S:0,3203:S:1,3204:S:0,3204:S:1,3205:S:0,3205:S:1,3206:S:0,3206:S:1,3207:S:0,3207:S:1,3208:S:0,3208:S:1,3209:S:0,3209:S:1,3210:S:0,3210:S:1,3211:S:0,3211:S:1,3212:S:0,3212:S:1,3213:S:0,3213:S:1,3214:S:0,3214:S:1,3215:S:0,3215:S:1,3216:S:0,3216:S:1,3217:S:0,3217:S:1,3218:S:0,3218:S:1,3219:S:0,3219:S:1,3220:S:0,3220:S:1,3221:S:0,3221:S:1,3222:S:0,3222:S:1,3223:S:0,3223:S:1,3224:S:0,3224:S:1,3225:S:0,3225:S:1,3226:S:0,3226:S:1,3227:S:0,3227:S:1,3228:S:0,3228:S:1,3229:S:0,3229:S:1,3230:S:0,3230:S:1,3231:S:0,3231:S:1,3232:S:0,3232:S:1,3233:S:0,3233:S:1,3234:S:0,3234:S:1,3235:S:0,3235:S:1,3236:S:0,3236:S:1,3237:S:0,3237:S:1,3238:S:0,3238:S:1,3239:S:0,3239:S:1,3240:S:0,3240:S:1,3241:S:0,3241:S:1,3242:S:0,3242:S:1,3243:S:0,3243:S:1,3244:S:0,3244:S:1,3245:S:0,3245:S:1,3246:S:0,3246:S:1,3247:S:0,3247:S:1,3248:S:0,3248:S:1,3249:S:0,3249:S:1,3250:S:0,3250:S:1,3251:S:0,3251:S:1,3252:S:0,3252:S:1,3253:S:0,3253:S:1,3254:S:0,3254:S:1,3255:S:0,3255:S:1,3256:S:0,3256:S:1,3257:S:0,3257:S:1,3258:S:0,3258:S:1,3259:S:0,3259:S:1,3260:S:0,3260:S:1,3261:S:0,3261:S:1,3262:S:0,3262:S:1,3263:S:0,3263:S:1,3264:S:0,3264:S:1,3265:S:0,3265:S:1,3266:S:0,3266:S:1,3267:S:0,3267:S:1,3268:S:0,3268:S:1,3269:S:0,3269:S:1,3270:S:0,3270:S:1,3271:S:0,3271:S:1,3272:S:0,3272:S:1,3273:S:0,3273:S:1,3274:S:0,3274:S:1,3275:S:0,3275:S:1,3276:S:0,3276:S:1,3277:S:0,3277:S:1,3278:S:0,3278:S:1,3279:S:0,3279:S:1,3280:S:0,3280:S:1,3281:S:0,3281:S:1,3282:S:0,3282:S:1,3283:S:0,3283:S:1,3284:S:0,3284:S:1,3285:S:0,3285:S:1,3286:S:0,3286:S:1,3287:S:0,3287:S:1,3288:S:0,3288:S:1,3289:S:0,3289:S:1,3290:S:0,3290:S:1,3291:S:0,3291:S:1,3292:S:0,3292:S:1,3293:S:0,3293:S:1,3294:S:0,3294:S:1,3295:S:0,3295:S:1,3296:S:0,3296:S:1,3297:S:0,3297:S:1,3298:S:0,3298:S:1,3299:S:0,3299:S:1,3300:S:0,3300:S:1,3301:S:0,3301:S:1,3302:S:0,3302:S:1,3303:S:0,3303:S:1,3304:S:0,3304:S:1,3305:S:0,3305:S:1,3306:S:0,3306:S:1,3307:S:0,3307:S:1,3308:S:0,3308:S:1,3309:S:0,3309:S:1,3310:S:0,3310:S:1,3311:S:0,3311:S:1,3312:S:0,3312:S:1,3313:S:0,3313:S:1,3314:S:0,3314:S:1,3315:S:0,3315:S:1,3316:S:0,3316:S:1,3317:S:0,3317:S:1,3318:S:0,3318:S:1,3319:S:0,3319:S:1,3320:S:0,3320:S:1,3321:S:0,3321:S:1,3322:S:0,3322:S:1,3323:S:0,3323:S:1,3324:S:0,3324:S:1,3325:S:0,3325:S:1,3326:S:0,3326:S:1,3327:S:0,3327:S:1,3328:S:0,3328:S:1,3329:S:0,3329:S:1,3330:S:0,3330:S:1,3331:S:0,3331:S:1,3332:S:0,3332:S:1,3333:S:0,3333:S:1,3334:S:0,3334:S:1,3335:S:0,3335:S:1,3336:S:0,3336:S:1,3337:S:0,3337:S:1,3338:S:0,3338:S:1,3339:S:0,3339:S:1,3340:S:0,3340:S:1,3341:S:0,3341:S:1,3342:S:0,3342:S:1,3343:S:0,3343:S:1,3344:S:0,3344:S:1,3345:S:0,3345:S:1,3346:S:0,3346:S:1,3347:S:0,3347:S:1,3348:S:0,3348:S:1,3349:S:0,3349:S:1,3350:S:0,3350:S:1,3351:S:0,3351:S:1,3352:S:0,3352:S:1,3353:S:0,3353:S:1,3354:S:0,3354:S:1,3355:S:0,3355:S:1,3356:S:0,3356:S:1,3357:S:0,3357:S:1,3358:S:0,3358:S:1,3359:S:0,3359:S:1,3360:S:0,3360:S:1,3361:S:0,3361:S:1,3362:S:0,3362:S:1,3363:S:0,3363:S:1,3364:S:0,3364:S:1,3365:S:0,3365:S:1,3366:S:0,3366:S:1,3367:S:0,3367:S:1,3368:S:0,3368:S:1,3369:S:0,3369:S:1,3370:S:0,3370:S:1,3371:S:0,3371:S:1,3372:S:0,3372:S:1,3373:S:0,3373:S:1,3374:S:0,3374:S:1,3375:S:0,3375:S:1,3376:S:0,3376:S:1,3377:S:0,3377:S:1,3378:S:0,3378:S:1,3379:S:0,3379:S:1,3380:S:0,3380:S:1,3381:S:0,3381:S:1,3382:S:0,3382:S:1,3383:S:0,3383:S:1,3384:S:0,3384:S:1,3385:S:0,3385:S:1,3386:S:0,3386:S:1,3387:S:0,3387:S:1,3388:S:0,3388:S:1,3389:S:0,3389:S:1,3390:S:0,3390:S:1,3391:S:0,3391:S:1,3392:S:0,3392:S:1,3393:S:0,3393:S:1,3394:S:0,3394:S:1,3395:S:0,3395:S:1,3396:S:0,3396:S:1,3397:S:0,3397:S:1,3398:S:0,3398:S:1,3399:S:0,3399:S:1,3400:S:0,3400:S:1,3401:S:0,3401:S:1,3402:S:0,3402:S:1,3403:S:0,3403:S:1,3404:S:0,3404:S:1,3405:S:0,3405:S:1,3406:S:0,3406:S:1,3407:S:0,3407:S:1,3408:S:0,3408:S:1,3409:S:0,3409:S:1,3410:S:0,3410:S:1,3411:S:0,3411:S:1,3412:S:0,3412:S:1,3413:S:0,3413:S:1,3414:S:0,3414:S:1,3415:S:0,3415:S:1,3416:S:0,3416:S:1,3417:S:0,3417:S:1,3418:S:0,3418:S:1,3419:S:0,3419:S:1,3420:S:0,3420:S:1,3421:S:0,3421:S:1,3422:S:0,3422:S:1,3423:S:0,3423:S:1,3424:S:0,3424:S:1,3425:S:0,3425:S:1,3426:S:0,3426:S:1,3427:S:0,3427:S:1,3428:S:0,3428:S:1,3429:S:0,3429:S:1,3430:S:0,3430:S:1,3431:S:0,3431:S:1,3432:S:0,3432:S:1,3433:S:0,3433:S:1,3434:S:0,3434:S:1,3435:S:0,3435:S:1,3436:S:0,3436:S:1,3437:S:0,3437:S:1,3438:S:0,3438:S:1,3439:S:0,3439:S:1,3440:S:0,3440:S:1,3441:S:0,3441:S:1,3442:S:0,3442:S:1,3443:S:0,3443:S:1,3444:S:0,3444:S:1,3445:S:0,3445:S:1,3446:S:0,3446:S:1,3447:S:0,3447:S:1,3448:S:0,3448:S:1,3449:S:0,3449:S:1,3450:S:0,3450:S:1,3451:S:0,3451:S:1,3452:S:0,3452:S:1,3453:S:0,3453:S:1,3454:S:0,3454:S:1,3455:S:0,3455:S:1,3456:S:0,3456:S:1,3457:S:0,3457:S:1,3458:S:0,3458:S:1,3459:S:0,3459:S:1,3460:S:0,3460:S:1,3461:S:0,3461:S:1,3462:S:0,3462:S:1,3463:S:0,3463:S:1,3464:S:0,3464:S:1,3465:S:0,3465:S:1,3466:S:0,3466:S:1,3467:S:0,3467:S:1,3468:S:0,3468:S:1,3469:S:0,3469:S:1,3470:S:0,3470:S:1,3471:S:0,3471:S:1,3472:S:0,3472:S:1,3473:S:0,3473:S:1,3474:S:0,3474:S:1,3475:S:0,3475:S:1,3476:S:0,3476:S:1,3477:S:0,3477:S:1,3478:S:0,3478:S:1,3479:S:0,3479:S:1,3480:S:0,3480:S:1,3481:S:0,3481:S:1,3482:S:0,3482:S:1,3483:S:0,3483:S:1,3484:S:0,3484:S:1,3485:S:0,3485:S:1,3486:S:0,3486:S:1,3487:S:0,3487:S:1,3488:S:0,3488:S:1,3489:S:0,3489:S:1,3490:S:0,3490:S:1,3491:S:0,3491:S:1,3492:S:0,3492:S:1,3493:S:0,3493:S:1,3494:S:0,3494:S:1,3495:S:0,3495:S:1,3496:S:0,3496:S:1,3497:S:0,3497:S:1,3498:S:0,3498:S:1,3499:S:0,3499:S:1,3500:S:0,3500:S:1,3501:S:0,3501:S:1,3502:S:0,3502:S:1,3503:S:0,3503:S:1,3504:S:0,3504:S:1,3505:S:0,3505:S:1,3506:S:0,3506:S:1,3507:S:0,3507:S:1,3508:S:0,3508:S:1,3509:S:0,3509:S:1,3510:S:0,3510:S:1,3511:S:0,3511:S:1,3512:S:0,3512:S:1,3513:S:0,3513:S:1,3514:S:0,3514:S:1,3515:S:0,3515:S:1,3516:S:0,3516:S:1,3517:S:0,3517:S:1,3518:S:0,3518:S:1,3519:S:0,3519:S:1,3520:S:0,3520:S:1,3521:S:0,3521:S:1,3522:S:0,3522:S:1,3523:S:0,3523:S:1,3524:S:0,3524:S:1,3525:S:0,3525:S:1,3526:S:0,3526:S:1,3527:S:0,3527:S:1,3528:S:0,3528:S:1,3529:S:0,3529:S:1,3530:S:0,3530:S:1,3531:S:0,3531:S:1,3532:S:0,3532:S:1,3533:S:0,3533:S:1,3534:S:0,3534:S:1,3535:S:0,3535:S:1,3536:S:0,3536:S:1,3537:S:0,3537:S:1,3538:S:0,3538:S:1,3539:S:0,3539:S:1,3540:S:0,3540:S:1,3541:S:0,3541:S:1,3542:S:0,3542:S:1,3543:S:0,3543:S:1,3544:S:0,3544:S:1,3545:S:0,3545:S:1,3546:S:0,3546:S:1,3547:S:0,3547:S:1,3548:S:0,3548:S:1,3549:S:0,3549:S:1,3550:S:0,3550:S:1,3551:S:0,3551:S:1,3552:S:0,3552:S:1,3553:S:0,3553:S:1,3554:S:0,3554:S:1,3555:S:0,3555:S:1,3556:S:0,3556:S:1,3557:S:0,3557:S:1,3558:S:0,3558:S:1,3559:S:0,3559:S:1,3560:S:0,3560:S:1,3561:S:0,3561:S:1,3562:S:0,3562:S:1,3563:S:0,3563:S:1,3564:S:0,3564:S:1,3565:S:0,3565:S:1,3566:S:0,3566:S:1,3567:S:0,3567:S:1,3568:S:0,3568:S:1,3569:S:0,3569:S:1,3570:S:0,3570:S:1,3571:S:0,3571:S:1,3572:S:0,3572:S:1,3573:S:0,3573:S:1,3574:S:0,3574:S:1,3575:S:0,3575:S:1,3576:S:0,3576:S:1,3577:S:0,3577:S:1,3578:S:0,3578:S:1,3579:S:0,3579:S:1,3580:S:0,3580:S:1,3581:S:0,3581:S:1,3582:S:0,3582:S:1,3583:S:0,3583:S:1,3584:S:0,3584:S:1,3585:S:0,3585:S:1,3586:S:0,3586:S:1,3587:S:0,3587:S:1,3588:S:0,3588:S:1,3589:S:0,3589:S:1,3590:S:0,3590:S:1,3591:S:0,3591:S:1,3592:S:0,3592:S:1,3593:S:0,3593:S:1,3594:S:0,3594:S:1,3595:S:0,3595:S:1,3596:S:0,3596:S:1,3597:S:0,3597:S:1,3598:S:0,3598:S:1,3599:S:0,3599:S:1,3600:S:0,3600:S:1,3601:S:0,3601:S:1,3602:S:0,3602:S:1,3603:S:0,3603:S:1,3604:S:0,3604:S:1,3605:S:0,3605:S:1,3606:S:0,3606:S:1,3607:S:0,3607:S:1,3608:S:0,3608:S:1,3609:S:0,3609:S:1,3610:S:0,3610:S:1,3611:S:0,3611:S:1,3612:S:0,3612:S:1,3613:S:0,3613:S:1,3614:S:0,3614:S:1,3615:S:0,3615:S:1,3616:S:0,3616:S:1,3617:S:0,3617:S:1,3618:S:0,3618:S:1,3619:S:0,3619:S:1,3620:S:0,3620:S:1,3621:S:0,3621:S:1,3622:S:0,3622:S:1,3623:S:0,3623:S:1,3624:S:0,3624:S:1,3625:S:0,3625:S:1,3626:S:0,3626:S:1,3627:S:0,3627:S:1,3628:S:0,3628:S:1,3629:S:0,3629:S:1,3630:S:0,3630:S:1,3631:S:0,3631:S:1,3632:S:0,3632:S:1,3633:S:0,3633:S:1,3634:S:0,3634:S:1,3635:S:0,3635:S:1,3636:S:0,3636:S:1,3637:S:0,3637:S:1,3638:S:0,3638:S:1,3639:S:0,3639:S:1,3640:S:0,3640:S:1,3641:S:0,3641:S:1,3642:S:0,3642:S:1,3643:S:0,3643:S:1,3644:S:0,3644:S:1,3645:S:0,3645:S:1,3646:S:0,3646:S:1,3647:S:0,3647:S:1,3648:S:0,3648:S:1,3649:S:0,3649:S:1,3650:S:0,3650:S:1,3651:S:0,3651:S:1,3652:S:0,3652:S:1,3653:S:0,3653:S:1,3654:S:0,3654:S:1,3655:S:0,3655:S:1,3656:S:0,3656:S:1,3657:S:0,3657:S:1,3658:S:0,3658:S:1,3659:S:0,3659:S:1,3660:S:0,3660:S:1,3661:S:0,3661:S:1,3662:S:0,3662:S:1,3663:S:0,3663:S:1,3664:S:0,3664:S:1,3665:S:0,3665:S:1,3666:S:0,3666:S:1,3667:S:0,3667:S:1,3668:S:0,3668:S:1,3669:S:0,3669:S:1,3670:S:0,3670:S:1,3671:S:0,3671:S:1,3672:S:0,3672:S:1,3673:S:0,3673:S:1,3674:S:0,3674:S:1,3675:S:0,3675:S:1,3676:S:0,3676:S:1,3677:S:0,3677:S:1,3678:S:0,3678:S:1,3679:S:0,3679:S:1,3680:S:0,3680:S:1,3681:S:0,3681:S:1,3682:S:0,3682:S:1,3683:S:0,3683:S:1,3684:S:0,3684:S:1,3685:S:0,3685:S:1,3686:S:0,3686:S:1,3687:S:0,3687:S:1,3688:S:0,3688:S:1,3689:S:0,3689:S:1,3690:S:0,3690:S:1,3691:S:0,3691:S:1,3692:S:0,3692:S:1,3693:S:0,3693:S:1,3694:S:0,3694:S:1,3695:S:0,3695:S:1,3696:S:0,3696:S:1,3697:S:0,3697:S:1,3698:S:0,3698:S:1,3699:S:0,3699:S:1,3700:S:0,3700:S:1,3701:S:0,3701:S:1,3702:S:0,3702:S:1,3703:S:0,3703:S:1,3704:S:0,3704:S:1,3705:S:0,3705:S:1,3706:S:0,3706:S:1,3707:S:0,3707:S:1,3708:S:0,3708:S:1,3709:S:0,3709:S:1,3710:S:0,3710:S:1,3711:S:0,3711:S:1,3712:S:0,3712:S:1,3713:S:0,3713:S:1,3714:S:0,3714:S:1,3715:S:0,3715:S:1,3716:S:0,3716:S:1,3717:S:0,3717:S:1,3718:S:0,3718:S:1,3719:S:0,3719:S:1,3720:S:0,3720:S:1,3721:S:0,3721:S:1,3722:S:0,3722:S:1,3723:S:0,3723:S:1,3724:S:0,3724:S:1,3725:S:0,3725:S:1,3726:S:0,3726:S:1,3727:S:0,3727:S:1,3728:S:0,3728:S:1,3729:S:0,3729:S:1,3730:S:0,3730:S:1,3731:S:0,3731:S:1,3732:S:0,3732:S:1,3733:S:0,3733:S:1,3734:S:0,3734:S:1,3735:S:0,3735:S:1,3736:S:0,3736:S:1,3737:S:0,3737:S:1,3738:S:0,3738:S:1,3739:S:0,3739:S:1,3740:S:0,3740:S:1,3741:S:0,3741:S:1,3742:S:0,3742:S:1,3743:S:0,3743:S:1,3744:S:0,3744:S:1,3745:S:0,3745:S:1,3746:S:0,3746:S:1,3747:S:0,3747:S:1,3748:S:0,3748:S:1,3749:S:0,3749:S:1,3750:S:0,3750:S:1,3751:S:0,3751:S:1,3752:S:0,3752:S:1,3753:S:0,3753:S:1,3754:S:0,3754:S:1,3755:S:0,3755:S:1,3756:S:0,3756:S:1,3757:S:0,3757:S:1,3758:S:0,3758:S:1,3759:S:0,3759:S:1,3760:S:0,3760:S:1,3761:S:0,3761:S:1,3762:S:0,3762:S:1,3763:S:0,3763:S:1,3764:S:0,3764:S:1,3765:S:0,3765:S:1,3766:S:0,3766:S:1,3767:S:0,3767:S:1,3768:S:0,3768:S:1,3769:S:0,3769:S:1,3770:S:0,3770:S:1,3771:S:0,3771:S:1,3772:S:0,3772:S:1,3773:S:0,3773:S:1,3774:S:0,3774:S:1,3775:S:0,3775:S:1,3776:S:0,3776:S:1,3777:S:0,3777:S:1,3778:S:0,3778:S:1,3779:S:0,3779:S:1,3780:S:0,3780:S:1,3781:S:0,3781:S:1,3782:S:0,3782:S:1,3783:S:0,3783:S:1,3784:S:0,3784:S:1,3785:S:0,3785:S:1,3786:S:0,3786:S:1,3787:S:0,3787:S:1,3788:S:0,3788:S:1,3789:S:0,3789:S:1,3790:S:0,3790:S:1,3791:S:0,3791:S:1,3792:S:0,3792:S:1,3793:S:0,3793:S:1,3794:S:0,3794:S:1,3795:S:0,3795:S:1,3796:S:0,3796:S:1,3797:S:0,3797:S:1,3798:S:0,3798:S:1,3799:S:0,3799:S:1,3800:S:0,3800:S:1,3801:S:0,3801:S:1,3802:S:0,3802:S:1,3803:S:0,3803:S:1,3804:S:0,3804:S:1,3805:S:0,3805:S:1,3806:S:0,3806:S:1,3807:S:0,3807:S:1,3808:S:0,3808:S:1,3809:S:0,3809:S:1,3810:S:0,3810:S:1,3811:S:0,3811:S:1,3812:S:0,3812:S:1,3813:S:0,3813:S:1,3814:S:0,3814:S:1,3815:S:0,3815:S:1,3816:S:0,3816:S:1,3817:S:0,3817:S:1,3818:S:0,3818:S:1,3819:S:0,3819:S:1,3820:S:0,3820:S:1,3821:S:0,3821:S:1,3822:S:0,3822:S:1,3823:S:0,3823:S:1,3824:S:0,3824:S:1,3825:S:0,3825:S:1,3826:S:0,3826:S:1,3827:S:0,3827:S:1,3828:S:0,3828:S:1,3829:S:0,3829:S:1,3830:S:0,3830:S:1,3831:S:0,3831:S:1,3832:S:0,3832:S:1,3833:S:0,3833:S:1,3834:S:0,3834:S:1,3835:S:0,3835:S:1,3836:S:0,3836:S:1,3837:S:0,3837:S:1,3838:S:0,3838:S:1,3839:S:0,3839:S:1,3840:S:0,3840:S:1,3841:S:0,3841:S:1,3842:S:0,3842:S:1,3843:S:0,3843:S:1,3844:S:0,3844:S:1,3845:S:0,3845:S:1,3846:S:0,3846:S:1,3847:S:0,3847:S:1,3848:S:0,3848:S:1,3849:S:0,3849:S:1,3850:S:0,3850:S:1,3851:S:0,3851:S:1,3852:S:0,3852:S:1,3853:S:0,3853:S:1,3854:S:0,3854:S:1,3855:S:0,3855:S:1,3856:S:0,3856:S:1,3857:S:0,3857:S:1,3858:S:0,3858:S:1,3859:S:0,3859:S:1,3860:S:0,3860:S:1,3861:S:0,3861:S:1,3862:S:0,3862:S:1,3863:S:0,3863:S:1,3864:S:0,3864:S:1,3865:S:0,3865:S:1,3866:S:0,3866:S:1,3867:S:0,3867:S:1,3868:S:0,3868:S:1,3869:S:0,3869:S:1,3870:S:0,3870:S:1,3871:S:0,3871:S:1,3872:S:0,3872:S:1,3873:S:0,3873:S:1,3874:S:0,3874:S:1,3875:S:0,3875:S:1,3876:S:0,3876:S:1,3877:S:0,3877:S:1,3878:S:0,3878:S:1,3879:S:0,3879:S:1,3880:S:0,3880:S:1,3881:S:0,3881:S:1,3882:S:0,3882:S:1,3883:S:0,3883:S:1,3884:S:0,3884:S:1,3885:S:0,3885:S:1,3886:S:0,3886:S:1,3887:S:0,3887:S:1,3888:S:0,3888:S:1,3889:S:0,3889:S:1,3890:S:0,3890:S:1,3891:S:0,3891:S:1,3892:S:0,3892:S:1,3893:S:0,3893:S:1,3894:S:0,3894:S:1,3895:S:0,3895:S:1,3896:S:0,3896:S:1,3897:S:0,3897:S:1,3898:S:0,3898:S:1,3899:S:0,3899:S:1,3900:S:0,3900:S:1,3901:S:0,3901:S:1,3902:S:0,3902:S:1,3903:S:0,3903:S:1,3904:S:0,3904:S:1,3905:S:0,3905:S:1,3906:S:0,3906:S:1,3907:S:0,3907:S:1,3908:S:0,3908:S:1,3909:S:0,3909:S:1,3910:S:0,3910:S:1,3911:S:0,3911:S:1,3912:S:0,3912:S:1,3913:S:0,3913:S:1,3914:S:0,3914:S:1,3915:S:0,3915:S:1,3916:S:0,3916:S:1,3917:S:0,3917:S:1,3918:S:0,3918:S:1,3919:S:0,3919:S:1,3920:S:0,3920:S:1,3921:S:0,3921:S:1,3922:S:0,3922:S:1,3923:S:0,3923:S:1,3924:S:0,3924:S:1,3925:S:0,3925:S:1,3926:S:0,3926:S:1,3927:S:0,3927:S:1,3928:S:0,3928:S:1,3929:S:0,3929:S:1,3930:S:0,3930:S:1,3931:S:0,3931:S:1,3932:S:0,3932:S:1,3933:S:0,3933:S:1,3934:S:0,3934:S:1,3935:S:0,3935:S:1,3936:S:0,3936:S:1,3937:S:0,3937:S:1,3938:S:0,3938:S:1,3939:S:0,3939:S:1,3940:S:0,3940:S:1,3941:S:0,3941:S:1,3942:S:0,3942:S:1,3943:S:0,3943:S:1,3944:S:0,3944:S:1,3945:S:0,3945:S:1,3946:S:0,3946:S:1,3947:S:0,3947:S:1,3948:S:0,3948:S:1,3949:S:0,3949:S:1,3950:S:0,3950:S:1,3951:S:0,3951:S:1,3952:S:0,3952:S:1,3953:S:0,3953:S:1,3954:S:0,3954:S:1,3955:S:0,3955:S:1,3956:S:0,3956:S:1,3957:S:0,3957:S:1,3958:S:0,3958:S:1,3959:S:0,3959:S:1,3960:S:0,3960:S:1,3961:S:0,3961:S:1,3962:S:0,3962:S:1,3963:S:0,3963:S:1,3964:S:0,3964:S:1,3965:S:0,3965:S:1,3966:S:0,3966:S:1,3967:S:0,3967:S:1,3968:S:0,3968:S:1,3969:S:0,3969:S:1,3970:S:0,3970:S:1,3971:S:0,3971:S:1,3972:S:0,3972:S:1,3973:S:0,3973:S:1,3974:S:0,3974:S:1,3975:S:0,3975:S:1,3976:S:0,3976:S:1,3977:S:0,3977:S:1,3978:S:0,3978:S:1,3979:S:0,3979:S:1,3980:S:0,3980:S:1,3981:S:0,3981:S:1,3982:S:0,3982:S:1,3983:S:0,3983:S:1,3984:S:0,3984:S:1,3985:S:0,3985:S:1,3986:S:0,3986:S:1,3987:S:0,3987:S:1,3988:S:0,3988:S:1,3989:S:0,3989:S:1,3990:S:0,3990:S:1,3991:S:0,3991:S:1,3992:S:0,3992:S:1,3993:S:0,3993:S:1,3994:S:0,3994:S:1,3995:S:0,3995:S:1,3996:S:0,3996:S:1,3997:S:0,3997:S:1,3998:S:0,3998:S:1,3999:S:0,3999:S:1,4000:S:0,4000:S:1,4001:S:0,4001:S:1,4002:S:0,4002:S:1,4003:S:0,4003:S:1,4004:S:0,4004:S:1,4005:S:0,4005:S:1,4006:S:0,4006:S:1,4007:S:0,4007:S:1,4008:S:0,4008:S:1,4009:S:0,4009:S:1,4010:S:0,4010:S:1,4011:S:0,4011:S:1,4012:S:0,4012:S:1,4013:S:0,4013:S:1,4014:S:0,4014:S:1,4015:S:0,4015:S:1,4016:S:0,4016:S:1,4017:S:0,4017:S:1,4018:S:0,4018:S:1,4019:S:0,4019:S:1,4020:S:0,4020:S:1,4021:S:0,4021:S:1,4022:S:0,4022:S:1,4023:S:0,4023:S:1,4024:S:0,4024:S:1,4025:S:0,4025:S:1,4026:S:0,4026:S:1,4027:S:0,4027:S:1,4028:S:0,4028:S:1,4029:S:0,4029:S:1,4030:S:0,4030:S:1,4031:S:0,4031:S:1,4032:S:0,4032:S:1,4033:S:0,4033:S:1,4034:S:0,4034:S:1,4035:S:0,4035:S:1,4036:S:0,4036:S:1,4037:S:0,4037:S:1,4038:S:0,4038:S:1,4039:S:0,4039:S:1,4040:S:0,4040:S:1,4041:S:0,4041:S:1,4042:S:0,4042:S:1,4043:S:0,4043:S:1,4044:S:0,4044:S:1,4045:S:0,4045:S:1,4046:S:0,4046:S:1,4047:S:0,4047:S:1,4048:S:0,4048:S:1,4049:S:0,4049:S:1,4050:S:0,4050:S:1,4051:S:0,4051:S:1,4052:S:0,4052:S:1,4053:S:0,4053:S:1,4054:S:0,4054:S:1,4055:S:0,4055:S:1,4056:S:0,4056:S:1,4057:S:0,4057:S:1,4058:S:0,4058:S:1,4059:S:0,4059:S:1,4060:S:0,4060:S:1,4061:S:0,4061:S:1,4062:S:0,4062:S:1,4063:S:0,4063:S:1,4064:S:0,4064:S:1,4065:S:0,4065:S:1,4066:S:0,4066:S:1,4067:S:0,4067:S:1,4068:S:0,4068:S:1,4069:S:0,4069:S:1,4070:S:0,4070:S:1,4071:S:0,4071:S:1,4072:S:0,4072:S:1,4073:S:0,4073:S:1,4074:S:0,4074:S:1,4075:S:0,4075:S:1,4076:S:0,4076:S:1,4077:S:0,4077:S:1,4078:S:0,4078:S:1,4079:S:0,4079:S:1,4080:S:0,4080:S:1,4081:S:0,4081:S:1,4082:S:0,4082:S:1,4083:S:0,4083:S:1,4084:S:0,4084:S:1,4085:S:0,4085:S:1,4086:S:0,4086:S:1,4087:S:0,4087:S:1,4088:S:0,4088:S:1,4089:S:0,4089:S:1,4090:S:0,4090:S:1,4091:S:0,4091:S:1,4092:S:0,4092:S:1,4093:S:0,4093:S:1,4094:S:1,4095:S:1', 'sec_index': 1, 'replica_disc_part': [3], 'master_disc_part': [4, 4094, 4095], 'pri_index': 3}}} - versions = {'10.71.71.169:3000':'3.6.0'} - ns_info = {} - ns_info['test'] = {} - ns_info['test']['diff_master'] = 10 - ns_info['test']['diff_replica'] = 10 - ns_info['test']['avg_replica_objs'] = 1111 - ns_info['test']['avg_master_objs'] = 1111 - ns_info['test']['repl_factor'] = 2 - actual_output = self.controller._get_pmap_data(input_config,ns_info,versions) + cluster = Cluster(('10.71.71.169', '3000', None)) + cluster.info_statistics = Mock() + cluster.info_statistics.return_value = {'10.71.71.169:3000': {'cluster_key': 'ck'}} + cluster.info_namespaces = Mock() + cluster.info_namespaces.return_value = {'10.71.71.169:3000': ['test']} + cluster.info_namespace_statistics = Mock() + cluster.info_namespace_statistics.return_value = {'10.71.71.169:3000': {'repl-factor': '2'}} + cluster.info = Mock() + cluster.info.side_effect = self.mock_info_call + self.controller = GetPmapController(cluster) + + + def test_get_pmap_data(self): + self.partition_info = {'10.71.71.169:3000': 'test:0:A:2:0:0:0:0:0:0:0:0;test:1:A:2:0:0:0:0:0:0:0:0;' + 'test:2:A:2:0:0:0:0:0:0:0:0;test:3:S:1:0:0:0:0:207069:3001:0:0;' + 'test:4:S:0:0:0:0:0:0:0:0:0;test:4094:S:0:0:0:0:0:206724:2996:0:0;' + 'test:4095:S:0:0:0:0:0:213900:3100:0:0'} + expected_output = {} + expected_output['10.71.71.169:3000'] = {} + expected_output['10.71.71.169:3000']['test'] = {} + expected_output['10.71.71.169:3000']['test']['cluster_key'] = 'ck' + expected_output['10.71.71.169:3000']['test']['master_partition_count'] = 3 + expected_output['10.71.71.169:3000']['test']['missing_partition_count'] = 8188 + expected_output['10.71.71.169:3000']['test']['prole_partition_count'] = 1 + actual_output = self.controller.get_pmap() + self.assertEqual(expected_output, actual_output) + + def test_get_pmap_data_with_migrations(self): + self.partition_info = {'10.71.71.169:3000': 'test:0:D:1:0:0:0:0:0:0:0:0;test:1:A:2:0:0:0:0:0:0:0:0;' + 'test:2:D:1:0:BB93039BC7AC40C:0:0:0:0:0:0;' + 'test:3:S:1:0:0:0:0:207069:3001:0:0;test:4:S:0:0:0:0:0:0:0:0:0;' + 'test:4094:S:0:BB93039BC7AC40C:0:0:0:206724:2996:0:0;test:4095:S:0:0:0:0:0:213900:3100:0:0'} + expected_output = {} + expected_output['10.71.71.169:3000'] = {} + expected_output['10.71.71.169:3000']['test'] = {} + expected_output['10.71.71.169:3000']['test']['cluster_key'] = 'ck' + expected_output['10.71.71.169:3000']['test']['master_partition_count'] = 3 + expected_output['10.71.71.169:3000']['test']['missing_partition_count'] = 8186 + expected_output['10.71.71.169:3000']['test']['prole_partition_count'] = 3 + actual_output = self.controller.get_pmap() self.assertEqual(expected_output, actual_output) - def test_get_pmap_data_neg(self): - input_config = {'10.71.71.169:3000': 'test:0:A:2:0:0:0:0:0:0:0:0;test:1:A:2:0:0:0:0:0:0:0:0;test:2:A:2:0:0:0:0:0:0:0:0;test:3:S:1:0:0:0:0:207069:3001:0:0;test:4:S:0:0:0:0:0:0:0:0:0;test:4094:S:0:0:0:0:0:206724:2996:0:0;test:4095:S:0:0:0:0:0:213900:3100:0:0'} - versions = {'10.71.71.169:3000':'3.6.0'} - expected_output = {'10.71.71.169:3000': {'test': {'missing_part': range(0, 4096), 'sec_index': 4, 'distribution_pct': 0, 'pri_index': 3}}} - ns_info = {} - ns_info['test'] = {} - ns_info['test']['diff_master'] = 10 - ns_info['test']['diff_replica'] = 10 - ns_info['test']['avg_replica_objs'] = 1111 - ns_info['test']['avg_master_objs'] = 1111 - ns_info['test']['repl_factor'] = 2 - actual_output = self.controller._get_pmap_data(input_config, ns_info, versions) - self.assertNotEqual(expected_output, actual_output)