blob: 16d884f53c4eab43f4371080706f493dd8c586ab [file] [log] [blame]
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03001import os
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03002import time
3import signal
koder aka kdanilov7f59d562016-12-26 01:34:23 +02004import pprint
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03005import logging
6import argparse
7import functools
koder aka kdanilov39e449e2016-12-17 15:15:26 +02008import contextlib
9from typing import List, Tuple, Any, Callable, IO, cast, Optional, Iterator
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030010from yaml import load as _yaml_load
11
koder aka kdanilov22d134e2016-11-08 11:33:19 +020012
13YLoader = Callable[[IO], Any]
14yaml_load = None # type: YLoader
15
16
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030017try:
18 from yaml import CLoader
koder aka kdanilov22d134e2016-11-08 11:33:19 +020019 yaml_load = cast(YLoader, functools.partial(_yaml_load, Loader=CLoader))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030020except ImportError:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020021 yaml_load = cast(YLoader, _yaml_load)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030022
23
24import texttable
25
26try:
27 import faulthandler
28except ImportError:
29 faulthandler = None
30
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +020031from . import utils, node
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020032from .storage import make_storage, Storage
koder aka kdanilov22d134e2016-11-08 11:33:19 +020033from .config import Config
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030034from .logger import setup_loggers
koder aka kdanilov39e449e2016-12-17 15:15:26 +020035from .stage import Stage
koder aka kdanilov22d134e2016-11-08 11:33:19 +020036from .test_run_class import TestRun
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020037from .ssh import set_ssh_key_passwd
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030038
39
koder aka kdanilov39e449e2016-12-17 15:15:26 +020040# stages
41from .ceph import DiscoverCephStage
42from .openstack import DiscoverOSStage
43from .fuel import DiscoverFuelStage
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020044from .run_test import (CollectInfoStage, ExplicitNodesStage, SaveNodesStage,
koder aka kdanilov7f59d562016-12-26 01:34:23 +020045 RunTestsStage, ConnectStage, SleepStage, PrepareNodes,
46 LoadStoredNodesStage)
koder aka kdanilov39e449e2016-12-17 15:15:26 +020047from .report import ConsoleReportStage, HtmlReportStage
48from .sensors import StartSensorsStage, CollectSensorsStage
49
50
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030051logger = logging.getLogger("wally")
52
53
koder aka kdanilov39e449e2016-12-17 15:15:26 +020054@contextlib.contextmanager
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020055def log_stage(stage: Stage, cleanup: bool = False) -> Iterator[None]:
56 logger.info("Start " + stage.name() + ("::cleanup" if cleanup else ""))
koder aka kdanilov39e449e2016-12-17 15:15:26 +020057 try:
58 yield
59 except utils.StopTestError as exc:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020060 raise
koder aka kdanilov39e449e2016-12-17 15:15:26 +020061 except Exception:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020062 logger.exception("During %s", stage.name() + ("::cleanup" if cleanup else ""))
63 raise
koder aka kdanilov39e449e2016-12-17 15:15:26 +020064
65
koder aka kdanilov22d134e2016-11-08 11:33:19 +020066def list_results(path: str) -> List[Tuple[str, str, str, str]]:
koder aka kdanilov73084622016-11-16 21:51:08 +020067 results = [] # type: List[Tuple[float, str, str, str, str]]
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030068
koder aka kdanilov22d134e2016-11-08 11:33:19 +020069 for dir_name in os.listdir(path):
70 full_path = os.path.join(path, dir_name)
71
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030072 try:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020073 stor = make_storage(full_path, existing=True)
74 except Exception as exc:
75 logger.warning("Can't load folder {}. Error {}".format(full_path, exc))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030076
koder aka kdanilov7f59d562016-12-26 01:34:23 +020077 comment = cast(str, stor.get('info/comment'))
78 run_uuid = cast(str, stor.get('info/run_uuid'))
79 run_time = cast(float, stor.get('info/run_time'))
koder aka kdanilov22d134e2016-11-08 11:33:19 +020080 test_types = ""
koder aka kdanilov73084622016-11-16 21:51:08 +020081 results.append((run_time,
koder aka kdanilov22d134e2016-11-08 11:33:19 +020082 run_uuid,
83 test_types,
koder aka kdanilov73084622016-11-16 21:51:08 +020084 time.ctime(run_time),
koder aka kdanilov22d134e2016-11-08 11:33:19 +020085 '-' if comment is None else comment))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030086
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030087 results.sort()
koder aka kdanilov22d134e2016-11-08 11:33:19 +020088 return [i[1:] for i in results]
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030089
90
koder aka kdanilov22d134e2016-11-08 11:33:19 +020091def log_nodes_statistic_stage(ctx: TestRun) -> None:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030092 utils.log_nodes_statistic(ctx.nodes)
93
94
95def parse_args(argv):
96 descr = "Disk io performance test suite"
97 parser = argparse.ArgumentParser(prog='wally', description=descr)
koder aka kdanilov22d134e2016-11-08 11:33:19 +020098 parser.add_argument("-l", '--log-level', help="print some extra log info")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020099 parser.add_argument("--ssh-key-passwd", default=None, help="Pass ssh key password")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200100 parser.add_argument("-s", '--settings-dir', default=None,
101 help="Folder to store key/settings/history files")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300102
103 subparsers = parser.add_subparsers(dest='subparser_name')
104
105 # ---------------------------------------------------------------------
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200106 report_parser = subparsers.add_parser('ls', help='list all results')
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300107 report_parser.add_argument("result_storage", help="Folder with test results")
108
109 # ---------------------------------------------------------------------
110 compare_help = 'compare two results'
111 report_parser = subparsers.add_parser('compare', help=compare_help)
112 report_parser.add_argument("data_path1", help="First folder with test results")
113 report_parser.add_argument("data_path2", help="Second folder with test results")
114
115 # ---------------------------------------------------------------------
116 report_help = 'run report on previously obtained results'
117 report_parser = subparsers.add_parser('report', help=report_help)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300118 report_parser.add_argument("data_dir", help="folder with rest results")
119
120 # ---------------------------------------------------------------------
121 test_parser = subparsers.add_parser('test', help='run tests')
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200122 test_parser.add_argument('--build-description', type=str, default="Build info")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300123 test_parser.add_argument('--build-id', type=str, default="id")
124 test_parser.add_argument('--build-type', type=str, default="GA")
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200125 test_parser.add_argument('--dont-collect', action='store_true', help="Don't collect cluster info")
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200126 test_parser.add_argument('-n', '--no-tests', action='store_true', help="Don't run tests")
127 test_parser.add_argument('--load-report', action='store_true')
128 test_parser.add_argument("-k", '--keep-vm', action='store_true', help="Don't remove test vm's")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300129 test_parser.add_argument("-d", '--dont-discover-nodes', action='store_true',
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200130 help="Don't connect/discover fuel nodes")
131 test_parser.add_argument('--no-report', action='store_true', help="Skip report stages")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200132 test_parser.add_argument('--result-dir', default=None, help="Save results to DIR", metavar="DIR")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300133 test_parser.add_argument("comment", help="Test information")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200134 test_parser.add_argument("config_file", help="Yaml config file")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300135
136 # ---------------------------------------------------------------------
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200137 test_parser = subparsers.add_parser('resume', help='resume tests')
138 test_parser.add_argument("storage_dir", help="Path to test directory")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300139
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200140 # ---------------------------------------------------------------------
141 test_parser = subparsers.add_parser('db', help='resume tests')
142 test_parser.add_argument("cmd", choices=("show",), help="Command to execute")
143 test_parser.add_argument("params", nargs='*', help="Command params")
144 test_parser.add_argument("storage_dir", help="Storage path")
145
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300146 return parser.parse_args(argv[1:])
147
148
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200149def get_config_path(config: Config, opts_value: Optional[str]) -> str:
150 if opts_value is None and 'settings_dir' not in config:
151 val = "~/.wally"
152 elif opts_value is not None:
153 val = opts_value
154 else:
155 val = config.settings_dir
156
157 return os.path.abspath(os.path.expanduser(val))
158
159
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200160def find_cfg_file(name: str, included_from: str = None) -> str:
161 paths = [".", os.path.expanduser('~/.wally')]
162 if included_from is not None:
163 paths.append(os.path.dirname(included_from))
164
165 search_paths = set(os.path.abspath(path) for path in paths if os.path.isdir(path))
166
167 for folder in search_paths:
168 path = os.path.join(folder, name)
169 if os.path.exists(path):
170 return path
171
172 raise FileNotFoundError(name)
173
174
175def load_config(path: str) -> Config:
176 path = os.path.abspath(path)
177 cfg_dict = yaml_load(open(path).read())
178
179 while 'include' in cfg_dict:
180 inc = cfg_dict.pop('include')
181 if isinstance(inc, str):
182 inc = [inc]
183
184 for fname in inc:
185 inc_path = find_cfg_file(fname, path)
186 inc_dict = yaml_load(open(inc_path).read())
187 inc_dict.update(cfg_dict)
188 cfg_dict = inc_dict
189
190 return Config(cfg_dict)
191
192
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200193def get_run_stages() -> List[Stage]:
194 return [DiscoverCephStage(),
195 DiscoverOSStage(),
196 DiscoverFuelStage(),
197 ExplicitNodesStage(),
198 StartSensorsStage(),
199 RunTestsStage(),
200 CollectSensorsStage(),
201 ConnectStage(),
202 SleepStage(),
203 PrepareNodes()]
204
205
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200206def main(argv: List[str]) -> int:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300207 if faulthandler is not None:
208 faulthandler.register(signal.SIGUSR1, all_threads=True)
209
210 opts = parse_args(argv)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200211 stages = [] # type: List[Stage]
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200212
213 # stop mypy from telling that config & storage might be undeclared
214 config = None # type: Config
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200215 storage = None # type: Storage
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300216
217 if opts.subparser_name == 'test':
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200218 config = load_config(opts.config_file)
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200219 config.storage_url, config.run_uuid = utils.get_uniq_path_uuid(config.results_dir)
220 config.comment = opts.comment
221 config.keep_vm = opts.keep_vm
222 config.no_tests = opts.no_tests
223 config.dont_discover_nodes = opts.dont_discover_nodes
224 config.build_id = opts.build_id
225 config.build_description = opts.build_description
226 config.build_type = opts.build_type
227 config.settings_dir = get_config_path(config, opts.settings_dir)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300228
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200229 storage = make_storage(config.storage_url)
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200230 storage.put(config, 'config')
231 stages.extend(get_run_stages())
232 stages.extend(SaveNodesStage())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300233
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200234 if not opts.dont_collect:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200235 stages.append(CollectInfoStage())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300236
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200237 argv2 = argv[:]
238 if '--ssh-key-passwd' in argv2:
239 # don't save ssh key password to storage
240 argv2[argv2.index("--ssh-key-passwd") + 1] = "<removed from output>"
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200241 storage.put(argv2, 'cli')
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200242
243 elif opts.subparser_name == 'resume':
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200244 opts.resumed = True
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200245 storage = make_storage(opts.storage_dir, existing=True)
246 config = storage.load(Config, 'config')
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200247 stages.extend(get_run_stages())
248 stages.append(LoadStoredNodesStage())
249 prev_opts = storage.get('cli')
250 if '--ssh-key-passwd' in prev_opts and opts.ssh_key_passwd:
251 prev_opts[prev_opts.index("--ssh-key-passwd") + 1] = opts.ssh_key_passwd
252
253 restored_opts = parse_args(prev_opts)
254 opts.__dict__.update(restored_opts.__dict__)
255 opts.subparser_name = 'resume'
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200256
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300257 elif opts.subparser_name == 'ls':
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200258 tab = texttable.Texttable(max_width=200)
259 tab.set_deco(tab.HEADER | tab.VLINES | tab.BORDER)
260 tab.set_cols_align(["l", "l", "l", "l"])
261 tab.header(["Name", "Tests", "Run at", "Comment"])
262 tab.add_rows(list_results(opts.result_storage))
263 print(tab.draw())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300264 return 0
265
266 elif opts.subparser_name == 'report':
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200267 storage = make_storage(opts.data_dir, existing=True)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200268 config.settings_dir = get_config_path(config, opts.settings_dir)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300269
270 elif opts.subparser_name == 'compare':
koder aka kdanilov73084622016-11-16 21:51:08 +0200271 # x = run_test.load_data_from_path(opts.data_path1)
272 # y = run_test.load_data_from_path(opts.data_path2)
273 # print(run_test.IOPerfTest.format_diff_for_console(
274 # [x['io'][0], y['io'][0]]))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300275 return 0
276
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200277 elif opts.subparser_name == 'db':
278 storage = make_storage(opts.storage_dir, existing=True)
279 if opts.cmd == 'show':
280 if len(opts.params) != 1:
281 print("'show' command requires parameter - key to show")
282 return 1
283 pprint.pprint(storage.get(opts.params[0]))
284 else:
285 print("Unknown/not_implemented command {!r}".format(opts.cmd))
286 return 1
287 return 0
288
289
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200290 report_stages = [] # type: List[Stage]
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200291 if not getattr(opts, "no_report", False):
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200292 report_stages.append(ConsoleReportStage())
293 report_stages.append(HtmlReportStage())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300294
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200295 # log level is not a part of config
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300296 if opts.log_level is not None:
297 str_level = opts.log_level
298 else:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200299 str_level = config.get('logging/log_level', 'INFO')
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300300
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200301 log_config_file = config.get('logging/config', None)
302
303 if log_config_file is not None:
304 log_config_file = find_cfg_file(log_config_file, opts.config_file)
305
306 setup_loggers(getattr(logging, str_level),
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200307 log_fd=storage.get_fd('log', "w"),
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200308 config_file=log_config_file)
309
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200310 logger.info("All info would be stored into %r", config.storage_url)
311
312 ctx = TestRun(config, storage)
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200313 ctx.rpc_code, ctx.default_rpc_plugins = node.get_rpc_server_code()
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300314
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200315 if opts.ssh_key_passwd is not None:
316 set_ssh_key_passwd(opts.ssh_key_passwd)
317
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200318 stages.sort(key=lambda x: x.priority)
319
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200320 # TODO: run only stages, which have config
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200321 failed = False
322 cleanup_stages = []
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200323
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300324 for stage in stages:
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200325 if stage.config_block is not None:
326 if stage.config_block not in ctx.config:
327 continue
328
329 cleanup_stages.append(stage)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200330 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200331 with log_stage(stage):
332 stage.run(ctx)
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200333 except (Exception, KeyboardInterrupt):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200334 failed = True
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300335 break
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200336 ctx.storage.sync()
337 ctx.storage.sync()
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300338
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200339 logger.debug("Start cleanup")
340 cleanup_failed = False
341 for stage in cleanup_stages[::-1]:
342 try:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200343 with log_stage(stage, cleanup=True):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200344 stage.cleanup(ctx)
345 except:
346 cleanup_failed = True
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200347 ctx.storage.sync()
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300348
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200349 if not failed:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300350 for report_stage in report_stages:
351 with log_stage(report_stage):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200352 report_stage.run(ctx)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300353
koder aka kdanilov7f59d562016-12-26 01:34:23 +0200354 ctx.storage.sync()
355
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200356 logger.info("All info is stored into %r", config.storage_url)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300357
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200358 if failed or cleanup_failed:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200359 logger.error("Tests are failed. See error details in log above")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300360 return 1
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200361 else:
362 logger.info("Tests finished successfully")
363 return 0