Example #1
0
    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))
Example #2
0
    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()
Example #3
0
        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
Example #4
0
    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
Example #5
0
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
Example #6
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
Example #7
0
    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
Example #8
0
 def set_stage(self, stage):
     check_arg(stage, str, stage in self.STAGES)
     logger.info("Enter stage '{}'".format(stage.upper()))
     self.stage = stage
Example #9
0
 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()")
Example #10
0
    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
Example #11
0
    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
Example #12
0
    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()
Example #13
0
 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))
Example #14
0
 def log_info(self, *args):
     logger.info(self.session_id, *args)