7. Crosscutting concepts

Separation of Input, Output and Processing

For fotoobo to be easily extendable with additional interfaces the input/output and processing layer need to be strictly separated. This is done by several measures:

  • Passing of input parameters is done by using parameters only. Any input specific parameter preprocessing should be done on the input/output layer.

  • Passing of return values is done by using a common Result class (see below).

  • The processing of the request is done by a so called fotoobo “tool”, inside the tools directory.

Common Result class

For passing output of a fotoobo tool to the interface there is a common Result class found in the fotoobo helpers.

See How To Use The Results Helper for more information about this.

Parallelization

By separating the processing from the intput/output part running a task in parallel against several Fortinet devices becomes easier to do. Please follow some guidelines when doing this:

  • Always do the parallelization inside a tool. The input/output layer should not be concerned about parallelization at all (except for example disabling it on request, if applicable).

  • You should make a nested function inside the tool function that will query one Fortinet instance and return the raw data.

  • Then use ThreadPoolExecutor of the Python concurrent.futures standard library to process and execute the single parts as depicted below.

def my_tools_method(any, parameters, needed) -> Result:
    """
    Example for parallelization, taken from tools.fgt.get.version()

    This example also includes the display of a progress bar on the console using
    rich.progress.Progress().
    """
    def _get_single_version(name: str, fgt: FortiGate):
        """
        Get version for single FortiGate.

        NOTE: Method shortened for legibility
        """
        fortigate_version = fgt.get_version()

        return name, fortigate_version

    result = Result[str]()

    with Progress() as progress:
        task = progress.add_task("getting FortiGate versions...", total=len(fgts))
        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
            futures = []
            # Register all the tasks that need to be done
            for name, fgt in fgts.items():
                futures.append(executor.submit(_get_single_version, name, fgt))

            # Process the results of the single tasks as they are finished
            for future in concurrent.futures.as_completed(futures):
                name, fortigate_version = future.result()
                result.push_result(name, fortigate_version)
                progress.update(task, advance=1)

    return result