Nautobot Plugin: Welcome Wizard

Blog Detail

Whenever I build out a new instance of Nautobot, I struggle with where to start. I try to add a Device, only to find that I need to create a Device Type, Site, and Device Role. I go to add a Device Type and find I need a Manufacturer (which is not directly required for a Device). It would be great if Nautobot would alert me when I am missing required objects.

Speaking of Device Types, I find that I am adding numerous Device Types to Nautobot. There is a nice community resource DeviceType-library to add Device Types into Nautobot, but tracking down the right YAML file to then import into Nautobot takes time.

We are excited to introduce the solution to these problems in the Welcome Wizard plugin for Nautobot.

welcome_wizard

Overview

The Welcome Wizard is an open-source Nautobot plugin with the goal to assist users with the necessary initial steps in populating data within Nautobot.

The Welcome Wizard adds four (4) key features:

Import Wizard

Welcome Wizard uses the Import Wizard to allow ease of adding community-defined Device Types and Manufacturers into Nautobot. This is built upon the Git datasources feature of Nautobot.

import_device_type

Quick-Start Settings

Welcome Wizard includes by default the DeviceType-library, but this can be disabled and a custom library can be used instead.

gitrepo

Helpful Middleware

Welcome Wizard includes banners in forms to alert the user when required form fields have no associated resources in Nautobot.

middleware_x3

Welcome Wizard Dashboard

The Welcome Wizard Dashboard contains a list of common Nautobot Data Models that many other Nautobot models require. This page allows ease of adding items to Nautobot or, if supported, importing them. This ties all of the features together.

dashboard_with_completions

Installing the Plugin

The plugin is available as a Python package in PyPI and can be installed atop an existing Nautobot installation using pip:

pip install nautobot-welcome-wizard

This plugin is compatible with Nautobot 1.0.0b4 and higher. Once installed, the plugin needs to be enabled in your nautobot_config.py:

# nautobot_config.py
PLUGINS = [
    # ...,
    "welcome_wizard",
]

Accessing the Welcome Wizard Dashboard

You can begin by selecting Plugins -> Nautobot Welcome Wizard -> Welcome Wizard from the navigation bar (or navigating to /plugins/welcome_wizard/) on your Nautobot instance to view the Welcome Wizard dashboard:

welcome_wizard

Welcome Wizard shows a set of Nautobot objects that lay the groundwork for other Nautobot objects. The goal of the dashboard is to help introduce features of Nautobot and keep track of their use. For instance, Sites are used in a number of objects inside of Nautobot. A Site is used when creating Devices, Racks, and Rack Groups. In addition, they are optional in many other objects.

From the dashboard, you can click on the green Add button in the Sites role to take you directly to the form for adding a Site. For Manufacturers and Device Types, you can click on the blue wizard hat to take you to the Welcome Wizard Import page.

Welcome Wizard Import

Welcome Wizard allows easy import from a Device Type library for Manufacturers and Device Types. Let’s take a look at the Device Type Import. From the dashboard, click on the blue wizard hat in the Device Types row.

import_device_type

Note if this is the first time loading this page, it will take a minute to synchronize the data with the Git Datasource.

On this page you can search for a Device Model or filter by Manufacturer. To import a Device Type into Nautobot, click the blue import button next to the Device Model you are trying to import. Once you confirm the import, the Device Type will now be available for use in Nautobot.


Conclusion

More information on this plugin can be found at Nautobot Welcome Wizard Docs. We hope this plugin will allow you to quickly and easily get started in Nautobot.

-Stephen



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Ansible Become Logging

Blog Detail

This blog post aims to assist Ansible users in providing documentation to security teams when using privileged escalation (become) in Ansible playbooks.

The Problem Statement

Ansible does not use a specific command when using privileged escalation on remote machines see here for more details. As an example, if you wanted to restart the httpd service, the command you run on the system would be sudo systemctl restart httpd but when Ansible executes the task, you would see something like /bin/sh BECOME-SUCCESS ; /usr/bin/python /home/myuser/.ansible/tmp/ansible-tmp-abcdef/AnsiballZ_12345.pyHere is a link explaining the Ansiballz framework. This presents two concerns to most security teams. First, they cannot limit the commands used by Ansible. Second, they cannot see which commands Ansible executes. We will be addressing the second concern.

The Solution

Ansible provides Callback Plugins that can be used when Ansible responds to various events inside a playbook. Ansible provides a small set of default callback plugins out of the box, but none that address our problem.

