mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 13:03:02 +02:00
admin@infix-00-00-00:~$ less /cfg/startup-config.cfg cat: can't open '/cfg/startup-config.cfg': Permission denied /cfg/startup-config.cfg: Permission denied admin@infix-00-00-00:~$ ls -l /cfg/ total 16 -rw------- 1 root wheel 13655 Jun 14 12:59 startup-config.cfg Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1065 lines
23 KiB
C
1065 lines
23 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 <sys/wait.h>
|
|
|
|
#include <sysrepo.h>
|
|
#include <sysrepo/netconf_acm.h>
|
|
#include <sysrepo/values.h>
|
|
|
|
#include "util.h"
|
|
|
|
#define err(rc, fmt, args...) { fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno)); exit(rc); }
|
|
#define errx(rc, fmt, args...) { fprintf(stderr, ERRMSG fmt "\n", ##args); exit(rc); }
|
|
#define warnx(fmt, args...) fprintf(stderr, ERRMSG fmt "\n", ##args)
|
|
#define warn(fmt, args...) fprintf(stderr, ERRMSG fmt ":%s\n", ##args, strerror(errno))
|
|
#define dbg(fmt, args...) if (debug) fprintf(stderr, DBGMSG fmt "\n", ##args)
|
|
|
|
struct infix_ds {
|
|
char *name; /* startup-config, etc. */
|
|
int datastore; /* sr_datastore_t and -1 */
|
|
bool rw; /* read-write:1 or not:0 */
|
|
char *path; /* local path or NULL */
|
|
};
|
|
|
|
const struct infix_ds infix_config[] = {
|
|
{ "startup-config", SR_DS_STARTUP, true, "/cfg/startup-config.cfg" },
|
|
{ "running-config", SR_DS_RUNNING, true, NULL },
|
|
/* { "candidate-config", SR_DS_CANDIDATE, true, NULL }, */
|
|
{ "operational-state", SR_DS_OPERATIONAL, false, NULL },
|
|
{ "factory-config", SR_DS_FACTORY_DEFAULT, false, NULL }
|
|
};
|
|
|
|
static const char *prognm;
|
|
static const char *remote_user;
|
|
static char *xpath = "/*";
|
|
static int debug;
|
|
static int force;
|
|
static int timeout;
|
|
static int dry_run;
|
|
static int sanitize;
|
|
|
|
/*
|
|
* Current system user, same as sysrepo user. We use getuid() here
|
|
* because `copy` is SUID root to work around sysrepo issues with a
|
|
* /dev/shm that's moounted 01777.
|
|
*/
|
|
static const char *getuser(void)
|
|
{
|
|
const struct passwd *pw;
|
|
uid_t uid = getuid();
|
|
|
|
pw = getpwuid(uid);
|
|
if (!pw)
|
|
err(1, "failed querying user info for uid %d", uid);
|
|
|
|
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) {
|
|
warn("failed in_group()");
|
|
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) {
|
|
const struct group *gr = getgrgid(gid);
|
|
|
|
warn("failed setting group owner %s (%d) on %s",
|
|
gr ? gr->gr_name : "<unknown>", gid, fn);
|
|
}
|
|
|
|
/* Make sure the group we just set can actually read/write the file.
|
|
* umask alone can't: the datastore export goes through a 0600 mkstemp
|
|
* temp and cp(1) propagates that mode to the destination.
|
|
*/
|
|
if (chmod(fn, 0660) && errno != EPERM)
|
|
warn("failed setting mode 0660 on %s", fn);
|
|
}
|
|
|
|
static const char *infix_ds(const char *text, const 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;
|
|
}
|
|
}
|
|
|
|
*ds = NULL;
|
|
return text;
|
|
}
|
|
|
|
static bool is_uri(const char *str)
|
|
{
|
|
return strstr(str, "://") != NULL;
|
|
}
|
|
|
|
static bool is_ssh_uri(const char *uri)
|
|
{
|
|
return !strncmp(uri, "scp://", 6) || !strncmp(uri, "sftp://", 7);
|
|
}
|
|
|
|
/*
|
|
* Parse scp:// or sftp:// URI into an scp(1)-compatible remote string
|
|
* of the form [user@]host:path, and optionally extract the port.
|
|
*
|
|
* URI format: scheme://[user@]host[:port]/path
|
|
*
|
|
* Returns a heap-allocated string the caller must free, and sets *portp
|
|
* to a heap-allocated port string (or NULL) the caller must also free.
|
|
* Returns NULL on parse failure.
|
|
*/
|
|
static char *ssh_uri_to_remote(const char *uri, char **portp)
|
|
{
|
|
const char *p, *slash, *path;
|
|
char auth[256], *auth_at, *auth_colon, *host_start;
|
|
char *user, *remote = NULL, *port = NULL;
|
|
size_t auth_len;
|
|
|
|
*portp = NULL;
|
|
|
|
if (!strncmp(uri, "scp://", 6))
|
|
p = uri + 6;
|
|
else if (!strncmp(uri, "sftp://", 7))
|
|
p = uri + 7;
|
|
else
|
|
return NULL;
|
|
|
|
slash = strchr(p, '/');
|
|
if (!slash)
|
|
slash = p + strlen(p);
|
|
|
|
auth_len = (size_t)(slash - p);
|
|
if (auth_len >= sizeof(auth))
|
|
return NULL;
|
|
memcpy(auth, p, auth_len);
|
|
auth[auth_len] = '\0';
|
|
|
|
auth_at = strchr(auth, '@');
|
|
if (auth_at) {
|
|
*auth_at = '\0';
|
|
user = auth;
|
|
host_start = auth_at + 1;
|
|
} else {
|
|
user = (char *)remote_user; /* may be NULL */
|
|
host_start = auth;
|
|
}
|
|
|
|
auth_colon = strchr(host_start, ':');
|
|
if (auth_colon) {
|
|
*auth_colon = '\0';
|
|
port = strdup(auth_colon + 1);
|
|
if (!port)
|
|
return NULL;
|
|
}
|
|
|
|
path = *slash ? slash : "/";
|
|
|
|
if (user)
|
|
(void)asprintf(&remote, "%s@%s:%s", user, host_start, path);
|
|
else
|
|
(void)asprintf(&remote, "%s:%s", host_start, path);
|
|
|
|
*portp = port;
|
|
return remote;
|
|
}
|
|
|
|
static bool is_stdout(const char *path)
|
|
{
|
|
if (!path)
|
|
return 1;
|
|
|
|
return !strcmp(path, "-") ||
|
|
!strcmp(path, "/dev/stdout") ||
|
|
!strcmp(path, "/dev/fd/1");
|
|
}
|
|
|
|
static char *mktmp(void)
|
|
{
|
|
mode_t oldmask;
|
|
char *path;
|
|
int fd;
|
|
|
|
path = strdup("/tmp/copy-XXXXXX");
|
|
if (!path)
|
|
goto err;
|
|
|
|
oldmask = umask(0077);
|
|
fd = mkstemp(path);
|
|
umask(oldmask);
|
|
|
|
if (fd < 0)
|
|
goto err;
|
|
|
|
if (chown(path, getuid(), -1))
|
|
dbg("Failed to chown %s: %s", path, strerror(errno));
|
|
|
|
close(fd);
|
|
return path;
|
|
err:
|
|
free(path);
|
|
return NULL;
|
|
}
|
|
|
|
static void rmtmp(const char *path)
|
|
{
|
|
if (remove(path)) {
|
|
if (errno == ENOENT)
|
|
return;
|
|
|
|
warn("failed removing temporary file %s", path);
|
|
}
|
|
}
|
|
|
|
static void sysrepo_print_error(sr_session_ctx_t *sess)
|
|
{
|
|
const sr_error_info_t *erri = NULL;
|
|
const char *msg;
|
|
int err;
|
|
|
|
if (!sess)
|
|
return;
|
|
|
|
err = sr_session_get_error(sess, &erri);
|
|
if (err || !erri || !erri->err_count)
|
|
return;
|
|
|
|
msg = erri->err->message;
|
|
if (!msg)
|
|
msg = sr_strerror(erri->err->err_code);
|
|
warnx("%s (%d)", msg, erri->err->err_code);
|
|
}
|
|
|
|
/* Connect to sysrepo and create NACM-aware session on running datastore */
|
|
static int sysrepo_init(sr_conn_ctx_t **conn, sr_session_ctx_t **sess,
|
|
sr_subscription_ctx_t **sub)
|
|
{
|
|
const char *user = getuser();
|
|
int err;
|
|
|
|
err = sr_connect(SR_CONN_DEFAULT, conn);
|
|
if (err != SR_ERR_OK) {
|
|
warnx("failed connecting to sysrepo: %s", sr_strerror(err));
|
|
return err;
|
|
}
|
|
|
|
/* Always open running, because sr_nacm_init() does not work
|
|
* against the factory DS.
|
|
*/
|
|
err = sr_session_start(*conn, SR_DS_RUNNING, sess);
|
|
if (err != SR_ERR_OK) {
|
|
warnx("failed starting session: %s", sr_strerror(err));
|
|
goto fail;
|
|
}
|
|
|
|
err = sr_nacm_init(*sess, 0, sub);
|
|
if (err != SR_ERR_OK) {
|
|
warnx("NACM init failed: %s", sr_strerror(err));
|
|
goto fail;
|
|
}
|
|
|
|
dbg("Setting NACM user %s for session", user);
|
|
err = sr_nacm_set_user(*sess, user);
|
|
if (err != SR_ERR_OK) {
|
|
warnx("NACM setup failed for user %s: %s", user, sr_strerror(err));
|
|
goto fail;
|
|
}
|
|
|
|
return SR_ERR_OK;
|
|
fail:
|
|
sysrepo_print_error(*sess);
|
|
sr_session_stop(*sess);
|
|
sr_disconnect(*conn);
|
|
|
|
return err;
|
|
}
|
|
|
|
static sr_session_ctx_t *sysrepo_session(const struct infix_ds *ds)
|
|
{
|
|
static sr_subscription_ctx_t *sub = NULL;
|
|
static sr_session_ctx_t *sess;
|
|
sr_conn_ctx_t *conn = NULL;
|
|
int err;
|
|
|
|
if (!ds) {
|
|
if (!sess)
|
|
return NULL;
|
|
|
|
conn = sr_session_get_connection(sess);
|
|
sr_session_stop(sess);
|
|
sr_disconnect(conn);
|
|
sess = NULL;
|
|
sub = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
if (!sess) {
|
|
err = sysrepo_init(&conn, &sess, &sub);
|
|
if (err != SR_ERR_OK) {
|
|
warnx("Failed to initialize session for %s", ds->name);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
err = sr_session_switch_ds(sess, ds->datastore);
|
|
if (err) {
|
|
sysrepo_print_error(sess);
|
|
warnx("%s activation failed", ds->name);
|
|
return NULL;
|
|
}
|
|
|
|
return sess;
|
|
}
|
|
|
|
static int sysrepo_export(const struct infix_ds *ds, const char *path)
|
|
{
|
|
sr_session_ctx_t *sess;
|
|
sr_data_t *data = NULL;
|
|
int err;
|
|
|
|
sess = sysrepo_session(ds);
|
|
if (!sess)
|
|
return 1;
|
|
|
|
err = sr_get_data(sess, xpath, 0, timeout * 1000, SR_OPER_DEFAULT, &data);
|
|
if (err) {
|
|
sysrepo_print_error(sess);
|
|
warnx("failed retrieving %s data", ds->name);
|
|
return err;
|
|
}
|
|
|
|
if (!data)
|
|
return 0;
|
|
|
|
err = lyd_print_path(path, data->tree, LYD_JSON, LYD_PRINT_SIBLINGS);
|
|
sr_release_data(data);
|
|
|
|
if (err) {
|
|
sysrepo_print_error(sess);
|
|
warnx("failed storing %s data", ds->name);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int sysrepo_import(const struct infix_ds *ds, const char *path)
|
|
{
|
|
const struct ly_ctx *ly;
|
|
sr_session_ctx_t *sess;
|
|
struct lyd_node *data;
|
|
int err;
|
|
|
|
sess = sysrepo_session(ds);
|
|
if (!sess)
|
|
return 1;
|
|
|
|
ly = sr_acquire_context(sr_session_get_connection(sess));
|
|
|
|
err = lyd_parse_data_path(ly, path, LYD_JSON,
|
|
LYD_PARSE_NO_STATE | LYD_PARSE_ONLY |
|
|
LYD_PARSE_STORE_ONLY | LYD_PARSE_STRICT, 0, &data);
|
|
if (err) {
|
|
warnx("failed parsing %s data", ds->name);
|
|
goto out;
|
|
}
|
|
|
|
err = dry_run ? 0 : sr_replace_config(sess, NULL, data, timeout * 1000);
|
|
if (err) {
|
|
sysrepo_print_error(sess);
|
|
warnx("failed importing %s data, error %d", ds->name, err);
|
|
}
|
|
|
|
out:
|
|
sr_release_context(sr_session_get_connection(sess));
|
|
return err ? 1 : 0;
|
|
/* return sysrepo_do(sysrepo_import_op, ds, path) ? 1 : 0; */
|
|
}
|
|
|
|
static int subprocess(char * const *argv)
|
|
{
|
|
int pid, status;
|
|
|
|
pid = fork();
|
|
if (!pid) {
|
|
execvp(argv[0], argv);
|
|
exit(1);
|
|
}
|
|
|
|
if (pid < 0)
|
|
return 1;
|
|
|
|
if (waitpid(pid, &status, 0) < 0)
|
|
return 1;
|
|
|
|
if (!WIFEXITED(status))
|
|
return 1;
|
|
|
|
return WEXITSTATUS(status);
|
|
}
|
|
|
|
static int curl(char *op, const char *path, const char *uri)
|
|
{
|
|
char *argv[10] = { "curl", "-L", NULL };
|
|
int err = 1, i = 2;
|
|
int path_i, uri_i = 0, user_i = 0;
|
|
|
|
argv[i++] = op;
|
|
|
|
path_i = i;
|
|
argv[i] = strdup(path);
|
|
if (!argv[i++])
|
|
goto out;
|
|
|
|
uri_i = i;
|
|
argv[i] = strdup(uri);
|
|
if (!argv[i++])
|
|
goto out;
|
|
|
|
if (remote_user) {
|
|
argv[i++] = "-u";
|
|
user_i = i;
|
|
argv[i] = strdup(remote_user);
|
|
if (!argv[i++])
|
|
goto out;
|
|
}
|
|
|
|
err = subprocess(argv);
|
|
|
|
out:
|
|
free(argv[path_i]);
|
|
if (uri_i)
|
|
free(argv[uri_i]);
|
|
if (user_i)
|
|
free(argv[user_i]);
|
|
return err;
|
|
}
|
|
|
|
static int curl_upload(const char *srcpath, const char *uri)
|
|
{
|
|
char upload[] = "-T";
|
|
|
|
if (curl(upload, srcpath, uri)) {
|
|
warnx("upload to %s failed", uri);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int curl_download(const char *uri, const char *dstpath)
|
|
{
|
|
char download[] = "-o";
|
|
int err;
|
|
|
|
if ((err = curl(download, dstpath, uri))) {
|
|
warnx("download of %s failed, exit code %d", uri, err);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int scp_upload(const char *srcpath, const char *uri)
|
|
{
|
|
char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL };
|
|
int i = 3, err = 1;
|
|
char *remote = NULL, *port = NULL, *src_dup = NULL;
|
|
|
|
remote = ssh_uri_to_remote(uri, &port);
|
|
if (!remote) {
|
|
warnx("failed to parse URI: %s", uri);
|
|
goto out;
|
|
}
|
|
|
|
if (port) {
|
|
argv[i++] = "-P";
|
|
argv[i++] = port;
|
|
}
|
|
|
|
src_dup = strdup(srcpath);
|
|
if (!src_dup)
|
|
goto out;
|
|
argv[i++] = src_dup;
|
|
argv[i++] = remote;
|
|
|
|
err = subprocess(argv);
|
|
if (err)
|
|
warnx("upload to %s failed", uri);
|
|
out:
|
|
free(src_dup);
|
|
free(port);
|
|
free(remote);
|
|
return err;
|
|
}
|
|
|
|
static int scp_download(const char *uri, const char *dstpath)
|
|
{
|
|
char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL };
|
|
int i = 3, err = 1;
|
|
char *remote = NULL, *port = NULL, *dst_dup = NULL;
|
|
|
|
remote = ssh_uri_to_remote(uri, &port);
|
|
if (!remote) {
|
|
warnx("failed to parse URI: %s", uri);
|
|
goto out;
|
|
}
|
|
|
|
if (port) {
|
|
argv[i++] = "-P";
|
|
argv[i++] = port;
|
|
}
|
|
|
|
argv[i++] = remote;
|
|
dst_dup = strdup(dstpath);
|
|
if (!dst_dup)
|
|
goto out;
|
|
argv[i++] = dst_dup;
|
|
|
|
err = subprocess(argv);
|
|
if (err)
|
|
warnx("download of %s failed", uri);
|
|
out:
|
|
free(dst_dup);
|
|
free(port);
|
|
free(remote);
|
|
return err;
|
|
}
|
|
|
|
static int cat(const char *srcpath)
|
|
{
|
|
char *argv[] = { "cat", NULL, NULL };
|
|
int err;
|
|
|
|
argv[1] = strdup(srcpath);
|
|
if (!argv[1])
|
|
return 1;
|
|
|
|
err = subprocess(argv);
|
|
if (err)
|
|
warnx("failed writing to stdout, exit code %d", err);
|
|
|
|
free(argv[1]);
|
|
return err;
|
|
}
|
|
|
|
static int cp(const char *srcpath, const char *dstpath)
|
|
{
|
|
char *argv[] = {
|
|
"cp", NULL, NULL, NULL,
|
|
};
|
|
int err = 1;
|
|
|
|
argv[1] = strdup(srcpath);
|
|
argv[2] = strdup(dstpath);
|
|
if (!(argv[1] && argv[2]))
|
|
goto out;
|
|
|
|
err = subprocess(argv);
|
|
if (err)
|
|
warnx("failed to save %s, exit code %d", dstpath, err);
|
|
out:
|
|
free(argv[2]);
|
|
free(argv[1]);
|
|
return err;
|
|
}
|
|
|
|
static int put(const char *srcpath, const char *dst,
|
|
const struct infix_ds *ds, const char *path)
|
|
{
|
|
int err = 0;
|
|
|
|
if (ds)
|
|
err = sysrepo_import(ds, srcpath);
|
|
else if (is_stdout(dst))
|
|
err = cat(srcpath);
|
|
else if (is_ssh_uri(dst))
|
|
err = scp_upload(srcpath, dst);
|
|
else if (is_uri(dst))
|
|
err = curl_upload(srcpath, dst);
|
|
|
|
if (err)
|
|
return err;
|
|
|
|
if (path) {
|
|
err = cp(srcpath, path);
|
|
if (!err)
|
|
set_owner(path, getuser());
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int get(const char *src, const struct infix_ds *ds, const char *path)
|
|
{
|
|
int err = 0;
|
|
|
|
if (ds)
|
|
err = sysrepo_export(ds, path);
|
|
else if (is_ssh_uri(src))
|
|
err = scp_download(src, path);
|
|
else if (is_uri(src))
|
|
err = curl_download(src, path);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int resolve_src(const char **src, const struct infix_ds **ds, char **path, bool *rm)
|
|
{
|
|
*src = infix_ds(*src, ds);
|
|
|
|
if (*ds || is_uri(*src)) {
|
|
*path = mktmp();
|
|
if (!*path)
|
|
return 1;
|
|
|
|
*rm = true;
|
|
return 0;
|
|
} else {
|
|
*path = cfg_adjust(*src, NULL, sanitize);
|
|
}
|
|
|
|
if (!*path) {
|
|
warn("no such file %s", *src);
|
|
return 1;
|
|
}
|
|
|
|
*rm = false;
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path)
|
|
{
|
|
if (is_stdout(*dst) || is_uri(*dst))
|
|
return 0;
|
|
|
|
*dst = infix_ds(*dst, ds);
|
|
|
|
if (*ds) {
|
|
if (!(*ds)->rw) {
|
|
warn("%s is not writable", (*ds)->name);
|
|
return 1;
|
|
}
|
|
|
|
if (!(*ds)->path)
|
|
return 0;
|
|
|
|
*path = strdup((*ds)->path);
|
|
} else {
|
|
*path = cfg_adjust(*dst, NULL, sanitize);
|
|
}
|
|
|
|
if (!*path) {
|
|
warn("no such file: %s", *dst);
|
|
return 1;
|
|
}
|
|
|
|
if (!force && !*ds && !access(*path, F_OK) && !yorn("Overwrite existing file %s", *path)) {
|
|
warnx("OK, aborting.");
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int copy(const char *src, const char *dst)
|
|
{
|
|
const struct infix_ds *srcds = NULL, *dstds = NULL;
|
|
char *srcpath = NULL, *dstpath = NULL;
|
|
char *dst_uri = NULL;
|
|
bool rmsrc = false;
|
|
mode_t oldmask;
|
|
int err = 1;
|
|
|
|
/* rw for user and group only */
|
|
oldmask = umask(0006);
|
|
|
|
if (dst && !strcmp(src, dst)) {
|
|
warn("source and destination are the same, aborting.");
|
|
goto err;
|
|
}
|
|
|
|
err = resolve_src(&src, &srcds, &srcpath, &rmsrc);
|
|
if (err)
|
|
goto err;
|
|
|
|
/*
|
|
* When uploading to an SSH URI ending in '/', scp(1) would use the
|
|
* basename of the local (temp) file as the remote name. Append a
|
|
* meaningful name instead: the datastore's on-disk filename, or the
|
|
* source file's own basename.
|
|
*/
|
|
if (dst && is_ssh_uri(dst) && dst[strlen(dst) - 1] == '/') {
|
|
const char *bn, *slash;
|
|
|
|
if (srcds && srcds->path) {
|
|
slash = strrchr(srcds->path, '/');
|
|
bn = slash ? slash + 1 : srcds->path;
|
|
} else if (srcds) {
|
|
bn = srcds->name;
|
|
} else {
|
|
slash = strrchr(srcpath, '/');
|
|
bn = slash ? slash + 1 : srcpath;
|
|
}
|
|
|
|
if (asprintf(&dst_uri, "%s%s", dst, bn) < 0) {
|
|
err = 1;
|
|
goto err;
|
|
}
|
|
dst = dst_uri;
|
|
}
|
|
|
|
err = resolve_dst(&dst, &dstds, &dstpath);
|
|
if (err)
|
|
goto err;
|
|
|
|
err = get(src, srcds, srcpath);
|
|
if (err)
|
|
goto err;
|
|
|
|
err = put(srcpath, dst, dstds, dstpath);
|
|
|
|
err:
|
|
/* If either src or dst came from sysrepo, close the session */
|
|
sysrepo_session(NULL);
|
|
|
|
if (rmsrc)
|
|
rmtmp(srcpath);
|
|
|
|
free(dst_uri);
|
|
if (dstpath)
|
|
free(dstpath);
|
|
free(srcpath);
|
|
|
|
sync();
|
|
umask(oldmask);
|
|
return err;
|
|
}
|
|
|
|
static int usage(int rc)
|
|
{
|
|
printf("Usage: %s [OPTIONS] SRC [DST]\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" -d Enable debug mode, verbose output on stderr\n"
|
|
" -f Force yes when copying to a file that exists already\n"
|
|
" -h This help text\n"
|
|
" -n Dry-run, validate configuration without applying\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"
|
|
" -x PATH XPath to copy, default: all\n"
|
|
"\n"
|
|
"Files:\n"
|
|
" SRC JSON configuration file, or a datastore\n"
|
|
" DST Optiional file or datastore, except factory-config,\n"
|
|
" when omitted output goes to stdout\n"
|
|
"\n"
|
|
"Datastores (short forms possible):\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"
|
|
" operational-state Operational status and state data"
|
|
"\n"
|
|
"Examples:\n"
|
|
" %s operational -x /system-state/software/boot-order\n"
|
|
"\n", prognm, timeout, prognm);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int usage_rpc(int rc)
|
|
{
|
|
printf("Usage: %s [OPTIONS] <rpc-xpath> [key value ...]\n"
|
|
"\n"
|
|
"Execute a YANG RPC/action with NACM enforcement.\n"
|
|
"\n"
|
|
"Options:\n"
|
|
" -d Enable debug mode, verbose output on stderr\n"
|
|
" -h This help text\n"
|
|
" -t SEC Timeout for the operation, or default %d sec\n"
|
|
" -v Show version\n"
|
|
"\n"
|
|
"Arguments:\n"
|
|
" rpc-xpath RPC XPath (e.g., /ietf-system:set-current-datetime)\n"
|
|
" key value Pairs of RPC argument names and values\n"
|
|
" Values can be comma-separated for lists/leaf-lists\n"
|
|
"\n"
|
|
"Examples:\n"
|
|
" %s /ietf-system:set-current-datetime current-datetime \"2025-01-01T00:00:00Z\"\n"
|
|
" %s /infix-system:set-boot-order boot-order primary boot-order secondary\n"
|
|
" %s /infix-system:set-boot-order boot-order primary,secondary,net\n"
|
|
"\n", prognm, timeout, prognm, prognm, prognm);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Execute RPC from CLI arguments: xpath and key-value pairs */
|
|
static int rpc_exec(const char *rpc_xpath, int argc, char *argv[])
|
|
{
|
|
sr_subscription_ctx_t *sub = NULL;
|
|
sr_conn_ctx_t *conn = NULL;
|
|
sr_session_ctx_t *sess = NULL;
|
|
sr_val_t *input = NULL;
|
|
sr_val_t *output = NULL;
|
|
size_t icnt = 0, ocnt = 0;
|
|
int rc = 1, err, i;
|
|
|
|
dbg("Executing RPC %s with %d arguments", rpc_xpath, argc / 2);
|
|
|
|
err = sysrepo_init(&conn, &sess, &sub);
|
|
if (err != SR_ERR_OK)
|
|
return 1;
|
|
|
|
for (i = 0; i < argc - 1; i += 2) {
|
|
const char *key = argv[i];
|
|
const char *val = argv[i + 1];
|
|
char *val_copy, *token, *saveptr;
|
|
|
|
/* Check if value contains commas - split into multiple values */
|
|
if (strchr(val, ',')) {
|
|
val_copy = strdup(val);
|
|
if (!val_copy) {
|
|
warnx("Memory allocation failed");
|
|
goto cleanup;
|
|
}
|
|
|
|
token = strtok_r(val_copy, ",", &saveptr);
|
|
while (token) {
|
|
sr_realloc_values(icnt, icnt + 1, &input);
|
|
sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
|
|
sr_val_set_str_data(&input[icnt], SR_STRING_T, token);
|
|
dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, token);
|
|
icnt++;
|
|
token = strtok_r(NULL, ",", &saveptr);
|
|
}
|
|
free(val_copy);
|
|
} else {
|
|
/* Single value */
|
|
sr_realloc_values(icnt, icnt + 1, &input);
|
|
sr_val_build_xpath(&input[icnt], "%s/%s", rpc_xpath, key);
|
|
sr_val_set_str_data(&input[icnt], SR_STRING_T, val);
|
|
dbg("Adding RPC argument %zu: %s = %s", icnt, input[icnt].xpath, val);
|
|
icnt++;
|
|
}
|
|
}
|
|
|
|
dbg("Sending RPC %s (timeout: %d ms)", rpc_xpath, timeout * 1000);
|
|
err = sr_rpc_send(sess, rpc_xpath, input, icnt, timeout * 1000, &output, &ocnt);
|
|
if (err != SR_ERR_OK) {
|
|
sysrepo_print_error(sess);
|
|
warnx("RPC execution failed: %s", sr_strerror(err));
|
|
goto cleanup;
|
|
}
|
|
|
|
/* Print output if any */
|
|
for (i = 0; i < (int)ocnt; i++) {
|
|
sr_print_val(&output[i]);
|
|
puts("");
|
|
}
|
|
|
|
rc = 0;
|
|
|
|
cleanup:
|
|
sr_free_values(input, icnt);
|
|
sr_free_values(output, ocnt);
|
|
if (sub)
|
|
sr_nacm_destroy();
|
|
if (sess)
|
|
sr_session_stop(sess);
|
|
if (conn)
|
|
sr_disconnect(conn);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int copy_main(int argc, char *argv[])
|
|
{
|
|
const char *dst = "/dev/stdout";
|
|
const char *src = NULL;
|
|
int c;
|
|
|
|
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
|
|
|
|
while ((c = getopt(argc, argv, "dfhnst:u:vx:")) != EOF) {
|
|
switch(c) {
|
|
case 'd':
|
|
debug = 1;
|
|
break;
|
|
case 'f':
|
|
force = 1;
|
|
break;
|
|
case 'h':
|
|
return usage(0);
|
|
case 'n':
|
|
dry_run = 1;
|
|
break;
|
|
case 's':
|
|
sanitize = 1;
|
|
break;
|
|
case 't':
|
|
timeout = atoi(optarg);
|
|
break;
|
|
case 'u':
|
|
remote_user = optarg;
|
|
break;
|
|
case 'v':
|
|
puts(PACKAGE_VERSION);
|
|
return 0;
|
|
case 'x':
|
|
xpath = optarg;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (timeout < 0)
|
|
timeout = 120;
|
|
|
|
switch (argc - optind) {
|
|
case 2:
|
|
src = argv[optind++];
|
|
dst = argv[optind++];
|
|
break;
|
|
case 1:
|
|
src = argv[optind++];
|
|
break;
|
|
default:
|
|
return usage(1);
|
|
}
|
|
|
|
return copy(src, dst);
|
|
}
|
|
|
|
static int rpc_main(int argc, char *argv[])
|
|
{
|
|
int c;
|
|
|
|
timeout = fgetint("/etc/default/confd", "=", "CONFD_TIMEOUT");
|
|
|
|
while ((c = getopt(argc, argv, "dht:v")) != EOF) {
|
|
switch(c) {
|
|
case 'd':
|
|
debug = 1;
|
|
break;
|
|
case 'h':
|
|
return usage_rpc(0);
|
|
case 't':
|
|
timeout = atoi(optarg);
|
|
break;
|
|
case 'v':
|
|
puts(PACKAGE_VERSION);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (timeout < 0)
|
|
timeout = 120;
|
|
|
|
/* Require at least RPC xpath */
|
|
if (optind >= argc) {
|
|
warnx("Missing RPC xpath");
|
|
return usage_rpc(1);
|
|
}
|
|
|
|
/* Validate RPC xpath starts with '/' */
|
|
if (argv[optind][0] != '/') {
|
|
warnx("RPC xpath must start with '/'");
|
|
return usage_rpc(1);
|
|
}
|
|
|
|
/* Validate argument count (must be key-value pairs) */
|
|
argc -= optind + 1;
|
|
if (argc % 2 != 0) {
|
|
warnx("Arguments must be key-value pairs after RPC xpath");
|
|
return usage_rpc(1);
|
|
}
|
|
|
|
return rpc_exec(argv[optind], argc, &argv[optind + 1]);
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
prognm = basename(argv[0]);
|
|
|
|
if (!strcmp(prognm, "rpc"))
|
|
return rpc_main(argc, argv);
|
|
|
|
return copy_main(argc, argv);
|
|
}
|