]> git.ipfire.org Git - thirdparty/lldpd.git/blobdiff - src/client/lldpcli.c
lldpcli: document json0 output format
[thirdparty/lldpd.git] / src / client / lldpcli.c
index 714b8cbc70f78335373c325a3204bc63a0d982ab..a7d9be4b4b20baf35433b0bf2283b013bc7167f9 100644 (file)
@@ -15,7 +15,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#define _GNU_SOURCE
+
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <errno.h>
 #include <string.h>
 #include <sys/types.h>
+#include <sys/stat.h>
 #include <sys/socket.h>
 #include <sys/un.h>
 #include <arpa/inet.h>
-#include <histedit.h>
 #include <libgen.h>
+#include <dirent.h>
+#include <signal.h>
+#include <sys/queue.h>
 
 #include "client.h"
 
@@ -39,6 +42,21 @@ extern const char    *__progname;
 
 /* Global for completion */
 static struct cmd_node *root = NULL;
+const char *ctlname = NULL;
+
+static int
+is_lldpctl(const char *name)
+{
+       static int last_result = -1;
+       if (last_result == -1 && name) {
+               char *basec = strdup(name);
+               if (!basec) return 0;
+               char *bname = basename(basec);
+               last_result = (!strcmp(bname, "lldpctl"));
+               free(basec);
+       }
+       return (last_result == -1)?0:last_result;
+}
 
 static void
 usage()
@@ -49,7 +67,14 @@ usage()
        fprintf(stderr, "\n");
 
        fprintf(stderr, "-d          Enable more debugging information.\n");
-       fprintf(stderr, "-f format   Choose output format (plain, keyvalue or xml).\n");
+       fprintf(stderr, "-u socket   Specify the Unix-domain socket used for communication with lldpd(8).\n");
+       fprintf(stderr, "-f format   Choose output format (plain, keyvalue, json, json0"
+#if defined USE_XML
+           ", xml"
+#endif
+           ").\n");
+       if (!is_lldpctl(NULL))
+               fprintf(stderr, "-c conf     Read the provided configuration file.\n");
 
        fprintf(stderr, "\n");
 
@@ -60,16 +85,23 @@ usage()
 static int
 is_privileged()
 {
-       return (!(getuid() != geteuid() || getgid() != getegid()));
+       /* Check we can access the control socket with read/write
+        * privileges. The `access()` function uses the real UID and real GID,
+        * therefore we don't have to mangle with our identity. */
+       return (ctlname && access(ctlname, R_OK|W_OK) == 0);
 }
 
 static char*
-prompt(EditLine *el)
+prompt()
 {
+#define CESC "\033"
        int privileged = is_privileged();
-       if (privileged)
-               return "[lldpcli] # ";
-       return "[lldpcli] $ ";
+       if (isatty(STDIN_FILENO)) {
+               if (privileged)
+                       return "[lldpcli] # ";
+               return "[lldpcli] $ ";
+       }
+       return "";
 }
 
 static int must_exit = 0;
@@ -107,55 +139,190 @@ cmd_update(struct lldpctl_conn_t *conn, struct writer *w,
                lldpctl_atom_dec_ref(config);
                return 0;
        }
-       log_info("lldpctl", "immediate retransmission requested successfuly");
+       log_info("lldpctl", "immediate retransmission requested successfully");
        lldpctl_atom_dec_ref(config);
        return 1;
 }
 