Custom Callback Plugin

We will be creating a custom callback plugin that hooks into the use of become and logs to stdout. This will not log the actual command used by Ansible but the module and arguments passed to the module. As an example, instead of systemctl restart httpd the log would show the ansible.builtin.service module with the name set to httpd and the state set to restarted.

First we need to build out our directory structure. We are choosing the name “become” for our plugin, but you could choose your own name. By convention, the filename and plugin name should match, but they do not have to. This assumes the callback plugin will be added to an existing Ansible project:

├── ansible.cfg                 // Ansible Configuration file.
├── example_pb.yaml             // Ansible playbook.
├── callback_plugins                  
│   └── become.py               // Our Become Callback Plugin.
mkdir callback_plugins
touch callback_plugins/become.py

Then add the callback plugin to your ansible.cfg file.

# ansible.cfg
[defaults]
callback_whitelist = become

From there we will create a sample playbook that restarts httpd:

# example_pb.yaml

---
- hosts: all
  tasks:
    - name: "ESCALATED TASK: RESTART HTTPD"
      ansible.builtin.service:
        name: httpd
        state: restarted
      become: true
    
    - name: "NON-ESCALATED TASK: DEBUG"
      ansible.builtin.debug:
        msg: "This task does not require escalation"

Next we will add the basic structure to our callback plugin. Ansible provides an example plugin that we will modify.

# become.py
# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

# not only visible to ansible-doc, it also 'declares' the options the plugin requires and how to configure them.
DOCUMENTATION = '''
  callback: become
  requirements:
    - whitelist in configuration
  short_description: When become is used, log to stdout.
  description:
      - This callback will log the become, become method, task name, action and arguments to stdout.
'''

from ansible.plugins.callback import CallbackBase


class CallbackModule(CallbackBase):
    """
    This callback module tells you when become is used.
    """
    CALLBACK_VERSION = 2.0
    CALLBACK_NAME = 'become'

    # only needed if you ship it and don't want to enable by default
    CALLBACK_NEEDS_WHITELIST = True

    def __init__(self):

        # make sure the expected objects are present, calling the base's __init__
        super(CallbackModule, self).__init__()

We have now added the skeleton that Ansible requires, but we have not added any new functionality to our plugin. Callback plugins listen for events that Ansible emits. Check out the callback init file in the Ansible repository for a list of all the events supported. The event that we will be looking at hooking into is the v2_runner_on_start, since it passes not only the task but also the host, and it executes at the start of a task. Let’s add our first callback now.

# become.py
class CallbackModule(CallbackBase):
...
    def v2_runner_on_start(self, host, task):
        """
        Process the runner on start event.
        """
        if task.become:
            print(f"Hostname: {host.name}")
            print(f"Task: {task.name}")
            print(f"Action: {task.action}")
            print(f"Arguments: {task.args}")

We are accessing attributes of task and host here. In this example we are showing just a few, but you can use dir(task) or dir(host) to find many others. When we execute the playbook, we should see the following:

TASK [Gathering Facts]****************************
ok: [my_hostname]
TASK [ESCALATED TASK: RESTART HTTPD]**************
Hostname: my_hostname
Task: ESCALATED TASK: RESTART HTTPD
Action: ansible.builtin.service
Arguments: {'name': 'httpd', 'state': 'restarted'}
changed: [my_hostname]
TASK [NON-ESCALATED TASK: DEBUG]
ok: [my_hostname] => {
    "msg": "This task does not require escalation"
}

Supporting Variables

At this point, We have a callback plugin that will log to stdout the use of become in our playbooks. But if our playbook uses variables, we will not see those variables. Let’s modify our example to show:

# example_pb.yaml

---
- hosts: all
  tasks:
    - name: "ESCALATED TASK: RESTART HTTPD"
      ansible.builtin.service:
        name: "{{ service }}"
        state: restarted
      become: true
      vars:
        service: httpd
    
    - name: "NON-ESCALATED TASK: DEBUG"
      ansible.builtin.debug:
        msg: "This task does not require escalation"

Now let’s run the playbook:

TASK [Gathering Facts]****************************
ok: [my_hostname]
TASK [ESCALATED TASK: RESTART HTTPD]**************
Hostname: my_hostname
Task: ESCALATED TASK: RESTART HTTPD
Action: ansible.builtin.service
Arguments: {'name': '{{ service }}', 'state': 'restarted'}
changed: [my_hostname]
TASK [NON-ESCALATED TASK: DEBUG]
ok: [my_hostname] => {
    "msg": "This task does not require escalation"
}

