Files
infix/src/bin/copy.c
T
Joachim Wiberg 21256a8ac1 bin: add bash completion for copy command
Add bash completion for the common datastores, like we already do in the
CLI, and update the usage text accordingly.

Also, make sure to install to /usr/bin, not /bin since we've now merged
the hierarchies since a while back.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-23 15:23:59 +02:00

570 lines
14 KiB
C

/* SPDX-License-Identifier: ISC */
#include "config.h"
#include <getopt.h>
#include <pwd.h>
#include <grp.h>
#include <stdio.h>
#include <stdarg.h>
#include <libgen.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sysrepo.h>
#include <sysrepo/netconf_acm.h>
#include "util.h"
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", "factory-default", SR_DS_FACTORY_DEFAULT, 0, NULL }
};
static const char *prognm = "copy";
static int timeout;
static int dry_run;
static int quiet;
static int sanitize;
/*
* Print sysrepo session errors followed by an optional string.
*/
static void emsg(sr_session_ctx_t *sess, const char *fmt, ...)
{
const sr_error_info_t *err = NULL;
va_list ap;
size_t i;
int rc;
if (!sess)
goto end;
rc = sr_session_get_error(sess, &err);
if ((rc != SR_ERR_OK) || !err)
goto end;
// Show the first error only. Because probably next errors are
// originated from internal sysrepo code but is not from subscribers.
// for (i = 0; i < err->err_count; i++)
for (i = 0; i < (err->err_count < 1 ? err->err_count : 1); i++)
fprintf(stderr, ERRMSG "%s\n", err->err[i].message);
end:
if (fmt) {
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
}
/*
* Current system user, same as sysrepo user
*/
static char *getuser(void)
{
const struct passwd *pw;
uid_t uid;
uid = getuid();
pw = getpwuid(uid);
if (!pw) {
perror("getpwuid");
exit(1);
}
return pw->pw_name;
}
/*
* If UNIX user is in UNIX group of directory containing file,
* return 1, otherwise 0.
*
* E.g., writing to /cfg/foo, where /cfg is owned by root:wheel,
* should result in the file being owned by $LOGNAME:wheel with
* 0660 perms for other users in same group.
*/
static int in_group(const char *user, const char *fn, gid_t *gid)
{
char path[PATH_MAX];
const struct passwd *pw;
int i, num = 0, rc = 0;
struct stat st;
gid_t *groups;
char *dir;
pw = getpwnam(user);
if (!pw)
return 0;
strlcpy(path, fn, sizeof(path));
dir = dirname(path);
if (stat(dir, &st))
return 0;
num = NGROUPS_MAX;
groups = malloc(num * sizeof(gid_t));
if (!groups) {
perror("in_group() malloc");
return 0;
}
getgrouplist(user, pw->pw_gid, groups, &num);
for (i = 0; i < num; i++) {
if (groups[i] == st.st_gid) {
*gid = st.st_gid;
rc = 1;
break;
}
}
free(groups);
return rc;
}
/*
* Set group owner so other users with same directory permissions can
* read/write the file as well. E.g., an 'admin' level user in group
* 'wheel' writing a new file to `/cfg` should be possible to read and
* write to by other administrators.
*
* This function is called only when the file has been successfully
* copied or created in a file system directory. This is why we can
* safely ignore any EPERM errors to chown(), below, because if the file
* already existed, created by another user, we are not allowed to chgrp
* it. The sole purpose of this function is to allow other users in the
* same group to access the file in the future.
*/
static void set_owner(const char *fn, const char *user)
{
gid_t gid = 9999;
if (!fn)
return; /* not an error, e.g., running-config is not a file */
if (!in_group(user, fn, &gid))
return; /* user not in parent directory's group */
if (chown(fn, -1, gid) && errno != EPERM) {
int _errno = errno;
const struct group *gr = getgrgid(gid);
fprintf(stderr, ERRMSG "setting group owner %s (%d) on %s: %s\n",
gr ? gr->gr_name : "<unknown>", gid, fn, strerror(_errno));
}
}
static const char *infix_ds(const char *text, struct infix_ds **ds)
{
size_t i, len = strlen(text);
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 text;
}
/*
* Load configuration from a file and replace running datastore.
* This triggers all sysrepo change callbacks, unlike sr_copy_config().
* In dry-run mode, only validates the configuration without applying.
*/
static int replace_running(sr_conn_ctx_t *conn, sr_session_ctx_t *sess,
const char *file, int timeout)
{
uint32_t flags = LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT;
struct lyd_node *data = NULL;
const struct ly_ctx *ctx;
LY_ERR err;
int rc;
ctx = sr_acquire_context(conn);
err = lyd_parse_data_path(ctx, file, LYD_JSON, flags, 0, &data);
sr_release_context(conn);
if (err) {
fprintf(stderr, ERRMSG "unable to load %s, error %d: %s\n", file, err, ly_errmsg(ctx));
return 1;
}
if (dry_run) {
if (!quiet) {
printf("Configuration validated, %s OK\n", file);
fflush(stdout);
}
lyd_free_all(data);
rc = 0;
} else {
/* Replace running config, assumes ownership of 'data' */
rc = sr_replace_config(sess, NULL, data, timeout * 1000);
if (rc) {
emsg(sess, ERRMSG "unable to replace configuration, err %d: %s\n",
rc, sr_strerror(rc));
lyd_free_all(data);
}
}
return rc;
}
static int copy(const char *src, const char *dst, const char *remote_user)
{
struct infix_ds *srcds = NULL, *dstds = NULL;
const char *tmpfn = NULL;
sr_session_ctx_t *sess;
const char *fn = NULL;
sr_conn_ctx_t *conn;
const char *user;
char adjust[256];
mode_t oldmask;
int rc = 0;
/* rw for user and group only */
oldmask = umask(0006);
src = infix_ds(src, &srcds);
if (!src)
goto err;
dst = infix_ds(dst, &dstds);
if (!dst)
goto err;
if (!strcmp(src, dst)) {
fprintf(stderr, ERRMSG "source and destination are the same, aborting.\n");
goto err;
}
user = getuser();
/* 1. Regular ds to ds copy */
if (srcds && dstds) {
if (!dstds->rw) {
fprintf(stderr, ERRMSG "not possible to write to \"%s\", skipping.\n", dst);
rc = 1;
goto err;
}
if (sr_connect(SR_CONN_DEFAULT, &conn)) {
fprintf(stderr, ERRMSG "connection to datastore failed\n");
rc = 1;
goto err;
}
sr_log_syslog("klishd", SR_LL_WRN);
if (sr_session_start(conn, dstds->datastore, &sess)) {
fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst);
} else {
sr_nacm_set_user(sess, user);
/*
* When copying TO running-config, use sr_replace_config()
* to trigger change callbacks. Otherwise use sr_copy_config()
* for direct datastore copy without callbacks.
*/
if (dstds->datastore == SR_DS_RUNNING && srcds->path) {
rc = replace_running(conn, sess, srcds->path, timeout);
if (!rc)
set_owner(dstds->path, user);
} else {
/* Direct copy for other datastores (no callbacks needed) */
rc = sr_copy_config(sess, NULL, srcds->datastore, timeout * 1000);
if (rc) {
emsg(sess, ERRMSG "unable to copy configuration, err %d: %s\n", rc, sr_strerror(rc));
} else {
/* Export datastore to backing file if it has one */
if (dstds->path) {
rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path);
if (rc)
fprintf(stderr, ERRMSG "failed saving %s to %s\n", srcds->name, dstds->path);
else
set_owner(dstds->path, user);
}
}
}
}
rc = sr_disconnect(conn);
if (!srcds->path || !dstds->path)
goto err; /* done, not an error */
/* allow copy factory startup */
}
if (srcds) {
/* 2. Export from a datastore somewhere else */
if (strstr(dst, "://")) {
if (srcds->path)
fn = srcds->path;
else {
snprintf(adjust, sizeof(adjust), "/tmp/%s.cfg", srcds->name);
fn = tmpfn = adjust;
remove(tmpfn);
rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn);
}
if (rc)
fprintf(stderr, ERRMSG "failed exporting %s to %s\n", src, fn);
else {
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
if (rc)
fprintf(stderr, ERRMSG "failed uploading %s to %s\n", src, dst);
else
set_owner(dst, user);
}
goto err;
}
if (dstds && dstds->path)
fn = dstds->path;
else
fn = cfg_adjust(dst, src, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "file not found.\n");
rc = -1;
goto err;
}
if (!access(fn, F_OK) && !yorn("Overwrite existing file %s", fn)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
if (srcds->path)
rc = systemf("cp %s %s", srcds->path, fn);
else
rc = systemf("sysrepocfg -d %s -X%s -f json", srcds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed copy %s to %s\n", src, fn);
else
set_owner(fn, user);
} else if (dstds) {
/* 3. Import from somewhere to a datastore */
if (!dstds->sysrepocfg) {
fprintf(stderr, ERRMSG "not possible to import to this datastore.\n");
rc = 1;
goto err;
}
if (!dstds->rw) {
fprintf(stderr, ERRMSG "not possible to write to %s", dst);
goto err;
}
if (strstr(src, "://")) {
fn = basenm(src);
if (fn[0] == 0) {
fprintf(stderr, ERRMSG "missing filename in sorce URI.\n");
rc = 1;
goto err;
}
snprintf(adjust, sizeof(adjust), "/tmp/%s", fn);
fn = tmpfn = adjust;
remove(tmpfn);
} else {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
}
if (tmpfn)
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
if (rc) {
fprintf(stderr, ERRMSG "failed downloading %s", src);
} else {
if (dstds->datastore == SR_DS_RUNNING) {
if (sr_connect(SR_CONN_DEFAULT, &conn)) {
fprintf(stderr, ERRMSG "connection to datastore failed\n");
rc = 1;
} else {
sr_log_syslog("klishd", SR_LL_WRN);
if (sr_session_start(conn, SR_DS_RUNNING, &sess)) {
fprintf(stderr, ERRMSG "unable to open transaction to %s\n", dst);
rc = 1;
} else {
sr_nacm_set_user(sess, user);
rc = replace_running(conn, sess, fn, timeout);
if (!rc)
set_owner(dstds->path, user);
}
sr_disconnect(conn);
}
} else {
rc = systemf("sysrepocfg -d %s -I%s -f json", dstds->sysrepocfg, fn);
if (rc)
fprintf(stderr, ERRMSG "failed loading %s from %s\n", dst, src);
else if (dstds->path) {
rc = systemf("sysrepocfg -d %s -X%s -f json", dstds->sysrepocfg, dstds->path);
if (rc)
fprintf(stderr, ERRMSG "failed saving %s\n", dstds->path);
else
set_owner(dstds->path, user);
}
}
}
} else {
/* 4. regular copy file -> file, either may be remote */
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, src, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
if (!access(fn, F_OK)) {
if (!yorn("Overwrite existing file %s", fn)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
}
rc = systemf("curl %s -Lo %s %s", remote_user, fn, src);
} else if (strstr(dst, "://")) {
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust), sanitize);
if (!fn) {
fprintf(stderr, ERRMSG "file not found.\n");
rc = 1;
goto err;
}
if (access(fn, F_OK))
fprintf(stderr, ERRMSG "no such file %s.", fn);
else
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
} else {
char bufa[256], bufb[256];
const char *from, *to;
from = cfg_adjust(src, NULL, bufa, sizeof(bufa), sanitize);
if (!from) {
fprintf(stderr, ERRMSG "no such file %s.", src);
rc = 1;
goto err;
}
to = cfg_adjust(dst, src, bufb, sizeof(bufb), sanitize);
if (!to) {
fprintf(stderr, ERRMSG "no such file %s.", dst);
rc = 1;
goto err;
}
if (!access(to, F_OK)) {
if (!yorn("Overwrite existing file %s", to)) {
fprintf(stderr, "OK, aborting.\n");
return 0;
}
}
rc = systemf("cp %s %s", from, to);
}
}
err:
if (tmpfn)
rc = remove(tmpfn);
sync(); /* ensure command is flushed to disk */
umask(oldmask);
return rc;
}
static int usage(int rc)
{
printf("Usage: %s [OPTIONS] SRC DST\n"
"\n"
"Options:\n"
" -h This help text\n"
" -n Dry-run, validate configuration without applying\n"
" -q Quiet mode, suppress informational messages\n"
" -s Sanitize paths for CLI use (restrict path traversal)\n"
" -t SEC Timeout for the operation, or default %d sec\n"
" -u USER Username for remote commands, like scp\n"
" -v Show version\n"
"\n"
"Files:\n"
" SRC JSON configuration file, or a datastore\n"
" DST A file or datastore, except factory-config\n"
"\n"
"Datastores:\n"
" running-config The running datastore, current active config\n"
" startup-config The non-volatile config used at startup\n"
" factory-config The device's factory default configuration\n"
"\n", prognm, timeout);
return rc;
}
int main(int argc, char *argv[])
{
const char *user = NULL, *src = NULL, *dst = NULL;
int c;
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
while ((c = getopt(argc, argv, "hnqst:u:v")) != EOF) {
switch(c) {
case 'h':
return usage(0);
case 'n':
dry_run = 1;
break;
case 'q':
quiet = 1;
break;
case 's':
sanitize = 1;
break;
case 't':
timeout = atoi(optarg);
break;
case 'u':
user = optarg;
break;
case 'v':
puts(PACKAGE_VERSION);
return 0;
}
}
if (timeout < 0)
timeout = 120;
if (argc - optind != 2)
return usage(1);
src = argv[optind++];
dst = argv[optind++];
return copy(src, dst, user);
}