Skip to content

Commit e0747e8

Browse files
mandrysmsobkowski
andcommitted
[#84714] Add an app sending requests to a protoplaster server
Co-authored-by: Maciej Sobkowski <msobkowski@antmicro.com> Signed-off-by: Maciej Sobkowski <msobkowski@antmicro.com>
1 parent c52912e commit e0747e8

File tree

8 files changed

+344
-1
lines changed

8 files changed

+344
-1
lines changed

.ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ check-code-quality:
1111
- apt -y update
1212
- apt -y install python3 pipx git
1313
- pipx install yapf==0.40.2
14-
- yapf -ipr protoplaster/
14+
- yapf -ipr protoplaster/ manager/
1515
- test $(git status --porcelain | wc -l) -eq 0 || { git diff; false; }
1616

1717
installation-test:

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,30 @@ ip:
296296
### Running as root
297297
By default, `sudo` doesn't preserve `PATH`.
298298
To run `protoplaster-system-report` installed by a non-root user as root, invoke `protoplaster-system-report --sudo`
299+
300+
## Protoplaster manager
301+
Protoplaster provides `protoplaster-mgmt`, a tool to remotely control Protoplaster via the API.
302+
For more detailed information, see the help messages associated with each subcommand.
303+
304+
```
305+
usage: protoplaster-mgmt [-h] [--url URL] [--config CONFIG] [--config-dir CONFIG_DIR] [--report-dir REPORT_DIR] [--artifact-dir ARTIFACT_DIR] {configs,runs} ...
306+
307+
Tool for managing Protoplaster via remote API
308+
309+
options:
310+
-h, --help show this help message and exit
311+
--url URL URL to a device running Protoplaster server (default: http://127.0.0.1:5000/)
312+
--config CONFIG Config file with values for url, config-dir, report-dir, artifact-dir
313+
--config-dir CONFIG_DIR
314+
Directory to save fetched config (default: ./)
315+
--report-dir REPORT_DIR
316+
Directory to save a test report (default: ./)
317+
--artifact-dir ARTIFACT_DIR
318+
Directory to save a test artifact (default: ./)
319+
320+
available commands:
321+
{configs,runs}
322+
configs Configs management
323+
runs Test runs management
324+
325+
```

manager/__init__.py

Whitespace-only changes.

manager/configs.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import argparse
2+
import json
3+
import os.path
4+
import requests
5+
from pathlib import Path
6+
7+
StrPath = str | Path
8+
9+
10+
def check_status(response: requests.models.Response):
11+
if response.status_code != 200:
12+
print(f"Error {response.status_code}: {response.json()['error']}")
13+
return True
14+
return False
15+
16+
17+
def configs_list(args):
18+
r = requests.get(f"{args.url}/api/v1/configs")
19+
if check_status(r):
20+
return
21+
print(f"Available configs:")
22+
print(json.dumps(r.json(), indent=4))
23+
24+
25+
def config_upload(args):
26+
try:
27+
files = {'file': open(args.path, 'rb')}
28+
except Exception as e:
29+
print(f"Failed to open the config to upload: {str(e)}")
30+
return
31+
r = requests.post(f"{args.url}/api/v1/configs", files=files)
32+
if check_status(r):
33+
return
34+
print(f"Uploaded `{args.path}`")
35+
36+
37+
def config_info(args):
38+
r = requests.get(f"{args.url}/api/v1/configs/{args.name}")
39+
if check_status(r):
40+
return
41+
print(f"Config info fetched from the server:")
42+
print(json.dumps(r.json(), indent=4))
43+
44+
45+
def config_fetch(args):
46+
r = requests.get(f"{args.url}/api/v1/configs/{args.name}/file")
47+
if check_status(r):
48+
return
49+
save_path = os.path.join(args.config_dir, args.name)
50+
try:
51+
with open(save_path, "wb") as file:
52+
file.write(r.content)
53+
print(f"Config written to {save_path}")
54+
except Exception as e:
55+
print(f"Failed to save the config: {str(e)}")
56+
57+
58+
def config_delete(args):
59+
r = requests.delete(f"{args.url}/api/v1/configs/{args.name}")
60+
if check_status(r):
61+
return
62+
print(f"Config '{args.name}' deleted")
63+
64+
65+
def add_configs_parser(parser: argparse._SubParsersAction):
66+
configs = parser.add_parser("configs", help="Configs management")
67+
sub = configs.add_subparsers(required=True, title="Configs commands")
68+
69+
list = sub.add_parser("list", help="List available test configs")
70+
list.set_defaults(func=configs_list)
71+
72+
upload = sub.add_parser("upload", help="Upload a test config")
73+
upload.set_defaults(func=config_upload)
74+
upload.add_argument("--path",
75+
type=str,
76+
required=True,
77+
help="Path to a test config file")
78+
79+
info = sub.add_parser("info", help="Information about a test config")
80+
info.set_defaults(func=config_info)
81+
info.add_argument("--name",
82+
type=str,
83+
required=True,
84+
help="Test config name")
85+
86+
fetch = sub.add_parser("fetch", help="Fetch a test config file")
87+
fetch.set_defaults(func=config_fetch)
88+
fetch.add_argument("--name",
89+
type=str,
90+
required=True,
91+
help="Test config name")
92+
93+
delete = sub.add_parser("delete", help="Delete a test config file")
94+
delete.set_defaults(func=config_delete)
95+
delete.add_argument("--name",
96+
type=str,
97+
required=True,
98+
help="Test config name")

manager/defaults.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
url: http://127.0.0.1:5000
2+
config-dir: configs
3+
report-dir: reports
4+
artifact-dir: artifacts

manager/protoplaster_mgmt.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import os
5+
import requests
6+
import sys
7+
import traceback
8+
import yaml
9+
10+
import manager.configs
11+
import manager.runs
12+
13+
14+
def main():
15+
parser = argparse.ArgumentParser(
16+
description="Tool for managing Protoplaster via remote API", )
17+
18+
parser.add_argument(
19+
"--url",
20+
default="http://127.0.0.1:5000/",
21+
help=
22+
"URL to a device running Protoplaster server (default: http://127.0.0.1:5000/)",
23+
)
24+
parser.add_argument(
25+
"--config",
26+
help=
27+
"Config file with values for url, config-dir, report-dir, artifact-dir",
28+
)
29+
parser.add_argument(
30+
"--config-dir",
31+
default="./",
32+
metavar="CONFIG_DIR",
33+
help="Directory to save fetched config (default: ./)",
34+
)
35+
parser.add_argument(
36+
"--report-dir",
37+
default="./",
38+
help="Directory to save a test report (default: ./)",
39+
)
40+
parser.add_argument(
41+
"--artifact-dir",
42+
default="./",
43+
help="Directory to save a test artifact (default: ./)",
44+
)
45+
46+
subparsers = parser.add_subparsers(required=True,
47+
title="available commands")
48+
49+
# Add all subparsers for the different subcommands
50+
manager.configs.add_configs_parser(subparsers)
51+
manager.runs.add_runs_parser(subparsers)
52+
# Wrap argv so when no arguments are passed, we inject a help screen
53+
wrapped_args = None if sys.argv[1:] else ["--help"]
54+
55+
args = parser.parse_args(args=wrapped_args)
56+
57+
config = {}
58+
if args.config is not None:
59+
try:
60+
with open(args.config) as file:
61+
config = yaml.safe_load(file)
62+
except Exception as e:
63+
print(f"Failed to read config: {e}")
64+
exit(1)
65+
66+
url = config.get("url", args.url)
67+
config_dir = config.get("config-dir", args.config_dir)
68+
report_dir = config.get("report-dir", args.report_dir)
69+
artifact_dir = config.get("artifact-dir", args.artifact_dir)
70+
71+
try:
72+
ret = args.func(args)
73+
if ret is not None:
74+
print("protoplaster-mgmt:", ret)
75+
exit(1)
76+
except requests.ConnectionError as e:
77+
print("protoplaster-mgmt: Connection to the server failed:", e)
78+
exit(1)
79+
except requests.Timeout as e:
80+
print("protoplaster-mgmt: Server request timed out:", e)
81+
exit(1)
82+
except RuntimeError as e:
83+
print("protoplaster-mgmt:", e)
84+
exit(1)
85+
except Exception:
86+
traceback.print_exc()
87+
print("protoplaster-mgmt: Unhandled exception!")
88+
exit(1)
89+
90+
91+
if __name__ == "__main__":
92+
main()

manager/runs.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import argparse
2+
import json
3+
import os.path
4+
import requests
5+
from pathlib import Path
6+
7+
StrPath = str | Path
8+
9+
10+
def check_status(response: requests.models.Response):
11+
if response.status_code != 200:
12+
print(f"Error {response.status_code}: {response.json()['error']}")
13+
return True
14+
return False
15+
16+
17+
def test_runs_list(args):
18+
r = requests.get(f"{args.url}/api/v1/test-runs")
19+
if check_status(r):
20+
return
21+
print("Tests runs on the server:")
22+
print(json.dumps(r.json(), indent=4))
23+
24+
25+
def test_run_trigger(args):
26+
params = {
27+
"config_name": args.test_config,
28+
"test_suite_name": args.test_suite or ""
29+
}
30+
r = requests.post(f"{args.url}/api/v1/test-runs", json=params)
31+
if check_status(r):
32+
return
33+
print("Triggered a test run:")
34+
print(json.dumps(r.json(), indent=4))
35+
36+
37+
def test_run_info(args):
38+
r = requests.get(f"{args.url}/api/v1/test-runs/{args.id}")
39+
if check_status(r):
40+
return
41+
print(f"Test info fetched from the server:")
42+
print(json.dumps(r.json(), indent=4))
43+
44+
45+
def test_run_abort(args):
46+
r = requests.delete(f"{args.url}/api/v1/test-runs/{args.id}")
47+
if check_status(r):
48+
return
49+
print(f"Test deleted from the server:")
50+
print(json.dumps(r.json(), indent=4))
51+
52+
53+
def test_run_report(args):
54+
r = requests.get(f"{args.url}/api/v1/test-runs/{args.id}/report")
55+
if check_status(r):
56+
return
57+
save_path = os.path.join(args.report_dir, f"{args.id}.csv")
58+
try:
59+
with open(save_path, "wb") as file:
60+
file.write(r.content)
61+
print(f"Report written to {save_path}")
62+
except Exception as e:
63+
print(f"Failed to save the report: {str(e)}")
64+
65+
66+
def test_run_artifact(args):
67+
r = requests.get(
68+
f"{args.url}/api/v1/test-runs/{args.id}/artifacts/{args.name}")
69+
if check_status(r):
70+
return
71+
save_path = os.path.join(args.artifact_dir, args.name)
72+
try:
73+
with open(save_path, "wb") as file:
74+
file.write(r.content)
75+
print(f"Artifact written to {save_path}")
76+
except Exception as e:
77+
print(f"Failed to save the artifact: {str(e)}")
78+
79+
80+
def add_runs_parser(parser: argparse._SubParsersAction):
81+
82+
runs = parser.add_parser("runs", help="Test runs management")
83+
sub = runs.add_subparsers(required=True, title="Test runs commands")
84+
85+
list = sub.add_parser("list", help="List test runs")
86+
list.set_defaults(func=test_runs_list)
87+
88+
run = sub.add_parser("run", help="Trigger a test run")
89+
run.set_defaults(func=test_run_trigger)
90+
run.add_argument(
91+
"--test-config",
92+
required=True,
93+
help="Test config name for this test run",
94+
)
95+
run.add_argument(
96+
"--test-suite",
97+
help="Test suite name for this test run",
98+
)
99+
100+
info = sub.add_parser("info", help="Information about a test run")
101+
info.set_defaults(func=test_run_info)
102+
info.add_argument("--id", type=str, required=True, help="Test run ID")
103+
104+
abort = sub.add_parser("abort", help="Abort a pending test run")
105+
abort.set_defaults(func=test_run_abort)
106+
abort.add_argument("--id", type=str, required=True, help="Test run ID")
107+
108+
report = sub.add_parser("report", help="Fetch test run report")
109+
report.set_defaults(func=test_run_report)
110+
report.add_argument("--id", type=str, required=True, help="Test run ID")
111+
112+
artifact = sub.add_parser("artifact", help="Fetch test run artifact")
113+
artifact.set_defaults(func=test_run_artifact)
114+
artifact.add_argument("--id", type=str, required=True, help="Test run ID")
115+
artifact.add_argument("--name",
116+
type=str,
117+
required=True,
118+
help="Artifact name")

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ authors=[{name = "Antmicro Ltd"}]
66
license={text = "Apache Software License (http://www.apache.org/licenses/LICENSE-2.0)"}
77
dynamic = ["dependencies"]
88

9+
[tool.setuptools.packages.find]
10+
include = ["protoplaster*", "manager*"]
11+
912
[project.scripts]
1013
protoplaster = 'protoplaster.protoplaster:main'
1114
protoplaster-test-report = 'protoplaster.report_generators.test_report.protoplaster_test_report:main'
1215
protoplaster-system-report = 'protoplaster.report_generators.system_report.protoplaster_system_report:main'
16+
protoplaster-mgmt = 'manager.protoplaster_mgmt:main'
1317

1418
[tool.setuptools.dynamic]
1519
dependencies = {file = ["requirements.txt"]}

0 commit comments

Comments
 (0)