mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 13:23:01 +02:00
The code assumes both SRC and DST is passed, here we ensure this early. Prior to this patch: root@host:~$ copy foo Segmentation fault (core dumped) Signed-off-by: Richard Alpe <richard@bit42.se>
429 lines
9.9 KiB
C
429 lines
9.9 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;
|
|
|
|
|
|
/*
|
|
* 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 gid_t 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;
|
|
}
|
|
|
|
|
|
static int copy(const char *src, const char *dst, const char *remote_user)
|
|
{
|
|
struct infix_ds *srcds = NULL, *dstds = NULL;
|
|
char temp_file[20] = "/tmp/copy.XXXXXX";
|
|
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 copy */
|
|
if (srcds && dstds) {
|
|
/* Ensure the dst ds is writable */
|
|
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);
|
|
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
|
|
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;
|
|
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));
|
|
|
|
if (!fn) {
|
|
fprintf(stderr, ERRMSG "invalid destination path.\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) {
|
|
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;
|
|
}
|
|
|
|
/* 3. Import from somewhere to a datastore */
|
|
if (strstr(src, "://")) {
|
|
tmpfn = mktemp(temp_file);
|
|
fn = tmpfn;
|
|
} else {
|
|
fn = cfg_adjust(src, NULL, adjust, sizeof(adjust));
|
|
if (!fn) {
|
|
fprintf(stderr, ERRMSG "invalid source file location.\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 {
|
|
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, src, adjust, sizeof(adjust));
|
|
if (!fn) {
|
|
fprintf(stderr, ERRMSG "invalid destination file location.\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));
|
|
if (!fn) {
|
|
fprintf(stderr, ERRMSG "invalid source file location.\n");
|
|
rc = 1;
|
|
goto err;
|
|
}
|
|
|
|
if (access(fn, F_OK))
|
|
fprintf(stderr, ERRMSG "no such file %s, aborting.", fn);
|
|
else
|
|
rc = systemf("curl %s -LT %s %s", remote_user, fn, dst);
|
|
} else {
|
|
if (!access(dst, F_OK)) {
|
|
if (!yorn("Overwrite existing file %s", dst)) {
|
|
fprintf(stderr, "OK, aborting.\n");
|
|
return 0;
|
|
}
|
|
}
|
|
rc = systemf("cp %s %s", src, dst);
|
|
}
|
|
}
|
|
|
|
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"
|
|
" -u USER Username for remote commands, like scp\n"
|
|
" -t SEEC Timeout for the operation, or default %d sec\n"
|
|
" -v Show version\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, "ht:u:v")) != EOF) {
|
|
switch(c) {
|
|
case 'h':
|
|
return usage(0);
|
|
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);
|
|
}
|