Skip to content

Commit

Permalink
Merge pull request #27 from SUNET/feature.changescore
Browse files Browse the repository at this point in the history
Feature.changescore
  • Loading branch information
krihal authored Aug 9, 2019
2 parents 48cc4df + c182e6f commit fec1cb7
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 3 deletions.
70 changes: 70 additions & 0 deletions src/cnaas_nms/confpush/changescore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import re


line_start = r"^[+-][ ]*"
line_start_remove = r"^-[ ]*"
DEFAULT_LINE_SCORE = 1.0
# Stops looking after first match. Only searches a single line at a time.
change_patterns = [
{
'name': 'description',
'regex': re.compile(str(line_start + r"description")),
'modifier': 0.0
},
{
'name': 'removed ip address',
'regex': re.compile(str(line_start_remove + r".*(ip address).*")),
'modifier': 10.0
},
{
'name': 'removed vlan',
'regex': re.compile(str(line_start_remove + r"vlan")),
'modifier': 10.0
},
{
'name': 'spanning-tree',
'regex': re.compile(str(line_start + r"spanning-tree")),
'modifier': 50.0
},
{
'name': 'removed routing',
'regex': re.compile(str(line_start_remove + r".*(routing|router).*")),
'modifier': 50.0
},
]
# TODO: multiline patterns / block-aware config


def calculate_line_score(line: str):
for pattern in change_patterns:
if re.match(pattern['regex'], line):
return 1 * pattern['modifier']
return DEFAULT_LINE_SCORE


def calculate_score(config: str, diff: str) -> float:
"""Calculate a score based on how much and what configurations were
changed in the diff.
Args:
config: the entire configuration of device after change
diff: the diff of what changed in the configuration
Returns:
Calculated score, can be much higher than 100.0
"""
config_lines = config.split('\n')
diff_lines = diff.split('\n')
changed_lines = 0
total_line_score = 0.0
for line in diff_lines:
if line.startswith('+') or line.startswith('-'):
changed_lines += 1
total_line_score += calculate_line_score(line)

changed_ratio = changed_lines / float(len(config_lines))

# Calculate score, 20% based on number of lines changed, 80% on individual
# line score with applied modifiers

return (changed_ratio*100*0.2) + (total_line_score*0.8)
1 change: 1 addition & 0 deletions src/cnaas_nms/confpush/nornir_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@dataclass
class NornirJobResult(JobResult):
nrresult: Optional[MultiResult] = None
change_score: Optional[float] = None


def cnaas_init():
Expand Down
30 changes: 29 additions & 1 deletion src/cnaas_nms/confpush/sync_devices.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional
from ipaddress import IPv4Interface
from statistics import median
import os
import yaml

Expand All @@ -11,6 +12,7 @@
import cnaas_nms.confpush.nornir_helper
from cnaas_nms.db.session import sqla_session
from cnaas_nms.confpush.get import get_uplinks, get_running_config_hash
from cnaas_nms.confpush.changescore import calculate_score
from cnaas_nms.tools.log import get_logger
from cnaas_nms.db.settings import get_settings
from cnaas_nms.db.device import Device, DeviceState, DeviceType
Expand Down Expand Up @@ -160,6 +162,12 @@ def push_sync_device(task, dry_run: bool = True, generate_only: bool = False):
dry_run=dry_run
)

change_score = 0
if task.results[1].diff:
config = task.results[1].host["config"]
diff = task.results[1].diff
task.host["change_score"] = calculate_score(config, diff)


def generate_only(hostname: str) -> (str, dict):
"""
Expand Down Expand Up @@ -240,6 +248,7 @@ def sync_devices(hostname: Optional[str] = None, device_type: Optional[str] = No
failed_hosts = list(nrresult.failed_hosts.keys())

if not dry_run:
# set new config hash and mark device synchronized
for key in nrresult.keys():
if key in failed_hosts:
continue
Expand All @@ -259,4 +268,23 @@ def sync_devices(hostname: Optional[str] = None, device_type: Optional[str] = No
if nrresult.failed:
logger.error("Not all devices were successfully synchronized")

return NornirJobResult(nrresult=nrresult)
total_change_score = 1
change_scores = []
# calculate change impact score
for host, results in nrresult.items():
if host in failed_hosts:
total_change_score = 100
break
change_scores.append(results[0].host["change_score"])
logger.debug("Change score for host {}: {}".format(
host, results[0].host["change_score"]))

if max(change_scores) > 1000:
# If some device has a score higher than this, disregard any averages
# and report max impact score
total_change_score = 100
else:
total_change_score = max(min(int(median(change_scores) + 0.5), 100), 1)
logger.info("Change impact score: {}".format(total_change_score))
# TODO: add field for change score in NornirJobResult object
return NornirJobResult(nrresult=nrresult, change_score=total_change_score)
3 changes: 3 additions & 0 deletions src/cnaas_nms/scheduler/jobtracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def finish_success(self, res: dict, next_job_id: Optional[str]):
try:
if isinstance(res, NornirJobResult) and isinstance(res.nrresult, AggregatedResult):
self.result = nr_result_serialize(res.nrresult)
self.result['_totals'] = {'selected_devices': len(res.nrresult)}
if res.change_score:
self.result['_totals']['change_score'] = res.change_score
elif isinstance(res, (StrJobResult, DictJobResult)):
self.result = res.result
else:
Expand Down
12 changes: 10 additions & 2 deletions test/integrationtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ def test_1_ztp(self):
self.assertFalse(result_step2['eosaccess'][0]['failed'],
"Could not reach device after ZTP")

def test_2_syncto(self):
def test_2_syncto_access(self):
r = requests.post(
f'{URL}/api/v1.0/device_syncto',
json={"hostname": "eosaccess", "dry_run": True},
verify=TLS_VERIFY
)
self.assertEqual(r.status_code, 200, "Failed to do sync_to")
self.assertEqual(r.status_code, 200, "Failed to do sync_to access")

def test_3_interfaces(self):
r = requests.get(
Expand All @@ -124,6 +124,14 @@ def test_3_interfaces(self):
)
self.assertEqual(r.status_code, 200, "Failed to update interface")

def test_4_syncto_dist(self):
r = requests.post(
f'{URL}/api/v1.0/device_syncto',
json={"hostname": "eosdist", "dry_run": True},
verify=TLS_VERIFY
)
self.assertEqual(r.status_code, 200, "Failed to do sync_to dist")


if __name__ == '__main__':
unittest.main()

0 comments on commit fec1cb7

Please sign in to comment.