Check that deployed EG code matches the code in git.
[sitka/sitka-tools.git] / deployment / integrity-checker.pl
1 #!/usr/bin/perl
2 # On Ubuntu, you'll want the following packages:
3 # - libconfig-simple-perl
4 # - libgit-repository-perl
5 # - libdate-manip-perl
6 use Config::Simple;
7 use File::Find;
8 use File::stat;
9 use Date::Manip qw/ParseDate UnixDate/;
10 use Time::localtime;
11 use Git::Repository;
12 use Git::Repository::Command;
13 use Getopt::Long;
14 use Data::Dumper;
15
16 my ($help, $config_file, $all, $print_hashes, $repo_path, $check_files);
17 my $branch = 'HEAD';
18 my $remote = 'origin';
19 my @components;
20
21 GetOptions(
22     'help'              => \$help,            # show help message and exit
23     'config=s'          => \$config_file,     # INI file for path mappings
24     'repo=s'            => \$repo_path,       # location of git repo
25     'branch=s'          => \$branch,          # git branch/head to check against (defaults to 'HEAD')
26     'remote=s'          => \$remote,          # git remote to pull from (defaults to 'origin')
27     'component=s'       => \@components,      # parts of EG to be checked (each component is a block in the INI file)
28     'all'               => \$all,             # check all components specified in config (overrides --component)
29     'check-files'       => \$check_files,     # check deployed files
30     'hash-file=s'       => \$hash_file,       # file containing git hashes (overrides --repo)
31     'since=s'           => \$since,           # check only files modified since this time
32     'git-output=s'      => \$git_output,      # output file for git hashes (optional)
33     'deployed-output=s' => \$deployed_output  # output file for hashes of deployed files (optional)
34 );
35
36 if ($help) {
37     print <<"HELP";
38 USAGE:
39     $0 --config pathmap.ini --repo /path/to/evergreen.git [ --component perl [ --component tt2 ] | --all ] --check-files [ --since <date> ]
40     $0 --config pathmap.ini --repo /path/to/evergreen.git [ --component perl [ --component tt2 ... ] | --all ] --print-git-hashes <git-hashes.txt>
41     $0 --config pathmap.ini --repo /path/to/evergreen.git [ --component perl [ --component tt2 ... ] | --all ] --print-deployed-hashes <deployed-hashes.txt>
42
43 OPTIONS:
44     --help
45         Show help message and exit.
46     --config
47         Location of INI file for path mappings.
48     --repo
49         Location of git repo (overridden by --hash-file).
50     --branch
51         Git branch (head) to check against (defaults to HEAD).
52     --remote
53         Git remote to pull from (defaults to origin).
54     --component
55         Parts of EG to be checked (each component is a block in the config file).
56         You can use this option multiple times: --component perl --component web
57     --all
58         Check all components specified in config file (overrides --component).
59     --check-files
60         Get file hashes from git, then check deployed files to see if they match.
61     --hash-file
62         File containing file hashes from git.  If you use this option, you don't
63         need to specify a git repo using the --repo option.
64         Use case: pull hashes from git once, copy the resulting file to multiple
65         servers, then check the deployed code against the file instead of pulling
66         hashes from git individually on each server.
67     --since
68         Only calculate hashes if file has been modified since the specified time.
69     --print-git-hashes
70         Get file hashes from git repo and print/append to the specified file.
71         Can be used with --check-files and --print-deployed-hashes.
72     --print-deployed-hashes
73         Print git-like hashes for deployed files.
74         Can be used with --check-files and --print-git-hashes.
75
76 HELP
77     exit;
78 }
79
80 # specify all possible components (--all option);
81 # it would be better to pull all block labels from the config file,
82 # but that's not possible with Config::Simple
83 @components = qw/perl tt2 web xul misc/ if ($all);
84
85 # load config
86 die "No config file specified\n" unless ($config_file);
87 die "Config file does not exist\n" unless (-r $config_file and -s $config_file);
88 my $cfg = new Config::Simple($config_file);
89
90 if ($git_output) {
91     open (GITOUTPUT, '>>', $git_output) or die "Could not open $git_output: $!\n";
92 }
93 if ($deployed_output) {
94     open (DEPLOYEDOUTPUT, '>>', $deployed_output) or die "Could not open $deployed_output: $!\n";
95 }
96
97 my %git_hashes;
98
99 # optionally read in git hashes from file
100 if ($hash_file) {
101     open (HASHFILE, '<', $hash_file) or die "Could not open $hash_file: $!\n";
102     while (<HASHFILE>) {
103         my ($hash, $file) = split(/\s+/, $_, 2);
104         $git_hashes{$file} = $hash;
105     }
106     close HASHFILE;
107 }
108
109 foreach my $component (@components) {
110     my $paths = $cfg->get_block($component);
111
112     # if no hash file was supplied, grab git hashes from repo
113     if (!$hash_file) {
114
115         # load git repo
116         die "No repo specified\n" unless ($repo_path);
117         $repo_path =~ s|/$||;
118         $repo_path = "$repo_path/.git" unless ($repo_path =~ /\.git$/);
119         my $repo = Git::Repository->new( git_dir => $repo_path ) or die "Could not load git repo $repo_path: $!\n";
120
121         # ensure git repo is up-to-date
122         if ($branch ne 'HEAD') {
123             $repo->run( 'pull' => $remote );
124             $repo->run( 'checkout' => $branch ); # TODO: is this necessary?
125         }
126
127         # get hashes from git
128         foreach my $srcpath (keys %$paths) {
129             # use git-ls-tree to traverse the file tree starting at $srcpath
130             # e.g. `git ls-tree -r HEAD Open-ILS/src/perlmods/lib`
131             my @tree = $repo->run( 'ls-tree' => '-r', $branch, $srcpath );
132             foreach my $file (@tree) {
133                 my ($mode, $type, $hash, $filename) = split(/\s+/, $file, 4);
134                 $git_hashes{$filename} = $hash;
135                 print GITOUTPUT $hash, "\t", $filename, "\n" if ($git_output);
136             }
137         }
138     }
139
140     # check deployed files
141     if ($check_files || $deployed_output) {
142         foreach my $srcpath (keys %$paths) {
143             my $destpath = $paths->{$srcpath};
144             my @files;
145
146             # for each file in the destination path, push the file's absolute path to @files;
147             # output will include symlinked files, but will not include directories
148             find( { wanted => sub { push @files, $_ if -f }, follow => 1, no_chdir => 1 }, $destpath );
149
150             foreach my $file (@files) {
151
152                 if ($since) {
153                     # convert $since to seconds since epoch
154                     my $since_ts = UnixDate($since, '%s');
155                     # get $file modification time as seconds since epoch
156                     my $file_ts = ctime(stat($file)->mtime);
157
158                     next unless $file_ts > $since_ts;
159                 }
160
161                 # you can calculate what the git hash would be
162                 # for any file using `git hash-object <file>`;
163                 # you don't even need to be in a git repo to run it!
164                 my $hash = Git::Repository->run( 'hash-object', $file );
165
166                 if ($check_files) {
167                     my $srcfile = $file;
168                     $srcfile =~ s|^$destpath|$srcpath|;
169
170                     if (!$git_hashes{$srcfile}) {
171                         print "untracked\t$file\n";
172                     } elsif ($git_hashes{$srcfile} ne $hash) {
173                         print "modified\t$file\n";
174                     }
175                 }
176                 print DEPLOYEDOUTPUT $hash, "\t", $filename, "\n" if ($deployed_output);
177
178             }
179         }
180     }
181 }
182
183 close (GITOUTPUT) if ($git_output);
184 close (DEPLOYEDOUTPUT) if ($deployed_output);
185