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