File Coverage

lib/Sysync.pm
Criterion Covered Total %
statement 223 278 80.2
branch 67 138 48.5
condition 13 25 52.0
subroutine 19 32 59.3
pod 27 27 100.0
total 349 500 69.8


line stmt bran cond sub pod time code
1             package Sysync;
2 1     1   839 use strict;
  1         3  
  1         55  
3 1     1   6 use Digest::MD5 qw(md5_hex);
  1         2  
  1         68  
4 1     1   15 use File::Find;
  1         2  
  1         66  
5 1     1   5 use File::Path;
  1         2  
  1         3949  
6              
7             our $VERSION = '0.35';
8              
9             =head1 NAME
10              
11             Sysync - Simplistic system management
12              
13             =head1 SYNOPSIS
14              
15             See: http://sysync.nongnu.org/tutorial.html
16              
17             =head1 METHODS
18              
19             =head3 new
20              
21             Creates a new Sysync object.
22              
23             my $sysync = Sysync->new({
24             sysdir => '/var/sysync',
25             stagedir => '/var/sysync/stage', # if omitted, appends ./stage to sysdir
26             salt_prefix => '', # if omitted, defaults to '$6$'
27             log => $file_handle_for_logging,
28             });
29              
30             =cut
31              
32             sub new
33             {
34 1     1 1 1561 my ($class, $params) = @_;
35 1 50 33     28 my $self = {
      33        
36             sysdir => $params->{sysdir},
37             stagedir => ($params->{stagedir} || "$params->{sysdir}/stage"),
38             stagefilesdir => ($params->{stagefiledir} || "$params->{sysdir}/stage-files"),
39             salt_prefix => (exists($params->{salt_prefix}) ? $params->{salt_prefix} : '$6$'),
40             log => $params->{log},
41             };
42              
43 1         5 bless($self, $class);
44              
45 1         4 return $self;
46             }
47              
48             =head3 log
49              
50             Log a message.
51              
52             $self->log('the moon is broken');
53              
54             =cut
55              
56             sub log
57             {
58 14     14 1 25 my $self = shift;
59 14         673 my $lt = localtime;
60 14         54 my $log = $self->{log};
61              
62 14         1421 print $log "$lt: $_[0]\n";
63             }
64              
65             =head3 sysdir
66              
67             Returns the base system directory for sysync.
68              
69             =cut
70              
71 123     123 1 310 sub sysdir { shift->{sysdir} }
72              
73             =head3 stagedir
74              
75             Returns stage directory.
76              
77             =cut
78              
79 2 50   2 1 13 sub stagedir { $_[0]->{stagedir} || join('/', $_[0]->sysdir, 'stage' ) }
80              
81             =head3 stagefilesdir
82              
83             Returns stage-files directory.
84              
85             =cut
86              
87 1 50   1 1 6 sub stagefilesdir { $_[0]->{stagefilesdir} || join('/', $_[0]->sysdir, 'stage-files' ) }
88              
89             =head3 get_user
90              
91             Returns hashref of user information. It's worth noting that passwords should not be returned here for normal users.
92              
93             Example:
94              
95             {
96             username => 'wafflewizard',
97             uid => 1001,
98             fullname => 'Waffle E. Wizzard',
99             homedir => '/home/wafflewizard',
100             shell => '/bin/bash',
101             disabled => 0,
102             ssh_keys => [
103             'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA10YAFEAByOlrMmd5Beh73SOg7okHpK5Bz9dOgmYb4idR3A6iz+ycyXtnCmwSGdmh6AQoeKfJx+9rxLtvdHUzhRa/YejqBGsTwYl5Q+1bKbCkJfgZhtB99Xt5j7grXzrJ0zp2vTfG2mPndnD7xuQQQnLsZrFSoTY8FPvQo3a9R1wPIuxBGs5jWm9+pvluJtAT3I7IaVfylNBCGU8+Fw/qvJtWEesyqyRmFJZ47XzFKJ5EzB6hLaW+MAaCH6fZDycdjiTfJOMThtpFF557rqz5EN76VRqHpnkiqKpatMX4h0hiL/Snl+fbUxOYm5qcHughuis4Sf6xXoABsyz2lsrqiQ== wafflewizard',
104             ],
105             }
106              
107             =cut
108              
109 0     0 1 0 sub get_user { die 'needs implemented' }
110              
111             =head3 get_all_users
112              
113             Return array of all usernames.
114              
115             =cut
116              
117 0     0 1 0 sub get_all_users { die 'needs implemented' }
118              
119             =head3 get_user_password
120              
121             Return a user's encrypted password.
122              
123             =cut
124              
125 0     0 1 0 sub get_user_password { die 'needs implemented' }
126              
127             =head3 set_user_password
128              
129             Set a user's encrypted password.
130              
131             =cut
132              
133 0     0 1 0 sub set_user_password { die 'needs implemented' }
134              
135             =head3 get_users_from_group
136              
137             Returns array of users in a given group.
138              
139             =cut
140              
141 0     0 1 0 sub get_users_from_group { die 'needs implemented' }
142              
143             =head3 get_all_groups
144              
145             Returns array of all groups.
146              
147             =cut
148              
149 0     0 1 0 sub get_all_groups { die 'needs implemented' }
150              
151             =head3 get_all_hosts
152              
153             Returns all hosts.
154              
155             =cut
156              
157 0     0 1 0 sub get_all_hosts { die 'needs implemented' }
158              
159             =head3 must_refresh
160              
161             Returns true if sysync must refresh.
162              
163             Passing 1 or 0 as an argument sets whether this returns true.
164              
165             =cut
166              
167 0     0 1 0 sub must_refresh { die 'needs implemented' }
168              
169             =head3 must_refresh_files
170              
171             Returns true if sysync must refresh managed files.
172              
173             Passing 1 or 0 as an argument sets whether this returns true.
174              
175             =cut
176              
177 0     0 1 0 sub must_refresh_files { die 'needs implemented' }
178              
179             =head3 generate_user_line
180              
181             Generate a line for both the user and shadow file.
182              
183             =cut
184              
185             sub generate_user_line
186             {
187 84     84 1 118 my ($self, $user, $what) = @_;
188              
189 84   100     279 my $gid = $user->{gid} || $user->{uid};
190 84   66     273 my $fullname = $user->{fullname} || $user->{username};
191              
192 84         86 my $password = '*';
193              
194 84 100       142 if ($user->{password})
195             {
196 6         11 $password = $user->{password};
197             }
198             else
199             {
200 78         200 my $p = $self->get_user_password($user->{username});
201              
202 78 50       180 $password = $p if $p;
203             }
204              
205 84         93 my $line = q[];
206 84 100       185 if ($what eq 'passwd')
    50          
207             {
208 42         155 $line = join(':', $user->{username}, 'x', $user->{uid}, $gid,
209             $fullname, $user->{homedir}, $user->{shell});
210             }
211             elsif ($what eq 'shadow')
212             {
213 42 50       79 my $password = $user->{disabled} ? '!' : $password;
214 42         130 $line = join(':', $user->{username}, $password, 15198, 0, 99999, 7, '','','');
215             }
216              
217 84         233 return $line;
218             }
219              
220             =head3 generate_group_line
221              
222             Generate a line for the group file.
223              
224             =cut
225              
226             sub generate_group_line
227             {
228 216     216 1 273 my ($self, $group, $what) = @_;
229              
230 216   100     191 my $users = join(',', @{$group->{users} || []}) || '';
231              
232 216         255 my $line = '';
233 216 100       407 if ($what eq 'group')
    50          
234             {
235 108         398 $line = join(':', $group->{groupname}, 'x', $group->{gid}, $users);
236             }
237             elsif ($what eq 'gshadow')
238             {
239 108         540 $line = sprintf('%s:*::%s', $group->{groupname}, $users);
240             }
241             }
242              
243             =head3 is_valid_host
244              
245             Returns true if host is valid.
246              
247             =cut
248              
249 0     0 1 0 sub is_valid_host { die 'needs implemented' }
250              
251             =head3 get_host_user
252              
253             Given a host, then a username, return a hashref with user details.
254              
255             =cut
256              
257             sub get_host_user
258             {
259 0     0 1 0 my ($self, $host, $username) = @_;
260              
261 0         0 my $data = $self->get_host_users_groups($host);
262 0 0       0 my @users = @{$data->{users} || []};
  0         0  
263              
264 0         0 return (grep { $username eq $_->{username} } @users)[0];
  0         0  
265             }
266              
267             =head3 get_host_group
268              
269             Given a host, then a group name, return a hashref with group details.
270              
271             =cut
272              
273             sub get_host_group
274             {
275 0     0 1 0 my ($self, $host, $groupname) = @_;
276              
277 0         0 my $data = $self->get_host_users_groups($host);
278 0 0       0 my @groups = @{$data->{groups} || []};
  0         0  
279              
280 0         0 return (grep { $groupname eq $_->{groupname} } @groups)[0];
  0         0  
281             }
282              
283             =head3 get_host_users
284              
285             Given a host return a hashref with user details.
286              
287             =cut
288              
289             sub get_host_users
290             {
291 2     2 1 5 my ($self, $host, $username) = @_;
292              
293 2         9 my $data = $self->get_host_users_groups($host);
294 2 50       4 my @users = @{$data->{users} || []};
  2         13  
295              
296 2         5 my %u = map { $_->{username} => $_ } @users;
  28         61  
297              
298 2         44 return \%u;
299             }
300              
301             =head3 get_host_groups
302              
303             Given a host return a hashref with group details.
304              
305             =cut
306              
307             sub get_host_groups
308             {
309 2     2 1 6 my ($self, $host, $groupname) = @_;
310              
311 2         9 my $data = $self->get_host_users_groups($host);
312 2 50       7 my @groups = @{$data->{groups} || []};
  2         19  
313              
314 2         7 my %g = map { $_->{groupname} => $_ } @groups;
  72         150  
315              
316 2         44 return \%g;
317             }
318              
319             =head3 get_host_ent
320              
321             For a generate all of the password data, including ssh keys, for a specific host.
322              
323             =cut
324              
325             sub get_host_ent
326             {
327 3     3 1 839 my ($self, $host) = @_;
328              
329 3 50       17 return unless $self->is_valid_host($host);
330            
331 3         16 my $data = $self->get_host_users_groups($host);
332 3 50       8 my @users = @{$data->{users} || []};
  3         25  
333 3 50       4 my @groups = @{$data->{groups} || []};
  3         20  
334              
335 3         9 my $passwd = join("\n", map { $self->generate_user_line($_, 'passwd') } @users) . "\n";
  42         98  
336 3         13 my $shadow = join("\n", map { $self->generate_user_line($_, 'shadow') } @users) . "\n";
  42         85  
337 3         13 my $group = join("\n", map { $self->generate_group_line($_, 'group',) } @groups) . "\n";
  108         197  
338 3         18 my $gshadow = join("\n", map { $self->generate_group_line($_, 'gshadow',) } @groups) . "\n";
  108         196  
339              
340 3         17 my @ssh_keys;
341 3         7 for my $user (@users)
342             {
343 42 100       94 next unless $user->{ssh_keys};
344              
345 3 50 33     14 if ($user->{disabled} and $user->{ssh_keys})
346             {
347 0         0 $_ = "# $_" for @{$user->{ssh_keys}};
  0         0  
348 0         0 unshift @{$user->{ssh_keys}}, '### ACCOUNT DISABLED VIA SYSYNC';
  0         0  
349             }
350              
351 3 50       5 my $keys = join("\n", @{$user->{ssh_keys} || []});
  3         16  
352 3 50       11 $keys .= "\n" if $keys;
353              
354 3 50       14 next unless $keys;
355              
356 3         22 push @ssh_keys, {
357             username => $user->{username},
358             keys => $keys,
359             uid => $user->{uid},
360             };
361             }
362              
363             return {
364 3         65 passwd => $passwd,
365             shadow => $shadow,
366             group => $group,
367             gshadow => $gshadow,
368             ssh_keys => \@ssh_keys,
369             data => $data,
370             };
371             }
372              
373             =head3 get_host_files
374              
375             Generate a list of files with their content.
376              
377             Returns hashref:
378             '/etc/filename.conf' => {
379             mode => 600,
380             gid => 0,
381             uid => 0,
382             data => 'data is here'
383             }
384              
385             =cut
386              
387 0     0 1 0 sub get_host_files { die 'needs implemented' }
388              
389             =head3 update_host_files
390              
391             Build host files from specifications.
392              
393             =cut
394              
395             sub update_host_files
396             {
397 1     1 1 802 my ($self, $host) = @_;
398              
399 1         8 my $stagefilesdir = $self->stagefilesdir;
400 1         4 my $stagedir = $self->stagedir;
401              
402 1         3 my $r = 0;
403              
404 1 50       6 next unless $self->is_valid_host($host);
405 1         6 my $files = $self->get_host_files($host);
406              
407 1 50       47 unless (-d "$stagefilesdir/$host")
408             {
409 1         120 mkdir "$stagefilesdir/$host";
410 1         31 chmod 0755, "$stagefilesdir/$host";
411 1         21 chown 0, 0, "$stagefilesdir/$host";
412              
413 1         8 $self->log("creating: $stagefilesdir/$host");
414 1         3 $r++;
415             }
416              
417 1 50       26 unless (-d "$stagefilesdir/$host/etc")
418             {
419 1         55 mkdir "$stagefilesdir/$host/etc";
420 1         25 chmod 0755, "$stagefilesdir/$host/etc";
421 1         23 chown 0, 0, "$stagefilesdir/$host/etc";
422              
423 1         7 $self->log("creating: $stagefilesdir/$host/etc");
424 1         3 $r++;
425             }
426              
427 1 50       26 unless (-d "$stagefilesdir/$host/etc/ssh")
428             {
429 1         57 mkdir "$stagefilesdir/$host/etc/ssh";
430 1         25 chmod 0755, "$stagefilesdir/$host/etc/ssh";
431 1         21 chown 0, 0, "$stagefilesdir/$host/etc/ssh";
432              
433 1         8 $self->log("creating: $stagefilesdir/$host/etc/ssh");
434 1         3 $r++;
435             }
436              
437 1 50       29 unless (-d "$stagefilesdir/$host/etc/ssh/authorized_keys")
438             {
439 1         57 mkdir "$stagefilesdir/$host/etc/ssh/authorized_keys";
440 1         28 chmod 0755, "$stagefilesdir/$host/etc/ssh/authorized_keys";
441 1         28 chown 0, 0, "$stagefilesdir/$host/etc/ssh/authorized_keys";
442              
443 1         7 $self->log("creating: $stagefilesdir/$host/etc/ssh/authorized_keys");
444 1         3 $r++;
445             }
446              
447 1 50       2 for my $path (sort keys %{ $files || {} })
  1         8  
448             {
449 1         3 my $item = $files->{$path};
450 1 50       5 next unless $item->{directory};
451              
452 0         0 $item->{directory} =~ s/\/$//;
453              
454 0 0       0 next if $item->{directory} eq '/etc';
455 0 0       0 next if $item->{directory} eq '/etc/ssh';
456 0 0       0 next if $item->{directory} eq '/etc/ssh/authorized_keys';
457              
458 0         0 $item->{directory} =~ s/^\///;
459              
460 0         0 my @path_parts = split('/', $item->{directory});
461 0         0 my $filename = pop @path_parts;
462 0         0 my $parent_dir = join('/', @path_parts);
463              
464 0         0 $item->{file} =~ s/^\///;
465              
466 0 0       0 unless (-d "$stagefilesdir/$host/$parent_dir")
467             {
468 0         0 die "[$host: error] parent directory $parent_dir not defined for $item->{directory}\n";
469             }
470              
471 0 0       0 unless (-d "$stagefilesdir/$host/$item->{directory}")
472             {
473 0         0 mkdir "$stagefilesdir/$host/$item->{directory}";
474 0         0 $self->log("creating: $stagefilesdir/$host/$item->{directory}");
475             }
476              
477 0         0 my $mode = sprintf("%04i", $item->{mode});
478 0         0 chmod $mode, "$stagefilesdir/$host/$item->{directory}";
479 0         0 chown $item->{uid}, $item->{gid}, "$stagefilesdir/$host/$item->{directory}";
480              
481 0         0 $r++;
482             }
483              
484 1 50       3 for my $path (keys %{ $files || {} })
  1         5  
485             {
486 1         3 my $item = $files->{$path};
487 1 50       5 next unless $item->{file};
488              
489 1         6 my @path_parts = split('/', $item->{file});
490              
491 1         3 my $filename = pop @path_parts;
492 1         4 my $parent_dir = join('/', @path_parts);
493              
494 1         6 $item->{file} =~ s/^\///;
495              
496 1 50       22 unless (-d "$stagefilesdir/$host/$parent_dir")
497             {
498 0         0 die "[$host: error] directory $parent_dir not defined for $item->{file}\n";
499             }
500              
501 1 50       9 if ($self->write_file_contents("$stagefilesdir/$host/$item->{file}", $item->{data}))
502             {
503 1         2 $r++;
504             }
505              
506 1         6 my $mode = sprintf("%04i", $item->{mode});
507              
508 1         33 chmod oct($mode), "$stagefilesdir/$host/$item->{file}";
509 1         28 chown $item->{uid}, $item->{gid}, "$stagefilesdir/$host/$item->{file}";
510             }
511              
512             # get list of staging directory contents
513 1         2 my @staged_file_list;
514             File::Find::find({
515 5     5   363 wanted => sub { push @staged_file_list, $_ },
516 1         95 no_chdir => 1,
517             }, "$stagefilesdir/$host");
518              
519 1         6 for my $staged_file (@staged_file_list)
520             {
521 5 50       84 next unless -e $staged_file;
522              
523 5         34 (my $local_staged_file = $staged_file) =~ s/^$stagefilesdir\/$host//;
524 5 100       13 next unless $local_staged_file;
525              
526 4 50       9 next if $local_staged_file eq '/';
527 4 100       10 next if $local_staged_file eq '/etc';
528 3 100       8 next if $local_staged_file eq '/etc/ssh';
529 2 100       7 next if $local_staged_file eq '/etc/ssh/authorized_keys';
530              
531 1 50       5 unless ($files->{$local_staged_file})
532             {
533 0 0       0 if (-d $staged_file)
    0          
534             {
535 0         0 $self->log("deleting directory: $staged_file");
536 0         0 rmtree($staged_file);
537             }
538             elsif (-e $staged_file)
539             {
540 0         0 $self->log("deleting file: $staged_file");
541 0         0 unlink($staged_file);
542             }
543 0         0 $r++;
544             }
545             }
546              
547 1         9 return $r;
548             }
549              
550             =head3 update_all_hosts
551              
552             Iterate through every host and build password files.
553              
554             =cut
555              
556             sub update_all_hosts
557             {
558 1     1 1 995 my ($self, %params) = @_;
559              
560             # get list of hosts along with image name
561 1   33     9 my $hosts = $params{hosts} || $self->get_all_hosts;
562              
563             # first, build staging directories
564 1 50       3 my @hosts = keys %{ $hosts->{hosts} || {} };
  1         12  
565              
566 1         20 my $stagedir = $self->stagedir;
567              
568 1         3 my $r = 0;
569              
570 1         4 for my $host (@hosts)
571             {
572 1 50       7 next unless $self->is_valid_host($host);
573              
574 1 50       65 unless (-d "$stagedir/$host")
575             {
576 1         163 mkdir "$stagedir/$host";
577 1         44 chmod 0755, "$stagedir/$host";
578 1         35 chown 0, 0, "$stagedir/$host";
579 1         17 $self->log("creating: $stagedir/$host");
580 1         4 $r++;
581             }
582              
583 1 50       38 unless (-d "$stagedir/$host/etc")
584             {
585 1         76 mkdir "$stagedir/$host/etc";
586 1         32 chmod 0755, "$stagedir/$host/etc";
587 1         33 chown 0, 0, "$stagedir/$host/etc";
588 1         9 $self->log("creating: $stagedir/$host/etc");
589 1         4 $r++;
590             }
591              
592 1 50       36 unless (-d "$stagedir/$host/etc/ssh")
593             {
594 1         72 mkdir "$stagedir/$host/etc/ssh";
595 1         33 chmod 0755, "$stagedir/$host/etc/ssh";
596 1         60 chown 0, 0, "$stagedir/$host/etc/ssh";
597 1         11 $self->log("creating: $stagedir/$host/etc/ssh");
598 1         4 $r++;
599             }
600              
601 1 50       35 unless (-d "$stagedir/$host/etc/ssh/authorized_keys")
602             {
603 1         73 mkdir "$stagedir/$host/etc/ssh/authorized_keys";
604 1         34 chmod 0755, "$stagedir/$host/etc/ssh/authorized_keys";
605 1         30 chown 0, 0, "$stagedir/$host/etc/ssh/authorized_keys";
606 1         10 $self->log("creating: $stagedir/$host/etc/ssh/authorized_keys");
607 1         4 $r++;
608             }
609              
610             # write host files
611 1         8 my $ent_data = $self->get_host_ent($host);
612              
613 1 50       7 next unless $ent_data;
614              
615 1 50       3 for my $key (@{ $ent_data->{ssh_keys} || [] })
  1         5  
616             {
617 1         3 my $username = $key->{username};
618 1         3 my $uid = $key->{uid};
619 1         2 my $text = $key->{keys};
620              
621 1 50       20 if ($self->write_file_contents("$stagedir/$host/etc/ssh/authorized_keys/$username", $text))
622             {
623 1         41 chmod 0600, "$stagedir/$host/etc/ssh/authorized_keys/$username";
624 1         33 chown $uid, 0, "$stagedir/$host/etc/ssh/authorized_keys/$username";
625 1         4 $r++;
626             }
627             }
628              
629 36         64 my ($shadow_group) =
630 1 50       9 grep { $_->{groupname} eq 'shadow' }
631 1         2 @{ $ent_data->{data}{groups} || [ ] };
632              
633 1 50       4 $shadow_group = {} unless defined $shadow_group;
634 1   50     5 $shadow_group = $shadow_group->{gid} || 0;
635              
636 1 50       7 if ($self->write_file_contents("$stagedir/$host/etc/passwd", $ent_data->{passwd}))
637             {
638 1         34 chmod 0644, "$stagedir/$host/etc/passwd";
639 1         26 chown 0, 0, "$stagedir/$host/etc/passwd";
640 1         2 $r++;
641             }
642              
643 1 50       7 if ($self->write_file_contents("$stagedir/$host/etc/group", $ent_data->{group}))
644             {
645 1         31 chmod 0644, "$stagedir/$host/etc/group";
646 1         25 chown 0, 0, "$stagedir/$host/etc/group";
647 1         3 $r++;
648             }
649              
650 1 50       10 if ($self->write_file_contents("$stagedir/$host/etc/shadow", $ent_data->{shadow}))
651             {
652 1         30 chmod 0640, "$stagedir/$host/etc/shadow";
653 1         26 chown 0, $shadow_group, "$stagedir/$host/etc/shadow";
654 1         2 $r++;
655             }
656            
657 1 50       7 if ($self->write_file_contents("$stagedir/$host/etc/gshadow", $ent_data->{gshadow}))
658             {
659 1         28 chmod 0640, "$stagedir/$host/etc/gshadow";
660 1         27 chown 0, $shadow_group, "$stagedir/$host/etc/gshadow";
661 1         55 $r++;
662             }
663             }
664              
665 1         9 return $r;
666             }
667              
668             =head3 write_file_contents
669              
670             =cut
671              
672             sub write_file_contents
673             {
674 6     6 1 13 my ($self, $file, $data) = @_;
675              
676             # check to see if this differs
677              
678 6 50       174 if (-e $file)
679             {
680 0 0       0 if (md5_hex($data) eq md5_hex($self->read_file_contents($file)))
681             {
682 0         0 return;
683             }
684             }
685              
686 6         65 $self->log("writing: $file");
687              
688 6 50       101 if (-e $file)
689             {
690 0 0       0 unlink($file) or die $!;
691             }
692              
693 6 50       497 open(F, "> $file") or die $!;
694 6         274 print F $data;
695 6         264 close(F);
696              
697 6         41 return 1;
698             }
699              
700             =head3 read_file_contents
701              
702             =cut
703              
704             sub read_file_contents
705             {
706 100     100 1 176 my ($self, $file, %params) = @_;
707              
708 100 50 33     238 die "error: $file does not exist\n"
709             if $params{must_exist} and not -f $file;
710              
711 100 100       1396 return unless -e $file;
712              
713 22         1100 open(my $fh, $file);
714 22         859 my @content = <$fh>;
715 22         313 close($fh);
716              
717 22         347 return join('', @content);
718             }
719              
720             1;
721              
722              
723             =head1 COPYRIGHT
724              
725             L L
726              
727             =head1 LICENSE
728              
729             Copyright (C) 2012, 2013 Bizowie
730              
731             This file is part of Sysync.
732            
733             Sysync is free software: you can redistribute it and/or modify
734             it under the terms of the GNU Affero General Public License as
735             published by the Free Software Foundation, either version 3 of the
736             License, or (at your option) any later version.
737            
738             Sysync is distributed in the hope that it will be useful,
739             but WITHOUT ANY WARRANTY; without even the implied warranty of
740             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
741             GNU Affero General Public License for more details.
742            
743             You should have received a copy of the GNU Affero General Public License
744             along with this program. If not, see .
745              
746             =head1 AUTHOR
747              
748             Michael J. Flickinger, C<< >>
749              
750             =cut
751