X-Git-Url: https://code.citadel.org/?a=blobdiff_plain;f=citadel%2Fctdlmigrate.c;h=d18707a27319dece1fac47bf3314214783739100;hb=068d33e5d8569b2c4a2c8582178427892b0a8dee;hp=65f06c64409198b388946c1618bd11684f7f5494;hpb=4eb74b26380dfde31c86c685f0589e0c653aebf0;p=citadel.git diff --git a/citadel/ctdlmigrate.c b/citadel/ctdlmigrate.c index 65f06c644..d18707a27 100644 --- a/citadel/ctdlmigrate.c +++ b/citadel/ctdlmigrate.c @@ -1,31 +1,14 @@ -/* - * $Id$ - * - * Across-the-wire migration utility for Citadel - * - * Yes, we used goto, and gets(), and committed all sorts of other heinous sins here. - * The scope of this program isn't wide enough to make a difference. If you don't like - * it you can rewrite it. - * - * Copyright (c) 2009 citadel.org - * - * 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, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * (Note: a useful future enhancement might be to support "-h" on both sides) - * - */ +// Across-the-wire migration utility for Citadel +// +// Copyright (c) 2009-2021 citadel.org +// +// This program is open source software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3. +// +// 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. #include #include @@ -43,272 +26,425 @@ #include #include #include +#include +#include +#include #include #include "citadel.h" #include "axdefs.h" #include "sysdep.h" #include "config.h" #include "citadel_dirs.h" -#if HAVE_BACKTRACE -#include -#endif +// yeah, this wouldn't work in a multithreaded program. +static int gmaxlen = 0; +static char *gdeftext = NULL; -/* - * Replacement for gets() that doesn't throw a compiler warning. - * We're only using it for some simple prompts, so we don't need - * to worry about attackers exploiting it. - */ -void getz(char *buf) { - char *ptr; - ptr = fgets(buf, 32767, stdin); - if (!ptr) { - buf[0] = 0; - return; +// support function for getz() +static int limit_rl(FILE *dummy) { + if (rl_end > gmaxlen) { + return '\b'; } + return rl_getc(dummy); +} + - ptr = strchr(buf, '\n'); - if (ptr) *ptr = 0; +// support function for getz() +static int getz_deftext(void) { + if (gdeftext) { + rl_insert_text(gdeftext); + gdeftext = NULL; + rl_startup_hook = NULL; + } + return 0; } +// Replacement for gets() that uses libreadline. +void getz(char *buf, int maxlen, char *default_value, char *prompt) { + rl_startup_hook = getz_deftext; + rl_getc_function = limit_rl; + gmaxlen = maxlen; + gdeftext = default_value; + strcpy(buf, readline(prompt)); +} +// Exit from the program while displaying an error code +void ctdlmigrate_exit(int cmdexit) { + printf("\n\n\033[3%dmExit code %d\033[0m\n", (cmdexit ? 1 : 2), cmdexit); + exit(cmdexit); +} -int main(int argc, char *argv[]) -{ - int relh=0; - int home=0; - char relhome[PATH_MAX]=""; - char ctdldir[PATH_MAX]=CTDLDIR; - char yesno[5]; - char sendcommand[PATH_MAX]; - int cmdexit; - char cmd[PATH_MAX]; - char buf[PATH_MAX]; - char socket_path[PATH_MAX]; - char remote_user[256]; - char remote_host[256]; - char remote_sendcommand[PATH_MAX]; - FILE *sourcefp = NULL; - FILE *targetfp = NULL; - int linecount = 0; - char spinning[4] = "-\\|/" ; - int exitcode = 0; - - calc_dirs_n_files(relh, home, relhome, ctdldir, 0); - CtdlMakeTempFileName(socket_path, sizeof socket_path); - cmdexit = system("clear"); - printf( "-------------------------------------------\n" - "Over-the-wire migration utility for Citadel\n" - "-------------------------------------------\n" - "\n" - "This utility is designed to migrate your Citadel installation\n" - "to a new host system via a network connection, without disturbing\n" - "the source system. The target may be a different CPU architecture\n" - "and/or operating system. The source system should be running\n" - "Citadel %d.%02d or newer, and the target system should be running\n" - "either the same version or a newer version. You will also need\n" - "the 'rsync' utility, and OpenSSH v4 or newer.\n" - "\n" - "You must run this utility on the TARGET SYSTEM. Any existing data\n" - "on this system will be ERASED.\n" - "\n" - "Do you wish to continue? " - , - EXPORT_REV_MIN / 100, - EXPORT_REV_MIN % 100 - ); +// Connect to a Citadel on a remote host using a TCP/IP socket +static int tcp_connectsock(char *host, char *service) { + struct in6_addr serveraddr; + struct addrinfo hints; + struct addrinfo *res = NULL; + struct addrinfo *ai = NULL; + int rc = (-1); + int sock = (-1); - if ((fgets(yesno, sizeof yesno, stdin) == NULL) || (tolower(yesno[0]) != 'y')) { - exit(0); + if ((host == NULL) || IsEmptyStr(host)) { + return(-1); + } + if ((service == NULL) || IsEmptyStr(service)) { + return(-1); } - printf("\n\nGreat! First we will check some things out here on our target\n" - "system to make sure it is ready to receive data.\n\n"); - - printf("Locating 'sendcommand' and checking connectivity to Citadel...\n"); - snprintf(sendcommand, sizeof sendcommand, "%s/sendcommand", ctdl_utilbin_dir); - snprintf(cmd, sizeof cmd, "%s 'NOOP'", sendcommand); - cmdexit = system(cmd); - if (cmdexit != 0) { - printf("\nctdlmigrate was unable to attach to the Citadel server\n" - "here on the target system. Is Citadel running?\n\n"); - exit(1); + memset(&hints, 0x00, sizeof(hints)); + hints.ai_flags = AI_NUMERICSERV; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + // Handle numeric IPv4 and IPv6 addresses + rc = inet_pton(AF_INET, host, &serveraddr); + if (rc == 1) { // dotted quad + hints.ai_family = AF_INET; + hints.ai_flags |= AI_NUMERICHOST; + } else { + rc = inet_pton(AF_INET6, host, &serveraddr); + if (rc == 1) { // IPv6 address + hints.ai_family = AF_INET6; + hints.ai_flags |= AI_NUMERICHOST; + } } - printf("\nOK, this side is ready to go. Now we must connect to the source system.\n\n"); - - printf("Enter the host name or IP address of the source system\n" - "(example: ctdl.foo.org)\n" - "--> "); - getz(remote_host); - printf("\nEnter the name of a user on %s who has full access to Citadel files\n" - "(usually root)\n--> ", - remote_host); - getz(remote_user); - - printf("\nEstablishing an SSH connection to the source system...\n\n"); - unlink(socket_path); - snprintf(cmd, sizeof cmd, "ssh -MNf -S %s %s@%s", socket_path, remote_user, remote_host); - cmdexit = system(cmd); - printf("\n"); - if (cmdexit != 0) { - printf("This program was unable to establish an SSH session to the source system.\n\n"); - exitcode = cmdexit; - goto THEEND; + + // Begin the connection process + rc = getaddrinfo(host, service, &hints, &res); + if (rc != 0) { + fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno)); + return (-1); } - printf("\nTesting a command over the connection...\n\n"); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s 'echo Remote commands are executing successfully.'", - socket_path, remote_user, remote_host); - cmdexit = system(cmd); - printf("\n"); - if (cmdexit != 0) { - printf("Remote commands are not succeeding.\n\n"); - exitcode = cmdexit; - goto THEEND; + // Try all available addresses until we connect to one or until we run out. + for (ai = res; ai != NULL; ai = ai->ai_next) { + sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock < 0) { + fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno)); + return (-1); + } + + rc = connect(sock, ai->ai_addr, ai->ai_addrlen); + if (rc >= 0) { + return (sock); // Connected! + } + else { + fprintf(stderr, "ctdlmigrate: %s\n", strerror(errno)); + close(sock); // Failed. Close the socket to avoid fd leak! + } } + return (-1); +} + - printf("\nLocating the remote 'sendcommand' and Citadel installation...\n"); - snprintf(remote_sendcommand, sizeof remote_sendcommand, "/usr/local/citadel/sendcommand"); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", - socket_path, remote_user, remote_host, remote_sendcommand); - cmdexit = system(cmd); - if (cmdexit != 0) { - snprintf(remote_sendcommand, sizeof remote_sendcommand, "/usr/sbin/sendcommand"); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", - socket_path, remote_user, remote_host, remote_sendcommand); - cmdexit = system(cmd); +// Connect to a Citadel on a remote host using a unix domaion socket +int uds_connectsock(char *sockpath) { + int s; + struct sockaddr_un addr; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, sockpath); + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s < 0) { + fprintf(stderr, "ctdlmigrate: Can't create socket: %s\n", strerror(errno)); + return(-1); } - if (cmdexit != 0) { - printf("\nUnable to locate Citadel programs on the remote system. Please enter\n" - "the name of the directory on %s which contains the 'sendcommand' program.\n" - "(example: /opt/foo/citadel)\n" - "--> ", remote_host); - getz(buf); - snprintf(remote_sendcommand, sizeof remote_sendcommand, "%s/sendcommand", buf); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", - socket_path, remote_user, remote_host, remote_sendcommand); - cmdexit = system(cmd); + + if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + fprintf(stderr, "ctdlmigrate: can't connect: %s\n", strerror(errno)); + close(s); + return(-1); } - printf("\n"); - if (cmdexit != 0) { - printf("ctdlmigrate was unable to attach to the remote Citadel system.\n\n"); - exitcode = cmdexit; - goto THEEND; + + return s; +} + + +// input binary data from socket +void serv_read(int serv_sock, char *buf, int bytes) { + int len = 0; + int rlen = 0; + + while (len < bytes) { + rlen = read(serv_sock, &buf[len], bytes - len); + if (rlen < 1) { + return; + } + len = len + rlen; } +} - printf("ctdlmigrate will now begin a database migration...\n"); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s -w3600 MIGR export", - socket_path, remote_user, remote_host, remote_sendcommand); - sourcefp = popen(cmd, "r"); - if (!sourcefp) { - printf("\n%s\n\n", strerror(errno)); - exitcode = 2; - goto THEEND; +// send binary to server +void serv_write(int serv_sock, char *buf, int nbytes) { + int bytes_written = 0; + int retval; + while (bytes_written < nbytes) { + retval = write(serv_sock, &buf[bytes_written], nbytes - bytes_written); + if (retval < 1) { + return; + } + bytes_written = bytes_written + retval; } +} - snprintf(cmd, sizeof cmd, "%s -w3600 MIGR import", sendcommand); - targetfp = popen(cmd, "w"); - if (!targetfp) { - printf("\n%s\n\n", strerror(errno)); - exitcode = 3; - goto THEEND; + +// input string from socket - implemented in terms of serv_read() +void serv_gets(int serv_sock, char *buf) { + int i; + + // Read one character at a time. + for (i = 0;; i++) { + serv_read(serv_sock, &buf[i], 1); + if (buf[i] == '\n' || i == (SIZ-1)) + break; } - while (fgets(buf, sizeof buf, sourcefp) != NULL) { - if (fwrite(buf, strlen(buf), 1, targetfp) < 1) { - exitcode = 4; - printf("%s\n", strerror(errno)); - goto FAIL; + // If we got a long line, discard characters until the newline. + if (i == (SIZ-1)) { + while (buf[i] != '\n') { + serv_read(serv_sock, &buf[i], 1); } - ++linecount; - if ((linecount % 100) == 0) { - printf("%c\r", spinning[((linecount / 100) % 4)]); - fflush(stdout); + } + + // Strip all trailing nonprintables (crlf) + buf[i] = 0; +} + + +// send line to server - implemented in terms of serv_write() +void serv_puts(int serv_sock, char *buf) { + serv_write(serv_sock, buf, strlen(buf)); + serv_write(serv_sock, "\n", 1); +} + + +// send formatted printable data to the server +void serv_printf(int serv_sock, const char *format, ...) { + va_list arg_ptr; + char buf[1024]; + + va_start(arg_ptr, format); + if (vsnprintf(buf, sizeof buf, format, arg_ptr) == -1) + buf[sizeof buf - 2] = '\n'; + serv_write(serv_sock, buf, strlen(buf)); + va_end(arg_ptr); +} + + +// You know what main() does. If you don't, you shouldn't be trying to understand this program. +int main(int argc, char *argv[]) { + char ctdldir[PATH_MAX]=CTDLDIR; + char yesno[2]; + int cmdexit = 0; // when something fails, set cmdexit to nonzero, and skip to the end + char buf[PATH_MAX]; + + char remote_user[128]; + char remote_host[128]; + char remote_pass[128]; + char *remote_port = "504"; + + int linecount = 0; + int a; + int remote_server_socket = (-1); + int local_admin_socket = (-1); + int progress = 0; + + // Parse command line + while ((a = getopt(argc, argv, "h:")) != EOF) { + switch (a) { + case 'h': + strcpy(ctdldir, optarg); + break; + default: + fprintf(stderr, "ctdlmigrate: usage: ctdlmigrate [-h server_dir]\n"); + return(1); } } -FAIL: if (sourcefp) pclose(sourcefp); - if (targetfp) pclose(targetfp); - if (exitcode != 0) goto THEEND; + if (chdir(ctdldir) != 0) { + fprintf(stderr, "ctdlmigrate: %s: %s\n", ctdldir, strerror(errno)); + exit(errno); + } - /* We need to copy a bunch of other stuff, and will do so using rsync */ + signal(SIGINT, ctdlmigrate_exit); + signal(SIGQUIT, ctdlmigrate_exit); + signal(SIGTERM, ctdlmigrate_exit); + signal(SIGSEGV, ctdlmigrate_exit); + signal(SIGHUP, ctdlmigrate_exit); + signal(SIGPIPE, ctdlmigrate_exit); - snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s MIGR listdirs", - socket_path, remote_user, remote_host, remote_sendcommand); - sourcefp = popen(cmd, "r"); - if (!sourcefp) { - printf("\n%s\n\n", strerror(errno)); - exitcode = 2; - goto THEEND; + printf( "\033[2J\033[H\n" + " \033[32m╔═══════════════════════════════════════════════╗\n" + " ║ ║\n" + " ║ \033[33mCitadel over-the-wire migration utility \033[32m║\n" + " ║ ║\n" + " ╚═══════════════════════════════════════════════╝\033[0m\n" + "\n" + "This utility is designed to migrate your Citadel installation\n" + "to a new host system via a network connection, without disturbing\n" + "the source system. The target may be a different CPU architecture\n" + "and/or operating system. The source system should be running\n" + "Citadel version \033[33m%d\033[0m or newer, and the target system should be running\n" + "either the same version or a newer version.\n" + "\n" + "You must run this utility on the TARGET SYSTEM. Any existing data\n" + "on this system will be ERASED. Your target system will be on this\n" + "host in %s.\n" + "\n", + EXPORT_REV_MIN, ctdldir + ); + + getz(yesno, 1, NULL, "Do you wish to continue? "); + puts(yesno); + if (tolower(yesno[0]) != 'y') { + cmdexit = 1; } - while ((fgets(buf, sizeof buf, sourcefp)) && (strcmp(buf, "000"))) { - buf[strlen(buf)-1] = 0; - if (!strncasecmp(buf, "bio|", 4)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[4], ctdl_bio_dir); - } - else if (!strncasecmp(buf, "files|", 6)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[6], ctdl_file_dir); - } - else if (!strncasecmp(buf, "userpics|", 9)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[9], ctdl_usrpic_dir); + if (!cmdexit) { + printf( "\033[2J\033[H\n" + "\033[32m╔═══════════════════════════════════════════════╗\n" + "║ ║\n" + "║ \033[33mValidate source and target systems\033[32m ║\n" + "║ ║\n" + "╚═══════════════════════════════════════════════╝\033[0m\n" + "\n\n"); + + printf("First we must validate that the local target system is running\n"); + printf("and ready to receive data.\n"); + printf("Checking connectivity to Citadel in %s...\n", ctdldir); + local_admin_socket = uds_connectsock("citadel-admin.socket"); + if (local_admin_socket < 0) { + cmdexit = 1; } - else if (!strncasecmp(buf, "messages|", 9)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[9], ctdl_message_dir); + } + + if (!cmdexit) { + serv_gets(local_admin_socket, buf); + puts(buf); + if (buf[0] != '2') { + cmdexit = 1; } - else if (!strncasecmp(buf, "netconfigs|", 11)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[11], ctdl_netcfg_dir); + } + + if (!cmdexit) { + serv_puts(local_admin_socket, "ECHO Connection to Citadel Server succeeded."); + serv_gets(local_admin_socket, buf); + puts(buf); + if (buf[0] != '2') { + cmdexit = 1; } - else if (!strncasecmp(buf, "keys|", 5)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[5], ctdl_key_dir); + } + + if (!cmdexit) { + printf("\n"); + printf("OK, this side is ready to go. Now we must connect to the source system.\n\n"); + + getz(remote_host, sizeof remote_host, NULL, "Enter the name or IP address of the source system: "); + getz(remote_user, sizeof remote_user, "admin", " Enter the user name of an administrator account: "); + getz(remote_pass, sizeof remote_pass, NULL, " Enter the password for this account: "); + + remote_server_socket = tcp_connectsock(remote_host, remote_port); + if (remote_server_socket < 0) { + cmdexit = 1; } - else if (!strncasecmp(buf, "images|", 7)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[7], ctdl_image_dir); + } + + if (!cmdexit) { + serv_gets(remote_server_socket, buf); + puts(buf); + if (buf[0] != '2') { + cmdexit = 1; } - else if (!strncasecmp(buf, "info|", 5)) { - snprintf(cmd, sizeof cmd, "rsync -va --rsh='ssh -S %s' %s@%s:%s/ %s/", - socket_path, remote_user, remote_host, &buf[5], ctdl_info_dir); + } + + if (!cmdexit) { + serv_printf(remote_server_socket, "USER %s\n", remote_user); + serv_gets(remote_server_socket, buf); + puts(buf); + if (buf[0] != '3') { + cmdexit = 1; } - else { - strcpy(cmd, "false"); /* cheap and sleazy way to throw an error */ + } + + if (!cmdexit) { + serv_printf(remote_server_socket, "PASS %s\n", remote_pass); + serv_gets(remote_server_socket, buf); + puts(buf); + if (buf[0] != '2') { + cmdexit = 1; } - printf("%s\n", cmd); - cmdexit = system(cmd); - if (cmdexit != 0) { - exitcode += cmdexit; + } + + if (!cmdexit) { + printf( "\033[2J\033[H\n" + "\033[32m╔═══════════════════════════════════════════════════════════════════╗\n" + "║ ║\n" + "║ \033[33mMigrating from: %-50s\033[32m║\n" + "║ ║\n" + "╠═══════════════════════════════════════════════════════════════════╣\n" + "║ ║\n" + "║ Lines received: 0 Percent complete: 0 ║\n" + "║ ║\n" + "╚═══════════════════════════════════════════════════════════════════╝\033[0m\n" + "\n", remote_host + ); + } + + if (!cmdexit) { + serv_puts(remote_server_socket, "MIGR export"); + serv_gets(remote_server_socket, buf); + if (buf[0] != '1') { + printf("\n\033[31m%s\033[0m\n", buf); + cmdexit = 3; } } - pclose(sourcefp); -THEEND: if (exitcode == 0) { - printf("\n\n *** Citadel migration was successful! *** \n\n"); + if (!cmdexit) { + serv_puts(local_admin_socket, "MIGR import"); + serv_gets(local_admin_socket, buf); + if (buf[0] != '4') { + printf("\n\033[31m%s\033[0m\n", buf); + cmdexit = 3; + } } - else { - printf("\n\n *** Citadel migration was unsuccessful. *** \n\n"); + + if (!cmdexit) { + char *ptr; + time_t last_update = time(NULL); + + while (serv_gets(remote_server_socket, buf), strcmp(buf, "000")) { + ptr = strchr(buf, '\n'); + if (ptr) *ptr = 0; // remove the newline character + ++linecount; + if (!strncasecmp(buf, "", 10)) { + progress = atoi(&buf[10]); + printf("\033[8;65H\033[33m%d\033[0m\033[12;0H\n", progress); + } + if (time(NULL) != last_update) { + last_update = time(NULL); + printf("\033[8;19H\033[33m%d\033[0m\033[12;0H\n", linecount); + } + serv_puts(local_admin_socket, buf); + } + + serv_puts(local_admin_socket, "000"); } - printf("\nShutting down the socket connection...\n\n"); - snprintf(cmd, sizeof cmd, "ssh -S %s -N -O exit %s@%s", - socket_path, remote_user, remote_host); - cmdexit = system(cmd); - printf("\n"); - if (cmdexit != 0) { - printf("There was an error shutting down the socket.\n\n"); - exitcode = cmdexit; + + if ( (cmdexit == 0) && (progress < 100) ) { + printf("\033[31mERROR: source stream ended before 100 percent of data was received.\033[0m\n"); + ctdlmigrate_exit(86); } - unlink(socket_path); - exit(exitcode); + // FIXME restart the local server here + + close(remote_server_socket); + close(local_admin_socket); + ctdlmigrate_exit(cmdexit); }