blob: 6396b6bcda18f9bf200f894e6d8726941c8099a9 [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
4import logging
5import argparse
6import functools
koder aka kdanilov39e449e2016-12-17 15:15:26 +02007import contextlib
8from typing import List, Tuple, Any, Callable, IO, cast, Optional, Iterator
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03009from yaml import load as _yaml_load
10
koder aka kdanilov22d134e2016-11-08 11:33:19 +020011
12YLoader = Callable[[IO], Any]
13yaml_load = None # type: YLoader
14
15
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030016try:
17 from yaml import CLoader
koder aka kdanilov22d134e2016-11-08 11:33:19 +020018 yaml_load = cast(YLoader, functools.partial(_yaml_load, Loader=CLoader))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030019except ImportError:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020020 yaml_load = cast(YLoader, _yaml_load)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030021
22
23import texttable
24
25try:
26 import faulthandler
27except ImportError:
28 faulthandler = None
29
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +020030from . import utils, node
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020031from .storage import make_storage, Storage
koder aka kdanilov22d134e2016-11-08 11:33:19 +020032from .config import Config
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030033from .logger import setup_loggers
koder aka kdanilov39e449e2016-12-17 15:15:26 +020034from .stage import Stage
koder aka kdanilov22d134e2016-11-08 11:33:19 +020035from .test_run_class import TestRun
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020036from .ssh import set_ssh_key_passwd
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030037
38
koder aka kdanilov39e449e2016-12-17 15:15:26 +020039# stages
40from .ceph import DiscoverCephStage
41from .openstack import DiscoverOSStage
42from .fuel import DiscoverFuelStage
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020043from .run_test import (CollectInfoStage, ExplicitNodesStage, SaveNodesStage,
44 RunTestsStage, ConnectStage, SleepStage, PrepareNodes)
koder aka kdanilov39e449e2016-12-17 15:15:26 +020045from .report import ConsoleReportStage, HtmlReportStage
46from .sensors import StartSensorsStage, CollectSensorsStage
47
48
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030049logger = logging.getLogger("wally")
50
51
koder aka kdanilov39e449e2016-12-17 15:15:26 +020052@contextlib.contextmanager
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020053def log_stage(stage: Stage, cleanup: bool = False) -> Iterator[None]:
54 logger.info("Start " + stage.name() + ("::cleanup" if cleanup else ""))
koder aka kdanilov39e449e2016-12-17 15:15:26 +020055 try:
56 yield
57 except utils.StopTestError as exc:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020058 raise
koder aka kdanilov39e449e2016-12-17 15:15:26 +020059 except Exception:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020060 logger.exception("During %s", stage.name() + ("::cleanup" if cleanup else ""))
61 raise
koder aka kdanilov39e449e2016-12-17 15:15:26 +020062
63
koder aka kdanilov22d134e2016-11-08 11:33:19 +020064def list_results(path: str) -> List[Tuple[str, str, str, str]]:
koder aka kdanilov73084622016-11-16 21:51:08 +020065 results = [] # type: List[Tuple[float, str, str, str, str]]
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030066
koder aka kdanilov22d134e2016-11-08 11:33:19 +020067 for dir_name in os.listdir(path):
68 full_path = os.path.join(path, dir_name)
69
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030070 try:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020071 stor = make_storage(full_path, existing=True)
72 except Exception as exc:
73 logger.warning("Can't load folder {}. Error {}".format(full_path, exc))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030074
koder aka kdanilov73084622016-11-16 21:51:08 +020075 comment = cast(str, stor['info/comment'])
76 run_uuid = cast(str, stor['info/run_uuid'])
77 run_time = cast(float, stor['info/run_time'])
koder aka kdanilov22d134e2016-11-08 11:33:19 +020078 test_types = ""
koder aka kdanilov73084622016-11-16 21:51:08 +020079 results.append((run_time,
koder aka kdanilov22d134e2016-11-08 11:33:19 +020080 run_uuid,
81 test_types,
koder aka kdanilov73084622016-11-16 21:51:08 +020082 time.ctime(run_time),
koder aka kdanilov22d134e2016-11-08 11:33:19 +020083 '-' if comment is None else comment))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030084
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030085 results.sort()
koder aka kdanilov22d134e2016-11-08 11:33:19 +020086 return [i[1:] for i in results]
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030087
88
koder aka kdanilov22d134e2016-11-08 11:33:19 +020089def log_nodes_statistic_stage(ctx: TestRun) -> None:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +030090 utils.log_nodes_statistic(ctx.nodes)
91
92
93def parse_args(argv):
94 descr = "Disk io performance test suite"
95 parser = argparse.ArgumentParser(prog='wally', description=descr)
koder aka kdanilov22d134e2016-11-08 11:33:19 +020096 parser.add_argument("-l", '--log-level', help="print some extra log info")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020097 parser.add_argument("--ssh-key-passwd", default=None, help="Pass ssh key password")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020098 parser.add_argument("-s", '--settings-dir', default=None,
99 help="Folder to store key/settings/history files")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300100
101 subparsers = parser.add_subparsers(dest='subparser_name')
102
103 # ---------------------------------------------------------------------
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200104 report_parser = subparsers.add_parser('ls', help='list all results')
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300105 report_parser.add_argument("result_storage", help="Folder with test results")
106
107 # ---------------------------------------------------------------------
108 compare_help = 'compare two results'
109 report_parser = subparsers.add_parser('compare', help=compare_help)
110 report_parser.add_argument("data_path1", help="First folder with test results")
111 report_parser.add_argument("data_path2", help="Second folder with test results")
112
113 # ---------------------------------------------------------------------
114 report_help = 'run report on previously obtained results'
115 report_parser = subparsers.add_parser('report', help=report_help)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300116 report_parser.add_argument("data_dir", help="folder with rest results")
117
118 # ---------------------------------------------------------------------
119 test_parser = subparsers.add_parser('test', help='run tests')
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200120 test_parser.add_argument('--build-description', type=str, default="Build info")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300121 test_parser.add_argument('--build-id', type=str, default="id")
122 test_parser.add_argument('--build-type', type=str, default="GA")
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200123 test_parser.add_argument('--dont-collect', action='store_true', help="Don't collect cluster info")
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200124 test_parser.add_argument('-n', '--no-tests', action='store_true', help="Don't run tests")
125 test_parser.add_argument('--load-report', action='store_true')
126 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 +0300127 test_parser.add_argument("-d", '--dont-discover-nodes', action='store_true',
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200128 help="Don't connect/discover fuel nodes")
129 test_parser.add_argument('--no-report', action='store_true', help="Skip report stages")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200130 test_parser.add_argument('--result-dir', default=None, help="Save results to DIR", metavar="DIR")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300131 test_parser.add_argument("comment", help="Test information")
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200132 test_parser.add_argument("config_file", help="Yaml config file")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300133
134 # ---------------------------------------------------------------------
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200135 test_parser = subparsers.add_parser('resume', help='resume tests')
136 test_parser.add_argument("storage_dir", help="Path to test directory")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300137
138 return parser.parse_args(argv[1:])
139
140
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200141def get_config_path(config: Config, opts_value: Optional[str]) -> str:
142 if opts_value is None and 'settings_dir' not in config:
143 val = "~/.wally"
144 elif opts_value is not None:
145 val = opts_value
146 else:
147 val = config.settings_dir
148
149 return os.path.abspath(os.path.expanduser(val))
150
151
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200152def find_cfg_file(name: str, included_from: str = None) -> str:
153 paths = [".", os.path.expanduser('~/.wally')]
154 if included_from is not None:
155 paths.append(os.path.dirname(included_from))
156
157 search_paths = set(os.path.abspath(path) for path in paths if os.path.isdir(path))
158
159 for folder in search_paths:
160 path = os.path.join(folder, name)
161 if os.path.exists(path):
162 return path
163
164 raise FileNotFoundError(name)
165
166
167def load_config(path: str) -> Config:
168 path = os.path.abspath(path)
169 cfg_dict = yaml_load(open(path).read())
170
171 while 'include' in cfg_dict:
172 inc = cfg_dict.pop('include')
173 if isinstance(inc, str):
174 inc = [inc]
175
176 for fname in inc:
177 inc_path = find_cfg_file(fname, path)
178 inc_dict = yaml_load(open(inc_path).read())
179 inc_dict.update(cfg_dict)
180 cfg_dict = inc_dict
181
182 return Config(cfg_dict)
183
184
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200185def main(argv: List[str]) -> int:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300186 if faulthandler is not None:
187 faulthandler.register(signal.SIGUSR1, all_threads=True)
188
189 opts = parse_args(argv)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200190 stages = [] # type: List[Stage]
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200191
192 # stop mypy from telling that config & storage might be undeclared
193 config = None # type: Config
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200194 storage = None # type: Storage
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300195
196 if opts.subparser_name == 'test':
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200197 config = load_config(opts.config_file)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300198
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200199 config.storage_url, config.run_uuid = utils.get_uniq_path_uuid(config.results_dir)
200 config.comment = opts.comment
201 config.keep_vm = opts.keep_vm
202 config.no_tests = opts.no_tests
203 config.dont_discover_nodes = opts.dont_discover_nodes
204 config.build_id = opts.build_id
205 config.build_description = opts.build_description
206 config.build_type = opts.build_type
207 config.settings_dir = get_config_path(config, opts.settings_dir)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300208
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200209 storage = make_storage(config.storage_url)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200210
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200211 storage['config'] = config # type: ignore
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300212
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200213 stages.append(DiscoverCephStage())
214 stages.append(DiscoverOSStage())
215 stages.append(DiscoverFuelStage())
216 stages.append(ExplicitNodesStage())
217 stages.append(SaveNodesStage())
218 stages.append(StartSensorsStage())
219 stages.append(RunTestsStage())
220 stages.append(CollectSensorsStage())
221 stages.append(ConnectStage())
222 stages.append(SleepStage())
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200223 stages.append(PrepareNodes())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300224
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200225 if not opts.dont_collect:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200226 stages.append(CollectInfoStage())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300227
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200228 argv2 = argv[:]
229 if '--ssh-key-passwd' in argv2:
230 # don't save ssh key password to storage
231 argv2[argv2.index("--ssh-key-passwd") + 1] = "<removed from output>"
232 storage['cli'] = argv2
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200233
234 elif opts.subparser_name == 'resume':
235 storage = make_storage(opts.storage_dir, existing=True)
236 config = storage.load(Config, 'config')
237 # TODO: fix this
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200238 # TODO: add node loading from storage
239 # TODO: fill nodes conncreds with keys
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200240 raise NotImplementedError("Resume in not fully implemented")
241
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300242 elif opts.subparser_name == 'ls':
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200243 tab = texttable.Texttable(max_width=200)
244 tab.set_deco(tab.HEADER | tab.VLINES | tab.BORDER)
245 tab.set_cols_align(["l", "l", "l", "l"])
246 tab.header(["Name", "Tests", "Run at", "Comment"])
247 tab.add_rows(list_results(opts.result_storage))
248 print(tab.draw())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300249 return 0
250
251 elif opts.subparser_name == 'report':
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200252 storage = make_storage(opts.data_dir, existing=True)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200253 config.settings_dir = get_config_path(config, opts.settings_dir)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300254
255 elif opts.subparser_name == 'compare':
koder aka kdanilov73084622016-11-16 21:51:08 +0200256 # x = run_test.load_data_from_path(opts.data_path1)
257 # y = run_test.load_data_from_path(opts.data_path2)
258 # print(run_test.IOPerfTest.format_diff_for_console(
259 # [x['io'][0], y['io'][0]]))
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300260 return 0
261
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200262 report_stages = [] # type: List[Stage]
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200263 if not getattr(opts, "no_report", False):
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200264 report_stages.append(ConsoleReportStage())
265 report_stages.append(HtmlReportStage())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300266
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200267 # log level is not a part of config
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300268 if opts.log_level is not None:
269 str_level = opts.log_level
270 else:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200271 str_level = config.get('logging/log_level', 'INFO')
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300272
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200273 log_config_file = config.get('logging/config', None)
274
275 if log_config_file is not None:
276 log_config_file = find_cfg_file(log_config_file, opts.config_file)
277
278 setup_loggers(getattr(logging, str_level),
279 log_fd=storage.get_stream('log', "w"),
280 config_file=log_config_file)
281
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200282 logger.info("All info would be stored into %r", config.storage_url)
283
284 ctx = TestRun(config, storage)
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200285 ctx.rpc_code, ctx.default_rpc_plugins = node.get_rpc_server_code()
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300286
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200287 if opts.ssh_key_passwd is not None:
288 set_ssh_key_passwd(opts.ssh_key_passwd)
289
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200290 stages.sort(key=lambda x: x.priority)
291
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200292 # TODO: run only stages, which have config
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200293 failed = False
294 cleanup_stages = []
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300295 for stage in stages:
koder aka kdanilove7e1a4d2016-12-17 20:29:52 +0200296 if stage.config_block is not None:
297 if stage.config_block not in ctx.config:
298 continue
299
300 cleanup_stages.append(stage)
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200301 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200302 with log_stage(stage):
303 stage.run(ctx)
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200304 except (Exception, KeyboardInterrupt):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200305 failed = True
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300306 break
307
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200308 logger.debug("Start cleanup")
309 cleanup_failed = False
310 for stage in cleanup_stages[::-1]:
311 try:
koder aka kdanilov962ee5f2016-12-19 02:40:08 +0200312 with log_stage(stage, cleanup=True):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200313 stage.cleanup(ctx)
314 except:
315 cleanup_failed = True
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300316
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200317 if not failed:
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300318 for report_stage in report_stages:
319 with log_stage(report_stage):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200320 report_stage.run(ctx)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300321
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200322 logger.info("All info is stored into %r", config.storage_url)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300323
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200324 if failed or cleanup_failed:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200325 logger.error("Tests are failed. See error details in log above")
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300326 return 1
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200327 else:
328 logger.info("Tests finished successfully")
329 return 0