-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create and start Run from command line (#348)
- Loading branch information
Vitalii Melnychuk
authored
Apr 12, 2022
1 parent
cb84334
commit 799dfb2
Showing
8 changed files
with
548 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"python.formatting.provider": "black" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,30 @@ | ||
import argparse | ||
|
||
from maestro_cli.logging import Logger | ||
from maestro_cli.commands.run import run_command | ||
|
||
Logger.setup_logging() | ||
|
||
|
||
def default_func(args): | ||
parser.print_help() | ||
parser.print_help() | ||
|
||
|
||
parser = argparse.ArgumentParser(prog="Maestro CLI", usage="maestro-cli") | ||
parser.set_defaults(func=default_func) | ||
subparsers = parser.add_subparsers() | ||
|
||
|
||
parser_foo = subparsers.add_parser('run') | ||
parser_foo.add_argument('configuration_id', type=str, help="Run Configuration to start a test") | ||
parser_foo.set_defaults(func=run_command) | ||
parser_run = subparsers.add_parser("run") | ||
parser_run.add_argument( | ||
"configuration_id", type=str, help="Run Configuration to start a test" | ||
) | ||
parser_run.set_defaults(func=run_command) | ||
|
||
|
||
args = parser.parse_args() | ||
|
||
if args: | ||
args.func(args) | ||
args.func(args) | ||
else: | ||
parser.print_help() | ||
parser.print_help() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,57 @@ | ||
from time import sleep | ||
from maestro_cli.services.maestro_api.run import RunApi, RunStatus | ||
from maestro_cli.services.maestro_api.run_metric import RunMetricApi | ||
|
||
from maestro_cli.logging import Logger | ||
|
||
|
||
def run_command(args): | ||
print(args.configuration_id) | ||
pass | ||
""" | ||
Start test based on run_configuration_id from command line | ||
The command also monitors the status of created Run | ||
and provides regular feedback to the console. | ||
""" | ||
|
||
def output_last_metric(run_id): | ||
metrics = RunMetricApi.all(run_id) | ||
|
||
if len(metrics) > 0: | ||
|
||
def sort_func(metric): | ||
return metric.min_datetime | ||
|
||
metrics.sort(key=sort_func, reverse=True) | ||
# Last metric is not accurate, use one before as last | ||
last_metric = metrics[1] if len(metrics) > 1 else 0 | ||
|
||
total_count = last_metric.total_count | ||
success_count = last_metric.success_count | ||
errors = round(1 - success_count / total_count, 2) | ||
|
||
Logger.info(f"Hits: {total_count} req/s. Errors: {errors} %") | ||
else: | ||
Logger.info("Waiting for metrics....") | ||
|
||
def monitor_run_status(run_id): | ||
run = RunApi.get(run_id) | ||
|
||
if run.run_status == RunStatus.RUNNING.value: | ||
output_last_metric(run_id) | ||
else: | ||
Logger.info(f"Run status is '{run.run_status}'") | ||
if run.run_status not in [ | ||
RunStatus.ERROR.value, | ||
RunStatus.FINISHED.value, | ||
RunStatus.STOPPED.value, | ||
]: | ||
sleep(1) | ||
monitor_run_status(run_id) | ||
|
||
run_configuration_id = args.configuration_id | ||
run = RunApi.create(run_configuration_id) | ||
RunApi.start(run.id) # Generate events to start a test | ||
|
||
Logger.info(f"Run started. run_id='{run.id}'") | ||
|
||
monitor_run_status(run.id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import requests | ||
|
||
from maestro_cli.settings import MAESTRO_API_HOST, MAESTRO_API_TOKEN | ||
|
||
|
||
class MaestroApiClient: | ||
headers = { | ||
"Authorization": "Bearer %s" % MAESTRO_API_TOKEN, | ||
"User-Agent": "maestroagent", | ||
} | ||
|
||
@staticmethod | ||
def get(url, data={}, mapper=None): | ||
response = requests.get( | ||
"%s%s" % (MAESTRO_API_HOST, url), | ||
headers=MaestroApiClient.headers, | ||
params=data, | ||
) | ||
|
||
return MaestroApiClient.handle_response(response, mapper) | ||
|
||
@staticmethod | ||
def put(url, data={}, mapper=None): | ||
response = requests.put( | ||
"%s%s" % (MAESTRO_API_HOST, url), | ||
headers=MaestroApiClient.headers, | ||
json=data, | ||
) | ||
|
||
return MaestroApiClient.handle_response(response, mapper) | ||
|
||
@staticmethod | ||
def post(url, data={}, mapper=None): | ||
response = requests.post( | ||
"%s%s" % (MAESTRO_API_HOST, url), | ||
headers=MaestroApiClient.headers, | ||
json=data, | ||
) | ||
|
||
return MaestroApiClient.handle_response(response, mapper) | ||
|
||
@staticmethod | ||
def download_file(url, to_url): | ||
r = requests.get( | ||
"%s%s" % (MAESTRO_API_HOST, url), | ||
headers=MaestroApiClient.headers, | ||
) | ||
|
||
open(to_url, "wb").write(r.content) | ||
|
||
@staticmethod | ||
def upload_file(url, data, files, mapper): | ||
response = requests.put( | ||
"%s%s" % (MAESTRO_API_HOST, url), | ||
headers=MaestroApiClient.headers, | ||
data=data, | ||
files=files, | ||
) | ||
|
||
return MaestroApiClient.handle_response(response, mapper) | ||
|
||
@staticmethod | ||
def handle_response(response, mapper): | ||
# 2xx | ||
if response.status_code < 300: | ||
return MaestroApiClient.map_response_json(response, mapper) | ||
|
||
# 3xx | ||
if response.status_code < 400: | ||
return MaestroApiClient.handle_3xx(response) | ||
|
||
# 4xx | ||
if response.status_code < 500: | ||
return MaestroApiClient.handle_4xx(response) | ||
|
||
# 5xx | ||
return MaestroApiClient.handle_5xx(response) | ||
|
||
@staticmethod | ||
def map_response_json(response, mapper): | ||
json = response.json() | ||
if mapper is None: | ||
return json | ||
isarray = isinstance(json, list) | ||
if isarray: | ||
return [mapper(item) for item in json] | ||
else: | ||
return mapper(json) | ||
|
||
@staticmethod | ||
def handle3xx(respose): | ||
raise Exception(respose) | ||
|
||
@staticmethod | ||
def handle_4xx(respose): | ||
if respose.status_code == 403: | ||
json = respose.json() | ||
raise Exception("Bad request: %s" % json.get("error")) | ||
else: | ||
raise Exception(respose) | ||
|
||
@staticmethod | ||
def handle_5xx(respose): | ||
raise Exception(respose) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from enum import Enum | ||
|
||
import dateutil.parser | ||
|
||
from maestro_cli.services.maestro_api import MaestroApiClient | ||
|
||
|
||
class RunStatus(Enum): | ||
PENDING = "PENDING" | ||
CREATING = "CREATING" | ||
RUNNING = "RUNNING" | ||
STOPPED = "STOPPED" | ||
FINISHED = "FINISHED" | ||
ERROR = "ERROR" | ||
|
||
|
||
class RunHost: | ||
def __init__(self, host, ip): | ||
|
||
self.host = host | ||
self.ip = ip | ||
|
||
|
||
class RunCustomProperty: | ||
def __init__(self, name, value): | ||
|
||
self.name = name | ||
self.value = value | ||
|
||
|
||
class RunLoadProfile: | ||
def __init__(self, start, end, duration): | ||
|
||
self.start = start | ||
self.end = end | ||
self.duration = duration | ||
|
||
|
||
class Run: | ||
def __init__( | ||
self, | ||
id, | ||
run_status, | ||
run_plan_id, | ||
agent_ids, | ||
custom_data_ids, | ||
hosts, | ||
load_profile, | ||
custom_properties, | ||
created_at, | ||
updated_at, | ||
): | ||
self.id = id | ||
self.run_status = run_status | ||
self.run_plan_id = run_plan_id | ||
self.agent_ids = agent_ids | ||
self.custom_data_ids = custom_data_ids | ||
self.hosts = [ | ||
RunHost(host=host.get("host"), ip=host.get("ip")) for host in hosts | ||
] | ||
self.custom_properties = [ | ||
RunCustomProperty(name=prop.get("name"), value=prop.get("value")) | ||
for prop in custom_properties | ||
] | ||
self.load_profile = [ | ||
RunLoadProfile( | ||
start=step.get("start"), | ||
end=step.get("end"), | ||
duration=step.get("duration"), | ||
) | ||
for step in load_profile | ||
] | ||
self.created_at = created_at | ||
self.updated_at = updated_at | ||
|
||
|
||
class RunApi: | ||
@staticmethod | ||
def run_json_to_object(json): | ||
return Run( | ||
id=json.get("id"), | ||
run_status=json.get("run_status"), | ||
run_plan_id=json.get("run_plan_id"), | ||
agent_ids=json.get("agent_ids"), | ||
custom_data_ids=json.get("custom_data_ids"), | ||
hosts=json.get("hosts"), | ||
custom_properties=json.get("custom_properties"), | ||
load_profile=json.get("load_profile"), | ||
created_at=dateutil.parser.parse(json.get("created_at")), | ||
updated_at=dateutil.parser.parse(json.get("updated_at")), | ||
) | ||
|
||
@staticmethod | ||
def get(run_id): | ||
|
||
return MaestroApiClient.get( | ||
"/api/run/%s" % run_id, mapper=RunApi.run_json_to_object | ||
) | ||
|
||
@staticmethod | ||
def create(run_configuration_id): | ||
return MaestroApiClient.post( | ||
"/api/run", | ||
data={"run_configuration_id": run_configuration_id}, | ||
mapper=RunApi.run_json_to_object, | ||
) | ||
|
||
@staticmethod | ||
def update(run_id, run_status): | ||
|
||
return MaestroApiClient.put( | ||
"/api/run/%s" % run_id, | ||
data={"run_status": run_status}, | ||
mapper=RunApi.run_json_to_object, | ||
) | ||
|
||
@staticmethod | ||
def start(run_id): | ||
return MaestroApiClient.post( | ||
f"/api/run_status/{run_id}/start", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from enum import Enum | ||
|
||
import dateutil.parser | ||
|
||
from maestro_cli.services.maestro_api import MaestroApiClient | ||
|
||
|
||
class RunStatus(Enum): | ||
PENDING = "PENDING" | ||
CREATING = "CREATING" | ||
RUNNING = "RUNNING" | ||
STOPPED = "STOPPED" | ||
FINISHED = "FINISHED" | ||
ERROR = "ERROR" | ||
|
||
|
||
class RunMetric: | ||
def __init__( | ||
self, | ||
latency_avg, | ||
latency_p99, | ||
latency_p95, | ||
latency_p90, | ||
latency_p50, | ||
success_count, | ||
total_count, | ||
min_datetime, | ||
max_datetime, | ||
): | ||
self.latency_avg = latency_avg | ||
self.latency_p99 = latency_p99 | ||
self.latency_p95 = latency_p95 | ||
self.latency_p90 = latency_p90 | ||
self.latency_p50 = latency_p50 | ||
self.success_count = success_count | ||
self.total_count = total_count | ||
self.min_datetime = min_datetime | ||
self.max_datetime = max_datetime | ||
|
||
|
||
class RunMetricApi: | ||
@staticmethod | ||
def run_metric_json_to_object(json): | ||
return RunMetric( | ||
latency_avg=json.get("latency_avg"), | ||
latency_p99=json.get("latency_p99"), | ||
latency_p95=json.get("latency_p95"), | ||
latency_p90=json.get("latency_p90"), | ||
latency_p50=json.get("latency_p50"), | ||
success_count=json.get("success_count"), | ||
total_count=json.get("total_count"), | ||
min_datetime=dateutil.parser.parse(json.get("min_datetime")), | ||
max_datetime=dateutil.parser.parse(json.get("max_datetime")), | ||
) | ||
|
||
@staticmethod | ||
def all(run_id, time_interval=15): | ||
|
||
return MaestroApiClient.get( | ||
"/api/run_metrics/%s" % run_id, | ||
data={"time_interval": time_interval}, | ||
mapper=RunMetricApi.run_metric_json_to_object, | ||
) |
Oops, something went wrong.