diff --git a/CLI/actioner/cli_client.py b/CLI/actioner/cli_client.py index a49ccbb5dc..778b258569 100755 --- a/CLI/actioner/cli_client.py +++ b/CLI/actioner/cli_client.py @@ -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 @@ -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 @@ -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": @@ -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 @@ -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 diff --git a/CLI/actioner/config_diff_infra.py b/CLI/actioner/config_diff_infra.py new file mode 100644 index 0000000000..9d13640adc --- /dev/null +++ b/CLI/actioner/config_diff_infra.py @@ -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 diff --git a/CLI/actioner/sessionctl.py b/CLI/actioner/sessionctl.py new file mode 100644 index 0000000000..147bb95187 --- /dev/null +++ b/CLI/actioner/sessionctl.py @@ -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", "") + 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": } 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 diff --git a/CLI/actioner/sonic_cli_authmgr.py b/CLI/actioner/sonic_cli_authmgr.py index 08c34d39c8..7b8dc4e789 100644 --- a/CLI/actioner/sonic_cli_authmgr.py +++ b/CLI/actioner/sonic_cli_authmgr.py @@ -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) diff --git a/CLI/actioner/sonic_cli_das.py b/CLI/actioner/sonic_cli_das.py index a0e759a852..5612515959 100644 --- a/CLI/actioner/sonic_cli_das.py +++ b/CLI/actioner/sonic_cli_das.py @@ -239,6 +239,22 @@ def get_sonic_das_client_counter_table_list(args): if len(dasClientsList) > 0: show_cli_output("show_das_client_counter_stats.j2", dasClientsList) +def get_das_running_config(): + import sonic_cli_show_config + response = cc.ApiClient().get(cc.Path('/restconf/data/sonic-das:sonic-das/DAS_GLOBAL_CONFIG_TABLE/DAS_GLOBAL_CONFIG_TABLE_LIST=GLOBAL')) + + # To display authentication command bounce-port ignore, authentication command disable-port global config commands + if (response and response.ok() and (response.content is not None) and ('sonic-das:DAS_GLOBAL_CONFIG_TABLE_LIST' in response.content)): + if "ignore_bounce_port" in response.content['sonic-das:DAS_GLOBAL_CONFIG_TABLE_LIST'][0] \ + and response.content['sonic-das:DAS_GLOBAL_CONFIG_TABLE_LIST'][0]['ignore_bounce_port']: + print("authentication command bounce-port ignore") + if "ignore_disable_port" in response.content['sonic-das:DAS_GLOBAL_CONFIG_TABLE_LIST'][0] \ + and response.content['sonic-das:DAS_GLOBAL_CONFIG_TABLE_LIST'][0]['ignore_disable_port']: + print("authentication command disable-port ignore") + + # To display configure-das view config comands + sonic_cli_show_config.run("show_view", ["views=configure-das"]) + def run(func, args): """ Main routine for Dynamic Authorization Server KLISH Actioner script @@ -252,6 +268,9 @@ def run(func, args): elif func == 'show_das_statistics': return get_das_stats_type(args) + elif func == 'show_das_running_config': + return get_das_running_config() + response = invoke_api(func, args) if response.ok(): diff --git a/CLI/actioner/sonic_cli_show_config.py b/CLI/actioner/sonic_cli_show_config.py index d852219861..c9c7d03a29 100644 --- a/CLI/actioner/sonic_cli_show_config.py +++ b/CLI/actioner/sonic_cli_show_config.py @@ -39,6 +39,7 @@ from show_config_utils import showrun_log from show_config_table_sort import natsort_list from collections import OrderedDict +from config_diff_infra import evaluate_config_diff CLI_XML_VIEW_MAP = {} @@ -1197,6 +1198,42 @@ def showconfig_views_to_buffer(viewlist): return output +def render_config_diff(from_fl, to_fl): + from_config = to_config = "" + from_sess_token = from_sess_name = "" + to_sess_token = to_sess_name = "" + + # To retrieve running-config, need to override Datastore to "running-config" in cli_client + if from_fl == "running-config": + from_sess_name = from_fl + with cc.DataStore("running-config"): + from_config = showconfig_views_to_buffer(viewlist=None) + else: + # from_fl in this case is session token-id:name + from_sess_token, from_sess_name = from_fl.split(":", 1) + with cc.DataStore("session", from_sess_token): + from_config = showconfig_views_to_buffer(viewlist=None) + + # To retrieve candidate-config, cli_client by default applies current session-id. + # There is no need to set Datastore in cli_client + if to_fl == "candidate-config": + to_sess_name = to_fl + to_config = showconfig_views_to_buffer(viewlist=None) + else: + # to_fl in this case is session token-id:name + to_sess_token, to_sess_name = to_fl.split(":", 1) + with cc.DataStore("session", to_sess_token): + to_config = showconfig_views_to_buffer(viewlist=None) + + if to_config != from_config: + diff_lines = evaluate_config_diff(from_config.split('\n'), to_config.split('\n'), fromfile=from_sess_name, tofile=to_sess_name) + + for line in diff_lines: + print(line) + + print('') + + def run(func="", args=[]): global format_read @@ -1218,6 +1255,8 @@ def run(func="", args=[]): if func == "show_view" or func == "show_multi_views": show_views(func, args) + elif func == "get_session_config_diff": + render_config_diff(args[0], args[1]) else: render_cli_config() cleanup() diff --git a/CLI/clitree/cli-xml/acl.xml b/CLI/clitree/cli-xml/acl.xml index b073e48501..d914298633 100644 --- a/CLI/clitree/cli-xml/acl.xml +++ b/CLI/clitree/cli-xml/acl.xml @@ -560,7 +560,7 @@ limitations under the License. - + @@ -643,7 +643,7 @@ limitations under the License. - + @@ -696,7 +696,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/authmgr.xml b/CLI/clitree/cli-xml/authmgr.xml index 4eb9c347a9..79c394b8a0 100644 --- a/CLI/clitree/cli-xml/authmgr.xml +++ b/CLI/clitree/cli-xml/authmgr.xml @@ -270,33 +270,54 @@ limitations under the License. - - - + + + - - - - sonic_cli_authmgr rpc_openconfig_authmgr_clear_session ${if-subcommands} ${port} + + + + + + + + + + sonic_cli_authmgr rpc_openconfig_authmgr_clear_session ${clear-session-opts} ${mac-addr} ${if-subcommands} ${port} diff --git a/CLI/clitree/cli-xml/bfd.xml b/CLI/clitree/cli-xml/bfd.xml index 0d7c16e112..0ccede6dd5 100644 --- a/CLI/clitree/cli-xml/bfd.xml +++ b/CLI/clitree/cli-xml/bfd.xml @@ -343,7 +343,7 @@ limitations under the License. @@ -520,7 +520,7 @@ limitations under the License. @@ -820,7 +820,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/bgp.xml b/CLI/clitree/cli-xml/bgp.xml index 11c9e2c6ff..076012b0fe 100644 --- a/CLI/clitree/cli-xml/bgp.xml +++ b/CLI/clitree/cli-xml/bgp.xml @@ -286,7 +286,7 @@ limitations under the License. - @@ -1376,7 +1376,7 @@ limitations under the License. - + @@ -2171,7 +2171,7 @@ limitations under the License. - + @@ -2926,7 +2926,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/bgp_af_ipv4.xml b/CLI/clitree/cli-xml/bgp_af_ipv4.xml index 5679d137a5..3522283b16 100644 --- a/CLI/clitree/cli-xml/bgp_af_ipv4.xml +++ b/CLI/clitree/cli-xml/bgp_af_ipv4.xml @@ -24,7 +24,7 @@ limitations under the License. > - + @@ -331,7 +331,7 @@ limitations under the License. - + @@ -959,7 +959,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/bgp_af_ipv6.xml b/CLI/clitree/cli-xml/bgp_af_ipv6.xml index c8e49d136b..446b39c4dc 100644 --- a/CLI/clitree/cli-xml/bgp_af_ipv6.xml +++ b/CLI/clitree/cli-xml/bgp_af_ipv6.xml @@ -24,7 +24,7 @@ limitations under the License. > - + @@ -156,7 +156,7 @@ limitations under the License. - + @@ -397,7 +397,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/bgp_af_l2vpn.xml b/CLI/clitree/cli-xml/bgp_af_l2vpn.xml index 7f1a0c8413..4ce3e879bb 100644 --- a/CLI/clitree/cli-xml/bgp_af_l2vpn.xml +++ b/CLI/clitree/cli-xml/bgp_af_l2vpn.xml @@ -27,7 +27,7 @@ limitations under the License. feature supported FEATURE_MULTI_SITE_DCI - + @@ -561,7 +561,7 @@ limitations under the License. - + @@ -836,7 +836,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/bgp_af_l2vpn_vni.xml b/CLI/clitree/cli-xml/bgp_af_l2vpn_vni.xml index 68d7540143..79b42dada1 100644 --- a/CLI/clitree/cli-xml/bgp_af_l2vpn_vni.xml +++ b/CLI/clitree/cli-xml/bgp_af_l2vpn_vni.xml @@ -24,7 +24,7 @@ limitations under the License. > - + diff --git a/CLI/clitree/cli-xml/config_session.xml b/CLI/clitree/cli-xml/config_session.xml new file mode 100644 index 0000000000..8a40505175 --- /dev/null +++ b/CLI/clitree/cli-xml/config_session.xml @@ -0,0 +1,273 @@ + + + + + + + + sessionctl enter + + + Creates or resumes a configuration session. It takes the shell to a config terminal + like mode, where all the config commands are available. This mode can be identified + through the shell prompt prefix "config-s". All configuration changes done from this + mode will not be saved to the running config immediately. They will be cached in the + management REST server until the session is committed or aborted. + + + * Only one configuration session can be open at any given time + * Only Users with administrator role can use this command + * Cannot resume configuration session created by other users + * Use "commit" command to save all changes done in the session to running config + * Use "abort" command to discard all changes done in the session + * Use "exit" and "end" commands to navigate out of the session mode without + committing/aborting it. Can be resumed later through "configure session" command + + + sonic# configure session + % Started new session + sonic(config-s)# + sonic(config-s)# exit + % Exited session + sonic# + + + sonic# configure session + % Resumed session + sonic(config-s)# + + + + + + + + sessionctl abort + + + This command terminates current configuration session by discarding all the + cached configuration changes made in the session. Prompts for confirmation. + + + sonic# configure session + % Resumed session + sonic(config-s)# abort + WARNING: this operation will discard all changes done in this configuration session + Do you want to continue? [Y/N] y + % Aborted session + sonic# + + + + + + + + + sessionctl commit label=${label_val} + + + Saves all cached configuration changes made in the session into config_db + and terminates the session. + + + sonic(config-s)# commit + % Committed session with checkpoint 1659637024-4 + + + sonic(config-s)# commit label mgmt_acl_cleanup + % Committed session with checkpoint 1659637024-5 + + + + + + + + + + sonic_cli_show_config dummy + + + Displays cached configuration changes made in the session and applied + configurations from running-configuration together. + + + sonic(config-s)# show session-config + ! + ip load-share hash ipv4 ipv4-src-ip + ip load-share hash ipv4 ipv4-dst-ip + ip load-share hash ipv4 ipv4-ip-proto + ip load-share hash ipv4 ipv4-l4-src-port + ... + ... + class class-oob-ipv6-multicast priority 1005 + police cir 256000 + ! + class class-oob-ip-multicast priority 1000 + police cir 256000 + ! + sonic(config-s)# + + + + + + sonic_cli_show_config get_session_config_diff running-config candidate-config + + + Displays difference between candidate-configuration and running-configuration. + + + sonic(config-s)# show session-config diff + --- running-config + +++ candidate-config + + ip access-list acl1 + - seq 1 permit tcp any any + + seq 5 permit udp host 12.11.13.12 any + + +interface Vlan11 + + mtu 4563 + + -interface Vlan101 + - mtu 3452 + + interface Ethernet0 + - mtu 5776 + + mtu 9100 + - ip address 101.3.4.1/24 + + ip address 3.4.2.3/24 + + sonic(config-s)# + + + + + + + + + + + + sessionctl status name=* template=show_cs_brief.j2 + + + Lists summary of all config sessions in a tabular format. + Each row includes name, state, age and owner username of a config session. + Age is displayed in the uptime format (`{}w{}d{}h` or `{}d{}h{}m` or `hh:mm:ss`). + + + sonic# show config session + Name State Age User + ------------------- --------- ---------- ---------- + (unnamed) Inactive 00:39:41 admin + + + + + + + sessionctl status name= template=show_cs_detail.j2 + + + Prints all properties of the unnamed config session. + + + sonic# show config session unnamed details + Session Name : (unnamed) + Session Token : 1660971655-6 + Session State : Active (PID 23598) + Created by : admin + Created at : 2022-08-20 10:30:55+0530 + Last Resumed at : + Last Exited at : 2022-08-20 10:57:27+0530 + Last Activity at : 2022-08-20 10:57:27+0530 + + + + + + sessionctl config_diff name= + + + Displays difference between running-configuration and unnamed session configuration. + + + sonic# show config session unnamed diff + --- running-config + +++ (unnamed) + + ip access-list acl1 + - seq 1 permit tcp any any + + seq 5 permit udp host 12.11.13.12 any + + +interface Vlan11 + + mtu 4563 + + -interface Vlan101 + - mtu 3452 + + interface Ethernet0 + - mtu 5776 + + mtu 9100 + - ip address 101.3.4.1/24 + + ip address 3.4.2.3/24 + + sonic(config-s)# + + + + + + + diff --git a/CLI/clitree/cli-xml/configure_mode.xml b/CLI/clitree/cli-xml/configure_mode.xml index 3cf21eff84..dc970de5a0 100644 --- a/CLI/clitree/cli-xml/configure_mode.xml +++ b/CLI/clitree/cli-xml/configure_mode.xml @@ -23,7 +23,7 @@ limitations under the License. http://www.dellemc.com/sonic/XMLSchema/clish.xsd" > - + diff --git a/CLI/clitree/cli-xml/copp.xml b/CLI/clitree/cli-xml/copp.xml index abd0269254..6b8d5e0d52 100644 --- a/CLI/clitree/cli-xml/copp.xml +++ b/CLI/clitree/cli-xml/copp.xml @@ -128,7 +128,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/das.xml b/CLI/clitree/cli-xml/das.xml index a07a738aed..00271a1b80 100644 --- a/CLI/clitree/cli-xml/das.xml +++ b/CLI/clitree/cli-xml/das.xml @@ -142,6 +142,56 @@ limitations under the License. + + + sonic_cli_das show_das_running_config + + + This command displays the all DAS running config. + + + This command displays the all DAS running config. + + + sonic# configure terminal + sonic(config)# authentication command + bounce-port disable-port + sonic(config)# authentication command bounce-port ignore + sonic(config)# authentication command disable-port ignore + sonic(config)# aaa server radius dynamic-author + sonic(config-radius-da)# + auth-type Sets the accepted authorization types for dynamic RADIUS clients. + client Configure DAC. + end Exit to EXEC mode + exit Exit from current mode + ignore Sets the switch to ignore certain authentication parameters from dynamic RADIUS clients. + no Unconfigure dynamic authorization. + port Sets the port on which to listen for requests from authorized dynamic RADIUS clients. + server-key Sets the shared secret to verify client COA requests for this server. + + sonic(config-radius-da)# auth-type session-key + sonic(config-radius-da)# client 1.1.1.1 server-key test + sonic(config-radius-da)# ignore server-key + sonic(config-radius-da)# ignore session-key + sonic(config-radius-da)# port 1212 + sonic(config-radius-da)# server-key 2.2.2.2 + + sonic(config-radius-da)# do show running-configuration das + authentication command bounce-port ignore + authentication command disable-port ignore + ! + aaa server radius dynamic-author + auth-type session-key + port 1212 + client 1.1.1.1 server-key U2FsdGVkX1+sVv1uXlOvwG7/17QGWv3GMQfDPizZ6O0= encrypted + ignore session-key + ignore server-key + server-key U2FsdGVkX19jHWn93adhy6NAG/DzAPYpuhn0UV14Kfw= encrypted + sonic(config-radius-da)# + + + + - - sonic# configure terminal - sonic(config)# tam - sonic(config-radius-da)# no auth-type - sonic(config-radius-da)# exit - sonic(config)# exit - sonic# show tam switch - - TAM Device information - ---------------------- - Switch ID : 51952 - Enterprise ID : 5678 - - sonic# - diff --git a/CLI/clitree/cli-xml/dropcounters.xml b/CLI/clitree/cli-xml/dropcounters.xml index 670d8d732f..9a0cec4772 100644 --- a/CLI/clitree/cli-xml/dropcounters.xml +++ b/CLI/clitree/cli-xml/dropcounters.xml @@ -119,7 +119,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/flow_based_services.xml b/CLI/clitree/cli-xml/flow_based_services.xml index ea932621e5..77299eb6fe 100644 --- a/CLI/clitree/cli-xml/flow_based_services.xml +++ b/CLI/clitree/cli-xml/flow_based_services.xml @@ -660,7 +660,7 @@ limitations under the License. - + @@ -737,7 +737,7 @@ limitations under the License. - + @@ -799,7 +799,7 @@ limitations under the License. - + @@ -1138,7 +1138,7 @@ limitations under the License. - + @@ -1177,7 +1177,7 @@ limitations under the License. - + @@ -1300,7 +1300,7 @@ limitations under the License. - + @@ -1363,7 +1363,7 @@ limitations under the License. - + @@ -1524,7 +1524,7 @@ limitations under the License. - + @@ -1570,7 +1570,7 @@ limitations under the License. - + @@ -1670,7 +1670,7 @@ limitations under the License. - + show_config_fbs show_running_next_hop_group_by_name ${fbs-nh-grp-name} @@ -1697,7 +1697,7 @@ limitations under the License. - + show_config_fbs show_running_next_hop_group_by_name ${fbs-nh-grp-name} @@ -1724,7 +1724,7 @@ limitations under the License. - + show_config_fbs show_running_replication_group_by_name ${fbs-repl-grp-name} @@ -1751,7 +1751,7 @@ limitations under the License. - + show_config_fbs show_running_replication_group_by_name ${fbs-repl-grp-name} diff --git a/CLI/clitree/cli-xml/hardware.xml b/CLI/clitree/cli-xml/hardware.xml index 476422bc2d..3a91bddadf 100644 --- a/CLI/clitree/cli-xml/hardware.xml +++ b/CLI/clitree/cli-xml/hardware.xml @@ -28,12 +28,12 @@ limitations under the License. - + - + @@ -43,7 +43,7 @@ limitations under the License. - + @@ -1527,7 +1527,7 @@ limitations under the License. @@ -1716,7 +1716,7 @@ limitations under the License. @@ -2086,7 +2086,7 @@ limitations under the License. @@ -2203,7 +2203,7 @@ limitations under the License. @@ -497,7 +497,7 @@ limitations under the License. @@ -831,7 +831,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/line_vty.xml b/CLI/clitree/cli-xml/line_vty.xml index aac6c7a533..e4ecd11b73 100644 --- a/CLI/clitree/cli-xml/line_vty.xml +++ b/CLI/clitree/cli-xml/line_vty.xml @@ -29,7 +29,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/link_track.xml b/CLI/clitree/cli-xml/link_track.xml index 09f6cf5c7c..06b74d2391 100644 --- a/CLI/clitree/cli-xml/link_track.xml +++ b/CLI/clitree/cli-xml/link_track.xml @@ -69,7 +69,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/mclag.xml b/CLI/clitree/cli-xml/mclag.xml index d8b5d3ba51..9b4e5932c6 100644 --- a/CLI/clitree/cli-xml/mclag.xml +++ b/CLI/clitree/cli-xml/mclag.xml @@ -53,7 +53,7 @@ - + diff --git a/CLI/clitree/cli-xml/nat.xml b/CLI/clitree/cli-xml/nat.xml index ea04409007..702a9c0227 100644 --- a/CLI/clitree/cli-xml/nat.xml +++ b/CLI/clitree/cli-xml/nat.xml @@ -51,7 +51,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/ospf.xml b/CLI/clitree/cli-xml/ospf.xml index f769706dc7..46bd986315 100644 --- a/CLI/clitree/cli-xml/ospf.xml +++ b/CLI/clitree/cli-xml/ospf.xml @@ -70,7 +70,7 @@ - + diff --git a/CLI/clitree/cli-xml/poe.xml b/CLI/clitree/cli-xml/poe.xml index 569814e3d3..4381e88089 100644 --- a/CLI/clitree/cli-xml/poe.xml +++ b/CLI/clitree/cli-xml/poe.xml @@ -138,7 +138,7 @@ - + sonic_cli_show_config show_view views=configure-poe-card view_keys="id=${id}" diff --git a/CLI/clitree/cli-xml/qos_map_dot1p.xml b/CLI/clitree/cli-xml/qos_map_dot1p.xml index b95e72c814..105e2630a9 100644 --- a/CLI/clitree/cli-xml/qos_map_dot1p.xml +++ b/CLI/clitree/cli-xml/qos_map_dot1p.xml @@ -85,7 +85,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_dscp.xml b/CLI/clitree/cli-xml/qos_map_dscp.xml index 4add07fdd1..b08edd97b6 100644 --- a/CLI/clitree/cli-xml/qos_map_dscp.xml +++ b/CLI/clitree/cli-xml/qos_map_dscp.xml @@ -106,7 +106,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_pfc_queue.xml b/CLI/clitree/cli-xml/qos_map_pfc_queue.xml index 78e44fe3bb..7955f19b53 100644 --- a/CLI/clitree/cli-xml/qos_map_pfc_queue.xml +++ b/CLI/clitree/cli-xml/qos_map_pfc_queue.xml @@ -85,7 +85,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_tc_dot1p.xml b/CLI/clitree/cli-xml/qos_map_tc_dot1p.xml index bbbfc0558a..6456c5d54d 100644 --- a/CLI/clitree/cli-xml/qos_map_tc_dot1p.xml +++ b/CLI/clitree/cli-xml/qos_map_tc_dot1p.xml @@ -86,7 +86,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_tc_dscp.xml b/CLI/clitree/cli-xml/qos_map_tc_dscp.xml index ec8c530ee6..aa4cf95424 100644 --- a/CLI/clitree/cli-xml/qos_map_tc_dscp.xml +++ b/CLI/clitree/cli-xml/qos_map_tc_dscp.xml @@ -86,7 +86,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_tc_pg.xml b/CLI/clitree/cli-xml/qos_map_tc_pg.xml index da27d540a5..ee1f0dcdfe 100644 --- a/CLI/clitree/cli-xml/qos_map_tc_pg.xml +++ b/CLI/clitree/cli-xml/qos_map_tc_pg.xml @@ -85,7 +85,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_map_tc_queue.xml b/CLI/clitree/cli-xml/qos_map_tc_queue.xml index 8c30d329c8..a58fc32314 100644 --- a/CLI/clitree/cli-xml/qos_map_tc_queue.xml +++ b/CLI/clitree/cli-xml/qos_map_tc_queue.xml @@ -85,7 +85,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/qos_scheduler.xml b/CLI/clitree/cli-xml/qos_scheduler.xml index aa13ab34be..ad2deb86ab 100644 --- a/CLI/clitree/cli-xml/qos_scheduler.xml +++ b/CLI/clitree/cli-xml/qos_scheduler.xml @@ -58,7 +58,7 @@ limitations under the License. @@ -136,7 +136,7 @@ limitations under the License. @@ -304,7 +304,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/routemap.xml b/CLI/clitree/cli-xml/routemap.xml index 705f193977..f6153fd051 100644 --- a/CLI/clitree/cli-xml/routemap.xml +++ b/CLI/clitree/cli-xml/routemap.xml @@ -124,7 +124,7 @@ limitations under the License. - diff --git a/CLI/clitree/cli-xml/stp.xml b/CLI/clitree/cli-xml/stp.xml index c7b0dad72c..66e7d2883c 100644 --- a/CLI/clitree/cli-xml/stp.xml +++ b/CLI/clitree/cli-xml/stp.xml @@ -1513,7 +1513,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/subinterface.xml b/CLI/clitree/cli-xml/subinterface.xml index fdaeec1199..d45c54162f 100644 --- a/CLI/clitree/cli-xml/subinterface.xml +++ b/CLI/clitree/cli-xml/subinterface.xml @@ -125,7 +125,7 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/telemetry.xml b/CLI/clitree/cli-xml/telemetry.xml index 1ff965e4f1..bc8424ce9e 100644 --- a/CLI/clitree/cli-xml/telemetry.xml +++ b/CLI/clitree/cli-xml/telemetry.xml @@ -400,7 +400,7 @@ limitations under the License. - + @@ -739,7 +739,7 @@ limitations under the License. - + @@ -871,7 +871,7 @@ limitations under the License. - + @@ -1064,7 +1064,7 @@ limitations under the License. - + diff --git a/CLI/clitree/cli-xml/vrrp.xml b/CLI/clitree/cli-xml/vrrp.xml index a782c02152..f74385e62a 100644 --- a/CLI/clitree/cli-xml/vrrp.xml +++ b/CLI/clitree/cli-xml/vrrp.xml @@ -314,14 +314,14 @@ limitations under the License. diff --git a/CLI/clitree/cli-xml/vxlan.xml b/CLI/clitree/cli-xml/vxlan.xml index 1da79f4e14..139bc37503 100644 --- a/CLI/clitree/cli-xml/vxlan.xml +++ b/CLI/clitree/cli-xml/vxlan.xml @@ -382,7 +382,7 @@ EVPN_4.4.4.5 0 0 0 N diff --git a/CLI/clitree/cli-xml/wred.xml b/CLI/clitree/cli-xml/wred.xml index 012ac3ba52..b5fcf8bb6a 100644 --- a/CLI/clitree/cli-xml/wred.xml +++ b/CLI/clitree/cli-xml/wred.xml @@ -106,7 +106,7 @@ diff --git a/CLI/clitree/scripts/klish_ins_def_cmd.py b/CLI/clitree/scripts/klish_ins_def_cmd.py index 1a8070abae..1b17c682c1 100755 --- a/CLI/clitree/scripts/klish_ins_def_cmd.py +++ b/CLI/clitree/scripts/klish_ins_def_cmd.py @@ -76,7 +76,9 @@ END_CMD = """""" + view="enable-view"> + + """ VIEW_TAG_STR = """{http://www.dellemc.com/sonic/XMLSchema}VIEW""" ENABLE_VIEW_STR = """enable-view""" diff --git a/CLI/klish/Makefile b/CLI/klish/Makefile index 99690bc4e0..60754c62d7 100644 --- a/CLI/klish/Makefile +++ b/CLI/klish/Makefile @@ -8,17 +8,25 @@ LIBCJSON_PATH ?= /usr/lib KLISH_SRC = $(SONIC_CLI_ROOT)/klish-$(KLISH_VERSION) -KLISH_LIBS ?= -l:libcurl-gnutls.so.4 -l:bash_tacplus.so -lpython$(PYTHONVER) $(LIBCJSON_PATH)/libcjson.a +KLISH_LIBS ?= -l:libcurl-gnutls.so.4 -lpython$(PYTHONVER) $(LIBCJSON_PATH)/libcjson.a KLISH_CFLAGS ?= $(CFLAGS) -g -I/usr/include/python$(PYTHONVER) KLISH_CXXFLAGS ?= $(CXXFLAGS) -KLISH_LDFLAGS ?= $(LDFLAGS) -L/usr/lib/x86_64-linux-gnu/security +KLISH_LDFLAGS ?= $(LDFLAGS) + +ifdef BASH_TACPLUS_VERSION +$(info Using bash_tacplus.so, v$(BASH_TACPLUS_VERSION)) +KLISH_LIBS += -l:bash_tacplus.so +KLISH_CFLAGS += -DHAVE_TACPLUS_LIB +KLISH_LDFLAGS += -L/usr/lib/x86_64-linux-gnu/security +endif SRC_REPLACEMENTS:=$(shell find patches -type f) all : $(SRC_REPLACEMENTS) tar xzvf klish-$(KLISH_VERSION).tgz -C $(SONIC_CLI_ROOT) ./patches/scripts/patchmake.sh -p VER=${KLISH_VERSION} TSP=${SONIC_CLI_ROOT} DSP=${CURDIR}/patches TWP=${SONIC_CLI_ROOT} - cd ${KLISH_SRC} && export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/security && sh autogen.sh && ./configure --with-libxml2=/usr --enable-debug=no LIBS='$(KLISH_LIBS)' CFLAGS='$(KLISH_CFLAGS)' CXXFLAGS='$(KLISH_CXXFLAGS)' LDFLAGS='$(KLISH_LDFLAGS)' && make + export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):/usr/lib/x86_64-linux-gnu/security + cd ${KLISH_SRC} && sh autogen.sh && ./configure --with-libxml2=/usr --enable-debug=no LIBS='$(KLISH_LIBS)' CFLAGS='$(KLISH_CFLAGS)' CXXFLAGS='$(KLISH_CXXFLAGS)' LDFLAGS='$(KLISH_LDFLAGS)' && make mkdir -p $(SONIC_CLI_ROOT)/target/.libs cp $(CURDIR)/clish_start $(SONIC_CLI_ROOT)/target/. diff --git a/CLI/klish/patches/README b/CLI/klish/patches/README new file mode 100644 index 0000000000..328cb627c4 --- /dev/null +++ b/CLI/klish/patches/README @@ -0,0 +1,66 @@ +Customizing klish: + +Sonic customizations to opensource klish are maintained as patches under +CLI/klish/patches/klish-2.1.4 directory. All changes to a base code file +are maintained in a {filepath}.diff file. New source files are kept as +is (i.e, as .h, .c or .cpp files). + +Minor changes to such new source files can be done directly. But it is +not practical (and dangerous) to modify the diff files. Developer should +extract the base files to a working directory, apply the patches, make +his changes in the working directory, build & test if needed and +finally bring the changes back in the aforementioned format. There are +scripts to assist in this process. + +unpack.sh WORKDIR [-b BRANCH] + * Extract base code CLI/klish/klish-2.1.4.tgz to a work directory + which also will be a git repo + * Apply a tag BASE on the base code + * Apply all the patches from CLI/klish/patches/klish-2.1.4/... + * Commit them to a new branch (in WORKDIR repo) + +repack.sh WORKDIR + * Import changes from staged & untracked files in WORKDIR repo into + CLI/klish/patches/klish-2.1.4/... + * If the changed file was present in base code, diff from BASE tag + is copied; otherwise the whole file is copied + + +Typical workflow for making a change in klish code: + * cd {mgmt-framework}/CLI/klish/patches + * ./scripts/unpack.sh ~/tools/klish + * cd ~/tools/klish/klish-2.1.4 + * Do the changes, but do not commit + * ./build.sh + * Do quick tests + * cd {mgmt-framework}/CLI/klish/patches + * ./scripts/repack.sh ~/tools/klish + * Build and test the sonic-cli + +Typical workflow for resolving branch merge conflicts: + * cd {mgmt-framework}/CLI/klish/patches + * git checkout {source_ver/branch} + * /scripts/unpack.sh ~/tools/klish -b merge_src + * git checkout {target_ver/branch} + * /scripts/unpack.sh ~/tools/klish -b merge_tgt + * cd ~/tools/klish/klish-2.1.4 + * git checkout merge_tgt + * git merge --squash --no-commit merge_src + * Resolve conflicts; can do git add, but do not commit + * ./build.sh + * Do quick tests if needed + * cd {mgmt-framework}/CLI/klish/patches + * ./scripts/repack.sh ~/tools/klish + * Build and test the sonic-cli + + +For quick testing of klish changes, the test sonic-cli can be made to +use the clish binary from the work directory itself. + * cd {mgmt-framework} + * export KLISH_BIN=~/tools/klish/klish-2.1.4/bin + * tools/test/cli.sh + * unset KLISH_BIN (after all tests are done) + +Note: ~/tools/klish is used as the work directory in all the above +examples. Replace with your favorite work directory. + diff --git a/CLI/klish/patches/klish-2.1.4/build.sh b/CLI/klish/patches/klish-2.1.4/build.sh new file mode 100755 index 0000000000..c3a9246e9d --- /dev/null +++ b/CLI/klish/patches/klish-2.1.4/build.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -e + +while [[ $# -gt 0 ]]; do +case "$1" in + clean|--clean) CLEAN=y; shift;; + syslogtostdout|--syslogtostdout) SYSLOGTOSTDOUT=y; shift ;; + *) echo "error: unknown option: $1"; exit 1;; +esac +done + +PYTHONVER=${PYTHONVER:-3.7m} +LIBCJSON_PATH=${LIBCJSON_PATH:-/usr/lib} + +if [[ -z ${KLISH_LIBS} ]]; then + KLISH_LIBS+=" -l:libcurl-gnutls.so.4" + KLISH_LIBS+=" -lpython${PYTHONVER}" + KLISH_LIBS+=" ${LIBCJSON_PATH}/libcjson.a" +fi + +if [[ -z ${KLISH_CFLAGS} ]]; then + KLISH_CFLAGS+=" ${CFLAGS} -g" + KLISH_CFLAGS+=" -I/usr/include/python${PYTHONVER}" +fi + +if [[ ${SYSLOGTOSTDOUT} == y ]]; then + KLISH_CFLAGS+=" -DSYSLOG_TO_STDOUT" +fi + +if [[ ${CLEAN} == y ]]; then + make clean +fi + +sh autogen.sh + +./configure --with-libxml2=/usr \ + --enable-debug=no \ + LIBS="${KLISH_LIBS}" \ + CFLAGS="${KLISH_CFLAGS}" \ + CXXFLAGS="${KLISH_CXXFLAGS:-${CXXFLAGS}}" \ + LDFLAGS="${KLISH_LDFLAGS:-${LDFLAGS}}" \ + +make + diff --git a/CLI/klish/patches/klish-2.1.4/clish/shell.h.diff b/CLI/klish/patches/klish-2.1.4/clish/shell.h.diff index 872656365f..96f75ee1c3 100644 --- a/CLI/klish/patches/klish-2.1.4/clish/shell.h.diff +++ b/CLI/klish/patches/klish-2.1.4/clish/shell.h.diff @@ -8,5 +8,11 @@ < void clish_shell_help(clish_shell_t * instance, const char *line); --- > void clish_shell_help(clish_shell_t * instance, const char *line, clish_context_t *context); +122,123c124,125 +< void clish_shell_insert_var(clish_shell_t *instance, clish_var_t *var); +< clish_var_t *clish_shell_find_var(clish_shell_t *instance, const char *name); +--- +> +> clish_var_t *clish_shell_set_var(clish_shell_t *shell, const char *name, const char *save_val); 190a193 > void clish_shell__set_timeout(clish_shell_t *instance, int timeout); diff --git a/CLI/klish/patches/klish-2.1.4/clish/shell/shell_var.c.diff b/CLI/klish/patches/klish-2.1.4/clish/shell/shell_var.c.diff new file mode 100644 index 0000000000..c8f88702a7 --- /dev/null +++ b/CLI/klish/patches/klish-2.1.4/clish/shell/shell_var.c.diff @@ -0,0 +1,26 @@ +diff --git a/clish/shell/shell_var.c b/clish/shell/shell_var.c +index 3bc4866..9053307 100644 +--- a/clish/shell/shell_var.c ++++ b/clish/shell/shell_var.c +@@ -537,3 +537,21 @@ char *clish_shell_expand_var_ex(const char *name, clish_context_t *context, clis + } + + /*----------------------------------------------------------- */ ++/* ++ * clish_shell_set_var sets the "saved" attribute of a shell variable. ++ * Creates a new clish_var_t entry if there was no such variable in the shell. ++ */ ++clish_var_t *clish_shell_set_var(clish_shell_t *shell, const char *name, const char *save_val) ++{ ++ clish_var_t *var = (clish_var_t *)lub_bintree_find(&shell->var_tree, name); ++ if (var == NULL) ++ { ++ var = clish_var_new(name); ++ lub_bintree_insert(&shell->var_tree, var); ++ } ++ ++ clish_var__set_saved(var, save_val); ++ return var; ++} ++ ++/*----------------------------------------------------------- */ diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/builtin_init.c.diff b/CLI/klish/patches/klish-2.1.4/plugins/clish/builtin_init.c.diff index 4af96f9eeb..fda53837a5 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/builtin_init.c.diff +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/builtin_init.c.diff @@ -8,10 +8,15 @@ > clish_plugin_add_psym(plugin, clish_pyobj, "clish_pyobj"); > clish_plugin_add_psym(plugin, clish_setenv, "clish_setenv"); > clish_plugin_add_psym(plugin, clish_set_idle_timeout, "clish_set_idle_timeout"); -> -> nos_extn_init(); -38a48,51 +> clish_plugin_add_psym(plugin, clish_start_session, "clish_start_session"); +> clish_plugin_add_psym(plugin, clish_exit_session, "clish_exit_session"); +34c43 +< clish_shell = clish_shell; /* Happy compiler */ +--- +> nos_extn_init(clish_shell); +38a48,52 > CLISH_PLUGIN_FINI(clish_plugin_clish_fini) > { -> nos_extn_fini(); +> nos_extn_fini(clish_shell); +> return 0; > } diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/call_pyobj.c b/CLI/klish/patches/klish-2.1.4/plugins/clish/call_pyobj.c index f9f91d5777..e05fe30a24 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/call_pyobj.c +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/call_pyobj.c @@ -93,6 +93,7 @@ int pyobj_update_environ(const char *key, const char *val) { return -1; } + int result = 0; PyObject *dict = PyModule_GetDict(module); PyObject *env_obj = PyDict_GetItemString(dict, "environ"); @@ -105,11 +106,11 @@ int pyobj_update_environ(const char *key, const char *val) { PyObject *args = PyTuple_New(1); PyTuple_SetItem(args, 0, pMap); - PyObject_CallObject(func, args); + PyObject *resp = PyObject_CallObject(func, args); if (PyErr_Occurred()) { pyobj_handle_error(); - return 1; + result = 1; } Py_XDECREF(module); @@ -117,8 +118,9 @@ int pyobj_update_environ(const char *key, const char *val) { Py_XDECREF(v_obj); Py_XDECREF(pMap); Py_XDECREF(args); + Py_XDECREF(resp); - return 0; + return result; } static int pyobj_set_user_cmd(const char *cmd) { diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_log.c.diff b/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_log.c.diff index 14afec2ec8..b7184caae0 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_log.c.diff +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_log.c.diff @@ -1,14 +1,34 @@ -11d10 +7a8 +> #include +11d11 < #include -13a13 +13a14,15 > #include "clish/plugin/mgmt_clish_utils.h" -15c15 +> #include "session.h" +15c17,34 < #define SYSLOG_IDENT "klish" --- > #define SYSLOG_IDENT "clish" -21d20 +> +> static void format_session_prefix(clish_context_t *ctx, char *buff, int cap) +> { +> const char *sname = get_session_token(ctx); +> memset(buff, 0, cap); +> if (sname == NULL) { +> return; +> } +> +> int n = snprintf(buff, cap, "[Session %s] ", sname); +> if (n >= cap) { +> // force write "] " suffix if the session name is too big +> buff[cap-4] = '*'; // truncation marker +> buff[cap-3] = ']'; +> buff[cap-2] = ' '; +> } +> } +21d39 < struct passwd *user = NULL; -31,41c30,46 +31,41c49,74 < /* Log the given line */ < /* Try to get username from environment variables < * USER and LOGNAME and then from /etc/passwd. @@ -33,8 +53,17 @@ > char u[32]; > strncpy(u, uname, sizeof(u) -1); > u[sizeof(u)-1] = '\0'; -> syslog(LOG_INFO|facility, "User \"%s\" command \"%s\" status - %s", -> u, masked_line, retcode ? "failure" : "success"); +> +> char pfx[32]; +> format_session_prefix(clish_context, pfx, sizeof(pfx)); +> +> syslog(LOG_INFO|facility, "%sUser \"%s\" command \"%s\" status - %s", +> pfx, u, masked_line, retcode ? "failure" : "success"); > free(masked_line); > masked_line = NULL; > } +> +> // Post action housekeeping for config session.. +> // Ignoring the return code -- we cannot do much here +> // as the command action is already done by this time. +> session_post_action_hook(clish_context); diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_pre_exec.c b/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_pre_exec.c index 928e3a9ebc..266261afef 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_pre_exec.c +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/hook_pre_exec.c @@ -19,6 +19,7 @@ #include #include "clish/shell.h" +#include "session.h" #include "clish/plugin/mgmt_clish_utils.h" #include "logging.h" #include "lub/dump.h" @@ -26,7 +27,11 @@ #define PRE_EXEC_MAX_NARGS 64 +#ifdef HAVE_TACPLUS_LIB int on_shell_execve (char *user, int shell_level, char *cmd, char **argv); +#else +static int on_shell_execve (char *user, int shell_level, char *cmd, char **argv) { return 0; } +#endif int check_ignore_user_list(char *user) { /* usr admin is always locally authenticated and never sent to TACACS+ server*/ @@ -49,7 +54,7 @@ int check_ignore_cmd_list(char *cmd) { return 0; } -CLISH_HOOK_PREEXEC(clish_hook_pre_exec) +int authorize_command(clish_context_t *clish_context) { int result = 0; char *argv[PRE_EXEC_MAX_NARGS + 2] = {0}, *cmd; @@ -119,3 +124,18 @@ CLISH_HOOK_PREEXEC(clish_hook_pre_exec) return result; } + +CLISH_HOOK_PREEXEC(clish_hook_pre_exec) +{ + int result = 0; + + if (result == 0) { + result = authorize_command(clish_context); + } + if (result == 0) { + result = session_pre_action_hook(clish_context); + } + + return result; +} + diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/module.am.diff b/CLI/klish/patches/klish-2.1.4/plugins/clish/module.am.diff index 1d75950f4e..f0991cc214 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/module.am.diff +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/module.am.diff @@ -1,6 +1,7 @@ 19a20 > plugins/clish/hook_pre_exec.c \ -20a22,24 +20a22,25 > plugins/clish/rest_cl.cpp \ > plugins/clish/call_pyobj.c \ > plugins/clish/nos_extn.c \ +> plugins/clish/session.c \ diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.c b/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.c index eb49ae003b..9013e10e1a 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.c +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.c @@ -20,6 +20,7 @@ #include "private.h" #include "nos_extn.h" +#include "session.h" #include "lub/string.h" #include "clish/shell/private.h" #include @@ -92,13 +93,25 @@ int clish_rest_thread_init() { return 0; } +/* Obtain lock for using pyobj and rest_cl tools */ +void nos_extn_lock() +{ + pthread_mutex_lock(&lock); +} + +/* Release lock for pyobj and rest_cl */ +void nos_extn_unlock() +{ + pthread_mutex_unlock(&lock); +} + CLISH_PLUGIN_SYM(clish_restcl) { char *cmd = clish_shell__get_full_line(clish_context); pthread_mutex_lock(&lock); - int ret = rest_cl(cmd, script); + int ret = rest_cl(cmd, script, NULL); pthread_mutex_unlock(&lock); @@ -153,7 +166,9 @@ CLISH_PLUGIN_SYM(clish_set_idle_timeout) return 0; } -void nos_extn_fini() { +void nos_extn_fini(clish_shell_t *shell) +{ + reset_session_context(shell); pthread_cancel(thread_id); @@ -161,20 +176,22 @@ void nos_extn_fini() { pthread_join(thread_id, NULL); } -void nos_extn_init() { - +void nos_extn_init(clish_shell_t *shell) +{ pthread_mutex_init(&lock, NULL); int auth_ena = (getenv("CLISH_NOAUTH") == NULL); rest_client_init(); pyobj_init(); - + if (auth_ena) { clish_rest_thread_init(); } - + if (!auth_ena) { syslog(LOG_WARNING, "CLISH running with auth disabled"); } + + reset_session_context(shell); } diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.h b/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.h index d43f6c53cf..dc10634b07 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.h +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/nos_extn.h @@ -7,8 +7,11 @@ extern "C" { #endif extern void pyobj_init(); -extern void nos_extn_init(); -extern void nos_extn_fini(); +extern void nos_extn_init(clish_shell_t *shell); +extern void nos_extn_fini(clish_shell_t *shell); + +extern void nos_extn_lock(); +extern void nos_extn_unlock(); extern int call_pyobj(char *cmd, const char *buff, char **out); extern int pyobj_set_rest_token(const char*); @@ -16,7 +19,8 @@ extern int pyobj_update_environ(const char *key, const char *val); extern void rest_client_init(); extern int rest_token_fetch(int *interval); -extern int rest_cl(char *cmd, const char *buff); +extern int rest_set_session_token(const char *token); +extern int rest_cl(char *cmd, const char *buff, char **o_resp); #ifdef __cplusplus } diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/private.h.diff b/CLI/klish/patches/klish-2.1.4/plugins/clish/private.h.diff index 909938b9ba..2b4960295a 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/private.h.diff +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/private.h.diff @@ -1,7 +1,9 @@ 11a12 > CLISH_HOOK_PREEXEC(clish_hook_pre_exec); -23a25,28 +23a25,30 > CLISH_PLUGIN_SYM(clish_restcl); > CLISH_PLUGIN_SYM(clish_pyobj); > CLISH_PLUGIN_SYM(clish_setenv); > CLISH_PLUGIN_SYM(clish_set_idle_timeout); +> CLISH_PLUGIN_SYM(clish_start_session); +> CLISH_PLUGIN_SYM(clish_exit_session); diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/rest_cl.cpp b/CLI/klish/patches/klish-2.1.4/plugins/clish/rest_cl.cpp index 2081238b0f..cb9b4457cd 100644 --- a/CLI/klish/patches/klish-2.1.4/plugins/clish/rest_cl.cpp +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/rest_cl.cpp @@ -92,12 +92,14 @@ int print_error(const char *str) { cJSON *ietf_err = cJSON_GetObjectItemCaseSensitive(ret_json, "ietf-restconf:errors"); if (!ietf_err) { syslog(LOG_DEBUG, "clish_restcl: No errors\r\n"); + cJSON_free(ret_json); return 0; } cJSON *errors = cJSON_GetObjectItemCaseSensitive(ietf_err, "error"); if (!errors) { syslog(LOG_DEBUG, "clish_restcl: No error\r\n"); + cJSON_free(ret_json); return 0; } @@ -114,6 +116,7 @@ int print_error(const char *str) { cJSON* err_tag = cJSON_GetObjectItemCaseSensitive(error, "error-tag"); if(err_tag == NULL) { lub_dump_printf("%% Error: %s\r\n", err_msg.c_str()); + cJSON_free(ret_json); return 1; } std::string err_tag_str = std::string {err_tag->valuestring}; @@ -129,8 +132,11 @@ int print_error(const char *str) { } lub_dump_printf("%% Error: %s\r\n", err_msg.c_str()); } + cJSON_free(ret_json); return 1; } + + cJSON_free(ret_json); return 0; } @@ -138,20 +144,69 @@ int print_error(const char *str) { std::string rest_token; CURL *curl = NULL; +struct curl_slist *headerList = NULL; -static int rest_set_curl_headers(bool use_token) { - struct curl_slist* headerList = NULL; - headerList = curl_slist_append(headerList, "accept: application/yang-data+json"); - headerList = curl_slist_append(headerList, "Content-Type: application/yang-data+json"); +/* Add a new header line to the curl instance */ +static int add_curl_header(const char *hdr_line) +{ + struct curl_slist *p = curl_slist_append(headerList, hdr_line); + if (p != headerList) { + headerList = p; + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + return 1; +} + +/* Remove a header from headerList by name, case insensitive */ +static int remove_curl_header(const char *name) +{ + int n = strlen(name); + struct curl_slist *p = headerList, *prev = NULL; + + while (p) { + if (strncasecmp(p->data, name, n) != 0 || p->data[n] != ':') { + prev = p; + p = p->next; + continue; + } + if (prev) { + prev->next = p->next; + } else { + headerList = p->next; + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + + free(p->data); + free(p); + return 1; + } + + return 0; +} + +/* Sets curl session header using given token, or NULL */ +int rest_set_session_token(const char *token) +{ + std::string hdr = "X-SonicDS"; + remove_curl_header(hdr.c_str()); + if (token && *token) { + hdr += ": Session "; + hdr += token; // "X-SonicDS: Session " + add_curl_header(hdr.c_str()); + return 1; + } + return 0; +} + +static int rest_set_curl_headers(bool use_token) { + remove_curl_header("Authorization"); if (rest_token.size() && use_token) { std::string auth_hdr = "Authorization: Bearer "; auth_hdr += rest_token; - headerList = curl_slist_append(headerList, auth_hdr.c_str()); + add_curl_header(auth_hdr.c_str()); + return 1; } - - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); - return 0; } @@ -172,10 +227,19 @@ static int _init_curl() { if (REST_API_ROOT.find("https://") == 0) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + } else if (REST_API_ROOT.find("http+unix://") == 0) { + char *sock = curl_easy_unescape(curl, REST_API_ROOT.c_str() + 12, 0, NULL); + curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, sock); + curl_free(sock); + REST_API_ROOT = "http://localhost"; } else { curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, "/var/run/rest-local.sock"); } + // Add base headers here.. Other headers will be added/removed later as needed + add_curl_header("accept: application/yang-data+json"); + add_curl_header("Content-Type: application/yang-data+json"); + return 0; } @@ -299,7 +363,7 @@ int _parse_args (std::string &input, std::string &oper_s, std::string &url_s, st return 0; } -int rest_cl(char *cmd, const char *buff) +int rest_cl(char *cmd, const char *buff, char **o_resp) { CURLcode res; @@ -350,6 +414,9 @@ int rest_cl(char *cmd, const char *buff) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if ((ret.size == 0) || (http_code == 200) || ((oper.compare("DELETE") == 0) && (http_code == 404))) { ret_code = 0; + if (o_resp && ret.size) { + *o_resp = strdup(ret.body.c_str()); + } } else { syslog(LOG_DEBUG, "clish_restcl: http_code:%ld [%d:%s]", http_code, ret.size, ret.body.c_str()); print_error(ret.body.c_str()); diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/session.c b/CLI/klish/patches/klish-2.1.4/plugins/clish/session.c new file mode 100644 index 0000000000..bb2ab40a95 --- /dev/null +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/session.c @@ -0,0 +1,259 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// 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. // +// // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include "clish/plugin.h" +#include "clish/shell.h" +#include "lub/dump.h" +#include "lub/string.h" +#include "logging.h" +#include "nos_extn.h" +#include "private.h" + +#define SESSION_NAME_ENV "_session_name" +#define SESSION_TOKEN_ENV "_session_token" +#define IN_SESSION_VAR "_in_session" +#define CONFIG_MODE_VAR "_config_mode" + +/** + * Current session context cache. + */ +static struct +{ + char *name; + char *token; +} _c_session; + +/* Per command overrides for session cache */ +static struct { + bool_t session_suppressed; // token was suppressed for 'do' command + char session_token[128]; // for logging, even after exit action +} _per_cmd; + +/* is_null returns TRUE if a string s is NULL or empty */ +static inline bool_t is_null(const char *s) { return !(s && *s); } + +/* in_session_mode returns TRUE if session context cache is valid */ +static inline bool_t in_session_mode() { return _c_session.token != NULL; } + +/** + * update_action_env sets a session token in python & rest_cl env. + * All subsequent REST calls will use this token. + */ +static int update_action_env(clish_shell_t *shell, const char *token) +{ + nos_extn_lock(); + + // for python actions + const char *effective_name = token && _c_session.name ? _c_session.name : ""; + pyobj_update_environ(SESSION_NAME_ENV, effective_name); + pyobj_update_environ(SESSION_TOKEN_ENV, token ? token : ""); + // for rest_cl actions + rest_set_session_token(token); + + nos_extn_unlock(); + return 0; +} + +/** + * set_shell_session_context refreshes values of config session + * related shell variables and environment variables by reading the + * current state from the cache. + */ +int set_shell_session_context(clish_shell_t *shell) +{ + if (in_session_mode()) { + clish_shell_set_var(shell, IN_SESSION_VAR, "y"); + clish_shell_set_var(shell, CONFIG_MODE_VAR, "config-s"); + } else { + clish_shell_set_var(shell, IN_SESSION_VAR, NULL); + clish_shell_set_var(shell, CONFIG_MODE_VAR, "config"); + } + + return update_action_env(shell, _c_session.token); +} + +/** + * reset_session_context clears all config session related state + * from the context cache and shell instance. + */ +int reset_session_context(clish_shell_t *shell) +{ + syslog(LOG_DEBUG, "%s: old name=%p, token=%p", __FUNCTION__, _c_session.name, _c_session.token); + lub_string_free(_c_session.name); + lub_string_free(_c_session.token); + _c_session.name = _c_session.token = NULL; + + return set_shell_session_context(shell); +} + +/* Returns session token for current command context; or NULL */ +const char *get_session_token(clish_context_t *ctx) +{ + if (_per_cmd.session_suppressed || is_null(_per_cmd.session_token)) + return NULL; + return _per_cmd.session_token; +} + +/* Cleanup per-command session cache overrides */ +static inline void reset_per_command_cache() +{ + _per_cmd.session_token[0] = '\0'; + _per_cmd.session_suppressed = BOOL_FALSE; +} + +/* Detect if a command will jump to a different view */ +static bool_t command_changes_view(const clish_command_t *cmd, clish_pargv_t *params) +{ + // Look for 'view' attribute in + if (clish_command__get_viewname(cmd)) { + return BOOL_TRUE; + } + + // Look for 'view' attribute in + const clish_param_t *param; + int i, n = clish_pargv__get_count(params); + for (i = 0; i < n; i++) { + param = clish_pargv__get_param(params, i); + if (param && clish_param__get_viewname(param)) { + return BOOL_TRUE; + } + } + + return BOOL_FALSE; +} + +/** + * session_pre_action_hook prepares the action execution environment + * for a command. Should be called before running each command action. + * Returns non-0 value if given command cannot be run from config session. + */ +int session_pre_action_hook(clish_context_t *ctx) +{ + // Precautionary cleanups + reset_per_command_cache(); + + // Skip if we are not inside session + if (!in_session_mode()) { + return 0; + } + + // Save the token for logging, will be valid even after exit action + strncpy(_per_cmd.session_token, _c_session.token, sizeof(_per_cmd.session_token)); + + // Skip if we are not running enable-view command (i.e, 'do' command) + const clish_command_t *cmd = clish_context__get_cmd(ctx); + clish_view_t *cmd_view = clish_command__get_pview(cmd); + if (clish_view__get_depth(cmd_view) != 0) { + return 0; + } + + // Prevent jumping directly to an outside view, like "do debug shell" + if (command_changes_view(cmd, clish_context__get_pargv(ctx))) { + lub_dump_printf("%%Error: not allowed from configure session mode\n"); + return -1; + } + + // Prepare for running enable-view action by suppressing session token + syslog(LOG_DEBUG, "%s: temporarily suppressing session token..", __FUNCTION__); + clish_shell_t *shell = clish_context__get_shell(ctx); + update_action_env(shell, NULL); + _per_cmd.session_suppressed = BOOL_TRUE; + + return 0; +} + +/** + * session_post_action_hook updates the action execution environment + * after a command's action is run. + */ +int session_post_action_hook(clish_context_t *ctx) +{ + if (_per_cmd.session_suppressed) { + syslog(LOG_DEBUG, "%s: restoring session token", __FUNCTION__); + clish_shell_t *shell = clish_context__get_shell(ctx); + update_action_env(shell, _c_session.token); + } + + reset_per_command_cache(); + return 0; +} + +/** + * Action handler for "configure session" command. + */ +CLISH_PLUGIN_SYM(clish_start_session) +{ + int status = 0; + clish_shell_t *shell = clish_context__get_shell(clish_context); + + status = clish_pyobj(clish_context, script, out); + if (status != 0) { + return status; // pyobj would have printed the error already + } + + // collect session name and token from pyobj + const char *name = getenv(SESSION_NAME_ENV); + const char *token = getenv(SESSION_TOKEN_ENV); + if (is_null(token)) { + reset_session_context(shell); + syslog(LOG_ERR, "%s: script did not return session token", __FUNCTION__); + lub_dump_printf("%%Error: internal error\n"); + return -1; + } + + _c_session.name = lub_string_dup(name); + _c_session.token = lub_string_dup(token); + strncpy(_per_cmd.session_token, _c_session.token, sizeof(_per_cmd.session_token)); + + status = set_shell_session_context(shell); + if (status != 0) + { + reset_session_context(shell); + syslog(LOG_ERR, "%s: failed to set context in shell", __FUNCTION__); + lub_dump_printf("%%Error: internal error\n"); + return status; + } + + return 0; +} + +/** + * Action handler for "end" and "exit" commands. + */ +CLISH_PLUGIN_SYM(clish_exit_session) +{ + clish_shell_t *shell = clish_context__get_shell(clish_context); + if (!in_session_mode()) { + clish_shell__set_depth(shell, 0); + return 0; + } + + // TODO avoid hardcoding the default exit script + if (is_null(script)) + script = "sessionctl exit"; + int result = clish_pyobj(clish_context, script, out); + if (result != 0) { + return result; + } + + reset_session_context(shell); + clish_shell__set_depth(shell, 0); + return 0; +} \ No newline at end of file diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/session.h b/CLI/klish/patches/klish-2.1.4/plugins/clish/session.h new file mode 100644 index 0000000000..926b1aeb00 --- /dev/null +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/session.h @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// 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. // +// // +//////////////////////////////////////////////////////////////////////////////// + +#ifndef __NOS_EXTN_SESSION_H__ +#define __NOS_EXTN_SESSION_H__ + +#include "clish/shell.h" + +_BEGIN_C_DECL + +/* Retrieve current session token or NULL */ +const char *get_session_token(clish_context_t *ctx); + +/* Remove all session context info from the shell */ +int reset_session_context(clish_shell_t *shell); + +/* Prepare env before executing an action */ +int session_pre_action_hook(clish_context_t *ctx); + +/* Update env after executing an action */ +int session_post_action_hook(clish_context_t *ctx); + +_END_C_DECL + +#endif diff --git a/CLI/klish/patches/klish-2.1.4/plugins/clish/sym_misc.c.diff b/CLI/klish/patches/klish-2.1.4/plugins/clish/sym_misc.c.diff new file mode 100644 index 0000000000..8683c233ab --- /dev/null +++ b/CLI/klish/patches/klish-2.1.4/plugins/clish/sym_misc.c.diff @@ -0,0 +1,28 @@ +diff --git a/plugins/clish/sym_misc.c b/plugins/clish/sym_misc.c +index d92fbd0..119ecf1 100644 +--- a/plugins/clish/sym_misc.c ++++ b/plugins/clish/sym_misc.c +@@ -5,6 +5,7 @@ + #include "lub/string.h" + #include "lub/argv.h" + #include "lub/conv.h" ++#include "session.h" + + #include + #include +@@ -163,6 +164,15 @@ CLISH_PLUGIN_SYM(clish_nested_up) + return 0; + } + ++ // Clear config session context when moving to the root view ++ if (depth == 0) { ++ int result = clish_exit_session(clish_context, NULL, out); ++ if (result != 0) { ++ clish_shell__set_depth(this, 1); // restore depth on error ++ return result; ++ } ++ } ++ + script = script; /* Happy compiler */ + out = out; /* Happy compiler */ + diff --git a/CLI/klish/patches/scripts/repack.sh b/CLI/klish/patches/scripts/repack.sh new file mode 100755 index 0000000000..05b72cb579 --- /dev/null +++ b/CLI/klish/patches/scripts/repack.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +function usage() { +cat < /dev/null || true) +if [[ ${ORIGIN} != */klish.git ]]; then + echo "error: ${WORKDIR} is not a klish repo" + exit 1 +fi +if [[ -z $(git tag --list BASE) ]]; then + echo "error: BASE tag not found at ${WORKDIR}" + exit 2 +fi + +# find all changes in the WORKDIR repo +CHANGES=( $(git status -s | awk '{print $2}' | sort -u) ) +TMPFILE=$(mktemp) + +echo "Packing ${#CHANGES[@]} changes from ${WORKDIR} into ${TOPDIR}" + +for F in ${CHANGES[@]}; do + mkdir -p ${PATCHDIR}/$(dirname $F) + # Current file has a diff file at PATCHDIR. Overwrite it + # with the new diff from BASE. + if [[ -f ${PATCHDIR}/$F.diff ]]; then + if [[ $(awk '{print $1; exit}' ${PATCHDIR}/$F.diff) == diff ]]; then + # Using git diff format + echo " * $F.diff (git diff)" + git diff BASE $F >| ${PATCHDIR}/$F.diff + else + # Using default unix diff format. Copy BASE version to + # a tempfile and run normal diff + echo " * $F.diff (unix diff)" + git show BASE:$F >| ${TMPFILE} + diff ${TMPFILE} $F >| ${PATCHDIR}/$F.diff || test $? -eq 1 + fi + continue + fi + + # Changed entry is a directory. It can only be an untracked one. + # Copy all its contents as is. + if [[ -d $F ]]; then + echo " * $F/..." + cp -R $F ${PATCHDIR}/$F + continue + fi + + # Checked if the changed file is was present in BASE version. + # If yes, save it as a new diff file with git diff format. + if [[ -z $(git show BASE:$F >& /dev/null || echo no) ]]; then + echo " * $F.diff (git diff)" + git diff BASE $F >| ${PATCHDIR}/$F.diff + continue + fi + + # Changed file is not in BASE version. It is a new file added + # by us. Copy it as is. + echo " * $F" + cp -f $F ${PATCHDIR}/$F +done + +rm -rf ${TMPFILE} diff --git a/CLI/klish/patches/scripts/unpack.sh b/CLI/klish/patches/scripts/unpack.sh new file mode 100755 index 0000000000..2ad24897f5 --- /dev/null +++ b/CLI/klish/patches/scripts/unpack.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +function usage() { +cat < /dev/null || true) + if [[ ${ORIGIN} != */klish.git ]]; then + echo "error: ${DESTDIR} is not a klish repo" + exit 1 + fi + if [[ -z $(git tag --list BASE) ]]; then + echo "error: BASE tag not found at ${DESTDIR}" + exit 2 + fi + if [[ $(git status --short | wc -l) != 0 ]]; then + echo "error: ${DESTDIR} is not clean" + exit 3 + fi +else + # Create new repo and apply BASE tag + mkdir -p ${WORKDIR} + tar xzvf ${TOPDIR}/CLI/klish/${SRCROOT}.tgz -C ${WORKDIR} + cd ${DESTDIR} + echo "Applying 'BASE' tag" + git tag BASE +fi + +# Create a new branch from BASE +git checkout BASE -b ${BRANCH} + +# Apply patches from the mgmt-framework repo +${TOPDIR}/CLI/klish/patches/scripts/patchmake.sh \ + -p \ + VER=${KLISH_VERSION} \ + TSP=${WORKDIR} \ + DSP=${TOPDIR}/CLI/klish/patches \ + TWP=${WORKDIR} + +# Remove patch log files +rm '##'* + +# Commit all the changes into the BRANCH +git add --all +git commit -m "Unpack from mgmt-framework ${COMMIT}" + +echo "" +echo "Work directory: ${DESTDIR}" diff --git a/CLI/renderer/scripts/render_cli.py b/CLI/renderer/scripts/render_cli.py index ef471df37d..aa5a8bf982 100755 --- a/CLI/renderer/scripts/render_cli.py +++ b/CLI/renderer/scripts/render_cli.py @@ -225,9 +225,12 @@ def datetimeformat_rfc3339(time): j2_env.globals.update(datetimeformat_rfc3339=datetimeformat_rfc3339) - def datetimeformat(time): + def datetimeformat(time, unit="s"): + if time is None: + return "" + secs = int(time) // 1000000000 if unit == "ns" else int(time) utc_offset = tm.strftime("%z", tm.localtime()) - return datetime.datetime.fromtimestamp(int(time)).strftime("%Y-%m-%d %H:%M:%S") + utc_offset + return datetime.datetime.fromtimestamp(secs).strftime("%Y-%m-%d %H:%M:%S") + utc_offset def datetimeformat_year(time): utc_offset = tm.strftime("%z", tm.localtime()) diff --git a/CLI/renderer/templates/show_cs_brief.j2 b/CLI/renderer/templates/show_cs_brief.j2 new file mode 100644 index 0000000000..fe7574adb8 --- /dev/null +++ b/CLI/renderer/templates/show_cs_brief.j2 @@ -0,0 +1,11 @@ +{% if "config-session" in json_output %} +Name State Age User +------------------- --------- ---------- ---------- +{% for cs in json_output["config-session"] %} +{% set csName = cs["name"]|default("(unnamed)", true) %} +{% set csState = cs["state"]|title %} +{% set csTime = cs["timestamps"]["created"]|int // 1000000000 %} +{{csName.ljust(19)}} {{csState.ljust(9)}} {{seconds_to_wdhm_str(csTime, True).ljust(10)}} {{cs["username"]}} +{% endfor %} +{{" "}} +{% endif %} \ No newline at end of file diff --git a/CLI/renderer/templates/show_cs_detail.j2 b/CLI/renderer/templates/show_cs_detail.j2 new file mode 100644 index 0000000000..903ad3c65a --- /dev/null +++ b/CLI/renderer/templates/show_cs_detail.j2 @@ -0,0 +1,16 @@ +{% if "config-session" in json_output %} +{% for cs in json_output["config-session"] %} +{% set pid = cs["terminal"]["pid"]|default(None) if "terminal" in cs else None %} +Session Name : {{cs["name"]|default("(unnamed)", true)}} +Session Token : {{cs["token"]}} +Session State : {{cs["state"]|title}} {% if pid %}(PID {{pid}}){% endif %}{{""}} +{% if "timestamps" in cs %} +Created by : {{cs["username"]}} +Created at : {{datetimeformat(cs["timestamps"]["created"]|default(None), "ns")}} +Last Resumed at : {{datetimeformat(cs["timestamps"]["last-resumed"]|default(None), "ns")}} +Last Exited at : {{datetimeformat(cs["timestamps"]["last-exited"]|default(None), "ns")}} +Last Activity at : {{datetimeformat(cs["timestamps"]["last-updated"]|default(None), "ns")}} +{% endif %} +{{" "}} +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/CLI/renderer/templates/show_interface_breakout.j2 b/CLI/renderer/templates/show_interface_breakout.j2 index fa1b83f7df..46fe939409 100755 --- a/CLI/renderer/templates/show_interface_breakout.j2 +++ b/CLI/renderer/templates/show_interface_breakout.j2 @@ -13,6 +13,7 @@ {% for entry in cfg %} {% set vars = {'status': ""} %} {% set vars = {'mode': "Default"} %} +{% set vars = {'member': ""} %} {% set vars = {'first': true} %} {% for status in state %} {% if entry["ifname"] == status["ifname"] %} @@ -22,6 +23,7 @@ {% endif %} {% endfor %} {% for member in members %} +{% if vars.update({'member':member["ifname"]}) %}{% endif %} {% if entry["ifname"] == member["master"] %} {% if vars.first %} {% if (entry["brkout_mode"]|length)>1 %} @@ -29,16 +31,20 @@ {% if (vars.status|length)<1 %} {% if vars.update({'status':"Completed"}) %}{% endif %} {% endif %} +{% if "1x" in vars.mode %} +{% if vars.update({'member':member["ifname"]}) %}{% endif %} +{% endif %} {% else %} {% if vars.update({'mode':"Default"}) %}{% endif %} +{% if vars.update({'member':member["ifname"]}) %}{% endif %} {% endif %} {% if (vars.status|length)>1 %} -{{'%-6s'|format(entry["port"])}}{{'%-15s'|format(vars.mode)}}{{'%-14s'|format(vars.status)}}{{'%-20s'|format(member["master"])}} +{{'%-6s'|format(entry["port"])}}{{'%-15s'|format(vars.mode)}}{{'%-14s'|format(vars.status)}}{{'%-20s'|format(vars.member)}} {% endif %} {% if vars.update({'first':false}) %}{% endif %} {% elif "Default" not in vars.mode and "1x" not in vars.mode %} {% if (vars.status|length)>1 %} -{{'%-35s'|format("")}}{{'%-20s'|format(member["ifname"])}} +{{'%-35s'|format("")}}{{'%-20s'|format(vars.member)}} {% endif %} {% endif %} {% endif %} diff --git a/rest/main/main.go b/rest/main/main.go index 128765ffb6..23c11fa389 100644 --- a/rest/main/main.go +++ b/rest/main/main.go @@ -41,8 +41,7 @@ var ( certFile string // Server certificate file path keyFile string // Server private key file path caFile string // Client CA certificate file path - noSocket bool // Do not start unix domain socket lister - internal bool // Enable internal features on https listener + socketFile string // Unix domain socket file for CLI clientAuth = server.NewUserAuth() funnel server.RequestFunnel // Controls number of concurrent requests @@ -67,8 +66,7 @@ func init() { flag.StringVar(&keyFile, "key", "", "Server private key file path") flag.StringVar(&caFile, "cacert", "", "CA certificate for client certificate validation") flag.Var(clientAuth, "client_auth", "Client auth mode(s) - default: password,jwt") - flag.BoolVar(&noSocket, "no-sock", false, "Do not start unix domain socket listener") - flag.BoolVar(&internal, "internal", false, "Enable internal, non-standard features on https listener") + flag.StringVar(&socketFile, "sock", "/var/run/rest-local.sock", "Unix socket path for the internal listener") flag.UintVar(&funnel.Limit, "reqlimit", 0, "Max concurrent requests allowed") flag.DurationVar(&readTimeout, "readtimeout", readTimeout, "Maximum duration for reading entire request") flag.DurationVar(&apiTimeout, "apitimeout", apiTimeout, "REST API timeout; value 0 disables it") @@ -104,8 +102,7 @@ func main() { server.JwtValidInt = time.Duration(3600 * time.Second) rtrConfig := server.RouterConfig{ - Auth: clientAuth, - Internal: internal, + Auth: clientAuth, } router := newRouter(&rtrConfig) @@ -129,8 +126,8 @@ func main() { ReadTimeout: readTimeout, } - if !noSocket { - spawnUnixListener() + if len(socketFile) != 0 { + spawnUnixListener(socketFile) } glog.Infof("Read timeout = %v", readTimeout) @@ -149,7 +146,7 @@ func main() { // spawnUnixListener listens using certificate authentication on a local // unix socket. This is used for authentication of the CLI client to the REST // server, and will not be used for any other client. -func spawnUnixListener() { +func spawnUnixListener(UDSock string) { var CLIAuth = server.UserAuth{"clisock": true, "jwt": true} rtrConfig := server.RouterConfig{ Auth: CLIAuth, @@ -158,7 +155,6 @@ func spawnUnixListener() { handler := newRouter(&rtrConfig) - const UDSock = "/var/run/rest-local.sock" os.Remove(UDSock) localListener, err := net.Listen("unix", UDSock) if err != nil { @@ -179,6 +175,7 @@ func spawnUnixListener() { } go func() { + glog.V(2).Info("Starting internal server on ", UDSock) if err := localServer.Serve(localListener); err != nil && err != http.ErrServerClosed { writeErrorStatus("Failed to create local server for CLI", STATUS_DN) glog.Fatal(err) diff --git a/rest/server/cliUserAuth.go b/rest/server/cliUserAuth.go index db0b11b096..71fe043928 100644 --- a/rest/server/cliUserAuth.go +++ b/rest/server/cliUserAuth.go @@ -6,20 +6,24 @@ import ( "net" "net/http" "os/user" + "syscall" "golang.org/x/sys/unix" "github.com/golang/glog" ) -func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { +// getConnectionUcred returns unix.Ucred from the request connection. +// Returns error if the request was not received on unix socket listener +// or socket ucred options are not available. +func getConnectionUcred(r *http.Request, rc *RequestContext) (*unix.Ucred, error) { netConn := r.Context().Value(cliConnectionContextKey) unixConn, ok := netConn.(*net.UnixConn) if !ok { glog.Errorf("[%s] Unable to obtain network connection", rc.ID) - return fmt.Errorf("Unable to obtain network connection") + return nil, fmt.Errorf("Unable to obtain network connection") } var cred *unix.Ucred @@ -27,7 +31,7 @@ func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { raw, err := unixConn.SyscallConn() if err != nil { glog.Errorf("[%s] Unable to get raw socket info (%v)", rc.ID, err) - return err + return nil, err } err2 := raw.Control(func(fd uintptr) { @@ -37,11 +41,20 @@ func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { if err != nil { glog.Errorf("[%s] Unable to get peer credentials (%v)", rc.ID, err) - return err + return nil, err } if err2 != nil { glog.Errorf("[%s] Unable to send control to raw socket (%v)", rc.ID, err2) - return err2 + return nil, err2 + } + + return cred, nil +} + +func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { + cred, err := getConnectionUcred(r, rc) + if err != nil { + return err } // PopulateAuthStruct will repeat the lookup by username, and we don't @@ -56,6 +69,7 @@ func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { return err } + rc.Auth.Pid = pid2Pgid(cred.Pid, rc.ID) rc.Auth.User = usr.Username rc.Auth.Roles, err = GetUserRoles(usr) if err != nil { @@ -67,6 +81,29 @@ func CliUserAuthenAndAuthor(r *http.Request, rc *RequestContext) error { return nil } +// collectClientPid attempts to discover the internal client's pid and +// stores in rc.Auth.Pid field. +func collectClientPid(r *http.Request, rc *RequestContext) error { + if rc.Auth.Pid > 0 { + return nil + } + cred, err := getConnectionUcred(r, rc) + if err == nil { + rc.Auth.Pid = pid2Pgid(cred.Pid, rc.ID) + } + return err +} + +// pid2Pgid gets the Process Group Id of a process from it's Pid +func pid2Pgid(pid int32, ID string) int32 { + if pgid, err := syscall.Getpgid(int(pid)); err != nil { + glog.Warningf("[%s] Getpgid() failed for Pid=%d (%v)", ID, pid, err) + return pid + } else { + return int32(pgid) + } +} + // CLIConnectionContextFactory is context factory function for CLI server. // To be set as http.Server.ConnContext value while setting up unix socket server for CLI. func CLIConnectionContextFactory(ctx context.Context, c net.Conn) context.Context { diff --git a/rest/server/context.go b/rest/server/context.go index 0e7175c14d..2b03d268c6 100644 --- a/rest/server/context.go +++ b/rest/server/context.go @@ -32,9 +32,10 @@ import ( type AuthInfo struct { // Username User string - // Roles Roles []string + // Shell pid for CLI user + Pid int32 } // RequestContext holds metadata about REST request. @@ -70,6 +71,9 @@ type RequestContext struct { Auth AuthInfo ClientAuth UserAuth + + // dataStore holds the parsed X-SonicDS header value + dataStore DSRef } type contextkey int diff --git a/rest/server/ds_header.go b/rest/server/ds_header.go new file mode 100644 index 0000000000..688cee3c5f --- /dev/null +++ b/rest/server/ds_header.go @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// 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. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package server + +import ( + "net/http" + "strings" + + "github.com/Azure/sonic-mgmt-common/translib/cs" + "github.com/golang/glog" +) + +// DSRef points to a specific data store instance by its type and label. +type DSRef struct { + cs.DataStore +} + +func (ds *DSRef) LogLabel() string { + switch ds.Type { + case cs.DSCandidate: + return "[Session:" + ds.Label + "]" + case cs.DSCheckpoint: + return "[Checkpoint:" + ds.Label + "]" + default: + return "" + } +} + +const xdsHeader = "X-SonicDS" + +// withDSParser creates a middleware which parses the X-SonicDS header +// and stores in the RequestContext. +func withDSParser(inner http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + dsValue := r.Header.Get(xdsHeader) + if len(dsValue) == 0 { + inner.ServeHTTP(w, r) + return + } + + cfg := getRouterConfig(r) + if cfg == nil || !cfg.Internal { + glog.V(2).Infof("%s header allowed on internal listener only", xdsHeader) + writeErrorResponse(w, r, httpBadRequest("%s header not allowed", xdsHeader)) + return + } + + rc, r := GetContext(r) + if err := parseDSHeader(dsValue, rc); err != nil { + writeErrorResponse(w, r, err) + } else { + inner.ServeHTTP(w, r) + } + } +} + +// parseDSHeader parses a string value s of "