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 | """ |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 83 | def __init__(self, data, connections_ids): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 84 | self.data = data |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 85 | self.connections_ids = connections_ids |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 86 | |
| 87 | def per_vm(self): |
| 88 | return self.data |
| 89 | |
| 90 | def per_th(self): |
| 91 | return sum(self.data, []) |
| 92 | |
| 93 | |
| 94 | class MeasurementResults(object): |
| 95 | def stat(self): |
| 96 | return data_property(self.data) |
| 97 | |
| 98 | def __str__(self): |
| 99 | return 'TS([' + ", ".join(map(str, self.data)) + '])' |
| 100 | |
| 101 | |
| 102 | class SimpleVals(MeasurementResults): |
| 103 | """ |
| 104 | data:[float] - list of values |
| 105 | """ |
| 106 | def __init__(self, data): |
| 107 | self.data = data |
| 108 | |
| 109 | |
| 110 | class TimeSeriesValue(MeasurementResults): |
| 111 | """ |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 112 | data:[(float, float, float)] - list of (start_time, lenght, average_value_for_interval) |
| 113 | odata: original values |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 114 | """ |
| 115 | def __init__(self, data): |
| 116 | assert len(data) > 0 |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 117 | self.odata = data[:] |
| 118 | self.data = [] |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 119 | |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 120 | cstart = 0 |
| 121 | for nstart, nval in data: |
| 122 | self.data.append((cstart, nstart - cstart, nval)) |
| 123 | cstart = nstart |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 124 | |
| 125 | @property |
| 126 | def values(self): |
| 127 | return [val[2] for val in self.data] |
| 128 | |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 129 | def average_interval(self): |
| 130 | return float(sum([val[1] for val in self.data])) / len(self.data) |
| 131 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 132 | def skip(self, seconds): |
| 133 | nres = [] |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 134 | for start, ln, val in self.data: |
| 135 | nstart = start + ln - seconds |
| 136 | if nstart > 0: |
| 137 | nres.append([nstart, val]) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 138 | return self.__class__(nres) |
| 139 | |
| 140 | def derived(self, tdelta): |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 141 | end = self.data[-1][0] + self.data[-1][1] |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 142 | tdelta = float(tdelta) |
| 143 | |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 144 | ln = end / tdelta |
| 145 | |
| 146 | if ln - int(ln) > 0: |
| 147 | ln += 1 |
| 148 | |
| 149 | res = [[tdelta * i, 0.0] for i in range(int(ln))] |
| 150 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 151 | for start, lenght, val in self.data: |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 152 | start_idx = int(start / tdelta) |
| 153 | end_idx = int((start + lenght) / tdelta) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 154 | |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 155 | for idx in range(start_idx, end_idx + 1): |
| 156 | rstart = tdelta * idx |
| 157 | rend = tdelta * (idx + 1) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 158 | |
koder aka kdanilov | bb6d6cd | 2015-06-20 02:55:07 +0300 | [diff] [blame] | 159 | intersection_ln = min(rend, start + lenght) - max(start, rstart) |
| 160 | if intersection_ln > 0: |
| 161 | try: |
| 162 | res[idx][1] += val * intersection_ln / tdelta |
| 163 | except IndexError: |
| 164 | raise |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 165 | |
| 166 | return self.__class__(res) |
| 167 | |
| 168 | |
| 169 | class PerfTest(object): |
| 170 | """ |
| 171 | Very base class for tests |
| 172 | config:TestConfig - test configuration |
| 173 | stop_requested:bool - stop for test requested |
| 174 | """ |
| 175 | def __init__(self, config): |
| 176 | self.config = config |
koder aka kdanilov | e2de58c | 2015-04-24 22:59:36 +0300 | [diff] [blame] | 177 | self.stop_requested = False |
| 178 | |
| 179 | def request_stop(self): |
| 180 | self.stop_requested = True |
koder aka kdanilov | 2066daf | 2015-04-23 21:05:41 +0300 | [diff] [blame] | 181 | |
| 182 | def join_remote(self, path): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 183 | return os.path.join(self.config.remote_dir, path) |
koder aka kdanilov | 4500a5f | 2015-04-17 16:55:17 +0300 | [diff] [blame] | 184 | |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 185 | @classmethod |
| 186 | @abc.abstractmethod |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 187 | def load(cls, path): |
koder aka kdanilov | 4af1c1d | 2015-05-18 15:48:58 +0300 | [diff] [blame] | 188 | pass |
| 189 | |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 190 | @abc.abstractmethod |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 191 | def run(self): |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 192 | pass |
| 193 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 194 | @abc.abstractmethod |
koder aka kdanilov | cff7b2e | 2015-04-18 20:48:15 +0300 | [diff] [blame] | 195 | def format_for_console(cls, data): |
koder aka kdanilov | ec1b973 | 2015-04-23 20:43:29 +0300 | [diff] [blame] | 196 | pass |
| 197 | |
koder aka kdanilov | 4643fd6 | 2015-02-10 16:20:13 -0800 | [diff] [blame] | 198 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 199 | def run_on_node(node): |
| 200 | def closure(*args, **kwargs): |
| 201 | return run_over_ssh(node.connection, |
| 202 | *args, |
| 203 | node=node.get_conn_id(), |
| 204 | **kwargs) |
| 205 | return closure |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 206 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 207 | |
| 208 | class ThreadedTest(PerfTest): |
| 209 | """ |
| 210 | Base class for tests, which spawn separated thread for each node |
| 211 | """ |
| 212 | |
| 213 | def run(self): |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 214 | barrier = Barrier(len(self.config.nodes)) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 215 | th_test_func = functools.partial(self.th_test_func, barrier) |
| 216 | |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 217 | with ThreadPoolExecutor(len(self.config.nodes)) as pool: |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 218 | return list(pool.map(th_test_func, self.config.nodes)) |
| 219 | |
| 220 | @abc.abstractmethod |
| 221 | def do_test(self, node): |
| 222 | pass |
| 223 | |
| 224 | def th_test_func(self, barrier, node): |
| 225 | logger.debug("Starting {0} test on {1} node".format(self.__class__.__name__, |
| 226 | node.conn_url)) |
| 227 | |
| 228 | logger.debug("Run preparation for {0}".format(node.get_conn_id())) |
| 229 | self.pre_run(node) |
| 230 | barrier.wait() |
| 231 | try: |
| 232 | logger.debug("Run test for {0}".format(node.get_conn_id())) |
| 233 | return self.do_test(node) |
| 234 | except StopTestError as exc: |
| 235 | pass |
| 236 | except Exception as exc: |
| 237 | msg = "In test {0} for node {1}".format(self, node.get_conn_id()) |
| 238 | logger.exception(msg) |
| 239 | exc = StopTestError(msg, exc) |
| 240 | |
| 241 | try: |
| 242 | self.cleanup() |
| 243 | except StopTestError as exc1: |
| 244 | if exc is None: |
| 245 | exc = exc1 |
| 246 | except Exception as exc1: |
| 247 | if exc is None: |
| 248 | msg = "Duringf cleanup - in test {0} for node {1}".format(self, node) |
| 249 | logger.exception(msg) |
| 250 | exc = StopTestError(msg, exc) |
| 251 | |
| 252 | if exc is not None: |
| 253 | raise exc |
| 254 | |
| 255 | def pre_run(self, node): |
| 256 | pass |
| 257 | |
| 258 | def cleanup(self, node): |
| 259 | pass |
| 260 | |
| 261 | |
| 262 | class TwoScriptTest(ThreadedTest): |
| 263 | def __init__(self, *dt, **mp): |
| 264 | ThreadedTest.__init__(self, *dt, **mp) |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 265 | self.remote_dir = '/tmp' |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 266 | self.prerun_script = self.config.params['prerun_script'] |
| 267 | self.run_script = self.config.params['run_script'] |
| 268 | |
| 269 | self.prerun_tout = self.config.params.get('prerun_tout', 3600) |
| 270 | self.run_tout = self.config.params.get('run_tout', 3600) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 271 | |
| 272 | def get_remote_for_script(self, script): |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 273 | return os.path.join(self.remote_dir, |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 274 | os.path.basename(script)) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 275 | |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 276 | def pre_run(self, node): |
| 277 | copy_paths(node.connection, |
| 278 | { |
| 279 | self.run_script: self.get_remote_for_script(self.run_script), |
| 280 | self.prerun_script: self.get_remote_for_script(self.prerun_script), |
| 281 | }) |
| 282 | |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 283 | cmd = self.get_remote_for_script(self.prerun_script) |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 284 | cmd += ' ' + self.config.params.get('prerun_opts', '') |
| 285 | run_on_node(node)(cmd, timeout=self.prerun_tout) |
Yulia Portnova | 7ddfa73 | 2015-02-24 17:32:58 +0200 | [diff] [blame] | 286 | |
Yulia Portnova | b0c977c | 2015-12-11 19:23:28 +0200 | [diff] [blame^] | 287 | def do_test(self, node): |
koder aka kdanilov | bc2c898 | 2015-06-13 02:50:43 +0300 | [diff] [blame] | 288 | cmd = self.get_remote_for_script(self.run_script) |
| 289 | cmd += ' ' + self.config.params.get('run_opts', '') |
| 290 | t1 = time.time() |
| 291 | res = run_on_node(node)(cmd, timeout=self.run_tout) |
| 292 | t2 = time.time() |
| 293 | return TestResults(self.config, None, res, (t1, t2)) |