1 /* quota.c -- program to report/reconstruct quotas
2 *
3 * Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 */
42
43 #include <config.h>
44
45 #ifdef HAVE_UNISTD_H
46 #include <unistd.h>
47 #endif
48 #include <stdlib.h>
49 #include <stdio.h>
50 #include <errno.h>
51 #include <string.h>
52 #include <syslog.h>
53 #include <sys/types.h>
54 #include <netinet/in.h>
55 #include <sys/stat.h>
56 #include <sys/poll.h>
57
58 #if HAVE_DIRENT_H
59 # include <dirent.h>
60 # define NAMLEN(dirent) strlen((dirent)->d_name)
61 #else
62 # define dirent direct
63 # define NAMLEN(dirent) (dirent)->d_namlen
64 # if HAVE_SYS_NDIR_H
65 # include <sys/ndir.h>
66 # endif
67 # if HAVE_SYS_DIR_H
68 # include <sys/dir.h>
69 # endif
70 # if HAVE_NDIR_H
71 # include <ndir.h>
72 # endif
73 #endif
74
75 #include "bsearch.h"
76 #include "global.h"
77 #include "exitcodes.h"
78 #include "mailbox.h"
79 #include "xmalloc.h"
80 #include "xstrlcpy.h"
81 #include "mboxlist.h"
82 #include "mboxname.h"
83 #include "quota.h"
84 #include "convert_code.h"
85 #include "util.h"
86 #include <jansson.h>
87
88 /* generated headers are not necessarily in current directory */
89 #include "imap/imap_err.h"
90
91 extern int optind;
92 extern char *optarg;
93
94 /* current namespace */
95 static struct namespace quota_namespace;
96
97 struct quotaentry {
98 char *name;
99 int refcount;
100 int deleted;
101 };
102
103 /* forward declarations */
104 static void usage(void);
105 static void reportquota(void);
106 static int buildquotalist(char *domain, char **roots, int nroots);
107 static int fixquotas(char *domain, char **roots, int nroots);
108 static int fixquota_dopass(char *domain, char **roots, int nroots,
109 mboxlist_cb *pass);
110 static int fixquota_fixroot(struct mailbox *mailbox, const char *root);
111 static int fixquota_finish(int thisquota);
112 static int (*compar)(const char *s1, const char *s2);
113
114 #define QUOTAGROW 300
115
116 static struct quotaentry *quotaroots;
117 static int quota_num = 0, quota_alloc = 0;
118 static int quota_todo = 0;
119
120 static int test_sync_mode = 0;
121
122 static json_t *jsonout;
123
main(int argc,char ** argv)124 int main(int argc,char **argv)
125 {
126 int opt;
127 int i;
128 int fflag = 0;
129 int r, code = 0;
130 int do_report = 1;
131 char *alt_config = NULL, *domain = NULL;
132
133 while ((opt = getopt(argc, argv, "C:d:fqJZ")) != EOF) {
134 switch (opt) {
135 case 'C': /* alt config file */
136 alt_config = optarg;
137 break;
138
139 case 'q':
140 do_report = 0;
141 break;
142
143 case 'd':
144 domain = optarg;
145 break;
146
147 case 'f':
148 fflag = 1;
149 break;
150
151 case 'J':
152 jsonout = json_object();
153 break;
154
155 /* deliberately undocumented option for testing */
156 case 'Z':
157 test_sync_mode = 1;
158 break;
159
160 default:
161 usage();
162 }
163 }
164
165 /* always report if not fixing, otherwise we do nothing */
166 if (!fflag)
167 do_report = 1;
168
169 cyrus_init(alt_config, "quota", 0, CONFIG_NEED_PARTITION_DATA);
170
171 /* Set namespace -- force standard (internal) */
172 if ((r = mboxname_init_namespace("a_namespace, 1)) != 0) {
173 syslog(LOG_ERR, "%s", error_message(r));
174 fatal(error_message(r), EC_CONFIG);
175 }
176
177 if (config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT))
178 compar = bsearch_compare_mbox;
179 else
180 compar = strcmp;
181
182 /*
183 * Lock mailbox list to prevent mailbox creation/deletion
184 * during work
185 */
186 mboxlist_init(0);
187 mboxlist_open(NULL);
188
189 quotadb_init(0);
190 quotadb_open(NULL);
191
192 quota_changelock();
193
194 if (!r)
195 r = buildquotalist(domain, argv+optind, argc-optind);
196
197 if (!r && fflag)
198 r = fixquotas(domain, argv+optind, argc-optind);
199
200 quota_changelockrelease();
201
202 if (r) code = convert_code(r);
203 else if (do_report) reportquota();
204
205 quotadb_close();
206 quotadb_done();
207
208 mboxlist_close();
209 mboxlist_done();
210
211 /* just for neatness */
212 for (i = 0; i < quota_num; i++)
213 free(quotaroots[i].name);
214 free(quotaroots);
215
216 if (jsonout) json_decref(jsonout);
217
218 cyrus_done();
219
220 return code;
221 }
222
usage(void)223 static void usage(void)
224 {
225 fprintf(stderr,
226 "usage: quota [-C <alt_config>] [-d <domain>] [-f] [-q] [prefix]...\n");
227 exit(EC_USAGE);
228 }
229
errmsg(const char * fmt,const char * arg,int err)230 static void errmsg(const char *fmt, const char *arg, int err)
231 {
232 char buf[1024];
233 size_t len;
234
235 len = snprintf(buf, sizeof(buf), fmt, arg);
236 if (len < sizeof(buf))
237 len += snprintf(buf+len, sizeof(buf)-len, ": %s", error_message(err));
238 if ((err == IMAP_IOERROR) && (len < sizeof(buf)))
239 len += snprintf(buf+len, sizeof(buf)-len, ": %s", strerror(errno));
240
241 syslog(LOG_ERR, "%s", buf);
242 fprintf(stderr, "%s\n", buf);
243 }
244
test_sync_wait(const char * mboxname)245 static void test_sync_wait(const char *mboxname)
246 {
247 char *filename;
248 struct stat sb;
249 clock_t start;
250 int status = 0;
251 int r;
252 #define TIMEOUT (30 * CLOCKS_PER_SEC)
253
254 if (!test_sync_mode)
255 return;
256 /* aha, we're in test synchronisation mode */
257
258 syslog(LOG_ERR, "quota -Z waiting for signal to do %s", mboxname);
259
260 filename = strconcat(config_dir, "/quota-sync/", mboxname, (char *)NULL);
261 start = sclock();
262
263 while ((r = stat(filename, &sb)) < 0 && errno == ENOENT) {
264 if (sclock() - start > TIMEOUT) {
265 status = 2;
266 break;
267 }
268 status = 1;
269 poll(NULL, 0, 20); /* try again in 20 millisec */
270 }
271
272 switch (status)
273 {
274 case 0:
275 syslog(LOG_ERR, "quota -Z did not wait");
276 break;
277 case 1:
278 syslog(LOG_ERR, "quota -Z waited %2.3f sec",
279 (sclock() - start) / (double) CLOCKS_PER_SEC);
280 break;
281 case 2:
282 syslog(LOG_ERR, "quota -Z timed out");
283 break;
284 }
285
286 free(filename);
287 #undef TIMEOUT
288 }
289
test_sync_done(const char * mboxname)290 static void test_sync_done(const char *mboxname)
291 {
292 char *filename;
293
294 if (!test_sync_mode)
295 return;
296 /* aha, we're in test synchronisation mode */
297
298 syslog(LOG_ERR, "quota -Z done with %s", mboxname);
299
300 filename = strconcat(config_dir, "/quota-sync/", mboxname, (char *)NULL);
301 unlink(filename);
302 free(filename);
303 }
304
305
306 /*
307 * A quotaroot was found, add it to our list
308 */
fixquota_addroot(struct quota * q,void * rock)309 static int fixquota_addroot(struct quota *q,
310 void *rock __attribute__((unused)))
311 {
312 struct quota localq;
313 struct txn *tid = NULL;
314 int r;
315
316 if (quota_num == quota_alloc) {
317 /* Create new qr list entry */
318 quota_alloc += QUOTAGROW;
319 quotaroots = (struct quotaentry *)
320 xrealloc((char *)quotaroots, quota_alloc * sizeof(struct quotaentry));
321 memset("aroots[quota_num], 0, QUOTAGROW * sizeof(struct quotaentry));
322 }
323
324 quotaroots[quota_num].name = xstrdup(q->root);
325
326 /* get a locked read */
327 quota_init(&localq, quotaroots[quota_num].name);
328 r = quota_read(&localq, &tid, 1);
329 if (r) {
330 errmsg("failed reading quota record for '%s'",
331 q->root, r);
332 goto done;
333 }
334
335 /* clean the scanused data if present */
336 if (localq.scanmbox) {
337 free(localq.scanmbox);
338 localq.scanmbox = NULL;
339
340 r = quota_write(&localq, &tid);
341 if (r) {
342 errmsg("failed writing quota record for '%s'",
343 q->root, r);
344 goto done;
345 }
346 }
347
348 done:
349 quota_free(&localq);
350 if (r) {
351 quota_abort(&tid);
352 free(quotaroots[quota_num].name);
353 quotaroots[quota_num].name = NULL;
354 }
355 else {
356 quota_commit(&tid);
357 quota_num++;
358 }
359
360 return r;
361 }
362
363 /*
364 * Build the list of quota roots in 'quota'
365 */
buildquotalist(char * domain,char ** roots,int nroots)366 int buildquotalist(char *domain, char **roots, int nroots)
367 {
368 int i, r = 0;
369 char buf[MAX_MAILBOX_BUFFER], *tail;
370 size_t domainlen = 0;
371
372 buf[0] = '\0';
373 tail = buf;
374 if (domain) {
375 domainlen = snprintf(buf, sizeof(buf), "%s!", domain);
376 tail += domainlen;
377 }
378
379 /* basic case - everything (potentially limited by domain still) */
380 if (!nroots) {
381 r = quota_foreach(buf, fixquota_addroot, buf, NULL);
382 if (r) {
383 errmsg("failed building quota list for '%s'", buf, IMAP_IOERROR);
384 }
385 }
386
387 /*
388 * Walk through all given pattern(s) and add all the quota roots
389 * with the matching prefixes.
390 */
391 for (i = 0; i < nroots; i++) {
392 strlcpy(tail, roots[i], sizeof(buf) - domainlen);
393 /* XXX - namespace fixes here */
394
395 r = quota_foreach(buf, fixquota_addroot, buf, NULL);
396 if (r) {
397 errmsg("failed building quota list for '%s'", buf, IMAP_IOERROR);
398 break;
399 }
400 }
401
402 return r;
403 }
404
findroot(const char * name,int * thisquota)405 static int findroot(const char *name, int *thisquota)
406 {
407 int i = config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT)
408 ? quota_todo : 0;
409
410 *thisquota = -1;
411
412 for (; i < quota_num; i++) {
413 const char *root = quotaroots[i].name;
414
415 /* have we already passed the name, then there can
416 * be no further matches */
417 if (compar(root, name) > 0)
418 break;
419
420 /* is the mailbox within this root? */
421 if (mboxname_is_prefix(name, root)) {
422 /* fantastic, but don't return yet, we may find
423 * a more exact match */
424 *thisquota = i;
425 }
426 }
427
428 if (*thisquota >= 0)
429 quotaroots[*thisquota].refcount++;
430
431 return 0;
432 }
433
434 /*
435 * Pass 2: account for mailbox 'name' when fixing the quota roots
436 */
fixquota_dombox(const mbentry_t * mbentry,void * rock)437 static int fixquota_dombox(const mbentry_t *mbentry, void *rock)
438 {
439 int r = 0;
440 const char *prefix = (const char *)rock;
441 size_t prefixlen = (prefix ? strlen(prefix) : 0);
442 struct mailbox *mailbox = NULL;
443 int thisquota = -1;
444 struct txn *txn = NULL;
445
446 if (config_getswitch(IMAPOPT_IMPROVED_MBOXLIST_SORT)) {
447 while (quota_todo < quota_num) {
448 const char *root = quotaroots[quota_todo].name;
449
450 /* in the future, definitely don't close yet */
451 if (compar(mbentry->name, root) < 0)
452 break;
453
454 /* inside the first root, don't close yet */
455 if (mboxname_is_prefix(mbentry->name, root))
456 break;
457
458 /* finished, close out now */
459 r = fixquota_finish(quota_todo);
460 quota_todo++;
461 if (r) goto done;
462 }
463 }
464
465 test_sync_wait(mbentry->name);
466
467 r = findroot(mbentry->name, &thisquota);
468 if (r) {
469 errmsg("failed finding quotaroot for mailbox '%s'", mbentry->name, r);
470 goto done;
471 }
472
473 r = mailbox_open_iwl(mbentry->name, &mailbox);
474 if (r) {
475 errmsg("failed opening header for mailbox '%s'", mbentry->name, r);
476 goto done;
477 }
478
479 if (thisquota == -1) {
480 /* no matching quotaroot exists, remove from
481 * mailbox if present */
482 if (mailbox->quotaroot) {
483 /* unless it's outside the current prefix of course */
484 if (strlen(mailbox->quotaroot) < prefixlen) goto done;
485 r = fixquota_fixroot(mailbox, NULL);
486 if (r) goto done;
487 }
488 }
489 else {
490 const char *root = quotaroots[thisquota].name;
491 quota_t useds[QUOTA_NUMRESOURCES];
492 struct quota localq;
493 int res;
494
495 /* matching quotaroot exists, ensure mailbox has the
496 * correct root */
497 if (!mailbox->quotaroot ||
498 strcmp(mailbox->quotaroot, root) != 0) {
499 r = fixquota_fixroot(mailbox, root);
500 if (r) goto done;
501 }
502
503 /* read the current data */
504 quota_init(&localq, root);
505 r = quota_read(&localq, &txn, 1);
506 if (r) goto done;
507
508 /* add the usage for this mailbox */
509 mailbox_get_usage(mailbox, useds);
510 for (res = 0; res < QUOTA_NUMRESOURCES; res++)
511 localq.scanuseds[res] += useds[res];
512
513 /* and mention that this mailbox has been scanned */
514 free(localq.scanmbox);
515 localq.scanmbox = xstrdup(mbentry->name);
516
517 r = quota_write(&localq, &txn);
518 quota_free(&localq);
519
520 if (r) {
521 quota_abort(&txn);
522 goto done;
523 }
524
525 quota_commit(&txn);
526 }
527
528 done:
529 mailbox_close(&mailbox);
530 test_sync_done(mbentry->name);
531
532 return r;
533 }
534
fixquota_fixroot(struct mailbox * mailbox,const char * root)535 int fixquota_fixroot(struct mailbox *mailbox,
536 const char *root)
537 {
538 int r;
539
540 fprintf(stderr, "%s: quota root %s --> %s\n", mailbox->name,
541 mailbox->quotaroot ? mailbox->quotaroot : "(none)",
542 root ? root : "(none)");
543
544 r = mailbox_set_quotaroot(mailbox, root);
545 if (r) errmsg("failed writing header for mailbox '%s'", mailbox->name, r);
546
547 return r;
548 }
549
550 /*
551 * Pass 3: finish fixing up a quota root
552 */
fixquota_finish(int thisquota)553 int fixquota_finish(int thisquota)
554 {
555 int res;
556 int r = 0;
557 struct txn *tid = NULL;
558 const char *root = quotaroots[thisquota].name;
559 struct quota localq;
560
561 if (!quotaroots[thisquota].refcount) {
562 quotaroots[thisquota].deleted = 1;
563 fprintf(stderr, "%s: removed\n", root);
564 r = quota_deleteroot(root);
565 if (r) {
566 errmsg("failed deleting quotaroot '%s'", root, r);
567 }
568 return r;
569 }
570
571 /* re-read the quota with the record locked */
572 quota_init(&localq, root);
573 r = quota_read(&localq, &tid, 1);
574 if (r) {
575 errmsg("failed reading quotaroot '%s'", root, r);
576 goto done;
577 }
578
579 /* is it different? */
580 for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
581 if (localq.scanuseds[res] != localq.useds[res]) {
582 fprintf(stderr, "%s: %s usage was " QUOTA_T_FMT ", now " QUOTA_T_FMT "\n",
583 root,
584 quota_names[res],
585 localq.useds[res],
586 localq.scanuseds[res]);
587 localq.useds[res] = localq.scanuseds[res];
588 }
589 }
590
591 /* remove the scanned data, we're now up-to-date */
592 free(localq.scanmbox);
593 localq.scanmbox = NULL;
594
595 r = quota_write(&localq, &tid);
596 if (r) {
597 errmsg("failed writing quotaroot: '%s'", root, r);
598 goto done;
599 }
600
601 done:
602 quota_free(&localq);
603
604 if (r) quota_abort(&tid);
605 else quota_commit(&tid);
606
607 return r;
608 }
609
610 /*
611 * Run a pass over all the quota roots
612 */
fixquota_dopass(char * domain,char ** roots,int nroots,mboxlist_cb * cb)613 int fixquota_dopass(char *domain, char **roots, int nroots,
614 mboxlist_cb *cb)
615 {
616 int i, r = 0;
617 char buf[MAX_MAILBOX_BUFFER], *tail;
618 size_t domainlen = 0;
619
620 buf[0] = '\0';
621 tail = buf;
622 if (domain) {
623 domainlen = snprintf(buf, sizeof(buf), "%s!", domain);
624 tail += domainlen;
625 }
626
627 /* basic case - everything (potentially limited by domain still) */
628 if (!nroots) {
629 r = mboxlist_allmbox(buf, cb, buf, /*incdel*/0);
630 if (r) {
631 errmsg("processing mbox list for '%s'", buf, IMAP_IOERROR);
632 }
633 }
634
635 /*
636 * Walk through all given pattern(s) and add all the quota roots
637 * with the matching prefixes.
638 */
639 for (i = 0; i < nroots; i++) {
640 strlcpy(tail, roots[i], sizeof(buf) - domainlen);
641
642 r = mboxlist_allmbox(buf, cb, buf, /*incdel*/0);
643 if (r) {
644 errmsg("processing mbox list for '%s'", buf, IMAP_IOERROR);
645 break;
646 }
647 }
648
649 return r;
650 }
651
652 /*
653 * Fix all the quota roots
654 */
fixquotas(char * domain,char ** roots,int nroots)655 int fixquotas(char *domain, char **roots, int nroots)
656 {
657 int r;
658
659 r = fixquota_dopass(domain, roots, nroots, fixquota_dombox);
660
661 while (!r && quota_todo < quota_num) {
662 r = fixquota_finish(quota_todo);
663 quota_todo++;
664 }
665
666 return r;
667 }
668
reportquota_resource(struct quota * quota,const char * root,int res,json_t * jsonroot)669 static void reportquota_resource(struct quota * quota, const char *root, int res, json_t *jsonroot)
670 {
671 if (jsonroot) {
672 json_t *obj = json_object();
673 json_object_set_new(obj, "used", json_integer(quota->useds[res]));
674 if (quota->limits[res] > 0)
675 json_object_set_new(obj, "limit", json_integer(quota->limits[res] * quota_units[res]));
676 json_object_set_new(jsonroot, quota_names[res], obj);
677 return;
678 }
679 if (quota->limits[res] > 0) {
680 printf(" %7lld %8lld", quota->limits[res],
681 (quota_t)((quota_t)((quota->useds[res] / quota_units[res])
682 * 100) / quota->limits[res]));
683 }
684 else if (quota->limits[res] == 0) {
685 printf(" 0 ");
686 }
687 else {
688 printf(" ");
689 }
690 printf(" %8lld %20s %s\n",
691 (quota_t)(quota->useds[res] / quota_units[res]),
692 quota_names[res], root);
693 }
694
695 /*
696 * Print out the quota report
697 */
reportquota(void)698 static void reportquota(void)
699 {
700 int i;
701 int res;
702
703 if (!jsonout)
704 printf(" Quota %% Used Used Resource Root\n");
705
706 for (i = 0; i < quota_num; i++) {
707 struct quota localq;
708 int r;
709
710 if (quotaroots[i].deleted) continue;
711
712 /* XXX - cache these from either the parse or the commit again */
713 quota_init(&localq, quotaroots[i].name);
714 r = quota_read(&localq, NULL, 0);
715 if (r) {
716 quota_free(&localq);
717 return;
718 }
719
720 mbname_t *mbname = mbname_from_intname(quotaroots[i].name);
721 const char *extname = mbname_extname(mbname, "a_namespace, NULL);
722
723 json_t *jsonroot = NULL;
724 if (jsonout) {
725 jsonroot = json_object();
726 json_object_set_new(jsonout, extname, jsonroot);
727 }
728
729 for (res = 0; res < QUOTA_NUMRESOURCES; res++) {
730 reportquota_resource(&localq, extname, res, jsonroot);
731 }
732
733 mbname_free(&mbname);
734 quota_free(&localq);
735 }
736
737 if (jsonout) {
738 char *buf = json_dumps(jsonout, JSON_INDENT(2));
739 printf("%s\n", buf);
740 free(buf);
741 }
742 }
743