Skip to content

Commit

Permalink
feat: Add napalm driver integration (#8)
Browse files Browse the repository at this point in the history
* feat: add format and sanitize options for active config

* feat: add napalm driver

* refactor: replace junos driver with napalm driver

* feat: add driver support for arista eos using napalm

* feat: add option to use default driver

* refactor: trim log line to fit line length

Co-authored-by: Adam Kirchberger <adamkirchberger@users.noreply.github.com>
  • Loading branch information
adamkirchberger and adamkirchberger committed Jul 17, 2022
1 parent f23ea22 commit 792ca97
Show file tree
Hide file tree
Showing 12 changed files with 1,034 additions and 649 deletions.
14 changes: 11 additions & 3 deletions docs/guide/drivers.md
Expand Up @@ -45,6 +45,8 @@ A driver can be a single file or a directory (with an `__init__.py`) which conta

Nectl will look for custom drivers in `demo-kit/drivers` but this can overridden in your kit [settings file](guide/settings.md).

A default driver can be specified in your kit [settings file](guide/settings.md) to be used by hosts which do not match a core or kit driver. This can be useful when you want to fallback to a library like Napalm.

### Driver example

This is an example that can be used for building your own driver for an operating system named `nos`
Expand Down Expand Up @@ -99,12 +101,13 @@ class NosDriver(BaseDriver):
return True
return False

def get_config(self, format: str = None) -> str:
def get_config(self, format: str = None, sanitized: bool = True) -> str:
"""
Returns the active configuration from the host.
Args:
format (str): new config format.
sanitized (bool): remove secret data.
Returns:
str: active config.
Expand All @@ -117,12 +120,13 @@ class NosDriver(BaseDriver):
# Return config
return config

def compare_config(self, config_filepath: str) -> str:
def compare_config(self, config_filepath: str, format: str = None) -> str:
"""
Returns the configuration diff between the active and supplied config.
Args:
config_filepath (str): new config file.
format (str): config format.
Returns:
str: active vs staged diff.
Expand All @@ -135,12 +139,16 @@ class NosDriver(BaseDriver):
# Return diff
return diff

def apply_config(self, config_filepath: str) -> str:
def apply_config(
self, config_filepath: str, format: str = None, commit_timer: int = 1
) -> str:
"""
Apply staged config onto host.
Args:
config_filepath (str): new config file.
format (str): config format.
commit_timer (int): automatic rollback in minutes. Defaults to 1.
Returns:
str: active vs staged diff.
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/settings.md
Expand Up @@ -52,3 +52,6 @@ For example settings files and values see [Quick start](quickstart.md)
| config_diffs_dir | Optional | configs/diffs | Default configs diffs directory. |
| active_configs_dir | Optional | configs/active | Default active configs directory. |
| configs_file_extension | Optional | txt | Default configs file extension. |
| configs_format | Optional | | Config format variable passed to driver methods. |
| configs_sanitized | Optional | True | Defines whether configs pulled from devices should be sanitized. |
| default_driver | Optional | None | Defines a default driver if one is not found. Test and use at own risk! |
27 changes: 22 additions & 5 deletions nectl/configs/drivers/__init__.py
Expand Up @@ -16,7 +16,7 @@
# along with Nectl. If not, see <http://www.gnu.org/licenses/>.

import time
from typing import List, Type, Optional
from typing import List, Type, Dict, Optional, Any

from ...logging import get_logger
from ...settings import Settings
Expand All @@ -29,7 +29,7 @@
from ..utils import write_configs_to_dir
from .utils import load_drivers_from_kit
from .basedriver import BaseDriver
from .junosdriver import JunosDriver
from .napalmdriver import NapalmDriver
from ...datatree.hosts import Host

logger = get_logger()
Expand All @@ -40,7 +40,7 @@ class Drivers:
Map os_name to drivers.
"""

core_drivers = {"junos": JunosDriver}
core_drivers = {"junos": NapalmDriver, "eos": NapalmDriver}
kit_drivers: Optional[dict] = None


Expand Down Expand Up @@ -72,6 +72,17 @@ def get_driver(settings: Settings, os_name: str) -> Type[BaseDriver]:
if os_name == driver_os_name:
return driver

# Use a default driver
logger.debug("checking if default driver is defined")
if settings.default_driver:
if settings.default_driver == "napalm":
return NapalmDriver

# Default driver does not exist
raise DriverNotFoundError(
f"no default driver found matching name: {settings.default_driver}"
)

raise DriverNotFoundError(f"no driver found that matches os_name: {os_name}")


Expand Down Expand Up @@ -107,7 +118,9 @@ def run_driver_method_on_hosts(
for host in hosts:
# Skip hosts with no os_name or mgmt_ip
if not host.os_name or not host.mgmt_ip:
logger.warning(f"[{host.id}] skipping due to missing 'os_name' or 'mgmt_ip'")
logger.warning(
f"[{host.id}] skipping due to missing 'os_name' or 'mgmt_ip'"
)
continue

# Create host driver
Expand All @@ -122,12 +135,16 @@ def run_driver_method_on_hosts(
errors += 1
continue # skip host

kwargs = {}
# Prepare args
kwargs: Dict[str, Any] = {}
if method_name in ["compare_config", "apply_config"]:
kwargs["config_filepath"] = (
f"{settings.kit_path}/{settings.staged_configs_dir}/"
+ f"{host.id}.{settings.configs_file_extension}"
)
elif method_name == "get_config":
kwargs["format"] = settings.configs_format
kwargs["sanitized"] = settings.configs_sanitized

# Open connection to host
try:
Expand Down
14 changes: 11 additions & 3 deletions nectl/configs/drivers/basedriver.py
Expand Up @@ -23,6 +23,8 @@
from ...datatree.hosts import Host

COMMIT_COMMENT = getenv("NECTL_COMMIT_COMMENT", "Configured by Nectl.")
COMMIT_WAIT_MULTIPLIER = 0.75
CONNECT_TIMEOUT = 5
logger = get_logger()


Expand Down Expand Up @@ -80,38 +82,44 @@ def is_connected(self) -> bool:

@abc.abstractmethod
@ensure_connected
def get_config(self, format: str = None) -> str:
def get_config(self, format: str = None, sanitized: bool = True) -> str:
"""
Returns the active configuration from the host.
Args:
format (str): new config format.
sanitized (bool): remove secret data.
Returns:
str: active config.
"""

@abc.abstractmethod
@ensure_connected
def compare_config(self, config_filepath: str) -> str:
def compare_config(self, config_filepath: str, format: str = None) -> str:
"""
Returns the configuration diff between the active and supplied config.
Args:
config_filepath (str): new config file.
format (str): config format.
Returns:
str: active vs staged diff.
"""

@abc.abstractmethod
@ensure_connected
def apply_config(self, config_filepath: str) -> str:
def apply_config(
self, config_filepath: str, format: str = None, commit_timer: int = 1
) -> str:
"""
Apply staged config onto host.
Args:
config_filepath (str): new config file.
format (str): optional config format.
commit_timer (int): automatic rollback in minutes. Defaults to 1.
Returns:
str: active vs staged diff.
Expand Down

0 comments on commit 792ca97

Please sign in to comment.