diff --git a/CHANGES.md b/CHANGES.md index 85d255a8b..997cbb005 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Version 5.0.2 +* Colored output in ecctool - Issue #527 * Bump tomcat to 9.0.86 - Issue #653 * Bump springboot to 2.7.18 - Issue #653 diff --git a/docs/autogenerated/openapi.yaml b/docs/autogenerated/openapi.yaml index 26b1405b6..4aa73dc18 100644 --- a/docs/autogenerated/openapi.yaml +++ b/docs/autogenerated/openapi.yaml @@ -158,6 +158,18 @@ paths: application/json: schema: $ref: '#/components/schemas/Schedule' + /repair-management/v2/running-job: + get: + tags: + - Repair-Management + operationId: getCurrentJobStatus + responses: + "200": + description: OK + content: + application/json: + schema: + type: string /repair-management/v2/repairs/{id}: get: tags: diff --git a/ecchronos-binary/generate-ecctool-doc.sh b/ecchronos-binary/generate-ecctool-doc.sh index a2de69f3a..75344d280 100755 --- a/ecchronos-binary/generate-ecctool-doc.sh +++ b/ecchronos-binary/generate-ecctool-doc.sh @@ -26,7 +26,9 @@ fi pip install sphinx pip install sphinxcontrib-autoprogram -pip install sphinxnotes-markdown-builder +pip install sphinxnotes-markdown-builder] +pip install colorama +pip install supports-color sphinx-build -M markdown ${SCRIPT_DIR}/src ${SCRIPT_DIR}/target/ sed -i 's:<:\<:g' ${SCRIPT_DIR}/target/markdown/index.md diff --git a/ecchronos-binary/src/bin/ecctool.py b/ecchronos-binary/src/bin/ecctool.py index a5d370c4f..1e64e5b12 100755 --- a/ecchronos-binary/src/bin/ecctool.py +++ b/ecchronos-binary/src/bin/ecctool.py @@ -66,6 +66,9 @@ def add_repairs_subcommand(sub_parsers): parser_repairs = sub_parsers.add_parser("repairs", description="Show the status of all manual repairs. This subcommand has " "no mandatory parameters.") + parser_repairs.add_argument("-c", "--colors", type=str, + help="Allow colored output by ecctool schedules, the option can be auto/on/off.", + default="auto") parser_repairs.add_argument("-k", "--keyspace", type=str, help="Show repairs for the specified keyspace. This argument is mutually exclusive " "with -i and --id.") @@ -92,6 +95,9 @@ def add_schedules_subcommand(sub_parsers): parser_schedules = sub_parsers.add_parser("schedules", description="Show the status of schedules. This subcommand has no " "mandatory parameters.") + parser_schedules.add_argument("-c", "--colors", type=str, + help="Allow colored output by ecctool schedules, the option can be auto/on/off.", + default="auto") parser_schedules.add_argument("-k", "--keyspace", type=str, help="Show schedules for the specified keyspace. This argument is mutually " "exclusive with -i and --id.") @@ -151,6 +157,9 @@ def add_repair_info_subcommand(sub_parsers): "specific table using --keyspace and --table, " "the duration will default to the table's " "GC_GRACE_SECONDS.") + parser_repair_info.add_argument("-c", "--colors", type=str, + help="Allow colored output by ecctool schedules, the option can be auto/on/off.", + default="auto") parser_repair_info.add_argument("-k", "--keyspace", type=str, help="Show repair information for all tables in the specified keyspace.") parser_repair_info.add_argument("-t", "--table", type=str, @@ -214,6 +223,7 @@ def schedules(arguments): # pylint: disable=too-many-branches request = rest.V2RepairSchedulerRequest(base_url=arguments.url) full = False + colors = color_option(arguments.colors) if arguments.id: if arguments.full: result = request.get_schedule(job_id=arguments.id, full=True) @@ -222,7 +232,7 @@ def schedules(arguments): result = request.get_schedule(job_id=arguments.id) if result.is_successful(): - table_printer.print_schedule(result.data, arguments.limit, full) + table_printer.print_schedule(result.data, arguments.limit, full, colors) else: print(result.format_exception()) elif arguments.full: @@ -234,23 +244,25 @@ def schedules(arguments): sys.exit(1) result = request.list_schedules(keyspace=arguments.keyspace, table=arguments.table) if result.is_successful(): - table_printer.print_schedules(result.data, arguments.limit) + table_printer.print_schedules(result.data, arguments.limit, colors) else: print(result.format_exception()) else: result = request.list_schedules(keyspace=arguments.keyspace) if result.is_successful(): - table_printer.print_schedules(result.data, arguments.limit) + table_printer.print_schedules(result.data, arguments.limit, colors) else: print(result.format_exception()) def repairs(arguments): request = rest.V2RepairSchedulerRequest(base_url=arguments.url) + colors = color_option(arguments.colors) + if arguments.id: result = request.get_repair(job_id=arguments.id, host_id=arguments.hostid) if result.is_successful(): - table_printer.print_repairs(result.data, arguments.limit) + table_printer.print_repairs(result.data, arguments.limit, colors) else: print(result.format_exception()) elif arguments.table: @@ -259,13 +271,13 @@ def repairs(arguments): sys.exit(1) result = request.list_repairs(keyspace=arguments.keyspace, table=arguments.table, host_id=arguments.hostid) if result.is_successful(): - table_printer.print_repairs(result.data, arguments.limit) + table_printer.print_repairs(result.data, arguments.limit, colors) else: print(result.format_exception()) else: result = request.list_repairs(keyspace=arguments.keyspace, host_id=arguments.hostid) if result.is_successful(): - table_printer.print_repairs(result.data, arguments.limit) + table_printer.print_repairs(result.data, arguments.limit, colors) else: print(result.format_exception()) @@ -285,6 +297,7 @@ def run_repair(arguments): def repair_info(arguments): request = rest.V2RepairSchedulerRequest(base_url=arguments.url) + colors = color_option(arguments.colors) if not arguments.keyspace and arguments.table: print("--keyspace must be specified if table is specified") sys.exit(1) @@ -301,7 +314,7 @@ def repair_info(arguments): since=arguments.since, duration=duration, local=arguments.local) if result.is_successful(): - table_printer.print_repair_info(result.data, arguments.limit) + table_printer.print_repair_info(result.data, arguments.limit, colors) else: print(result.format_exception()) @@ -382,7 +395,16 @@ def running_job(arguments): result = request.running_job() print(result) +def color_option(color_arg): + colors = "auto" + if color_arg in ["auto", "on", "off"]: + if color_arg != colors: + colors = color_arg + else: + print(f"'{color_arg}' is not a valid option, it must be auto/on/off.") + print("Using 'auto' as a color option.") + return colors def run_subcommand(arguments): if arguments.subcommand == "repairs": diff --git a/ecchronos-binary/src/pylib/ecchronoslib/displaying.py b/ecchronos-binary/src/pylib/ecchronoslib/displaying.py new file mode 100644 index 000000000..894cb6b42 --- /dev/null +++ b/ecchronos-binary/src/pylib/ecchronoslib/displaying.py @@ -0,0 +1,95 @@ +# +# Copyright 2024 Telefonaktiebolaget LM Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from colorama import Fore, init +from supports_color import supportsColor + +init(autoreset=True) + +RED = Fore.RED +GREEN = Fore.GREEN +BLUE = Fore.BLUE +YELLOW = Fore.YELLOW +MAGENTA = Fore.MAGENTA +CYAN = Fore.CYAN +RESET = Fore.RESET +DARK_MAGENTA = '\033[0;35m' + +color_map = { + "Id": RED, + "Host Id": RED, + "Keyspace": CYAN, + "Table": CYAN, + "Status": MAGENTA, + "Repaired(%)": MAGENTA, + "Completed at": MAGENTA, + "Next repair": MAGENTA, + "Repair type": MAGENTA, + "Start token": GREEN, + "End token": RED, + "Replicas": CYAN, + "Repaired at": GREEN, + "Repaired": CYAN, + "Repair time taken": GREEN, + "Config": MAGENTA, + "UUID": GREEN, + "FLOAT": CYAN, + "DATETIME": GREEN, + "INT": YELLOW, + "TEXT": YELLOW, + "COMPLETED": GREEN, + "IN_QUEUE": CYAN, + "BLOCKED": MAGENTA, + "WARNING": YELLOW, + "ERROR": RED, + "ON_TIME": BLUE, + "LATE": YELLOW, + "OVERDUE": YELLOW, + "Collection": MAGENTA +} + +def color_str(field, color, field_type): + if should_color(color): + colored_str = color_map[field_type] + str(field) + RESET + return colored_str + return field + +def color_key(key, color): + if should_color(color): + colored_str = color_map[key] + str(key) + RESET + return colored_str + return key + +def color_index(summary, color): + if should_color(color): + colored_summary = [] + for collum in summary: + colored_collum = color_map[collum] + collum + RESET + colored_summary.append(colored_collum) + return colored_summary + return summary + +def verify_system_compatibility() -> bool: + if supportsColor.stdout: + return True + return False + +def should_color(color) -> bool: + should_colorize = False + if color == "auto": + should_colorize = verify_system_compatibility() + if color == "on": + should_colorize = True + return should_colorize diff --git a/ecchronos-binary/src/pylib/ecchronoslib/table_formatter.py b/ecchronos-binary/src/pylib/ecchronoslib/table_formatter.py index 500932e26..9ecde6ad3 100644 --- a/ecchronos-binary/src/pylib/ecchronoslib/table_formatter.py +++ b/ecchronos-binary/src/pylib/ecchronoslib/table_formatter.py @@ -15,7 +15,6 @@ from __future__ import print_function - def calculate_max_len(data, i): max_len = 0 for array in data: @@ -24,7 +23,7 @@ def calculate_max_len(data, i): return max_len -def format_table(data): +def format_table(data, colors): if len(data) <= 0: return @@ -32,9 +31,12 @@ def format_table(data): total_length = 2 for idx, _ in enumerate(data[0]): + remove_str = 0 + if colors: + remove_str = 10 # remove ANSI extra strings max_len = calculate_max_len(data, idx) print_format = "{0}{{{1}:{2}s}} | ".format(print_format, idx, max_len) - total_length += max_len + 3 + total_length += max_len + 3 - remove_str total_length -= 1 # Last space is not counted print("-" * total_length) diff --git a/ecchronos-binary/src/pylib/ecchronoslib/table_printer.py b/ecchronos-binary/src/pylib/ecchronoslib/table_printer.py index 3442f7e84..586d2d28e 100644 --- a/ecchronos-binary/src/pylib/ecchronoslib/table_printer.py +++ b/ecchronos-binary/src/pylib/ecchronoslib/table_printer.py @@ -15,28 +15,47 @@ from __future__ import print_function from datetime import datetime -from ecchronoslib import table_formatter +from ecchronoslib import table_formatter, displaying -def print_schedule(schedule, max_lines, full=False): +def print_schedule(schedule, max_lines, full=False, colors="auto"): if not schedule.is_valid(): print('Schedule not found') return - verbose_print_format = "{0:15s}: {1}" - - print(verbose_print_format.format("Id", schedule.job_id)) - print(verbose_print_format.format("Keyspace", schedule.keyspace)) - print(verbose_print_format.format("Table", schedule.table)) - print(verbose_print_format.format("Status", schedule.status)) - print(verbose_print_format.format("Repaired(%)", schedule.get_repair_percentage())) - print(verbose_print_format.format("Completed at", schedule.get_last_repaired_at())) - print(verbose_print_format.format("Next repair", schedule.get_next_repair())) - print(verbose_print_format.format("Repair type", schedule.repair_type)) - print(verbose_print_format.format("Config", schedule.get_config())) + verbose_print_format = "{0:25s}: {1}" + + print(verbose_print_format.format( + displaying.color_key("Id", colors ), + displaying.color_str(schedule.job_id, colors, "UUID"))) + print(verbose_print_format.format( + displaying.color_key("Keyspace", colors ), + displaying.color_str(schedule.keyspace, colors, "TEXT"))) + print(verbose_print_format.format( + displaying.color_key("Table", colors ), + displaying.color_str(schedule.table, colors, "TEXT"))) + print(verbose_print_format.format( + displaying.color_key("Status", colors ), + displaying.color_key(schedule.status, colors))) + print(verbose_print_format.format( + displaying.color_key("Repaired(%)", colors ), + displaying.color_str(schedule.get_repair_percentage(), colors, "TEXT"))) + print(verbose_print_format.format( + displaying.color_key("Completed at", colors ), + displaying.color_str(schedule.get_last_repaired_at(), colors, "DATETIME"))) + print(verbose_print_format.format( + displaying.color_key("Next repair", colors ), + displaying.color_str(schedule.get_next_repair(), colors, "DATETIME"))) + print(verbose_print_format.format( + displaying.color_key("Repair type", colors ), + displaying.color_str(schedule.repair_type, colors, "TEXT"))) + print(verbose_print_format.format( + displaying.color_key("Config", colors ), + displaying.color_str(schedule.get_config(), colors, "Collection"))) if full: - vnode_state_table = [["Start token", "End token", "Replicas", "Repaired at", "Repaired"]] + vnode_index = ["Start token", "End token", "Replicas", "Repaired at", "Repaired"] + vnode_state_table = [displaying.color_index(vnode_index, colors)] sorted_vnode_states = sorted(schedule.vnode_states, key=lambda vnode: vnode.last_repaired_at_in_ms, reverse=True) @@ -45,14 +64,19 @@ def print_schedule(schedule, max_lines, full=False): sorted_vnode_states = sorted_vnode_states[:max_lines] for vnode_state in sorted_vnode_states: - _add_vnode_state_to_table(vnode_state, vnode_state_table) + _add_vnode_state_to_table(vnode_state, vnode_state_table, colors) - table_formatter.format_table(vnode_state_table) + table_formatter.format_table(vnode_state_table, displaying.should_color(colors)) -def _add_vnode_state_to_table(vnode_state, table): - entry = [vnode_state.start_token, vnode_state.end_token, ', '.join(vnode_state.replicas), - vnode_state.get_last_repaired_at(), vnode_state.repaired] +def _add_vnode_state_to_table(vnode_state, table, colors): + entry = [ + displaying.color_str(vnode_state.start_token, colors, "Start token"), + displaying.color_str(vnode_state.end_token, colors, "End token"), + displaying.color_str((', '.join(vnode_state.replicas)), colors, "Replicas"), + displaying.color_str(vnode_state.get_last_repaired_at(), colors, "DATETIME"), + displaying.color_str(vnode_state.repaired, colors, "Repaired") + ] table.append(entry) @@ -77,80 +101,99 @@ def print_repair_summary(repairs): status_list.count('ERROR'))) -def print_schedules(schedules, max_lines): - schedule_table = [["Id", "Keyspace", "Table", "Status", "Repaired(%)", - "Completed at", "Next repair", "Repair type"]] +def print_schedules(schedules, max_lines, colors): + summary = ["Id", "Keyspace", "Table", "Status", "Repaired(%)", + "Completed at", "Next repair", "Repair type"] + colored_summary = displaying.color_index(summary, colors) + schedule_table = [colored_summary] print("Snapshot as of", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - print_schedule_table(schedule_table, schedules, max_lines) + print_schedule_table(schedule_table, schedules, max_lines, colors) print_summary(schedules) -def print_repairs(repairs, max_lines=-1): - repair_table = [["Id", "Host Id", "Keyspace", "Table", "Status", "Repaired(%)", - "Completed at", "Repair type"]] - print_repair_table(repair_table, repairs, max_lines) +def print_repairs(repairs, max_lines=-1, colors="auto"): + summary = ["Id", "Host Id", "Keyspace", "Table", "Status", "Repaired(%)", + "Completed at", "Repair type"] + colored_summary = displaying.color_index(summary, colors) + repair_table = [colored_summary] + print_repair_table(repair_table, repairs, max_lines, colors) print_repair_summary(repairs) -def print_schedule_table(schedule_table, schedules, max_lines): +def print_schedule_table(schedule_table, schedules, max_lines, colors): sorted_schedules = sorted(schedules, key=lambda x: (x.last_repaired_at_in_ms, x.repaired_ratio), reverse=False) if max_lines > -1: sorted_schedules = sorted_schedules[:max_lines] for schedule in sorted_schedules: - schedule_table.append(_convert_schedule(schedule)) - table_formatter.format_table(schedule_table) + schedule_table.append(_convert_schedule(schedule, colors)) + table_formatter.format_table(schedule_table, displaying.should_color(colors)) -def print_repair_table(repair_table, repairs, max_lines): +def print_repair_table(repair_table, repairs, max_lines, colors): sorted_repairs = sorted(repairs, key=lambda x: (x.completed_at, x.repaired_ratio), reverse=False) if max_lines > -1: sorted_repairs = sorted_repairs[:max_lines] for repair in sorted_repairs: - repair_table.append(_convert_repair(repair)) - table_formatter.format_table(repair_table) + repair_table.append(_convert_repair(repair, colors)) + table_formatter.format_table(repair_table, displaying.should_color(colors)) -def print_repair(repair): +def print_repair(repair, colors): repair_table = [["Id", "Host Id", "Keyspace", "Table", "Status", "Repaired(%)", - "Completed at", "Repair type"], _convert_repair(repair)] - table_formatter.format_table(repair_table) - - -def _convert_repair(repair): - entry = [repair.job_id, repair.host_id, repair.keyspace, repair.table, repair.status, - repair.get_repair_percentage(), repair.get_completed_at(), repair.repair_type] + "Completed at", "Repair type"], _convert_repair(repair, colors)] + table_formatter.format_table(repair_table, colors) + + +def _convert_repair(repair, colors): + entry = [displaying.color_str(repair.job_id, colors, "UUID"), + displaying.color_str(repair.host_id, colors, "UUID"), + displaying.color_str(repair.keyspace, colors, "TEXT"), + displaying.color_str(repair.table, colors, "TEXT"), + displaying.color_key(repair.status, colors), + displaying.color_str(repair.get_repair_percentage(), colors, "TEXT"), + displaying.color_str(repair.get_completed_at(), colors, "DATETIME"), + displaying.color_str(repair.repair_type, colors, "TEXT")] return entry -def _convert_schedule(schedule): - entry = [schedule.job_id, schedule.keyspace, schedule.table, schedule.status, - schedule.get_repair_percentage(), schedule.get_last_repaired_at(), schedule.get_next_repair(), - schedule.repair_type] +def _convert_schedule(schedule, colors): + entry = [displaying.color_str(schedule.job_id, colors, "UUID"), + displaying.color_str(schedule.keyspace, colors, "TEXT"), + displaying.color_str(schedule.table, colors, "TEXT"), + displaying.color_key(schedule.status, colors), + displaying.color_str(schedule.get_repair_percentage(), colors, "TEXT"), + displaying.color_str(schedule.get_last_repaired_at(), colors, "DATETIME"), + displaying.color_str(schedule.get_next_repair(), colors, "DATETIME"), + displaying.color_str(schedule.repair_type, colors, "TEXT")] return entry -def print_repair_info(repair_info, max_lines=-1): +def print_repair_info(repair_info, max_lines=-1, colors="auto"): print("Time window between '{0}' and '{1}'".format(repair_info.get_since(), repair_info.get_to())) - print_repair_stats(repair_info.repair_stats, max_lines) + print_repair_stats(repair_info.repair_stats, max_lines, colors) -def print_repair_stats(repair_stats, max_lines=-1): - repair_stats_table = [["Keyspace", "Table", "Repaired (%)", - "Repair time taken"]] +def print_repair_stats(repair_stats, max_lines=-1, colors="auto"): + summary = ["Keyspace", "Table", "Repaired(%)", + "Repair time taken"] + colored_summary = displaying.color_index(summary, colors) + repair_stats_table = [colored_summary] sorted_repair_stats = sorted(repair_stats, key=lambda x: (x.repaired_ratio, x.keyspace, x.table), reverse=False) if max_lines > -1: sorted_repair_stats = sorted_repair_stats[:max_lines] for repair_stat in sorted_repair_stats: - repair_stats_table.append(_convert_repair_stat(repair_stat)) - table_formatter.format_table(repair_stats_table) + repair_stats_table.append(_convert_repair_stat(repair_stat, colors)) + table_formatter.format_table(repair_stats_table, displaying.should_color(colors)) -def _convert_repair_stat(repair_stat): - entry = [repair_stat.keyspace, repair_stat.table, repair_stat.get_repaired_percentage(), - repair_stat.get_repair_time_taken()] +def _convert_repair_stat(repair_stat, colors): + entry = [displaying.color_str(repair_stat.keyspace, colors, "TEXT"), + displaying.color_str(repair_stat.table, colors, "TEXT"), + displaying.color_str(repair_stat.get_repaired_percentage(), colors, "TEXT"), + displaying.color_str(repair_stat.get_repair_time_taken(), colors, "DATETIME")] return entry diff --git a/ecchronos-binary/src/test/behave/ecc_step_library/common.py b/ecchronos-binary/src/test/behave/ecc_step_library/common.py index f5c7523cc..22f54424a 100644 --- a/ecchronos-binary/src/test/behave/ecc_step_library/common.py +++ b/ecchronos-binary/src/test/behave/ecc_step_library/common.py @@ -81,6 +81,10 @@ def validate_last_table_row(rows): def get_job_id(context): out = context.out.decode('ascii') + print(f"LOG AQUI: {out}") + print(f"LOG AQUI: {out}") + print(f"LOG AQUI: {out}") + print(f"LOG AQUI: {out}") job_id = re.search(ID_PATTERN, out).group(0) assert job_id, "Could not find job id matching {0} in {1}".format(ID_PATTERN, out) return job_id diff --git a/ecchronos-binary/src/test/behave/features/steps/ecctool_repair_info.py b/ecchronos-binary/src/test/behave/features/steps/ecctool_repair_info.py index 5c67fd0e4..171cbfb64 100644 --- a/ecchronos-binary/src/test/behave/features/steps/ecctool_repair_info.py +++ b/ecchronos-binary/src/test/behave/features/steps/ecctool_repair_info.py @@ -22,7 +22,7 @@ def run_ecc_repair_info(context, params): - run_ecctool(context, ["repair-info"] + params) + run_ecctool(context, ["repair-info", "-c", "off"] + params) def handle_repair_info_output(context): diff --git a/ecchronos-binary/src/test/behave/features/steps/ecctool_repairs.py b/ecchronos-binary/src/test/behave/features/steps/ecctool_repairs.py index 9ee62457e..a5ee9b18b 100644 --- a/ecchronos-binary/src/test/behave/features/steps/ecctool_repairs.py +++ b/ecchronos-binary/src/test/behave/features/steps/ecctool_repairs.py @@ -18,7 +18,7 @@ def run_ecc_repair_status(context, params): - run_ecctool(context, ["repairs"] + params) + run_ecctool(context, ["repairs", "-c", "off"] + params) @when('we list all repairs') diff --git a/ecchronos-binary/src/test/behave/features/steps/ecctool_schedules.py b/ecchronos-binary/src/test/behave/features/steps/ecctool_schedules.py index ce1f75342..0c1ca636a 100644 --- a/ecchronos-binary/src/test/behave/features/steps/ecctool_schedules.py +++ b/ecchronos-binary/src/test/behave/features/steps/ecctool_schedules.py @@ -25,7 +25,7 @@ def run_ecc_schedule_status(context, params): - run_ecctool(context, ["schedules"] + params) + run_ecctool(context, ["schedules", "-c", "off"] + params) def handle_schedule_output(context): diff --git a/ecchronos-binary/src/test/bin/codestyle.sh b/ecchronos-binary/src/test/bin/codestyle.sh index fe261e626..41ad480fd 100644 --- a/ecchronos-binary/src/test/bin/codestyle.sh +++ b/ecchronos-binary/src/test/bin/codestyle.sh @@ -34,6 +34,8 @@ pip install behave pip install requests pip install jsonschema pip install cassandra-driver +pip install colorama +pip install supports-color for directory in "$@" do diff --git a/ecchronos-binary/src/test/bin/pytests.sh b/ecchronos-binary/src/test/bin/pytests.sh index 19804a1fa..debff2940 100755 --- a/ecchronos-binary/src/test/bin/pytests.sh +++ b/ecchronos-binary/src/test/bin/pytests.sh @@ -30,6 +30,8 @@ pip install behave pip install requests pip install jsonschema pip install cassandra-driver +pip install colorama +pip install supports-color BASE_DIR="$TEST_DIR"/ecchronos-binary-${project.version} CONF_DIR="$BASE_DIR"/conf