koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 1 | import abc |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 2 | import time |
| 3 | import logging |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 4 | import os.path |
koder aka kdanilov | 88407ff | 2015-05-26 15:35:57 +0300 | [diff] [blame] | 5 | import functools |
koder aka kdanilov | 652cd80 | 2015-04-13 12:21:07 +0300 | [diff] [blame] | 6 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 7 | from concurrent.futures import ThreadPoolExecutor |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 8 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 9 | from wally.utils import Barrier, StopTestError |
| 10 | from wally.statistic import data_property |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 11 | from wally.ssh_utils import run_over_ssh, copy_paths |
| 12 | |
| 13 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 14 | logger = logging.getLogger("wally") |
koder aka kdanilov | 88407ff | 2015-05-26 15:35:57 +0300 | [diff] [blame] | 15 | |
| 16 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 17 | class TestConfig(object): |
| 18 | """ |
| 19 | this class describe test input configuration |
koder aka kdanilov | 88407ff | 2015-05-26 15:35:57 +0300 | [diff] [blame] | 20 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 21 | test_type:str - test type name |
| 22 | params:{str:Any} - parameters from yaml file for this test |
| 23 | test_uuid:str - UUID to be used to create filenames and Co |
| 24 | log_directory:str - local directory to store results |
| 25 | nodes:[Node] - node to run tests on |
| 26 | remote_dir:str - directory on nodes to be used for local files |
| 27 | """ |
| 28 | def __init__(self, test_type, params, test_uuid, nodes, |
| 29 | log_directory, remote_dir): |
| 30 | self.test_type = test_type |
| 31 | self.params = params |
| 32 | self.test_uuid = test_uuid |
| 33 | self.log_directory = log_directory |
| 34 | self.nodes = nodes |
| 35 | self.remote_dir = remote_dir |
koder aka kdanilov | 88407ff | 2015-05-26 15:35:57 +0300 | [diff] [blame] | 36 | |
| 37 | |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 38 | class TestResults(object): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 39 | """ |
| 40 | this class describe test results |
| 41 | |
| 42 | config:TestConfig - test config object |
| 43 | params:dict - parameters from yaml file for this test |
| 44 | results:{str:MeasurementMesh} - test results object |
| 45 | raw_result:Any - opaque object to store raw results |
| 46 | run_interval:(float, float) - test tun time, used for sensors |
| 47 | """ |
| 48 | def __init__(self, config, results, raw_result, run_interval): |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 49 | self.config = config |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 50 | self.params = config.params |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 51 | self.results = results |
| 52 | self.raw_result = raw_result |
| 53 | self.run_interval = run_interval |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 54 | |
| 55 | def __str__(self): |
| 56 | res = "{0}({1}):\n results:\n".format( |
| 57 | self.__class__.__name__, |
| 58 | self.summary()) |
| 59 | |
| 60 | for name, val in self.results.items(): |
| 61 | res += " {0}={1}\n".format(name, val) |
| 62 | |
| 63 | res += " params:\n" |
| 64 | |
| 65 | for name, val in self.params.items(): |
| 66 | res += " {0}={1}\n".format(name, val) |
| 67 | |
| 68 | return res |
| 69 | |
| 70 | @abc.abstractmethod |
| 71 | def summary(self): |
| 72 | pass |
| 73 | |
| 74 | @abc.abstractmethod |
| 75 | def get_yamable(self): |
| 76 | pass |
koder aka kdanilov | e21d747 | 2015-02-14 19:02:04 -0800 | [diff] [blame] | 77 | |
| 78 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 79 | class MeasurementMatrix(object): |
| 80 | """ |
| 81 | data:[[MeasurementResult]] - VM_COUNT x TH_COUNT matrix of MeasurementResult |
| 82 | """ |
| 83 | def __init__(self, data): |
| 84 | self.data = data |
| 85 | |
| 86 | def per_vm(self): |
| 87 | return self.data |
| 88 | |
| 89 | def per_th(self): |
| 90 | return sum(self.data, []) |
| 91 | |
| 92 | |
| 93 | class MeasurementResults(object): |
| 94 | def stat(self): |
| 95 | return data_property(self.data) |
| 96 | |
| 97 | def __str__(self): |
| 98 | return 'TS([' + ", ".join(map(str, self.data)) + '])' |
| 99 | |
| 100 | |
| 101 | class SimpleVals(MeasurementResults): |
| 102 | """ |
| 103 | data:[float] - list of values |
| 104 | """ |
| 105 | def __init__(self, data): |
| 106 | self.data = data |
| 107 | |
| 108 | |
| 109 | class TimeSeriesValue(MeasurementResults): |
| 110 | """ |
| 111 | values:[(float, float, float)] - list of (start_time, lenght, average_value_for_interval) |
| 112 | """ |
| 113 | def __init__(self, data): |
| 114 | assert len(data) > 0 |
| 115 | data = [(0, 0)] + data |
| 116 | |
| 117 | self.values = [] |
| 118 | for (cstart, cval), (nstart, nval) in zip(data[:-1], data[1:]): |
| 119 | self.values.append((cstart, nstart - cstart, nval)) |
| 120 | |
| 121 | @property |
| 122 | def values(self): |
| 123 | return [val[2] for val in self.data] |
| 124 | |
| 125 | def skip(self, seconds): |
| 126 | nres = [] |
| 127 | for start, ln, val in enumerate(self.data): |
| 128 | if start + ln < seconds: |
| 129 | continue |
| 130 | elif start > seconds: |
| 131 | nres.append([start + ln - seconds, val]) |
| 132 | else: |
| 133 | nres.append([0, val]) |
| 134 | return self.__class__(nres) |
| 135 | |
| 136 | def derived(self, tdelta): |
| 137 | end = tdelta |
| 138 | res = [[end, 0.0]] |
| 139 | tdelta = float(tdelta) |
| 140 | |
| 141 | for start, lenght, val in self.data: |
| 142 | if start < end: |
| 143 | ln = min(end, start + lenght) - start |
| 144 | res[-1][1] += val * ln / tdelta |
| 145 | |
| 146 | if end <= start + lenght: |
| 147 | end += tdelta |
| 148 | res.append([end, 0.0]) |
| 149 | while end < start + lenght: |
| 150 | res[-1][1] = val |
| 151 | res.append([end, 0.0]) |
| 152 | end += tdelta |
| 153 | |
| 154 | if res[-1][1] < 1: |
| 155 | res = res[:-1] |
| 156 | |
| 157 | return self.__class__(res) |
| 158 | |
| 159 | |
| 160 | class PerfTest(object): |
| 161 | """ |
| 162 | Very base class for tests |
| 163 | config:TestConfig - test configuration |
| 164 | stop_requested:bool - stop for test requested |
| 165 | """ |
| 166 | def __init__(self, config): |
| 167 | self.config = config |
koder aka kdanilov | e2de58c | 2015-04-24 22:59:36 +0300 | [diff] [blame] | 168 | self.stop_requested = False |
| 169 | |
| 170 | def request_stop(self): |
| 171 | self.stop_requested = True |
koder aka kdanilov | 2066daf | 2015-04-23 21:05:41 +0300 | [diff] [blame] | 172 | |
| 173 | def join_remote(self, path): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 174 | return os.path.join(self.config.remote_dir, path) |
koder aka kdanilov | 4500a5f | 2015-04-17 16:55:17 +0300 | [diff] [blame] | 175 | |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 176 | @classmethod |
| 177 | @abc.abstractmethod |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 178 | def load(cls, path): |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 179 | pass |
| 180 | |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 181 | @abc.abstractmethod |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 182 | def run(self): |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 183 | pass |
| 184 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 185 | @abc.abstractmethod |
koder aka kdanilov | cff7b2e | 2015-04-18 20:48:15 +0300 | [diff] [blame] | 186 | def format_for_console(cls, data): |
koder aka kdanilov | ec1b973 | 2015-04-23 20:43:29 +0300 | [diff] [blame] | 187 | pass |
| 188 | |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 189 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 190 | def run_on_node(node): |
| 191 | def closure(*args, **kwargs): |
| 192 | return run_over_ssh(node.connection, |
| 193 | *args, |
| 194 | node=node.get_conn_id(), |
| 195 | **kwargs) |
| 196 | return closure |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 197 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 198 | |
| 199 | class ThreadedTest(PerfTest): |
| 200 | """ |
| 201 | Base class for tests, which spawn separated thread for each node |
| 202 | """ |
| 203 | |
| 204 | def run(self): |
| 205 | barrier = Barrier(len(self.nodes)) |
| 206 | th_test_func = functools.partial(self.th_test_func, barrier) |
| 207 | |
| 208 | with ThreadPoolExecutor(len(self.nodes)) as pool: |
| 209 | return list(pool.map(th_test_func, self.config.nodes)) |
| 210 | |
| 211 | @abc.abstractmethod |
| 212 | def do_test(self, node): |
| 213 | pass |
| 214 | |
| 215 | def th_test_func(self, barrier, node): |
| 216 | logger.debug("Starting {0} test on {1} node".format(self.__class__.__name__, |
| 217 | node.conn_url)) |
| 218 | |
| 219 | logger.debug("Run preparation for {0}".format(node.get_conn_id())) |
| 220 | self.pre_run(node) |
| 221 | barrier.wait() |
| 222 | try: |
| 223 | logger.debug("Run test for {0}".format(node.get_conn_id())) |
| 224 | return self.do_test(node) |
| 225 | except StopTestError as exc: |
| 226 | pass |
| 227 | except Exception as exc: |
| 228 | msg = "In test {0} for node {1}".format(self, node.get_conn_id()) |
| 229 | logger.exception(msg) |
| 230 | exc = StopTestError(msg, exc) |
| 231 | |
| 232 | try: |
| 233 | self.cleanup() |
| 234 | except StopTestError as exc1: |
| 235 | if exc is None: |
| 236 | exc = exc1 |
| 237 | except Exception as exc1: |
| 238 | if exc is None: |
| 239 | msg = "Duringf cleanup - in test {0} for node {1}".format(self, node) |
| 240 | logger.exception(msg) |
| 241 | exc = StopTestError(msg, exc) |
| 242 | |
| 243 | if exc is not None: |
| 244 | raise exc |
| 245 | |
| 246 | def pre_run(self, node): |
| 247 | pass |
| 248 | |
| 249 | def cleanup(self, node): |
| 250 | pass |
| 251 | |
| 252 | |
| 253 | class TwoScriptTest(ThreadedTest): |
| 254 | def __init__(self, *dt, **mp): |
| 255 | ThreadedTest.__init__(self, *dt, **mp) |
| 256 | |
| 257 | self.prerun_script = self.config.params['prerun_script'] |
| 258 | self.run_script = self.config.params['run_script'] |
| 259 | |
| 260 | self.prerun_tout = self.config.params.get('prerun_tout', 3600) |
| 261 | self.run_tout = self.config.params.get('run_tout', 3600) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 262 | |
| 263 | def get_remote_for_script(self, script): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 264 | return os.path.join(self.options.remote_dir, |
| 265 | os.path.basename(script)) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 266 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 267 | def pre_run(self, node): |
| 268 | copy_paths(node.connection, |
| 269 | { |
| 270 | self.run_script: self.get_remote_for_script(self.run_script), |
| 271 | self.prerun_script: self.get_remote_for_script(self.prerun_script), |
| 272 | }) |
| 273 | |
Yulia Portnova | b1a1507 | 2015-05-06 14:59:25 +0300 | [diff] [blame] | 274 | cmd = self.get_remote_for_script(self.pre_run_script) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 275 | cmd += ' ' + self.config.params.get('prerun_opts', '') |
| 276 | run_on_node(node)(cmd, timeout=self.prerun_tout) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 277 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame^] | 278 | def run(self, node): |
| 279 | cmd = self.get_remote_for_script(self.run_script) |
| 280 | cmd += ' ' + self.config.params.get('run_opts', '') |
| 281 | t1 = time.time() |
| 282 | res = run_on_node(node)(cmd, timeout=self.run_tout) |
| 283 | t2 = time.time() |
| 284 | return TestResults(self.config, None, res, (t1, t2)) |