1#!/usr/bin/env python3 2# Copyright 2021 The gRPC Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16# This script generates a load test configuration template from a collection of 17# load test configurations. 18# 19# Configuration templates contain client and server configurations for multiple 20# languages, and may contain template substitution keys. These templates are 21# used to generate load test configurations by selecting clients and servers for 22# the required languages. The source files for template generation may be load 23# test configurations or load test configuration templates. Load test 24# configuration generation is performed by loadtest_config.py. See documentation 25# below: 26# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md 27 28import argparse 29import os 30import sys 31from typing import Any, Dict, Iterable, List, Mapping, Type 32 33import yaml 34 35sys.path.append(os.path.dirname(os.path.abspath(__file__))) 36import loadtest_config 37 38TEMPLATE_FILE_HEADER_COMMENT = """ 39# Template generated from load test configurations by loadtest_template.py. 40# 41# Configuration templates contain client and server configurations for multiple 42# languages, and may contain template substitution keys. These templates are 43# used to generate load test configurations by selecting clients and servers for 44# the required languages. The source files for template generation may be load 45# test configurations or load test configuration templates. Load test 46# configuration generation is performed by loadtest_config.py. See documentation 47# below: 48# https://github.com/grpc/grpc/blob/master/tools/run_tests/performance/README.md 49""" 50 51 52def insert_worker( 53 worker: Dict[str, Any], workers: List[Dict[str, Any]] 54) -> None: 55 """Inserts client or server into a list, without inserting duplicates.""" 56 57 def dump(w): 58 return yaml.dump(w, Dumper=yaml.SafeDumper, default_flow_style=False) 59 60 worker_str = dump(worker) 61 if any((worker_str == dump(w) for w in workers)): 62 return 63 workers.append(worker) 64 65 66def uniquify_workers(workermap: Dict[str, List[Dict[str, Any]]]) -> None: 67 """Name workers if there is more than one for the same map key.""" 68 for workers in list(workermap.values()): 69 if len(workers) <= 1: 70 continue 71 for i, worker in enumerate(workers): 72 worker["name"] = str(i) 73 74 75def loadtest_template( 76 input_file_names: Iterable[str], 77 metadata: Mapping[str, Any], 78 inject_client_pool: bool, 79 inject_driver_image: bool, 80 inject_driver_pool: bool, 81 inject_server_pool: bool, 82 inject_big_query_table: bool, 83 inject_timeout_seconds: bool, 84 inject_ttl_seconds: bool) -> Dict[str, Any]: # fmt: skip 85 """Generates the load test template.""" 86 spec = dict() # type: Dict[str, Any] 87 clientmap = dict() # Dict[str, List[Dict[str, Any]]] 88 servermap = dict() # Dict[Str, List[Dict[str, Any]]] 89 template = { 90 "apiVersion": "e2etest.grpc.io/v1", 91 "kind": "LoadTest", 92 "metadata": metadata, 93 } 94 for input_file_name in input_file_names: 95 with open(input_file_name) as f: 96 input_config = yaml.safe_load(f.read()) 97 98 if input_config.get("apiVersion") != template["apiVersion"]: 99 raise ValueError( 100 "Unexpected api version in file {}: {}".format( 101 input_file_name, input_config.get("apiVersion") 102 ) 103 ) 104 if input_config.get("kind") != template["kind"]: 105 raise ValueError( 106 "Unexpected kind in file {}: {}".format( 107 input_file_name, input_config.get("kind") 108 ) 109 ) 110 111 for client in input_config["spec"]["clients"]: 112 del client["name"] 113 if inject_client_pool: 114 client["pool"] = "${client_pool}" 115 if client["language"] not in clientmap: 116 clientmap[client["language"]] = [] 117 insert_worker(client, clientmap[client["language"]]) 118 119 for server in input_config["spec"]["servers"]: 120 del server["name"] 121 if inject_server_pool: 122 server["pool"] = "${server_pool}" 123 if server["language"] not in servermap: 124 servermap[server["language"]] = [] 125 insert_worker(server, servermap[server["language"]]) 126 127 input_spec = input_config["spec"] 128 del input_spec["clients"] 129 del input_spec["servers"] 130 del input_spec["scenariosJSON"] 131 spec.update(input_config["spec"]) 132 133 uniquify_workers(clientmap) 134 uniquify_workers(servermap) 135 136 spec.update( 137 { 138 "clients": sum( 139 (clientmap[language] for language in sorted(clientmap)), 140 start=[], 141 ), 142 "servers": sum( 143 (servermap[language] for language in sorted(servermap)), 144 start=[], 145 ), 146 } 147 ) 148 149 if "driver" not in spec: 150 spec["driver"] = {"language": "cxx"} 151 152 driver = spec["driver"] 153 if "name" in driver: 154 del driver["name"] 155 if inject_driver_image: 156 if "run" not in driver: 157 driver["run"] = [{"name": "main"}] 158 driver["run"][0]["image"] = "${driver_image}" 159 if inject_driver_pool: 160 driver["pool"] = "${driver_pool}" 161 162 if "run" not in driver: 163 if inject_driver_pool: 164 raise ValueError("Cannot inject driver.pool: missing driver.run.") 165 del spec["driver"] 166 167 if inject_big_query_table: 168 if "results" not in spec: 169 spec["results"] = dict() 170 spec["results"]["bigQueryTable"] = "${big_query_table}" 171 if inject_timeout_seconds: 172 spec["timeoutSeconds"] = "${timeout_seconds}" 173 if inject_ttl_seconds: 174 spec["ttlSeconds"] = "${ttl_seconds}" 175 176 template["spec"] = spec 177 178 return template 179 180 181def template_dumper(header_comment: str) -> Type[yaml.SafeDumper]: 182 """Returns a custom dumper to dump templates in the expected format.""" 183 184 class TemplateDumper(yaml.SafeDumper): 185 def expect_stream_start(self): 186 super().expect_stream_start() 187 if isinstance(self.event, yaml.StreamStartEvent): 188 self.write_indent() 189 self.write_indicator(header_comment, need_whitespace=False) 190 191 def str_presenter(dumper, data): 192 if "\n" in data: 193 return dumper.represent_scalar( 194 "tag:yaml.org,2002:str", data, style="|" 195 ) 196 return dumper.represent_scalar("tag:yaml.org,2002:str", data) 197 198 TemplateDumper.add_representer(str, str_presenter) 199 200 return TemplateDumper 201 202 203def main() -> None: 204 argp = argparse.ArgumentParser( 205 description="Creates a load test config generator template.", 206 fromfile_prefix_chars="@", 207 ) 208 argp.add_argument( 209 "-i", 210 "--inputs", 211 action="extend", 212 nargs="+", 213 type=str, 214 help="Input files.", 215 ) 216 argp.add_argument( 217 "-o", 218 "--output", 219 type=str, 220 help="Output file. Outputs to stdout if not set.", 221 ) 222 argp.add_argument( 223 "--inject_client_pool", 224 action="store_true", 225 help="Set spec.client(s).pool values to '${client_pool}'.", 226 ) 227 argp.add_argument( 228 "--inject_driver_image", 229 action="store_true", 230 help="Set spec.driver(s).image values to '${driver_image}'.", 231 ) 232 argp.add_argument( 233 "--inject_driver_pool", 234 action="store_true", 235 help="Set spec.driver(s).pool values to '${driver_pool}'.", 236 ) 237 argp.add_argument( 238 "--inject_server_pool", 239 action="store_true", 240 help="Set spec.server(s).pool values to '${server_pool}'.", 241 ) 242 argp.add_argument( 243 "--inject_big_query_table", 244 action="store_true", 245 help="Set spec.results.bigQueryTable to '${big_query_table}'.", 246 ) 247 argp.add_argument( 248 "--inject_timeout_seconds", 249 action="store_true", 250 help="Set spec.timeoutSeconds to '${timeout_seconds}'.", 251 ) 252 argp.add_argument( 253 "--inject_ttl_seconds", action="store_true", help="Set timeout " 254 ) 255 argp.add_argument( 256 "-n", "--name", default="", type=str, help="metadata.name." 257 ) 258 argp.add_argument( 259 "-a", 260 "--annotation", 261 action="append", 262 type=str, 263 help="metadata.annotation(s), in the form key=value.", 264 dest="annotations", 265 ) 266 args = argp.parse_args() 267 268 annotations = loadtest_config.parse_key_value_args(args.annotations) 269 270 metadata = {"name": args.name} 271 if annotations: 272 metadata["annotations"] = annotations 273 274 template = loadtest_template( 275 input_file_names=args.inputs, 276 metadata=metadata, 277 inject_client_pool=args.inject_client_pool, 278 inject_driver_image=args.inject_driver_image, 279 inject_driver_pool=args.inject_driver_pool, 280 inject_server_pool=args.inject_server_pool, 281 inject_big_query_table=args.inject_big_query_table, 282 inject_timeout_seconds=args.inject_timeout_seconds, 283 inject_ttl_seconds=args.inject_ttl_seconds, 284 ) 285 286 with open(args.output, "w") if args.output else sys.stdout as f: 287 yaml.dump( 288 template, 289 stream=f, 290 Dumper=template_dumper(TEMPLATE_FILE_HEADER_COMMENT.strip()), 291 default_flow_style=False, 292 ) 293 294 295if __name__ == "__main__": 296 main() 297