Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / sbin / rt-serializer.in
1 #!@PERL@
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
8 #
9 # (Except where explicitly superseded by other copyright notices)
10 #
11 #
12 # LICENSE:
13 #
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18 #
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29 #
30 #
31 # CONTRIBUTION SUBMISSION POLICY:
32 #
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38 #
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47 #
48 # END BPS TAGGED BLOCK }}}
49 use strict;
50 use warnings;
51
52 # fix lib paths, some may be relative
53 BEGIN {
54     require File::Spec;
55     my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
56     my $bin_path;
57
58     for my $lib (@libs) {
59         unless ( File::Spec->file_name_is_absolute($lib) ) {
60             unless ($bin_path) {
61                 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
62                     $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
63                 }
64                 else {
65                     require FindBin;
66                     no warnings "once";
67                     $bin_path = $FindBin::Bin;
68                 }
69             }
70             $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
71         }
72         unshift @INC, $lib;
73     }
74
75 }
76
77 use RT;
78 RT::LoadConfig();
79 RT::Init();
80
81 @RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base );
82
83 use RT::Migrate;
84 use RT::Migrate::Serializer::File;
85 use Getopt::Long;
86 use Pod::Usage qw//;
87 use Time::HiRes qw//;
88
89 my %OPT;
90 GetOptions(
91     \%OPT,
92     "help|?",
93     "verbose|v!",
94     "quiet|q!",
95
96     "directory|d=s",
97     "force|f!",
98     "size|s=i",
99
100     "users!",
101     "groups!",
102     "deleted!",
103
104     "scrips!",
105     "tickets!",
106     "acls!",
107
108     "clone",
109     "incremental",
110
111     "gc=i",
112     "page=i",
113 ) or Pod::Usage::pod2usage();
114
115 Pod::Usage::pod2usage(-verbose => 1) if $OPT{help};
116
117 my %args;
118 $args{Directory}   = $OPT{directory};
119 $args{Force}       = $OPT{force};
120 $args{MaxFileSize} = $OPT{size} if $OPT{size};
121
122 $args{AllUsers}      = $OPT{users}    if defined $OPT{users};
123 $args{AllGroups}     = $OPT{groups}   if defined $OPT{groups};
124 $args{FollowDeleted} = $OPT{deleted}  if defined $OPT{deleted};
125
126 $args{FollowScrips}  = $OPT{scrips}   if defined $OPT{scrips};
127 $args{FollowTickets} = $OPT{tickets}  if defined $OPT{tickets};
128 $args{FollowACL}     = $OPT{acls}     if defined $OPT{acls};
129
130 $args{Clone}         = $OPT{clone}       if $OPT{clone};
131 $args{Incremental}   = $OPT{incremental} if $OPT{incremental};
132
133 $args{GC}   = defined $OPT{gc}   ? $OPT{gc}   : 5000;
134 $args{Page} = defined $OPT{page} ? $OPT{page} : 100;
135
136 if (($OPT{clone} or $OPT{incremental})
137         and grep { /^(users|groups|deleted|scrips|tickets|acls)$/ } keys %OPT) {
138     die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n";
139 }
140
141 my $walker;
142
143 my $gnuplot = `which gnuplot`;
144 my $msg = "";
145 if (-t STDOUT and not $OPT{verbose} and not $OPT{quiet}) {
146     $args{Progress} = RT::Migrate::progress(
147         top    => \&gnuplot,
148         bottom => sub { print "\n$msg"; $msg = ""; },
149         counts => sub { $walker->ObjectCount },
150         max    => { estimate() },
151     );
152     $args{MessageHandler} = sub {
153         print "\r", " "x60, "\r", $_[-1]; $msg = $_[-1];
154     };
155     $args{Verbose}  = 0;
156 }
157 $args{Verbose} = 0 if $OPT{quiet};
158
159
160 $walker = RT::Migrate::Serializer::File->new( %args );
161
162 my $log = RT::Migrate::setup_logging( $walker->{Directory} => 'serializer.log' );
163 print "Logging warnings and errors to $log\n" if $log;
164
165 print "Beginning database serialization...";
166 my %counts = $walker->Export;
167
168 my @files = $walker->Files;
169 print "Wrote @{[scalar @files]} files:\n";
170 print "    $_\n" for @files;
171 print "\n";
172
173 print "Total object counts:\n";
174 for (sort {$counts{$b} <=> $counts{$a}} keys %counts) {
175     printf "%8d %s\n", $counts{$_}, $_;
176 }
177
178 if ($log and -s $log) {
179     print STDERR "\n! Some warnings or errors occurred during serialization."
180                 ."\n! Please see $log for details.\n\n";
181 } else {
182     unlink $log;
183 }
184
185 sub estimate {
186     $| = 1;
187     my %e;
188
189     # Expected types we'll serialize
190     my @types = map {"RT::$_"} qw/
191         Queue Ticket Transaction Attachment Link
192         User  Group  GroupMember Attribute
193         CustomField CustomFieldValue
194         ObjectCustomField ObjectCustomFieldValue
195                                  /;
196
197     for my $class (@types) {
198         print "Estimating $class count...";
199         my $collection = $class . "s";
200         if ($collection->require) {
201             my $objs = $collection->new( RT->SystemUser );
202             $objs->FindAllRows;
203             $objs->UnLimit;
204             $objs->{allow_deleted_search} = 1 if $class eq "RT::Ticket";
205             $e{$class} = $objs->DBIx::SearchBuilder::Count;
206         }
207         print "\r", " "x60, "\r";
208     }
209
210     return %e;
211 }
212
213
214 sub gnuplot {
215     my ($elapsed, $rows, $cols) = @_;
216     my $length = $walker->StackSize;
217     my $file = $walker->Directory . "/progress.plot";
218     open(my $dat, ">>", $file);
219     printf $dat "%10.3f\t%8d\n", $elapsed, $length;
220     close $dat;
221
222     if ($rows <= 24 or not $gnuplot) {
223         print "\n\n";
224     } elsif ($elapsed) {
225         my $gnuplot = qx|
226             gnuplot -e '
227                 set term dumb $cols @{[$rows - 12]};
228                 set xlabel "Seconds";
229                 unset key;
230                 set xrange [0:*];
231                 set yrange [0:*];
232                 set title "Queue length";
233                 plot "$file" using 1:2 with lines
234             '
235         |;
236         if ($? == 0 and $gnuplot) {
237             $gnuplot =~ s/^(\s*\n)//;
238             print $gnuplot;
239             unlink $file;
240         } else {
241             warn "Couldn't run gnuplot (\$? == $?): $!\n";
242         }
243     } else {
244         print "\n" for 1..($rows - 13);
245     }
246 }
247
248 =head1 NAME
249
250 rt-serializer - Serialize an RT database to disk
251
252 =head1 SYNOPSIS
253
254     rt-validator --check && rt-serializer
255
256 This script is used to write out the entire RT database to disk, for
257 later import into a different RT instance.  It requires that the data in
258 the database be self-consistent, in order to do so; please make sure
259 that the database being exported passes validation by L<rt-validator>
260 before attempting to use C<rt-serializer>.
261
262 While running, it will attempt to estimate the number of remaining
263 objects to be serialized; these estimates are pessimistic, and will be
264 incorrect if C<--no-users>, C<--no-groups>, or C<--no-tickets> are used.
265
266 If the controlling terminal is large enough (more than 25 columns high)
267 and the C<gnuplot> program is installed, it will also show a textual
268 graph of the queue size over time.
269
270 =head2 OPTIONS
271
272 =over
273
274 =item B<--directory> I<name>
275
276 The name of the output directory to write data files to, which should
277 not exist yet; it is a fatal error if it does.  Defaults to
278 C<< ./I<$Organization>:I<Date>/ >>, where I<$Organization> is as set in
279 F<RT_SiteConfig.pm>, and I<Date> is today's date.
280
281 =item B<--force>
282
283 Remove the output directory before starting.
284
285 =item B<--size> I<megabytes>
286
287 By default, C<rt-serializer> chunks its output into data files which are
288 around 32Mb in size; this option is used to set a different threshold
289 size, in megabytes.  Note that this is the threshold after which it
290 rotates to writing a new file, and is as such the I<lower bound> on the
291 size of each output file.
292
293 =item B<--no-users>
294
295 By default, all privileged users are serialized; passing C<--no-users>
296 limits it to only those users which are referenced by serialized tickets
297 and history, and are thus necessary for internal consistency.
298
299 =item B<--no-groups>
300
301 By default, all groups are serialized; passing C<--no-groups> limits it
302 to only system-internal groups, which are needed for internal
303 consistency.
304
305 =item B<--no-deleted>
306
307 By default, all tickets, including deleted tickets, are serialized;
308 passing C<--no-deleted> skips deleted tickets during serialization.
309
310 =item B<--scrips>
311
312 No scrips or templates are serialized by default; this option forces all
313 scrips and templates to be serialized.
314
315 =item B<--acls>
316
317 No ACLs are serialized by default; this option forces all ACLs to be
318 serialized.
319
320 =item B<--no-tickets>
321
322 Skip serialization of all ticket data.
323
324 =item B<--clone>
325
326 Serializes your entire database, creating a clone.  This option should
327 be used if you want to migrate your RT database from one database type
328 to another (e.g.  MySQL to Postgres).  It is an error to combine
329 C<--clone> with any option that limits object types serialized.  No
330 dependency walking is performed when cloning. C<rt-importer> will detect
331 that your serialized data set was generated by a clone.
332
333 =item B<--incremental>
334
335 Will generate an incremenal serialized dataset using the data stored in
336 your IncrementalRecords database table.  This assumes that you have created
337 that table and run RT using the Record_Local.pm shim as documented in
338 C<docs/incremental-export/>.
339
340 =item B<--gc> I<n>
341
342 Adjust how often the garbage collection sweep is done; lower numbers are
343 more frequent.  See L</GARBAGE COLLECTION>.
344
345 =item B<--page> I<n>
346
347 Adjust how many rows are pulled from the database in a single query.  Disable
348 paging by setting this to 0.  Defaults to 100.
349
350 Keep in mind that rows from RT's Attachments table are the limiting factor when
351 determining page size.  You should likely be aiming for 60-75% of your total
352 memory on an otherwise unloaded box.
353
354 =item B<--quiet>
355
356 Do not show graphical progress UI.
357
358 =item B<--verbose>
359
360 Do not show graphical progress UI, but rather log was each row is
361 written out.
362
363 =back
364
365 =head1 GARBAGE COLLECTION
366
367 C<rt-serializer> maintains a priority queue of objects to serialize, or
368 searches which may result in objects to serialize.  When inserting into
369 this queue, it does no checking if the object in question is already in
370 the queue, or if the search will contain any results.  These checks are
371 done when the object reaches the front of the queue, or during periodic
372 garbage collection.
373
374 During periodic garbage collection, the entire queue is swept for
375 objects which have already been serialized, occur more than once in the
376 queue, and searches which contain no results in the database.  This is
377 done to reduce the memory footprint of the serialization process, and is
378 triggered when enough new objects have been placed in the queue.  This
379 parameter is tunable via the C<--gc> parameter, which defaults to
380 running garbage collection every 5,000 objects inserted into the queue;
381 smaller numbers will result in more frequent garbage collection.
382
383 The default of 5,000 is roughly tuned based on a database with several
384 thousand tickets, but optimal values will vary wildly depending on
385 database configuration and size.  Values as low as 25 have provided
386 speedups with smaller databases; if speed is a factor, experimenting
387 with different C<--gc> values may be helpful.  Note that there are
388 significant boundary condition changes in serialization rate, as the
389 queue empties and fills, causing the time estimates to be rather
390 imprecise near the start and end of the process.
391
392 Setting C<--gc> to 0 turns off all garbage collection.  Be aware that
393 this will bloat the memory usage of the serializer.  Any negative value
394 for C<--gc> turns off periodic garbage collection and instead objects
395 already serialized or in the queue are checked for at the time they
396 would be inserted.
397
398 =cut
399