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

Closes #35: Add ability to define password for accessing priviliged exec mode #45

Merged
merged 8 commits into from
Oct 26, 2023
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools wheel
python -m pip install -r requirements/dev.txt
- name: Run black
run: black --check --diff --color .
- name: Run ruff format
run: ruff format .
- name: Run ruff
run: ruff .

Expand Down
9 changes: 3 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
args: [--check, --diff, --color, .]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.280
rev: v0.1.2
hooks:
- id: ruff
args: [netbox_config_diff]
- id: ruff-format
exclude: migrations
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[![NetBox version](https://img.shields.io/badge/NetBox-3.5|3.6-blue.svg)](https://github.com/netbox-community/netbox)
[![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
[![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![CI](https://github.com/miaow2/netbox-config-diff/actions/workflows/commit.yaml/badge.svg?branch=develop)](https://github.com/miaow2/netbox-config-diff/actions)
Expand Down Expand Up @@ -78,6 +77,7 @@ PLUGINS_CONFIG = {
"netbox_config_diff": {
"USERNAME": "foo",
"PASSWORD": "bar",
"AUTH_SECONDARY": "foobar", # define here password for accessing Privileged EXEC mode, this variable is optional
},
}
```
Expand Down Expand Up @@ -120,6 +120,14 @@ No diff

![Screenshot of the compliance ok](docs/media/screenshots/compliance-ok.png)

Configuration request

![Screenshot of the CR](docs/media/screenshots/cr-created.png)

Completed Configuration request

![Screenshot of the completed CR](docs/media/screenshots/cr-completed.png)

## Credits

Based on the NetBox plugin tutorial:
Expand Down
12 changes: 12 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 2.1.0 (2023-10-26)

* [#35](https://github.com/miaow2/netbox-config-diff/issues/35) Add ability to define password for accessing priviliged exec mode
* [#37](https://github.com/miaow2/netbox-config-diff/issues/37) Add `DeviceRole` field to `CollectDiffScript`
* [#38](https://github.com/miaow2/netbox-config-diff/issues/38) Remove config template filter for devices filed in forms
* [#39](https://github.com/miaow2/netbox-config-diff/issues/39) Add `Status` field to `CollectDiffScript`
* [#43](https://github.com/miaow2/netbox-config-diff/issues/43) `ConfigDiffScript` does not create empty changelog entries

## 2.0.1 (2023-10-22)

* [#33](https://github.com/miaow2/netbox-config-diff/issues/33) Fix failing migrations on fresh database install

## 2.0.0 (2023-10-18)

* [#25](https://github.com/miaow2/netbox-config-diff/issues/25) Add configuration management
Expand Down
Binary file added docs/media/screenshots/cr-completed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/screenshots/cr-created.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ You can store credentials for devices authentification in NetBox secrets [plugin

Read NetBox secrets docs for more info.

In plugin variables define secrets roles for username (`USER_SECRET_ROLE`) and password (`PASSWORD_SECRET_ROLE`).
In plugin variables define secrets roles for username (`USER_SECRET_ROLE`), password (`PASSWORD_SECRET_ROLE`) and
password (`SECOND_AUTH_SECRET_ROLE`) for Privileged EXEC mode.

Default values for this variables are:

Expand All @@ -13,6 +14,7 @@ PLUGINS_CONFIG = {
"netbox_config_diff": {
"USER_SECRET_ROLE": "Username",
"PASSWORD_SECRET_ROLE": "Password",
"SECOND_AUTH_SECRET_ROLE": "Second Auth",
},
}
```
Expand Down
3 changes: 2 additions & 1 deletion netbox_config_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__author__ = "Artem Kotik"
__email__ = "[email protected]"
__version__ = "2.0.1"
__version__ = "2.1.0"


class ConfigDiffConfig(PluginConfig):
Expand All @@ -18,6 +18,7 @@ class ConfigDiffConfig(PluginConfig):
default_settings = {
"USER_SECRET_ROLE": "Username",
"PASSWORD_SECRET_ROLE": "Password",
"SECOND_AUTH_SECRET_ROLE": "Second Auth",
}


Expand Down
20 changes: 11 additions & 9 deletions netbox_config_diff/compliance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
from netutils.config.compliance import diff_network_config
from utilities.exceptions import AbortScript

from .models import DeviceDataClass
from netbox_config_diff.models import ConplianceDeviceDataClass

from .secrets import SecretsMixin
from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff

Expand Down Expand Up @@ -106,24 +107,24 @@ def validate_data(self, data: dict) -> Iterable[Device]:
self.log_info(f"Working with device(s): {', '.join(d.name for d in devices)}")
return devices

def update_in_db(self, devices: list[DeviceDataClass]) -> None:
def update_in_db(self, devices: list[ConplianceDeviceDataClass]) -> None:
for device in devices:
self.log_results(device)
device.send_to_db()

def log_results(self, device: DeviceDataClass) -> None:
def log_results(self, device: ConplianceDeviceDataClass) -> None:
if device.error:
self.log_failure(f"{device.name} errored")
elif device.diff:
self.log_warning(f"{device.name} has diff between intented and actual configurations")
else:
self.log_success(f"{device.name} no diff")

def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[DeviceDataClass]:
def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[ConplianceDeviceDataClass]:
self.check_netbox_secrets()
self.substitutes = {}
for device in devices:
username, password = self.get_credentials(device)
username, password, auth_secondary = self.get_credentials(device)
rendered_config = None
error = None
context_data = device.get_config_context()
Expand All @@ -142,7 +143,7 @@ def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterat
if substitutes := device.platform.platform_setting.substitutes.all():
self.substitutes[platform] = [s.regexp for s in substitutes]

yield DeviceDataClass(
yield ConplianceDeviceDataClass(
pk=device.pk,
name=device.name,
mgmt_ip=str(device.primary_ip.address.ip),
Expand All @@ -151,11 +152,12 @@ def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterat
exclude_regex=device.platform.platform_setting.exclude_regex,
username=username,
password=password,
auth_secondary=auth_secondary,
rendered_config=rendered_config,
error=error,
)

def get_config_from_datasource(self, devices: list[DeviceDataClass]) -> None:
def get_config_from_datasource(self, devices: list[ConplianceDeviceDataClass]) -> None:
for device in devices:
if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device.name).first():
if config := df.data_as_string:
Expand All @@ -165,14 +167,14 @@ def get_config_from_datasource(self, devices: list[DeviceDataClass]) -> None:
else:
device.error = f"Not found file in DataSource for device {device.name}"

def get_actual_configs(self, devices: list[DeviceDataClass]) -> None:
def get_actual_configs(self, devices: list[ConplianceDeviceDataClass]) -> None:
if self.data["data_source"]:
self.get_config_from_datasource(devices)
else:
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*(d.get_actual_config() for d in devices)))

def get_diff(self, devices: list[DeviceDataClass]) -> None:
def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None:
for device in devices:
if device.error is not None:
continue
Expand Down
39 changes: 24 additions & 15 deletions netbox_config_diff/compliance/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class SecretsMixin:

def get_session_key(self) -> None:
if "netbox_secrets_sessionid" in self.request.COOKIES:
self.session_key = base64.b64decode(self.request.COOKIES['netbox_secrets_sessionid'])
self.session_key = base64.b64decode(self.request.COOKIES["netbox_secrets_sessionid"])
elif "HTTP_X_SESSION_KEY" in self.request.META:
self.session_key = base64.b64decode(self.request.META['HTTP_X_SESSION_KEY'])
self.session_key = base64.b64decode(self.request.META["HTTP_X_SESSION_KEY"])
else:
self.session_key = None

Expand Down Expand Up @@ -45,24 +45,33 @@ def get_secret(self, secret: "Secret") -> str | None:
return None
return secret.plaintext

def get_credentials(self, device: Device) -> tuple[str, str]:
if self.netbox_secrets_installed:
if secret := device.secrets.filter(role__name=self.user_role).first():
if value := self.get_secret(secret):
username = value
if secret := device.secrets.filter(role__name=self.password_role).first():
if value := self.get_secret(secret):
password = value
return username, password
def get_credentials(self, device: Device) -> tuple[str, str, str]:
if not self.netbox_secrets_installed:
return self.username, self.password, self.auth_secondary

return self.username, self.password
if secret := device.secrets.filter(role__name=self.user_role).first():
username = value if (value := self.get_secret(secret)) else self.username
else:
username = self.username
if secret := device.secrets.filter(role__name=self.password_role).first():
password = value if (value := self.get_secret(secret)) else self.password
else:
password = self.password
if secret := device.secrets.filter(role__name=self.auth_secondary_role).first():
auth_secondary = value if (value := self.get_secret(secret)) else self.auth_secondary
else:
auth_secondary = self.auth_secondary

return username, password, auth_secondary

def check_netbox_secrets(self) -> None:
if "netbox_secrets" in get_installed_plugins():
self.get_master_key()
self.user_role = get_plugin_config("netbox_config_diff", "USER_SECRET_ROLE")
self.password_role = get_plugin_config("netbox_config_diff", "PASSWORD_SECRET_ROLE")
self.auth_secondary_role = get_plugin_config("netbox_config_diff", "SECOND_AUTH_SECRET_ROLE")
self.netbox_secrets_installed = True
else:
self.username = get_plugin_config("netbox_config_diff", "USERNAME")
self.password = get_plugin_config("netbox_config_diff", "PASSWORD")

self.username = get_plugin_config("netbox_config_diff", "USERNAME")
self.password = get_plugin_config("netbox_config_diff", "PASSWORD")
self.auth_secondary = get_plugin_config("netbox_config_diff", "AUTH_SECONDARY")
25 changes: 15 additions & 10 deletions netbox_config_diff/configurator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
from scrapli_cfg.response import ScrapliCfgResponse
from utilities.utils import NetBoxFakeRequest

from netbox_config_diff.compliance.models import DeviceDataClass
from netbox_config_diff.compliance.secrets import SecretsMixin
from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff
from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError
from netbox_config_diff.configurator.utils import CustomLogger
from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
from netbox_config_diff.models import ConfiguratorDeviceDataClass

from .factory import AsyncScrapliCfg

Expand All @@ -27,17 +27,17 @@ class Configurator(SecretsMixin):
def __init__(self, devices: Iterable[Device], request: NetBoxFakeRequest) -> None:
self.devices = devices
self.request = request
self.unprocessed_devices: set[DeviceDataClass] = set()
self.processed_devices: set[DeviceDataClass] = set()
self.failed_devices: set[DeviceDataClass] = set()
self.unprocessed_devices: set[ConfiguratorDeviceDataClass] = set()
self.processed_devices: set[ConfiguratorDeviceDataClass] = set()
self.failed_devices: set[ConfiguratorDeviceDataClass] = set()
self.substitutes: dict[str, list] = {}
self.logger = CustomLogger()
self.connections: dict[str, AsyncScrapliCfgPlatform] = {}

def validate_devices(self) -> None:
self.check_netbox_secrets()
for device in self.devices:
username, password = self.get_credentials(device)
username, password, auth_secondary = self.get_credentials(device)
if device.platform.platform_setting is None:
self.logger.log_warning(f"Skipping {device}, add PlatformSetting for {device.platform} platform")
elif device.platform.platform_setting.driver not in ACCEPTABLE_DRIVERS:
Expand All @@ -59,13 +59,14 @@ def validate_devices(self) -> None:
error = "Define config template for device"
self.logger.log_failure(error)

d = DeviceDataClass(
d = ConfiguratorDeviceDataClass(
pk=device.pk,
name=device.name,
mgmt_ip=str(device.primary_ip.address.ip),
platform=device.platform.platform_setting.driver,
username=username,
password=password,
auth_secondary=auth_secondary,
rendered_config=rendered_config,
error=error,
)
Expand Down Expand Up @@ -115,7 +116,7 @@ async def _collect_diffs(self) -> None:
await asyncio.gather(*(self._collect_one_diff(d) for d in self.unprocessed_devices))
await self.update_diffs()

async def _collect_one_diff(self, device: DeviceDataClass) -> None:
async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None:
self.logger.log_info(f"Collecting diff on {device.name}")
try:
conn = self.connections[device.name]
Expand Down Expand Up @@ -161,7 +162,7 @@ async def _push_configs(self) -> None:
devices=", ".join(f"{d.name}: {d.config_error}" for d in self.failed_devices),
)

async def _push_one_config(self, device: DeviceDataClass) -> None:
async def _push_one_config(self, device: ConfiguratorDeviceDataClass) -> None:
self.logger.log_info(f"Push config to {device.name}")
try:
conn = self.connections[device.name]
Expand All @@ -184,7 +185,11 @@ async def _push_one_config(self, device: DeviceDataClass) -> None:
self.failed_devices.add(device)

async def abort_config(
self, operation: str, conn: AsyncScrapliCfgPlatform, response: ScrapliCfgResponse, device: DeviceDataClass
self,
operation: str,
conn: AsyncScrapliCfgPlatform,
response: ScrapliCfgResponse,
device: ConfiguratorDeviceDataClass,
) -> None:
self.logger.log_failure(f"Failed to {operation} config on {device.name}: {response.result}")
device.config_error = response.result
Expand All @@ -197,7 +202,7 @@ async def rollback(self) -> None:
self.logger.log_info(f"Rollback config: {', '.join(d.name for d in self.processed_devices)}")
await asyncio.gather(*(self._rollback_one(d) for d in self.processed_devices))

async def _rollback_one(self, device: DeviceDataClass) -> None:
async def _rollback_one(self, device: ConfiguratorDeviceDataClass) -> None:
conn = self.connections[device.name]
await conn.load_config(config=device.actual_config, replace=True)
await conn.commit_config()
Expand Down
2 changes: 1 addition & 1 deletion netbox_config_diff/configurator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _log(self, message: str, log_level: str | None = None) -> None:
raise Exception(f"Unknown logging level: {log_level}")
if log_level is None:
log_level = LogLevelChoices.LOG_DEFAULT
self.log_data.append((timezone.now().strftime('%Y-%m-%d %H:%M:%S'), log_level, message))
self.log_data.append((timezone.now().strftime("%Y-%m-%d %H:%M:%S"), log_level, message))

def log(self, message: str) -> None:
self._log(message, log_level=LogLevelChoices.LOG_DEFAULT)
Expand Down
11 changes: 11 additions & 0 deletions netbox_config_diff/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .data_models import ConfiguratorDeviceDataClass, ConplianceDeviceDataClass
from .models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute

__all__ = (
"ConfigCompliance",
"ConfigurationRequest",
"ConfiguratorDeviceDataClass",
"ConplianceDeviceDataClass",
"PlatformSetting",
"Substitute",
)
10 changes: 10 additions & 0 deletions netbox_config_diff/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.db import models
from django.urls import reverse


class AbsoluteURLMixin(models.Model):
class Meta:
abstract = True

def get_absolute_url(self):
return reverse(f"plugins:netbox_config_diff:{self._meta.model_name}", args=[self.pk])
Loading