author | Sergey Poznyakoff <gray@gnu.org> | 2019-05-25 06:43:38 (GMT) |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2019-05-25 16:58:35 (GMT) |
commit | 585a5a925342c357d46fbad273bfafc03bc33dcd (patch) (unidiff) | |
tree | 0aa81c8e85543ebaa25ef88292bb318ac4294e04 | |
parent | edfeb103b85e10aa9e6825d3b92c5888fb4dea94 (diff) | |
download | config-haproxy-585a5a925342c357d46fbad273bfafc03bc33dcd.tar.gz config-haproxy-585a5a925342c357d46fbad273bfafc03bc33dcd.tar.bz2 |
Check configuration file syntax before saving it
* Makefile.PL: Require IPC::Cmd
* lib/Config/HAProxy.pm (lint): New method.
(save): Call linter prior to saving. Take optional dry_run keyword
as argument.
* t/lint.t: New file.
-rw-r--r-- | MANIFEST.SKIP | 2 | ||||
-rw-r--r-- | Makefile.PL | 3 | ||||
-rw-r--r-- | lib/Config/HAProxy.pm | 125 | ||||
-rw-r--r-- | t/lint.t | 33 |
4 files changed, 153 insertions, 10 deletions
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index c1d2412..e7c7b44 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP | |||
@@ -60,4 +60,4 @@ | |||
60 | ^\.emacs\.* | 60 | ^\.emacs\.* |
61 | \.tar$ | 61 | \.tar$ |
62 | \.tar\.gz$ | 62 | \.tar\.gz$ |
63 | Config/HAProxy/VirtualHost.pm | 63 | |
diff --git a/Makefile.PL b/Makefile.PL index d9cc6e8..754d3aa 100644 --- a/Makefile.PL +++ b/Makefile.PL | |||
@@ -16,7 +16,8 @@ WriteMakefile( | |||
16 | 'Text::Locus' => 1.00, | 16 | 'Text::Locus' => 1.00, |
17 | 'Text::ParseWords' => 0, | 17 | 'Text::ParseWords' => 0, |
18 | 'File::Basename' => 0, | 18 | 'File::Basename' => 0, |
19 | 'File::Temp' => 0 | 19 | 'File::Temp' => 0, |
20 | 'IPC::Cmd' => 0 | ||
20 | }, | 21 | }, |
21 | META_MERGE => { | 22 | META_MERGE => { |
22 | 'meta-spec' => { version => 2 }, | 23 | 'meta-spec' => { version => 2 }, |
diff --git a/lib/Config/HAProxy.pm b/lib/Config/HAProxy.pm index 9a8d666..ff4fa8a 100644 --- a/lib/Config/HAProxy.pm +++ b/lib/Config/HAProxy.pm | |||
@@ -12,6 +12,8 @@ use Text::ParseWords; | |||
12 | use File::Basename; | 12 | use File::Basename; |
13 | use File::Temp qw(tempfile); | 13 | use File::Temp qw(tempfile); |
14 | use File::stat; | 14 | use File::stat; |
15 | use File::Spec; | ||
16 | use IPC::Cmd; | ||
15 | use Carp; | 17 | use Carp; |
16 | 18 | ||
17 | our $VERSION = '1.01'; | 19 | our $VERSION = '1.01'; |
@@ -26,7 +28,8 @@ my %sections = ( | |||
26 | sub new { | 28 | sub new { |
27 | my $class = shift; | 29 | my $class = shift; |
28 | my $filename = shift // '/etc/haproxy/haproxy.cfg'; | 30 | my $filename = shift // '/etc/haproxy/haproxy.cfg'; |
29 | my $self = bless { _filename => $filename }, $class; | 31 | my $self = bless { _filename => $filename, |
32 | _lint => { enable => 1 } }, $class; | ||
30 | $self->reset(); | 33 | $self->reset(); |
31 | return $self; | 34 | return $self; |
32 | } | 35 | } |
@@ -169,15 +172,74 @@ sub write { | |||
169 | close $fh unless ref($file) eq 'GLOB'; | 172 | close $fh unless ref($file) eq 'GLOB'; |
170 | } | 173 | } |
171 | 174 | ||
172 | sub save { | 175 | sub lint { |
173 | my $self = shift; | 176 | my $self = shift; |
174 | 177 | ||
178 | if (@_) { | ||
179 | if (@_ == 1) { | ||
180 | $self->{_lint}{enable} = !!shift; | ||
181 | } elsif (@_ % 2 == 0) { | ||
182 | local %_ = @_; | ||
183 | my $v; | ||
184 | if (defined($v = delete $_{enable})) { | ||
185 | $self->{_lint}{enable} = $v; | ||
186 | } | ||
187 | if (defined($v = delete $_{command})) { | ||
188 | $self->{_lint}{command} = $v; | ||
189 | } | ||
190 | if (defined($v = delete $_{path})) { | ||
191 | $self->{_lint}{path} = $v; | ||
192 | } | ||
193 | croak "unrecognized keywords" if keys %_; | ||
194 | } else { | ||
195 | croak "bad number of arguments"; | ||
196 | } | ||
197 | } | ||
198 | |||
199 | if ($self->{_lint}{enable}) { | ||
200 | $self->{_lint}{command} ||= 'haproxy -c -f'; | ||
201 | if ($self->{_lint}{path}) { | ||
202 | my ($prog, $args) = split /\s+/, $self->{_lint}{command}, 2; | ||
203 | if (!File::Spec->file_name_is_absolute($prog)) { | ||
204 | foreach my $dir (split /:/, $self->{_lint}{path}) { | ||
205 | my $name = File::Spec->catfile($dir, $prog); | ||
206 | if (-x $name) { | ||
207 | $prog = $name; | ||
208 | last; | ||
209 | } | ||
210 | } | ||
211 | if ($args) { | ||
212 | $prog .= ' '.$args; | ||
213 | } | ||
214 | $self->{_lint}{command} = $prog; | ||
215 | } | ||
216 | } | ||
217 | return $self->{_lint}{command}; | ||
218 | } | ||
219 | } | ||
220 | |||
221 | sub save { | ||
222 | my $self = shift; | ||
223 | croak "bad number of arguments" if @_ % 2; | ||
224 | local %_ = @_; | ||
225 | my $dry_run = delete $_{dry_run}; | ||
226 | my @wrargs = %_; | ||
227 | |||
175 | return unless $self->tree;# FIXME | 228 | return unless $self->tree;# FIXME |
176 | return unless $self->tree->is_dirty; | 229 | return unless $self->tree->is_dirty; |
177 | my ($fh, $tempfile) = tempfile('haproxy.XXXXXX', | 230 | my ($fh, $tempfile) = tempfile('haproxy.XXXXXX', |
178 | DIR => dirname($self->filename)); | 231 | DIR => dirname($self->filename)); |
179 | $self->write($fh, @_); | 232 | $self->write($fh, @wrargs); |
180 | close($fh); | 233 | close($fh); |
234 | |||
235 | if (my $cmd = $self->lint) { | ||
236 | my ($ok, $err, undef, undef, $errbuf) = | ||
237 | IPC::Cmd->run(command => "$cmd $tempfile"); | ||
238 | unless ($ok) { | ||
239 | croak "Syntax check failed: $errbuf\n"; | ||
240 | } | ||
241 | } | ||
242 | return 1 if $dry_run; | ||
181 | 243 | ||
182 | my $sb = stat($self->filename); | 244 | my $sb = stat($self->filename); |
183 | $self->backup; | 245 | $self->backup; |
@@ -188,7 +250,8 @@ sub save { | |||
188 | # This will fail unless we are root, let it be so. | 250 | # This will fail unless we are root, let it be so. |
189 | chown $sb->uid, $sb->gid, $self->filename; | 251 | chown $sb->uid, $sb->gid, $self->filename; |
190 | 252 | ||
191 | $self->tree->clear_dirty | 253 | $self->tree->clear_dirty; |
254 | return 1; | ||
192 | } | 255 | } |
193 | 256 | ||
194 | sub backup_name { | 257 | sub backup_name { |
@@ -233,9 +296,12 @@ Config::HAProxy - Parser for HAProxy configuration file | |||
233 | # do something with $node | 296 | # do something with $node |
234 | } | 297 | } |
235 | 298 | ||
236 | $cfg->save; | 299 | $cfg->lint(enable => 1, command => 'haproxy -c -f', |
300 | path => '/sbin:/usr/sbin') | ||
301 | |||
302 | $cfg->save(%hash); | ||
237 | 303 | ||
238 | $cfg->write($file_or_handle); | 304 | $cfg->write($file_or_handle, %hash); |
239 | 305 | ||
240 | $cfg->backup; | 306 | $cfg->backup; |
241 | $name = $self->backup_name; | 307 | $name = $self->backup_name; |
@@ -361,11 +427,54 @@ Returns the last node in the tree. | |||
361 | 427 | ||
362 | =head1 SAVING | 428 | =head1 SAVING |
363 | 429 | ||
430 | =head2 lint | ||
431 | |||
432 | $cfg->lint(%ARGS); | ||
433 | |||
434 | Configures syntax checking program to be run before saving. Takes a | ||
435 | hash as argument. Allowed keys are: | ||
436 | |||
437 | =over 4 | ||
438 | |||
439 | =item B<enable =E<gt> I<BOOL>> | ||
440 | |||
441 | If I<BOOL> is 0, disables syntax check. Default is 1. | ||
442 | |||
443 | =item B<command =E<gt> I<CMD>> | ||
444 | |||
445 | Configures the command to use for syntax check. The command will be run as | ||
446 | |||
447 | CMD FILE | ||
448 | |||
449 | where I<FILE> is the name of the HAProxy configuration file to check. | ||
450 | |||
451 | Default command is B<haproxy -c -f>. | ||
452 | |||
453 | =item B<path =E<gt> I<PATH>> | ||
454 | |||
455 | Sets the search path for the check program. I<PATH> is a colon-delimited | ||
456 | list of directories. Unless the first word of B<command> is an absolute | ||
457 | file name, it will be looked for in these directories. The first match | ||
458 | will be used. Default is system B<$PATH>. | ||
459 | |||
460 | =back | ||
461 | |||
462 | Returns the command name. | ||
463 | |||
364 | =head2 save | 464 | =head2 save |
365 | 465 | ||
366 | $cfg->save; | 466 | $cfg->save(%hash); |
467 | |||
468 | Saves the parse tree in the configuration file. Syntax check will be run | ||
469 | prior to saving (unless previously disabled). If syntax errors are discovered, | ||
470 | the method will B<croak> with the appropriate diagnostics, beginning with | ||
471 | words C<Syntax check failed:>. | ||
472 | |||
473 | If I<%hash> contains a non-zero B<dry_run> value, B<save> will only run syntax | ||
474 | check, without actually saving the file. If B<$cfg-E<gt>lint(enable =E<gt> 0)> | ||
475 | was called previously, this is a no-op. | ||
367 | 476 | ||
368 | Saves the parse tree in the configuration file. | 477 | Other keys in I<%hash> are the same as in B<write>, described below. |
369 | 478 | ||
370 | =head2 write | 479 | =head2 write |
371 | 480 | ||
diff --git a/t/lint.t b/t/lint.t new file mode 100644 index 0000000..aa92fd1 --- a/dev/null +++ b/t/lint.t | |||
@@ -0,0 +1,33 @@ | |||
1 | # -*- perl -*- | ||
2 | use lib qw(t lib); | ||
3 | use strict; | ||
4 | use warnings; | ||
5 | use Test::More; | ||
6 | |||
7 | BEGIN { | ||
8 | plan tests => 7; | ||
9 | use_ok('Test::HAProxy'); | ||
10 | } | ||
11 | |||
12 | my $hp = new Test::HAProxy; | ||
13 | isa_ok($hp,'Test::HAProxy'); | ||
14 | |||
15 | ok($hp->lint, 'haproxy -c -f'); | ||
16 | |||
17 | $hp->lint(0); | ||
18 | ok(!$hp->lint); | ||
19 | |||
20 | $hp->lint(1); | ||
21 | ok($hp->lint, 'haproxy -c -f'); | ||
22 | |||
23 | $hp->lint(enable => 0); | ||
24 | ok(!$hp->lint); | ||
25 | |||
26 | $hp->lint(enable => 1, command => '/usr/local/bin/haproxy -c -f'); | ||
27 | ok($hp->lint, '/usr/local/bin/haproxy -c -f'); | ||
28 | |||
29 | __DATA__ | ||
30 | global | ||
31 | log /dev/log daemon | ||
32 | user haproxy | ||
33 | group haproxy | ||