per-agent configuration of batch processors, #71837
[freeside.git] / torrus / perllib / Torrus / ConfigTree / Writer.pm
1 #  Copyright (C) 2002-2007  Stanislav Sinyagin
2 #
3 #  This program is free software; you can redistribute it and/or modify
4 #  it under the terms of the GNU General Public License as published by
5 #  the Free Software Foundation; either version 2 of the License, or
6 #  (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software
15 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
16
17 # $Id: Writer.pm,v 1.1 2010-12-27 00:03:45 ivan Exp $
18 # Stanislav Sinyagin <ssinyagin@yahoo.com>
19
20 #
21 # Write access for ConfigTree
22 #
23
24 package Torrus::ConfigTree::Writer;
25
26 use Torrus::ConfigTree;
27 our @ISA=qw(Torrus::ConfigTree);
28
29 use Torrus::Log;
30 use Torrus::TimeStamp;
31 use Torrus::SiteConfig;
32 use Torrus::ServiceID;
33     
34 use strict;
35 use Digest::MD5 qw(md5); # needed as hash function
36
37
38 our %multigraph_remove_space =
39     ('ds-expr-' => 1,
40      'graph-legend-' => 0);
41
42
43 # instance of Torrus::ServiceID object, if needed
44 my $srvIdParams;
45
46 # tree names where we initialized service IDs
47 my %srvIdInitialized;
48
49
50 sub new
51 {
52     my $proto = shift;
53     my %options = @_;
54     my $class = ref($proto) || $proto;
55     $options{'-WriteAccess'} = 1;
56     my $self  = $class->SUPER::new( %options );
57     if( not defined( $self ) )
58     {
59         return undef;
60     }
61     
62     bless $self, $class;
63
64     $self->{'viewparent'} = {};
65     $self->{'mayRunCollector'} =
66         Torrus::SiteConfig::mayRunCollector( $self->treeName() );
67
68     $self->{'collectorInstances'} =
69         Torrus::SiteConfig::collectorInstances( $self->treeName() );
70
71     $self->{'db_collectortokens'} = [];
72     foreach my $instance ( 0 .. ($self->{'collectorInstances'} - 1) )
73     {
74         $self->{'db_collectortokens'}->[$instance] =
75             new Torrus::DB( 'collector_tokens' . '_' .
76                             $instance . '_' . $self->{'ds_config_instance'},
77                             -Subdir => $self->treeName(),
78                             -WriteAccess => 1,
79                             -Truncate    => 1 );
80     }
81
82     # delay writing of frequently changed values
83     $self->{'db_dsconfig'}->delay();
84     $self->{'db_otherconfig'}->delay();    
85     return $self;
86 }
87
88
89 sub newToken
90 {
91     my $self = shift;
92     my $token = $self->{'next_free_token'};
93     $token = 1 unless defined( $token );
94     $self->{'next_free_token'} = $token + 1;
95     return sprintf('T%.4d', $token);
96 }
97
98
99 sub setParam
100 {
101     my $self  = shift;
102     my $name  = shift;
103     my $param = shift;
104     my $value = shift;
105
106     if( $self->getParamProperty( $param, 'remspace' ) )
107     {
108         $value =~ s/\s+//go;
109     }
110
111     $self->{'paramcache'}{$name}{$param} = $value;
112     $self->{'db_otherconfig'}->put( 'P:'.$name.':'.$param, $value );
113     $self->{'db_otherconfig'}->addToList('Pl:'.$name, $param);
114 }
115
116 sub setNodeParam
117 {
118     my $self  = shift;
119     my $name  = shift;
120     my $param = shift;
121     my $value = shift;
122
123     if( $self->getParamProperty( $param, 'remspace' ) )
124     {
125         $value =~ s/\s+//go;
126     }
127
128     $self->{'paramcache'}{$name}{$param} = $value;
129     $self->{'db_dsconfig'}->put( 'P:'.$name.':'.$param, $value );
130     $self->{'db_dsconfig'}->addToList('Pl:'.$name, $param);
131 }
132
133
134 sub setParamProperty
135 {
136     my $self = shift;
137     my $param = shift;
138     my $prop = shift;
139     my $value = shift;
140
141     $self->{'paramprop'}{$prop}{$param} = $value;
142     $self->{'db_paramprops'}->put( $param . ':' . $prop, $value );
143 }
144
145
146 sub initRoot
147 {
148     my $self  = shift;
149     if( not defined( $self->token('/') ) )
150     {
151         my $token = $self->newToken();
152         $self->{'db_dsconfig'}->put( 'pt:/', $token );
153         $self->{'db_dsconfig'}->put( 'tp:'.$token, '/' );
154         $self->{'db_dsconfig'}->put( 'n:'.$token, 0 );
155         $self->{'nodetype_cache'}{$token} = 0;
156     }
157 }
158
159 sub addChild
160 {
161     my $self = shift;
162     my $token = shift;
163     my $childname = shift;
164     my $isAlias = shift;
165
166     if( not $self->isSubtree( $token ) )
167     {
168         Error('Cannot add a child to a non-subtree node: ' .
169               $self->path($token));
170         return undef;
171     }
172
173     my $path = $self->path($token) . $childname;
174
175     # If the child already exists, do nothing
176
177     my $ctoken = $self->token($path);
178     if( not defined($ctoken) )
179     {
180         $ctoken = $self->newToken();
181
182         $self->{'db_dsconfig'}->put( 'pt:'.$path, $ctoken );
183         $self->{'db_dsconfig'}->put( 'tp:'.$ctoken, $path );
184
185         $self->{'db_dsconfig'}->addToList( 'c:'.$token, $ctoken );
186         $self->{'db_dsconfig'}->put( 'p:'.$ctoken, $token );
187         $self->{'parentcache'}{$ctoken} = $token;
188
189         my $nodeType;
190         if( $isAlias )
191         {
192             $nodeType = 2; # alias
193         }
194         elsif( $childname =~ /\/$/o )
195         {
196             $nodeType = 0; # subtree
197         }
198         else
199         {
200             $nodeType = 1; # leaf
201         }
202         $self->{'db_dsconfig'}->put( 'n:'.$ctoken, $nodeType );
203         $self->{'nodetype_cache'}{$ctoken} = $nodeType;
204     }
205     return $ctoken;
206 }
207
208 sub setAlias
209 {
210     my $self = shift;
211     my $token = shift;
212     my $apath = shift;
213
214     my $ok = 1;
215
216     my $iamLeaf = $self->isLeaf($token);
217
218     # TODO: Add more verification here
219     if( not defined($apath) or $apath !~ /^\//o or
220         ( not $iamLeaf and $apath !~ /\/$/o ) or
221         ( $iamLeaf and $apath =~ /\/$/o ) )
222     {
223         my $path = $self->path($token);
224         Error("Incorrect alias at $path: $apath"); $ok = 0;
225     }
226     elsif( $self->token( $apath ) )
227     {
228         my $path = $self->path($token);
229         Error("Alias already exists: $apath at $path"); $ok = 0;
230     }
231     else
232     {
233         # Go through the alias and create subtrees if neccessary
234
235         my @pathelements = $self->splitPath($apath);
236         my $aliasChildName = pop @pathelements;
237
238         my $nodepath = '';
239         my $parent_token = $self->token('/');
240
241         foreach my $nodename ( @pathelements )
242         {
243             $nodepath .= $nodename;
244             my $child_token = $self->token( $nodepath );
245             if( not defined( $child_token ) )
246             {
247                 $child_token = $self->addChild( $parent_token, $nodename );
248                 if( not defined( $child_token ) )
249                 {
250                     return 0;
251                 }
252             }
253             $parent_token = $child_token;
254         }
255
256         my $alias_token = $self->addChild( $parent_token, $aliasChildName, 1 );
257         if( not defined( $alias_token ) )
258         {
259             return 0;
260         }
261
262         $self->{'db_dsconfig'}->put( 'a:'.$alias_token, $token );
263         $self->{'db_dsconfig'}->addToList( 'ar:'.$token, $alias_token );
264         $self->{'db_aliases'}->put( $apath, $token );
265     }
266     return $ok;
267 }
268
269 sub addView
270 {
271     my $self = shift;
272     my $vname = shift;
273     my $parent = shift;
274     $self->{'db_otherconfig'}->addToList('V:', $vname);
275     if( defined( $parent ) )
276     {
277         $self->{'viewparent'}{$vname} = $parent;
278     }
279 }
280
281
282 sub addMonitor
283 {
284     my $self = shift;
285     my $mname = shift;
286     $self->{'db_otherconfig'}->addToList('M:', $mname);
287 }
288
289
290 sub addAction
291 {
292     my $self = shift;
293     my $aname = shift;
294     $self->{'db_otherconfig'}->addToList('A:', $aname);
295 }
296
297
298 sub addDefinition
299 {
300     my $self = shift;
301     my $name = shift;
302     my $value = shift;
303     $self->{'db_dsconfig'}->put( 'd:'.$name, $value );
304     $self->{'db_dsconfig'}->addToList('D:', $name);
305 }
306
307
308 sub setVar
309 {
310     my $self = shift;
311     my $token = shift;
312     my $name = shift;
313     my $value = shift;
314     
315     $self->{'setvar'}{$token}{$name} = $value;
316 }
317
318
319 sub isTrueVar
320 {
321     my $self = shift;
322     my $token = shift;
323     my $name = shift;
324
325     my $ret = 0;
326
327     while( defined( $token ) and
328            not defined( $self->{'setvar'}{$token}{$name} ) )
329     {
330         $token = $self->getParent( $token );
331     }
332
333     if( defined( $token ) )
334     {
335         my $value = $self->{'setvar'}{$token}{$name};
336         if( defined( $value ) )
337         {
338             if( $value eq 'true' or
339                 $value =~ /^\d+$/o and $value )
340             {
341                 $ret = 1;
342             }
343         }
344     }
345     
346     return $ret;
347 }
348
349 sub finalize
350 {
351     my $self = shift;
352     my $status = shift;
353
354     if( $status )
355     {
356         # write delayed data
357         $self->{'db_dsconfig'}->commit();
358         $self->{'db_otherconfig'}->commit();    
359         
360         Verbose('Configuration has compiled successfully. Switching over to ' .
361              'DS config instance ' . $self->{'ds_config_instance'} .
362              ' and Other config instance ' .
363              $self->{'other_config_instance'} );
364
365         $self->setReady(1);
366         if( not $self->{'-NoDSRebuild'} )
367         {
368             $self->{'db_config_instances'}->
369                 put( 'ds:' . $self->treeName(),
370                      $self->{'ds_config_instance'} );
371         }
372
373         $self->{'db_config_instances'}->
374             put( 'other:' . $self->treeName(),
375                  $self->{'other_config_instance'} );
376
377         Torrus::TimeStamp::init();
378         Torrus::TimeStamp::setNow($self->treeName() . ':configuration');
379         Torrus::TimeStamp::release();
380     }
381 }
382
383
384 sub postProcess
385 {
386     my $self = shift;
387
388     my $ok = $self->postProcessNodes();
389
390     # Propagate view inherited parameters
391     $self->{'viewParamsProcessed'} = {};
392     foreach my $vname ( $self->getViewNames() )
393     {
394         &Torrus::DB::checkInterrupted();
395         
396         $self->propagateViewParams( $vname );
397     }
398     return $ok;
399 }
400
401
402
403 sub postProcessNodes
404 {
405     my $self = shift;
406     my $token = shift;
407
408     &Torrus::DB::checkInterrupted();
409
410     my $ok = 1;
411
412     if( not defined( $token ) )
413     {
414         $token = $self->token('/');
415     }
416
417     my $nodeid = $self->getNodeParam( $token, 'nodeid', 1 );
418     if( defined( $nodeid ) )
419     {
420         # verify the uniqueness of nodeid
421         
422         my $oldToken = $self->{'db_nodeid'}->get($nodeid);
423         if( defined($oldToken) )
424         {
425             Error('Non-unique nodeid ' . $nodeid .
426                   ' in ' . $self->path($token) .
427                   ' and ' . $self->path($oldToken));
428             $ok = 0;
429         }
430         else
431         {
432             $self->{'db_nodeid'}->put($nodeid, $token);
433         }
434     }
435
436     
437     if( $self->isLeaf($token) )
438     {
439         # Process static tokenset members
440
441         my $tsets = $self->getNodeParam( $token, 'tokenset-member' );
442         if( defined( $tsets ) )
443         {
444             foreach my $tset ( split(/,/o, $tsets) )
445             {
446                 my $tsetName = 'S'.$tset;
447                 if( not $self->tsetExists( $tsetName ) )
448                 {
449                     my $path = $self->path( $token );
450                     Error("Referenced undefined token set $tset in $path");
451                     $ok = 0;
452                 }
453                 else
454                 {
455                     $self->tsetAddMember( $tsetName, $token, 'static' );
456                 }
457             }
458         }
459
460         my $dsType = $self->getNodeParam( $token, 'ds-type' );
461         if( defined( $dsType ) )
462         {
463             if( $dsType eq 'rrd-multigraph' )
464             {
465                 # Expand parameter substitutions in multigraph leaves
466                 
467                 my @dsNames =
468                     split(/,/o, $self->getNodeParam($token, 'ds-names') );
469                 
470                 foreach my $dname ( @dsNames )
471                 {
472                     foreach my $param ( 'ds-expr-', 'graph-legend-' )
473                     {
474                         my $dsParam = $param . $dname;
475                         my $value = $self->getNodeParam( $token, $dsParam );
476                         if( defined( $value ) )
477                         {
478                             my $newValue = $value;
479                             if( $multigraph_remove_space{$param} )
480                             {
481                                 $newValue =~ s/\s+//go;
482                             }
483                             $newValue =
484                                 $self->expandSubstitutions( $token, $dsParam,
485                                                             $newValue );
486                             if( $newValue ne $value )
487                             {
488                                 $self->setNodeParam( $token, $dsParam,
489                                                      $newValue );
490                             }
491                         }
492                     }
493                 }
494             }
495             elsif( $dsType eq 'collector' and $self->{'mayRunCollector'} )
496             {
497                 # Split the collecting job between collector instances
498                 my $instance = 0;
499                 my $nInstances = $self->{'collectorInstances'};
500
501                 my $oldOffset =
502                     $self->getNodeParam($token, 'collector-timeoffset');
503                 my $newOffset = $oldOffset;
504                 
505                 my $period =
506                     $self->getNodeParam($token, 'collector-period');
507                 
508                 if( $nInstances > 1 )
509                 {
510                     my $hashString =
511                         $self->getNodeParam($token,
512                                             'collector-instance-hashstring');
513                     if( not defined( $hashString ) )
514                     {
515                         Error('collector-instance-hashstring is not defined ' .
516                               'in ' . $self->path( $token ));
517                         $hashString = '';
518                     }
519                     
520                     $instance =
521                         unpack( 'N', md5( $hashString ) ) % $nInstances;
522                 }          
523
524                 $self->setNodeParam( $token,
525                                      'collector-instance',
526                                      $instance );
527                 
528                 my $dispersed =
529                     $self->getNodeParam($token,
530                                         'collector-dispersed-timeoffset');
531                 if( defined( $dispersed ) and $dispersed eq 'yes' )
532                 {
533                     # Process dispersed collector offsets
534                     
535                     my %p;
536                     foreach my $param ( 'collector-timeoffset-min',
537                                         'collector-timeoffset-max',
538                                         'collector-timeoffset-step',
539                                         'collector-timeoffset-hashstring' )
540                     {
541                         my $val = $self->getNodeParam( $token, $param );
542                         if( not defined( $val ) )
543                         {
544                             Error('Mandatory parameter ' . $param . ' is not '.
545                                   ' defined in ' . $self->path( $token ));
546                             $ok = 0;
547                         }
548                         else
549                         {
550                             $p{$param} = $val;
551                         }
552                     }
553
554                     if( $ok )
555                     {
556                         my $min = $p{'collector-timeoffset-min'};
557                         my $max = $p{'collector-timeoffset-max'};
558                         if( $max < $min )
559                         {
560                             Error('collector-timeoffset-max is less than ' .
561                                   'collector-timeoffset-min in ' .
562                                   $self->path( $token ));
563                             $ok = 0;
564                         }
565                         else
566                         {
567                             my $step = $p{'collector-timeoffset-step'};
568                             my $hashString =
569                                 $p{'collector-timeoffset-hashstring'};
570                             
571                             my $bucketSize = int( ($max - $min) / $step );
572                             $newOffset =
573                                 $min
574                                 +
575                                 $step * ( unpack( 'N', md5( $hashString ) ) %
576                                           $bucketSize )
577                                 +
578                                 $instance * int( $step / $nInstances );
579                         }
580                     }
581                 }
582                 else
583                 {
584                     $newOffset += $instance * int( $period / $nInstances ); 
585                 } 
586
587                 $newOffset %= $period;
588                 
589                 if( $newOffset != $oldOffset )
590                 {
591                     $self->setNodeParam( $token,
592                                          'collector-timeoffset',
593                                          $newOffset );
594                 }
595
596                 $self->{'db_collectortokens'}->[$instance]->put
597                     ( $token, sprintf('%d:%d', $period, $newOffset) );
598
599                 my $storagetypes =
600                     $self->getNodeParam( $token, 'storage-type' );
601                 foreach my $stype ( split(/,/o, $storagetypes) )
602                 {
603                     if( $stype eq 'ext' )
604                     {
605                         if( not defined( $srvIdParams ) )
606                         {
607                             $srvIdParams =
608                                 new Torrus::ServiceID( -WriteAccess => 1 );
609                         }
610
611                         my $srvTrees =
612                             $self->getNodeParam($token, 'ext-service-trees');
613
614                         if( not defined( $srvTrees ) or
615                             length( $srvTrees ) == 0 )
616                         {
617                             $srvTrees = $self->treeName();
618                         }
619                                                 
620                         my $serviceid =
621                             $self->getNodeParam($token, 'ext-service-id');
622
623                         foreach my $srvTree (split(/\s*,\s*/o, $srvTrees))
624                         {
625                             if( not Torrus::SiteConfig::treeExists($srvTree) )
626                             {
627                                 Error
628                                     ('Error processing ext-service-trees' .
629                                      'for ' . $self->path( $token ) .
630                                      ': tree ' . $srvTree .
631                                      ' does not exist');
632                                 $ok = 0;
633                             }
634                             else
635                             {
636                                 if( not $srvIdInitialized{$srvTree} )
637                                 {
638                                     $srvIdParams->cleanAllForTree
639                                         ( $srvTree );
640                                     $srvIdInitialized{$srvTree} = 1;
641                                 }
642                                 else
643                                 {
644                                     if( $srvIdParams->idExists( $serviceid,
645                                                                 $srvTree ) )
646                                     {
647                                         Error('Duplicate ServiceID: ' .
648                                               $serviceid . ' in tree ' .
649                                               $srvTree);
650                                         $ok = 0;
651                                     }
652                                 }
653                             }
654                         }
655
656                         if( $ok )
657                         {
658                             # sorry for ackward Emacs auto-indent
659                             my $params = {
660                                 'trees' => $srvTrees,
661                                 'token' => $token,
662                                 'dstype' =>
663                                     $self->getNodeParam($token,
664                                                         'ext-dstype'),
665                                     'units' =>
666                                     $self->getNodeParam
667                                     ($token, 'ext-service-units')
668                                 };
669                             
670                             $srvIdParams->add( $serviceid, $params );
671                         }
672                     }
673                 }
674             }
675         }
676         else
677         {
678             my $path = $self->path( $token );
679             Error("Mandatory parameter 'ds-type' is not defined for $path");
680             $ok = 0;
681         }            
682     }
683     else
684     {
685         foreach my $ctoken ( $self->getChildren( $token ) )
686         {
687             if( not $self->isAlias( $ctoken ) )
688             {
689                 $ok = $self->postProcessNodes( $ctoken ) ? $ok:0;
690             }
691         }
692     }
693     return $ok;
694 }
695
696
697 sub propagateViewParams
698 {
699     my $self = shift;
700     my $vname = shift;
701
702     # Avoid processing the same view twice
703     if( $self->{'viewParamsProcessed'}{$vname} )
704     {
705         return;
706     }
707
708     # First we do the same for parent
709     my $parent = $self->{'viewparent'}{$vname};
710     if( defined( $parent ) )
711     {
712         $self->propagateViewParams( $parent );
713
714         my $parentParams = $self->getParams( $parent );
715         foreach my $param ( keys %{$parentParams} )
716         {
717             if( not defined( $self->getParam( $vname, $param ) ) )
718             {
719                 $self->setParam( $vname, $param, $parentParams->{$param} );
720             }
721         }
722     }
723
724     # mark this view as processed
725     $self->{'viewParamsProcessed'}{$vname} = 1;
726 }
727
728
729 sub validate
730 {
731     my $self = shift;
732
733     my $ok = 1;
734
735     $self->{'is_writing'} = undef;
736
737     if( not $self->{'-NoDSRebuild'} )
738     {
739         $ok = Torrus::ConfigTree::Validator::validateNodes($self);
740     }
741     $ok = Torrus::ConfigTree::Validator::validateViews($self) ? $ok:0;
742     $ok = Torrus::ConfigTree::Validator::validateMonitors($self) ? $ok:0;
743     $ok = Torrus::ConfigTree::Validator::validateTokensets($self) ? $ok:0;
744
745     return $ok;
746 }
747
748
749 1;
750
751 # Local Variables:
752 # mode: perl
753 # indent-tabs-mode: nil
754 # perl-indent-level: 4
755 # End: