26 |
27 |
27 sub ok; |
28 sub ok; |
28 sub warning; |
29 sub warning; |
29 sub critical; |
30 sub critical; |
30 sub unknown; |
31 sub unknown; |
|
32 sub verbose; |
|
33 sub unique { my %h; @h{@_} = (); keys %h } |
31 |
34 |
32 $SIG{__DIE__} = sub { unknown @_ unless $^S }; |
35 $SIG{__DIE__} = sub { unknown @_ unless $^S }; |
33 |
36 |
34 exit main @ARGV; |
37 exit main @ARGV; |
35 |
38 |
36 #---- |
39 #---- |
37 |
40 |
38 sub main { |
41 sub main { |
39 my @opt_ignore; |
42 my @opt_ignore; |
40 |
43 my $opt_verbose = 0; |
41 GetOptions( |
44 |
42 'i|ignore=s@' => \@opt_ignore, |
45 GetOptions( |
43 'h|help' => sub { pod2usage(-verbose => 1, -exit => 0) }, |
46 'i|ignore=s@' => \@opt_ignore, |
44 'm|man' => sub { pod2usage(-verbose => 2, -exit => 0) }, |
47 'h|help' => sub { pod2usage(-verbose => 1, -exit => 0) }, |
45 ) or pod2usage; |
48 'm|man' => sub { pod2usage(-verbose => 2, -exit => 0) }, |
46 |
49 'v|verbose' => \$opt_verbose, |
47 # test needs to be run as root:* or as backup:backup |
50 ) or pod2usage; |
48 my $USER = 'backup'; |
51 |
49 my $CFDIR = '/etc/amanda'; |
52 if ($opt_verbose) { |
50 |
53 *::verbose = sub { say '# ', @_ } |
51 # change to backup if still root |
54 } |
52 su $USER if $> == 0; |
55 else { |
53 |
56 *::verbose = sub { } |
54 # amservice needs to be suid root, but executable |
57 } |
55 # by the backup user/group |
58 |
56 eval { check_perms find_tool('amservice'), 04750, 'root', $) } |
59 # test needs to be run as root:* or as backup:backup |
57 or unknown $@; |
60 my $USER = 'backup'; |
58 |
61 my $CFDIR = '/etc/amanda'; |
59 # find the backup sets we know about |
62 |
60 # here we suppose that it's possible to find strings like |
63 # change to backup if still root |
61 # 'conf "foo"' in files named 'amanda-client.conf' below /etc/amanda |
64 su $USER if $> == 0; |
62 |
65 |
63 my @confs = eval { config_names $CFDIR } |
66 # amservice needs to be suid root, but executable |
64 or unknown $@; |
67 # by the backup user/group |
65 |
68 verbose q{checking permissions for `amservice'}; |
66 eval { amchecks @confs } or critical $@; |
69 eval { check_perms find_tool('amservice'), 04750, 'root', $) } |
67 |
70 or unknown $@; |
68 my @dles = eval { amlists @confs } or critical $@; |
71 |
69 ok @dles; |
72 # find the backup sets we know about |
70 |
73 # here we suppose that it's possible to find strings like |
71 # never reached |
74 # 'conf "foo"' in files named 'amanda-client.conf' below /etc/amanda |
72 return 0; |
75 |
|
76 verbose qq{find config names from $CFDIR}; |
|
77 my @confs = sort +unique eval { config_names $CFDIR } |
|
78 or unknown $@; |
|
79 |
|
80 eval { amchecks @confs } or critical $@; |
|
81 |
|
82 my @dles = eval { amlists @confs } or critical $@; |
|
83 ok 'config: ' . join(', ', @confs), @dles; |
|
84 |
|
85 # never reached |
|
86 return 0; |
73 } |
87 } |
74 |
88 |
75 # compare the file systems |
89 # compare the file systems |
76 # get a list of file system |
90 # get a list of file system |
77 sub get_devices { |
91 sub get_devices { |
78 open(my $fh, '/proc/filesystems'); |
92 open(my $fh, '/proc/filesystems'); |
79 my @types = map { /^\s+(\S+)/ ? $1 : () } <$fh>; |
93 my @types = map { /^\s+(\S+)/ ? $1 : () } <$fh>; |
80 my @df = (df => '-P', map { -t => $_ } @types); |
94 my @df = (df => '-P', map { -t => $_ } @types); |
81 map { [$_, (stat)[0]] } map { (split ' ', $_)[5] } grep { /^\// } `@df`; |
95 map { [$_, (stat)[0]] } map { (split ' ', $_)[5] } grep { /^\// } `@df`; |
82 } |
96 } |
83 |
97 |
84 sub su { |
98 sub su { |
85 my $user = shift; |
99 my $user = shift; |
86 my $group = (getgrnam $user)[0]; |
100 my $group = (getgrnam $user)[0]; |
87 my $uid = getpwnam $user; |
101 my $uid = getpwnam $user; |
88 my $gid = getgrnam $group; |
102 my $gid = getgrnam $group; |
89 |
103 |
90 my @groups; |
104 my @groups; |
91 |
105 |
92 setgrent; |
106 setgrent; |
93 my @rc; |
107 my @rc; |
94 while (my @g = getgrent) { |
108 while (my @g = getgrent) { |
95 push @groups, $g[2] if $user ~~ [split ' ', $g[3]]; |
109 push @groups, $g[2] if $user ~~ [split ' ', $g[3]]; |
96 } |
110 } |
97 endgrent; |
111 endgrent; |
98 $) = "@groups"; |
112 $) = "@groups"; |
99 setgid $gid; |
113 |
100 setuid $uid; |
114 verbose "su to $uid:$gid"; |
|
115 setgid $gid; |
|
116 setuid $uid; |
101 } |
117 } |
102 |
118 |
103 sub find_tool { |
119 sub find_tool { |
104 my $name = shift; |
120 my $name = shift; |
105 my @rc = grep { -f -x } map { catfile $_, $name } split /:/, $ENV{PATH} |
121 my @rc = grep { -f -x } map { catfile $_, $name } split /:/, $ENV{PATH} |
106 or die "Can't find `$name' in $ENV{PATH}\n"; |
122 or die "Can't find `$name' in $ENV{PATH}\n"; |
107 $rc[0]; |
123 $rc[0]; |
108 }; |
124 } |
109 |
125 |
110 sub check_perms { |
126 sub check_perms { |
111 my ($file, $mode, $owner, $group) = @_; |
127 my ($file, $mode, $owner, $group) = @_; |
112 |
128 |
113 $owner = getpwuid $owner if $owner ~~ /^\d+$/; |
129 $owner = getpwuid $owner if $owner ~~ /^\d+$/; |
114 |
130 |
115 $group = getgrgid +(split ' ', $group)[0] |
131 $group = getgrgid +(split ' ', $group)[0] |
116 if $group ~~ /^[\d\s]+$/; |
132 if $group ~~ /^[\d\s]+$/; |
117 |
133 |
118 stat $file or croak "Can't stat `$file': $!\n"; |
134 stat $file or croak "Can't stat `$file': $!\n"; |
119 |
135 |
120 eval { |
136 eval { |
121 my $f_owner = getpwuid +(stat _)[4] or die $!; |
137 my $f_owner = getpwuid +(stat _)[4] or die $!; |
122 my $f_group = getgrgid +(stat _)[5] or die $!; |
138 my $f_group = getgrgid +(stat _)[5] or die $!; |
123 my $f_mode = (stat _)[2] & 07777 or die $!; |
139 my $f_mode = (stat _)[2] & 07777 or die $!; |
124 |
140 |
125 my $msg = sprintf "need: 0%04o root:$group, got: 0%04o $f_owner:$f_group\n", |
141 my $msg = |
126 $mode, $f_mode; |
142 sprintf "need: 0%04o root:$group, got: 0%04o $f_owner:$f_group\n", |
127 |
143 $mode, $f_mode; |
128 die $msg unless $f_owner eq $owner; |
144 |
129 die $msg unless $f_group eq $group; |
145 die $msg unless $f_owner eq $owner; |
130 die $msg unless $f_mode == $mode; |
146 die $msg unless $f_group eq $group; |
131 }; |
147 die $msg unless $f_mode == $mode; |
132 die "wrong permissions for `$file', $@" if $@; |
148 }; |
133 1; |
149 die "wrong permissions for `$file', $@" if $@; |
134 } |
150 1; |
135 |
151 } |
136 |
152 |
137 sub config_names { |
153 sub config_names { |
138 my $dir = shift; |
154 my $dir = shift; |
139 my @configs = (); |
155 my @configs = (); |
140 find(sub { |
156 find( |
141 -f and /^amanda-client\.conf$/ or return; |
157 sub { |
142 open(my $fh, '<', $_) or die "Can't open $File::Find::name: $!\n"; |
158 -f and /^amanda-client\.conf$/ or return; |
143 push @configs, map { /^conf\s+"(.+?)"/ ? $1 : () } <$fh>; |
159 open(my $fh, '<', $_) or die "Can't open $File::Find::name: $!\n"; |
144 }, $dir); |
160 push @configs, map { /^conf\s+"(.+?)"/ ? $1 : () } <$fh>; |
145 |
161 }, |
146 die "no configs found below $dir (amanda-client.conf needs need `conf \"foo\"' line)\n" |
162 $dir |
147 if not @configs; |
163 ); |
148 return @configs; |
164 |
149 }; |
165 die |
|
166 "no configs found below $dir (amanda-client.conf needs need `conf \"foo\"' line)\n" |
|
167 if not @configs; |
|
168 return @configs; |
|
169 } |
150 |
170 |
151 sub _amcheck { |
171 sub _amcheck { |
152 #config: daily |
172 |
153 #CHECKING |
173 #config: daily |
154 # |
174 #CHECKING |
155 #Amanda Backup Client Hosts Check |
175 # |
156 #-------------------------------- |
176 #Amanda Backup Client Hosts Check |
157 #Client check: 1 host checked in 2.242 seconds. 0 problems found. |
177 #-------------------------------- |
158 # |
178 #Client check: 1 host checked in 2.242 seconds. 0 problems found. |
159 #(brought to you by Amanda 3.3.1) |
179 # |
160 #The check is finished |
180 #(brought to you by Amanda 3.3.1) |
161 |
181 #The check is finished |
162 my $conf = shift; |
182 |
163 my $_ = qx(amdump_client --config '$conf' check 2>&1); |
183 my $conf = shift; |
164 /^config:\s+$conf\n |
184 my $_ = qx(amdump_client --config '$conf' check 2>&1); |
|
185 /^config:\s+$conf\n |
165 CHECKING\n |
186 CHECKING\n |
166 .*\n |
187 .*\n |
167 Client.check:.1.host.checked.in.\d+\.\d+.seconds\.\s+0.problems.found\.\n |
188 Client.check:.1.host.checked.in.\d+\.\d+.seconds\.\s+0.problems.found\.\n |
168 .*\n |
189 .*\n |
169 The.check.is.finished$ |
190 The.check.is.finished$ |
170 /smx or die "unexpected output from check:\n$_"; |
191 /smx or die "unexpected output from check:\n$_"; |
171 } |
192 } |
172 |
193 |
173 sub amchecks { |
194 sub amchecks { |
174 my @errors = (); |
195 my @errors = (); |
175 foreach my $conf (@_) { |
196 foreach my $conf (@_) { |
176 eval { _amcheck $conf } or push @errors, $@; |
197 eval { _amcheck $conf } or push @errors, $@; |
177 } |
198 } |
178 die @errors if @errors; |
199 die @errors if @errors; |
179 return 1; |
200 return 1; |
180 } |
201 } |
181 |
202 |
182 sub _amlist { |
203 sub _amlist { |
183 # return a list of [ name, dev ] tupels. |
204 |
184 # name: the name of the disk/device |
205 # return a list of [ name, dev ] tupels. |
185 # dev: the local device id (stat)[0] |
206 # name: the name of the disk/device |
186 # iff the inum of $name != 2, it's not the top directory |
207 # dev: the local device id (stat)[0] |
187 # and we set the device id to -1, since $name does not stand for a whole |
208 # iff the inum of $name != 2, it's not the top directory |
188 # device |
209 # and we set the device id to -1, since $name does not stand for a whole |
189 my $conf = shift; |
210 # device |
190 chomp((undef, my @dles) = qx(amdump_client --config '$conf' list)); |
211 my $conf = shift; |
191 return map { [$_, (stat $_)[1] == 2 ? (stat $_)[0] : -1 ] } @dles; |
212 chomp((undef, my @dles) = qx(amdump_client --config '$conf' list)); |
|
213 return map { [$_, (stat $_)[1] == 2 ? (stat $_)[0] : -1] } @dles; |
192 } |
214 } |
193 |
215 |
194 sub amlists { |
216 sub amlists { |
195 my @confs = @_; |
217 my @confs = @_; |
196 my @candidates = get_devices; |
218 my @candidates = get_devices; |
197 |
219 |
198 my %missing; |
220 my %missing; |
199 |
221 |
200 foreach my $conf (@confs) { |
222 foreach my $conf (@confs) { |
201 my @dles = _amlist $conf; |
223 my @dles = _amlist $conf; |
202 foreach my $candidate (@candidates) { |
224 foreach my $candidate (@candidates) { |
203 # we're satisfied if either the name of the device is in |
225 |
204 # the disklist, or the device id is found |
226 # we're satisfied if either the name of the device is in |
205 $candidate->[0] ~~ [ map { $_->[0] } @dles ] and next; |
227 # the disklist, or the device id is found |
206 $candidate->[1] ~~ [ map { $_->[1] } @dles ] and next; |
228 $candidate->[0] ~~ [map { $_->[0] } @dles] and next; |
207 push @{$missing{$conf}}, $candidate->[0]; |
229 $candidate->[1] ~~ [map { $_->[1] } @dles] and next; |
208 } |
230 push @{ $missing{$conf} }, $candidate->[0]; |
209 } |
231 } |
210 die map { "$_ missing: " . join(', ' => @{$missing{$_}}) . "\n" } keys %missing |
232 } |
211 if %missing; |
233 die map { "$_ missing: " . join(', ' => @{ $missing{$_} }) . "\n" } |
212 return map { $_->[0] } @candidates; |
234 keys %missing |
|
235 if %missing; |
|
236 return map { $_->[0] } @candidates; |
213 } |
237 } |
214 |
238 |
215 sub ok { say "$NAME OK\n", join "\n" => @_; exit 0 } |
239 sub ok { say "$NAME OK\n", join "\n" => @_; exit 0 } |
216 sub warning { print "$NAME WARNING\n", join "\n" => @_; exit 1 } |
240 sub warning { print "$NAME WARNING\n", join "\n" => @_; exit 1 } |
217 sub critical { print "$NAME CRITICAL\n", join "\n" => @_; exit 2 } |
241 sub critical { print "$NAME CRITICAL\n", join "\n" => @_; exit 2 } |
218 sub unknown { print "$NAME UNKNOWN\n", join "\n" => @_; exit 3 } |
242 sub unknown { print "$NAME UNKNOWN\n", join "\n" => @_; exit 3 } |
219 |
243 |
220 __END__ |
244 __END__ |
221 |
245 |
222 =head1 NAME |
246 =head1 NAME |
223 |
247 |