Skip to content

Commit

Permalink
Ansible dynamic inventory support (#2)
Browse files Browse the repository at this point in the history
* add dynamic inventory support

* readme
  • Loading branch information
carlmontanari authored Jan 11, 2020
1 parent 3ec475a commit c0fb8b0
Show file tree
Hide file tree
Showing 46 changed files with 1,955 additions and 278 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Carl Montanari
Copyright (c) 2020 Carl Montanari

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,36 @@
[![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/)
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)

nornsible
Nornsible
=======

Wrap Nornir with Ansible-like host/group limits and tagging!
Bring some of the nice things about Ansible to Nornir!

The idea behind nornsible is to allow for Nornir scripts to be written to operate on an entire environment, and then limited to a subset of host(s) based on simple command line arguments. Of course you can simply do this yourself, but why not let nornsible handle it for you!
The idea behind Nornsible is to allow for Nornir scripts to be written to operate on an entire environment, and then limited to a subset of host(s) based on simple command line arguments. Of course you can simply do this yourself, but why not let nornsible handle it for you!

Nornsible provides the ability to -- via command line arguments -- filter Nornir inventories by hostname or group name, or both. There is also a handy flag to set the number of workers; quite useful for setting workers equal to 1 for troubleshooting purposes.

Lastly, nornsible supports the concept of tags. Tags correspond to the name of *custom* tasks and operate very much like tags in Ansible. Provide a list of tags to execute, and Nornsible will ensure that Nornir only runs those tasks. Provide a list of tags to skip, and Nornsible will ensure that Nornir only runs those not in that list. Easy peasy.
Nornsible all supports the concept of tags. Tags correspond to the name of *custom* tasks and operate very much like tags in Ansible. Provide a list of tags to execute, and Nornsible will ensure that Nornir only runs those tasks. Provide a list of tags to skip, and Nornsible will ensure that Nornir only runs those not in that list. Easy peasy.

Finally, Nornsible supports multiple inventory files as well as dynamic inventory files -- just like Ansible.

# How does nornsible work?

# How does Nornsible work?

Nornsible accepts an instantiated Nornir object as an argument and returns a slightly modified Nornir object. Nornsible sets the desired number of workers if applicable, and adds an attribute for "run_tags" and "skip_tags" based on your command line input.

To take advantage of the tags feature Nornsible provides a decorator that you can use to wrap your custom tasks. This decorator inspects the task being ran and checks the task name against the lists of run and skip tags. If the task is allowed, Nornsible simply allows the task to run as per normal, if it is *not* allowed, Nornsible will print a pretty message and move on.

Nornsible inventory can be used by simply installing Nornsible and setting the inventory plugin in your config file as follows:

```yaml
inventory:
plugin: nornsible.inventory.AnsibleInventory
options:
inventory: inventory.yaml
```
Note that instead of `hostsfile` Nornsible inventory uses `inventory` -- this is intentional to make sure to differentiate between the standard Nornir Ansible support and nornsible.

# Caveats

Expand Down Expand Up @@ -137,3 +149,9 @@ I broke testing into two main categories -- unit and integration. Unit is what y
# To Do

- Add handling for "not" in host/group limit; i.e.: "-t !localhost" to run against all hosts *not* localhost.
- Add more examples for using nornsible inventory in different ways -- i.e. multiple inventory, dynamic inventory, hash_behavior settings, etc.
- Add more detailed readme info on inventory stuff.
- Add support for host/group vars in directories in the host/group vars parent directory...
- i.e. `host_vars/myhost/somevar1.yaml` and `host_vars/myhost/somevar2.yaml`
- Add integration testing for inventory bits.
- Fix/add logging -- ensure inventory logs to nornir log as per usual, but also create a nornsible log for all nornsible "stuff".
4 changes: 4 additions & 0 deletions examples/basic_inventory/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
inventory:
plugin: nornsible.inventory.AnsibleInventory
options:
inventory: inventory.yaml
3 changes: 3 additions & 0 deletions examples/basic_inventory/group_vars/all.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
username: username
password: password
7 changes: 7 additions & 0 deletions examples/basic_inventory/group_vars/ios.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
platform: ios
connection_options:
netmiko:
platform: cisco_ios
napalm:
platform: ios
7 changes: 7 additions & 0 deletions examples/basic_inventory/inventory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
all:
children:
ios:
hosts:
c3560:
ansible_host: c3560x
15 changes: 15 additions & 0 deletions examples/basic_inventory/try_me.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from nornir import InitNornir
from nornir.plugins.tasks import networking


def main():
nr = InitNornir(config_file="config.yaml")
nr = nr.filter(name="c3560")
agg_result = nr.run(
task=networking.netmiko_send_command, command_string="show run | i hostname"
)
print(agg_result["c3560"].result)


if __name__ == "__main__":
main()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
24 changes: 8 additions & 16 deletions nornsible/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
"""nornir tag and host/group limit wrapper"""
"""ansible-like inventory utility for nornir"""
import logging
from logging import NullHandler

from nornsible.nornsible import (
InitNornsible,
nornsible_delegate,
nornsible_task,
nornsible_task_message,
patch_inventory,
parse_cli_args,
patch_config,
print_result,
)
from nornsible.decorators import nornsible_delegate, nornsible_task

from nornsible.nornsible import InitNornsible
from nornsible.functions import print_result
from nornsible.inventory import AnsibleInventory


__version__ = "2019.11.05"
__version__ = "2020.01.11"
__all__ = (
"AnsibleInventory",
"InitNornsible",
"nornsible_delegate",
"nornsible_task",
"nornsible_task_message",
"patch_inventory",
"parse_cli_args",
"patch_config",
"print_result",
)

Expand Down
57 changes: 57 additions & 0 deletions nornsible/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import argparse
from typing import List


def parse_cli_args(raw_args: List[str]) -> dict:
"""
Parse CLI provided arguments; ignore unrecognized.
Arguments:
raw_args: List of CLI provided arguments
Returns:
cli_args: Processed CLI arguments
Raises:
N/A # noqa
"""
parser = argparse.ArgumentParser(description="Nornir Script Wrapper")
parser.add_argument(
"-w",
"--workers",
help="number of workers to set for global configuration",
type=int,
default=0,
)
parser.add_argument(
"-l",
"--limit",
help="limit to host or comma separated list of hosts",
type=str.lower,
default="",
)
parser.add_argument(
"-g",
"--groups",
help="limit to group or comma separated list of groups",
type=str.lower,
default="",
)
parser.add_argument(
"-t", "--tags", help="names of tasks to explicitly run", type=str.lower, default=""
)
parser.add_argument("-s", "--skip", help="names of tasks to skip", type=str.lower, default="")
parser.add_argument(
"-d", "--disable-delegate", help="disable adding delegate host", action="store_true"
)
args, _ = parser.parse_known_args(raw_args)
cli_args = {
"workers": args.workers if args.workers else False,
"limit": set(args.limit.split(",")) if args.limit else False,
"groups": set(args.groups.split(",")) if args.groups else False,
"run_tags": set(args.tags.split(",")) if args.tags else [],
"skip_tags": set(args.skip.split(",")) if args.skip else [],
"disable_delegate": args.disable_delegate,
}
return cli_args
113 changes: 113 additions & 0 deletions nornsible/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import threading
from typing import Dict, List, Any, Union, Callable, Optional

from colorama import Back, Fore, init, Style
from nornir.core.task import Result, Task


init(autoreset=True, strip=False)
LOCK = threading.Lock()


def nornsible_task_message(msg: str, critical: Optional[bool] = False) -> None:
"""
Handle printing pretty messages for nornsible_task decorator
Args:
msg: message to beautifully print to stdout
critical: (optional) message is critical
Returns:
N/A
Raises:
N/A # noqa
"""
if critical:
back = Back.RED
fore = Fore.WHITE
else:
back = Back.CYAN
fore = Fore.WHITE

LOCK.acquire()
try:
print(f"{Style.BRIGHT}{back}{fore}{msg}{'-' * (80 - len(msg))}")
finally:
LOCK.release()


def nornsible_task(wrapped_func: Callable) -> Callable:
"""
Decorate an "operation" -- execute or skip the operation based on tags
Args:
wrapped_func: function to wrap in tag processor
Returns:
tag_wrapper: wrapped function
Raises:
N/A # noqa
"""

def tag_wrapper(
task: Task, *args: List[Any], **kwargs: Dict[str, Any]
) -> Union[Callable, Result]:
if task.host.name == "delegate":
return Result(
host=task.host, result="Task skipped, delegate host!", failed=False, changed=False
)
if {wrapped_func.__name__}.intersection(task.nornir.skip_tags):
msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
nornsible_task_message(msg)
return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
if not task.nornir.run_tags:
return wrapped_func(task, *args, **kwargs)
if {wrapped_func.__name__}.intersection(task.nornir.run_tags):
return wrapped_func(task, *args, **kwargs)
msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
nornsible_task_message(msg)
return Result(host=task.host, result="Task skipped!", failed=False, changed=False)

tag_wrapper.__name__ = wrapped_func.__name__
return tag_wrapper


def nornsible_delegate(wrapped_func: Callable) -> Callable:
"""
Decorate an "operation" -- execute only on "delegate" (localhost)
Args:
wrapped_func: function to wrap in delegate_wrapper
Returns:
tag_wrapper: wrapped function
Raises:
N/A # noqa
"""

def delegate_wrapper(
task: Task, *args: List[Any], **kwargs: Dict[str, Any]
) -> Union[Callable, Result]:
if "delegate" not in task.nornir.inventory.hosts.keys():
msg = f"---- WARNING no delegate available for task {wrapped_func.__name__} "
nornsible_task_message(msg, critical=True)
return Result(
host=task.host, result="Task skipped, delegate host!", failed=False, changed=False
)
if task.host.name != "delegate":
return Result(
host=task.host,
result="Task skipped, non-delegate host!",
failed=False,
changed=False,
)
return wrapped_func(task, *args, **kwargs)

delegate_wrapper.__name__ = wrapped_func.__name__
return delegate_wrapper
36 changes: 36 additions & 0 deletions nornsible/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
import threading
from typing import List, Optional

from nornir.core.task import AggregatedResult, MultiResult, Result
from nornir.plugins.functions.text import _print_result


LOCK = threading.Lock()


def print_result(
result: Result,
host: Optional[str] = None,
nr_vars: List[str] = None,
failed: bool = False,
severity_level: int = logging.INFO,
) -> None:
updated_agg_result = AggregatedResult(result.name)
for hostname, multi_result in result.items():
updated_multi_result = MultiResult(result.name)
for r in multi_result:
if isinstance(r.result, str) and r.result.startswith("Task skipped"):
continue
updated_multi_result.append(r)
if updated_multi_result:
updated_agg_result[hostname] = updated_multi_result # noqa

if not updated_agg_result:
return

LOCK.acquire()
try:
_print_result(updated_agg_result, host, nr_vars, failed, severity_level)
finally:
LOCK.release()
Loading

0 comments on commit c0fb8b0

Please sign in to comment.