-static unsigned char
-_cmd_complete(EditLine *el, int ch, int all)
+/**
+ * Pause or resume execution of lldpd.
+ *
+ * @param conn    The connection to lldpd.
+ * @param pause   1 if we want to pause lldpd, 0 otherwise
+ * @return 1 on success, 0 on error
+ */
+static int
+cmd_pause_resume(lldpctl_conn_t *conn, int pause)
 {
-       int rc = CC_ERROR;
-       Tokenizer *eltok;
-       if ((eltok = tok_init(NULL)) == NULL)
-               goto end;
+       lldpctl_atom_t *config = lldpctl_get_configuration(conn);
+       if (config == NULL) {
+               log_warnx("lldpctl", "unable to get configuration from lldpd. %s",
+                   lldpctl_last_strerror(conn));
+               return 0;
+       }
+       if (lldpctl_atom_get_int(config, lldpctl_k_config_paused) == pause) {
+               log_debug("lldpctl", "lldpd is already %s",
+                   pause?"paused":"resumed");
+               lldpctl_atom_dec_ref(config);
+               return 1;
+       }
+       if (lldpctl_atom_set_int(config,
+               lldpctl_k_config_paused, pause) == NULL) {
+               log_warnx("lldpctl", "unable to ask lldpd to %s operations. %s",
+                   pause?"pause":"resume",
+                   lldpctl_last_strerror(conn));
+               lldpctl_atom_dec_ref(config);
+               return 0;
+       }
+       log_info("lldpctl", "lldpd should %s operations",
+           pause?"pause":"resume");
+       lldpctl_atom_dec_ref(config);
+       return 1;
+}
+static int
+cmd_pause(struct lldpctl_conn_t *conn, struct writer *w,
+    struct cmd_env *env, void *arg) {
+       (void)w; (void)env;
+       return cmd_pause_resume(conn, 1);
+}
+static int
+cmd_resume(struct lldpctl_conn_t *conn, struct writer *w,
+    struct cmd_env *env, void *arg) {
+       (void)w; (void)env;
+       return cmd_pause_resume(conn, 0);
+}
 
-       const LineInfo *li = el_line(el);
 