In order to access variables passed into a task (or from group vars), we need to add the Templar class to our callback plugin. We will need to add a few more hooks to get all of the play data and the hostvars.

# become.py

from ansible.plugins.callback import CallbackBase
from ansible.template import Templar


class CallbackModule(CallbackBase):
...

    def v2_playbook_on_start(self, playbook):
        """
        Initialize self.playbook.
        """
        self.playbook = playbook

    def v2_playbook_on_play_start(self, play):
        """
        Get the variable manager of the current play from the playbook.
        """
        self.play = play
        self.vm = play.get_variable_manager()

    def _all_vars(self, host=None, task=None):
        """
        Load all variables for the given inputs.
        """
        return self.vm.get_vars(
            play = self.play,
            host = host,
            task = task
        )

    def v2_runner_on_start(self, host, task):
        """
        Process the runner on start event.
        """
        templar = Templar(loader=self.playbook.get_loader(),
                  variables=self._vars(host=host, task=task))

        if task.become:
            print(f"Hostname: {host.name}")
            print(f"Task: {task.name}")
            print(f"Action: {task.action}")
            print(f"Arguments: {templar.template(task.args, fail_on_undefined=False)}")

With the above changes, running the playbook will now fill in the variables:

TASK [Gathering Facts]****************************
ok: [my_hostname]
TASK [ESCALATED TASK: RESTART HTTPD]**************
Hostname: my_hostname
Task: ESCALATED TASK: RESTART HTTPD
Action: ansible.builtin.service
Arguments: {'name': 'httpd', 'state': 'restarted'}
changed: [my_hostname]
TASK [NON-ESCALATED TASK: DEBUG]
ok: [my_hostname] => {
    "msg": "This task does not require escalation"
}

The Next Step

Now that we know how to access the task attributes and variables, we could extend any of the other popular callback plugins, such as syslog_jsonLogstash, or Splunk, to send the data directly to the security team.

There are a few Ansible playbook options that are not covered here, such as loop or with_items, become_method, or become_user, but this should be enough to get you started. Another suggested change would be to set CALLBACK_NEEDS_WHITELIST = False. With this change, we would no longer need to whitelist become in the ansible.cfg file. This, paired with Ansible Tower or AWX, would allow the security team to ensure conformance to standards. For this to work, the plugin would need to be moved to the environment created by Ansible Tower or AWX. See Adding a plugin locally for more details.

Conclusion

Ansible callback plugins give us the ability to provide detailed evidence of our privileged escalation to answer some of the security team’s concerns.

Thanks for taking the time to read!

-Stephen

Here is the full code from become.py:

# become.py
# Make coding more python3-ish, this is required for contributions to Ansible
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

# not only visible to ansible-doc, it also 'declares' the options the plugin requires and how to configure them.
DOCUMENTATION = '''
  callback: become
  requirements:
    - whitelist in configuration
  short_description: Logs become use to stdout
  description:
      - This callback will log the become, become method, task name, action and arguments to stdout.
'''

from ansible.plugins.callback import CallbackBase
from ansible.template import Templar


class CallbackModule(CallbackBase):
    """
    This callback module tells you when become is used.
    """
    CALLBACK_VERSION = 2.0
    CALLBACK_NAME = 'become'

    # only needed if you ship it and don't want to enable by default
    CALLBACK_NEEDS_WHITELIST = True

    def __init__(self):

        # make sure the expected objects are present, calling the base's __init__
        super(CallbackModule, self).__init__()

    def v2_playbook_on_start(self, playbook):
        """
        Initialize self.playbook.
        """
        self.playbook = playbook

    def v2_playbook_on_play_start(self, play):
        """
        Get the variable manager of the current play from the playbook.
        """
        self.play = play
        self.vm = play.get_variable_manager()

    def _all_vars(self, host=None, task=None):
        """
        Load all variables for the given inputs.
        """
        return self.vm.get_vars(
            play = self.play,
            host = host,
            task = task
        )

    def v2_runner_on_start(self, host, task):
        """
        Process the runner on start event.
        """
        templar = Templar(loader=self.playbook.get_loader(),
                  variables=self._vars(host=host, task=task))

        if task.become:
            print(f"Hostname: {host.name}")
            print(f"Task: {task.name}")
            print(f"Action: {task.action}")
            print(f"Arguments: {templar.template(task.args, fail_on_undefined=False)}")

References



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!