Skip to content

Commit 14d1afd

Browse files
author
Jeny Sadadia
committed
Implement command to validate builds
Get builds from dashboard and validate them with maestro result. Command introduced with the commit: `kci-dev validate build --all-checkouts --giturl <URL> \ --commit <commit-hash> --branch <git-branch>` Provide `--all-checkouts` to build validation stats for all available checkouts. Build validation for a specific checkout can be performed by using all three options: --giturl, --branch, and --commit. The command will provide below information: 1. Build count validation 2. Missing build information 3. Build status validation Signed-off-by: Jeny Sadadia <[email protected]>
1 parent 0ac11ac commit 14d1afd

File tree

8 files changed

+260
-20
lines changed

8 files changed

+260
-20
lines changed

kcidev/libs/common.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33

4+
import json
45
import logging
56
import os
67
import sys
@@ -142,3 +143,7 @@ def kci_msg_yellow(content, nl=True):
142143

143144
def kci_msg_cyan(content, nl=True):
144145
click.secho(content, fg="cyan", nl=nl)
146+
147+
148+
def kci_msg_json(content, indent=1):
149+
click.echo(json.dumps(content, indent=indent))

kcidev/libs/maestro_common.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,29 @@ def maestro_get_node(url, nodeid):
8080
return node_data
8181

8282

83-
def maestro_get_nodes(url, limit, offset, filter):
83+
def maestro_get_nodes(url, limit, offset, filter, paginate):
8484
headers = {
8585
"Content-Type": "application/json; charset=utf-8",
8686
}
87-
url = url + "latest/nodes/fast?limit=" + str(limit) + "&offset=" + str(offset)
8887

89-
logging.info(f"Fetching Maestro nodes - limit: {limit}, offset: {offset}")
90-
if filter:
91-
logging.debug(f"Applying filters: {filter}")
92-
for f in filter:
93-
# TBD: We need to add translate filter to API
94-
# if we need anything more complex than eq(=)
95-
url = url + "&" + f
88+
if paginate:
89+
url = url + "latest/nodes/fast?limit=" + str(limit) + "&offset=" + str(offset)
90+
logging.info(f"Fetching Maestro nodes - limit: {limit}, offset: {offset}")
91+
if filter:
92+
for f in filter:
93+
logging.debug(f"Applying filters: {filter}")
94+
# TBD: We need to add translate filter to API
95+
# if we need anything more complex than eq(=)
96+
url = url + "&" + f
9697

98+
else:
99+
url = url + "latest/nodes/fast"
100+
if filter:
101+
url = url + "?"
102+
for f in filter:
103+
# TBD: We need to add translate filter to API
104+
# if we need anything more complex than eq(=)
105+
url = url + "&" + f
97106
logging.debug(f"Full nodes URL: {url}")
98107
maestro_print_api_call(url)
99108

kcidev/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
maestro_results,
1515
results,
1616
testretry,
17+
validate,
1718
watch,
1819
)
1920

@@ -63,6 +64,7 @@ def run():
6364
cli.add_command(maestro_results.maestro_results)
6465
cli.add_command(testretry.testretry)
6566
cli.add_command(results.results)
67+
cli.add_command(validate.validate)
6668
cli.add_command(watch.watch)
6769
cli()
6870

