Skip to content

Executors

Runner

Bases: Base

Source code in atomic_operator/execution/runner.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class Runner(Base):

    def clean_output(self, data):
        """Decodes data and strips CLI garbage from returned outputs and errors

        Args:
            data (str): A output or error returned from subprocess

        Returns:
            str: A cleaned string which will be displayed on the console and in logs
        """
        if data:
            # Remove Windows CLI garbage
            data = re.sub(r"Microsoft\ Windows\ \[version .+\]\r?\nCopyright.*(\r?\n)+[A-Z]\:.+?\>", "", data.decode("utf-8", "ignore"))
            # formats strings with newline and return characters
            return re.sub(r"(\r?\n)*[A-Z]\:.+?\>", "", data)

    def print_process_output(self, command, return_code, output, errors):
        """Outputs the appropriate outputs if they exists to the console and log files

        Args:
            command (str): The command which was ran by subprocess
            return_code (int): The return code from subprocess
            output (bytes): Output from subprocess which is typically in bytes
            errors (bytes): Errors from subprocess which is typically in bytes
        """
        return_dict = {}
        if return_code == 127:
            return_dict['error'] =  f"\n\nCommand Not Found: {command} returned exit code {return_code}: \nErrors: {self.clean_output(errors)}/nOutput: {output}"
            self.__logger.warning(return_dict['error'])
            return return_dict
        if output or errors:
            if output:
                return_dict['output'] = self.clean_output(output)
                self.__logger.info("\n\nOutput: {}".format(return_dict['output']))
            else:
                return_dict['error'] =  f"\n\nCommand: {command} returned exit code {return_code}: \n{self.clean_output(errors)}"
                self.__logger.warning(return_dict['error'])
        else:
            self.__logger.info("(No output)")
        return return_dict

    def _run_dependencies(self, host=None, executor=None):
        """Checking dependencies
        """
        return_dict = {}
        if self.test.dependency_executor_name:
            executor = self.test.dependency_executor_name
        for dependency in self.test.dependencies:
            self.__logger.debug(f"Dependency description: {dependency.description}")
            if Base.CONFIG.check_prereqs and dependency.prereq_command:
                self.__logger.debug("Running prerequisite command")
                response = self.execute_process(
                    command=dependency.prereq_command,
                    executor=executor,
                    host=host
                )
                for key,val in response.items():
                    if key not in return_dict:
                        return_dict[key] = {}
                    return_dict[key].update({'prereq_command': val})
                if return_dict.get('error'):
                    return return_dict
            if Base.CONFIG.get_prereqs and dependency.get_prereq_command:
                self.__logger.debug(f"Retrieving prerequistes")
                get_prereq_response = self.execute_process(
                    command=dependency.get_prereq_command,
                    executor=executor,
                    host=host
                )
                for key,val in get_prereq_response.items():
                    if key not in return_dict:
                        return_dict[key] = {}
                    return_dict[key].update({'get_prereqs': val})
        return return_dict

    def execute(self, host_name='localhost', executor=None, host=None):
        """The main method which runs a single AtomicTest object on a local system.
        """
        return_dict = {}
        self.__logger.debug(f"Using {executor} as executor.")
        if executor:
            if not Base.CONFIG.check_prereqs and not Base.CONFIG.get_prereqs and not Base.CONFIG.cleanup:
                self.__logger.debug("Running command")
                response = self.execute_process(
                    command=self.test.executor.command,
                    executor=executor,
                    host=host,
                    cwd=self.test_path,
                    elevation_required=self.test.executor.elevation_required
                )
                return_dict.update({'command': response})
            elif Base.CONFIG.check_prereqs or Base.CONFIG.get_prereqs:
                if self.test.dependencies:
                    return_dict.update(self._run_dependencies(host=host, executor=executor))
            elif Runner.CONFIG.cleanup and self.test.executor.cleanup_command:
                self.__logger.debug("Running cleanup command")
                cleanup_response = self.execute_process(
                    command=self.test.executor.cleanup_command,
                    executor=executor,
                    host=host,
                    cwd=self.test_path
                )
                return_dict.update({'cleanup': cleanup_response})
        return {host_name: return_dict}

    @abc.abstractmethod
    def start(self):
        raise NotImplementedError

    @abc.abstractmethod
    def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
        raise NotImplementedError

clean_output(data)

Decodes data and strips CLI garbage from returned outputs and errors

Parameters:

Name Type Description Default
data str

A output or error returned from subprocess

required

Returns:

Name Type Description
str

