Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new functions and update print_result #21

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ _________

* **print_result** - Formats nicely and prints results on stdout.
* **print_title** - Formats nicely a title and prints it on stdout.
* **print_stat** - Prints results statistic on stdout.
* **write_result** - Writes results to file.
* **write_results** - Writes results to files.

Processors
__________
Expand Down
385 changes: 383 additions & 2 deletions docs/source/tutorials/function_print_result.ipynb

Large diffs are not rendered by default.

389 changes: 389 additions & 0 deletions docs/source/tutorials/function_print_stat.ipynb

Large diffs are not rendered by default.

785 changes: 785 additions & 0 deletions docs/source/tutorials/function_write_result.ipynb

Large diffs are not rendered by default.

900 changes: 900 additions & 0 deletions docs/source/tutorials/function_write_results.ipynb

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions nornir_utils/plugins/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
from .print_result import print_result, print_title
from colorama import init

__all__ = ("print_result", "print_title")
from .print_result import (
print_result,
print_title,
)
from .print_stat import print_stat
from .write_result import write_result
from .write_results import write_results

init(autoreset=True, strip=False)

__all__ = (
"print_result",
"print_title",
"print_stat",
"write_result",
"write_results",
)
95 changes: 72 additions & 23 deletions nornir_utils/plugins/functions/print_result.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import json
import logging
import pprint
import sys
import threading
from typing import List, cast, Optional
from collections import OrderedDict
import json
from typing import IO, List, cast, Optional

from colorama import Fore, Style, init
from colorama import AnsiToWin32, Fore, Style, init
from colorama.ansitowin32 import StreamWrapper

from nornir.core.task import AggregatedResult, MultiResult, Result

Expand All @@ -16,12 +18,15 @@
init(autoreset=True, strip=False)


def print_title(title: str) -> None:
def print_title(title: str, file: IO[str] = sys.stdout) -> None:
"""
Helper function to print a title.
"""
msg = "**** {} ".format(title)
print("{}{}{}{}".format(Style.BRIGHT, Fore.GREEN, msg, "*" * (80 - len(msg))))
print(
"{}{}{}{}".format(Style.BRIGHT, Fore.GREEN, msg, "*" * (80 - len(msg))),
file=file,
)


def _get_color(result: Result, failed: bool) -> str:
Expand All @@ -40,11 +45,16 @@ def _print_individual_result(
failed: bool,
severity_level: int,
task_group: bool = False,
no_errors: bool = False,
print_host: bool = False,
file: IO[str] = sys.stdout,
) -> None:
if result.severity_level < severity_level:
return

if no_errors and result.exception:
return