-       const char **argv;
-       char *compl;
-       int argc, cursorc, cursoro;
-       if (tok_line(eltok, li, &argc, &argv, &cursorc, &cursoro) != 0)
+#ifdef HAVE_LIBREADLINE
+static int
+_cmd_complete(int all)
+{
+       char **argv = NULL;
+       int argc = 0;
+       int rc = 1;
+       size_t len = strlen(rl_line_buffer);
+       char *line = malloc(len + 2);
+       if (!line) return -1;
+       strlcpy(line, rl_line_buffer, len + 2);
+       line[rl_point]   = 2;   /* empty character, will force a word */
+       line[rl_point+1] = 0;
+
+       if (tokenize_line(line, &argc, &argv) != 0)
                goto end;
-       compl = commands_complete(root, argc, argv, cursorc, cursoro, all);
-       if (compl) {
-               el_deletestr(el, cursoro);
-               if (el_insertstr(el, compl) == -1) {
+
+       char *compl = commands_complete(root, argc, (const char **)argv, all, is_privileged());
+       if (compl && strlen(argv[argc-1]) < strlen(compl)) {
+               if (rl_insert_text(compl + strlen(argv[argc-1])) < 0) {
                        free(compl);
                        goto end;
                }
                free(compl);
-               rc = CC_REDISPLAY;
+               rc = 0;
                goto end;
        }
-       /* No completion or several completion available. We beep. */
-       el_beep(el);
-       rc = CC_REDISPLAY;
+       /* No completion or several completion available. */
+       free(compl);
+       fprintf(stderr, "\n");
+       rl_forced_update_display();
+       rc = 0;
 end:
-       if (eltok) tok_end(eltok);
+       free(line);
+       tokenize_free(argc, argv);
        return rc;
 }
 
-static unsigned char
-cmd_complete(EditLine *el, int ch)
+static int
+cmd_complete(int count, int ch)
 {
-       return _cmd_complete(el, ch, 0);
+       return _cmd_complete(0);
 }
 
-static unsigned char
-cmd_help(EditLine *el, int ch)
+static int
+cmd_help(int count, int ch)
 {
-       return _cmd_complete(el, ch, 1);
+       return _cmd_complete(1);
+}
+#else
+static char*
+readline(const char *p)
+{
+       static char line[2048];
+       fprintf(stderr, "%s", p);
+       fflush(stderr);
+       if (fgets(line, sizeof(line) - 2, stdin) == NULL)
+               return NULL;
+       return strdup(line);
+}
+#endif
+
+/**
+ * Execute a tokenized command and display its output.
+ *
+ * @param conn The connection to lldpd.
+ * @param fmt  Output format.
+ * @param argc Number of arguments.
+ * @param argv Array of arguments.
+ * @return 0 if an error occurred, 1 otherwise
+ */
+static int
+cmd_exec(lldpctl_conn_t *conn, const char *fmt, int argc, const char **argv)
+{
+       /* Init output formatter */
+       struct writer *w;
+
+       if      (strcmp(fmt, "plain")    == 0) w = txt_init(stdout);
+       else if (strcmp(fmt, "keyvalue") == 0) w = kv_init(stdout);
+       else if (strcmp(fmt, "json")     == 0) w = json_init(stdout, 1);
+       else if (strcmp(fmt, "json0")    == 0) w = json_init(stdout, 0);
+#ifdef USE_XML
+       else if (strcmp(fmt, "xml")      == 0) w = xml_init(stdout);
+#endif
+       else {
+               log_warnx("lldpctl", "unknown output format \"%s\"", fmt);
+               w = txt_init(stdout);
+       }
+
+       /* Execute command */
+       int rc = commands_execute(conn, w,
+           root, argc, argv, is_privileged());
+       if (rc != 0) {
+               log_info("lldpctl", "an error occurred while executing last command");
+               w->finish(w);
+               return 0;
+       }
+       w->finish(w);
+       return 1;
+}
+
+/**
+ * Execute a command line and display its output.
+ *
+ * @param conn The connection to lldpd.
+ * @param fmt  Output format.
+ * @param line Line to execute.
+ * @return -1 if an error occurred, 0 if nothing was executed. 1 otherwise.
+ */
+static int
+parse_and_exec(lldpctl_conn_t *conn, const char *fmt, const char *line)
+{
+       int cargc = 0; char **cargv = NULL;
+       int n;
+       log_debug("lldpctl", "tokenize command line");
+       n = tokenize_line(line, &cargc, &cargv);
+       switch (n) {
+       case -1:
+               log_warnx("lldpctl", "internal error while tokenizing");
+               return -1;
+       case 1:
+               log_warnx("lldpctl", "unmatched quotes");
+               return -1;
+       }
+       if (cargc != 0)
+               n = cmd_exec(conn, fmt, cargc, (const char **)cargv);
+       tokenize_free(cargc, cargv);
+       return (cargc == 0)?0:
+           (n == 0)?-1:
+           1;
 }
 
 static struct cmd_node*
@@ -164,210 +331,277 @@ register_commands()
        root = commands_root();
        register_commands_show(root);
        register_commands_watch(root);
-       if (is_privileged()) {
-               commands_new(
-                       commands_new(root, "update", "Update information and send LLDPU on all ports",
-                           NULL, NULL, NULL),
-                       NEWLINE, "Update information and send LLDPU on all ports",
-                       NULL, cmd_update, NULL);
-               register_commands_configure(root);
-       }
+       commands_privileged(commands_new(
+               commands_new(root, "update", "Update information and send LLDPU on all ports",
+                   NULL, NULL, NULL),
+               NEWLINE, "Update information and send LLDPU on all ports",
+               NULL, cmd_update, NULL));
+       register_commands_configure(root);
+       commands_hidden(commands_new(root, "complete", "Get possible completions from a given command",
+               NULL, cmd_store_env_and_pop, "complete"));
+       commands_new(root, "help", "Get help on a possible command",
+           NULL, cmd_store_env_and_pop, "help");
+       commands_new(
+               commands_new(root, "pause", "Pause lldpd operations", NULL, NULL, NULL),
+               NEWLINE, "Pause lldpd operations", NULL, cmd_pause, NULL);
+       commands_new(
+               commands_new(root, "resume", "Resume lldpd operations", NULL, NULL, NULL),
+               NEWLINE, "Resume lldpd operations", NULL, cmd_resume, NULL);
        commands_new(
                commands_new(root, "exit", "Exit interpreter", NULL, NULL, NULL),
                NEWLINE, "Exit interpreter", NULL, cmd_exit, NULL);
        return root;
 }
 
+struct input {
+       TAILQ_ENTRY(input) next;
+       char *name;
+};
+TAILQ_HEAD(inputs, input);
 static int
-is_lldpctl(const char *name)
+filter(const struct dirent *dir)
 {
-       static int last_result = -1;
-       if (last_result == -1 && name) {
-               char *basec = strdup(name);
-               if (!basec) return 0;
-               char *bname = basename(basec);
-               last_result = (!strcmp(bname, "lldpctl"));
-               free(basec);
+       if (strlen(dir->d_name) < 5) return 0;
+       if (strcmp(dir->d_name + strlen(dir->d_name) - 5, ".conf")) return 0;
+       return 1;
+}
+
+/**
+ * Append a new input file/directory to the list of inputs.
+ *
+ * @param arg       Directory or file name to add.
+ * @param inputs    List of inputs
+ * @param acceptdir 1 if we accept a directory, 0 otherwise
+ */
+static void
+input_append(const char *arg, struct inputs *inputs, int acceptdir, int warn)
+{
+       struct stat statbuf;
+       if (stat(arg, &statbuf) == -1) {
+               if (warn) {
+                       log_warn("lldpctl", "cannot find configuration file/directory %s",
+                           arg);
+               } else {
+                       log_debug("lldpctl", "cannot find configuration file/directory %s",
+                           arg);
+               }
+               return;
        }
-       return (last_result == -1)?0:last_result;
+
+       if (!S_ISDIR(statbuf.st_mode)) {
+               struct input *input = malloc(sizeof(struct input));
+               if (!input) {
+                       log_warn("lldpctl", "not enough memory to process %s",
+                           arg);
+                       return;
+               }
+               log_debug("lldpctl", "input: %s", arg);
+               input->name = strdup(arg);
+               TAILQ_INSERT_TAIL(inputs, input, next);
+               return;
+       }
+       if (!acceptdir) {
+               log_debug("lldpctl", "skip directory %s",
+                   arg);
+               return;
+       }
+
+       struct dirent **namelist = NULL;
+       int n = scandir(arg, &namelist, filter, alphasort);
+       if (n < 0) {
+               log_warnx("lldpctl", "unable to read directory %s",
+                   arg);
+               return;
+       }
+       for (int i=0; i < n; i++) {
+               char *fullname;
+               if (asprintf(&fullname, "%s/%s", arg, namelist[i]->d_name) != -1) {
+                       input_append(fullname, inputs, 0, 1);
+                       free(fullname);
+               }
+               free(namelist[i]);
+       }
+       free(namelist);
 }
 
 int
 main(int argc, char *argv[])
 {
-       int ch, debug = 1, rc = EXIT_FAILURE;
-       char *fmt = "plain";
-       lldpctl_conn_t *conn;
-       struct writer *w;
+       int ch, debug = 0, use_syslog = 0, rc = EXIT_FAILURE;
+       const char *fmt = "plain";
+       lldpctl_conn_t *conn = NULL;
+       const char *options = is_lldpctl(argv[0])?"hdvf:u:":"hdsvf:c:C:u:";
 
-       EditLine  *el = NULL;
-       History   *elhistory = NULL;
-       HistEvent  elhistev;
-       Tokenizer *eltok = NULL;
+       int gotinputs = 0, version = 0;
+       struct inputs inputs;
+       TAILQ_INIT(&inputs);
 
-       char *interfaces = NULL;
+       ctlname = lldpctl_get_default_transport();
+
+       signal(SIGHUP, SIG_IGN);
 
        /* Get and parse command line options */
-       while ((ch = getopt(argc, argv, "hdvf:")) != -1) {
+       optind = 1;
+       while ((ch = getopt(argc, argv, options)) != -1) {
                switch (ch) {
+               case 'd':
+                       if (use_syslog)
+                               use_syslog = 0;
+                       else
+                               debug++;
+                       break;
+               case 's':
+                       if (debug == 0)
+                               use_syslog = 1;
+                       else
+                               debug--;
+                       break;
                case 'h':
                        usage();
                        break;
-               case 'd':
-                       debug++;
+               case 'u':
+                       ctlname = optarg;
                        break;
                case 'v':
-                       fprintf(stdout, "%s\n", PACKAGE_VERSION);
-                       exit(0);
+                       version++;
                        break;
                case 'f':
                        fmt = optarg;
                        break;
+               case 'C':
+               case 'c':
+                       if (!gotinputs) {
+                               log_init(use_syslog, debug, __progname);
+                               lldpctl_log_level(debug + 1);
+                               gotinputs = 1;
+                       }
+                       input_append(optarg, &inputs, 1, ch == 'c');
+                       break;
                default:
                        usage();
                }
        }
 
-       log_init(debug, __progname);
+       if (version) {
+               version_display(stdout, "lldpcli", version > 1);
+               exit(0);
+       }
+
+       if (!gotinputs) {
+               log_init(use_syslog, debug, __progname);
+               lldpctl_log_level(debug + 1);
+       }
+
+       /* Disable SIGPIPE */
+       signal(SIGPIPE, SIG_IGN);
 
        /* Register commands */
        root = register_commands();
 
-       if (is_lldpctl(argv[0])) {
+       /* Make a connection */
+       log_debug("lldpctl", "connect to lldpd");
+       conn = lldpctl_new_name(ctlname, NULL, NULL, NULL);
+       if (conn == NULL) goto end;
+
+       /* Process file inputs */
+       while (gotinputs && !TAILQ_EMPTY(&inputs)) {
+               /* coverity[use_after_free]
+                  TAILQ_REMOVE does the right thing */
+               struct input *first = TAILQ_FIRST(&inputs);
+               log_debug("lldpctl", "process: %s", first->name);
+               FILE *file = fopen(first->name, "r");
+               if (file) {
+                       size_t n;
+                       ssize_t len;
+                       char *line;
+                       while (line = NULL, len = 0, (len = getline(&line, &n, file)) > 0) {
+                               if (line[len - 1] == '\n') {
+                                       line[len - 1] = '\0';
+                                       parse_and_exec(conn, fmt, line);
+                               }
+                               free(line);
+                       }
+                       free(line);
+                       fclose(file);
+               } else {
+                       log_warn("lldpctl", "unable to open %s",
+                           first->name);
+               }
+               TAILQ_REMOVE(&inputs, first, next);
+               free(first->name);
+               free(first);
+       }
+
+       /* Process additional arguments. First if we are lldpctl (interfaces) */
+       if (is_lldpctl(NULL)) {
+               char *line = NULL;
                for (int i = optind; i < argc; i++) {
-                       char *prev = interfaces;
-                       if (asprintf(&interfaces, "%s%s%s",
-                               prev?prev:"", prev?",":"", argv[i]) == -1) {
+                       char *prev = line;
+                       if (asprintf(&line, "%s%s%s",
+                               prev?prev:"show neigh ports ", argv[i],
+                               (i == argc - 1)?" details":",") == -1) {
                                log_warnx("lldpctl", "not enough memory to build list of interfaces");
+                               free(prev);
                                goto end;
                        }
                        free(prev);
                }
-               must_exit = 1;
-               goto skipeditline;
-       } else {
-               if (optind < argc) {
-                       /* More arguments! */
-                       must_exit = 1;
+               if (line == NULL && (line = strdup("show neigh details")) == NULL) {
+                       log_warnx("lldpctl", "not enough memory to build command line");
+                       goto end;
                }
-       }
-
-       /* Init editline */
-       log_debug("lldpctl", "init editline");
-       el = el_init("lldpctl", stdin, stdout, stderr);
-       if (el == NULL) {
-               log_warnx("lldpctl", "unable to setup editline");
+               log_debug("lldpctl", "execute %s", line);
+               if (parse_and_exec(conn, fmt, line) != -1)
+                       rc = EXIT_SUCCESS;
+               free(line);
                goto end;
        }
-       el_set(el, EL_PROMPT, prompt);
-       el_set(el, EL_SIGNAL, 1);
-       el_set(el, EL_EDITOR, "emacs");
-       /* If on a TTY, setup completion */
-       if (isatty(STDERR_FILENO)) {
-               el_set(el, EL_ADDFN, "command_complete",
-                   "Execute completion", cmd_complete);
-               el_set(el, EL_ADDFN, "command_help",
-                   "Show completion", cmd_help);
-               el_set(el, EL_BIND, "^I", "command_complete", NULL);
-               el_set(el, EL_BIND, "?", "command_help", NULL);
-       }
 
-       /* Init history */
-       elhistory = history_init();
-       if (elhistory == NULL) {
-               log_warnx("lldpctl", "unable to enable history");
-       } else {
-               history(elhistory, &elhistev, H_SETSIZE, 800);
-               el_set(el, EL_HIST, history, elhistory);
+       /* Then, if we are regular lldpcli (command line) */
+       if (optind < argc) {
+               const char **cargv;
+               int cargc;
+               cargv = &((const char **)argv)[optind];
+               cargc = argc - optind;
+               if (cmd_exec(conn, fmt, cargc, cargv) == 1)
+                       rc = EXIT_SUCCESS;
+               goto end;
        }
 
-       /* Init tokenizer */
-       eltok = tok_init(NULL);
-       if (eltok == NULL) {
-               log_warnx("lldpctl", "unable to initialize tokenizer");
+       if (gotinputs) {
+               rc = EXIT_SUCCESS;
                goto end;
        }
 
-skipeditline:
-       /* Make a connection */
-       log_debug("lldpctl", "connect to lldpd");
-       conn = lldpctl_new(NULL, NULL, NULL);
-       if (conn == NULL)
-               exit(EXIT_FAILURE);
-
-       do {
-               const char *line;
-               const char **cargv;
-               int count, n, cargc;
-
-               if (!is_lldpctl(NULL) && (optind >= argc)) {
-                       /* Read a new line. */
-                       line = el_gets(el, &count);
-                       if (line == NULL) break;
-
-                       /* Tokenize it */
-                       log_debug("lldpctl", "tokenize command line");
-                       n = tok_str(eltok, line, &cargc, &cargv);
-                       switch (n) {
-                       case -1:
-                               log_warnx("lldpctl", "internal error while tokenizing");
-                               goto end;
-                       case 1:
-                       case 2:
-                       case 3:
-                               /* TODO: handle multiline statements */
-                               log_warnx("lldpctl", "unmatched quotes");
-                               tok_reset(eltok);
-                               continue;
-                       }
-                       if (cargc == 0) {
-                               tok_reset(eltok);
-                               continue;
-                       }
-                       if (elhistory) history(elhistory, &elhistev, H_ENTER, line);
-               }
-
-               /* Init output formatter */
-               if      (strcmp(fmt, "plain")    == 0) w = txt_init(stdout);
-               else if (strcmp(fmt, "keyvalue") == 0) w = kv_init(stdout);
-#ifdef USE_XML
-               else if (strcmp(fmt, "xml")      == 0) w = xml_init(stdout);
+       /* Interactive session */
+#ifdef HAVE_LIBREADLINE
+       rl_bind_key('?',  cmd_help);
+       rl_bind_key('\t', cmd_complete);
 #endif
-#ifdef USE_JSON
-               else if (strcmp(fmt, "json")     == 0) w = json_init(stdout);
+       char *line = NULL;
+       do {
+               if ((line = readline(prompt()))) {
+                       int n = parse_and_exec(conn, fmt, line);
+                       if (n != 0) {
+#ifdef HAVE_READLINE_HISTORY
+                               add_history(line);
 #endif
-               else w = txt_init(stdout);
-
-               if (is_lldpctl(NULL)) {
-                       if (!interfaces) {
-                               cargv = (const char*[]){ "show", "neighbors", "details" };
-                               cargc = 3;
-                       } else {
-                               cargv = (const char*[]){ "show", "neighbors", "ports", interfaces, "details" };
-                               cargc = 5;
                        }
-               } else if (optind < argc) {
-                       cargv = (const char **)argv;
-                       cargv = &cargv[optind];
-                       cargc = argc - optind;
+                       free(line);
                }
-
-               /* Execute command */
-               if (commands_execute(conn, w,
-                       root, cargc, cargv) != 0)
-                       log_info("lldpctl", "an error occurred while executing last command");
-               w->finish(w);
-
-               if (eltok) tok_reset(eltok);
-       } while (!must_exit);
-
+       } while (!must_exit && line != NULL);
        rc = EXIT_SUCCESS;
+
 end:
+       while (!TAILQ_EMPTY(&inputs)) {
+               /* coverity[use_after_free]
+                  TAILQ_REMOVE does the right thing */
+               struct input *first = TAILQ_FIRST(&inputs);
+               TAILQ_REMOVE(&inputs, first, next);
+               free(first->name);
+               free(first);
+       }
        if (conn) lldpctl_release(conn);
-       if (eltok) tok_end(eltok);
-       if (elhistory) history_end(elhistory);
-       if (el) el_end(el);
        if (root) commands_free(root);
-       free(interfaces);
        return rc;
 }