A cleaned string which will be displayed on the console and in logs

Source code in atomic_operator/execution/runner.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def clean_output(self, data):
    """Decodes data and strips CLI garbage from returned outputs and errors

    Args:
        data (str): A output or error returned from subprocess

    Returns:
        str: A cleaned string which will be displayed on the console and in logs
    """
    if data:
        # Remove Windows CLI garbage
        data = re.sub(r"Microsoft\ Windows\ \[version .+\]\r?\nCopyright.*(\r?\n)+[A-Z]\:.+?\>", "", data.decode("utf-8", "ignore"))
        # formats strings with newline and return characters
        return re.sub(r"(\r?\n)*[A-Z]\:.+?\>", "", data)

execute(host_name='localhost', executor=None, host=None)

The main method which runs a single AtomicTest object on a local system.

Source code in atomic_operator/execution/runner.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def execute(self, host_name='localhost', executor=None, host=None):
    """The main method which runs a single AtomicTest object on a local system.
    """
    return_dict = {}
    self.__logger.debug(f"Using {executor} as executor.")
    if executor:
        if not Base.CONFIG.check_prereqs and not Base.CONFIG.get_prereqs and not Base.CONFIG.cleanup:
            self.__logger.debug("Running command")
            response = self.execute_process(
                command=self.test.executor.command,
                executor=executor,
                host=host,
                cwd=self.test_path,
                elevation_required=self.test.executor.elevation_required
            )
            return_dict.update({'command': response})
        elif Base.CONFIG.check_prereqs or Base.CONFIG.get_prereqs:
            if self.test.dependencies:
                return_dict.update(self._run_dependencies(host=host, executor=executor))
        elif Runner.CONFIG.cleanup and self.test.executor.cleanup_command:
            self.__logger.debug("Running cleanup command")
            cleanup_response = self.execute_process(
                command=self.test.executor.cleanup_command,
                executor=executor,
                host=host,
                cwd=self.test_path
            )
            return_dict.update({'cleanup': cleanup_response})
    return {host_name: return_dict}

print_process_output(command, return_code, output, errors)

Outputs the appropriate outputs if they exists to the console and log files

Parameters:

Name Type Description Default
command str

The command which was ran by subprocess

required
return_code int

The return code from subprocess

required
output bytes

Output from subprocess which is typically in bytes

required
errors bytes

Errors from subprocess which is typically in bytes

required
Source code in atomic_operator/execution/runner.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def print_process_output(self, command, return_code, output, errors):
    """Outputs the appropriate outputs if they exists to the console and log files

    Args:
        command (str): The command which was ran by subprocess
        return_code (int): The return code from subprocess
        output (bytes): Output from subprocess which is typically in bytes
        errors (bytes): Errors from subprocess which is typically in bytes
    """
    return_dict = {}
    if return_code == 127:
        return_dict['error'] =  f"\n\nCommand Not Found: {command} returned exit code {return_code}: \nErrors: {self.clean_output(errors)}/nOutput: {output}"
        self.__logger.warning(return_dict['error'])
        return return_dict
    if output or errors:
        if output:
            return_dict['output'] = self.clean_output(output)
            self.__logger.info("\n\nOutput: {}".format(return_dict['output']))
        else:
            return_dict['error'] =  f"\n\nCommand: {command} returned exit code {return_code}: \n{self.clean_output(errors)}"
            self.__logger.warning(return_dict['error'])
    else:
        self.__logger.info("(No output)")
    return return_dict

LocalRunner

Bases: Runner

Runs AtomicTest objects locally

