Exemple #1
0
    def _github_create_repo(cls, **kwargs):
        """Create repo on GitHub.

        If repository already exists then RunException will be raised.

        Raises:
            devassistant.exceptions.RunException on error
        """
        username = cls._github_username(**kwargs)
        reponame = cls._github_reponame(**kwargs)
        password = getpass.getpass(prompt='GitHub password:'******'Repository already exists on GitHub'
                logger.error(msg)
                raise exceptions.RunException(msg)
            else:
                new_repo = user.create_repo(reponame)
                logger.info('Your new repository: {0}'.format(
                    new_repo.html_url))
        except github.GithubException as e:
            msg = 'GitHub error: {0}'.format(e)
            logger.log(msg)
            raise exceptions.RunException(msg)
Exemple #2
0
 def run(cls, comm_type, comm, **kwargs):
     if comm_type in map(lambda x: 'log_{0}'.format(x), settings.LOG_LEVELS_MAP):
         logger.log(logging._levelNames[settings.LOG_LEVELS_MAP[comm_type[-1]]], comm)
         if comm_type[-1] in 'ce':
             raise exceptions.RunException(comm)
     else:
         logger.warning('Unknown logging command {0} with message {1}'.format(comm_type, comm))
Exemple #3
0
    def _github_create_repo(cls, **kwargs):
        """Create repo on GitHub.

        If repository already exists then RunException will be raised.

        Raises:
            devassistant.exceptions.RunException on error
        """
        username = cls._github_username(**kwargs)
        reponame = cls._github_reponame(**kwargs)
        password = getpass.getpass(prompt='GitHub password:'******'Repository already exists on GitHub'
                logger.error(msg)
                raise exceptions.RunException(msg)
            else:
                new_repo = user.create_repo(reponame)
                logger.info('Your new repository: {0}'.format(new_repo.html_url))
        except github.GithubException as e:
            msg = 'GitHub error: {0}'.format(e)
            logger.log(msg)
            raise exceptions.RunException(msg)
Exemple #4
0
    def run(cls, c):
        if c.comm_type in map(lambda x: 'log_{0}'.format(x), settings.LOG_LEVELS_MAP):
            logger.log(settings.LOG_SHORT_TO_NUM_LEVEL[c.comm_type[-1]], c.input_res)
            if c.comm_type[-1] in 'ce':
                e = exceptions.CommandException(c.input_res)
                e.already_logged = True
                raise e
        else:
            raise exceptions.CommandException('Unknown command type {ct}.'.format(ct=c.comm_type))

        return [True, c.input_res]
    def run(cls, c):
        if c.comm_type in map(lambda x: 'log_{0}'.format(x), settings.LOG_LEVELS_MAP):
            logger.log(logging._levelNames[settings.LOG_LEVELS_MAP[c.comm_type[-1]]], c.input_res)
            if c.comm_type[-1] in 'ce':
                e = exceptions.CommandException(c.input_res)
                e.already_logged = True
                raise e
        else:
            raise exceptions.CommandException('Unknown command type {ct}.'.format(ct=c.comm_type))

        return [True, c.input_res]
    def run(cls, c):
        if c.comm_type in map(lambda x: "log_{0}".format(x), settings.LOG_LEVELS_MAP):
            logger.log(settings.LOG_SHORT_TO_NUM_LEVEL[c.comm_type[-1]], c.input_res)
            if c.comm_type[-1] in "ce":
                e = exceptions.CommandException(c.input_res)
                e.already_logged = True
                raise e
        else:
            raise exceptions.CommandException("Unknown command type {ct}.".format(ct=c.comm_type))

        return [True, c.input_res]
Exemple #7
0
 def load_yaml_by_path(cls, path, log_debug=False):
     """Load a yaml file that is at given path"""
     try:
         return yaml.load(open(path, 'r'), Loader=Loader) or {}
     except (yaml.scanner.ScannerError, yaml.parser.ParserError) as e:
         log_level = logging.DEBUG if log_debug else logging.WARNING
         logger.log(log_level, 'Yaml error in {path} (line {ln}, column {col}): {err}'.
                    format(path=path,
                           ln=e.problem_mark.line,
                           col=e.problem_mark.column,
                           err=e.problem))
         return None
Exemple #8
0
 def run(cls, comm_type, comm, **kwargs):
     if comm_type in map(lambda x: 'log_{0}'.format(x),
                         settings.LOG_LEVELS_MAP):
         logger.log(
             logging._levelNames[settings.LOG_LEVELS_MAP[comm_type[-1]]],
             comm)
         if comm_type[-1] in 'ce':
             raise exceptions.RunException(comm)
     else:
         logger.warning(
             'Unknown logging command {0} with message {1}'.format(
                 comm_type, comm))
 def load_yaml_by_path(cls, path, log_debug=False):
     """Load a yaml file that is at given path"""
     try:
         return yaml.load(open(path, 'r'), Loader=Loader) or {}
     except (yaml.scanner.ScannerError, yaml.parser.ParserError) as e:
         log_level = logging.DEBUG if log_debug else logging.WARNING
         logger.log(log_level, 'Yaml error in {path} (line {ln}, column {col}): {err}'.\
             format(path=path,
                    ln=e.problem_mark.line,
                    col=e.problem_mark.column,
                    err=e.problem))
         return None
 def load_yaml_by_path(cls, path, log_debug=False):
     """Load a yaml file that is at given path,
     if the path is not a string, it is assumed it's a file-like object"""
     try:
         if isinstance(path, six.string_types):
             return yaml.load(open(path, 'r'), Loader=Loader) or {}
         else:
             return yaml.load(path, Loader=Loader) or {}
     except (yaml.scanner.ScannerError, yaml.parser.ParserError) as e:
         log_level = logging.DEBUG if log_debug else logging.WARNING
         logger.log(log_level, 'Yaml error in {path} (line {ln}, column {col}): {err}'.
                    format(path=path,
                           ln=e.problem_mark.line,
                           col=e.problem_mark.column,
                           err=e.problem))
         return None
Exemple #11
0
 def load_yaml_by_path(cls, path, log_debug=False):
     """Load a yaml file that is at given path,
     if the path is not a string, it is assumed it's a file-like object"""
     try:
         if isinstance(path, six.string_types):
             return yaml.load(open(path, 'r'), Loader=Loader) or {}
         else:
             return yaml.load(path, Loader=Loader) or {}
     except (yaml.scanner.ScannerError, yaml.parser.ParserError) as e:
         log_level = logging.DEBUG if log_debug else logging.WARNING
         logger.log(
             log_level,
             'Yaml error in {path} (line {ln}, column {col}): {err}'.format(
                 path=path,
                 ln=e.problem_mark.line,
                 col=e.problem_mark.column,
                 err=e.problem))
         return None
Exemple #12
0
    def run_command(cls, cmd_str, log_level=logging.DEBUG, scls=[], ignore_sigint=False):
        """Runs a command from string, e.g. "cp foo bar" """
        # format for scl execution if needed
        cmd_str = cls.format_for_scls(cmd_str, scls)
        logger.log(log_level, cmd_str, extra={'event_type': 'cmd_call'})

        if cmd_str.startswith('cd '):
            # special-case cd to behave like shell cd and stay in the directory
            try:
                # delete any qoutes, the quoting is automatical in os.chdir
                directory = cmd_str.split()[1].replace('"', '').replace('\'', '')
                os.chdir(directory)
            except OSError as e:
                raise exceptions.ClException(cmd_str, 1, str(e))
            return ''

        stdin_pipe = None
        stdout_pipe = subprocess.PIPE
        stderr_pipe = subprocess.STDOUT
        preexec_fn = cls.ignore_sigint if ignore_sigint else None
        proc = subprocess.Popen(cmd_str,
                                stdin=stdin_pipe,
                                stdout=stdout_pipe,
                                stderr=stderr_pipe,
                                shell=True,
                                preexec_fn=preexec_fn)
        stdout = []
        while proc.poll() == None:
            output = proc.stdout.readline().strip().decode('utf8')
            stdout.append(output)
            logger.log(log_level, output, extra={'event_type': 'cmd_out'})
        stdout = '\n'.join(stdout)
        # there may be some remains not read after exiting the previous loop
        output_rest = proc.stdout.read().strip().decode('utf8')
        # we want to log lines separately, not as one big chunk
        output_rest_lines = output_rest.splitlines()
        for i, l in enumerate(output_rest.splitlines()):
            logger.log(log_level, l, extra={'event_type': 'cmd_out'})
            # if rest of the output doesn't end with \n, we don't add \n after last line
            if i == len(output_rest_lines) - 1 and not output_rest.endswith('\n'):
                stdout += l
            # else we add it everytime
            else:
                stdout += l + '\n'

        # log return code always on debug level
        logger.log(logging.DEBUG, proc.returncode, extra={'event_type': 'cmd_retcode'})

        if proc.returncode == 0:
            return stdout.strip()
        else:
            raise exceptions.ClException(cmd_str,
                                         proc.returncode,
                                         stdout)
 def log(cls, level, msg, event_type, secret):
     extra = {'event_type': event_type}
     if secret:
         if event_type == 'cmd_call':
             logger.log(level, 'LOGGING PREVENTED FOR SECURITY REASONS', extra=extra)
         elif event_type == 'cmd_retcode':
             logger.log(level, msg, extra=extra)
         # no output logging for secret commands
     else:
         logger.log(level, msg, extra=extra)
Exemple #14
0
 def log(cls, level, msg, event_type, secret):
     extra = {'event_type': event_type}
     if secret:
         if event_type == 'cmd_call':
             logger.log(level,
                        'LOGGING PREVENTED FOR SECURITY REASONS',
                        extra=extra)
         elif event_type == 'cmd_retcode':
             logger.log(level, msg, extra=extra)
         # no output logging for secret commands
     else:
         logger.log(level, msg, extra=extra)
Exemple #15
0
    def _run_one_section(self, section, kwargs):
        skip_else = False

        for i, command_dict in enumerate(section):
            if self.stop_flag:
                break
            for comm_type, comm in command_dict.items():
                if comm_type.startswith('call'):
                    # calling workflow:
                    # 1) get proper run section (either from self or from snippet)
                    # 2) if running snippet, add its files to kwargs['__files__']
                    # 3) actually run
                    # 4) if running snippet, pop its files from kwargs['__files__']
                    sect = self._get_section_from_call(comm, 'run')

                    if sect is None:
                        logger.warning(
                            'Couldn\'t find section to run: {0}.'.format(comm))
                        continue

                    if self._is_snippet_call(comm, **kwargs):
                        # we're calling a snippet => add files and template_dir to kwargs
                        snippet = yaml_snippet_loader.YamlSnippetLoader.get_snippet_by_name(
                            comm.split('.')[0])

                        if '__files__' not in kwargs:
                            kwargs['__files__'] = []
                            kwargs['__template_dir__'] = []
                        kwargs['__files__'].append(snippet.get_files_section())
                        kwargs['__template_dir__'].append(
                            snippet.get_template_dir())

                    self._run_one_section(sect, copy.deepcopy(kwargs))

                    if self._is_snippet_call(comm, **kwargs):
                        kwargs['__files__'].pop()
                        kwargs['__template_dir__'].pop()
                elif comm_type.startswith('$'):
                    # intentionally pass kwargs as dict, not as keywords
                    try:
                        self._assign_variable(comm_type, comm, kwargs)
                    except exceptions.YamlSyntaxError as e:
                        logger.error(e)
                        raise e
                elif comm_type.startswith('if'):
                    possible_else = None
                    if len(section) > i + 1:  # do we have "else" clause?
                        possible_else = list(section[i + 1].items())[0]
                    _, skip_else, to_run = self._get_section_from_condition(
                        (comm_type, comm), possible_else, **kwargs)
                    if to_run:
                        # run with original kwargs, so that they might be changed for code after this if
                        self._run_one_section(to_run, kwargs)
                elif comm_type == 'else':
                    if not skip_else:
                        logger.warning(
                            'Yaml error: encountered "else" with no associated "if", skipping.'
                        )
                    skip_else = False
                elif comm_type.startswith('for'):
                    # syntax: "for $i in $x: <section> or "for $i in cl_command: <section>"
                    try:
                        control_var, expression = self._parse_for(comm_type)
                    except exceptions.YamlSyntaxError as e:
                        logger.error(e)
                        raise e
                    try:
                        eval_expression = evaluate_expression()
                    except exceptions.YamlSyntaxError as e:
                        logger.log(e)
                        raise e

                    for i in eval_expression:
                        kwargs[control_var] = i
                        self._run_one_section(comm, kwargs)
                elif comm_type.startswith('scl'):
                    if '__scls__' not in kwargs:
                        kwargs['__scls__'] = []
                    # list of lists of scl names
                    kwargs['__scls__'].append(comm_type.split()[1:])
                    self._run_one_section(comm, kwargs)
                    kwargs['__scls__'].pop()
                else:
                    files = kwargs['__files__'][-1] if kwargs.get(
                        '__files__', None) else self._files
                    template_dir = kwargs['__template_dir__'][-1] if kwargs.get(
                        '__template_dir__', None) else self.template_dir
                    run_command(
                        comm_type,
                        CommandFormatter.format(comm_type, comm, template_dir,
                                                files, **kwargs), **kwargs)
    def run_command(cls,
                    cmd_str,
                    log_level=logging.DEBUG,
                    ignore_sigint=False,
                    output_callback=None,
                    as_user=None):
        """Runs a command from string, e.g. "cp foo bar"
        Args:
            cmd_str: the command to run as string
            log_level: level at which to log command output (DEBUG by default)
            ignore_sigint: should we ignore sigint during this command (False by default)
            output_callback: function that gets called with every line of output as argument
            as_user: run as specified user (the best way to do this will be deduced by DA)
                runs as current user if as_user == None
        """
        # run format processors on cmd_str
        for name, cmd_proc in cls.command_processors.items():
            cmd_str = cmd_proc(cmd_str)

        # TODO: how to do cd with as_user?
        if as_user and not cmd_str.startswith('cd '):
            cmd_str = cls.format_for_another_user(cmd_str, as_user)
        logger.log(log_level, cmd_str, extra={'event_type': 'cmd_call'})

        if cmd_str.startswith('cd '):
            # special-case cd to behave like shell cd and stay in the directory
            try:
                directory = cmd_str[3:]
                # delete any quotes, os.chdir doesn't split words like sh does
                if directory[0] == directory[-1] == '"':
                    directory = directory[1:-1]
                os.chdir(directory)
            except OSError as e:
                raise exceptions.ClException(cmd_str, 1, six.text_type(e))
            return ''

        stdin_pipe = None
        stdout_pipe = subprocess.PIPE
        stderr_pipe = subprocess.STDOUT
        preexec_fn = cls.ignore_sigint if ignore_sigint else None
        proc = subprocess.Popen(cmd_str,
                                stdin=stdin_pipe,
                                stdout=stdout_pipe,
                                stderr=stderr_pipe,
                                shell=True,
                                preexec_fn=preexec_fn)
        # register process to cls.subprocesses
        cls.subprocesses[proc.pid] = proc

        stdout = []
        while proc.poll() is None:
            output = proc.stdout.readline().decode('utf8')
            if output:
                output = output.strip()
                stdout.append(output)
                logger.log(log_level, output, extra={'event_type': 'cmd_out'})
            if output_callback:
                output_callback(output)

        # remove process from cls.subprocesses
        cls.subprocesses.pop(proc.pid)

        # add a newline to the end - if there is more output in output_rest, we'll be appending
        # it line by line; if there's no more output, we strip anyway
        stdout = '\n'.join(stdout) + '\n'
        # there may be some remains not read after exiting the previous loop
        output_rest = proc.stdout.read().strip().decode('utf8')
        # we want to log lines separately, not as one big chunk
        output_rest_lines = output_rest.splitlines()
        for i, l in enumerate(output_rest_lines):
            logger.log(log_level, l, extra={'event_type': 'cmd_out'})
            # add newline for every line - for last line, only add it if it was originally present
            if i != len(output_rest_lines) - 1 or output_rest.endswith('\n'):
                l += '\n'
            stdout += l
            if output_callback:
                output_callback(l)

        # log return code always on debug level
        logger.log(logging.DEBUG,
                   proc.returncode,
                   extra={'event_type': 'cmd_retcode'})
        stdout = stdout.strip()

        if proc.returncode == 0:
            return stdout
        else:
            raise exceptions.ClException(cmd_str, proc.returncode, stdout)
    def run_command(cls,
                    cmd_str,
                    log_level=logging.DEBUG,
                    ignore_sigint=False,
                    output_callback=None,
                    as_user=None):
        """Runs a command from string, e.g. "cp foo bar"
        Args:
            cmd_str: the command to run as string
            log_level: level at which to log command output (DEBUG by default)
            ignore_sigint: should we ignore sigint during this command (False by default)
            output_callback: function that gets called with every line of output as argument
            as_user: run as specified user (the best way to do this will be deduced by DA)
                runs as current user if as_user == None
        """
        # run format processors on cmd_str
        for name, cmd_proc in cls.command_processors.items():
            cmd_str = cmd_proc(cmd_str)

        # TODO: how to do cd with as_user?
        if as_user and not cmd_str.startswith('cd '):
            cmd_str = cls.format_for_another_user(cmd_str, as_user)
        logger.log(log_level, cmd_str, extra={'event_type': 'cmd_call'})

        if cmd_str.startswith('cd '):
            # special-case cd to behave like shell cd and stay in the directory
            try:
                directory = cmd_str[3:]
                # delete any quotes, os.chdir doesn't split words like sh does
                if directory[0] == directory[-1] == '"':
                    directory = directory[1:-1]
                os.chdir(directory)
            except OSError as e:
                raise exceptions.ClException(cmd_str, 1, six.text_type(e))
            return ''

        stdin_pipe = None
        stdout_pipe = subprocess.PIPE
        stderr_pipe = subprocess.STDOUT
        preexec_fn = cls.ignore_sigint if ignore_sigint else None
        proc = subprocess.Popen(cmd_str,
                                stdin=stdin_pipe,
                                stdout=stdout_pipe,
                                stderr=stderr_pipe,
                                shell=True,
                                preexec_fn=preexec_fn)
        # register process to cls.subprocesses
        cls.subprocesses[proc.pid] = proc

        stdout = []
        while proc.poll() is None:
            output = proc.stdout.readline().decode('utf8')
            if output:
                output = output.strip()
                stdout.append(output)
                logger.log(log_level, output, extra={'event_type': 'cmd_out'})
            if output_callback:
                output_callback(output)

        # remove process from cls.subprocesses
        cls.subprocesses.pop(proc.pid)

        # add a newline to the end - if there is more output in output_rest, we'll be appending
        # it line by line; if there's no more output, we strip anyway
        stdout = '\n'.join(stdout) + '\n'
        # there may be some remains not read after exiting the previous loop
        output_rest = proc.stdout.read().strip().decode('utf8')
        # we want to log lines separately, not as one big chunk
        output_rest_lines = output_rest.splitlines()
        for i, l in enumerate(output_rest_lines):
            logger.log(log_level, l, extra={'event_type': 'cmd_out'})
            # add newline for every line - for last line, only add it if it was originally present
            if i != len(output_rest_lines) - 1 or output_rest.endswith('\n'):
                l += '\n'
            stdout += l
            if output_callback:
                output_callback(l)

        # log return code always on debug level
        logger.log(logging.DEBUG, proc.returncode, extra={'event_type': 'cmd_retcode'})
        stdout = stdout.strip()

        if proc.returncode == 0:
            return stdout
        else:
            raise exceptions.ClException(cmd_str,
                                         proc.returncode,
                                         stdout)
Exemple #18
0
    def _run_one_section(self, section, kwargs):
        skip_else = False

        for i, command_dict in enumerate(section):
            if self.stop_flag:
                break
            for comm_type, comm in command_dict.items():
                if comm_type.startswith('call'):
                    # calling workflow:
                    # 1) get proper run section (either from self or from snippet)
                    # 2) if running snippet, add its files to kwargs['__files__']
                    # 3) actually run
                    # 4) if running snippet, pop its files from kwargs['__files__']
                    sect = self._get_section_from_call(comm, 'run')

                    if sect is None:
                        logger.warning('Couldn\'t find section to run: {0}.'.format(comm))
                        continue

                    if self._is_snippet_call(comm, **kwargs):
                        # we're calling a snippet => add files and template_dir to kwargs
                        snippet = yaml_snippet_loader.YamlSnippetLoader.get_snippet_by_name(comm.split('.')[0])

                        if '__files__' not in kwargs:
                            kwargs['__files__'] = []
                            kwargs['__template_dir__'] = []
                        kwargs['__files__'].append(snippet.get_files_section())
                        kwargs['__template_dir__'].append(snippet.get_template_dir())

                    self._run_one_section(sect, copy.deepcopy(kwargs))

                    if self._is_snippet_call(comm, **kwargs):
                        kwargs['__files__'].pop()
                        kwargs['__template_dir__'].pop()
                elif comm_type.startswith('$'):
                    # intentionally pass kwargs as dict, not as keywords
                    try:
                        self._assign_variable(comm_type, comm, kwargs)
                    except exceptions.YamlSyntaxError as e:
                        logger.error(e)
                        raise e
                elif comm_type.startswith('if'):
                    possible_else = None
                    if len(section) > i + 1: # do we have "else" clause?
                        possible_else = list(section[i + 1].items())[0]
                    _, skip_else, to_run = self._get_section_from_condition((comm_type, comm), possible_else, **kwargs)
                    if to_run:
                        # run with original kwargs, so that they might be changed for code after this if
                        self._run_one_section(to_run, kwargs)
                elif comm_type == 'else':
                    if not skip_else:
                        logger.warning('Yaml error: encountered "else" with no associated "if", skipping.')
                    skip_else = False
                elif comm_type.startswith('for'):
                    # syntax: "for $i in $x: <section> or "for $i in cl_command: <section>"
                    try:
                        control_var, expression = self._parse_for(comm_type)
                    except exceptions.YamlSyntaxError as e:
                        logger.error(e)
                        raise e
                    try:
                        eval_expression = evaluate_expression()
                    except exceptions.YamlSyntaxError as e:
                        logger.log(e)
                        raise e

                    for i in eval_expression:
                        kwargs[control_var] = i
                        self._run_one_section(comm, kwargs)
                elif comm_type.startswith('scl'):
                    if '__scls__' not in kwargs:
                        kwargs['__scls__'] = []
                    # list of lists of scl names
                    kwargs['__scls__'].append(comm_type.split()[1:])
                    self._run_one_section(comm, kwargs)
                    kwargs['__scls__'].pop()
                else:
                    files = kwargs['__files__'][-1] if kwargs.get('__files__', None) else self._files
                    template_dir = kwargs['__template_dir__'][-1] if kwargs.get('__template_dir__', None) else self.template_dir
                    run_command(comm_type, CommandFormatter.format(comm_type, comm, template_dir, files, **kwargs), **kwargs)
Exemple #19
0
    def run_command(cls,
                    cmd_str,
                    log_level=logging.DEBUG,
                    scls=[],
                    ignore_sigint=False,
                    output_callback=None):
        """Runs a command from string, e.g. "cp foo bar"
        Args:
            cmd_str: the command to run as string
            log_level: level at which to log command output (DEBUG by default)
            scls: list of ['enable', 'foo', 'bar'] (scriptlet name + arbitrary number of scl names)
            ignore_sigint: should we ignore sigint during this command (False by default)
            output_callback: function that gets called with every line of output as argument
        """
        # format for scl execution if needed
        cmd_str = cls.format_for_scls(cmd_str, scls)
        logger.log(log_level, cmd_str, extra={'event_type': 'cmd_call'})

        if cmd_str.startswith('cd '):
            # special-case cd to behave like shell cd and stay in the directory
            try:
                # delete any qoutes, the quoting is automatical in os.chdir
                directory = cmd_str.split()[1].replace('"',
                                                       '').replace('\'', '')
                os.chdir(directory)
            except OSError as e:
                raise exceptions.ClException(cmd_str, 1, six.text_type(e))
            return ''

        stdin_pipe = None
        stdout_pipe = subprocess.PIPE
        stderr_pipe = subprocess.STDOUT
        preexec_fn = cls.ignore_sigint if ignore_sigint else None
        proc = subprocess.Popen(cmd_str,
                                stdin=stdin_pipe,
                                stdout=stdout_pipe,
                                stderr=stderr_pipe,
                                shell=True,
                                preexec_fn=preexec_fn)
        stdout = []
        while proc.poll() is None:
            output = proc.stdout.readline().decode('utf8')
            if output:
                output = output.strip()
                stdout.append(output)
                logger.log(log_level, output, extra={'event_type': 'cmd_out'})
            if output_callback:
                output_callback(output)
        # add a newline to the end - if there is more output in output_rest, we'll be appending
        # it line by line; if there's no more output, we strip anyway
        stdout = '\n'.join(stdout) + '\n'
        # there may be some remains not read after exiting the previous loop
        output_rest = proc.stdout.read().strip().decode('utf8')
        # we want to log lines separately, not as one big chunk
        output_rest_lines = output_rest.splitlines()
        for i, l in enumerate(output_rest_lines):
            logger.log(log_level, l, extra={'event_type': 'cmd_out'})
            # add newline for every line - for last line, only add it if it was originally present
            if i != len(output_rest_lines) - 1 or output_rest.endswith('\n'):
                l += '\n'
            stdout += l
            if output_callback:
                output_callback(l)

        # log return code always on debug level
        logger.log(logging.DEBUG,
                   proc.returncode,
                   extra={'event_type': 'cmd_retcode'})
        stdout = stdout.strip()

        if proc.returncode == 0:
            return stdout
        else:
            raise exceptions.ClException(cmd_str, proc.returncode, stdout)
    def run_command(cls,
                    cmd_str,
                    log_level=logging.DEBUG,
                    scls=[],
                    ignore_sigint=False,
                    output_callback=None):
        """Runs a command from string, e.g. "cp foo bar"
        Args:
            cmd_str: the command to run as string
            log_level: level at which to log command output (DEBUG by default)
            scls: list of ['enable', 'foo', 'bar'] (scriptlet name + arbitrary number of scl names)
            ignore_sigint: should we ignore sigint during this command (False by default)
            output_callback: function that gets called with every line of output as argument
        """
        # format for scl execution if needed
        cmd_str = cls.format_for_scls(cmd_str, scls)
        logger.log(log_level, cmd_str, extra={'event_type': 'cmd_call'})

        if cmd_str.startswith('cd '):
            # special-case cd to behave like shell cd and stay in the directory
            try:
                # delete any qoutes, the quoting is automatical in os.chdir
                directory = cmd_str.split()[1].replace('"', '').replace('\'', '')
                os.chdir(directory)
            except OSError as e:
                raise exceptions.ClException(cmd_str, 1, six.text_type(e))
            return ''

        stdin_pipe = None
        stdout_pipe = subprocess.PIPE
        stderr_pipe = subprocess.STDOUT
        preexec_fn = cls.ignore_sigint if ignore_sigint else None
        proc = subprocess.Popen(cmd_str,
                                stdin=stdin_pipe,
                                stdout=stdout_pipe,
                                stderr=stderr_pipe,
                                shell=True,
                                preexec_fn=preexec_fn)
        stdout = []
        while proc.poll() is None:
            output = proc.stdout.readline().decode('utf8')
            if output:
                output = output.strip()
                stdout.append(output)
                logger.log(log_level, output, extra={'event_type': 'cmd_out'})
            if output_callback:
                output_callback(output)
        # add a newline to the end - if there is more output in output_rest, we'll be appending
        # it line by line; if there's no more output, we strip anyway
        stdout = '\n'.join(stdout) + '\n'
        # there may be some remains not read after exiting the previous loop
        output_rest = proc.stdout.read().strip().decode('utf8')
        # we want to log lines separately, not as one big chunk
        output_rest_lines = output_rest.splitlines()
        for i, l in enumerate(output_rest_lines):
            logger.log(log_level, l, extra={'event_type': 'cmd_out'})
            # add newline for every line - for last line, only add it if it was originally present
            if i != len(output_rest_lines) - 1 or output_rest.endswith('\n'):
                l += '\n'
            stdout += l
            if output_callback:
                output_callback(l)

        # log return code always on debug level
        logger.log(logging.DEBUG, proc.returncode, extra={'event_type': 'cmd_retcode'})
        stdout = stdout.strip()

        if proc.returncode == 0:
            return stdout
        else:
            raise exceptions.ClException(cmd_str,
                                         proc.returncode,
                                         stdout)