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

Implement chatops-enabled configuration consistency checks #10

Merged
merged 11 commits into from Apr 10, 2017
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
@@ -1,3 +1,6 @@
# OS files
*.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -226,3 +226,16 @@ This pack is actively being developed.
Most actions put their output under the key raw and when the htmlout option is ticked (see above)
the output is in 'raw' and 'html'. the cli action also adds a 'raw_array' key to the result so
you can iterate through the lines as 'raw' contains lines with a newline ending.

### Developing the NAPALM Pack

If you're copying or rsyncing files directly into a VM, bypassing the normal pack installation process
(which is normal during development) then you'll want to be aware of a few things.

First, you'll need to install the virtualenv yourself:

st2 run packs.setup_virtualenv packs=napalm

Also, you will need to manually register the pack if you make changes to its metadata or configuration:

st2 pack register napalm
37 changes: 37 additions & 0 deletions actions/check_consistency.meta.yaml
@@ -0,0 +1,37 @@
---
name: "check_consistency"
runner_type: "python-script"
description: "Check that the device's configuration is consistent with the 'golden' config in a Git repository"
enabled: true
entry_point: "check_consistency.py"
parameters:
hostname:
type: "string"
description: "The hostname of the device to connect to. Driver must be specified if hostname is not in configuration. Hostname without FQDN can be given if device is in configuration."
required: true
position: 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python actions don't need positions. If it's a python script then, yes. The runner type being python-script can be confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack that. Unfortunately all the actions in the pack are like this. So I'll address this in a follow-up PR that tackles all of them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#11

driver:
type: "string"
description: "Device driver name for connecting to device, see https://napalm.readthedocs.io/en/latest/support/index.html for list."
required: false
position: 1
port:
type: "string"
description: "port for accessing device"
required: false
position: 2
credentials:
type: "string"
description: "The credentials group which contains the username and password to log in"
required: false
position: 3
htmlout:
type: "boolean"
description: "In addition to the normal output also includes html output."
required: false

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably supply default: false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as last comment - this needs to get fixed on all actions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#12

position: 4
repository:
type: "string"
description: "Git repository where the 'golden' device configurations are stored (overrides configuration)"
required: false
position: 5
97 changes: 97 additions & 0 deletions actions/check_consistency.py
@@ -0,0 +1,97 @@
import difflib
import logging
import os
import re
import shutil
import tempfile

from lib.action import NapalmBaseAction

import git

# Suppressing the 'No handlers could be found for logger "st2.st2common.util.loader"' message
# that appears when including the TempRepo class, until I have a chance to troubleshoot why
# this is happening TODO(mierdin)
loaderlog = logging.getLogger('st2.st2common.util.loader')
loaderlog.setLevel(logging.DEBUG)
loaderlog.addHandler(logging.NullHandler())


class TempRepo(object):
def __enter__(self):
self.name = tempfile.mkdtemp()
return self.name

def __exit__(self, exc_type, exc_value, traceback):
shutil.rmtree(self.name)


class NapalmCheckConsistency(NapalmBaseAction):
"""Check that the device's configuration is consistent with the 'golden' config in a Git repository
"""

def get_golden_config(self, repo, device):

with TempRepo() as tmpdir:
repo_path = os.path.join(tmpdir, 'repo')
git.Repo.clone_from(repo, repo_path, branch='master')

try:

with open('%s/%s.txt' % (repo_path, device)) as config_file:
return "".join(config_file.readlines())

except IOError:
self.logger.error("Golden config not present in repo")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide the repo name for easy debugging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack that, done in 651bcc2

raise

def run(self, repository=None, **std_kwargs):

if not self.config['config_repo'] and not repository:
raise Exception("Golden configs repository not provided in args or config")
else:
# Use config if arg not provided
if not repository:
repository = self.config['config_repo']

result = {
"deviation": False,
"diff_contents": ""
}

try:

with self.get_driver(**std_kwargs) as device:

# Get golden and actual configs
golden_config = self.get_golden_config(repository, self.hostname)
actual_config = device.get_config()['running']

# Regular expressions for matching lines to ignore
# Lot of network devices have lines like "last modified" that we don't
# want to include in the diff
#
# In the future, this may be a configurable option, but we're doing
# this statically for now.
ignore_regexs = [
"## .*\n"
]
for pattern in ignore_regexs:
actual_config = re.sub(pattern, "", actual_config)
golden_config = re.sub(pattern, "", golden_config)

# Generate diff
golden = golden_config.splitlines(1)
actual = actual_config.splitlines(1)
diff = difflib.unified_diff(golden, actual)
diff = ''.join(diff)

if diff:
result['deviation'] = True
result['diff_contents'] = ''.join(diff)

except Exception, e:
self.logger.error(str(e))
return (False, str(e))

return (True, result)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you paste an example of the diff in description please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized you meant the PR description. Duh. Posted

4 changes: 2 additions & 2 deletions actions/lib/action.py
Expand Up @@ -113,8 +113,8 @@ def find_device_from_config(self, search, driver=None, credentials=None):
# we fail.

if not driver:
raise ValueError('Can not find driver for host {}, try with \
driver parameter.'.format(host_result))
raise ValueError('Can not find driver for host {}, try with '
'driver parameter.'.format(host_result))

if not credentials:
raise ValueError(('Can not find credential group for host {}, try with credentials'
Expand Down
20 changes: 20 additions & 0 deletions aliases/check_consistency.yaml
@@ -0,0 +1,20 @@
---
name: "check_consistency"
pack: "napalm"
action_ref: "napalm.check_consistency"
description: "Check consistency of the device's configuration"
formats:
- "napalm check consistency on {{hostname}}"
# - "check consistency on all" #TODO(mierdin): Won't work until actions can iterate over device list

result:
format: |
{% if execution.result.result.deviation %}
The configuration on {{ execution.parameters.hostname }} has deviated from the golden configuration.

Diff to follow:
{{ execution.result.result.diff_contents }}

{% else %}
The configuration on {{ execution.parameters.hostname }} exactly matches the golden configuration.
{% endif %}
5 changes: 5 additions & 0 deletions config.schema.yaml
Expand Up @@ -4,6 +4,11 @@ html_table_class:
type: "string"
required: true

config_repo:
description: "Git repository where configurations are located"
type: "string"
required: false

devices:
description: "List of devices to map the device hostname with the driver and credentials"
required: true
Expand Down
1 change: 1 addition & 0 deletions napalm.yaml.example
@@ -1,5 +1,6 @@
---
html_table_class: napalm
config_repo: https://github.com/StackStorm/vsrx-configs.git

credentials:
core:
Expand Down
2 changes: 1 addition & 1 deletion pack.yaml
Expand Up @@ -8,6 +8,6 @@ keywords:
- juniper
- arista
- ibm
version: 0.2.1
version: 0.2.2
author: mierdin, Rob Woodward
email: info@stackstorm.com
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -10,3 +10,4 @@ napalm-pluribus
napalm-panos
napalm
json2table
GitPython