Skip to content

Commit

Permalink
Merge pull request #185 from e-sonic/merge-0919/broadcom_sonic_4.x_sh…
Browse files Browse the repository at this point in the history
…are-to-dell_sonic_4.x_share

sync from broadcom_sonic_4.x_share to dell_sonic_4.x_share - 0919
  • Loading branch information
jeff-yin authored and GitHub Enterprise committed Sep 20, 2022
2 parents abea1a5 + ca9e257 commit 64ab93d
Show file tree
Hide file tree
Showing 81 changed files with 2,108 additions and 205 deletions.
74 changes: 62 additions & 12 deletions CLI/actioner/cli_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Bulk:
"""
Class to create a YANG Patch request
"""

def __init__(self, patch_id="", comment=None):
"""
Returns a payload template for YANG Patch request
Expand Down Expand Up @@ -125,8 +126,14 @@ def stop_timer(self, method, url):

self.request_timer_running -= 1

def set_headers(self):
return CaseInsensitiveDict({"User-Agent": "CLI"})
def get_headers(self):
headers = CaseInsensitiveDict({"User-Agent": "CLI"})
client_token = os.getenv("REST_API_TOKEN")
if client_token:
headers["Authorization"] = "Bearer " + client_token
if _ds_setter_func:
_ds_setter_func(headers)
return headers

def sig_handler(self, signum, frame):
# got interrupt, perform graceful termination
Expand All @@ -136,19 +143,12 @@ def request(self,
method,
path,
data=None,
headers={},
query=None,
response_type=None):

url = "{0}{1}".format(ApiClient._api_root, path)

req_headers = self.set_headers()
req_headers.update(headers)

client_token = os.getenv("REST_API_TOKEN", None)

if client_token:
req_headers["Authorization"] = "Bearer " + client_token
req_headers = self.get_headers()

yang_patch_request = False
if method == "YANG_PATCH":
Expand Down Expand Up @@ -191,11 +191,11 @@ def request(self,
self.stop_timer(method, url)

def post(self, path, data={}, response_type=None):
return self.request("POST", path, data, {}, None, response_type)
return self.request("POST", path, data, response_type=response_type)

def get(self, path, depth=None, ignore404=True, response_type=None):
q = self.prepare_query(depth=depth)
resp = self.request("GET", path, None, {}, q, response_type)
resp = self.request("GET", path, query=q, response_type=response_type)
if ignore404 and resp.status_code == 404:
resp.status_code = 200
resp.content = None
Expand Down Expand Up @@ -397,3 +397,53 @@ def add_error_prefix(err_msg):
if not err_msg.startswith("%Error"):
return "%Error: " + err_msg
return err_msg


def _default_ds_header(headers):
session_token = os.getenv("_session_token")
if session_token:
headers["X-SonicDS"] = "Session " + session_token


# _ds_setter_func sets the X-SonicDS header into the requests by ApiClient.
# Default implementation sets the current session token, if called from a
# config session mode. Can be overridden through DataStore context manager.
_ds_setter_func = _default_ds_header


class DataStore(object):
"""DataStore allows to temporarily override the X-SonicDS header used
by ApiClient requests. Affects existing ApiClient instances also.
Typical usage:
c = cli_client.ApiClient()
with cli_client.DataStore("running-config"):
c.get(.....)
with cli_client.DataStore("session", someSessionToken):
c.get(.....)
"""

def __init__(self, ds_type, ds_label=None):
"""Creates a DataStore object with given type and label.
ds_type : one of 'running-config', 'session' or 'checkpoint'.
ds_label : Session token or checkpoint id.
"""
self.ds_func_original = None
ds_type_ = ds_type.lower()
if ds_type_ == "running-config":
self.ds_func_override = None
elif ds_type_ in ["session", "checkpoint"]:
ds_value = f"{ds_type_} {ds_label}"
self.ds_func_override = lambda h: h.update({"X-SonicDS": ds_value})
else:
raise ValueError("unsupported ds_type: " + ds_type)

def __enter__(self):
global _ds_setter_func
self.ds_func_original = _ds_setter_func
_ds_setter_func = self.ds_func_override

def __exit__(self, exc_type, exc_val, exc_tb):
global _ds_setter_func
_ds_setter_func = self.ds_func_original
self.ds_func_original = None
100 changes: 100 additions & 0 deletions CLI/actioner/config_diff_infra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
################################################################################
# #
# Copyright 2021 Broadcom. The term Broadcom refers to Broadcom Inc. and/or #
# its subsidiaries. #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); #
# you may not use this file except in compliance with the License. #
# You may obtain a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# #
################################################################################

from difflib import SequenceMatcher

def _check_types(a, b, *args):
if a and not isinstance(a[0], str):
raise TypeError('lines to compare must be str, not %s (%r)' % (type(a[0]).__name__, a[0]))

if b and not isinstance(b[0], str):
raise TypeError('lines to compare must be str, not %s (%r)' % (type(b[0]).__name__, b[0]))

for arg in args:
if not isinstance(arg, str):
raise TypeError('all arguments must be str, not: %r' % (arg,))


def _count_leading_spaces(line):
return len(line) - len(line.lstrip(' '))


# To find the parent line of a given config line, we need to backtrace
# and determine first line whose indentation is less than given config line.
def _get_parent_line(line_index, config_lines):
if len(config_lines) > line_index:
idx = line_index
current_line = config_lines[idx]
while idx > 0:
prev_line = config_lines[idx-1]
if len(prev_line) == 1 and prev_line[0:] == '!':
return prev_line
if _count_leading_spaces(prev_line) < _count_leading_spaces(current_line):
return prev_line
idx = idx - 1

return ""

def evaluate_config_diff(a, b, fromfile='', tofile=''):
diff = []
_check_types(a, b)

print("--- {}".format(fromfile))
print("+++ {}".format(tofile))

config_lines_dict = {}
for opcode in SequenceMatcher(None, a, b).get_opcodes():
tag, i1, i2, j1, j2 = opcode

if tag == 'equal':
continue

if tag in {'replace', 'delete'}:
lidx = i1
for line in a[i1:i2]:
pl = _get_parent_line(lidx, a)
lidx += 1
if len(line) == 1 and line[0:] == '!':
continue
if pl[0:] == '!':
config_lines_dict[line] = True
diff.append('')
elif not pl in config_lines_dict:
config_lines_dict[pl] = True
diff.append('')
diff.append(pl)
diff.append('-' + line)

if tag in {'replace', 'insert'}:
lidx = j1
for line in b[j1:j2]:
pl = _get_parent_line(lidx, b)
lidx += 1
if len(line) == 1 and line[0:] == '!':
continue
if pl[0:] == '!':
config_lines_dict[line] = True
diff.append('')
elif not pl in config_lines_dict:
config_lines_dict[pl] = True
diff.append('')
diff.append(pl)
diff.append('+' + line)

return diff
141 changes: 141 additions & 0 deletions CLI/actioner/sessionctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
################################################################################
# #
# Copyright 2022 Broadcom. The term Broadcom refers to Broadcom Inc. and/or #
# its subsidiaries. #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); #
# you may not use this file except in compliance with the License. #
# You may obtain a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# #
################################################################################

import os
from cli_client import ApiClient
from syslog import syslog, LOG_WARNING


def run(name, args):
func = globals()["do_"+name]
kwargs = dict(v.split("=", maxsplit=1) if "=" in v else (v, True) for v in args)
return func(**kwargs)


def do_enter(name=""):
"""Action for 'configure session' command"""
payload = {"name": name}
res = _session_rpc("/internal/config-session/start", payload)
if not res:
return -1
name = res["name"]
os.environ.update({"_session_name": name, "_session_token": res["token"]})
if res.get("is-new", False):
print(f"% Started new session {name}")
else:
print(f"% Resumed session {name}")
return 0


def do_exit():
"""Action for 'exit' and 'end' commands"""
res = _session_rpc("/internal/config-session/exit")
if not res:
syslog(LOG_WARNING, "Could not confirm session exit at server side; but going ahead anyway..")
return 0 # exit anyway
print(f"% Exited session {res.get('name', '')}")
return 0


def do_abort():
"""Action for 'abort' command"""
banner = "WARNING: this operation will discard all changes done in this configuration session"
if not _confirm(banner):
return -2
res = _session_rpc("/internal/config-session/abort")
if not res:
return -1
print(f"% Aborted session {res.get('name', '')}")
return 0


def do_commit(label=None):
"""Action for 'commit [label XYZ]' command"""
req = {}
if label:
req["commit-label"] = label

res = _session_rpc("/internal/config-session/commit", req)
if not res:
return -1
chkpt = res.get("commit-id", "<unknown>")
print(f"% Committed session with checkpoint {chkpt}")
return 0


def do_status(template, name=""):
"""Action for 'show config session' commands"""
req = {"openconfig-session-rpc:input": {"name": name}}
res = _session_rpc("/restconf/operations/openconfig-session-rpc:get-config-session", req)
if not res:
return -1
if "openconfig-session-rpc:output" in res:
from scripts.render_cli import show_cli_output
show_cli_output(template, res["openconfig-session-rpc:output"])
return 0


def _confirm(msg):
opt = input(msg + "\nDo you want to continue? [Y/N] ")
return opt and opt.lower() in ["y", "yes"]


def do_config_diff(name=""):
token_found = False
session_token = ""
session_name = ""

# Get session details
req = {"openconfig-session-rpc:input": {"name": name}}
res = _session_rpc("/restconf/operations/openconfig-session-rpc:get-config-session", req)
if not res:
return -1

if "openconfig-session-rpc:output" in res:
sess_details = res["openconfig-session-rpc:output"]['config-session']
for cs in sess_details:
if name == cs['name']:
session_token = cs['token']
session_name = cs['name']
token_found = True
break

if token_found == True:
if len(session_name) == 0:
session_name = "(unnamed)"
from sonic_cli_show_config import render_config_diff
render_config_diff("running-config", session_token + ":" + session_name)
else:
print("%Error: Session '{}' is not available".format(name))


def _session_rpc(path, payload=None):
"""Invokes REST POST with given path & payload; returns the response body.
Prints the error message and returns None on error. Returns a dummy response
{"name": <SESSION_NAME>} if POST response had no/empty body.
"""
api = ApiClient()
res = api.post(path, data=payload)
if not res.ok():
print(res.error_message())
return None
if not res.content:
name = os.environ.get("_session_name", "")
return {"name": name}
return res.content
10 changes: 7 additions & 3 deletions CLI/actioner/sonic_cli_authmgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,10 +595,14 @@ def invoke_api(func, args):
return api.get(path)

elif func == 'rpc_openconfig_authmgr_clear_session':
if args[0] == 'all':
body = {"openconfig-authmgr-rpc:input": {"interface":args[0] }}
else:
if args[0] == 'interface':
if len(args) == 3:
body = {"openconfig-authmgr-rpc:input": {"interface":args[2] }}
else:
body = {"openconfig-authmgr-rpc:input": {"interface":args[1] }}
elif args[0] == 'mac':
body = {"openconfig-authmgr-rpc:input": {"interface":args[1] }}

keypath = cc.Path('/restconf/operations/openconfig-authmgr-rpc:clear-authmgr-sessions')
return api.post(keypath, body)

Expand Down
Loading

0 comments on commit 64ab93d

Please sign in to comment.