Skip to content

Commit

Permalink
feat: create and start Run from command line (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vitalii Melnychuk authored Apr 12, 2022
1 parent cb84334 commit 799dfb2
Show file tree
Hide file tree
Showing 8 changed files with 548 additions and 8 deletions.
3 changes: 3 additions & 0 deletions cli/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.formatting.provider": "black"
}
18 changes: 12 additions & 6 deletions cli/cli.py
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()
58 changes: 56 additions & 2 deletions cli/maestro_cli/commands/run.py
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)
104 changes: 104 additions & 0 deletions cli/maestro_cli/services/maestro_api/__init__.py
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)
121 changes: 121 additions & 0 deletions cli/maestro_cli/services/maestro_api/run.py
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",
)
63 changes: 63 additions & 0 deletions cli/maestro_cli/services/maestro_api/run_metric.py
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,
)
Loading

0 comments on commit 799dfb2

Please sign in to comment.