Source code in atomic_operator/execution/localrunner.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class LocalRunner(Runner):
    """Runs AtomicTest objects locally
    """

    def __init__(self, atomic_test, test_path):
        """A single AtomicTest object is provided and ran on the local system

        Args:
            atomic_test (AtomicTest): A single AtomicTest object.
            test_path (Atomic): A path where the AtomicTest object resides
        """
        self.test = atomic_test
        self.test_path = test_path
        self.__local_system_platform = self.get_local_system_platform()

    def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
        """Executes commands using subprocess

        Args:
            executor (str): An executor or shell used to execute the provided command(s)
            command (str): The commands to run using subprocess
            cwd (str): A string which indicates the current working directory to run the command
            elevation_required (bool): Whether or not elevation is required

        Returns:
            tuple: A tuple of either outputs or errors from subprocess
        """
        if elevation_required:
            if executor in ['powershell']:
                command = f"Start-Process PowerShell -Verb RunAs; {command}"
            elif executor in ['cmd', 'command_prompt']:
                command = f'cmd.exe /c "{command}"'
            elif executor in ['sh', 'bash', 'ssh']:
                command = f"sudo {command}"
            else:
                self.__logger.warning(f"Elevation is required but the executor '{executor}' is unknown!")
        command = self._replace_command_string(command, self.CONFIG.atomics_path, input_arguments=self.test.input_arguments, executor=executor)
        executor = self.command_map.get(executor).get(self.__local_system_platform)
        p = subprocess.Popen(
            executor, 
            shell=False, 
            stdin=subprocess.PIPE, 
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT, 
            env=os.environ, 
            cwd=cwd
        )
        try:
            outs, errs = p.communicate(
                bytes(command, "utf-8") + b"\n", 
                timeout=Runner.CONFIG.command_timeout
            )
            response = self.print_process_output(command, p.returncode, outs, errs)
            return response
        except subprocess.TimeoutExpired as e:
            # Display output if it exists.
            if e.output:
                self.__logger.warning(e.output)
            if e.stdout:
                self.__logger.warning(e.stdout)
            if e.stderr:
                self.__logger.warning(e.stderr)
            self.__logger.warning("Command timed out!")

            # Kill the process.
            p.kill()
            return {}

    def _get_executor_command(self):
        """Checking if executor works with local system platform
        """
        __executor = None
        self.__logger.debug(f"Checking if executor works on local system platform.")
        if self.__local_system_platform in self.test.supported_platforms:
            if self.test.executor.name != 'manual':
                __executor = self.command_map.get(self.test.executor.name).get(self.__local_system_platform)
        return __executor

    def start(self):
        return self.execute(executor=self.test.executor.name)

__init__(atomic_test, test_path)

A single AtomicTest object is provided and ran on the local system

Parameters:

Name Type Description Default
atomic_test AtomicTest

A single AtomicTest object.

required
test_path Atomic

A path where the AtomicTest object resides

required
Source code in atomic_operator/execution/localrunner.py
10
11
12
13
14
15
16
17
18
19
def __init__(self, atomic_test, test_path):
    """A single AtomicTest object is provided and ran on the local system

    Args:
        atomic_test (AtomicTest): A single AtomicTest object.
        test_path (Atomic): A path where the AtomicTest object resides
    """
    self.test = atomic_test
    self.test_path = test_path
    self.__local_system_platform = self.get_local_system_platform()

execute_process(command, executor=None, host=None, cwd=None, elevation_required=False)

Executes commands using subprocess

Parameters:

Name Type Description Default
executor str

An executor or shell used to execute the provided command(s)

None
command str

The commands to run using subprocess

required
cwd str

A string which indicates the current working directory to run the command

None
elevation_required bool

Whether or not elevation is required

False

Returns:

Name Type Description
tuple

A tuple of either outputs or errors from subprocess

Source code in atomic_operator/execution/localrunner.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
    """Executes commands using subprocess

    Args:
        executor (str): An executor or shell used to execute the provided command(s)
        command (str): The commands to run using subprocess
        cwd (str): A string which indicates the current working directory to run the command
        elevation_required (bool): Whether or not elevation is required

    Returns:
        tuple: A tuple of either outputs or errors from subprocess
    """
    if elevation_required:
        if executor in ['powershell']:
            command = f"Start-Process PowerShell -Verb RunAs; {command}"
        elif executor in ['cmd', 'command_prompt']:
            command = f'cmd.exe /c "{command}"'
        elif executor in ['sh', 'bash', 'ssh']:
            command = f"sudo {command}"
        else:
            self.__logger.warning(f"Elevation is required but the executor '{executor}' is unknown!")
    command = self._replace_command_string(command, self.CONFIG.atomics_path, input_arguments=self.test.input_arguments, executor=executor)
    executor = self.command_map.get(executor).get(self.__local_system_platform)
    p = subprocess.Popen(
        executor, 
        shell=False, 
        stdin=subprocess.PIPE, 
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT, 
        env=os.environ, 
        cwd=cwd
    )
    try:
        outs, errs = p.communicate(
            bytes(command, "utf-8") + b"\n", 
            timeout=Runner.CONFIG.command_timeout
        )
        response = self.print_process_output(command, p.returncode, outs, errs)
        return response
    except subprocess.TimeoutExpired as e:
        # Display output if it exists.
        if e.output:
            self.__logger.warning(e.output)
        if e.stdout:
            self.__logger.warning(e.stdout)
        if e.stderr:
            self.__logger.warning(e.stderr)
        self.__logger.warning("Command timed out!")

        # Kill the process.
        p.kill()
        return {}

