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/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/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..69b4a95 --- /dev/null +++ b/extensions/plugins/cli-command/README.md @@ -0,0 +1,18 @@ +# CLI command plugin + +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: + +```bash +cd cli-command +make +make install +``` + +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: + +```bash +cp example.cli /usr/local/lib/controller/clispec/ +``` 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"); +}