ctdb-common: Factor out basic script abstraction
authorMartin Schwenke <martin@meltin.net>
Thu, 12 Jul 2018 03:20:55 +0000 (13:20 +1000)
committerAmitay Isaacs <amitay@samba.org>
Sat, 28 Jul 2018 15:14:11 +0000 (17:14 +0200)
Provides for listing of scripts and chmod enable/disable.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=13551

Signed-off-by: Martin Schwenke <martin@meltin.net>
Reviewed-by: Amitay Isaacs <amitay@gmail.com>
ctdb/common/event_script.c [new file with mode: 0644]
ctdb/common/event_script.h [new file with mode: 0644]
ctdb/tests/cunit/event_script_test_001.sh [new file with mode: 0755]
ctdb/tests/src/event_script_test.c [new file with mode: 0644]
ctdb/wscript

diff --git a/ctdb/common/event_script.c b/ctdb/common/event_script.c
new file mode 100644 (file)
index 0000000..8978d14
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+   Low level event script handling
+
+   Copyright (C) Amitay Isaacs  2017
+   Copyright (C) Martin Schwenke  2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "replace.h"
+#include "system/filesys.h"
+#include "system/dir.h"
+#include "system/glob.h"
+
+#include <talloc.h>
+
+#include "common/event_script.h"
+
+static int script_filter(const struct dirent *de)
+{
+       int ret;
+
+       /* Match a script pattern */
+       ret = fnmatch("[0-9][0-9].*.script", de->d_name, 0);
+       if (ret == 0) {
+               return 1;
+       }
+
+       return 0;
+}
+
+int event_script_get_list(TALLOC_CTX *mem_ctx,
+                         const char *script_dir,
+                         struct event_script_list **out)
+{
+       struct dirent **namelist = NULL;
+       struct event_script_list *script_list = NULL;
+       size_t ds_len;
+       int count, ret;
+       int i;
+
+       count = scandir(script_dir, &namelist, script_filter, alphasort);
+       if (count == -1) {
+               ret = errno;
+               goto done;
+       }
+
+       script_list = talloc_zero(mem_ctx, struct event_script_list);
+       if (script_list == NULL) {
+               goto nomem;
+       }
+
+       if (count == 0) {
+               ret = 0;
+               *out = script_list;
+               goto done;
+       }
+
+       script_list->num_scripts = count;
+       script_list->script = talloc_zero_array(script_list,
+                                               struct event_script *,
+                                               count);
+       if (script_list->script == NULL) {
+               goto nomem;
+       }
+
+       ds_len = strlen(".script");
+       for (i = 0; i < count; i++) {
+               struct event_script *s;
+               struct stat statbuf;
+
+               s = talloc_zero(script_list->script, struct event_script);
+               if (s == NULL) {
+                       goto nomem;
+               }
+
+               script_list->script[i] = s;
+
+               s->name = talloc_strndup(script_list->script,
+                                        namelist[i]->d_name,
+                                        strlen(namelist[i]->d_name) - ds_len);
+               if (s->name == NULL) {
+                       goto nomem;
+               }
+
+               s->path = talloc_asprintf(script_list->script,
+                                         "%s/%s",
+                                         script_dir,
+                                         namelist[i]->d_name);
+               if (s->path == NULL) {
+                       goto nomem;
+               }
+
+               ret = stat(s->path, &statbuf);
+               if (ret == 0) {
+                       /*
+                        * If ret != 0 this is either a dangling
+                        * symlink or it has just disappeared.  Either
+                        * way, it isn't executable.  See the note
+                        * below about things that have disappeared.
+                        */
+                       if (statbuf.st_mode & S_IXUSR) {
+                               s->enabled = true;
+                       }
+               }
+       }
+
+       *out = script_list;
+       return 0;
+
+nomem:
+       ret = ENOMEM;
+       talloc_free(script_list);
+
+done:
+       if (namelist != NULL && count != -1) {
+               for (i=0; i<count; i++) {
+                       free(namelist[i]);
+               }
+               free(namelist);
+       }
+
+       return ret;
+}
+
+int event_script_chmod(const char *script_dir,
+                      const char *script_name,
+                      bool enable)
+{
+       const char *dot_script = ".script";
+       size_t ds_len = strlen(dot_script);
+       size_t sn_len = strlen(script_name);
+       DIR *dirp;
+       struct dirent *de;
+       char buf[PATH_MAX];
+       const char *script_file;
+       int ret, new_mode;
+       char filename[PATH_MAX];
+       struct stat st;
+       bool found;
+       ino_t found_inode;
+       int fd = -1;
+
+       /* Allow script_name to already have ".script" suffix */
+       if (sn_len > ds_len &&
+           strcmp(&script_name[sn_len - ds_len], dot_script) == 0) {
+               script_file = script_name;
+       } else {
+               ret = snprintf(buf, sizeof(buf), "%s.script", script_name);
+               if (ret >= sizeof(buf)) {
+                       return ENAMETOOLONG;
+               }
+               script_file = buf;
+       }
+
+       dirp = opendir(script_dir);
+       if (dirp == NULL) {
+               return errno;
+       }
+
+       found = false;
+       while ((de = readdir(dirp)) != NULL) {
+               if (strcmp(de->d_name, script_file) == 0) {
+                       /* check for valid script names */
+                       ret = script_filter(de);
+                       if (ret == 0) {
+                               closedir(dirp);
+                               return EINVAL;
+                       }
+
+                       found = true;
+                       found_inode = de->d_ino;
+                       break;
+               }
+       }
+       closedir(dirp);
+
+       if (! found) {
+               return ENOENT;
+       }
+
+       ret = snprintf(filename,
+                      sizeof(filename),
+                      "%s/%s",
+                      script_dir,
+                      script_file);
+       if (ret >= sizeof(filename)) {
+               return ENAMETOOLONG;
+       }
+
+       fd = open(filename, O_RDWR);
+       if (fd == -1) {
+               ret = errno;
+               goto done;
+       }
+
+       ret = fstat(fd, &st);
+       if (ret != 0) {
+               ret = errno;
+               goto done;
+       }
+
+       /*
+        * If the directory entry inode number doesn't match the one
+        * returned by fstat() then this is probably a symlink, so the
+        * caller should not be calling this function.  Note that this
+        * is a cheap sanity check to catch most programming errors.
+        * This doesn't cost any extra system calls but can still miss
+        * the unlikely case where the symlink is to a file on a
+        * different filesystem with the same inode number as the
+        * symlink.
+        */
+       if (found && found_inode != st.st_ino) {
+               ret = EINVAL;
+               goto done;
+       }
+
+       if (enable) {
+               new_mode = st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH);
+       } else {
+               new_mode = st.st_mode & ~(S_IXUSR | S_IXGRP | S_IXOTH);
+       }
+
+       ret = fchmod(fd, new_mode);
+       if (ret != 0) {
+               ret = errno;
+               goto done;
+       }
+
+done:
+       if (fd != -1) {
+               close(fd);
+       }
+       return ret;
+}
diff --git a/ctdb/common/event_script.h b/ctdb/common/event_script.h
new file mode 100644 (file)
index 0000000..bf5a8fd
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+   Low level event script handling
+
+   Copyright (C) Amitay Isaacs  2017
+   Copyright (C) Martin Schwenke  2018
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef __CTDB_SCRIPT_H__
+#define __CTDB_SCRIPT_H__
+
+#include "replace.h"
+#include "system/filesys.h"
+
+#include <talloc.h>
+
+/**
+ * @file script.h
+ *
+ * @brief Script listing and manipulation
+ */
+
+
+struct event_script {
+       char *name;
+       char *path;
+       bool enabled;
+};
+
+struct event_script_list {
+       unsigned int num_scripts;
+       struct event_script **script;
+};
+
+
+/**
+ * @brief Retrieve a list of scripts
+ *
+ * @param[in] mem_ctx Talloc memory context
+ * @param[in] script_dir Directory containing scripts
+ * @param[out] out List of scripts
+ * @return 0 on success, errno on failure
+ */
+int event_script_get_list(TALLOC_CTX *mem_ctx,
+                         const char *script_dir,
+                         struct event_script_list **out);
+
+/**
+ * @brief Make a script executable or not executable
+ *
+ * @param[in] script_dir Directory containing script
+ * @param[in] script_name Name of the script to enable
+ * @param[in] executable True if script should be made executable
+ * @return 0 on success, errno on failure
+ */
+int event_script_chmod(const char *script_dir,
+                      const char *script_name,
+                      bool executable);
+
+#endif /* __CTDB_SCRIPT_H__ */
diff --git a/ctdb/tests/cunit/event_script_test_001.sh b/ctdb/tests/cunit/event_script_test_001.sh
new file mode 100755 (executable)
index 0000000..9a26412
--- /dev/null
@@ -0,0 +1,125 @@
+#!/bin/sh
+
+. "${TEST_SCRIPTS_DIR}/unit.sh"
+
+scriptdir="${TEST_VAR_DIR}/cunit/scriptdir"
+mkdir -p "${scriptdir}"
+
+test_cleanup "rm -rf ${scriptdir}"
+
+# Invalid path
+invalid="${scriptdir}/notfound"
+ok <<EOF
+Script list ${invalid} failed with result=$(errcode ENOENT)
+EOF
+unit_test event_script_test list "${invalid}"
+
+# Empty directory
+ok <<EOF
+No scripts found
+EOF
+unit_test event_script_test list "$scriptdir"
+
+# Invalid script, doesn't end in ".script"
+touch "${scriptdir}/prog"
+
+ok <<EOF
+No scripts found
+EOF
+unit_test event_script_test list "$scriptdir"
+
+# Is not found because enabling "prog" actually looks for "prog.script"
+ok <<EOF
+Script enable ${scriptdir} prog completed with result=$(errcode ENOENT)
+EOF
+unit_test event_script_test enable "$scriptdir" "prog"
+
+required_result 1 <<EOF
+EOF
+unit_test test -x "${scriptdir}/prog"
+
+# Is not found because enabling "prog" actually looks for "prog.script"
+ok <<EOF
+Script disable ${scriptdir} prog completed with result=$(errcode ENOENT)
+EOF
+unit_test event_script_test disable "$scriptdir" "prog"
+
+# Valid script
+touch "$scriptdir/11.foo.script"
+
+ok <<EOF
+11.foo
+EOF
+unit_test event_script_test list "$scriptdir"
+
+ok <<EOF
+Script enable ${scriptdir} 11.foo completed with result=0
+EOF
+unit_test event_script_test enable "$scriptdir" "11.foo"
+
+ok <<EOF
+EOF
+unit_test test -x "${scriptdir}/11.foo.script"
+
+ok <<EOF
+Script disable ${scriptdir} 11.foo.script completed with result=0
+EOF
+unit_test event_script_test disable "$scriptdir" "11.foo.script"
+
+required_result 1 <<EOF
+EOF
+unit_test test -x "${scriptdir}/11.foo.script"
+
+# Multiple scripts
+touch "${scriptdir}/22.bar.script"
+
+ok <<EOF
+11.foo
+22.bar
+EOF
+unit_test event_script_test list "$scriptdir"
+
+# Symlink to existing file
+ln -s "${scriptdir}/prog" "${scriptdir}/33.link.script"
+
+ok <<EOF
+11.foo
+22.bar
+33.link
+EOF
+unit_test event_script_test list "$scriptdir"
+
+ok <<EOF
+Script enable ${scriptdir} 33.link completed with result=$(errcode EINVAL)
+EOF
+unit_test event_script_test enable "$scriptdir" "33.link"
+
+
+ok <<EOF
+Script disable ${scriptdir} 33.link.script completed with result=$(errcode EINVAL)
+EOF
+unit_test event_script_test disable "$scriptdir" "33.link.script"
+
+# Dangling symlink
+rm "${scriptdir}/33.link.script"
+ln -s "${scriptdir}/nosuchfile" "${scriptdir}/33.link.script"
+
+ok <<EOF
+11.foo
+22.bar
+33.link
+EOF
+unit_test event_script_test list "$scriptdir"
+
+ok <<EOF
+Script enable ${scriptdir} 33.link completed with result=$(errcode ENOENT)
+EOF
+unit_test event_script_test enable "$scriptdir" "33.link"
+
+
+ok <<EOF
+Script disable ${scriptdir} 33.link.script completed with result=$(errcode ENOENT)
+EOF
+unit_test event_script_test disable "$scriptdir" "33.link.script"
+
+exit 0
diff --git a/ctdb/tests/src/event_script_test.c b/ctdb/tests/src/event_script_test.c
new file mode 100644 (file)
index 0000000..73c974d
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+   Low level event script handling tests
+
+   Copyright (C) Martin Schwenke  2018
+
+   Based on run_event_test.c:
+
+     Copyright (C) Amitay Isaacs  2017
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "replace.h"
+
+#include <popt.h>
+#include <talloc.h>
+
+#include <assert.h>
+
+#include "common/event_script.c"
+
+static void usage(const char *prog)
+{
+       fprintf(stderr,
+               "Usage: %s list <scriptdir>\n",
+               prog);
+       fprintf(stderr,
+               "       %s chmod enable <scriptdir> <scriptname>\n",
+               prog);
+       fprintf(stderr,
+               "       %s chmod diable <scriptdir> <scriptname>\n",
+               prog);
+}
+
+static void do_list(TALLOC_CTX *mem_ctx, int argc, const char **argv)
+{
+       struct event_script_list *script_list = NULL;
+       int ret, i;
+
+       if (argc != 3) {
+               usage(argv[0]);
+               exit(1);
+       }
+
+       ret = event_script_get_list(mem_ctx, argv[2], &script_list);
+       if (ret != 0) {
+               printf("Script list %s failed with result=%d\n", argv[2], ret);
+               return;
+       }
+
+       if (script_list == NULL || script_list->num_scripts == 0) {
+               printf("No scripts found\n");
+               return;
+       }
+
+       for (i=0; i < script_list->num_scripts; i++) {
+               struct event_script *s = script_list->script[i];
+               printf("%s\n", s->name);
+       }
+}
+
+static void do_chmod(TALLOC_CTX *mem_ctx,
+                    int argc,
+                    const char **argv,
+                    bool enable)
+{
+       int ret;
+
+       if (argc != 4) {
+               usage(argv[0]);
+               exit(1);
+       }
+
+       ret = event_script_chmod(argv[2], argv[3], enable);
+
+       printf("Script %s %s %s completed with result=%d\n",
+              argv[1], argv[2], argv[3], ret);
+}
+
+int main(int argc, const char **argv)
+{
+       TALLOC_CTX *mem_ctx;
+
+       if (argc < 3) {
+               usage(argv[0]);
+               exit(1);
+       }
+
+       mem_ctx = talloc_new(NULL);
+       if (mem_ctx == NULL) {
+               fprintf(stderr, "talloc_new() failed\n");
+               exit(1);
+       }
+
+       if (strcmp(argv[1], "list") == 0) {
+               do_list(mem_ctx, argc, argv);
+       } else if (strcmp(argv[1], "enable") == 0) {
+               do_chmod(mem_ctx, argc, argv, true);
+       } else if (strcmp(argv[1], "disable") == 0) {
+               do_chmod(mem_ctx, argc, argv, false);
+       } else {
+               fprintf(stderr, "Invalid command %s\n", argv[2]);
+               usage(argv[0]);
+       }
+
+       talloc_free(mem_ctx);
+       exit(0);
+}
index 6e69e4999858e3b6ab886bacb209d7750e1e584e..05044cebdebe72041e954bf89a143c35c73facd5 100644 (file)
@@ -402,7 +402,8 @@ def build(bld):
                                              pkt_read.c pkt_write.c comm.c
                                              logging.c rb_tree.c tunable.c
                                              pidfile.c run_proc.c
-                                             hash_count.c run_event.c
+                                             hash_count.c
+                                             run_event.c event_script.c
                                              sock_client.c version.c
                                              cmdline.c path.c conf.c line.c
                                           '''),
@@ -869,6 +870,7 @@ def build(bld):
         'cmdline_test',
         'conf_test',
         'line_test',
+        'event_script_test',
     ]
 
     for target in ctdb_unit_tests: