|
|
|
#!/usr/bin/env python3
|
|
|
|
# Copyright 2021 The gRPC Authors
|
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
# This script generates a load test configuration template from a collection of
|
|
|
|
# load test configurations.
|
|
|
|
#
|
|
|
|
# Configuration templates contain client and server configurations for multiple
|
|
|
|
# languages, and may contain template substitution keys. These templates are
|
|
|
|
# used to generate load test configurations by selecting clients and servers for
|
|
|
|
# the required languages. The source files for template generation may be load
|
|
|
|
# test configurations or load test configuration templates. Load test
|
|
|
|
# configuration generation is performed by loadtest_config.py. See documentation
|
|
|
|
# below:
|
|
|
|
# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
from typing import Any, Dict, Iterable, List, Mapping, Type
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
import loadtest_config
|
|
|
|
|
|
|
|
TEMPLATE_FILE_HEADER_COMMENT = """
|
|
|
|
# Template generated from load test configurations by loadtest_template.py.
|
|
|
|
#
|
|
|
|
# Configuration templates contain client and server configurations for multiple
|
|
|
|
# languages, and may contain template substitution keys. These templates are
|
|
|
|
# used to generate load test configurations by selecting clients and servers for
|
|
|
|
# the required languages. The source files for template generation may be load
|
|
|
|
# test configurations or load test configuration templates. Load test
|
|
|
|
# configuration generation is performed by loadtest_config.py. See documentation
|
|
|
|
# below:
|
|
|
|
# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def insert_worker(
|
|
|
|
worker: Dict[str, Any], workers: List[Dict[str, Any]]
|
|
|
|
) -> None:
|
|
|
|
"""Inserts client or server into a list, without inserting duplicates."""
|
|
|
|
|
|
|
|
def dump(w):
|
|
|
|
return yaml.dump(w, Dumper=yaml.SafeDumper, default_flow_style=False)
|
|
|
|
|
|
|
|
worker_str = dump(worker)
|
|
|
|
if any((worker_str == dump(w) for w in workers)):
|
|
|
|
return
|
|
|
|
workers.append(worker)
|
|
|
|
|
|
|
|
|
|
|
|
def uniquify_workers(workermap: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
|
|
"""Name workers if there is more than one for the same map key."""
|
|
|
|
for workers in list(workermap.values()):
|
|
|
|
if len(workers) <= 1:
|
|
|
|
continue
|
|
|
|
for i, worker in enumerate(workers):
|
|
|
|
worker["name"] = str(i)
|
|
|
|
|
|
|
|
|
|
|
|
def loadtest_template(
|
|
|
|
input_file_names: Iterable[str],
|
|
|
|
metadata: Mapping[str, Any],
|
|
|
|
inject_client_pool: bool,
|
|
|
|
inject_driver_image: bool,
|
|
|
|
inject_driver_pool: bool,
|
|
|
|
inject_server_pool: bool,
|
|
|
|
inject_big_query_table: bool,
|
|
|
|
inject_timeout_seconds: bool,
|
|
|
|
inject_ttl_seconds: bool) -> Dict[str, Any]: # fmt: skip
|
|
|
|
"""Generates the load test template."""
|
|
|
|
spec = dict() # type: Dict[str, Any]
|
|
|
|
clientmap = dict() # Dict[str, List[Dict[str, Any]]]
|
|
|
|
servermap = dict() # Dict[Str, List[Dict[str, Any]]]
|
|
|
|
template = {
|
|
|
|
"apiVersion": "e2etest.grpc.io/v1",
|
|
|
|
"kind": "LoadTest",
|
|
|
|
"metadata": metadata,
|
|
|
|
}
|
|
|
|
for input_file_name in input_file_names:
|
|
|
|
with open(input_file_name) as f:
|
|
|
|
input_config = yaml.safe_load(f.read())
|
|
|
|
|
|
|
|
if input_config.get("apiVersion") != template["apiVersion"]:
|
|
|
|
raise ValueError(
|
|
|
|
"Unexpected api version in file {}: {}".format(
|
|
|
|
input_file_name, input_config.get("apiVersion")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if input_config.get("kind") != template["kind"]:
|
|
|
|
raise ValueError(
|
|
|
|
"Unexpected kind in file {}: {}".format(
|
|
|
|
input_file_name, input_config.get("kind")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
for client in input_config["spec"]["clients"]:
|
|
|
|
del client["name"]
|
|
|
|
if inject_client_pool:
|
|
|
|
client["pool"] = "${client_pool}"
|
|
|
|
if client["language"] not in clientmap:
|
|
|
|
clientmap[client["language"]] = []
|
|
|
|
insert_worker(client, clientmap[client["language"]])
|
|
|
|
|
|
|
|
for server in input_config["spec"]["servers"]:
|
|
|
|
del server["name"]
|
|
|
|
if inject_server_pool:
|
|
|
|
server["pool"] = "${server_pool}"
|
|
|
|
if server["language"] not in servermap:
|
|
|
|
servermap[server["language"]] = []
|
|
|
|
insert_worker(server, servermap[server["language"]])
|
|
|
|
|
|
|
|
input_spec = input_config["spec"]
|
|
|
|
del input_spec["clients"]
|
|
|
|
del input_spec["servers"]
|
|
|
|
del input_spec["scenariosJSON"]
|
|
|
|
spec.update(input_config["spec"])
|
|
|
|
|
|
|
|
uniquify_workers(clientmap)
|
|
|
|
uniquify_workers(servermap)
|
|
|
|
|
|
|
|
spec.update(
|
|
|
|
{
|
|
|
|
"clients": sum(
|
|
|
|
(clientmap[language] for language in sorted(clientmap)),
|
|
|
|
start=[],
|
|
|
|
),
|
|
|
|
"servers": sum(
|
|
|
|
(servermap[language] for language in sorted(servermap)),
|
|
|
|
start=[],
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
if "driver" not in spec:
|
|
|
|
spec["driver"] = {"language": "cxx"}
|
|
|
|
|
|
|
|
driver = spec["driver"]
|
|
|
|
if "name" in driver:
|
|
|
|
del driver["name"]
|
|
|
|
if inject_driver_image:
|
|
|
|
if "run" not in driver:
|
|
|
|
driver["run"] = [{"name": "main"}]
|
|
|
|
driver["run"][0]["image"] = "${driver_image}"
|
|
|
|
if inject_driver_pool:
|
|
|
|
driver["pool"] = "${driver_pool}"
|
|
|
|
|
|
|
|
if "run" not in driver:
|
|
|
|
if inject_driver_pool:
|
|
|
|
raise ValueError("Cannot inject driver.pool: missing driver.run.")
|
|
|
|
del spec["driver"]
|
|
|
|
|
|
|
|
if inject_big_query_table:
|
|
|
|
if "results" not in spec:
|
|
|
|
spec["results"] = dict()
|
|
|
|
spec["results"]["bigQueryTable"] = "${big_query_table}"
|
|
|
|
if inject_timeout_seconds:
|
|
|
|
spec["timeoutSeconds"] = "${timeout_seconds}"
|
|
|
|
if inject_ttl_seconds:
|
|
|
|
spec["ttlSeconds"] = "${ttl_seconds}"
|
|
|
|
|
|
|
|
template["spec"] = spec
|
|
|
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
def template_dumper(header_comment: str) -> Type[yaml.SafeDumper]:
|
|
|
|
"""Returns a custom dumper to dump templates in the expected format."""
|
|
|
|
|
|
|
|
class TemplateDumper(yaml.SafeDumper):
|
|
|
|
def expect_stream_start(self):
|
|
|
|
super().expect_stream_start()
|
|
|
|
if isinstance(self.event, yaml.StreamStartEvent):
|
|
|
|
self.write_indent()
|
|
|
|
self.write_indicator(header_comment, need_whitespace=False)
|
|
|
|
|
|
|
|
def str_presenter(dumper, data):
|
|
|
|
if "\n" in data:
|
|
|
|
return dumper.represent_scalar(
|
|
|
|
"tag:yaml.org,2002:str", data, style="|"
|
|
|
|
)
|
|
|
|
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
|
|
|
|
|
|
TemplateDumper.add_representer(str, str_presenter)
|
|
|
|
|
|
|
|
return TemplateDumper
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
argp = argparse.ArgumentParser(
|
|
|
|
description="Creates a load test config generator template.",
|
|
|
|
fromfile_prefix_chars="@",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-i",
|
|
|
|
"--inputs",
|
|
|
|
action="extend",
|
|
|
|
nargs="+",
|
|
|
|
type=str,
|
|
|
|
help="Input files.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-o",
|
|
|
|
"--output",
|
|
|
|
type=str,
|
|
|
|
help="Output file. Outputs to stdout if not set.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_client_pool",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.client(s).pool values to '${client_pool}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_driver_image",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.driver(s).image values to '${driver_image}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_driver_pool",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.driver(s).pool values to '${driver_pool}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_server_pool",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.server(s).pool values to '${server_pool}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_big_query_table",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.results.bigQueryTable to '${big_query_table}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_timeout_seconds",
|
|
|
|
action="store_true",
|
|
|
|
help="Set spec.timeoutSeconds to '${timeout_seconds}'.",
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"--inject_ttl_seconds", action="store_true", help="Set timeout "
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-n", "--name", default="", type=str, help="metadata.name."
|
|
|
|
)
|
|
|
|
argp.add_argument(
|
|
|
|
"-a",
|
|
|
|
"--annotation",
|
|
|
|
action="append",
|
|
|
|
type=str,
|
|
|
|
help="metadata.annotation(s), in the form key=value.",
|
|
|
|
dest="annotations",
|
|
|
|
)
|
|
|
|
args = argp.parse_args()
|
|
|
|
|
|
|
|
annotations = loadtest_config.parse_key_value_args(args.annotations)
|
|
|
|
|
|
|
|
metadata = {"name": args.name}
|
|
|
|
if annotations:
|
|
|
|
metadata["annotations"] = annotations
|
|
|
|
|
|
|
|
template = loadtest_template(
|
|
|
|
input_file_names=args.inputs,
|
|
|
|
metadata=metadata,
|
|
|
|
inject_client_pool=args.inject_client_pool,
|
|
|
|
inject_driver_image=args.inject_driver_image,
|
|
|
|
inject_driver_pool=args.inject_driver_pool,
|
|
|
|
inject_server_pool=args.inject_server_pool,
|
|
|
|
inject_big_query_table=args.inject_big_query_table,
|
|
|
|
inject_timeout_seconds=args.inject_timeout_seconds,
|
|
|
|
inject_ttl_seconds=args.inject_ttl_seconds,
|
|
|
|
)
|
|
|
|
|
|
|
|
with open(args.output, "w") if args.output else sys.stdout as f:
|
|
|
|
yaml.dump(
|
|
|
|
template,
|
|
|
|
stream=f,
|
|
|
|
Dumper=template_dumper(TEMPLATE_FILE_HEADER_COMMENT.strip()),
|
|
|
|
default_flow_style=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|