# Copyright (C) 2002-2007 Stanislav Sinyagin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. # $Id: ConfigTree.pm,v 1.1 2010-12-27 00:03:41 ivan Exp $ # Stanislav Sinyagin package Torrus::ConfigTree; use Torrus::DB; use Torrus::Log; use Torrus::TimeStamp; use strict; sub new { my $self = {}; my $class = shift; my %options = @_; bless $self, $class; $self->{'treename'} = $options{'-TreeName'}; die('ERROR: TreeName is mandatory') if not $self->{'treename'}; $self->{'db_config_instances'} = new Torrus::DB( 'config_instances', -WriteAccess => 1 ); defined( $self->{'db_config_instances'} ) or return( undef ); my $i = $self->{'db_config_instances'}->get('ds:' . $self->{'treename'}); if( not defined($i) ) { $i = 0; $self->{'first_time_created'} = 1; } my $dsConfInstance = sprintf( '%d', $i ); $i = $self->{'db_config_instances'}->get('other:' . $self->{'treename'}); $i = 0 unless defined( $i ); my $otherConfInstance = sprintf( '%d', $i ); if( $options{'-WriteAccess'} ) { $self->{'is_writing'} = 1; # Acquire exlusive lock on the database and set the compiling flag { my $ok = 1; my $key = 'compiling:' . $self->{'treename'}; my $cursor = $self->{'db_config_instances'}->cursor( -Write => 1 ); my $compilingFlag = $self->{'db_config_instances'}->c_get( $cursor, $key ); if( $compilingFlag ) { if( $options{'-ForceWriter'} ) { Warn('Another compiler process is probably still ' . 'running. This may lead to an unusable ' . 'database state'); } else { Error('Another compiler is running for the tree ' . $self->{'treename'}); $ok = 0; } } else { $self->{'db_config_instances'}->c_put( $cursor, $key, 1 ); } undef $cursor; if( not $ok ) { return undef; } $self->{'iam_writer'} = 1; } if( not $options{'-NoDSRebuild'} ) { $dsConfInstance = sprintf( '%d', ( $dsConfInstance + 1 ) % 2 ); } $otherConfInstance = sprintf( '%d', ( $otherConfInstance + 1 ) % 2 ); } $self->{'ds_config_instance'} = $dsConfInstance; $self->{'other_config_instance'} = $otherConfInstance; $self->{'db_readers'} = new Torrus::DB('config_readers', -Subdir => $self->{'treename'}, -WriteAccess => 1 ); defined( $self->{'db_readers'} ) or return( undef ); $self->{'db_dsconfig'} = new Torrus::DB('ds_config_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => $options{'-WriteAccess'}); defined( $self->{'db_dsconfig'} ) or return( undef ); $self->{'db_otherconfig'} = new Torrus::DB('other_config_' . $otherConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => $options{'-WriteAccess'}); defined( $self->{'db_otherconfig'} ) or return( undef ); $self->{'db_aliases'} = new Torrus::DB('aliases_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => $options{'-WriteAccess'}); defined( $self->{'db_aliases'} ) or return( undef ); if( $options{'-WriteAccess'} ) { $self->setReady(0); $self->waitReaders(); if( $options{'-Rebuild'} ) { $self->{'db_otherconfig'}->trunc(); if( not $options{'-NoDSRebuild'} ) { $self->{'db_dsconfig'}->trunc(); $self->{'db_aliases'}->trunc(); } } } else { $self->setReader(); if( not $self->isReady() ) { if( $options{'-Wait'} ) { Warn('Configuration is not ready'); my $waitingTimeout = time() + $Torrus::Global::ConfigReadyTimeout; my $success = 0; while( not $success and time() < $waitingTimeout ) { $self->clearReader(); Info('Sleeping ' . $Torrus::Global::ConfigReadyRetryPeriod . ' seconds'); sleep $Torrus::Global::ConfigReadyRetryPeriod; $self->setReader(); if( $self->isReady() ) { $success = 1; Info('Now configuration is ready'); } else { Info('Configuration is still not ready'); } } if( not $success ) { Error('Configuration wait timed out'); $self->clearReader(); return undef; } } else { Error('Configuration is not ready'); $self->clearReader(); return undef; } } } # Read the parameter properties into memory $self->{'db_paramprops'} = new Torrus::DB('paramprops_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => $options{'-WriteAccess'}); defined( $self->{'db_paramprops'} ) or return( undef ); if( $options{'-Rebuild'} ) { $self->{'db_paramprops'}->trunc(); } else { my $cursor = $self->{'db_paramprops'}->cursor(); while( my ($key, $val) = $self->{'db_paramprops'}->next( $cursor ) ) { my( $param, $prop ) = split( /:/o, $key ); $self->{'paramprop'}{$prop}{$param} = $val; } undef $cursor; $self->{'db_paramprops'}->closeNow(); delete $self->{'db_paramprops'}; } $self->{'db_sets'} = new Torrus::DB('tokensets_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 0, -WriteAccess => 1, -Truncate => $options{'-Rebuild'}); defined( $self->{'db_sets'} ) or return( undef ); $self->{'db_nodepcache'} = new Torrus::DB('nodepcache_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => 1, -Truncate => ($options{'-Rebuild'} and not $options{'-NoDSRebuild'})); defined( $self->{'db_nodepcache'} ) or return( undef ); $self->{'db_nodeid'} = new Torrus::DB('nodeid_' . $dsConfInstance, -Subdir => $self->{'treename'}, -Btree => 1, -WriteAccess => 1, -Truncate => ($options{'-Rebuild'} and not $options{'-NoDSRebuild'})); defined( $self->{'db_nodeid'} ) or return( undef ); return $self; } sub DESTROY { my $self = shift; Debug('Destroying ConfigTree object'); if( $self->{'iam_writer'} ) { # Acquire exlusive lock on the database and clear the compiling flag my $cursor = $self->{'db_config_instances'}->cursor( -Write => 1 ); $self->{'db_config_instances'}->c_put ( $cursor, 'compiling:' . $self->{'treename'}, 0 ); undef $cursor; } else { $self->clearReader(); } undef $self->{'db_dsconfig'}; undef $self->{'db_otherconfig'}; undef $self->{'db_aliases'}; undef $self->{'db_sets'}; undef $self->{'db_nodepcache'}; undef $self->{'db_readers'}; } # Manage the readinness flag sub setReady { my $self = shift; my $ready = shift; $self->{'db_otherconfig'}->put( 'ConfigurationReady', $ready ? 1:0 ); } sub isReady { my $self = shift; return $self->{'db_otherconfig'}->get( 'ConfigurationReady' ); } # Manage the readers database sub setReader { my $self = shift; my $readerId = 'pid=' . $$ . ',rand=' . sprintf('%.10d', rand(1e9)); Debug('Setting up reader: ' . $readerId); $self->{'reader_id'} = $readerId; $self->{'db_readers'}->put( $readerId, sprintf('%d:%d:%d', time(), $self->{'ds_config_instance'}, $self->{'other_config_instance'}) ); } sub clearReader { my $self = shift; if( defined( $self->{'reader_id'} ) ) { Debug('Clearing reader: ' . $self->{'reader_id'}); $self->{'db_readers'}->del( $self->{'reader_id'} ); delete $self->{'reader_id'}; } } sub waitReaders { my $self = shift; # Let the active readers finish their job my $noReaders = 0; while( not $noReaders ) { my @readers = (); my $cursor = $self->{'db_readers'}->cursor(); while( my ($key, $val) = $self->{'db_readers'}->next( $cursor ) ) { my( $timestamp, $dsInst, $otherInst ) = split( /:/o, $val ); if( $dsInst == $self->{'ds_config_instance'} or $otherInst == $self->{'other_config_instance'} ) { push( @readers, { 'reader' => $key, 'timestamp' => $timestamp } ); } } undef $cursor; if( @readers > 0 ) { Info('Waiting for ' . scalar(@readers) . ' readers:'); my $recentTS = 0; foreach my $reader ( @readers ) { Info($reader->{'reader'} . ', timestamp: ' . localtime( $reader->{'timestamp'} )); if( $reader->{'timestamp'} > $recentTS ) { $recentTS = $reader->{'timestamp'}; } } if( $recentTS + $Torrus::Global::ConfigReadersWaitTimeout >= time() ) { Info('Sleeping ' . $Torrus::Global::ConfigReadersWaitPeriod . ' seconds'); sleep( $Torrus::Global::ConfigReadersWaitPeriod ); } else { # the readers are too long active. we ignore them now Warn('Readers wait timed out. Flushing the readers list for ' . 'DS config instance ' . $self->{'ds_config_instance'} . ' and Other config instance ' . $self->{'other_config_instance'}); my $cursor = $self->{'db_readers'}->cursor( -Write => 1 ); while( my ($key, $val) = $self->{'db_readers'}->next( $cursor ) ) { my( $timestamp, $dsInst, $otherInst ) = split( /:/o, $val ); if( $dsInst == $self->{'ds_config_instance'} or $otherInst == $self->{'other_config_instance'} ) { $self->{'db_readers'}->c_del( $cursor ); } } undef $cursor; $noReaders = 1; } } else { $noReaders = 1; } } } # This should be called after Torrus::TimeStamp::init(); sub getTimestamp { my $self = shift; return Torrus::TimeStamp::get($self->{'treename'} . ':configuration'); } sub treeName { my $self = shift; return $self->{'treename'}; } # Returns array with path components sub splitPath { my $self = shift; my $path = shift; my @ret = (); while( length($path) > 0 ) { my $node; $path =~ s/^([^\/]*\/?)//o; $node = $1; push(@ret, $node); } return @ret; } sub nodeName { my $self = shift; my $path = shift; $path =~ s/.*\/([^\/]+)\/?$/$1/o; return $path; } sub token { my $self = shift; my $path = shift; my $token = $self->{'db_dsconfig'}->get( 'pt:'.$path ); if( not defined( $token ) ) { my $prefixLen = 1; # the leading slash is anyway there my $pathLen = length( $path ); while( not defined( $token ) and $prefixLen < $pathLen ) { my $result = $self->{'db_aliases'}->getBestMatch( $path ); if( not defined( $result ) ) { $prefixLen = $pathLen; # exit the loop } else { # Found a partial match $prefixLen = length( $result->{'key'} ); my $aliasTarget = $self->path( $result->{'value'} ); $path = $aliasTarget . substr( $path, $prefixLen ); $token = $self->{'db_dsconfig'}->get( 'pt:'.$path ); } } } return $token; } sub path { my $self = shift; my $token = shift; return $self->{'db_dsconfig'}->get( 'tp:'.$token ); } sub nodeExists { my $self = shift; my $path = shift; return defined( $self->{'db_dsconfig'}->get( 'pt:'.$path ) ); } sub nodeType { my $self = shift; my $token = shift; my $type = $self->{'nodetype_cache'}{$token}; if( not defined( $type ) ) { $type = $self->{'db_dsconfig'}->get( 'n:'.$token ); $self->{'nodetype_cache'}{$token} = $type; } return $type; } sub isLeaf { my $self = shift; my $token = shift; return ( $self->nodeType($token) == 1 ); } sub isSubtree { my $self = shift; my $token = shift; return( $self->nodeType($token) == 0 ); } # Returns the real token or undef sub isAlias { my $self = shift; my $token = shift; return( ( $self->nodeType($token) == 2 ) ? $self->{'db_dsconfig'}->get( 'a:'.$token ) : undef ); } # Returns the list of tokens pointing to this one as an alias sub getAliases { my $self = shift; my $token = shift; return $self->{'db_dsconfig'}->getListItems('ar:'.$token); } sub getParam { my $self = shift; my $name = shift; my $param = shift; my $fromDS = shift; if( exists( $self->{'paramcache'}{$name}{$param} ) ) { return $self->{'paramcache'}{$name}{$param}; } else { my $db = $fromDS ? $self->{'db_dsconfig'} : $self->{'db_otherconfig'}; my $val = $db->get( 'P:'.$name.':'.$param ); $self->{'paramcache'}{$name}{$param} = $val; return $val; } } sub retrieveNodeParam { my $self = shift; my $token = shift; my $param = shift; # walk up the tree and save the grandparent's value at parent's cache my $value; my $currtoken = $token; my @ancestors; my $walked = 0; while( not defined($value) and defined($currtoken) ) { $value = $self->getParam( $currtoken, $param, 1 ); if( not defined $value ) { if( $walked ) { push( @ancestors, $currtoken ); } else { $walked = 1; } # walk up to the parent $currtoken = $self->getParent($currtoken); } } foreach my $ancestor ( @ancestors ) { $self->{'paramcache'}{$ancestor}{$param} = $value; } return $self->expandNodeParam( $token, $param, $value ); } sub expandNodeParam { my $self = shift; my $token = shift; my $param = shift; my $value = shift; # %parameter_substitutions% in ds-path-* in multigraph leaves # are expanded by the Writer post-processing if( defined $value and $self->getParamProperty( $param, 'expand' ) ) { $value = $self->expandSubstitutions( $token, $param, $value ); } return $value; } sub expandSubstitutions { my $self = shift; my $token = shift; my $param = shift; my $value = shift; my $ok = 1; my $changed = 1; while( $changed and $ok ) { $changed = 0; # Substitute definitions if( index($value, '$') >= 0 ) { if( not $value =~ /\$(\w+)/o ) { my $path = $self->path($token); Error("Incorrect definition reference: $value in $path"); $ok = 0; } else { my $dname = $1; my $dvalue = $self->getDefinition($dname); if( not defined( $dvalue ) ) { my $path = $self->path($token); Error("Cannot find definition $dname in $path"); $ok = 0; } else { $value =~ s/\$$dname/$dvalue/g; $changed = 1; } } } # Substitute parameter references if( index($value, '%') >= 0 and $ok ) { if( not $value =~ /\%([a-zA-Z0-9\-_]+)\%/o ) { Error("Incorrect parameter reference: $value"); $ok = 0; } else { my $pname = $1; my $pval = $self->getNodeParam( $token, $pname ); if( not defined( $pval ) ) { my $path = $self->path($token); Error("Cannot expand parameter reference %". $pname."% in ".$path); $ok = 0; } else { $value =~ s/\%$pname\%/$pval/g; $changed = 1; } } } } if( ref( $Torrus::ConfigTree::nodeParamHook ) ) { $value = &{$Torrus::ConfigTree::nodeParamHook}( $self, $token, $param, $value ); } return $value; } sub getNodeParam { my $self = shift; my $token = shift; my $param = shift; my $noclimb = shift; my $value; if( $noclimb ) { $value = $self->getParam( $token, $param, 1 ); return $self->expandNodeParam( $token, $param, $value ); } if( $self->{'is_writing'} ) { return $self->retrieveNodeParam( $token, $param ); } my $cachekey = $token.':'.$param; my $cacheval = $self->{'db_nodepcache'}->get( $cachekey ); if( defined( $cacheval ) ) { my $status = substr( $cacheval, 0, 1 ); if( $status eq 'U' ) { return undef; } else { return substr( $cacheval, 1 ); } } $value = $self->retrieveNodeParam( $token, $param ); if( defined( $value ) ) { $self->{'db_nodepcache'}->put( $cachekey, 'D'.$value ); } else { $self->{'db_nodepcache'}->put( $cachekey, 'U' ); } return $value; } sub getParamNames { my $self = shift; my $name = shift; my $fromDS = shift; my $db = $fromDS ? $self->{'db_dsconfig'} : $self->{'db_otherconfig'}; return $db->getListItems('Pl:'.$name); } sub getParams { my $self = shift; my $name = shift; my $fromDS = shift; my $ret = {}; foreach my $param ( $self->getParamNames( $name, $fromDS ) ) { $ret->{$param} = $self->getParam( $name, $param, $fromDS ); } return $ret; } sub getParent { my $self = shift; my $token = shift; if( exists( $self->{'parentcache'}{$token} ) ) { return $self->{'parentcache'}{$token}; } else { my $parent = $self->{'db_dsconfig'}->get( 'p:'.$token ); $self->{'parentcache'}{$token} = $parent; return $parent; } } sub getChildren { my $self = shift; my $token = shift; if( (my $alias = $self->isAlias($token)) ) { return $self->getChildren($alias); } else { return $self->{'db_dsconfig'}->getListItems( 'c:'.$token ); } } sub getParamProperty { my $self = shift; my $param = shift; my $prop = shift; return $self->{'paramprop'}{$prop}{$param}; } sub getParamProperties { my $self = shift; return $self->{'paramprop'}; } # Recognize the regexp patterns within a path, # like /Netflow/Exporters/.*/.*/bps. # Each pattern is applied against direct child names only. # sub getNodesByPattern { my $self = shift; my $pattern = shift; if( $pattern !~ /^\//o ) { Error("Incorrect pattern: $pattern"); return undef; } my @retlist = (); foreach my $nodepattern ( $self->splitPath($pattern) ) { my @next_retlist = (); # Cut the trailing slash, if any my $patternname = $nodepattern; $patternname =~ s/\/$//o; if( $patternname =~ /\W/o ) { foreach my $candidate ( @retlist ) { # This is a pattern, let's get all matching children foreach my $child ( $self->getChildren( $candidate ) ) { # Cut the trailing slash and leading path my $childname = $self->path($child); $childname =~ s/\/$//o; $childname =~ s/.*\/([^\/]+)$/$1/o; if( $childname =~ $patternname ) { push( @next_retlist, $child ); } } } } elsif( length($patternname) == 0 ) { @next_retlist = ( $self->token('/') ); } else { foreach my $candidate ( @retlist ) { my $proposal = $self->path($candidate).$nodepattern; if( defined( my $proptoken = $self->token($proposal) ) ) { push( @next_retlist, $proptoken ); } } } @retlist = @next_retlist; } return @retlist; } # # Recognizes absolute or relative path, '..' as the parent subtree # sub getRelative { my $self = shift; my $token = shift; my $relPath = shift; if( $relPath =~ /^\//o ) { return $self->token( $relPath ); } else { if( length( $relPath ) > 0 ) { $token = $self->getParent( $token ); } while( length( $relPath ) > 0 ) { if( $relPath =~ /^\.\.\//o ) { $relPath =~ s/^\.\.\///o; if( $token ne $self->token('/') ) { $token = $self->getParent( $token ); } } else { my $childName; $relPath =~ s/^([^\/]*\/?)//o; $childName = $1; my $path = $self->path( $token ); $token = $self->token( $path . $childName ); if( not defined $token ) { return undef; } } } return $token; } } sub getNodeByNodeid { my $self = shift; my $nodeid = shift; return $self->{'db_nodeid'}->get( $nodeid ); } # Returns arrayref or undef. # Each element is an arrayref to [nodeid, token] pair sub searchNodeidPrefix { my $self = shift; my $prefix = shift; return $self->{'db_nodeid'}->searchPrefix( $prefix ); } # Returns arrayref or undef. # Each element is an arrayref to [nodeid, token] pair sub searchNodeidSubstring { my $self = shift; my $substring = shift; return $self->{'db_nodeid'}->searchSubstring( $substring ); } sub getDefaultView { my $self = shift; my $token = shift; my $view; if( $self->isTset($token) ) { if( $token eq 'SS' ) { $view = $self->getParam('SS', 'default-tsetlist-view'); } else { $view = $self->getParam($token, 'default-tset-view'); if( not defined( $view ) ) { $view = $self->getParam('SS', 'default-tset-view'); } } } elsif( $self->isSubtree($token) ) { $view = $self->getNodeParam($token, 'default-subtree-view'); } else { # This must be leaf $view = $self->getNodeParam($token, 'default-leaf-view'); } if( not defined( $view ) ) { Error("Cannot find default view for $token"); } return $view; } sub getInstanceParam { my $self = shift; my $type = shift; my $name = shift; my $param = shift; if( $type eq 'node' ) { return $self->getNodeParam($name, $param); } else { return $self->getParam($name, $param); } } sub getViewNames { my $self = shift; return $self->{'db_otherconfig'}->getListItems( 'V:' ); } sub viewExists { my $self = shift; my $vname = shift; return $self->searchOtherList('V:', $vname); } sub getMonitorNames { my $self = shift; return $self->{'db_otherconfig'}->getListItems( 'M:' ); } sub monitorExists { my $self = shift; my $mname = shift; return $self->searchOtherList('M:', $mname); } sub getActionNames { my $self = shift; return $self->{'db_otherconfig'}->getListItems( 'A:' ); } sub actionExists { my $self = shift; my $mname = shift; return $self->searchOtherList('A:', $mname); } # Search for a value in comma-separated list sub searchOtherList { my $self = shift; my $key = shift; my $name = shift; return $self->{'db_otherconfig'}->searchList($key, $name); } # Token sets manipulation sub isTset { my $self = shift; my $token = shift; return substr($token, 0, 1) eq 'S'; } sub addTset { my $self = shift; my $tset = shift; $self->{'db_sets'}->addToList('S:', $tset); } sub tsetExists { my $self = shift; my $tset = shift; return $self->{'db_sets'}->searchList('S:', $tset); } sub getTsets { my $self = shift; return $self->{'db_sets'}->getListItems('S:'); } sub tsetMembers { my $self = shift; my $tset = shift; return $self->{'db_sets'}->getListItems('s:'.$tset); } sub tsetMemberOrigin { my $self = shift; my $tset = shift; my $token = shift; return $self->{'db_sets'}->get('o:'.$tset.':'.$token); } sub tsetAddMember { my $self = shift; my $tset = shift; my $token = shift; my $origin = shift; $self->{'db_sets'}->addToList('s:'.$tset, $token); $self->{'db_sets'}->put('o:'.$tset.':'.$token, $origin); } sub tsetDelMember { my $self = shift; my $tset = shift; my $token = shift; $self->{'db_sets'}->delFromList('s:'.$tset, $token); $self->{'db_sets'}->del('o:'.$tset.':'.$token); } # Definitions manipulation sub getDefinition { my $self = shift; my $name = shift; return $self->{'db_dsconfig'}->get( 'd:'.$name ); } sub getDefinitionNames { my $self = shift; return $self->{'db_dsconfig'}->getListItems( 'D:' ); } 1; # Local Variables: # mode: perl # indent-tabs-mode: nil # perl-indent-level: 4 # End: