def load_config(self, run_config_file): """Load configuration file and set shortcuts.""" cr = ConfigManager(self.stats) cr.read(run_config_file, load_files=True) self.config_manager = cr # self.run_config = cr.run_config logger.info("Successfully compiled configuration {}.".format(cr.path))
def run_in_threads(self, user_list, context): self.publish("start_run", run_manager=self) self.stop_request.clear() thread_list = [] self.session_list = [] for i, user in enumerate(user_list, 1): name = "t{:02}".format(i) sess = SessionManager(self, context, name, user) self.session_list.append(sess) t = threading.Thread(name=name, target=self._run_one, args=[sess]) t.setDaemon(True) # Required to make Ctrl-C work thread_list.append(t) logger.info("Starting {} session workers...".format(len(thread_list))) self.set_stage("running") self.stats.report_start(None, None, None) ramp_up_delay = self.config_manager.sessions.get("ramp_up_delay") start_run = time.monotonic() for i, t in enumerate(thread_list): if ramp_up_delay and i > 1: delay = get_random_number(ramp_up_delay) logger.info( "Ramp-up delay for t{:02}: {:.2f} seconds...".format(i, delay) ) time.sleep(delay) t.start() logger.important( "All {} sessions running, waiting for them to terminate...".format( len(thread_list) ) ) for t in thread_list: t.join() self.set_stage("done") elap = time.monotonic() - start_run # self.stats.add_timing("run", elap) self.stats.report_end(None, None, None) self.publish("end_run", run_manager=self, elap=elap) logger.debug("Results for {}:\n{}".format(self, self.stats.format_result())) return not self.has_errors()
def _work(name): # logger.debug("StaticRequests({}) started...".format(name, )) while not session.stop_request.is_set(): try: url = queue.get(False) except Empty: break if debug: logger.info("StaticRequests({}, {})...".format(name, url)) # The actual HTTP request: # TODO: requests.Session is not guaranteed to be thread-safe! try: res = bs.request(method, url, **r_args) res.raise_for_status() results.append((True, name, url, None)) except Exception as e: results.append((False, name, url, "{}".format(e))) queue.task_done() logger.debug("StaticRequests({}) stopped.".format(name)) return
def update_config(self, extra_config, context_only=False): """Override self.config (and self.context) with new items. self.config was already copied to self.context, so normally we want to update both. Args: extra_config (dict): new values context_only (bool): pass true to only set the shadow-copy (i.e. context) """ check_arg(extra_config, dict, or_none=True) if not extra_config: return config = self.config context = self.context for k, v in extra_config.items(): if not context_only: logger.info("Set config.{}: {!r} -> {!r}".format(k, config.get(k), v)) config[k] = v else: logger.info("Set context.{}: {!r} -> {!r}".format(k, context.get(k), v)) context[k] = v return
def handle_run_command(parser, args): options = { "monitor": args.monitor, "log_summary": True, # "dry_run": args.dry_run, } extra_context = {} # Parse `--option=NAME:VALUE` arguments: try: extra_context.update(parse_option_args(args.option, coerce_values=True)) except Exception as e: parser.error("--option: {}".format(e)) # Make sure that --dry-run arg is always honored: if args.dry_run: extra_context["dry_run"] = True scenario_fspec = args.scenario if os.path.isdir(scenario_fspec): scenario_fspec = os.path.join(scenario_fspec, "scenario.yaml") logger.info("Looking for {}".format(scenario_fspec)) rm = RunManager() rm.load_config(scenario_fspec) if args.single: rm.config_manager.config["force_single"] = True if args.max_time: rm.config_manager.config["max_time"] = float(args.max_time) if args.max_errors: rm.config_manager.config["max_errors"] = int(args.max_errors) res = rm.run(options, extra_context) if not res: logger.error("Finished with errors.") return 1 logger.info("Stressor run succesfully completed.") return 0
def register_plugins(cls, arg_parser): """Call `register_fn` on all loaded entry points.""" if cls.plugins_registered: return cls.plugins_registered = True # Import stock class definitions, so we can scan the subclasses. # This will add standard plugins to ActivityBase.__subclasses__() # and MacroBase.__subclasses__(): import stressor.plugins.common # noqa F401 import stressor.plugins.http_activities # noqa F401 import stressor.plugins.script_activities # noqa F401 # Load entry points from all installed mosules that have the # 'stressor.plugins' namespace: cls.find_plugins() # Call `register_fn` on all loaded entry points_ ep_map = cls._entry_point_map for name, ep in cls._entry_point_map.items(): logger.info("Load plugins {}...".format(ep.dist)) try: register_fn = ep.load() if not callable(register_fn): raise RuntimeError( "Entry point {} is not a function".format(ep)) ep_map[ep.name] = register_fn except Exception: logger.exception("Failed to load {}".format(ep)) prev_activities = list(ActivityBase.__subclasses__()) prev_macros = list(MacroBase.__subclasses__()) logger.debug("Register plugins {}...".format(ep.dist)) try: # The plugin must declare new classes derrived from # ActivityBase and/or MacroBase register_fn( activity_base=ActivityBase, macro_base=MacroBase, arg_parser=arg_parser, ) except Exception: logger.exception("Could not register {}".format(name)) continue found_one = False for activity_cls in ActivityBase.__subclasses__(): if activity_cls in prev_activities: continue found_one = True logger.info("Register {}.{}".format(ep.dist, activity_cls)) for macro_cls in MacroBase.__subclasses__(): if macro_cls in prev_macros: continue found_one = True logger.info("Register {}.{}".format(ep.dist, macro_cls)) if not found_one: logger.warning( "Plugin {} did not register activites nor macros".format( ep.dist)) # Build plugin maps from currently known subclasses cls._register_subclasses(ActivityBase, cls.activity_plugin_map) logger.debug("Registered activity plugins:\n{}".format( cls.activity_plugin_map)) cls._register_subclasses(MacroBase, cls.macro_plugin_map) logger.debug("Registered macro plugins:\n{}".format( cls.macro_plugin_map)) return
def run(self, options, extra_context=None): """Run the current Args: options (dict): see RunManager.DEFAULT_OPTS extra_context (dict, optional): Returns: (int) Exit code 0 if no errors occurred """ check_arg(options, dict) check_arg(extra_context, dict, or_none=True) self.options.update(options) if extra_context: self.config_manager.update_config(extra_context) context = self.config_manager.context sessions = self.config_manager.sessions count = int(sessions.get("count", 1)) if count > 1 and self.config_manager.config.get("force_single"): logger.info("force_single: restricting sessions count to one.") count = 1 # Construct a `User` with at least 'name', 'password', and optional # custom attributes user_list = [] for user_dict in sessions["users"]: user = User(**user_dict) user_list.append(user) # We have N users and want `count` sessions: re-use round-robin user_list = itertools.islice(itertools.cycle(user_list), 0, count) user_list = list(user_list) monitor = None if self.options.get("monitor"): monitor = MonitorServer(self) monitor.start() time.sleep(0.5) monitor.open_browser() self.start_stamp = time.monotonic() self.start_dt = datetime.now() self.end_dt = None self.end_stamp = None try: try: res = False res = self.run_in_threads(user_list, context) except KeyboardInterrupt: # if not self.stop_request.is_set(): logger.warning("Caught Ctrl-C: terminating...") self.stop() finally: self.end_dt = datetime.now() self.end_stamp = time.monotonic() if self.options.get("log_summary", True): logger.important(self.get_cli_summary()) if monitor: self.set_stage("waiting") logger.important("Waiting for monitor... Press Ctrl+C to quit.") self.stop_request.wait() finally: if monitor: monitor.shutdown() # print("RES", res, self.has_errors(), self.stats.format_result()) self.set_stage("stopped") return res
def set_stage(self, stage): check_arg(stage, str, stage in self.STAGES) logger.info("Enter stage '{}'".format(stage.upper())) self.stage = stage
def set_console_ctrl_handler(): if RunManager.CTRL_HANDLER_SET is None: RunManager.CTRL_HANDLER_SET = set_console_ctrl_handler( RunManager._console_ctrl_handler ) logger.info("set_console_ctrl_handler()")
def execute(self, session, **expanded_args): """""" global_vars = { # "foo": 41, # "__builtins__": {}, } # local_vars = session.context local_vars = session.context.copy() assert "result" not in local_vars assert "session" not in local_vars local_vars["session"] = session.make_helper() # prev_local_keys = set(locals()) prev_global_keys = set(globals()) prev_context_keys = set(local_vars.keys()) try: exec(self.script, global_vars, local_vars) except ConnectionError as e: # TODO: more requests-exceptions? msg = "Script failed: {!r}: {}".format(e, e) raise ScriptActivityError(msg) except Exception as e: msg = "Script failed: {!r}: {}".format(e, e) if session.verbose >= 4: logger.exception(msg) raise ScriptActivityError(msg) from e raise ScriptActivityError(msg) finally: local_vars.pop("session") result = local_vars.pop("result", None) context_keys = set(local_vars.keys()) new_keys = context_keys.difference(prev_context_keys) if new_keys: if self.export is None: logger.info( "Skript activity has no `export` defined. Ignoring new variables: '{}'".format( "', '".join(new_keys) ) ) else: for k in self.export: v = local_vars.get(k) assert type(v) in (int, float, str, list, dict) session.context[k] = v logger.debug("Set context.{} = {!r}".format(k, v)) # store_keys = new_keys.intersection(self.export) # TODO: this cannot happen? new_globals = set(globals().keys()).difference(prev_global_keys) if new_globals: logger.warning("Script-defined globals: {}".format(new_globals)) raise ScriptActivityError("Script introduced globals") # new_context = context_keys.difference(prev_context_keys) # logger.info("Script-defined context-keys: {}".format(new_context)) # new_locals = set(locals().keys()).difference(prev_local_keys) # if new_locals: # logger.info("Script-defined locals: {}".format(new_locals)) # logger.info("Script locals:\n{}".format(pformat(local_vars))) if expanded_args.get("debug") or session.verbose >= 5: logger.info( "{} {}\n Context after execute:\n {}\n return value: {!r}".format( session.context_stack, self, pformat(session.context, indent=4), result, ) ) elif session.verbose >= 3 and result is not None: logger.info( "{} returnd: {!r}".format( session.context_stack, shorten_string(result, 200) if isinstance(result, str) else result, ) ) return result
def execute(self, session, **expanded_args): """ Raises: ActivityAssertionError: requests.exceptions.ConnectionError: 'Connection refused', etc. requests.exceptions.HTTPError: On 404, 500, etc. """ url = expanded_args.get("url") base_url = session.get_context("base_url") if not base_url and is_relative_url(url): raise ActivityError( "Missing context variable 'base_url' to resolve relative URLs") expanded_args.setdefault("timeout", session.get_context("request_timeout")) assert "timeout" in expanded_args debug = expanded_args.get("debug") # print("session.dry_run", session.dry_run) if session.dry_run: return expanded_args.get("mock_result", "dummy_result") bs = session.browser_session method = self.raw_args["method"] url = expanded_args.pop("url") url = resolve_url(base_url, url) r_args = { k: v for k, v in expanded_args.items() if k in self.REQUEST_ARGS } verify_ssl = session.sessions.get("verify_ssl", True) r_args.setdefault("verify", verify_ssl) basic_auth = session.sessions.get("basic_auth", False) if basic_auth: r_args.setdefault("auth", session.user.auth) headers = r_args.setdefault("headers", {}) headers.setdefault( "User-Agent", "session/{} Stressor/{}".format(session.session_id, __version__), ) # if debug: # http_client.HTTPConnection.debuglevel = 1 # else: # http_client.HTTPConnection.debuglevel = 0 if debug: logger.info("HTTPRequest({}, {}, {})...".format( method, url, r_args)) # The actual HTTP request: try: resp = bs.request(method, url, **r_args) except requests.exceptions.Timeout as e: raise ActivityTimeoutError("{}".format(e)) except RequestException as e: raise ActivityError("{}".format(e)) is_json = False try: result = resp.json() is_json = True except ValueError: result = resp.text if not resp.ok: logger.error(self._format_response(resp, short=not debug)) elif debug: logger.info(self._format_response(resp, short=False)) assert_status = expanded_args.get("assert_status") if not assert_status: # requests.exceptions.HTTPError: On 404, 500, etc. resp.raise_for_status() elif resp.status_code not in assert_status: self._raise_assertion( "HTTP status does not match {}: {}".format( assert_status, resp.status_code), resp, ) arg = expanded_args.get("assert_match_headers") if arg: text = str(resp.headers) if not re.match(arg, text): self._raise_assertion( "Result headers do not match `{}`".format(arg), resp) arg = expanded_args.get("assert_json") if arg: if not is_json: self._raise_assertion("Unexpected result type (expected JSON)", resp) for key, pattern in arg.items(): value = get_dict_attr(result, key) # print(result, key, value) match, msg = match_value(pattern, value, key) if not match: self._raise_assertion( "Unexpected JSON result {}".format(msg), resp) arg = expanded_args.get("assert_html") if arg: if is_json: self._raise_assertion("Unexpected result type (expected HTML)", resp) for xpath, pattern in arg.items(): # print(result) tree = html.fromstring(result) # print("tree", html.tostring(tree)) # match = tree.xpath("body/span[contains(@class, 'test') and text() = 'abc']") match = tree.xpath(xpath) if pattern is True: ok = bool(match) elif pattern is False: ok = not match elif not match: ok = False else: # print("match", html.tostring(match)) raise NotImplementedError if not ok: self._raise_assertion( "Unexpected HTML result: XPath {!r} -> {}".format( xpath, match), resp, ) return result
def run(self): stack = self.context_stack rm = self.run_manager config_manager = rm.config_manager config = config_manager.config sequences = config_manager.sequences scenario = config_manager.scenario sessions = config_manager.sessions session_duration = float(sessions.get("duration", 0.0)) self.publish("start_session", session=self) self.stats.report_start(self, None, None) start_session = time.monotonic() skip_all = False skip_all_but_end = False for seq_idx, seq_def in enumerate(scenario, 1): seq_name = seq_def["sequence"] if skip_all or (skip_all_but_end and seq_name != "end"): logger.warning("Skipping sequence '{}'.".format(seq_name)) continue sequence = sequences.get(seq_name) loop_repeat = int(seq_def.get("repeat", 0)) loop_duration = float(seq_def.get("duration", 0.0)) start_seq_loop = time.monotonic() loop_idx = 0 while True: loop_idx += 1 if not self.check_run_limits(seq_name=seq_name): skip_all_but_end = True break # One single pass by default if not loop_repeat and not loop_duration and loop_idx > 1: break # `Sequence repeat: COUNT`: if loop_repeat and loop_idx > loop_repeat: break # `--single`: if loop_idx > 1 and config.get("force_single"): logger.warning( "force_single: sequence '{}' skipping remaining {} loops." .format(seq_name, loop_repeat - 1 if loop_repeat else "")) break now = time.monotonic() # `Sequence duration: SECS`: if loop_duration > 0 and now > (start_seq_loop + loop_duration): logger.info( "Stopping sequence '{}' loop after {} sec.".format( seq_name, loop_duration)) break # `Session duration: SECS` (but run 'end' sequence): elif (seq_name != "end" and session_duration > 0 and now > (start_session + session_duration)): logger.info( "Stopping scenario '{}' loop after {} sec.".format( seq_name, session_duration)) skip_all_but_end = True break with stack.enter("#{:02}-{}@{}".format(seq_idx, seq_name, loop_idx)): is_ok = self.run_sequence(seq_name, sequence) if seq_name == "init" and not is_ok: logger.error( "Stopping scenario due to an error in the 'init' sequence." ) skip_all = True break elif self.stop_request.is_set(): logger.error( "Stopping scenario due to a stop request.") # TODO: a second 'ctrl-c' should not be so graceful skip_all_but_end = True break # self.stats.report_end(self, seq_name, None) elap = time.monotonic() - start_session self.stats.report_end(self, None, None) self.publish("end_session", session=self, elap=elap) # if self._cancelled_seq: # return False return not self.has_errors()
def report_activity_start(self, sequence, activity): """Called by session runner before activities is executed.""" path = self.context_stack.path() self.stats.report_start(self, sequence, activity, path=path) logger.info("{} {}".format("DRY-RUN" if self.dry_run else "Execute", path))
def log_info(self, *args): logger.info(self.session_id, *args)