diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index d3ea2807..b9b1b22a 100644 --- a/src/klish-plugin-infix/src/infix.c +++ b/src/klish-plugin-infix/src/infix.c @@ -3,8 +3,11 @@ #include #include #include +#include #include +#include #include +#include #include #include @@ -16,10 +19,136 @@ #include #define ERRMSG "Error: " +#define INFMSG "Note: " + +#ifndef NELEMS +#define NELEMS(v) (sizeof(v) / sizeof(v[0])) +#endif const uint8_t kplugin_infix_major = 1; const uint8_t kplugin_infix_minor = 0; +struct infix_ds { + char *name; /* startup-config, etc. */ + char *sysrepocfg; /* ds name in sysrepocfg */ + int datastore; /* sr_datastore_t and -1 */ + int rw; /* read-write:1 or not:0 */ + char *path; /* local path or NULL */ +}; + +struct infix_ds infix_config[] = { + { "startup-config", "startup", SR_DS_STARTUP, 1, "/cfg/startup-config.cfg" }, + { "running-config", "running", SR_DS_RUNNING, 1, NULL }, + { "candidate-config", "candidate", SR_DS_CANDIDATE, 1, NULL }, + { "operational-config", "operational", SR_DS_OPERATIONAL, 1, NULL }, + { "factory-config", NULL, SR_DS_FACTORY_DEFAULT, 0, "/cfg/factory-config.cfg" }, +}; + +static int has_ext(const char *fn, const char *ext) +{ + size_t pos = strlen(fn); + size_t len = strlen(ext); + + if (len < pos && !strcmp(&fn[pos - len], ext)) + return pos - len; + return 0; +} + +static char *cfg_adjust(const char *fn, char *buf, size_t len) +{ + if (strncmp(fn, "/cfg/", 5)) { + if (fn[0] == '.' || fn[0] == '/') + return NULL; + + snprintf(buf, len, "/cfg/%s", fn); + } + + if (!has_ext(buf, ".cfg")) + strcat(buf, ".cfg"); + + return buf; +} + +static char rawgetch(void) +{ + struct termios saved, c; + char key; + + if (tcgetattr(fileno(stdin), &saved) < 0) + return -1; + + c = saved; + c.c_lflag &= ~ICANON; + c.c_lflag &= ~ECHO; + c.c_cc[VMIN] = 1; + c.c_cc[VTIME] = 0; + + if (tcsetattr(fileno(stdin), TCSANOW, &c) < 0) { + tcsetattr(fileno(stdin), TCSANOW, &saved); + return -1; + } + + key = getchar(); + tcsetattr(fileno(stdin), TCSANOW, &saved); + + return key; +} + +static int yorn(const char *fmt, ...) +{ + va_list ap; + char ch; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, " (y/N)? "); + ch = rawgetch(); + fprintf(stderr, "%c\n", ch); + if (ch != 'y' && ch != 'Y') + return 0; + + return 1; +} + +static int systemf(const char *fmt, ...) +{ + va_list ap; + char *cmd; + int len; + int rc; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + cmd = alloca(++len); + if (!cmd) { + errno = ENOMEM; + return -1; + } + + va_start(ap, fmt); + vsnprintf(cmd, len, fmt, ap); + va_end(ap); + +// fprintf(stderr, INFMSG "CMD: '%s'\n", cmd); + + rc = system(cmd); + if (rc == -1) + return -1; + + if (WIFEXITED(rc)) { + errno = 0; + rc = WEXITSTATUS(rc); + } else if (WIFSIGNALED(rc)) { + errno = EINTR; + rc = -1; + } + + return rc; +} /* * Print sysrepo session errors followed by an optional string. @@ -51,18 +180,11 @@ end: } } -int infix_files(kcontext_t *ctx) +static int files(const char *path, const char *stripext) { - const char *path; struct dirent *d; DIR *dir; - path = kcontext_script(ctx); - if (!path) { - fprintf(stderr, ERRMSG "missing path argument to file search.\n"); - return -1; - } - dir = opendir(path); if (!dir) { fprintf(stderr, ERRMSG "%s", strerror(errno)); @@ -70,16 +192,99 @@ int infix_files(kcontext_t *ctx) } while ((d = readdir(dir))) { + char name[sizeof(d->d_name) + 1]; + if (d->d_type != DT_REG) continue; - printf("%s\n", d->d_name); + strncpy(name, d->d_name, sizeof(name)); + if (stripext) { + size_t pos = has_ext(name, stripext); + + if (pos) + name[pos] = 0; + } + + printf("%s\n", name); + } + + return closedir(dir); +} + +int infix_datastore(kcontext_t *ctx) +{ + const char *ds; + + ds = kcontext_script(ctx); + if (!ds) + goto done; + + if (!strcmp(ds, "src")) { + puts("factory-config"); + puts("running-config"); + } + if (!strcmp(ds, "dst")) { + puts("running-config"); + } + +done: + return files("/cfg", ".cfg"); +} + +int infix_dir(kcontext_t *ctx) +{ + return systemf("ls --color=always --group-directories-first -A /cfg"); +} + +int infix_erase(kcontext_t *ctx) +{ + kpargv_t *pargv = kcontext_pargv(ctx); + const char *path; + char *fn; + + path = kparg_value(kpargv_find(pargv, "file")); + if (!path) { + fprintf(stderr, ERRMSG "missing file argument to remove.\n"); + return -1; + } + + if (access(path, F_OK)) { + size_t len = strlen(path) + 10; + + fn = alloca(len); + if (!fn) { + fprintf(stderr, ERRMSG "failed allocating memory.\n"); + return -1; + } + + cfg_adjust(path, fn, len); + } else + fn = (char *)path; + + if (!yorn("Remove %s, are you sure", fn)) + return 0; + + if (remove(fn)) { + fprintf(stderr, ERRMSG "failed removing %s: %s\n", fn, strerror(errno)); + return -1; } - closedir(dir); return 0; } +int infix_files(kcontext_t *ctx) +{ + const char *path; + + path = kcontext_script(ctx); + if (!path) { + fprintf(stderr, ERRMSG "missing path argument to file search.\n"); + return -1; + } + + return files(path, NULL); +} + int infix_ifaces(kcontext_t *ctx) { (void)ctx; @@ -87,91 +292,173 @@ int infix_ifaces(kcontext_t *ctx) return 0; } -int infix_ds_from_str(const char *text, sr_datastore_t *ds) +static const char *infix_ds(const char *text, const char *type, struct infix_ds **ds) { - size_t len = strlen(text); + size_t i, len = strlen(text); - if (!strncmp("startup-config", text, len)) - *ds = SR_DS_STARTUP; - else if (!strncmp("running-config", text, len)) - *ds = SR_DS_RUNNING; - else if (!strncmp("candidate-config", text, len)) - *ds = SR_DS_CANDIDATE; - else if (!strncmp("operational-config", text, len)) - *ds = SR_DS_OPERATIONAL; - else if (!strncmp("factory-config", text, len)) - *ds = SR_DS_FACTORY_DEFAULT; - else - return -1; + for (i = 0; i < NELEMS(infix_config); i++) { + if (!strncmp(infix_config[i].name, text, len)) { + *ds = &infix_config[i]; + return infix_config[i].name; + } + } - return 0; + if (strchr(text, '/') && !strstr(text, "://")) { + fprintf(stderr, ERRMSG "invalid %s argument: %s\n", type, text); + return NULL; + } + + return text; } int infix_copy(kcontext_t *ctx) { + struct infix_ds *srcds = NULL, *dstds = NULL; kpargv_t *pargv = kcontext_pargv(ctx); - sr_datastore_t srcds, dstds; - kparg_t *srcarg, *dstarg; sr_session_ctx_t *sess; + const char *src, *dst; sr_conn_ctx_t *conn; - int err; + int rc = 0; - srcarg = kpargv_find(pargv, "src"); - dstarg = kpargv_find(pargv, "dst"); - if (!srcarg || !dstarg) + src = kparg_value(kpargv_find(pargv, "src")); + dst = kparg_value(kpargv_find(pargv, "dst")); + if (!src || !dst) goto err; - if (infix_ds_from_str(kparg_value(srcarg), &srcds)) { - fprintf(stderr, ERRMSG "\"%s\" is not the name of any known datastore\n", kparg_value(srcarg)); + src = infix_ds(src, "source", &srcds); + if (!src) goto err; - } - if (infix_ds_from_str(kparg_value(dstarg), &dstds)) { - fprintf(stderr, ERRMSG "\"%s\" is not the name of any known datastore\n", kparg_value(dstarg)); + dst = infix_ds(dst, "destination", &dstds); + if (!dst) + goto err; + + if (!strcmp(src, dst)) { + fprintf(stderr, ERRMSG "source and destination are the same, aborting."); goto err; } - switch (srcds) { - case SR_DS_STARTUP: - case SR_DS_RUNNING: - case SR_DS_FACTORY_DEFAULT: - break; - default: - fprintf(stderr, - ERRMSG "\"%s\" is not a valid source datastore\n", kparg_value(srcarg)); - goto err; + /* 1. Regular ds copy */ + if (srcds && dstds) { + /* Ensure the dst ds is writable */ + if (!dstds->rw) { + invalid: + fprintf(stderr, ERRMSG "\"%s\" is not a valid file or datastore\n", dst); + goto err; + } + + if (sr_connect(SR_CONN_DEFAULT, &conn)) { + fprintf(stderr, ERRMSG "connection to datastore failed\n"); + goto err; + } + if (sr_session_start(conn, dstds->datastore, &sess)) { + fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst); + } else { + rc = sr_copy_config(sess, NULL, srcds->datastore, 0); + if (rc) + emsg(sess, ERRMSG "unable to copy configuration (%d)\n", rc); + } + rc = sr_disconnect(conn); + } else { + char temp_file[20] = "/tmp/copy.XXXXXX"; + const char *tmpfn = NULL; + const char *fn = NULL; + char adjust[256]; + + if (srcds) { + /* 2. Export from a datastore somewhere else */ + if (strstr(dst, "://")) { + snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name); + tmpfn = adjust; + fn = tmpfn; + } else { + fn = cfg_adjust(dst, adjust, sizeof(adjust)); + if (!fn) { + fprintf(stderr, "2.3\n"); + goto invalid; + } + + if (!access(fn, F_OK) && !yorn("Overwrite existing file %s", fn)) { + fprintf(stderr, "OK, aborting.\n"); + return 0; + } + } + + if (srcds->path) + fn = srcds->path; /* user directly => correct name */ + else + rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn); + + if (rc) + fprintf(stderr, ERRMSG "failed exporting %s\n", src); + else if (tmpfn) { + rc = systemf("curl -T %s %s", fn, dst); + if (rc) + fprintf(stderr, ERRMSG "failed uploading %s to %s", src, dst); + } + } else if (dstds) { + if (!dstds->sysrepocfg) + goto invalid; + if (!dstds->rw) { + fprintf(stderr, ERRMSG "not possible to write to %s", dst); + goto err; + } + + /* 3. Import from somewhere to a datastore */ + if (strstr(src, "://")) { + tmpfn = mktemp(temp_file); + fn = tmpfn; + } else { + fn = cfg_adjust(src, adjust, sizeof(adjust)); + if (!fn) + goto invalid; + } + + if (tmpfn) + rc = systemf("curl -o %s %s", fn, src); + if (rc) { + fprintf(stderr, ERRMSG "failed downloading %s", src); + } else { + rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn); + if (rc) + fprintf(stderr, ERRMSG "failed loading %s from %s", dst, src); + } + } else { + if (strstr(src, "://") && strstr(dst, "://")) { + fprintf(stderr, ERRMSG "copy from remote to remote is not supported.\n"); + goto err; + } + + if (strstr(src, "://")) { + fn = cfg_adjust(dst, adjust, sizeof(adjust)); + if (!fn) + goto invalid; + + if (!access(fn, F_OK)) { + if (!yorn("Overwrite existing file %s", fn)) { + fprintf(stderr, "OK, aborting.\n"); + return 0; + } + } + + rc = systemf("curl -o %s %s", fn, src); + } else if (strstr(dst, "://")) { + fn = cfg_adjust(src, adjust, sizeof(adjust)); + if (!fn) + goto invalid; + + if (access(fn, F_OK)) + fprintf(stderr, ERRMSG "no such file %s, aborting.", fn); + else + rc = systemf("curl -T %s %s", fn, dst); + } + } + + if (tmpfn) + rc = remove(tmpfn); } - switch (dstds) { - case SR_DS_STARTUP: - case SR_DS_RUNNING: - break; - default: - fprintf(stderr, ERRMSG "\"%s\" is not a valid destination datastore\n", kparg_value(dstarg)); - goto err; - } - - if (sr_connect(SR_CONN_DEFAULT, &conn)) { - fprintf(stderr, ERRMSG "connection to datastore failed\n"); - goto err; - } - if (sr_session_start(conn, dstds, &sess)) { - fprintf(stderr, ERRMSG "unable to open transaction to %s\n", kparg_value(dstarg)); - goto err_disconnect; - } - - err = sr_copy_config(sess, NULL, srcds, 0); - if (err) { - emsg(sess, ERRMSG "unable to copy configuration (%d)\n", err); - goto err_disconnect; - } - - sr_disconnect(conn); - return 0; - -err_disconnect: - sr_disconnect(conn); err: - return -1; + return rc; } int infix_commit(kcontext_t *ctx) @@ -290,6 +577,9 @@ int kplugin_infix_init(kcontext_t *ctx) kplugin_add_syms(plugin, ksym_new("copy", infix_copy)); kplugin_add_syms(plugin, ksym_new("commit", infix_commit)); + kplugin_add_syms(plugin, ksym_new("datastore", infix_datastore)); + kplugin_add_syms(plugin, ksym_new("dir", infix_dir)); + kplugin_add_syms(plugin, ksym_new("erase", infix_erase)); kplugin_add_syms(plugin, ksym_new("files", infix_files)); kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces)); kplugin_add_syms(plugin, ksym_new("rpc", infix_rpc)); diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index 16bac67a..5c2f02ae 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -21,19 +21,23 @@ Oneliners = n + + + cfg + + + + - factory-config - startup-config - running-config + src - startup-config - running-config + dst @@ -95,6 +99,13 @@ + + + /cfg + + + + @@ -154,7 +165,16 @@ - + + + + + + + + + +