source: dasscm/trunk/usr/bin/dasscm@ 884

Last change on this file since 884 was 884, checked in by joergs, on Jun 15, 2010 at 11:21:02 PM

preparation for bash completion

  • Property keyword set to id
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 41.8 KB
Line 
1#!/usr/bin/perl -w
2
3# $Id: dasscm 884 2010-06-15 21:21:02Z joergs $
4
5use strict;
6
7use Env
8 qw($DASSCM_PROD $DASSCM_REPO $USER $DASSCM_USERNAME $DASSCM_USER $DASSCM_PASSWORD $SHELL);
9use Cwd;
10use Getopt::Long;
11use File::Basename;
12use File::Compare;
13# used system("cp -a"), because File::Copy does not keep permissions
14#use File::Copy;
15use File::Find;
16use File::stat;
17use File::Path;
18use Term::ReadKey;
19
20#use Data::Dumper;
21
22#####################################################################
23#
24# global
25#
26
27# shell exit codes
28my $RETURN_OK = 0;
29my $RETURN_NOK = 1;
30
31# Nagios return codes
32my $RETURN_WARN = 1;
33my $RETURN_CRIT = 2;
34my $RETURN_UNKNOWN = 3;
35
36# documentation file (for usage)
37my $doc_file = "/usr/share/doc/packages/dasscm/dasscm_howto.txt";
38
39my $config_file = "/etc/dasscm.conf";
40my $config = get_config($config_file);
41
42
43
44my %COMMANDS = (
45 '--help' => 'help',
46 'help' => 'help',
47 'login' => 'login',
48 'init' => 'init',
49 'ls' => 'ls',
50 'update' => 'update',
51 'up' => 'update',
52 'add' => 'add',
53 'commit' => 'commit',
54 'checkin' => 'commit',
55 'ci' => 'commit',
56 'revert' => 'revert',
57 'blame' => 'blame',
58 'diff' => 'diff',
59 'status' => 'status',
60 'st' => 'status',
61 'check' => 'check',
62 'permissions' => 'permissions',
63 'cleanup' => 'cleanup',
64 'complete' => 'complete'
65);
66
67# desc: description (eg. for usage)
68# params: parameters
69# CMD
70# USER
71# PATH
72# REPOPATH
73# require:
74# WRITE commands that require write access (and therefore a login)
75my %COMMAND_DEFINITIONS = (
76 'help' => {
77 'desc' => [],
78 'params' => [ "CMD" ],
79 'function' => \&help,
80 },
81 'login' => {
82 'desc' => [],
83 'params' => [ "USER" ],
84 'function' => \&login
85 },
86 'init' => {
87 'desc' => [ "initialize local subversion checkout.", "This is the first thing to do (after configuring $config_file)" ],
88 'params' => [],
89 'function' => \&init
90 },
91 'ls' => {
92 'desc' => [],
93 'params' => [ "PATH" ],
94 'function' => \&ls
95 },
96 'update' => {
97 'desc' => [],
98 'params' => [ "PATH" ],
99 'function' => \&update
100 },
101 'add' => {
102 'desc' => [],
103 'params' => [ "PATH" ],
104 'require' => [ "WRITE" ],
105 'function' => \&add
106 },
107 'commit' => {
108 'desc' => [],
109 'params' => [ "PATH" ],
110 'require' => [ "WRITE" ],
111 'function' => \&commit
112 },
113 'revert' => {
114 'desc' => [ "revert local changes back to version from repository" ],
115 'params' => [ "REPOPATH" ],
116 'function' => \&revert
117 },
118 'blame' => {
119 'desc' => [],
120 'params' => [ "PATH" ],
121 'function' => \&blame
122 },
123 'diff' => {
124 'desc' => [],
125 'params' => [ "PATH" ],
126 'function' => \&diff
127 },
128 'status' => {
129 'desc' => [],
130 'params' => [ "PATH" ],
131 'function' => \&status
132 },
133 'check' => {
134 'desc' => [ "perform Nagios NRPE conform check" ],
135 'params' => [],
136 'function' => \&check
137 },
138 'permissions' => {
139 'desc' => [],
140 'params' => [],
141 'function' => \&permissions
142 },
143 'cleanup' => {
144 'desc' => [],
145 'params' => [],
146 'function' => \&cleanup
147 },
148 'complete' => {
149 'desc' => [ "internally, used for bash completion" ],
150 'params' => [ "CMD" ],
151 'function' => \&complete
152 },
153);
154
155
156# configuration file
157my $DASSCM_LOCAL_REPOSITORY_BASE;
158my $DASSCM_REPOSITORY_NAME;
159my $DASSCM_SVN_REPOSITORY;
160my $DASSCM_CHECKOUT_USERNAME;
161my $DASSCM_CHECKOUT_PASSWORD;
162my $DASSCM_PERMISSION_FILE;
163
164# current directory at program start
165my $StartDirectory = cwd();
166
167my $diff = "diff --exclude .svn ";
168my $SVN = "svn ";
169my $svnOptions = "";
170my $svnCheckoutCredentials = "";
171my $svnPasswordCredentials = "";
172
173# flag. Set to true by svn_update
174# This prevents, that svn_update is called multiple times
175my $svnRepositoryIsUptodate = 0;
176
177# command line options get stored in options hash
178my %options = ();
179
180# subcommand, that gets executed (add, commit, ...)
181my $command;
182
183my $verbose = 0;
184
185#####################################################################
186#
187# util functions
188#
189sub usage()
190{
191 print '$Id: dasscm 884 2010-06-15 21:21:02Z joergs $';
192 print "\n\n";
193 print "usage: dasscm <subcommand> [options] [args]\n";
194 print "\n";
195 print "dasscm is intended to help versioning configuration files\n";
196 print "\n";
197 print "Available subcommands:\n";
198 # print " help <subcommand>\n";
199 # print " init\n";
200 # print " login <username>\n";
201 # print " up <path>\n";
202 # print " ls <path>\n";
203 # print " add <path>\n";
204 # print " commit <path>\n";
205 # print " revert <path>\n";
206 # print " diff <path>\n";
207 # print " status <path>\n";
208 # print " check\n";
209 # print " cleanup\n";
210 # print " permissions\n";
211 foreach my $i (sort keys(%COMMAND_DEFINITIONS)) {
212 print " ", $i, " ", join( " ", get_command_possible_params($i) ), "\n";
213 foreach my $line ( get_command_desc($i) ) {
214 print " "x20, $line, "\n";
215 }
216 }
217 print "\n";
218 print "If dasscm is not yet configured, read $doc_file\n";
219}
220
221sub warning(@)
222{
223 print "Warning: " . join( "\n ", @_ ) . "\n";
224}
225
226sub error(@)
227{
228 print "Error: " . join( "\n ", @_ ) . "\n";
229}
230
231sub fatalerror(@)
232{
233 error(@_);
234
235 #print "Exiting\n";
236 exit 1;
237}
238
239#
240# reading config file and return key/value pairs as hash
241#
242sub get_config
243{
244 my $file = $_[0];
245
246 if ( !$file ) {
247 fatalerror( "failed to open config file" . $file );
248 }
249
250 my $data = {};
251
252 # try to open config file
253 if ( !open( FH, $file ) ) {
254 fatalerror( "failed to open config file" . $file );
255 } else {
256 while (<FH>) {
257 chomp;
258 if (/^#/) {
259 next;
260 }
261 if ( $_ =~ /=/g ) {
262
263 # splitting in 2 fields at maximum
264 my ( $option, $value ) = split( /=/, $_, 2 );
265 $option =~ s/^\s+//g;
266 $option =~ s/\s+$//g;
267 $option =~ s/\"+//g;
268 $value =~ s/^\s+//g;
269 $value =~ s/\s+$//g;
270 $value =~ s/\"+//g;
271
272 if ( length($option) ) {
273 $data->{$option} = $value;
274 }
275 }
276 }
277 }
278 close(FH);
279
280 return $data;
281}
282
283#
284# check and evaluate environment variables
285#
286sub check_env()
287{
288
289 # DASSCM_PROD
290 if ( !$DASSCM_PROD ) {
291 $DASSCM_PROD = "/";
292 }
293
294 if ( !-d $DASSCM_PROD ) {
295 die "DASSCM_PROD ($DASSCM_PROD) is not set to a directory.\n";
296 }
297 if ($verbose) { print "DASSCM_PROD: " . $DASSCM_PROD . "\n"; }
298
299 # DASSCM_REPOSITORY_NAME
300 if ( !$DASSCM_REPOSITORY_NAME ) {
301 die
302 "Variable DASSCM_REPOSITORY_NAME is not defined.\nIt needs to be a unique name.\nNormally the full qualified host name is used.\nUse file $config_file to configure it.\n";
303 }
304
305 # DASSCM_REPO
306 if ( !$DASSCM_REPO ) {
307 if ( $DASSCM_LOCAL_REPOSITORY_BASE && $DASSCM_REPOSITORY_NAME ) {
308 $DASSCM_REPO =
309 $DASSCM_LOCAL_REPOSITORY_BASE . "/" . $DASSCM_REPOSITORY_NAME;
310 } else {
311 die
312 "Envirnonment variable DASSCM_REPO not set.\nSet DASSCM_REPO to the directory of the versioning system checkout for this machine.\n";
313 }
314 }
315 if ($verbose) { print "DASSCM_REPO: " . $DASSCM_REPO . "\n"; }
316
317 #
318 # subversion checkout user
319 #
320 if ( !$DASSCM_CHECKOUT_USERNAME ) {
321 fatalerror(
322 "variable DASSCM_CHECKOUT_USERNAME is not defined.",
323 "Use file $config_file to configure it."
324 );
325 }
326
327 if ( !$DASSCM_CHECKOUT_PASSWORD ) {
328 fatalerror(
329 "variable DASSCM_CHECKOUT_PASSWORD is not defined.",
330 "Use file $config_file to configure it."
331 );
332 }
333
334 #
335 # check if local repository directory exist
336 # (if not creating by init)
337 #
338 if ( $command ne "init" ) {
339 if ( not -d $DASSCM_REPO ) {
340 die
341 "Can't access local repository DASSCM_REPO\n($DASSCM_REPO)\nCheck configuration and execute\n dasscm init\n";
342 }
343
344 #
345 # user settings
346 #
347
348 # DASSCM_USER is legacy. Use DASSCM_USERNAME instead
349 if ( !$DASSCM_USERNAME ) {
350 $DASSCM_USERNAME = $DASSCM_USER;
351 }
352
353 # user root is not allowed for checkins.
354 # if user is root, DASSCM_USER has to be set,
355 # otherwise USER can be used
356 if ( "$USER" eq "root" ) {
357 if ( ( not $DASSCM_USERNAME )
358 and ( get_command_requires_write( $command ) ) )
359 {
360
361 #( $command ne "login" ) and ( $command ne "status" ) ) {
362 fatalerror(
363 "Envirnonment variable DASSCM_USERNAME not set.",
364 "Set DASSCM_USERNAME to your subversion user account or",
365 "use 'dasscm login'"
366 );
367 }
368 $svnOptions .= " --no-auth-cache ";
369 } elsif ( !$DASSCM_USERNAME ) {
370 $DASSCM_USERNAME = $USER;
371 }
372
373 #
374 # password
375 #
376 if ($DASSCM_PASSWORD) {
377 $svnPasswordCredentials = " --password '$DASSCM_PASSWORD' ";
378 }
379 }
380
381 #$svnOptions .= " --username $DASSCM_USERNAME "
382}
383
384#
385# has been intendend,
386# to check addtitional parameters.
387# Currently not used.
388#
389sub check_parameter(@)
390{
391}
392
393sub get_command_uniform_name( $ )
394{
395 my $command_abbrivation = $_[0];
396 if( defined($COMMANDS{$command_abbrivation}) ) {
397 return $COMMANDS{$command_abbrivation};
398 }
399 return;
400}
401
402sub get_command_desc( $ )
403{
404 my $command = get_command_uniform_name($_[0]);
405 my @desc = ();
406 if( $command && defined($COMMAND_DEFINITIONS{$command}{'desc'}) ) {
407 @desc = @{$COMMAND_DEFINITIONS{$command}{'desc'}};
408 }
409 return @desc;
410}
411
412sub get_command_function( $ )
413{
414 my $command = get_command_uniform_name($_[0]);
415 my $func;
416 if( $command && defined($COMMAND_DEFINITIONS{$command}{'function'}) ) {
417 $func = $COMMAND_DEFINITIONS{$command}{'function'};
418 }
419 return $func;
420}
421
422sub get_command_possible_params( $ )
423{
424 my $command = get_command_uniform_name($_[0]);
425 my @params = ();
426 if( $command && defined($COMMAND_DEFINITIONS{$command}{'params'}) ) {
427 @params = @{$COMMAND_DEFINITIONS{$command}{'params'}};
428 }
429 return @params;
430}
431
432sub get_command_requirements( $ )
433{
434 my $command = get_command_uniform_name($_[0]);
435 my @requirements = ();
436 if( $command && defined($COMMAND_DEFINITIONS{$command}{'require'}) ) {
437 @requirements = @{$COMMAND_DEFINITIONS{$command}{'require'}};
438 }
439 return @requirements;
440}
441
442sub get_command_requires_write( $ )
443{
444 return grep( /^WRITE$/, get_command_requirements($_[0]) );
445}
446
447
448#
449# normalize path namens:
450# - directories should end with "/"
451# - use only single "/"
452#
453sub normalize_path($)
454{
455 my $path = shift || "";
456
457 if ( $path =~ m|^/| ) {
458
459 # full path
460 if ( -d $path ) {
461
462 # ensure, a directory ends with '/'
463 $path .= '/';
464 }
465 } elsif ( -d cwd() . '/' . $path ) {
466
467 # ensure, a directory ends with '/'
468 $path .= '/';
469 }
470
471 # remove double (triple) slashes (/)
472 $path =~ s|/[/]*|/|g;
473
474 return $path;
475}
476
477#
478# generate from (relative) filename
479# all required file and directory names:
480# $basename, $dirname_prod, $dirname_repo,
481# $filename_prod, $filename_repo
482#
483sub get_filenames(@)
484{
485 my $filename_prod = $_[0] || ".";
486
487 # make filename absolut
488 if ( !( $filename_prod =~ m/^\// ) ) {
489 $filename_prod = cwd() . '/' . $filename_prod;
490 }
491
492 # file must be readable. Only exception is, when file should be reverted
493 if ( $command ne "revert" ) {
494 if ( not -r $filename_prod ) {
495 fatalerror( $filename_prod . " is not accessable" );
496 }
497 }
498
499 # dirname buggy: eg. "/etc/" is reduced to "/",
500 # "/etc" is used as filename
501 # herefore make sure, that if filename is a directory,
502 # it will end by "/"
503 $filename_prod = normalize_path($filename_prod);
504
505 ( my $basename, my $dirname_prod ) = fileparse($filename_prod);
506
507 # normalize path.
508 # not done for reverting, because in this case, the directory may not exist
509 # and the correct path should already be stored in the repository
510 if ( $command ne "revert" ) {
511
512 # uses chdir to determine real directory in a unique way
513 chdir $dirname_prod
514 or fatalerror( "failed to access directory $dirname_prod: " . $! );
515 $dirname_prod = normalize_path( cwd() );
516 chdir $StartDirectory;
517 }
518
519 my $dirname_repo = normalize_path( $DASSCM_REPO . "/" . $dirname_prod );
520 my $filename_repo = normalize_path("$dirname_repo/$basename");
521
522 if ($verbose) {
523 print "filename_repo: " . $filename_repo . "\n";
524 print "dirname_repo: " . $dirname_repo . "\n";
525 print "filename_prod: " . $filename_prod . "\n";
526 print "dirname_prod: " . $dirname_prod . "\n";
527 print "basename: " . $basename . "\n";
528 }
529
530 return (
531 $basename, $dirname_prod, $dirname_repo,
532 $filename_prod, $filename_repo
533 );
534}
535
536sub copy_file_to_repository( $ )
537{
538 my $filename = shift;
539
540 (
541 my $basename,
542 my $dirname_prod,
543 my $dirname_repo,
544 my $filename_prod,
545 my $filename_repo
546 ) = get_filenames($filename);
547
548 #copy( $filename_prod, $filename_repo )
549 ( my $rc, my @result ) = run_command( "cp -a \"$filename_prod\" \"$filename_repo\"" );
550 if( $rc != 0 ) {
551 error( "failed to copy $filename_prod to repository: ", @result );
552 }
553
554 # return success
555 return $rc == 0;
556}
557
558
559
560sub copy_file_from_repository_to_system( $ )
561{
562 my $filename = shift;
563
564 (
565 my $basename,
566 my $dirname_prod,
567 my $dirname_repo,
568 my $filename_prod,
569 my $filename_repo
570 ) = get_filenames($filename);
571
572 ( my $rc, my @result ) = run_command( "cp -a \"$filename_repo\" \"$filename_prod\"" );
573 if( $rc != 0 ) {
574 error( "failed to copy $filename_repo to $filename_prod: ", @result );
575 }
576
577 # return success
578 return $rc == 0;
579}
580
581
582
583#
584# creates a file with permissions
585#
586sub generatePermissionList
587{
588
589 # generieren der Zeilen für Permission-Savefile
590 my @files = @_;
591 my @permlist = ();
592 foreach my $file (@files) {
593 $file = "/" . $file;
594 if ( -e $file ) {
595 my $info = stat($file) || die "failed to stat $file: aborting";
596 my $mode = get_type( $info->mode ) & 07777;
597 my $modestring = sprintf( "%04o", $mode );
598 my $uidnumber = $info->uid;
599 my $uid = getpwuid($uidnumber) || $uidnumber;
600 my $gidnumber = $info->gid;
601 my $gid = getgrgid($gidnumber) || $gidnumber;
602 push(
603 @permlist,
604 sprintf( "%-55s %-17s %4d",
605 $file, "${uid}:${gid}", $modestring )
606 );
607 }
608 }
609 return @permlist;
610}
611
612sub get_type
613{
614
615 # Funktion übernommen aus /usr/bin/chkstat
616 my $S_IFLNK = 0120000; # symbolic link
617 my $S_IFREG = 0100000; # regular file
618 my $S_IFDIR = 0040000; # directory
619 my $S_IFCHAR = 0020000; # character device
620 my $S_IFBLK = 0060000; # block device
621 my $S_IFFIFO = 0010000; # fifo
622 my $S_IFSOCK = 0140000; # socket
623 my $S_IFMT = 0170000; # type of file
624
625 my $S_m;
626 if ( ( $_[0] & $S_IFMT ) == $S_IFLNK ) { $S_m = $_[0] - $S_IFLNK; }
627 elsif ( ( $_[0] & $S_IFMT ) == $S_IFREG ) { $S_m = $_[0] - $S_IFREG; }
628 elsif ( ( $_[0] & $S_IFMT ) == $S_IFDIR ) { $S_m = $_[0] - $S_IFDIR; }
629 elsif ( ( $_[0] & $S_IFMT ) == $S_IFCHAR ) { $S_m = $_[0] - $S_IFCHAR; }
630 elsif ( ( $_[0] & $S_IFMT ) == $S_IFBLK ) { $S_m = $_[0] - $S_IFBLK; }
631 elsif ( ( $_[0] & $S_IFMT ) == $S_IFFIFO ) { $S_m = $_[0] - $S_IFFIFO; }
632 elsif ( ( $_[0] & $S_IFMT ) == $S_IFSOCK ) { $S_m = $_[0] - $S_IFSOCK; }
633 $S_m;
634}
635
636sub run_command
637{
638 my $command = shift;
639
640 #print "executing command: " . $command . "\n";
641
642 open( RESULT, $command . ' 2>&1 |' );
643 my @result = <RESULT>;
644 close(RESULT);
645 my $retcode = $? >> 8;
646
647 #print @result;
648 #if( $retcode ) { print "return code: " . $retcode . "\n"; }
649
650 return ( $retcode, @result );
651}
652
653sub run_interactive
654{
655
656 if ($verbose) {
657 print "run_interactive:" . join( " ", @_ ) . "\n";
658 }
659
660 system(@_);
661 if ( $? == -1 ) {
662 printf "failed to execute: $!\n";
663 } elsif ( $? & 127 ) {
664 printf "child died with signal %d, %s coredump\n", ( $? & 127 ),
665 ( $? & 128 ) ? 'with' : 'without';
666 } elsif ( $? >> 8 != 0 ) {
667 printf "child exited with value %d\n", $? >> 8;
668 }
669 return ( $? >> 8 );
670}
671
672sub svn_check_credentials( $$;$$ )
673{
674 my $username = shift;
675 my $password = shift;
676
677 # check silently are allow user interaction?
678 my $interactive = shift || 0;
679
680 # default: exit program, if repository is not accessable
681 # (do not exit for 'init')
682 my $fatalerror = shift || 1;
683
684 print "checking credentials ";
685
686 if ( !$username ) {
687 fatalerror("no username given");
688 }
689
690 if ( !$password ) {
691 fatalerror("no password given");
692 }
693
694 print "for " . $username . "@" . $DASSCM_SVN_REPOSITORY . ": ";
695
696 # Options for "svn info" are not supported by subversion 1.0.0 (SLES9),
697 # therefore switching to "svn status"
698 # ( my $rc_update, my @result ) =
699 # run_command(
700 # "$SVN info --non-interactive --no-auth-cache --username $username --password $password $DASSCM_SVN_REPOSITORY"
701 # );
702 #print @result;
703
704 my $rc_update;
705 if ($interactive) {
706 $rc_update = run_interactive(
707 "$SVN ls --no-auth-cache --username '$username' --password '$password' $DASSCM_SVN_REPOSITORY"
708 );
709 } else {
710 ( $rc_update, my @result ) = run_command(
711 "$SVN ls --non-interactive --no-auth-cache --username '$username' --password '$password' $DASSCM_SVN_REPOSITORY"
712 );
713
714 if ( $rc_update != 0 ) {
715 print "\n", @result;
716 if ($fatalerror) {
717 fatalerror();
718 }
719 return;
720 }
721 }
722
723 # return success
724 return $rc_update == 0;
725}
726
727sub svn_update( ;$ )
728{
729 my $update_path = shift || "";
730
731 # return value
732 my $update_ok = 1;
733
734 # use this flag to do only one update per run
735 if ( !$svnRepositoryIsUptodate ) {
736 ( my $rc_update, my @result ) = run_command(
737 "$SVN update --non-interactive $svnCheckoutCredentials '$DASSCM_REPO/$update_path'"
738 );
739 print @result;
740 if ( $rc_update != 0 ) {
741 error("failed to update local repository ($update_path)");
742 $update_ok = 0;
743 } elsif ( not $update_path ) {
744
745 # set this flag if a full update is done
746 $svnRepositoryIsUptodate = 1;
747 }
748 }
749 return $update_ok;
750}
751
752sub svn_ls( ;@ )
753{
754 (
755 my $basename,
756 my $dirname_prod,
757 my $dirname_repo,
758 my $filename_prod,
759 my $filename_repo
760 ) = get_filenames( $_[0] );
761
762 # svn ls -R is better, but much, much slower
763 # ( my $rc, my @result ) = run_command("$SVN ls --recursive $svnCheckoutCredentials $path");
764
765 my @files = ();
766 my @links = ();
767 my @dirs = ();
768 my @others = ();
769
770 find(
771 {
772 wanted => sub {
773 my $name = normalize_path($File::Find::name);
774 $name =~ s|^$dirname_repo||;
775
776 #print "($name)\n";# . $File::Find::dir . "\n";
777 if ( not $name ) {
778
779 # name string is empty (top directory).
780 # do nothing
781 } elsif ( $name =~ m/\.svn/ ) {
782
783 # skip svn meta data
784 } elsif ( -l $_ ) {
785
786 # soft link
787 # important: check for links first
788 # to exclude them from further checks
789 push( @links, $name );
790 } elsif ( -d $_ ) {
791
792 # directories
793 push( @dirs, $name );
794 } elsif ( -f $_ ) {
795
796 # regular file
797 push( @files, $name );
798 } else {
799 push( @others, $name );
800 }
801 }
802 },
803 ($filename_repo)
804 );
805
806 return ( sort( @dirs, @files ) );
807}
808
809sub svn_revert( ;$ )
810{
811 my $path = shift || $DASSCM_REPO;
812
813 ( my $rc_update, my @result ) = run_command("$SVN revert -R '$path'");
814
815 if ( $rc_update != 0 ) {
816 print "\n", @result;
817 error("failed to revert subversion repository changes");
818 }
819}
820
821sub svn_remove_unknown_files( ;$ )
822{
823 my $path = shift || $DASSCM_REPO;
824
825 ( my $rc_update, my @result ) = run_command("$SVN status '$path'");
826
827 if ( $rc_update != 0 ) {
828 print "\n", @result;
829 error("failed to receive subversion repository information");
830 } else {
831 foreach (@result) {
832 if (s/^\? +//) {
833 chomp;
834
835 # if file is unknown to subversion (line starts with "?")
836 # remove it
837 print "removing $_\n";
838
839 # unlink doesn't work recursive, there "rm -rf" is used
840 #unlink($_);
841 system("rm -rf $_");
842 }
843 }
844 }
845}
846
847sub getModifiedFiles( ;$ )
848{
849 (
850 my $basename,
851 my $dirname_prod,
852 my $dirname_repo,
853 my $filename_prod,
854 my $filename_repo
855 ) = get_filenames( $_[0] );
856
857 my @files = svn_ls($filename_prod);
858
859 # stores result from status (cvscheck)
860 my %removedfiles = ();
861 my %changedfiles = ();
862 my %unknownfiles = ();
863
864 # create list of modified files
865 if (@files) {
866
867 foreach my $file (@files) {
868
869 my $realfile = $dirname_prod . $file;
870 my $cvsworkfile = $dirname_repo . $file;
871
872 if ( -d $realfile ) {
873
874 # directory
875 if ( !-d "$cvsworkfile" ) {
876
877 # real is directory, repository is not. This is a problem
878 $changedfiles{"$realfile"} = $cvsworkfile;
879 }
880 } elsif ( !-e $realfile ) {
881 $removedfiles{"$realfile"} = $cvsworkfile;
882 } elsif ( !-r $realfile ) {
883
884 # don't have permission to read the file,
885 # can't check it
886 $unknownfiles{"$realfile"} = $cvsworkfile;
887 } else {
888 ( -r "$cvsworkfile" )
889 || fatalerror("failed to read $cvsworkfile");
890 if ( compare( $cvsworkfile, $realfile ) != 0 ) {
891 $changedfiles{"$realfile"} = $cvsworkfile;
892 }
893 }
894 }
895 }
896
897 return ( \%changedfiles, \%removedfiles, \%unknownfiles );
898}
899
900#
901# from an array of files/dirs,
902# generates list of files
903# sorted by type
904#
905sub get_files( @ )
906{
907 my @files = ();
908 my @links = ();
909 my @dirs = ();
910 my @others = ();
911
912 if (@_) {
913 find(
914 {
915 wanted => sub {
916 my $fullname = cwd() . "/" . $_;
917 if ( -l $_ ) {
918
919 # soft link
920 # important: check for links first
921 # to exclude them from further checks
922 push( @links, $fullname );
923 } elsif ( -d $_ ) {
924
925 # directories
926 push( @dirs, $fullname );
927 } elsif ( -f $_ ) {
928
929 # regular file
930 push( @files, $fullname );
931 } else {
932 push( @others, $fullname );
933 }
934 }
935 },
936 @_
937 );
938 }
939
940 # don't rely on others.
941 # If more specific file types are needed,
942 # they will be added
943 return {
944 files => \@files,
945 links => \@links,
946 dirs => \@dirs,
947 others => \@others
948 };
949}
950
951#####################################################################
952#
953# functions
954sub help(;@)
955{
956 if ( @_ == 0 ) {
957 usage();
958 } else {
959 print "help for @_: ...\n";
960 usage();
961 }
962}
963
964sub login(@)
965{
966 check_parameter( @_, 1 );
967 check_env();
968
969 my $input_username = $_[0];
970
971 if ( not $input_username ) {
972 my $output_username = "";
973 if ($DASSCM_USERNAME) {
974 $output_username = " ($DASSCM_USERNAME)";
975 }
976
977 print "Enter DASSCM user name", $output_username, ": ";
978 $input_username = <STDIN>;
979 chomp($input_username);
980
981 $input_username = $input_username || $DASSCM_USERNAME;
982 }
983
984 # hidden password input
985 print "Enter password for $input_username: ";
986 ReadMode('noecho');
987 my $input_password = <STDIN>;
988 ReadMode('normal');
989 chomp($input_password);
990 print "\n";
991
992 # checking checkout username/password
993 svn_check_credentials( $DASSCM_CHECKOUT_USERNAME,
994 $DASSCM_CHECKOUT_PASSWORD );
995 print "checkout access okay\n";
996
997 svn_check_credentials( $input_username, $input_password );
998
999 #
1000 # set environment variables
1001 #
1002 $ENV{'DASSCM_USERNAME'} = "$input_username";
1003 $ENV{'DASSCM_PASSWORD'} = "$input_password";
1004
1005 print "subversion access okay\n\n", "DASSCM_USERNAME: $input_username\n",
1006 "DASSCM_PASSWORD: (hidden)\n", "DASSCM_PROD: $DASSCM_PROD\n",
1007 "DASSCM_REPO: $DASSCM_REPO\n",
1008 "Server Repository: $DASSCM_SVN_REPOSITORY\n", "\n";
1009
1010 status();
1011
1012 print "\n[dasscm shell]\n\n";
1013 my $shell = $SHELL || "bash";
1014 exec($shell) or die "failed to start new shell";
1015}
1016
1017#
1018# initialize local checkout directory (initial checkout)
1019#
1020sub init(@)
1021{
1022 check_parameter( @_, 1 );
1023 check_env();
1024
1025 # don't do repository creation (svn mkdir) here,
1026 # because then their must be a lot of prior checks
1027
1028 # update complete repository
1029 # and create permission file
1030 my $retcode = run_interactive(
1031 "cd $DASSCM_LOCAL_REPOSITORY_BASE; $SVN checkout $svnCheckoutCredentials $svnOptions $DASSCM_SVN_REPOSITORY; mkdir -p `dirname $DASSCM_PERMISSION_FILE`; touch $DASSCM_PERMISSION_FILE"
1032 );
1033}
1034
1035sub ls(@)
1036{
1037 check_parameter( @_, 1 );
1038 check_env();
1039
1040 my @files = svn_ls(@_);
1041
1042 if (@files) {
1043 print join( "\n", @files );
1044 print "\n";
1045 }
1046}
1047
1048sub update(@)
1049{
1050 check_parameter( @_, 1 );
1051 check_env();
1052
1053 #
1054 # update local repository
1055 #
1056 svn_update();
1057}
1058
1059#
1060# helper function for "add" command
1061#
1062sub add_helper(@)
1063{
1064 (
1065 my $basename,
1066 my $dirname_prod,
1067 my $dirname_repo,
1068 my $filename_prod,
1069 my $filename_repo
1070 ) = get_filenames( $_[0] );
1071
1072 mkpath($dirname_repo);
1073 copy_file_to_repository( $filename_prod );
1074
1075 # already checked in?
1076 chdir $DASSCM_REPO;
1077
1078 # also add the path to filename.
1079 for my $dir ( split( '/', $dirname_prod ) ) {
1080 if ($dir) {
1081 my ( $rc, @out ) = run_command("$SVN add --non-recursive '$dir'");
1082 if ( $rc > 0 ) {
1083 print join( "\n", @out );
1084 }
1085 chdir $dir;
1086 }
1087 }
1088 my ( $rc, @out ) = run_command("$SVN add '$basename'");
1089 if ( $rc > 0 ) {
1090 print join( "\n", @out );
1091 }
1092 chdir $StartDirectory;
1093
1094}
1095
1096#
1097# adding new files (or directories)
1098#
1099sub add(@)
1100{
1101 check_parameter( @_, 1 );
1102 check_env();
1103
1104 #
1105 # update local repository
1106 #
1107 svn_update();
1108
1109 # get all regular files and links
1110 my $href_files = get_files(@_);
1111
1112 #print Dumper( $href_files );
1113
1114 my @files = @{ $href_files->{files} };
1115 my @links = @{ $href_files->{links} };
1116
1117 if (@files) {
1118 my $number = $#files + 1;
1119 print "files to check-in ($number): \n";
1120 print join( "\n", @files );
1121 print "\n";
1122 }
1123
1124 # TODO: check in links and also link target? At least warn about link target
1125 if (@links) {
1126 my $number = $#links + 1;
1127 print "\n";
1128 print "ignoring links ($number):\n";
1129 print join( "\n", @links );
1130 print "\n";
1131 }
1132
1133 # TODO: confirm
1134
1135 # copy files one by one to local repository
1136 for my $file (@files) {
1137
1138 # add file
1139 add_helper($file);
1140 }
1141
1142 # create new permissions file
1143 permissions();
1144
1145 # add permissions file
1146 add_helper($DASSCM_PERMISSION_FILE);
1147
1148 if ( $options{'message'} ) {
1149 $svnOptions .= " --message \"$options{'message'}\" ";
1150 }
1151
1152 # commit calls $EDITOR.
1153 # use "interactive" here, to display output
1154 my $retcode = run_interactive(
1155 "$SVN commit $svnOptions --username '$DASSCM_USERNAME' $svnPasswordCredentials $DASSCM_REPO"
1156 );
1157
1158 # svn commit does not deliever an error return code, if commit is canceld,
1159 # so a revert is performed in any case
1160 svn_revert();
1161}
1162
1163#
1164# checks in all modified files
1165#
1166sub commit(@)
1167{
1168 check_parameter( @_, 1 );
1169 check_env();
1170
1171 (
1172 my $basename,
1173 my $dirname_prod,
1174 my $dirname_repo,
1175 my $filename_prod,
1176 my $filename_repo
1177 ) = get_filenames( $_[0] );
1178
1179 #
1180 # update local repository
1181 #
1182 svn_update();
1183
1184 ( my $refChangedFiles, my $refRemovedFiles ) =
1185 getModifiedFiles($filename_prod);
1186 my %changedfiles = %{$refChangedFiles};
1187 my %removedfiles = %{$refRemovedFiles};
1188
1189 if (%removedfiles) {
1190 my $removedFilesString =
1191 '"' . join( '" "', values(%removedfiles) ) . '"';
1192 my ( $rc, @out ) = run_command("$SVN rm $removedFilesString");
1193 if ( $rc > 0 ) {
1194 print join( "\n", @out );
1195 }
1196 }
1197
1198 # copy files one by one to local repository
1199 for my $file ( keys(%changedfiles) ) {
1200 copy_file_to_repository($file);
1201 }
1202
1203 # create new permissions file
1204 permissions();
1205
1206 # add permissions file
1207 add_helper($DASSCM_PERMISSION_FILE);
1208
1209 if ( $options{'message'} ) {
1210 $svnOptions .= " --message \"$options{'message'}\" ";
1211 }
1212
1213 # commit calls $EDITOR.
1214 # use "interactive" here, to display output
1215 my $retcode = run_interactive(
1216 "$SVN commit $svnOptions --username '$DASSCM_USERNAME' $svnPasswordCredentials $DASSCM_REPO"
1217 );
1218
1219 # svn commit does not deliever an error return code, if commit is canceld,
1220 # so a revert is performed in any case
1221 svn_revert();
1222}
1223
1224#
1225# revert: copies files back from repository to system
1226#
1227sub revert(@)
1228{
1229 check_parameter( @_, 1 );
1230 check_env();
1231
1232 (
1233 my $basename,
1234 my $dirname_prod,
1235 my $dirname_repo,
1236 my $filename_prod,
1237 my $filename_repo
1238 ) = get_filenames( $_[0] );
1239
1240 # return code for the shell
1241 # default: error
1242 my $return_code = $RETURN_OK;
1243
1244 # cleanup repository
1245 cleanup();
1246 #svn_update();
1247
1248 ( my $refChangedFiles, my $refRemovedFiles, my $refUnknownFiles ) =
1249 getModifiedFiles($filename_prod);
1250 my %changedfiles = %{$refChangedFiles};
1251 my %removedfiles = %{$refRemovedFiles};
1252 my %unknownfiles = %{$refUnknownFiles};
1253
1254 if ( %removedfiles or %changedfiles or %unknownfiles ) {
1255
1256 if (%removedfiles) {
1257 print "DELETED files and directories. Recreated from repository:\n";
1258 my @removedPaths =
1259 ( sort { length $a > length $b } keys %removedfiles );
1260 print join( "\n", @removedPaths ) . "\n\n";
1261
1262 # copy files one by one from local repository to system
1263 # and also create directories
1264 # paths are sorted, so that directories are created first
1265 for my $real_path (@removedPaths) {
1266 if ( -d $removedfiles{"$real_path"} ) {
1267 mkpath("$real_path");
1268 } else {
1269 copy_file_from_repository_to_system( $real_path );
1270 }
1271 }
1272 }
1273
1274 if (%changedfiles) {
1275 print "MODIFIED files. Copied from repository to the system:\n";
1276 print join( "\n", ( keys %changedfiles ) ) . "\n\n";
1277
1278 # copy files one by one from local repository to system
1279 for my $real_file ( keys(%changedfiles) ) {
1280 copy_file_from_repository_to_system( $real_file );
1281 }
1282
1283 }
1284
1285 if (%unknownfiles) {
1286 print "UNKNOWN: insufficient permission to check files:\n";
1287 print join( "\n", ( keys %unknownfiles ) ) . "\n\n";
1288
1289 $return_code = $RETURN_NOK;
1290 }
1291
1292 } else {
1293 print "no modified files found in $dirname_repo\n";
1294 }
1295
1296 return $return_code;
1297}
1298
1299sub blame(@)
1300{
1301 check_parameter( @_, 1 );
1302 check_env();
1303
1304 (
1305 my $basename,
1306 my $dirname_prod,
1307 my $dirname_repo,
1308 my $filename_prod,
1309 my $filename_repo
1310 ) = get_filenames( $_[0] );
1311
1312 my $retcode = run_interactive("$SVN blame $svnOptions $filename_repo");
1313}
1314
1315sub diff(@)
1316{
1317 check_parameter( @_, 1 );
1318 check_env();
1319
1320 (
1321 my $basename,
1322 my $dirname_prod,
1323 my $dirname_repo,
1324 my $filename_prod,
1325 my $filename_repo
1326 ) = get_filenames( $_[0] );
1327
1328 #print "$basename,$dirname_prod,$dirname_repo\n";
1329
1330 svn_update();
1331
1332 ( my $rc_diff, my @diff_result ) =
1333 run_command( $diff . " $filename_repo $filename_prod" );
1334
1335 print @diff_result;
1336}
1337
1338sub status(@)
1339{
1340 check_parameter( @_, 1 );
1341 check_env();
1342
1343 (
1344 my $basename,
1345 my $dirname_prod,
1346 my $dirname_repo,
1347 my $filename_prod,
1348 my $filename_repo
1349 ) = get_filenames( $_[0] || "/" );
1350
1351 # return code for the shell
1352 # default: error
1353 my $return_code = $RETURN_NOK;
1354
1355 #
1356 # update local repository
1357 #
1358 #svn_update( $filename_prod );
1359
1360 # check, if permissions have changed
1361 permissions();
1362
1363 # get modified files
1364 ( my $refChangedFiles, my $refRemovedFiles, my $refUnknownFiles ) =
1365 getModifiedFiles($dirname_prod);
1366 my %changedfiles = %{$refChangedFiles};
1367 my %removedfiles = %{$refRemovedFiles};
1368 my %unknownfiles = %{$refUnknownFiles};
1369
1370 if ( %removedfiles or %changedfiles or %unknownfiles ) {
1371
1372 if (%removedfiles) {
1373 print "DELETED: files found in repository, but not in system:\n";
1374 print join( "\n", sort ( keys %removedfiles ) ) . "\n\n";
1375 }
1376
1377 if (%changedfiles) {
1378 print "MODIFIED: files differs between repository and system:\n";
1379 print join( "\n", ( keys %changedfiles ) ) . "\n\n";
1380 }
1381
1382 if (%unknownfiles) {
1383 print "UNKNOWN: insufficient permission to check files:\n";
1384 print join( "\n", ( keys %unknownfiles ) ) . "\n\n";
1385 }
1386
1387 } else {
1388 print "no modified files found in $dirname_repo\n";
1389 $return_code = $RETURN_OK;
1390 }
1391
1392 return $return_code;
1393}
1394
1395#
1396# return short status in Nagios plugin conform way
1397#
1398sub check()
1399{
1400 check_env();
1401
1402 # return code for the shell
1403 my $return_code = $RETURN_OK;
1404 my $return_string = "OK: no modified files";
1405
1406 # check, if permissions have changed
1407 permissions();
1408
1409 # get modified files
1410 ( my $refChangedFiles, my $refRemovedFiles, my $refUnknownFiles ) =
1411 getModifiedFiles("/");
1412 my %changedfiles = %{$refChangedFiles};
1413 my %removedfiles = %{$refRemovedFiles};
1414 my %unknownfiles = %{$refUnknownFiles};
1415
1416 if ( %removedfiles or %changedfiles ) {
1417 $return_string = "Warning: ";
1418 if (%changedfiles) {
1419 $return_string .=
1420 "changed: " . join( ", ", ( keys %changedfiles ) ) . ". ";
1421 }
1422 if (%removedfiles) {
1423 $return_string .=
1424 "removed: " . join( ", ", ( keys %removedfiles ) ) . ". ";
1425 }
1426 if (%unknownfiles) {
1427 $return_string .=
1428 "unknown: " . join( ", ", ( keys %unknownfiles ) ) . ". ";
1429 }
1430 $return_code = $RETURN_WARN;
1431 }
1432
1433 # addition nagios Service Status
1434 #Critical
1435 #Unknown
1436
1437 print "$return_string\n";
1438 return $return_code;
1439}
1440
1441sub permissions()
1442{
1443 check_env();
1444
1445 my $return_code = $RETURN_OK;
1446
1447 #
1448 # update local repository
1449 #
1450 #svn_update();
1451
1452 my $dir = $DASSCM_REPO;
1453 my @files = svn_ls("/");
1454
1455 if (@files) {
1456
1457 # generieren der Permissions
1458 my @permissions = generatePermissionList(@files);
1459 my $OUTFILE;
1460 my $tofile = 0; # Status für schreiben in File
1461
1462 if ( -w dirname($DASSCM_PERMISSION_FILE) ) {
1463
1464 # Verzeichnis existiert => schreiben
1465 open( OUTFILE, ">$DASSCM_PERMISSION_FILE" )
1466 || die("failed to write to $DASSCM_PERMISSION_FILE: $!");
1467 $tofile = 1; # Merken, daß in File geschrieben wird
1468 print OUTFILE "#\n";
1469 print OUTFILE "# created by dasscm permissions\n";
1470 print OUTFILE
1471 "# It is intended to be used for restoring permissions\n";
1472 print OUTFILE "#\n";
1473 } else {
1474
1475 if ( $command eq "permission" ) {
1476
1477 # Pfad für Sicherungsdatei existiert nicht => schreiben auf stdout
1478 # Alias Filehandle für stdout erzeugen
1479 $return_code = $RETURN_WARN;
1480 *OUTFILE = *STDOUT;
1481 } else {
1482
1483 # TODO: improve this. Check for diff?
1484 $return_code = $RETURN_CRIT;
1485 return $return_code;
1486 }
1487 }
1488
1489 foreach my $line (@permissions) {
1490 print OUTFILE "$line\n";
1491 }
1492
1493 if ($tofile) {
1494 close(OUTFILE);
1495 }
1496 }
1497
1498 return $return_code;
1499}
1500
1501#
1502# remove all uncommited changes in the repository
1503#
1504sub cleanup()
1505{
1506 check_env();
1507
1508 svn_revert($DASSCM_REPO);
1509 svn_remove_unknown_files($DASSCM_REPO);
1510}
1511
1512#
1513# used for bash completion
1514# prints the next possible command line parameters
1515#
1516sub complete(@)
1517{
1518 use Data::Dumper;
1519
1520 my $number_arguments = @_;
1521 my $input_command = $_[0] || "";
1522
1523 print "args: $number_arguments\n";
1524 if ( $number_arguments <= 1 ) {
1525
1526 foreach my $i ( keys %COMMANDS ) {
1527 #print "$i: ";
1528 #print get_command_requires_write( $i ), " ";
1529 $_ = $i;
1530 if( m/^$input_command/ ) {
1531 my $command = get_command_uniform_name($i);
1532 print $command, " ";
1533 print join( ",", get_command_possible_params($i) ) ;
1534 #print " (", get_command_desc( $i ), ") ";
1535 print "\n";
1536 }
1537 #print "\n";
1538 }
1539 } else {
1540 my $command = get_command_uniform_name($input_command);
1541 my @params = get_command_possible_params($input_command);
1542 print "params: ", Dumper(@params);
1543 if( defined($params[$number_arguments-2]) && $params[$number_arguments-2] ) {
1544 my $param = $params[$number_arguments-2];
1545 print "param: $param\n";
1546 print Dumper($param);
1547 }
1548 }
1549}
1550
1551
1552
1553#####################################################################
1554#
1555# main
1556#
1557
1558my $return_code = $RETURN_OK;
1559my $number_arguments = @ARGV;
1560
1561if ( $number_arguments > 0 ) {
1562
1563 # get subcommand and remove it from @ARGV
1564 $command = get_command_uniform_name($ARGV[0]);
1565 shift @ARGV;
1566
1567 $DASSCM_LOCAL_REPOSITORY_BASE = $config->{'DASSCM_LOCAL_REPOSITORY_BASE'};
1568 $DASSCM_REPOSITORY_NAME = $config->{'DASSCM_REPOSITORY_NAME'};
1569
1570 # TODO: check variables
1571 $DASSCM_SVN_REPOSITORY =
1572 $config->{'DASSCM_SVN_REPOSITORY_BASE'} . "/" . $DASSCM_REPOSITORY_NAME;
1573
1574 $DASSCM_CHECKOUT_USERNAME = $config->{'DASSCM_CHECKOUT_USERNAME'};
1575 $DASSCM_CHECKOUT_PASSWORD = $config->{'DASSCM_CHECKOUT_PASSWORD'};
1576
1577 #
1578 # if a user is given by dasscm configuration file, we use it.
1579 # Otherwise we expect that read-only account is configured
1580 # as local subversion configuration.
1581 # If this is also not the case,
1582 # user is required to type username and password.
1583 # This will be stored as local subversion configuration thereafter.
1584 #
1585 if ( $DASSCM_CHECKOUT_USERNAME && $DASSCM_CHECKOUT_PASSWORD ) {
1586 $svnCheckoutCredentials =
1587 " --username $DASSCM_CHECKOUT_USERNAME --password $DASSCM_CHECKOUT_PASSWORD ";
1588 }
1589
1590 $DASSCM_PERMISSION_FILE = $config->{'DASSCM_PERMISSION_FILE'}
1591 || "/etc/permissions.d/dasscm.permission_backup";
1592
1593 # get command line options and store them in options hash
1594 my $result = GetOptions( \%options, 'verbose', 'message=s' );
1595
1596 # print options
1597 foreach my $option ( keys %options ) {
1598 print "${option}: $options{$option}\n";
1599 }
1600
1601 # set verbose to command line option
1602 $verbose = $options{'verbose'};
1603
1604 #
1605 # action accordinly to command are taken
1606 # $command is rewritten in standard format,
1607 # so we can test for it later on more simply
1608 #
1609
1610 # $_ = $command;
1611 # if (m/^help$/i) {
1612 # help(@ARGV);
1613 # } elsif (m/^login$/i) {
1614 # $command = "login";
1615 # login(@ARGV);
1616 # } elsif (m/^init$/i) {
1617 # $command = "init";
1618 # init(@ARGV);
1619 # } elsif (m/^ls$/i) {
1620 # $command = "ls";
1621 # ls(@ARGV);
1622 # } elsif ( (m/^update$/i) || (m/^up$/i) ) {
1623 # $command = "update";
1624 # update(@ARGV);
1625 # } elsif (m/^add$/i) {
1626 # $command = "add";
1627 # add(@ARGV);
1628 # } elsif ( (m/^commit$/i) || (m/^checkin$/i) || (m/^ci$/i) ) {
1629 # $command = "commit";
1630 # commit(@ARGV);
1631 # } elsif (m/^revert$/i) {
1632 # $command = "revert";
1633 # $return_code = revert(@ARGV);
1634 # } elsif (m/^blame$/i) {
1635 # $command = "blame";
1636 # blame(@ARGV);
1637 # } elsif (m/^diff$/i) {
1638 # $command = "diff";
1639 # diff(@ARGV);
1640 # } elsif ( (m/^status$/i) || (m/^st$/i) ) {
1641 # $command = "status";
1642 # $return_code = status(@ARGV);
1643 # } elsif (m/^check$/i) {
1644 # $command = "check";
1645 # $return_code = check();
1646 # } elsif (m/^permissions$/i) {
1647 # $command = "permissions";
1648 # $return_code = permissions();
1649 # } elsif (m/^cleanup$/i) {
1650 # $command = "cleanup";
1651 # cleanup();
1652 # } elsif (m/^complete$/i) {
1653 # $command = "complete";
1654 # $return_code = complete(@ARGV);
1655
1656 if( get_command_function( $command ) ) {
1657 &{get_command_function( $command )}( @ARGV );
1658 } else {
1659 print "unknown command: $command\n\n";
1660 usage();
1661 check_env();
1662 $return_code = $RETURN_NOK;
1663 }
1664}
1665
1666exit $return_code;
Note: See TracBrowser for help on using the repository browser.