From 7c0991ad3db1dc3a041456c529a2bf8e2e8bcb9a Mon Sep 17 00:00:00 2001 From: Kristofer Hallin Date: Thu, 11 Dec 2025 11:34:13 +0000 Subject: [PATCH 1/2] Add a new plugin to run arbitrary commands from the CLI. --- configure | 3 +- configure.ac | 1 + extensions/plugins/Makefile.in | 2 +- extensions/plugins/cli-command/Makefile.in | 67 +++++++ extensions/plugins/cli-command/README.md | 22 +++ .../cli-command/controller_cli_command.c | 168 ++++++++++++++++++ extensions/plugins/cli-command/example.cli | 6 + 7 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 extensions/plugins/cli-command/Makefile.in create mode 100644 extensions/plugins/cli-command/README.md create mode 100644 extensions/plugins/cli-command/controller_cli_command.c create mode 100644 extensions/plugins/cli-command/example.cli diff --git a/configure b/configure index 7bcdb4a..2bac855 100755 --- a/configure +++ b/configure @@ -4939,7 +4939,7 @@ if test "x$with_systemdsystemunitdir" != "xno" ; then HAVE_SYSTEMD=true fi -ac_config_files="$ac_config_files Makefile clixon-controller.service src/Makefile src/controller.xml yang/Makefile util/Makefile docker/Makefile test/config.sh doc/Makefile extensions/Makefile extensions/plugins/Makefile extensions/plugins/junos-native/Makefile extensions/yang/Makefile extensions/yang/junos-bgp/Makefile extensions/yang/junos-macsec/Makefile extensions/yang/junos-users/Makefile" +ac_config_files="$ac_config_files Makefile clixon-controller.service src/Makefile src/controller.xml yang/Makefile util/Makefile docker/Makefile test/config.sh doc/Makefile extensions/Makefile extensions/plugins/Makefile extensions/plugins/junos-native/Makefile extensions/plugins/cli-command/Makefile extensions/yang/Makefile extensions/yang/junos-bgp/Makefile extensions/yang/junos-macsec/Makefile extensions/yang/junos-users/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -5654,6 +5654,7 @@ do "extensions/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/Makefile" ;; "extensions/plugins/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/plugins/Makefile" ;; "extensions/plugins/junos-native/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/plugins/junos-native/Makefile" ;; + "extensions/plugins/cli-command/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/plugins/cli-command/Makefile" ;; "extensions/yang/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/yang/Makefile" ;; "extensions/yang/junos-bgp/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/yang/junos-bgp/Makefile" ;; "extensions/yang/junos-macsec/Makefile") CONFIG_FILES="$CONFIG_FILES extensions/yang/junos-macsec/Makefile" ;; diff --git a/configure.ac b/configure.ac index bd69e6a..236796c 100644 --- a/configure.ac +++ b/configure.ac @@ -212,6 +212,7 @@ AC_CONFIG_FILES([Makefile extensions/Makefile extensions/plugins/Makefile extensions/plugins/junos-native/Makefile + extensions/plugins/cli-command/Makefile extensions/yang/Makefile extensions/yang/junos-bgp/Makefile extensions/yang/junos-macsec/Makefile diff --git a/extensions/plugins/Makefile.in b/extensions/plugins/Makefile.in index 0dff6ff..edfbfee 100644 --- a/extensions/plugins/Makefile.in +++ b/extensions/plugins/Makefile.in @@ -23,7 +23,7 @@ VPATH = @srcdir@ srcdir = @srcdir@ top_srcdir = @top_srcdir@ -SUBDIRS = junos-native +SUBDIRS = junos-native cli-command .PHONY: all clean depend install $(SUBDIRS) diff --git a/extensions/plugins/cli-command/Makefile.in b/extensions/plugins/cli-command/Makefile.in new file mode 100644 index 0000000..d9376a2 --- /dev/null +++ b/extensions/plugins/cli-command/Makefile.in @@ -0,0 +1,67 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# +# Copyright (C) 2025 Olof Hagsand +# +# This file is part of CLIXON +# +# 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. +# +# ***** END LICENSE BLOCK ***** +# + +srcdir = . +top_srcdir = ../../.. +top_builddir = ../../.. +prefix = /usr/local +exec_prefix = /usr/local +bindir = ${exec_prefix}/bin +includedir = ${prefix}/include +datarootdir = ${prefix}/share +sysconfdir = ${prefix}/etc +localstatedir = ${prefix}/var +runstatedir = ${localstatedir}/run +libdir = ${exec_prefix}/lib +version = 1.6.0-1+5+g966282a + +APPNAME = controller + +CC = gcc +CFLAGS = -O2 -Wall -Werror -fPIC +LDFLAGS = +INSTALLFLAGS = -s + +INCLUDES = -I. -I$(top_srcdir)/src +CPPFLAGS = @CPPFLAGS@ -fPIC -DCONTROLLER_VERSION=\"$(version)\" + +CLI_PLUGIN = $(APPNAME)_cli_command.so +CLI_SRC = $(APPNAME)_cli_command.c +CLI_OBJ = $(CLI_SRC:%.c=%.o) + +$(CLI_PLUGIN): $(CLI_OBJ) $(GENOBJS) + $(CC) -Wall -shared $(LDFLAGS) -o $@ -lc $^ -lclixon -lclixon_cli + +PLUGINS = $(CLI_PLUGIN) +OBJS = $(CLI_OBJ) + +all: $(PLUGINS) + +clean: + rm -f $(PLUGINS) $(OBJS) + +install: $(PLUGINS) + install -d -m 0755 $(DESTDIR)$(libdir)/$(APPNAME)/cli + install -m 0644 $(INSTALLFLAGS) $(CLI_PLUGIN) $(DESTDIR)$(libdir)/$(APPNAME)/cli + +depend: + $(CC) $(DEPENDFLAGS) $(INCLUDES) $(CFLAGS) -MM $(SRC) > .depend diff --git a/extensions/plugins/cli-command/README.md b/extensions/plugins/cli-command/README.md new file mode 100644 index 0000000..0131693 --- /dev/null +++ b/extensions/plugins/cli-command/README.md @@ -0,0 +1,22 @@ +# Junos native plugin + +Backend plugin for Junos native for NON rfc- and yang-compliant +Juniper PTX,MX and QFX (possibly others). The plugin rewrites +the XML config on pull/sync and push/commit. + +No configuration is necessary. + +The extension allocates three device flags to keep track of whether the device is a +JunOS device, a QFX JunOS device and if the check has been made or not. + +It then intercepts incoming NETCONF/XML messages from the device, +detects if it is a JusOS device in need of rewrite by examining its +system compliance settings, and then potentially rewrites the XML to RFC +and YANG compliant. + +If flags are set appropriately, the extension also intercepts outgoing +NETCONF/XML messages from RFC/YANG compliant to Junos non-compliant +native mode. + +The interception uses the user-defined callback which in the controller is placed in +the path of incoming and outgoing NETCONF messages. diff --git a/extensions/plugins/cli-command/controller_cli_command.c b/extensions/plugins/cli-command/controller_cli_command.c new file mode 100644 index 0000000..e5d0065 --- /dev/null +++ b/extensions/plugins/cli-command/controller_cli_command.c @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#include + +#include +#include + +/*! Generic cli command to run script with arguments + * + * @param[in] h Clixon handle + * @param[in] cvv Vector of command variables + * @param[in] argv Vector of arguments, first is script runner, second is script path + * @retval 0 OK + * @retval -1 Error + */ +int cli_command_run(clixon_handle h, cvec *cvv, cvec *argv) +{ + int pid = 0; + int retval = -1; + int s = 0; + int arg_count = 0; + int cvv_count = 0; + int i = 0; + int status = 0; + char *script_path = NULL; + char *runner = NULL; + char *buf = NULL; + char *work_dir = NULL; + char *reserve_path = NULL; + char **args = NULL; + size_t bufsize = 0; + struct passwd pw, *pwresult = NULL; + + + /* Check parameters */ + if (cvec_len(argv) == 0) { + clixon_err(OE_PLUGIN, EINVAL, "Can not find argument"); + goto done; + } + + /* get data */ + arg_count = cvec_len(argv); + cvv_count = cvec_len(cvv); + + runner = cv_string_get(cvec_i(argv, 0)); + + if (arg_count > 1) { + script_path = cv_string_get(cvec_i(argv, 1)); + } + + if (script_path) { + reserve_path = strdup(script_path); + work_dir = dirname(reserve_path); + } + + bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); + + if (bufsize == -1) { + bufsize = 16384; + } + + buf = malloc(bufsize); + + if (buf == NULL) { + perror("malloc"); + goto done; + } + + s = getpwuid_r(getuid(), &pw, buf, bufsize, &pwresult); + + if (pwresult == NULL) { + if (s == 0) + clixon_err(OE_PLUGIN, errno, "getpwuid_r"); + else + perror("getpwuid_r"); + goto done; + } + + /* Prepare arguments for execlp */ + args = malloc((arg_count + cvv_count) * sizeof(char *)); + + if (args == NULL) { + perror("malloc"); + goto done; + } + + for (i = 0; i < arg_count; i++) { + args[i] = cv_string_get(cvec_i(argv, i)); + } + + for (i = 0; i < cvv_count; i++) { + args[arg_count + i] = cv_string_get(cvec_i(cvv, i + 1)); + } + + /* main run */ + if ((pid = fork()) == 0) { + + /* child process */ + if ((work_dir ? chdir(work_dir) : chdir(pw.pw_dir)) < 0) { + clixon_err(OE_PLUGIN, errno, "chdir"); + } + + execvp(runner, args); + clixon_err(OE_PLUGIN, errno, "Error running script"); + + goto done; + } else if(pid == -1) { + clixon_err(OE_PLUGIN, errno, "fork"); + } else { + /* parent process */ + if (waitpid(pid, &status, 0) != pid ){ + clixon_err(OE_PLUGIN, errno, "waitpid error"); + goto done; + } else { + retval = WEXITSTATUS(status); + goto done; + } + } + +done: + if (buf) + free(buf); + if (reserve_path) + free(reserve_path); + if (args) + free(args); + return retval; +} + +/*! Called when plugin is loaded. + * @param[in] h Clixon handle + * @retval 0 OK + * @retval -1 Error +*/ +int controller_cli_start(clixon_handle h) +{ + return 0; +} + +/*! Called just before plugin unloaded. + * + * @param[in] h Clixon handle + * @retval 0 OK + * @retval -1 Error + */ +int controller_cli_exit(clixon_handle h) +{ + return 0; +} + +static clixon_plugin_api api = { + "controller_test", + clixon_plugin_init, + controller_cli_start, + controller_cli_exit, +}; + +/*! CLI plugin initialization + * + * @param[in] h Clixon handle + * @retval api Pointer to API struct + */ +clixon_plugin_api *clixon_plugin_init(clixon_handle h) +{ + return &api; +} \ No newline at end of file diff --git a/extensions/plugins/cli-command/example.cli b/extensions/plugins/cli-command/example.cli new file mode 100644 index 0000000..54c5408 --- /dev/null +++ b/extensions/plugins/cli-command/example.cli @@ -0,0 +1,6 @@ +CLICON_MODE="operation"; +CLICON_PLUGIN="controller_cli_command"; + +test("Test") { + foo("Test"), cli_command_run("/home/debian/test.py", "foo", "bar"); +} From c41bc4cc1af8dd7e014cd5c3c0444c50164ac06b Mon Sep 17 00:00:00 2001 From: Kristofer Hallin Date: Thu, 11 Dec 2025 13:58:00 +0100 Subject: [PATCH 2/2] Updated readmes for plugins. --- extensions/plugins/README.md | 6 ++++- extensions/plugins/cli-command/README.md | 32 +++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/extensions/plugins/README.md b/extensions/plugins/README.md index bf164cf..29e6a63 100644 --- a/extensions/plugins/README.md +++ b/extensions/plugins/README.md @@ -11,7 +11,11 @@ The following plugins exist: Juniper PTX,MX and QFX (possibly others). The plugin rewrites the XML config on pull/sync and push/commit -To build and install a plugin, for example junos_native, do +- cli-command CLI plugin to run arbitrary shell commands on the server side. + The plugin maps CLI commands to shell commands and returns + the output as CLI output. + +To build and install a plugin, for example junos_native, do: cd junos-native make diff --git a/extensions/plugins/cli-command/README.md b/extensions/plugins/cli-command/README.md index 0131693..69b4a95 100644 --- a/extensions/plugins/cli-command/README.md +++ b/extensions/plugins/cli-command/README.md @@ -1,22 +1,18 @@ -# Junos native plugin +# CLI command plugin -Backend plugin for Junos native for NON rfc- and yang-compliant -Juniper PTX,MX and QFX (possibly others). The plugin rewrites -the XML config on pull/sync and push/commit. +The CLI command plugin allows mapping of CLI commands to arbitrary shell commands on the server side. +To build and install the CLI command plugin, follow these steps: -No configuration is necessary. +```bash +cd cli-command +make +make install +``` -The extension allocates three device flags to keep track of whether the device is a -JunOS device, a QFX JunOS device and if the check has been made or not. +You have to create a CLI specification file that maps CLI commands to shell commands. +An example specification named example.cli is provided in the cli-command directory. +The CLI specification file should be installed in the Clixon CLI specification directory: -It then intercepts incoming NETCONF/XML messages from the device, -detects if it is a JusOS device in need of rewrite by examining its -system compliance settings, and then potentially rewrites the XML to RFC -and YANG compliant. - -If flags are set appropriately, the extension also intercepts outgoing -NETCONF/XML messages from RFC/YANG compliant to Junos non-compliant -native mode. - -The interception uses the user-defined callback which in the controller is placed in -the path of incoming and outgoing NETCONF messages. +```bash +cp example.cli /usr/local/lib/controller/clispec/ +```