kcidev/subcommands/maestro_results.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@
6565
required=False,
6666
help="Filter results by tree name",
6767
)
68+
@click.option(
69+
"--count",
70+
is_flag=True,
71+
required=False,
72+
help="Print only count of nodes",
73+
)
74+
@click.option(
75+
"--paginate",
76+
is_flag=True,
77+
required=False,
78+
default=True,
79+
help="Set True if pagination is required in the output. Default is True",
80+
)
6881
@add_filter_options
6982
@click.pass_context
7083
def maestro_results(
@@ -82,6 +95,8 @@ def maestro_results(
8295
compiler,
8396
config,
8497
git_branch,
98+
count,
99+
paginate,
85100
):
86101
logging.info("Starting maestro-results command")
87102
logging.debug(
@@ -125,9 +140,14 @@ def maestro_results(
125140
logging.info(
126141
f"Fetching nodes with {len(filter)} filters, limit: {limit}, offset: {offset}"
127142
)
128-
results = maestro_get_nodes(url, limit, offset, filter)
143+
results = maestro_get_nodes(url, limit, offset, filter, paginate)
144+
if count:
145+
kci_msg_cyan("Maestro:")
146+
kci_msg(f"Build count:{len(results)}")
147+
return results
129148

130149
logging.debug(f"Displaying results with fields: {field if field else 'all'}")
150+
131151
maestro_print_nodes(results, field)
132152

133153

kcidev/subcommands/results/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,16 @@ def summary(origin, git_folder, giturl, branch, commit, latest, arch, tree, use_
9898
type=int,
9999
default=7,
100100
)
101+
@click.option(
102+
"--verbose",
103+
is_flag=True,
104+
default=True,
105+
help="Print tree details",
106+
)
101107
@results_display_options
102-
def trees(origin, use_json, days):
108+
def trees(origin, use_json, days, verbose):
103109
"""List trees from a give origin."""
104-
cmd_list_trees(origin, use_json, days)
110+
return cmd_list_trees(origin, use_json, days, verbose)
105111

106112

107113
@results.command()
@@ -139,7 +145,7 @@ def builds(
139145
data = dashboard_fetch_builds(
140146
origin, giturl, branch, commit, arch, tree, start_date, end_date, use_json
141147
)
142-
cmd_builds(
148+
return cmd_builds(
143149
data,
144150
commit,
145151
download_logs,

kcidev/subcommands/results/parser.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,21 +149,24 @@ def get_command_summary(command_data):
149149
return inconclusive_cmd, pass_cmd, fail_cmd
150150

151151

152-
def cmd_list_trees(origin, use_json, days):
152+
def cmd_list_trees(origin, use_json, days, verbose):
153153
logging.info(f"Listing trees for origin: {origin}")
154154
trees = dashboard_fetch_tree_list(origin, use_json, days)
155155
logging.debug(f"Found {len(trees)} trees")
156156
if use_json:
157157
kci_msg(json.dumps(list(map(lambda t: create_tree_json(t), trees))))
158158
return
159-
for t in trees:
159+
160+
if verbose:
160161
logging.debug(
161162
f"Tree: {t['tree_name']}/{t['git_repository_branch']} - {t['git_commit_hash']}"
162163
)
163-
kci_msg_green(f"- {t['tree_name']}/{t['git_repository_branch']}:")
164-
kci_msg(f" giturl: {t['git_repository_url']}")
165-
kci_msg(f" latest: {t['git_commit_hash']} ({t['git_commit_name']})")
166-
kci_msg(f" latest: {t['start_time']}")
164+
for t in trees:
165+
kci_msg_green(f"- {t['tree_name']}/{t['git_repository_branch']}:")
166+
kci_msg(f" giturl: {t['git_repository_url']}")
167+
kci_msg(f" latest: {t['git_commit_hash']} ({t['git_commit_name']})")
168+
kci_msg(f" latest: {t['start_time']}")
169+
return trees
167170

168171

169172
def cmd_builds(
@@ -219,9 +222,11 @@ def cmd_builds(
219222
if count and use_json:
220223
kci_msg(f'{{"count":{filtered_builds}}}')
221224
elif count:
222-
kci_msg(filtered_builds)
225+
kci_msg_cyan("Dashboard:")
226+
kci_msg(f"Build count:{filtered_builds}")
223227
elif use_json:
224228
kci_msg(json.dumps(builds))
229+
return data["builds"]
225230

226231

227232
def print_build(build, log_path):
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import click
2+
3+
from kcidev.libs.common import kci_msg_yellow
4+
from kcidev.libs.git_repo import set_giturl_branch_commit
5+
from kcidev.subcommands.results import trees
6+
7+
from .helper import get_build_stats
8+
9+
10+
@click.group(
11+
help="Get results from the dashboard and validate them with maestro",
12+
)
13+
def validate():
14+
"""Commands related to results validation"""
15+
16+
17+
@validate.command()
18+
@click.option(
19+
"--all-checkouts",
20+
is_flag=True,
21+
help="Get build validation stats for all available checkouts",
22+
)
23+
@click.option(
24+
"--days",
25+
help="Provide a period of time in days to get results for",
26+
type=int,
27+
default="7",
28+
)
29+
@click.option(
30+
"--origin",
31+
help="Select KCIDB origin",
32+
default="maestro",
33+
)
34+
@click.option(
35+
"--giturl",
36+
help="Git URL of kernel tree",
37+
)
38+
@click.option(
39+
"--branch",
40+
help="Branch to get results for",
41+
)
42+
@click.option(
43+
"--commit",
44+
help="Commit or tag to get results for",
45+
)
46+
@click.option(
47+
"--git-folder",
48+
help="Path of git repository folder",
49+
)
50+
@click.option(
51+
"--latest",
52+
is_flag=True,
53+
help="Select latest results available",
54+
)
55+
@click.option("--arch", help="Filter by arch")
56+
@click.option(
57+
"--verbose",
58+
is_flag=True,
59+
default=False,
60+
help="Get detailed output",
61+
)
62+
@click.pass_context
63+
def build(
64+
ctx,
65+
all_checkouts,
66+
origin,
67+
giturl,
68+
branch,
69+
commit,
70+
git_folder,
71+
latest,
72+
arch,
73+
days,
74+
verbose,
75+
):
76+
"""
77+
Provide --all-checkouts flag to pull all builds of available
78+
checkouts from dashboard and compare them with results from maestro.
79+
80+
Build validation for a specific checkout can be performed by
81+
using all three options: --giturl, --branch, and --commit
82+
If above options are not provided, if will take latest/provided commit
83+
checkout from the git folder specified.
84+
"""
85+
if all_checkouts:
86+
if giturl or branch or commit:
87+
raise click.UsageError(
88+
"Cannot use --all-checkouts with --giturl, --branch, or --commit"
89+
)
90+
trees_list = ctx.invoke(trees, origin=origin, days=days, verbose=False)
91+
for tree in trees_list:
92+
giturl = tree["git_repository_url"]
93+
branch = tree["git_repository_branch"]
94+
commit = tree["git_commit_hash"]
95+
kci_msg_yellow(f"URL: {giturl}\nbranch: {branch}\ncommit: {commit}")
96+
get_build_stats(ctx, giturl, branch, commit, verbose, arch)
97+
else:
98+
giturl, branch, commit = set_giturl_branch_commit(
99+
origin, giturl, branch, commit, latest, git_folder
100+
)
101+
get_build_stats(ctx, giturl, branch, commit, verbose, arch)
102+
103+
104+
if __name__ == "__main__":
105+
main_kcidev()

kcidev/subcommands/validate/helper.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
3+
import click
4+
5+
from kcidev.libs.common import kci_msg, kci_msg_green, kci_msg_json, kci_msg_red
6+
from kcidev.subcommands.maestro_results import maestro_results
7+
from kcidev.subcommands.results import builds
8+
9+
10+
def get_builds(ctx, giturl, branch, commit, arch=None):
11+
"""Get builds matching git URL, branch, and commit
12+
Architecture can also be provided for filtering"""
13+
maestro_builds = []
14+
dashboard_builds = []
15+
filters = [
16+
"kind=kbuild",
17+
"data.error_code__ne=node_timeout", # maestro doesn't submit timed-out nodes
18+
"data.kernel_revision.url=" + giturl,
19+
"data.kernel_revision.branch=" + branch,
20+
"data.kernel_revision.commit=" + commit,
21+
"state__in=done,available",
22+
]
23+
if arch:
24+
filters.append("data.arch=" + arch)
25+
maestro_builds = ctx.invoke(
26+
maestro_results,
27+
count=True,
28+
nodes=True,
29+
filter=filters,
30+
paginate=False,
31+
)
32+
try:
33+
dashboard_builds = ctx.invoke(
34+
builds, giturl=giturl, branch=branch, commit=commit, count=True
35+
)
36+
except click.Abort:
37+
kci_msg_red("Aborted while fetching dashboard builds")
38+
return maestro_builds, None
39+
return maestro_builds, dashboard_builds
40+
41+
42+
def find_missing_builds(maestro_builds, dashboard_builds, verbose):
43+
"""
44+
Compare build IDs found in maestro with dashboard builds
45+
Return missing builds in dashboard.
46+
"""
47+
missing_builds = []
48+
dashboard_build_id = [b["id"].split(":")[1] for b in dashboard_builds]
49+
for b in maestro_builds:
50+
if b["id"] not in dashboard_build_id:
51+
missing_builds.append(b)
52+
if missing_builds:
53+
if verbose:
54+
kci_msg("Missing builds:")
55+
kci_msg_json(missing_builds)
56+
else:
57+
missing_build_ids = [b["id"] for b in missing_builds]
58+
kci_msg(f"Missing build IDs:{missing_build_ids}")
59+
60+
61+
def validate_build_status(maestro_builds, dashboard_builds):
62+
"""
63+
Validate if the build status of dashboard pulled build
64+
matches with the maestro build status
65+
"""
66+
status_map = {
67+
"pass": "PASS",
68+
"fail": "FAIL",
69+
"incomplete": "ERROR",
70+
}
71+
dashboard_build_status_dict = {b["id"]: b["status"] for b in dashboard_builds}
72+
for b in maestro_builds:
73+
build_id = f"maestro:{b['id']}"
74+
if build_id in dashboard_build_status_dict:
75+
if dashboard_build_status_dict[build_id] != status_map.get(b["result"]):
76+
kci_msg_red(f"Build status mismatched: {build_id}")
77+
78+
79+
def get_build_stats(ctx, giturl, branch, commit, verbose, arch=None):
80+
"""Get build stats"""
81+
maestro_builds, dashboard_builds = get_builds(ctx, giturl, branch, commit, arch)
82+
if dashboard_builds is not None:
83+
if len(dashboard_builds) == len(maestro_builds):
84+
kci_msg_green("Build count matched")
85+
else:
86+
kci_msg_red("Build count didn't match")
87+
find_missing_builds(maestro_builds, dashboard_builds, verbose)
88+
validate_build_status(maestro_builds, dashboard_builds)

0 commit comments

Comments
 (0)