Large migrations over SSH continue failing despite the addition of keepalives in...
[citadel] / citadel / ctdlmigrate.c
1 /*
2  * Across-the-wire migration utility for Citadel
3  *
4  * Copyright (c) 2009-2021 citadel.org
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <stdio.h>
18 #include <string.h>
19 #include <ctype.h>
20 #include <fcntl.h>
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <sys/utsname.h>
24 #include <sys/wait.h>
25 #include <signal.h>
26 #include <netdb.h>
27 #include <errno.h>
28 #include <limits.h>
29 #include <pwd.h>
30 #include <time.h>
31 #include <readline/readline.h>
32 #include <sys/socket.h>
33 #include <sys/un.h>
34 #include <libcitadel.h>
35 #include "citadel.h"
36 #include "axdefs.h"
37 #include "sysdep.h"
38 #include "config.h"
39 #include "citadel_dirs.h"
40
41
42
43 // yeah, this wouldn't work in a multithreaded program.
44 static int gmaxlen = 0;
45 static char *gdeftext = NULL;
46
47 static int limit_rl(FILE *dummy) {
48         if (rl_end > gmaxlen) {
49                 return '\b';
50         }
51         return rl_getc(dummy);
52 }
53
54
55 static int getz_deftext(void) {
56         if (gdeftext) {
57                 rl_insert_text(gdeftext);
58                 gdeftext = NULL;
59                 rl_startup_hook = NULL;
60         }
61         return 0;
62 }
63
64
65 /*
66  * Replacement for gets() that doesn't throw a compiler warning.
67  * We're only using it for some simple prompts, so we don't need
68  * to worry about attackers exploiting it.
69  */
70 void getz(char *buf, int maxlen, char *default_value, char *prompt) {
71         rl_startup_hook = getz_deftext;
72         rl_getc_function = limit_rl;
73         gmaxlen = maxlen;
74         gdeftext = default_value;
75         strcpy(buf, readline(prompt));
76 }
77
78
79 // These variables are used by both main() and ctdlmigrate_exit()
80 // They are global so that ctdlmigrate_exit can be called from a signal handler
81 FILE *sourcefp = NULL;
82 char socket_path[PATH_MAX];
83 pid_t sshpid = (-1);
84
85 void ctdlmigrate_exit(int cmdexit) {
86         if (sourcefp) {
87                 printf("Closing the data connection from the source system...\n");
88                 pclose(sourcefp);
89         }
90         unlink(socket_path);
91         if (sshpid > 0) {
92                 printf("Shutting down the SSH session...\n");
93                 kill(sshpid, SIGKILL);
94         }
95         printf("\n\n\033[3%dmExit code %d\033[0m\n", (cmdexit ? 1 : 2), cmdexit);
96         exit(cmdexit);
97 }
98
99
100 int uds_connectsock(char *sockpath) {
101         int s;
102         struct sockaddr_un addr;
103
104         memset(&addr, 0, sizeof(addr));
105         addr.sun_family = AF_UNIX;
106         strcpy(addr.sun_path, sockpath);
107
108         s = socket(AF_UNIX, SOCK_STREAM, 0);
109         if (s < 0) {
110                 fprintf(stderr, "ctdlmigrate: Can't create socket: %s\n", strerror(errno));
111                 ctdlmigrate_exit(3);
112         }
113
114         if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
115                 fprintf(stderr, "ctdlmigrate: can't connect: %s\n", strerror(errno));
116                 close(s);
117                 ctdlmigrate_exit(3);
118         }
119
120         return s;
121 }
122
123
124 /*
125  * input binary data from socket
126  */
127 void serv_read(int serv_sock, char *buf, int bytes) {
128         int len, rlen;
129
130         len = 0;
131         while (len < bytes) {
132                 rlen = read(serv_sock, &buf[len], bytes - len);
133                 if (rlen < 1) {
134                         return;
135                 }
136                 len = len + rlen;
137         }
138 }
139
140
141 /*
142  * send binary to server
143  */
144 void serv_write(int serv_sock, char *buf, int nbytes) {
145         int bytes_written = 0;
146         int retval;
147         while (bytes_written < nbytes) {
148                 retval = write(serv_sock, &buf[bytes_written], nbytes - bytes_written);
149                 if (retval < 1) {
150                         return;
151                 }
152                 bytes_written = bytes_written + retval;
153         }
154 }
155
156
157 /*
158  * input string from socket - implemented in terms of serv_read()
159  */
160 void serv_gets(int serv_sock, char *buf) {
161         int i;
162
163         /* Read one character at a time.
164          */
165         for (i = 0;; i++) {
166                 serv_read(serv_sock, &buf[i], 1);
167                 if (buf[i] == '\n' || i == (SIZ-1))
168                         break;
169         }
170
171         /* If we got a long line, discard characters until the newline.
172          */
173         if (i == (SIZ-1)) {
174                 while (buf[i] != '\n') {
175                         serv_read(serv_sock, &buf[i], 1);
176                 }
177         }
178
179         /* Strip all trailing nonprintables (crlf)
180          */
181         buf[i] = 0;
182 }
183
184
185 /*
186  * send line to server - implemented in terms of serv_write()
187  */
188 void serv_puts(int serv_sock, char *buf) {
189         serv_write(serv_sock, buf, strlen(buf));
190         serv_write(serv_sock, "\n", 1);
191 }
192
193
194 int main(int argc, char *argv[]) {
195         char ctdldir[PATH_MAX]=CTDLDIR;
196         char yesno[5];
197         int cmdexit = 0;                                // when something fails, set cmdexit to nonzero, and skip to the end
198         char cmd[PATH_MAX];
199         char buf[PATH_MAX];
200         char remote_user[SIZ];
201         char remote_host[SIZ];
202         char remote_sendcommand[PATH_MAX];
203         int linecount = 0;
204         int a;
205         int local_admin_socket = (-1);
206         int progress = 0;
207
208         /* Parse command line */
209         while ((a = getopt(argc, argv, "h:")) != EOF) {
210                 switch (a) {
211                 case 'h':
212                         strcpy(ctdldir, optarg);
213                         break;
214                 default:
215                         fprintf(stderr, "ctdlmigrate: usage: ctdlmigrate [-h server_dir]\n");
216                         return(1);
217                 }
218         }
219
220         if (chdir(ctdldir) != 0) {
221                 fprintf(stderr, "ctdlmigrate: %s: %s\n", ctdldir, strerror(errno));
222                 exit(errno);
223         }
224
225         signal(SIGINT, ctdlmigrate_exit);
226         signal(SIGQUIT, ctdlmigrate_exit);
227         signal(SIGTERM, ctdlmigrate_exit);
228         signal(SIGSEGV, ctdlmigrate_exit);
229         signal(SIGHUP, ctdlmigrate_exit);
230         signal(SIGPIPE, ctdlmigrate_exit);
231
232         printf( "\033[2J\033[H\n"
233                 "          \033[32m╔═══════════════════════════════════════════════╗\n"
234                 "          ║                                               ║\n"
235                 "          ║    \033[33mCitadel over-the-wire migration utility    \033[32m║\n"
236                 "          ║                                               ║\n"
237                 "          ╚═══════════════════════════════════════════════╝\033[0m\n"
238                 "\n"
239                 "This utility is designed to migrate your Citadel installation\n"
240                 "to a new host system via a network connection, without disturbing\n"
241                 "the source system.  The target may be a different CPU architecture\n"
242                 "and/or operating system.  The source system should be running\n"
243                 "Citadel version \033[33m%d\033[0m or newer, and the target system should be running\n"
244                 "either the same version or a newer version.  You will also need\n"
245                 "the \033[33mrsync\033[0m utility, and OpenSSH v4 or newer.\n"
246                 "\n"
247                 "You must run this utility on the TARGET SYSTEM.  Any existing data\n"
248                 "on this system will be ERASED.  Your target system will be on this\n"
249                 "host in %s.\n"
250                 "\n",
251                 EXPORT_REV_MIN, ctdldir
252         );
253
254         getz(yesno, 1, NULL, "Do you wish to continue? ");
255         puts(yesno);
256         if (tolower(yesno[0]) != 'y') {
257                 cmdexit = 1;
258         }
259
260         if (!cmdexit) {
261
262                 printf( "\033[2J\033[H\n"
263                         "          \033[32m╔═══════════════════════════════════════════════╗\n"
264                         "          ║                                               ║\n"
265                         "          ║       \033[33mValidate source and target systems\033[32m      ║\n"
266                         "          ║                                               ║\n"
267                         "          ╚═══════════════════════════════════════════════╝\033[0m\n"
268                         "\n\n");
269         
270                 printf("First we must validate that the local target system is running and ready to receive data.\n");
271                 printf("Checking connectivity to Citadel in %s...\n", ctdldir);
272                 local_admin_socket = uds_connectsock("citadel-admin.socket");
273
274                 serv_gets(local_admin_socket, buf);
275                 puts(buf);
276                 if (buf[0] != '2') {
277                         cmdexit = 1;
278                 }
279         }
280
281         if (!cmdexit) {
282                 serv_puts(local_admin_socket, "ECHO Connection to Citadel Server succeeded.");
283                 serv_gets(local_admin_socket, buf);
284                 puts(buf);
285                 if (buf[0] != '2') {
286                         cmdexit = 1;
287                 }
288         }
289
290         if (!cmdexit) {
291                 printf("\n");
292                 printf("OK, this side is ready to go.  Now we must connect to the source system.\n\n");
293                 printf("Enter the host name or IP address of the source system\n");
294                 printf("(example: ctdl.foo.org)\n");
295                 getz(remote_host, sizeof remote_host, NULL, "--> ");
296         
297                 while (IsEmptyStr(remote_user)) {
298                         printf("\n");
299                         printf("Enter the name of a user on %s who has full access to Citadel files.\n", remote_host);
300                         getz(remote_user, sizeof remote_user, "root", "--> ");
301                 }
302
303                 printf("\n");
304                 printf("Establishing an SSH connection to the source system...\n");
305                 sprintf(socket_path, "/tmp/ctdlmigrate-socket.%ld.%d", time(NULL), getpid());
306                 unlink(socket_path);
307
308                 snprintf(cmd, sizeof cmd, "ssh -MNf -o ServerAliveInterval=5 -S %s -l %s %s", socket_path, remote_user, remote_host);
309                 sshpid = fork();
310                 if (sshpid < 0) {
311                         printf("%s\n", strerror(errno));
312                         cmdexit = errno;
313                 }
314                 else if (sshpid == 0) {
315                         execl("/bin/bash", "bash", "-c", cmd, (char *) NULL);
316                         exit(1);
317                 }
318                 else {                                          // Wait for SSH to go into the background
319                         waitpid(sshpid, &cmdexit, 0);
320                 }
321         }
322
323         if (!cmdexit) {
324                 printf("\nTesting a command over the connection...\n\n");
325                 snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s 'echo Remote commands are executing successfully.'",
326                         socket_path, remote_user, remote_host);
327                 cmdexit = system(cmd);
328                 printf("\n");
329                 if (cmdexit != 0) {
330                         printf("\033[31mRemote commands are not succeeding.\033[0m\n\n");
331                 }
332         }
333
334         if (!cmdexit) {
335                 printf("\nLocating the remote 'sendcommand' and Citadel installation...\n");
336                 snprintf(remote_sendcommand, sizeof remote_sendcommand, "/usr/local/citadel/sendcommand");
337                 snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", socket_path, remote_user, remote_host, remote_sendcommand);
338                 cmdexit = system(cmd);
339
340                 if (cmdexit) {
341                         snprintf(remote_sendcommand, sizeof remote_sendcommand, "/usr/sbin/sendcommand");
342                         snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", socket_path, remote_user, remote_host, remote_sendcommand);
343                         cmdexit = system(cmd);
344                 }
345
346                 if (cmdexit) {
347                         printf("\n");
348                         printf("Unable to locate Citadel programs on the remote system.  Please enter\n"
349                                 "the name of the directory on %s which contains the 'sendcommand' program.\n"
350                                 "(example: /opt/foo/citadel)\n", remote_host);
351                         getz(remote_host, sizeof remote_host, "/usr/local/citadel", "--> ");
352                         snprintf(remote_sendcommand, sizeof remote_sendcommand, "%s/sendcommand", buf);
353                         snprintf(cmd, sizeof cmd, "ssh -S %s %s@%s %s NOOP", socket_path, remote_user, remote_host, remote_sendcommand);
354                         cmdexit = system(cmd);
355                         if (!cmdexit) {
356                                 printf("ctdlmigrate was unable to attach to the remote Citadel system.\n\n");
357                         }
358                 }
359         }
360
361         if (!cmdexit) {
362                 printf( "\033[2J\033[H\n"
363                         "          \033[32m╔═══════════════════════════════════════════════════════════════════╗\n"
364                         "          ║                                                                   ║\n"
365                         "          ║ \033[33mMigrating from: %-50s\033[32m║\n"
366                         "          ║                                                                   ║\n"
367                         "          ╠═══════════════════════════════════════════════════════════════════╣\n"
368                         "          ║                                                                   ║\n"
369                         "          ║ Lines received: 0                           Percent complete: 0   ║\n"
370                         "          ║                                                                   ║\n"
371                         "          ╚═══════════════════════════════════════════════════════════════════╝\033[0m\n"
372                         "\n", remote_host
373                 );
374
375                 snprintf(cmd, sizeof cmd, "ssh -o ServerAliveInterval=5 -S %s %s@%s %s -w3600 MIGR export", socket_path, remote_user, remote_host, remote_sendcommand);
376                 sourcefp = popen(cmd, "r");
377                 if (!sourcefp) {
378                         cmdexit = errno;
379                         printf("\n%s\n\n", strerror(errno));
380                 }
381         }
382
383         if (!cmdexit) {
384                 serv_puts(local_admin_socket, "MIGR import");
385                 serv_gets(local_admin_socket, buf);
386                 if (buf[0] != '4') {
387                         printf("\n\033[31m%s\033[0m\n", buf);
388                         cmdexit = 3;
389                 }
390         }
391
392         if (!cmdexit) {
393                 char *ptr;
394                 time_t last_update = time(NULL);
395                 while (ptr = fgets(buf, SIZ, sourcefp), (ptr != NULL)) {
396                         ptr = strchr(buf, '\n');
397                         if (ptr) *ptr = 0;      // remove the newline character
398                         ++linecount;
399                         if (!strncasecmp(buf, "<progress>", 10)) {
400                                 progress = atoi(&buf[10]);
401                                 printf("\033[8;75H\033[33m%d\033[0m\033[20;0H\n", progress);
402                         }
403                         if (time(NULL) != last_update) {
404                                 last_update = time(NULL);
405                                 printf("\033[8;29H\033[33m%d\033[0m\033[20;0H\n", linecount);
406                         }
407                         serv_puts(local_admin_socket, buf);
408                 }
409         
410                 serv_puts(local_admin_socket, "000");
411         }
412
413         if ( (cmdexit == 0) && (progress < 100) ) {
414                 printf("\033[31mERROR: source stream ended before 100 percent of data was received.\033[0m\n");
415                 ctdlmigrate_exit(86);
416         }
417
418         // FIXME restart the local server now
419
420         ctdlmigrate_exit(cmdexit);
421 }