RemoteRunner

Bases: Runner

Source code in atomic_operator/execution/remoterunner.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
class RemoteRunner(Runner):

    def __init__(self, atomic_test, test_path):
        """A single AtomicTest object is provided and ran on the local system

        Args:
            atomic_test (AtomicTest): A single AtomicTest object.
            test_path (Atomic): A path where the AtomicTest object resides
        """
        self.test = atomic_test
        self.test_path = test_path

    def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
        """Main method to execute commands using state machine

        Args:
            command (str): The command to run remotely on the desired systems
            executor (str): An executor that can be passed to state machine. Defaults to None.
            host (str): A host to run remote commands on. Defaults to None.
        """
        self.state = CreationState()
        final_state = None
        try:
            finished = False
            while not finished:
                if str(self.state) == 'CreationState':
                    self.__logger.debug('Running CreationState on_event')
                    self.state = self.state.on_event(executor, command)
                if str(self.state) == 'InnvocationState':
                    self.__logger.debug('Running InnvocationState on_event')
                    self.state = self.state.invoke(host, executor, command, input_arguments=self.test.input_arguments, elevation_required=elevation_required)
                if str(self.state) == 'ParseResultsState':
                    self.__logger.debug('Running ParseResultsState on_event')
                    final_state = self.state.on_event()
                    self.__logger.info(final_state)
                    finished = True
        except NoValidConnectionsError as ec:
            error_string = f'SSH Error - Unable to connect to {host.hostname} - Received {type(ec).__name__}'
            self.__logger.debug(f'Full stack trace: {ec}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except AuthenticationException as ea:
            error_string = f'SSH Error - Unable to authenticate to host - {host.hostname} - Received {type(ea).__name__}'
            self.__logger.debug(f'Full stack trace: {ea}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except BadAuthenticationType as eb:
            error_string = f'SSH Error - Unable to use provided authentication type to host - {host.hostname} - Received {type(eb).__name__}'
            self.__logger.debug(f'Full stack trace: {eb}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except PasswordRequiredException as ep:
            error_string = f'SSH Error - Must provide a password to authenticate to host - {host.hostname} - Received {type(ep).__name__}'
            self.__logger.debug(f'Full stack trace: {ep}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except AuthenticationError as ewa:
            error_string = f'Windows Error - Unable to authenticate to host - {host.hostname} - Received {type(ewa).__name__}'
            self.__logger.debug(f'Full stack trace: {ewa}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except WinRMTransportError as ewt:
            error_string = f'Windows Error - Error occurred during transport on host - {host.hostname} - Received {type(ewt).__name__}'
            self.__logger.debug(f'Full stack trace: {ewt}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except WSManFaultError as ewf:
            error_string = f'Windows Error - Received WSManFault information from host - {host.hostname} - Received {type(ewf).__name__}'
            self.__logger.debug(f'Full stack trace: {ewf}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except RequestException as re:
            error_string = f'Request Exception - Connection Error to the configured host - {host.hostname} - Received {type(re).__name__}'
            self.__logger.debug(f'Full stack trace: {re}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        except Exception as ex:
            error_string = f'Uknown Error - Received an unknown error from host - {host.hostname} - Received {type(ex).__name__}'
            self.__logger.debug(f'Full stack trace: {ex}')
            self.__logger.warning(error_string)
            return {'error': error_string}
        return final_state

    def start(self, host=None, executor=None):
        """The main method which runs a single AtomicTest object remotely on one remote host.
        """
        return self.execute(host_name=host.hostname, executor=executor, host=host)

__init__(atomic_test, test_path)

A single AtomicTest object is provided and ran on the local system

Parameters:

Name Type Description Default
atomic_test AtomicTest

A single AtomicTest object.

required
test_path Atomic

A path where the AtomicTest object resides

required
Source code in atomic_operator/execution/remoterunner.py
19
20
21
22
23
24
25
26
27
def __init__(self, atomic_test, test_path):
    """A single AtomicTest object is provided and ran on the local system

    Args:
        atomic_test (AtomicTest): A single AtomicTest object.
        test_path (Atomic): A path where the AtomicTest object resides
    """
    self.test = atomic_test
    self.test_path = test_path

execute_process(command, executor=None, host=None, cwd=None, elevation_required=False)

Main method to execute commands using state machine

Parameters:

Name Type Description Default
command str

The command to run remotely on the desired systems

required
executor str

An executor that can be passed to state machine. Defaults to None.

None
host str

A host to run remote commands on. Defaults to None.

None
Source code in atomic_operator/execution/remoterunner.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
    """Main method to execute commands using state machine

    Args:
        command (str): The command to run remotely on the desired systems
        executor (str): An executor that can be passed to state machine. Defaults to None.
        host (str): A host to run remote commands on. Defaults to None.
    """
    self.state = CreationState()
    final_state = None
    try:
        finished = False
        while not finished:
            if str(self.state) == 'CreationState':
                self.__logger.debug('Running CreationState on_event')
                self.state = self.state.on_event(executor, command)
            if str(self.state) == 'InnvocationState':
                self.__logger.debug('Running InnvocationState on_event')
                self.state = self.state.invoke(host, executor, command, input_arguments=self.test.input_arguments, elevation_required=elevation_required)
            if str(self.state) == 'ParseResultsState':
                self.__logger.debug('Running ParseResultsState on_event')
                final_state = self.state.on_event()
                self.__logger.info(final_state)
                finished = True
    except NoValidConnectionsError as ec:
        error_string = f'SSH Error - Unable to connect to {host.hostname} - Received {type(ec).__name__}'
        self.__logger.debug(f'Full stack trace: {ec}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except AuthenticationException as ea:
        error_string = f'SSH Error - Unable to authenticate to host - {host.hostname} - Received {type(ea).__name__}'
        self.__logger.debug(f'Full stack trace: {ea}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except BadAuthenticationType as eb:
        error_string = f'SSH Error - Unable to use provided authentication type to host - {host.hostname} - Received {type(eb).__name__}'
        self.__logger.debug(f'Full stack trace: {eb}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except PasswordRequiredException as ep:
        error_string = f'SSH Error - Must provide a password to authenticate to host - {host.hostname} - Received {type(ep).__name__}'
        self.__logger.debug(f'Full stack trace: {ep}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except AuthenticationError as ewa:
        error_string = f'Windows Error - Unable to authenticate to host - {host.hostname} - Received {type(ewa).__name__}'
        self.__logger.debug(f'Full stack trace: {ewa}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except WinRMTransportError as ewt:
        error_string = f'Windows Error - Error occurred during transport on host - {host.hostname} - Received {type(ewt).__name__}'
        self.__logger.debug(f'Full stack trace: {ewt}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except WSManFaultError as ewf:
        error_string = f'Windows Error - Received WSManFault information from host - {host.hostname} - Received {type(ewf).__name__}'
        self.__logger.debug(f'Full stack trace: {ewf}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except RequestException as re:
        error_string = f'Request Exception - Connection Error to the configured host - {host.hostname} - Received {type(re).__name__}'
        self.__logger.debug(f'Full stack trace: {re}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    except Exception as ex:
        error_string = f'Uknown Error - Received an unknown error from host - {host.hostname} - Received {type(ex).__name__}'
        self.__logger.debug(f'Full stack trace: {ex}')
        self.__logger.warning(error_string)
        return {'error': error_string}
    return final_state

start(host=None, executor=None)

The main method which runs a single AtomicTest object remotely on one remote host.

Source code in atomic_operator/execution/remoterunner.py
100
101
102
103
def start(self, host=None, executor=None):
    """The main method which runs a single AtomicTest object remotely on one remote host.
    """
    return self.execute(host_name=host.hostname, executor=executor, host=host)

AWSRunner

Bases: ExecutionBase

Runs AtomicTest objects against AWS using the aws-cli

Source code in atomic_operator/execution/awsrunner.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class AWSRunner(ExecutionBase):
    """Runs AtomicTest objects against AWS using the aws-cli
    """

    def __init__(self, atomic_test, test_path):
        """A single AtomicTest object is provided and ran using the aws-cli

        Args:
            atomic_test (AtomicTest): A single AtomicTest object.
            test_path (Atomic): A path where the AtomicTest object resides
        """
        self.test = atomic_test
        self.test_path = test_path
        self.__local_system_platform = self.get_local_system_platform()

    def __check_for_aws_cli(self):
        self.__logger.debug('Checking to see if aws cli is installed.')
        response = self.execute_process(command='aws --version', executor=self._get_executor_command(), cwd=os.getcwd())
        if response and response.get('error'):
            self.__logger.warning(response['error'])
        return response

    def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
        """Executes commands using subprocess

        Args:
            executor (str): An executor or shell used to execute the provided command(s)
            command (str): The commands to run using subprocess
            cwd (str): A string which indicates the current working directory to run the command
            elevation_required (bool): Whether or not elevation is required

        Returns:
            tuple: A tuple of either outputs or errors from subprocess
        """
        if elevation_required:
            if executor in ['powershell']:
                command = f"Start-Process PowerShell -Verb RunAs; {command}"
            elif executor in ['cmd', 'command_prompt']:
                command = f'{self.command_map.get(executor).get(self.__local_system_platform)} /c "{command}"'
            elif executor in ['sh', 'bash', 'ssh']:
                command = f"sudo {command}"
            else:
                self.__logger.warning(f"Elevation is required but the executor '{executor}' is unknown!")
        command = self._replace_command_string(command, self.CONFIG.atomics_path, input_arguments=self.test.input_arguments, executor=executor)
        executor = self.command_map.get(executor).get(self.__local_system_platform)
        p = subprocess.Popen(
            executor, 
            shell=False, 
            stdin=subprocess.PIPE, 
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT, 
            env=os.environ, 
            cwd=cwd
        )
        try:
            outs, errs = p.communicate(
                bytes(command, "utf-8") + b"\n", 
                timeout=Runner.CONFIG.command_timeout
            )
            response = self.print_process_output(command, p.returncode, outs, errs)
            return response
        except subprocess.TimeoutExpired as e:
            # Display output if it exists.
            if e.output:
                self.__logger.warning(e.output)
            if e.stdout:
                self.__logger.warning(e.stdout)
            if e.stderr:
                self.__logger.warning(e.stderr)
            self.__logger.warning("Command timed out!")
            # Kill the process.
            p.kill()
            return {}

    def _get_executor_command(self):
        """Checking if executor works with local system platform
        """
        __executor = None
        self.__logger.debug(f"Checking if executor works on local system platform.")
        if 'iaas:aws' in self.test.supported_platforms:
            if self.test.executor.name != 'manual':
                __executor = self.command_map.get(self.test.executor.name).get(self.__local_system_platform)
        return __executor

    def start(self):
        response = self.__check_for_aws_cli()
        if not response.get('error'):
            return self.execute(executor=self.test.executor.name)
        return response

__init__(atomic_test, test_path)

A single AtomicTest object is provided and ran using the aws-cli

Parameters:

Name Type Description Default
atomic_test AtomicTest

A single AtomicTest object.

required
test_path Atomic

A path where the AtomicTest object resides

required
Source code in atomic_operator/execution/awsrunner.py
10
11
12
13
14
15
16
17
18
19
def __init__(self, atomic_test, test_path):
    """A single AtomicTest object is provided and ran using the aws-cli

    Args:
        atomic_test (AtomicTest): A single AtomicTest object.
        test_path (Atomic): A path where the AtomicTest object resides
    """
    self.test = atomic_test
    self.test_path = test_path
    self.__local_system_platform = self.get_local_system_platform()

execute_process(command, executor=None, host=None, cwd=None, elevation_required=False)

Executes commands using subprocess

Parameters:

Name Type Description Default
executor str

An executor or shell used to execute the provided command(s)

None
command str

The commands to run using subprocess

required
cwd str

A string which indicates the current working directory to run the command

None
elevation_required bool

Whether or not elevation is required

False

Returns:

Name Type Description
tuple

A tuple of either outputs or errors from subprocess

Source code in atomic_operator/execution/awsrunner.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def execute_process(self, command, executor=None, host=None, cwd=None, elevation_required=False):
    """Executes commands using subprocess

    Args:
        executor (str): An executor or shell used to execute the provided command(s)
        command (str): The commands to run using subprocess
        cwd (str): A string which indicates the current working directory to run the command
        elevation_required (bool): Whether or not elevation is required

    Returns:
        tuple: A tuple of either outputs or errors from subprocess
    """
    if elevation_required:
        if executor in ['powershell']:
            command = f"Start-Process PowerShell -Verb RunAs; {command}"
        elif executor in ['cmd', 'command_prompt']:
            command = f'{self.command_map.get(executor).get(self.__local_system_platform)} /c "{command}"'
        elif executor in ['sh', 'bash', 'ssh']:
            command = f"sudo {command}"
        else:
            self.__logger.warning(f"Elevation is required but the executor '{executor}' is unknown!")
    command = self._replace_command_string(command, self.CONFIG.atomics_path, input_arguments=self.test.input_arguments, executor=executor)
    executor = self.command_map.get(executor).get(self.__local_system_platform)
    p = subprocess.Popen(
        executor, 
        shell=False, 
        stdin=subprocess.PIPE, 
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT, 
        env=os.environ, 
        cwd=cwd
    )
    try:
        outs, errs = p.communicate(
            bytes(command, "utf-8") + b"\n", 
            timeout=Runner.CONFIG.command_timeout
        )
        response = self.print_process_output(command, p.returncode, outs, errs)
        return response
    except subprocess.TimeoutExpired as e:
        # Display output if it exists.
        if e.output:
            self.__logger.warning(e.output)
        if e.stdout:
            self.__logger.warning(e.stdout)
        if e.stderr:
            self.__logger.warning(e.stderr)
        self.__logger.warning("Command timed out!")
        # Kill the process.
        p.kill()
        return {}

CreationState

Bases: State

The state which is used to modify commands

Source code in atomic_operator/execution/statemachine.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class CreationState(State):
    """
    The state which is used to modify commands
    """

    def powershell(self, event):
        command = None
        if event:
            if '\n' in event or '\r' in event:
                if '\n' in event:
                    command = event.replace('\n', '; ')
                if '\r' in event:
                    if command:
                        command = command.replace('\r', '; ')
                    else:
                        command = event.replace('\r', '; ')
            return InnvocationState()

    def cmd(self):
        return InnvocationState()

    def ssh(self):
        return InnvocationState()

    def on_event(self, command_type, command):
        if command_type == 'powershell':
            return self.powershell(command)
        elif command_type == 'cmd':
            return self.cmd()
        elif command_type == 'ssh':
            return self.ssh()
        elif command_type == 'sh':
            return self.ssh()
        elif command_type == 'bash':
            return self.ssh()
        return self

InnvocationState

Bases: State, Base

The state which indicates the invocation of a command

Source code in atomic_operator/execution/statemachine.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
class InnvocationState(State, Base):
    """
    The state which indicates the invocation of a command
    """

    __win_client = None

    def __handle_windows_errors(self, stream):
        return_list = []
        for item in stream.error:
            return_list.append({
                'type': 'error',
                'value': str(item)
            })
        for item in stream.debug:
            return_list.append({
                'type': 'debug',
                'value': str(item)
            })
        for item in stream.information:
            return_list.append({
                'type': 'information',
                'value': str(item)
            })
        for item in stream.verbose:
            return_list.append({
                'type': 'verbose',
                'value': str(item)
            })
        for item in stream.warning:
            return_list.append({
                'type': 'warning',
                'value': str(item)
            })
        return return_list

    def __create_win_client(self, hostinfo):
        self.__win_client = Client(
            hostinfo.hostname,
            username=hostinfo.username,
            password=hostinfo.password,
            ssl=hostinfo.verify_ssl
        )

    def __invoke_cmd(self, command, input_arguments=None, elevation_required=False):
        if not self.__win_client:
            self.__create_win_client(self.hostinfo)
        # TODO: NEED TO ADD LOGIC TO TRANSFER FILES TO WINDOWS SYSTEMS USING CMD
        Copier(windows_client=self.__win_client, elevation_required=elevation_required).copy(input_arguments)
        command = self._replace_command_string(command, path='c:/temp', input_arguments=input_arguments, executor='command_prompt')
        if elevation_required:
            command = f'runas /user:{self.hostinfo.username}:{self.hostinfo.password} cmd.exe; {command}'
        # TODO: NEED TO ADD LOGIC TO TRANSFER FILES TO WINDOWS SYSTEMS USING CMD
        stdout, stderr, rc = self.__win_client.execute_cmd(command)
        # NOTE: rc (return code of process) should equal 0 but we are not adding logic here this is handled int he ParseResultsState class
        if stderr:
            self.__logger.error('{host} responded with the following message(s): {message}'.format(
                host=self.hostinfo.hostname,
                message=stderr
            ))
        return ParseResultsState(
            command=command,
            return_code=rc,
            output=stdout,
            error=stderr
        )

    def join_path_regardless_of_separators(self, *paths):
        return os.path.sep.join(path.rstrip(r"\/") for path in paths)

    def __invoke_powershell(self, command, input_arguments=None, elevation_required=False):
        if not self.__win_client:
            self.__create_win_client(self.hostinfo)

        # TODO: NEED TO ADD LOGIC TO TRANSFER FILES TO WINDOWS SYSTEMS USING POWERSHELL
        Copier(windows_client=self.__win_client, elevation_required=elevation_required).copy(input_arguments=input_arguments)
        command = self._replace_command_string(command, path='c:/temp', input_arguments=input_arguments, executor='powershell')
        # TODO: NEED TO ADD LOGIC TO TRANSFER FILES TO WINDOWS SYSTEMS USING POWERSHELL
        if elevation_required:
            command = f'Start-Process PowerShell -Verb RunAs; {command}'
        output, streams, had_errors = self.__win_client.execute_ps(command)
        if not output:
            output = self.__handle_windows_errors(streams)
        if had_errors:
            self.__logger.error('{host} responded with the following message(s): {message}'.format(
                host=self.hostinfo.hostname,
                message=self.__handle_windows_errors(streams)
            ))
        return ParseResultsState(
            command=command, 
            return_code=had_errors, 
            output=output, 
            error=self.__handle_windows_errors(streams)
        )

    def __invoke_ssh(self, command, input_arguments=None, elevation_required=False):
        import paramiko
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        if self.hostinfo.ssh_key_path:
            ssh.connect(
                self.hostinfo.hostname,
                port=self.hostinfo.port,
                username=self.hostinfo.username,
                key_filename=self.hostinfo.ssh_key_path
            )
        elif self.hostinfo.private_key_string:
            ssh.connect(
                self.hostinfo.hostname,
                port=self.hostinfo.port,
                username=self.hostinfo.username,
                pkey=self.hostinfo.private_key_string
            )
        elif self.hostinfo.password:
            ssh.connect(
                self.hostinfo.hostname,
                port=self.hostinfo.port,
                username=self.hostinfo.username,
                password=self.hostinfo.password,
                timeout=self.hostinfo.timeout
            )
        else:
            raise AttributeError('Please provide either a ssh_key_path or a password')
        out = None
        from .base import Base
        base = Base()

        Copier(ssh_client=ssh, elevation_required=elevation_required).copy(input_arguments=input_arguments)

        command = base._replace_command_string(command=command, path='/tmp', input_arguments=input_arguments)
        if elevation_required:
            command = f'sudo {command}'
        ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command(command)
        return_code = ssh_stdout.channel.recv_exit_status()
        out = ssh_stdout.read()
        err = ssh_stderr.read()
        ssh_stdin.flush()
        ssh.close()
        return ParseResultsState(
            command=command, 
            return_code=return_code,
            output=out, 
            error=err
        )

    def invoke(self, hostinfo, command_type, command, input_arguments=None, elevation_required=False):
        self.hostinfo = hostinfo
        command_type = self.get_remote_executor(command_type)
        result = None
        if command_type == 'powershell':
            result = self.__invoke_powershell(command, input_arguments=input_arguments, elevation_required=elevation_required)
        elif command_type == 'cmd':
            result = self.__invoke_cmd(command, input_arguments=input_arguments, elevation_required=elevation_required)
        elif command_type == 'ssh':
            result = self.__invoke_ssh(command, input_arguments=input_arguments, elevation_required=elevation_required)
        return result

ParseResultsState

Bases: State, Runner

The state which is used to parse the results

Source code in atomic_operator/execution/statemachine.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
class ParseResultsState(State, Runner):
    """
    The state which is used to parse the results
    """

    def __init__(self, command=None, return_code=None, output=None, error=None):
        self.result = {}
        self.print_process_output(
                command=command, 
                return_code=return_code, 
                output=output,
                errors=error
            )
        if output:
            self.result.update({'output': self.__parse(output)})
        if error:
            self.result.update({'error': self.__parse(error)})

    def __parse(self, results):
        if isinstance(results, bytes):
            results = results.decode("utf-8").strip()
        return results

    def on_event(self):
        return self.result

State

We define a state object which provides some utility functions for the individual states within the state machine.

Source code in atomic_operator/execution/statemachine.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class State:
    """
    We define a state object which provides some utility functions for the
    individual states within the state machine.
    """

    @classmethod
    def get_remote_executor(cls, executor):
        if executor == 'command_prompt':
            return 'cmd'
        elif executor == 'powershell':
            return 'powershell'
        elif executor == 'sh':
            return 'ssh'
        elif executor == 'bash':
            return 'ssh'
        elif executor == 'manual':
            return None
        else:
            return executor

    def on_event(self, event):
        """
        Handle events that are delegated to this State.
        """
        pass

    def __repr__(self):
        """
        Leverages the __str__ method to describe the State.
        """
        return self.__str__()

    def __str__(self):
        """
        Returns the name of the State.
        """
        return self.__class__.__name__

on_event(event)

Handle events that are delegated to this State.

Source code in atomic_operator/execution/statemachine.py
43
44
45
46
47
def on_event(self, event):
    """
    Handle events that are delegated to this State.
    """
    pass