color = _get_color(result, failed)
subtitle = (
"" if result.changed is None else " ** changed : {} ".format(result.changed)
Expand All @@ -60,36 +70,42 @@ def _print_individual_result(
print(
"{}{}{}{} {}".format(
Style.BRIGHT, color, msg, symbol * (80 - len(msg)), level_name
)
),
file=file,
)
for attribute in attrs:
x = getattr(result, attribute, "")
if isinstance(x, BaseException):
# for consistency between py3.6 and py3.7
print(f"{x.__class__.__name__}{x.args}")
print(f"{x.__class__.__name__}{x.args}", file=file)
elif x and not isinstance(x, str):
if isinstance(x, OrderedDict):
print(json.dumps(x, indent=2))
print(json.dumps(x, indent=2), file=file)
else:
pprint.pprint(x, indent=2)
pprint.pprint(x, indent=2, stream=file)
elif x:
print(x)
print(x, file=file)


def _print_result(
result: Result,
attrs: Optional[List[str]] = None,
failed: bool = False,
severity_level: int = logging.INFO,
no_errors: bool = False,
print_host: bool = False,
file: IO[str] = sys.stdout,
) -> None:
attrs = attrs or ["diff", "result", "stdout"]
if isinstance(attrs, str):
attrs = [attrs]

if isinstance(result, AggregatedResult):
msg = result.name
print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "*" * (80 - len(msg))))
print(
"{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "*" * (80 - len(msg))),
file=file,
)
for host, host_data in sorted(result.items()):
title = (
""
Expand All @@ -98,27 +114,43 @@ def _print_result(
)
msg = "* {}{}".format(host, title)
print(
"{}{}{}{}".format(Style.BRIGHT, Fore.BLUE, msg, "*" * (80 - len(msg)))
"{}{}{}{}".format(Style.BRIGHT, Fore.BLUE, msg, "*" * (80 - len(msg))),
file=file,
)
_print_result(
host_data, attrs, failed, severity_level, no_errors, file=file
)
_print_result(host_data, attrs, failed, severity_level)
elif isinstance(result, MultiResult):
_print_individual_result(
result[0],
attrs,
failed,
severity_level,
task_group=True,
no_errors=no_errors,
print_host=print_host,
file=file,
)
for r in result[1:]:
_print_result(r, attrs, failed, severity_level)
_print_result(r, attrs, failed, severity_level, no_errors, file=file)
color = _get_color(result[0], failed)
msg = "^^^^ END {} ".format(result[0].name)
if result[0].severity_level >= severity_level:
print("{}{}{}{}".format(Style.BRIGHT, color, msg, "^" * (80 - len(msg))))
if result[0].severity_level >= severity_level or not (
result[0].exception and no_errors
):
print(
"{}{}{}{}".format(Style.BRIGHT, color, msg, "^" * (80 - len(msg))),
file=file,
)
elif isinstance(result, Result):
_print_individual_result(
result, attrs, failed, severity_level, print_host=print_host
result,
attrs,
failed,
severity_level,
no_errors=no_errors,
print_host=print_host,
file=file,
)


Expand All @@ -127,18 +159,35 @@ def print_result(
vars: Optional[List[str]] = None,
failed: bool = False,
severity_level: int = logging.INFO,
no_errors: bool = False,
print_host: bool = True,
file: IO[str] = sys.stdout,
) -> None:
"""
Prints an object of type `nornir.core.task.Result`

Arguments:
result: from a previous task
vars: Which attributes you want to print
failed: if ``True`` assume the task failed
severity_level: Print only errors with this severity level or higher
Args:
result: From a previous task
vars: Which attributes you want to print
failed: If ``True`` assume the task failed
severity_level: Print/write only errors with this severity level or higher
no_errors: Don't print/write results with exceptions
print_host: Print/write hostname for MultiResult and Result objects
file: The file argument must be an object with a write(string) method;
if it is not present or None, sys.stdout will be used.
The file argument of the print_result function uses the same file argument
of the print function

Returns:
N/A

Raises:
N/A
"""
if file and not isinstance(file, StreamWrapper):
file = AnsiToWin32(file, strip=True, autoreset=True).stream
LOCK.acquire()
try:
_print_result(result, vars, failed, severity_level, print_host=True)
_print_result(result, vars, failed, severity_level, no_errors, print_host, file)
finally:
LOCK.release()
89 changes: 89 additions & 0 deletions nornir_utils/plugins/functions/print_stat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import Tuple

from colorama import Fore, Style

from nornir.core.task import AggregatedResult, MultiResult, Result

from .print_result import LOCK, _get_color


def _print_individual_stat(
result: Result,
res_sum: int = 0,
ch_sum: int = 0,
f_sum: int = 0,
) -> Tuple[int, int, int]:
f, ch = (result.failed, result.changed)

res_sum += 1
ch_sum += int(ch)
f_sum += int(f)

color = _get_color(result, f)

msg = "{:<35} ok={:<15} changed={:<15} failed={:<15}".format(
result.name, not f, ch, f
)
print("{}{}{}".format(Style.BRIGHT, color, msg))
return res_sum, ch_sum, f_sum


def _print_stat(
result: Result,
res_sum: int = 0,
ch_sum: int = 0,
f_sum: int = 0,
) -> Tuple[int, int, int]:
if isinstance(result, AggregatedResult):
msg = result.name
print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "*" * (80 - len(msg))))
for host, host_data in result.items():
msg_host = "* {} ".format(host)
print(
"{}{}{}{}".format(
Style.BRIGHT,
Fore.BLUE,
msg_host,
"*" * (80 - len(msg_host)),
)
)
res_sum, ch_sum, f_sum = _print_stat(host_data, res_sum, ch_sum, f_sum)
elif isinstance(result, MultiResult):
for res in result:
res_sum, ch_sum, f_sum = _print_stat(res, res_sum, ch_sum, f_sum)
elif isinstance(result, Result):
res_sum, ch_sum, f_sum = _print_individual_stat(result, res_sum, ch_sum, f_sum)

return res_sum, ch_sum, f_sum


def print_stat(result: Result) -> None:
"""
Prints statistic for `nornir.core.task.Result` object

Args:
result: From a previous task

Returns:
N/A

Raises:
N/A
"""
LOCK.acquire()
try:
res_sum, ch_sum, f_sum = _print_stat(
result,
res_sum=0,
ch_sum=0,
f_sum=0,
)
print()
for state, summary, color in zip(
("OK", "CHANGED", "FAILED"),
(res_sum - f_sum, ch_sum, f_sum),
(Fore.GREEN, Fore.YELLOW, Fore.RED),
):
print("{}{}{:<8}: {}".format(Style.BRIGHT, color, state, summary))
finally:
LOCK.release()
60 changes: 60 additions & 0 deletions nornir_utils/plugins/functions/write_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging
import os
import threading
from pathlib import Path
from typing import List, Optional

from nornir.core.task import Result

from .print_result import print_result


LOCK = threading.Lock()


def write_result(
result: Result,
filepath: str,
vars: Optional[List[str]] = None,
failed: bool = False,
severity_level: int = logging.INFO,
no_errors: bool = False,
write_host: bool = True,
append: bool = False,
) -> None:
"""
Writes an object of type `nornir.core.task.Result` to file

Args:
result: From a previous task
(Result or AggregatedResult or MultiResult)
filepath: Filepath you want to write the result
vars: Which attributes you want to write
(see ``class Result`` attributes)
failed: If ``True`` assume the task failed
severity_level: Write only errors with this severity level or higher
no_errors: Don't write results with exceptions
write_host: Write hostname to file for MultiResult and Result objects
append: "a+" if ``True`` or "w+" if ``False``

Returns:
N/A

Raises:
N/A
"""

dirname = os.path.dirname(filepath)
Path(dirname).mkdir(parents=True, exist_ok=True)

mode = "a+" if append else "w+"

LOCK.acquire()

try:
with open(filepath, mode=mode) as f:
if append and Path(filepath).stat().st_size != 0:
f.write("\n\n")
print_result(result, vars, failed, severity_level, no_errors, write_host, f)
finally:
LOCK.release()
Loading